生成器

用法

一個 常用的 yield 應用場景是使用它生成斐波那契數(shù)列,代碼如下:

def fib():
    a, b = 0, 1
    while True:
        yield b
    a, b = b, a + b

當我們調用 fib 時并不是直接獲得函數(shù)的返回值,而是獲得一個生成器對象:

def fib():
    a, b = 0, 1
    while True:
        yield b
    a, b = b, a + b
print(fib())
# <generator object fib at 0x030C30B0>

一種簡單的使用生產器對象方式為循環(huán):

def fib(n):
    a, b = 0, 1
    while True:
        yield b
        if b >= n:
            return
        a, b = b, a + b
for x in fib(5):
    print(x)
# 1, 1, 2, 3, 5

這里我把迭代器最大的值限制在5,避免程序無休止運行下去。此外,還可以采用 next 方法來獲取生成器的值:

g = fib(5)
print(next(g))  # 1
print(next(g))  # 1
print(next(g))  # 2
print(next(g))  # 3
print(next(g))  # 5
print(next(g))  # StopIteration

可以看出第 6 次調用拋出了 StopIteration。如果一個函數(shù)已近包含 yield, 那么這個函數(shù)在執(zhí)行 return 語句時即會拋出 StopIteration 異常,且返回值在異常對象的 value 屬性中。例如:

def gen():
    yield 5
    return "end"
g = gen()
next(g)
try:
    next(g)
except StopIteration as e:
    print(e.value) # end

當然,以下這種情況依然會拋出 StopIteration,只是最終的返回值為 None 而已:

def gen():
    yield 5
g = gen()
next(g)
try:
    next(g)
except StopIteration as e:
    print(e.value) # None

為什么需要 yield

在了解 yield 的基本用法后,還需要知道為什么需要使用 yield。如果沒有 yield,我們當然可以使用 list 來保存數(shù)列的值:

def fiblist(n):
    res = []
    a, b = 0, 1
    while b <= n:
        res.append(b)
        a, b = b, a + b
    return res
print(fiblist(5)) # [1, 1, 2, 3, 5]

可以看出,使用 list 同樣可以計算數(shù)列。但是這時存在一個問題,如果我需要生成大量的數(shù)列這時就存在兩個問題:

  1. 在生成數(shù)列的同時我們需要進行一次循環(huán),而從 list 里面取出數(shù)列時我們同樣還要循環(huán)一次。增加了循環(huán)的成本。
  2. 所有生成的數(shù)都保存在內存中,如果后續(xù)依舊采用這種方式會急速的使得內存浪費。

其實者涉及到代碼設計中的一種使用資源的方式: 延遲加載。即資源并不在它聲明的時候馬上讀取到內存中,而是當真正需要使用的時候菜才進行加載。

而當采用 yield 生產數(shù)列的時候則是符合這一觀點的,即如果你不使用循環(huán)或者 next 去讀取生成器,則它永遠只是一個生成器對象,并不會發(fā)生真正的計算。

為什么yield能行

許多解析 yield 的文章都止步于前兩個內容,而并未分析為什么 yield 能行。yield 最關鍵的思想在于當我們 "調用" 一次含有 yield 的函數(shù)之后,函數(shù)的上下文依然存在。即 yield 的真正作用為在函數(shù)在切換到其他上下文,依然會保存自己的上下文。例如對于 fib 函數(shù),當我們調用一次 next 后,a, b 的值未變?yōu)槌跏贾?,?是 yield 之前的值。這有一點類似于 c 語言中的靜態(tài)函數(shù)。

yield 還能做什么

利用上面提到的特點,yield 還可以完成三個基礎操作:

  1. 保存上下文,進行迭代計算
  2. 保存上下文,緩存資源
  3. 保存上下文,使得異步更加優(yōu)雅

其中計算斐波那契數(shù)列則是上面的第一點應用。至于第二點應用,還需要提到學習一下生成器的其他用法。

send

生成器除了使用 next 函數(shù)取值以外,還可以使用 send 函數(shù)向生成器傳值并獲得一個返回值,例如:

def gen():
    b = yield "start"
    while True:
        c = yield b
        if c == None:
            return "end"
        b = c

g = gen()
a = g.send(None)
print(a)  # start
print(g.send(1))  # 1
print(g.send(2))  # 2
print(g.send(3))  # 3
print(g.send(None))  # StopIteration value = end

python 規(guī)定,第一次向生成器發(fā)送的值必需為 None,起作用為啟動生成器。上面函數(shù)的運行流程為:

  1. 調用 a = g.send(None)。gen 運行到 yield "start",gen 返回 "start"。
  2. 調用 a.send(1)。gen 函數(shù)繼續(xù)運行,此使獲取到 send 發(fā)送的值為 1,則 b = 1。gen函數(shù)繼續(xù)運行到 yield b, 此使gen 函數(shù)返回 b,即1。
  3. 調用a.send(2)。gen 函數(shù)繼續(xù)運行,此時獲取到 c 的值為 2,繼續(xù)運行,b = c = 2,遇到下一個 yield ,返回2。

其實 a = yield b 這條語句可以看作兩個部分: 第一步為 yield 即函數(shù)先返回 b。此時函數(shù)停止運行,等待send。當下一個 send 調用產生后,函數(shù)繼續(xù)運行并將接收的值傳遞給 a。

有了這個知識后??紤]這樣一個需求: 接收一個正則表達式,并判定某個字符串是否被該表達式匹配。一般來說可以這樣寫:

class Re:
    def __init__(self, reg_expr):
        self._reg_expr = re.compile(reg_expr)

    def test(self, string):
        return bool(self._reg_expr.match(string))
r = Re("hello")
print(r.test("hello world"))  # True
print(r.test("python"))  # False

但是有了 yield 之后,我們則可以這樣寫:

def re_test(re_expr):
    cached_re = re.compile(re_expr)
    sentence = yield None
    while True:
        sentence = yield bool(cached_re.match(sentence))
test = re_test("hello")
test.send(None)
print(test.send("hello world"))  # True
print(test.send("python"))  # False

雖然這相比使用類實現(xiàn)的方式并沒有節(jié)省代碼,但可以更清楚的理解 yield 的保存上下文的功能。

此外,第三個作用將在下一篇文章中介紹。

yield from

具有 yield 的函數(shù)雖然被當成生成器對象對待。但在開發(fā)中依然希望具有 yield 的函數(shù)能像普通函數(shù)一樣工作。 對于一個普通函數(shù),進行函數(shù)嵌套是最基礎的操作,非常簡單也非常好理解。例如在函數(shù) a 中調用 函數(shù) b:

def a():
    b()

但對于一個生成器對象來說,并不如此簡單。例如一個生成器對象為 a:

def a():
    for i in range(10):
        yield i

另一個生成器 b 想調用生成器 a,并在結束后做點其他事情。這時我們并不能:

def b():
    a()
    yield "ok"

因為 a 是一個生成器對象,因此我們需要:

def b():
    for i in a():
        yield i
    yield "ok"

這就與普通的函數(shù)調用存在差異了。這時即可以使用 yield from,上面的代碼等價于:

def b():
    yield from a()
    yield "ok"

是不是更加優(yōu)雅了?此外,如果 a 生成器還需要接收數(shù)據(jù),例如:

def a():
    b = yield None
    while True:
        print(b)
        b = yield
        if b == None:
            return "end"

那如果不使用 yield from, b 函數(shù)將變得異常復雜:

def b():
    g = a()
    g.send(None)
    c = yield None
    while True:
        try:
            g.send(c)
        except StopIteration as e:
            print(e)
        c = yield
x = b()
x.send(None)
x.send(1)  # 1
x.send(2)  # 2
x.send(None)  # end

實際上,如果使用 yield from,我們只需要:

def b():
    yield from a()
x = b()
x.send(None)
x.send(1)  # 1
x.send(2)  # 2

可以看出,yield from 使得生成器的嵌套更加的優(yōu)雅。此外,yield from 有更多的優(yōu)點,這里不再贅述。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 本節(jié)課綱 可迭代對象 迭代器 生成器Python中內置的序列,如list、tuple、str、bytes、dict...
    郭_揚閱讀 1,385評論 0 0
  • Generator 函數(shù)的語法 簡介 基本概念 Generator 函數(shù)是 ES6 提供的一種異步編程解決方案,語...
    站在大神的肩膀上看世界閱讀 4,315評論 0 6
  • PEP原文 : https://www.python.org/dev/peps/pep-0342/ PEP標題: ...
    豌豆花下貓閱讀 516評論 0 0
  • 生成器 通過列表生成式,我們可以直接創(chuàng)建一個列表。但是受內存限制,列表容量肯定是有限的。 而且,創(chuàng)建一個很多很多元...
    小學弟_閱讀 426評論 0 0
  • 在兒童時期,由于心血管系統(tǒng)和呼吸系統(tǒng)尚未發(fā)育完善。較宜采用有氧耐力訓練,刺激相關系統(tǒng)更好的發(fā)育,但負荷不宜過大。
    蔡蔡_678a閱讀 200評論 0 0

友情鏈接更多精彩內容