用法
一個 常用的 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ù)列這時就存在兩個問題:
- 在生成數(shù)列的同時我們需要進行一次循環(huán),而從 list 里面取出數(shù)列時我們同樣還要循環(huán)一次。增加了循環(huán)的成本。
- 所有生成的數(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 還可以完成三個基礎操作:
- 保存上下文,進行迭代計算
- 保存上下文,緩存資源
- 保存上下文,使得異步更加優(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ù)的運行流程為:
- 調用 a = g.send(None)。gen 運行到 yield "start",gen 返回 "start"。
- 調用 a.send(1)。gen 函數(shù)繼續(xù)運行,此使獲取到 send 發(fā)送的值為 1,則 b = 1。gen函數(shù)繼續(xù)運行到 yield b, 此使gen 函數(shù)返回 b,即1。
- 調用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)點,這里不再贅述。