一文讓你搞懂 Python 的生成器,以及我和一個(gè)奇葩之間的恩怨情仇
楔子
本次來(lái)聊一聊 Python 的生成器,它是我們后續(xù)理解協(xié)程的基礎(chǔ)(對(duì)不起,沒(méi)有后續(xù)了)。生成器的話,估計(jì)大部分人在寫程序的時(shí)候都不怎么用,但其實(shí)生成器一旦用好了,確實(shí)能給程序帶來(lái)性能上的提升,那么下面就來(lái)看一看吧。
生成器的基礎(chǔ)知識(shí)
我們知道,如果函數(shù)的內(nèi)部出現(xiàn)了 yield 關(guān)鍵字,那么它就不再是普通的函數(shù)了,而是一個(gè)生成器函數(shù),調(diào)用之后會(huì)返回一個(gè)生成器對(duì)象。
生成器對(duì)象一般用于處理循環(huán)結(jié)構(gòu),應(yīng)用得當(dāng)?shù)脑捒梢詷O大優(yōu)化內(nèi)存使用率。比如:我們讀取一個(gè)大文件。
def read_file(file):
return open(file, encoding="utf-8").readlines()
print(read_file("假裝是大文件.txt"))
"""
['人生是什么?\n', '大概是閃閃發(fā)光的同時(shí)\n', '又讓人感到痛苦的東西吧']
"""
這個(gè)版本的函數(shù),直接將里面的內(nèi)容全部讀取出來(lái)了,返回了一個(gè)列表。如果文件非常大,那么內(nèi)存的開(kāi)銷可想而知。于是我們可以通過(guò) yield 關(guān)鍵字,將普通函數(shù)變成一個(gè)生成器函數(shù)。
from typing import Iterator, Generator
def read_file(file):
with open(file, encoding="utf-8") as f:
for line in f:
yield line
data = read_file("假裝是大文件.txt")
# 返回一個(gè)生成器對(duì)象
print(data)
"""
<generator object read_file at 0x0000019B4FA8BAC0>
"""
# 使用 for 循環(huán)遍歷
for line in data:
# 文件每一行自帶換行符, 所以這里的 print 就不用換行符了
print(line, end="")
"""
人生是什么?
大概是閃閃發(fā)光的同時(shí)
又讓人感到痛苦的東西吧
"""
由于生成器是一種特殊的迭代器,所以也可以使用它的 __next__ 方法。
def gen():
yield 123
yield 456
yield 789
return "result"
# 調(diào)用生成器函數(shù)時(shí),會(huì)創(chuàng)建一個(gè)生成器
# 生成器雖然創(chuàng)建了,但是里面的代碼并沒(méi)有執(zhí)行
g = gen()
# 調(diào)用 __next__ 方法時(shí)才會(huì)執(zhí)行
# 當(dāng)遇到 yield,會(huì)將生成器暫停、并返回 yield 后面的值
print(g.__next__()) # 123
# 此時(shí)生成器處于暫停狀態(tài),如果我們不驅(qū)動(dòng)它的話,它是不會(huì)前進(jìn)的
# 再次執(zhí)行 __next__,生成器恢復(fù)執(zhí)行,并在下一個(gè) yield 處暫停
print(g.__next__()) # 456
# 生成器會(huì)記住自己的執(zhí)行進(jìn)度,它總是在遇到 yield 時(shí)暫停
# 調(diào)用 __next__ 時(shí)恢復(fù)執(zhí)行,直到遇見(jiàn)下一個(gè) yield
print(g.__next__()) # 789
# 顯然再調(diào)用 __next__ 時(shí),已經(jīng)找不到下一個(gè) yield 了
# 那么生成器會(huì)拋出 StopIteration,并將返回值設(shè)置在里面
try:
g.__next__()
except StopIteration as e:
print(f"返回值:{e.value}") # 返回值:result
可以看到,基于生成器,我們能夠?qū)崿F(xiàn)惰性求值。
當(dāng)然啦,生成器不僅僅有 __next__ 方法,它還有 send 和 throw 方法,我們先來(lái)說(shuō)一說(shuō) send。
def gen():
res1 = yield "yield 1"
print(f"***** {res1} *****")
res2 = yield "yield 2"
return res2
g = gen()
# 此時(shí)程序在第一個(gè) yield 處暫停
print(g.__next__())
"""
yield 1
"""
# 調(diào)用 g.send(val) 依舊可以驅(qū)動(dòng)生成器執(zhí)行
# 同時(shí)還可以傳遞一個(gè)值,交給第一個(gè) yield 左邊的 res1
# 然后尋找第二個(gè) yield
print(g.send("嘿嘿"))
"""
***** 嘿嘿 *****
yield 2
"""
# 上面輸出了兩行,第一行是生成器里面的 print 打印的
try:
# 此時(shí)生成器在第二個(gè) yield 處暫停,調(diào)用 g.send 驅(qū)動(dòng)執(zhí)行
# 同時(shí)傳遞一個(gè)值交給第二個(gè) yield 左邊的 res2,然后尋找第三個(gè) yield
# 但是生成器里面沒(méi)有第三個(gè) yield 了,于是拋出 StopIteration
g.send("蛤蛤")
except StopIteration as e:
print(f"返回值:{e.value}")
"""
返回值:蛤蛤
"""
生成器永遠(yuǎn)在 yield 處暫停,并將 yield 后面的值返回。如果想驅(qū)動(dòng)生成器繼續(xù)執(zhí)行,可以調(diào)用 __next__ 或 send,會(huì)去尋找下一個(gè) yield,然后在下一個(gè) yield 處暫停。依次往復(fù),直到找不到 yield 時(shí),拋出 StopIteration,并將返回值包在里面。
但是這兩者的不同之處在于,send 可以接收參數(shù),假設(shè)生成器在 res = yield 123 這里停下來(lái)了。
當(dāng)調(diào)用 __next__ 和 send 的時(shí)候,都可以驅(qū)動(dòng)執(zhí)行,但調(diào)用 send 時(shí)可以傳遞一個(gè) value,并將 value 賦值給變量 res。而 __next__ 沒(méi)有這個(gè)功能,如果是調(diào)用 __next__ 的話,那么 res 得到的就是一個(gè) None。
所以 res = yield 123 這一行語(yǔ)句需要兩次驅(qū)動(dòng)生成器才能完成,第一次驅(qū)動(dòng)會(huì)讓生成器執(zhí)行到 yield 123,然后暫停執(zhí)行,將 123 返回。第二次驅(qū)動(dòng)才會(huì)給變量 res 賦值,此時(shí)會(huì)尋找下一個(gè) yield 然后暫停。
生成器的預(yù)激
剛創(chuàng)建生成器的時(shí)候,里面的代碼還沒(méi)有執(zhí)行,它的 f_lasti 是 -1。關(guān)于什么是 f_lasti,需要解釋一下。
首先隨著 CPython 版本的升級(jí),一些數(shù)據(jù)結(jié)構(gòu)的底層實(shí)現(xiàn)也在發(fā)生改變,比如棧幀等等。在之前的版本中,棧幀有一個(gè)字段叫 f_lasti,它表示最近一條執(zhí)行完畢的字節(jié)碼指令的偏移量。而在 3.12 里面,這個(gè)字段已經(jīng)沒(méi)了。
雖然解釋器內(nèi)部結(jié)構(gòu)會(huì)發(fā)生變化,但暴露出來(lái)的 Python 接口是不變的,所以我們依舊可以訪問(wèn)該字段。
def gen():
res1 = yield 123
res2 = yield 456
return "result"
g = gen()
# 生成器函數(shù)和普通函數(shù)一樣,執(zhí)行時(shí)也會(huì)創(chuàng)建棧幀
# 通過(guò) g.gi_frame 可以很方便的獲取
print(g.gi_frame.f_lasti) # -1
f_lasti 是 -1,表示生成器剛被創(chuàng)建,還沒(méi)有執(zhí)行任何指令。而第一次驅(qū)動(dòng)生成器執(zhí)行,叫做生成器的預(yù)激。但在生成器還沒(méi)有被預(yù)激時(shí),我們調(diào)用 send,里面只能傳遞一個(gè) None,否則報(bào)錯(cuò)。
def gen():
res1 = yield 123
res2 = yield 456
return "result"
g = gen()
try:
g.send("小云同學(xué)")
except TypeError as e:
print(e)
"""
can't send non-None value to a just-started generator
"""
對(duì)于尚未被預(yù)激的生成器,我們只能傳遞一個(gè) None,也就是 g.send(None)?;蛘哒{(diào)用 g.__next__(),因?yàn)椴还芎螘r(shí)它傳遞的都是 None。
其實(shí)也很好理解,我們之所以傳值是為了賦給 yield 左邊的變量,這就意味著生成器必須至少被驅(qū)動(dòng)一次、在某個(gè) yield 處停下來(lái)才可以。而未被預(yù)激的生成器,它里面的代碼壓根就沒(méi)有執(zhí)行,所以第一次驅(qū)動(dòng)的時(shí)候只能傳遞一個(gè) None 進(jìn)去。
如果查看生成器的源代碼的話,也能證明這一點(diǎn):
圖片
在之前的版本中,判斷條件是 f_lasti 是否等于 -1,而在 3.12 中引入了 gi_frame_state 字段,表示生成器的狀態(tài)。如果生成器剛創(chuàng)建,并且接收的參數(shù) arg 不為 None,那么報(bào)錯(cuò)。
那么生成器的狀態(tài)都有哪些呢?
// Include/internal/pycore_frame.h
typedef enum _framestate {
FRAME_CREATED = -2,
FRAME_SUSPENDED = -1,
FRAME_EXECUTING = 0,
FRAME_COMPLETED = 1,
FRAME_CLEARED = 4
} PyFrameState;
狀態(tài)總共有五種。
- FRAME_CREATED:生成器剛創(chuàng)建。
- FRAME_SUSPENDED:生成器被掛起,也就是執(zhí)行到某個(gè) yield 之后返回了。
- FRAME_EXECUTING:生成器執(zhí)行中。
- FRAME_COMPLETED:生成器執(zhí)行完畢,但棧幀對(duì)象還未被清理。
- FRAME_CLEARED:生成器的棧幀對(duì)象被清理。
相關(guān)源碼細(xì)節(jié)下一篇文章(對(duì)不起,沒(méi)有下一篇了)會(huì)分析。
生成器的 throw 方法
除了 __next__ 和 send 方法之外,生成器還有一個(gè) throw 方法,該方法的作用和前兩者類似,也是驅(qū)動(dòng)生成器執(zhí)行,并在下一個(gè) yield 處暫停。但它在調(diào)用的時(shí)候,需要傳遞一個(gè)異常進(jìn)去。
def gen():
try:
yield 123
except ValueError as e:
print(f"異常:{e}")
yield 456
return "result"
g = gen()
# 生成器在 yield 123 處暫停
g.__next__()
# 向生成器傳遞一個(gè)異常
# 如果當(dāng)前生成器的暫停位置處無(wú)法捕獲傳遞的異常,那么會(huì)將異常拋出來(lái)
# 如果能夠捕獲,那么會(huì)驅(qū)動(dòng)生成器執(zhí)行,并在下一個(gè) yield 處暫停
# 當(dāng)前生成器位于 yield 123 處,而它所在的位置能夠捕獲異常
# 所以不會(huì)報(bào)錯(cuò),結(jié)果就是 456 會(huì)賦值給 val
val = g.throw(ValueError("一個(gè) ValueError"))
"""
異常:一個(gè) ValueError
"""
print(val)
"""
456
"""
關(guān)于生成器的 __next__、send、throw 三個(gè)方法的用法我們就說(shuō)完了,還是比較簡(jiǎn)單的。
關(guān)閉生成器
生成器也是可以關(guān)閉的,我們來(lái)看一下。
def gen():
yield 123
yield 456
return "result"
g = gen()
# 生成器在 yield 123 處停止
print(g.__next__()) # 123
# 關(guān)閉生成器
g.close()
# 生成器一旦關(guān)閉,就代表執(zhí)行完畢了,它的棧幀會(huì)被重置為 None
print(g.gi_frame) # None
try:
# 再次調(diào)用 __next__,會(huì)拋出 StopIteration
g.__next__()
except StopIteration as e:
# 此時(shí) e.value 為 None
print(e.value) # None
無(wú)論是顯式地關(guān)閉生成器,還是正常情況下生成器執(zhí)行完畢,內(nèi)部的棧幀都會(huì)被重置為 None。而驅(qū)動(dòng)一個(gè)已經(jīng)執(zhí)行結(jié)束的生成器,會(huì)拋出 StopIteration 異常,并且異常的 value 屬性為 None。
GeneratorExit 異常
這里再來(lái)說(shuō)一說(shuō) GeneratorExit 這個(gè)異常,如果我們關(guān)閉一個(gè)生成器(或者生成器被刪除時(shí)),那么會(huì)往里面扔一個(gè) GeneratorExit 進(jìn)去。
def gen():
try:
yield 123
except GeneratorExit as e:
print("生成器被刪除了")
g = gen()
# 生成器在 yield 123 處暫停
g.__next__()
# 關(guān)閉生成器,會(huì)往里面扔一個(gè) GeneratorExit
g.close()
"""
生成器被刪除了
"""
這里我們捕獲了傳遞的 GeneratorExit,所以 print 語(yǔ)句執(zhí)行了,但如果沒(méi)有捕獲呢?
def gen():
yield 123
g = gen()
g.__next__()
g.close()
此時(shí)無(wú)事發(fā)生,但是注意:如果是手動(dòng)調(diào)用 throw 方法扔一個(gè) GeneratorExit 進(jìn)去,異常還是會(huì)拋出來(lái)的。
那么問(wèn)題來(lái)了,生成器為什么要提供這樣一種機(jī)制呢?直接刪就完了,干嘛還要往生成器內(nèi)部丟一個(gè)異常呢?答案是為了資源的清理和釋放。
在 Python 還未提供原生協(xié)程,以及 asyncio 還尚未流行起來(lái)的時(shí)候,很多開(kāi)源的協(xié)程框架都是基于生成器實(shí)現(xiàn)的協(xié)程。而創(chuàng)建連接的邏輯,一般都會(huì)寫在 yield 后面。
def _create_connection():
# 一些邏輯
yield conn
# 一些邏輯
但是這些連接在不用的時(shí)候,要不要進(jìn)行釋放呢?答案是肯定的,所以便可以這么做。
def _create_connection():
# 一些邏輯
try:
yield conn
except GeneratorExit:
conn.close()
# 一些邏輯
這樣當(dāng)我們關(guān)閉或刪除生成器的時(shí)候,就能夠自動(dòng)對(duì)連接進(jìn)行釋放了。
不過(guò)還有一個(gè)需要注意的點(diǎn),就是在捕獲 GeneratorExit 之后,不可以再執(zhí)行 yield,否則會(huì)拋出 RuntimeError。
def gen():
try:
yield 123
except GeneratorExit:
print("生成器被刪除")
yield
g = gen()
g.__next__()
g.close()
"""
生成器被刪除
Traceback (most recent call last):
File "...", line 10, in <module>
g.close()
RuntimeError: generator ignored GeneratorExit
"""
調(diào)用 close 方法時(shí),如果沒(méi)有成功捕獲 GeneratorExit,那么生成器會(huì)直接關(guān)閉,不會(huì)有任何事情發(fā)生。但如果捕獲了 GeneratorExit,那么可以在對(duì)應(yīng)的語(yǔ)句塊里做一些資源清理邏輯,但不應(yīng)該再出現(xiàn) yield。
而上面的例子中出現(xiàn)了 yield,所以解釋器會(huì)拋出 RuntimeError,因?yàn)闆](méi)捕獲 GeneratorExit 還好,解釋器不會(huì)有什么抱怨。但如果捕獲了 GeneratorExit,說(shuō)明我們知道生成器是被關(guān)閉了,既然知道,那里面還出現(xiàn) yield 的意義何在呢?
當(dāng)然啦,如果出現(xiàn)了 yield,但沒(méi)有執(zhí)行到,則不會(huì)拋 RuntimeError。
def gen():
try:
yield 123
except GeneratorExit:
print("生成器被刪除")
return
yield
g = gen()
g.__next__()
g.close()
print("------------")
"""
生成器被刪除
------------
"""
遇見(jiàn) yield 之前就返回了,所以此時(shí)不會(huì)出現(xiàn) RuntimeError。
注意:GeneratorExit 繼承自 BaseException,它無(wú)法被 Exception 捕獲。
yield from 的用法
當(dāng)函數(shù)內(nèi)部出現(xiàn)了 yield 關(guān)鍵字,那么它就是一個(gè)生成器函數(shù),對(duì)于 yield from 而言亦是如此。那么問(wèn)題來(lái)了,這兩者之間有什么區(qū)別呢?
from typing import Generator
def gen1():
yield [1, 2, 3]
def gen2():
yield from [1, 2, 3]
g1 = gen1()
g2 = gen2()
# 兩者都是生成器
print(isinstance(g1, Generator)) # True
print(isinstance(g2, Generator)) # True
print(g1.__next__()) # [1, 2, 3]
print(g2.__next__()) # 1
結(jié)論很清晰,yield 對(duì)后面的值沒(méi)有要求,會(huì)直接將其返回。而 yield from 后面必須跟一個(gè)可迭代對(duì)象(否則報(bào)錯(cuò)),然后每次返回可迭代對(duì)象的一個(gè)值。
def gen():
yield from [1, 2, 3]
return "result"
g = gen()
print(g.__next__()) # 1
print(g.__next__()) # 2
print(g.__next__()) # 3
try:
g.__next__()
except StopIteration as e:
print(e.value) # result
除了要求必須跟一個(gè)可迭代對(duì)象,然后每次只返回一個(gè)值之外,其它表現(xiàn)和 yield 是類似的。而對(duì)于當(dāng)前這個(gè)例子來(lái)說(shuō),yield from [1, 2, 3] 等價(jià)于 for item in [1, 2, 3]: yield item。
所以有人覺(jué)得 yield from 貌似沒(méi)啥用啊,它完全可以用 for 循環(huán)加 yield 進(jìn)行代替。很明顯不是這樣的,yield from 背后做了非常多的事情,我們稍后說(shuō)。
這里先出一道思考題:
圖片
這時(shí)候便可以通過(guò) yield 和 yield from 來(lái)實(shí)現(xiàn)這一點(diǎn)。
def flatten(data):
for item in data:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
data = [1, [[[[[3, 3], 5]]], [[[[[[[[[[[[6]]]]], 8]]], "aaa"]]]], 250]]
print(list(flatten(data))) # [1, 3, 3, 5, 6, 8, 'aaa', 250]
怎么樣,是不是很簡(jiǎn)單呢?
委托生成器
如果單從語(yǔ)法上來(lái)看的話,會(huì)發(fā)現(xiàn) yield from 貌似沒(méi)什么特殊的地方,但其實(shí) yield from 還可以作為委托生成器。委托生成器會(huì)在調(diào)用方和子生成器之間建立一個(gè)雙向通道,什么意思呢?我們舉例說(shuō)明。
def gen():
yield 123
yield 456
return "result"
def middle():
res = yield from gen()
print(f"接收到子生成器的返回值: {res}")
# middle 里面出現(xiàn)了 yield from gen()
# 此時(shí) middle() 便是委托生成器,gen() 是子生成器
g = middle()
# 而 yield from 會(huì)在調(diào)用方和子生成器之間建立一個(gè)雙向通道
# 兩者是可以互通的,調(diào)用 g.send、g.throw 都會(huì)直接傳遞給子生成器
print(g.__next__()) # 123
print(g.__next__()) # 456
# 問(wèn)題來(lái)了,如果再調(diào)用一次 __next__ 會(huì)有什么后果呢?
# 按照之前的理解,應(yīng)該會(huì)拋出 StopIteration
print(g.__next__())
"""
接收到子生成器的返回值: result
Traceback (most recent call last):
File "...", line 21, in <module>
print(g.__next__())
StopIteration
"""
在第三次調(diào)用 __next__ 的時(shí)候,確實(shí)拋了異常,但是委托生成器收到了子生成器的返回值。也就是說(shuō),委托生成器在調(diào)用方和子生成器之間建立了雙向通道,兩者是直接通信的,并且當(dāng)子生成器出現(xiàn) StopIteration 時(shí),委托生成器還要負(fù)責(zé)兜底。
委托生成器會(huì)將子生成器拋出的 StopIteration 里面的 value 取出來(lái),然后賦值給左側(cè)的變量 res,并在自己內(nèi)部繼續(xù)尋找 yield。
換句話說(shuō),當(dāng)子生成器 return 之后,委托生成器會(huì)拿到返回值,并將子生成器拋出的異常給捕獲掉。但是還沒(méi)完,因?yàn)檫€要找到下一個(gè) yield,那么從哪里找呢?顯然是從委托生成器的內(nèi)部尋找,于是接下來(lái)就變成了調(diào)用方和委托生成器之間的通信。
如果在委托生成器內(nèi)部能找到下一個(gè) yield,那么會(huì)將值返回給調(diào)用方。如果找不到,那么就重新構(gòu)造一個(gè) StopIteration,將異常拋出去。此時(shí)異常的 value 屬性,就是委托生成器的返回值。
def gen():
yield 123
return "result"
def middle():
res = yield from gen()
return f"委托生成器返回了子生成器的返回值:{res}"
g = middle()
print(g.__next__()) # 123
try:
g.__next__()
except StopIteration as e:
print(e.value) # 委托生成器返回了子生成器的返回值:result
大部分情況下,我們并不關(guān)注委托生成器的返回值,我們更關(guān)注的是子生成器。于是可以換種寫法:
def gen():
yield 123
yield 456
yield 789
return "result"
def middle():
yield (yield from gen())
g = middle()
for v in g:
print(v)
"""
123
456
789
result
"""
所以委托生成器負(fù)責(zé)在調(diào)用方和子生成器之間建立一個(gè)雙向通道,通道一旦建立,調(diào)用方可以和子生成器直接通信。雖然調(diào)用的是委托生成器的 __next__、send、throw 等方法,但影響的都是子生成器。
并且委托生成器還可以對(duì)子生成器拋出的 StopIteration 異常進(jìn)行兜底,會(huì)捕獲掉該異常,然后拿到返回值,這樣就無(wú)需手動(dòng)捕獲子生成器的異常了。但問(wèn)題是委托生成器還要找到下一個(gè) yield,并將值返回給調(diào)用方,此時(shí)這個(gè)重?fù)?dān)就落在了它自己頭上。
如果找不到,還是要將異常拋出來(lái)的,只不過(guò)拋出的 StopIteration 是委托生成器構(gòu)建的。而子生成器拋出的 StopIteration,早就被委托生成器捕獲掉了。于是我們可以考慮在 yield from 的前面再加上一個(gè) yield,這樣就不會(huì)拋異常了。
為什么要有委托生成器
我們上面已經(jīng)了解了委托生成器的用法,不過(guò)問(wèn)題來(lái)了,這玩意為啥會(huì)存在呢?上面的邏輯,即便不使用 yield from 也可以完成啊。
其實(shí)是因?yàn)槲覀兩厦娴氖纠a比較簡(jiǎn)單(為了演示用法),當(dāng)需求比較復(fù)雜時(shí),將生成器內(nèi)部的部分操作委托給另一個(gè)生成器是有必要的,這也是委托生成器的由來(lái)。
而委托生成器不僅要能保證調(diào)用方和子生成器之間直接通信,還要能夠以一種優(yōu)雅的方式獲取子生成器的返回值,于是新的語(yǔ)法 yield from 就誕生了。
但其實(shí) yield from 背后為我們做得事情還不止這么簡(jiǎn)單,它不單單是建立雙向通道、獲取子生成器的返回值,它還會(huì)處理子生成器內(nèi)部出現(xiàn)的異常,詳細(xì)內(nèi)容可以查看 PEP380。
https://peps.python.org/pep-0380/
這里我們直接給出結(jié)論,并通過(guò)代碼演示一下。
1)子生成器 yield 后面的值,會(huì)直接返回給調(diào)用方;調(diào)用方 send 發(fā)送的值,也會(huì)直接傳給子生成器。
def gen():
res = yield 123
yield [res]
return "result"
def middle():
yield (yield from gen())
g = middle()
# 子生成器 yield 后面的值,會(huì)直接返回給調(diào)用方
print(g.__next__()) # 123
# 調(diào)用方 send 發(fā)送的值,也會(huì)直接傳給子生成器
print(g.send("小云同學(xué)")) # ['小云同學(xué)']
另外還要補(bǔ)充一個(gè)細(xì)節(jié),如果 yield from 一個(gè)已經(jīng)消耗完畢的生成器,會(huì)直接返回 None。
def gen():
yield 123
return "result"
def middle():
sub = gen()
res = yield from sub
yield res + " from gen()"
# 到這里的話,sub = gen() 這個(gè)生成器已經(jīng)被消耗完畢了
# 如果我們繼續(xù) yield from 的話,會(huì)直接返回 None
res = yield from sub
yield f"res: {res}"
g = middle()
print(g.__next__()) # 123
print(g.__next__()) # result from gen()
# 此處執(zhí)行 g.__next__() 時(shí)
# 委托生成器內(nèi)部會(huì)執(zhí)行第二個(gè) res = yield from sub
# 但問(wèn)題是 sub 之前就已經(jīng)被消耗完了,所以會(huì)直接返回 None,然后尋找下一個(gè) yield
print(g.__next__()) # res: None
所以不要對(duì)生成器做二次消費(fèi)。
2)子生成器結(jié)束時(shí),最后的 return value 等價(jià)于 raise StopIteration(value)。然后該異常會(huì)被 yield from 捕獲,并將 value 賦值給 yield from 左側(cè)的變量。并且在拿到子生成器的返回值時(shí),委托生成器會(huì)繼續(xù)運(yùn)行,尋找下一個(gè) yield。
def gen():
yield 123
return "result"
def middle():
res = yield from gen()
yield res + " from middle()"
g = middle()
print(g.__next__()) # 123
# 子生成器 gen() 在 return 時(shí)會(huì)拋出 StopIteration
# 然后在委托生成器內(nèi)部被捕獲,并將返回值賦給 res
# 接著繼續(xù)尋找下一個(gè) yield
print(g.__next__()) # result from middle()
另外補(bǔ)充一點(diǎn),生成器在 return 時(shí),等價(jià)于拋出一個(gè) StopIteration。但異常必須在 return 的時(shí)候隱式拋出,如果是在生成器內(nèi)部 raise StopIteration 則是不合法的。
def gen():
yield 123
raise StopIteration("result")
g = gen()
print(g.__next__()) # 123
print(g.__next__())
"""
Traceback (most recent call last):
File "......", line 3, in gen
raise StopIteration("result")
StopIteration: result
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "......", line 7, in <module>
print(g.__next__())
RuntimeError: generator raised StopIteration
"""
此時(shí)會(huì)引發(fā)一個(gè) RuntimeError。
3)如果子生成器在執(zhí)行的過(guò)程中,內(nèi)部出現(xiàn)了異常,那么會(huì)將異常丟給委托生成器。委托生成器會(huì)嘗試處理該異常,如果處理不了,那么再調(diào)用子生成器的 throw 方法將異常扔回去。
def gen():
yield 123
raise ValueError("出了個(gè)錯(cuò)")
return "result"
def middle():
yield from gen()
g = middle()
print(g.__next__()) # 123
# 此時(shí)子生成器會(huì)拋出 ValueError,而委托生成器沒(méi)有異常捕獲邏輯,無(wú)法處理
# 于是會(huì)調(diào)用子生成器的 throw 方法,將異常重新扔回去,最終由調(diào)用方來(lái)處理
try:
print(g.__next__()) # 123
except ValueError as e:
print(e) # 出了個(gè)錯(cuò)
那如果委托生成器可以處理子生成器拋出的異常呢?
def gen():
yield 123
raise ValueError("出了個(gè)錯(cuò)")
return "result"
def middle():
try:
yield from gen()
except ValueError as e:
yield f"異常:{e}"
# 當(dāng)子生成器拋出異常時(shí),它就已經(jīng)結(jié)束了
yield "result from middle()"
g = middle()
print(g.__next__()) # 123
print(g.__next__()) # 異常:出了個(gè)錯(cuò)
print(g.__next__()) # result from middle()
如果委托生成器可以處理子生成器拋出的異常,那么接下來(lái)就是調(diào)用方和委托生成器之間的事情了。
再比如我們將生成器 close 掉,看看結(jié)果會(huì)怎樣,我們知道它會(huì) throw 一個(gè) GeneratorExit。
def gen():
yield 123
return "result"
def middle():
try:
yield from gen()
except GeneratorExit as e:
print(f"子生成器結(jié)束了")
g = middle()
print(g.__next__()) # 123
# 關(guān)閉子生成器,會(huì) throw 一個(gè) GeneratorExit
# 然后這個(gè) GeneratorExit 會(huì)向上透?jìng)鹘o委托生成器
g.close()
"""
子生成器結(jié)束了
"""
# 注意:委托生成器也是同理
# 一旦捕獲了 GeneratorExit,后續(xù)不應(yīng)該再出現(xiàn) yield
yield from 算是 Python 里面特別難懂的一個(gè)語(yǔ)法了,但如果理解了 yield from,后續(xù)理解 await 就會(huì)簡(jiǎn)單很多。
生成器表達(dá)式
Python 里面還有一個(gè)生成器表達(dá)式,我們來(lái)看一下。
from typing import Generator
g = (x for x in range(10))
print(isinstance(g, Generator)) # True
print(g) # <generator object <genexpr> at 0x...>
print(g.__next__()) # 0
print(g.__next__()) # 1
如果表達(dá)式是在一個(gè)函數(shù)里面,那么生成器表達(dá)式周圍的小括號(hào)可以省略掉。
import random
d = [random.randint(1, 10) for _ in range(100)]
# 我們想統(tǒng)計(jì)里面大于 5 的元素的總和
# 下面兩種做法都是可以的
print(
sum((x for x in d if x > 5)),
sum(x for x in d if x > 5)
) # 397 397
這兩種做法是等價(jià)的,字節(jié)碼完全一樣。
但要注意,生成器表達(dá)式還存在一些陷阱,一不小心就可能踩進(jìn)去。至于是什么陷阱呢?很簡(jiǎn)單,一句話:使用生成器表達(dá)式創(chuàng)建生成器的時(shí)候,in 后面的變量就已經(jīng)確定了,但其它的變量則不會(huì)。舉個(gè)栗子:
g = (巭孬嫑夯烎 for x in [1, 2, 3])
執(zhí)行這段代碼不會(huì)報(bào)錯(cuò),盡管 for 前面那一坨我們沒(méi)有定義,但不要緊,因?yàn)樯善魇嵌栊詧?zhí)行的。但如果我們調(diào)用了 g.__next__(),那么很明顯就會(huì)報(bào)錯(cuò)了,會(huì)拋出 NameError。
g = (x for x in lst)
但是這段代碼會(huì)報(bào)錯(cuò):NameError: name 'lst' is not defined,因?yàn)?in 后面的變量在創(chuàng)建生成器的時(shí)候就已經(jīng)確定好了。而在創(chuàng)建生成器的時(shí)候,發(fā)現(xiàn) lst 沒(méi)有定義,于是拋出 NameError。
所以,陷阱就來(lái)了:
i = 1
g = (x + i for x in [1, 2, 3])
i = 10
# 輸出的不是 (2, 3, 4)
print(tuple(g)) # (11, 12, 13)
因?yàn)樯善髦挥性趫?zhí)行的時(shí)候,才會(huì)去確定變量 i 究竟指向誰(shuí),而調(diào)用 tuple(g) 的時(shí)候 i 已經(jīng)被修改了。
lst = [1, 2, 3]
g = (x for x in lst)
lst = [4, 5, 6]
print(tuple(g)) # (1, 2, 3)
但這里輸出的又是 (1, 2, 3),因?yàn)樵趧?chuàng)建生成器的時(shí)候,in 后面的變量就已經(jīng)確定了,這里會(huì)和 lst 指向同一個(gè)列表。而第三行改變的只是變量 lst 的指向,和生成器無(wú)關(guān)。
g = (x for x in [1, 2, 3, 4])
for i in [1, 10]:
g = (x + i for x in g)
print(tuple(g))
思考一下,上面代碼會(huì)打印啥?下面進(jìn)行分析:
- 初始的 g,可以看成是 (1, 2, 3, 4),因?yàn)?in 后面是啥,在創(chuàng)建生成器的時(shí)候就確定了;
- 第一次循環(huán)之后,g 就相當(dāng)于 (1+i, 2+i, 3+i, 4+i);
- 第二次循環(huán)之后,g 就相當(dāng)于 (1+i+i, 2+i+i, 3+i+i, 4+i+i);
而循環(huán)結(jié)束后,變量 i 會(huì)指向 10,所以打印結(jié)果就是 (21, 22, 23, 24)。
生成器與協(xié)程
在 Python 還沒(méi)有引入原生協(xié)程的時(shí)候,很多開(kāi)源框架都是基于生成器模擬的協(xié)程,最經(jīng)典的莫過(guò)于 Tornado。然而事實(shí)上,即便是原生協(xié)程,在底層也是基于生成器實(shí)現(xiàn)的。
async def native_coroutine():
return "古明地覺(jué)"
try:
native_coroutine().__await__().__next__()
except StopIteration as e:
print(e.value) # 古明地覺(jué)
這里沒(méi)有創(chuàng)建事件循環(huán),而是直接驅(qū)動(dòng)協(xié)程執(zhí)行。我們?cè)傺菔疽欢未a,看看讓生成器協(xié)程和原生協(xié)程混合使用會(huì)是什么效果。
import asyncio
import time
import types
async def some_task():
"""
某個(gè)耗時(shí)較長(zhǎng)的任務(wù)
"""
await asyncio.sleep(3)
return "task result"
async def native_coroutine():
"""
原生協(xié)程
"""
result = await some_task()
return f"{result} from native coroutine"
@types.coroutine # 或者使用 @asyncio.coroutine
def generator_coroutine():
"""
生成器模擬的協(xié)程
"""
result = yield from some_task()
return f"{result} from generator coroutine"
async def main():
start = time.time()
result = await asyncio.gather(
native_coroutine(), generator_coroutine()
)
end = time.time()
print(result)
print(f"耗時(shí):{end - start}")
asyncio.run(main())
"""
['task result from native coroutine', 'task result from generator coroutine']
耗時(shí):3.0016210079193115
"""
從效果上來(lái)看,兩種方式是等價(jià)的。yield from 會(huì)驅(qū)動(dòng)協(xié)程對(duì)象執(zhí)行,當(dāng)協(xié)程執(zhí)行 return 的時(shí)候,會(huì)拋出一個(gè) StopIteration 異常。然后 yield from 再將異常捕獲掉,并取出里面的返回值。
但使用裝飾器 + yield from 這種方式不夠優(yōu)雅,并且 yield from 即用于生成器,又用于協(xié)程,容易給人造成困惑。為此 Python 從 3.5 開(kāi)始引入了原生協(xié)程,使用 async def 定義協(xié)程,使用 await 驅(qū)動(dòng)協(xié)程執(zhí)行。
關(guān)于協(xié)程的更多細(xì)節(jié),后續(xù)在介紹協(xié)程的時(shí)候再說(shuō),總之我們現(xiàn)在應(yīng)該使用原生協(xié)程,至于 yield from 就讓它留在歷史的塵埃中吧,我們只需要知道整個(gè)演進(jìn)過(guò)程即可。
小結(jié)
以上我們就從 Python 的角度梳理了一遍生成器相關(guān)的知識(shí),下一篇文章我們將從源碼的角度來(lái)分析生成器的具體實(shí)現(xiàn)。