我之前向大家介紹了某個神奇的物種,這里再把原因解釋一下。有個網(wǎng)名叫 chao 的人,自稱超哥,在我的群里引戰(zhàn)。由于我當(dāng)初不怎么看群,中間也只是簡單勸過他,直到后面越來越多的人被他氣到退群,我沒辦法,只能將他移出群聊,移出的時候還說了句抱歉。
本來是一件非常簡單的事情,但他莫名對我產(chǎn)生了恨意,特別是當(dāng)我整了個付費合集之后,在后臺私信追著我罵。 類似的記錄還有一大堆,更加的污言穢語,這些我就不貼了。至于我當(dāng)初為什么不拉黑他,主要是我想看看一個人能莫名產(chǎn)生多大的恨意。另外我也一直在跟他復(fù)盤,希望他能明白整個事情的經(jīng)過,但他似乎只專注于罵人。 如果只是簡單的臟話,我倒還無所謂,關(guān)鍵的是它一直在污蔑我,說我教唆別人罵他,說我干了很多黑心事,問題是我怎么不知道。 當(dāng)然以上這些都不是重點,重點是它說要發(fā)文章打敗我。
他之前評論也說了,要讓大家明白我的文章其實一文不值。聽到這句話我還挺開心的,我也好奇并期待他能寫出什么樣的文章,但是之后就沒影了······。 它還建立了一個微信群,我有一個群友加進(jìn)去了,這是它的豪言壯語。 現(xiàn)如今已經(jīng) 4 個月過去了,它依舊沒什么動靜,并且說完這話沒幾天,群友就被踢出去了?????? 可能是我和這個人之間產(chǎn)生了深厚的感情,昨晚睡覺的時候想起來自己還沒有介紹生成器,今天專門請了半天假,把文章肝出來了。超哥沒有完成的事情,就由我來替他完成吧,畢竟謝幕的時候,它作為主角應(yīng)該站在舞臺上,閃閃發(fā)光,熠熠生輝。 ??守護(hù)全世界最好的超哥?? ??即使全世界都拋棄了你?? ??我也會堅定的?? ??和全世界站在一起?? 本次來聊一聊 Python 的生成器,它是我們后續(xù)理解協(xié)程的基礎(chǔ)(對不起,沒有后續(xù)了)。生成器的話,估計大部分人在寫程序的時候都不怎么用,但其實生成器一旦用好了,確實能給程序帶來性能上的提升,那么下面就來看一看吧。 我們知道,如果函數(shù)的內(nèi)部出現(xiàn)了 yield 關(guān)鍵字,那么它就不再是普通的函數(shù)了,而是一個生成器函數(shù),調(diào)用之后會返回一個生成器對象。 生成器對象一般用于處理循環(huán)結(jié)構(gòu),應(yīng)用得當(dāng)?shù)脑捒梢詷O大優(yōu)化內(nèi)存使用率。比如:我們讀取一個大文件。 def read_file(file): return open(file, encoding="utf-8").readlines()
print(read_file("假裝是大文件.txt")) """ ['人生是什么?\n', '大概是閃閃發(fā)光的同時\n', '又讓人感到痛苦的東西吧'] """
這個版本的函數(shù),直接將里面的內(nèi)容全部讀取出來了,返回了一個列表。如果文件非常大,那么內(nèi)存的開銷可想而知。于是我們可以通過 yield 關(guān)鍵字,將普通函數(shù)變成一個生成器函數(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") # 返回一個生成器對象 print(data) """ <generator object read_file at 0x0000019B4FA8BAC0> """
# 使用 for 循環(huán)遍歷 for line in data: # 文件每一行自帶換行符, 所以這里的 print 就不用換行符了 print(line, end="") """ 人生是什么? 大概是閃閃發(fā)光的同時 又讓人感到痛苦的東西吧 """
由于生成器是一種特殊的迭代器,所以也可以使用它的 __next__ 方法。 def gen(): yield 123 yield 456 yield 789 return "result"
# 調(diào)用生成器函數(shù)時,會創(chuàng)建一個生成器 # 生成器雖然創(chuàng)建了,但是里面的代碼并沒有執(zhí)行 g = gen()
# 調(diào)用 __next__ 方法時才會執(zhí)行 # 當(dāng)遇到 yield,會將生成器暫停、并返回 yield 后面的值 print(g.__next__()) # 123
# 此時生成器處于暫停狀態(tài),如果我們不驅(qū)動它的話,它是不會前進(jìn)的 # 再次執(zhí)行 __next__,生成器恢復(fù)執(zhí)行,并在下一個 yield 處暫停 print(g.__next__()) # 456
# 生成器會記住自己的執(zhí)行進(jìn)度,它總是在遇到 yield 時暫停 # 調(diào)用 __next__ 時恢復(fù)執(zhí)行,直到遇見下一個 yield print(g.__next__()) # 789
# 顯然再調(diào)用 __next__ 時,已經(jīng)找不到下一個 yield 了 # 那么生成器會拋出 StopIteration,并將返回值設(shè)置在里面 try: g.__next__() except StopIteration as e: print(f"返回值:{e.value}") # 返回值:result
可以看到,基于生成器,我們能夠?qū)崿F(xiàn)惰性求值。 當(dāng)然啦,生成器不僅僅有 __next__ 方法,它還有 send 和 throw 方法,我們先來說一說 send。 def gen(): res1 = yield "yield 1" print(f"***** {res1} *****") res2 = yield "yield 2" return res2
g = gen() # 此時程序在第一個 yield 處暫停 print(g.__next__()) """ yield 1 """
# 調(diào)用 g.send(val) 依舊可以驅(qū)動生成器執(zhí)行 # 同時還可以傳遞一個值,交給第一個 yield 左邊的 res1 # 然后尋找第二個 yield print(g.send("嘿嘿")) """ ***** 嘿嘿 ***** yield 2 """ # 上面輸出了兩行,第一行是生成器里面的 print 打印的
try: # 此時生成器在第二個 yield 處暫停,調(diào)用 g.send 驅(qū)動執(zhí)行 # 同時傳遞一個值交給第二個 yield 左邊的 res2,然后尋找第三個 yield # 但是生成器里面沒有第三個 yield 了,于是拋出 StopIteration g.send("蛤蛤") except StopIteration as e: print(f"返回值:{e.value}") """ 返回值:蛤蛤 """
生成器永遠(yuǎn)在 yield 處暫停,并將 yield 后面的值返回。如果想驅(qū)動生成器繼續(xù)執(zhí)行,可以調(diào)用 __next__ 或 send,會去尋找下一個 yield,然后在下一個 yield 處暫停。依次往復(fù),直到找不到 yield 時,拋出 StopIteration,并將返回值包在里面。 但是這兩者的不同之處在于,send 可以接收參數(shù),假設(shè)生成器在 res = yield 123 這里停下來了。 當(dāng)調(diào)用 __next__ 和 send 的時候,都可以驅(qū)動執(zhí)行,但調(diào)用 send 時可以傳遞一個 value,并將 value 賦值給變量 res。而 __next__ 沒有這個功能,如果是調(diào)用 __next__ 的話,那么 res 得到的就是一個 None。 所以 res = yield 123 這一行語句需要兩次驅(qū)動生成器才能完成,第一次驅(qū)動會讓生成器執(zhí)行到 yield 123,然后暫停執(zhí)行,將 123 返回。第二次驅(qū)動才會給變量 res 賦值,此時會尋找下一個 yield 然后暫停。 剛創(chuàng)建生成器的時候,里面的代碼還沒有執(zhí)行,它的 f_lasti 是 -1。關(guān)于什么是 f_lasti,需要解釋一下。 首先隨著 CPython 版本的升級,一些數(shù)據(jù)結(jié)構(gòu)的底層實現(xiàn)也在發(fā)生改變,比如棧幀等等。在之前的版本中,棧幀有一個字段叫 f_lasti,它表示最近一條執(zhí)行完畢的字節(jié)碼指令的偏移量。而在 3.12 里面,這個字段已經(jīng)沒了。
雖然解釋器內(nèi)部結(jié)構(gòu)會發(fā)生變化,但暴露出來的 Python 接口是不變的,所以我們依舊可以訪問該字段。 def gen(): res1 = yield 123 res2 = yield 456 return "result"
g = gen() # 生成器函數(shù)和普通函數(shù)一樣,執(zhí)行時也會創(chuàng)建棧幀 # 通過 g.gi_frame 可以很方便的獲取 print(g.gi_frame.f_lasti) # -1
f_lasti 是 -1,表示生成器剛被創(chuàng)建,還沒有執(zhí)行任何指令。而第一次驅(qū)動生成器執(zhí)行,叫做生成器的預(yù)激。但在生成器還沒有被預(yù)激時,我們調(diào)用 send,里面只能傳遞一個 None,否則報錯。 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 """
對于尚未被預(yù)激的生成器,我們只能傳遞一個 None,也就是 g.send(None)?;蛘哒{(diào)用 g.__next__(),因為不管何時它傳遞的都是 None。 其實也很好理解,我們之所以傳值是為了賦給 yield 左邊的變量,這就意味著生成器必須至少被驅(qū)動一次、在某個 yield 處停下來才可以。而未被預(yù)激的生成器,它里面的代碼壓根就沒有執(zhí)行,所以第一次驅(qū)動的時候只能傳遞一個 None 進(jìn)去。 如果查看生成器的源代碼的話,也能證明這一點: 在之前的版本中,判斷條件是 f_lasti 是否等于 -1,而在 3.12 中引入了 gi_frame_state 字段,表示生成器的狀態(tài)。如果生成器剛創(chuàng)建,并且接收的參數(shù) arg 不為 None,那么報錯。 那么生成器的狀態(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í)行到某個 yield 之后返回了。 FRAME_EXECUTING:生成器執(zhí)行中。 FRAME_COMPLETED:生成器執(zhí)行完畢,但棧幀對象還未被清理。 FRAME_CLEARED:生成器的棧幀對象被清理。
相關(guān)源碼細(xì)節(jié)下一篇文章(對不起,沒有下一篇了)會分析。
除了 __next__ 和 send 方法之外,生成器還有一個 throw 方法,該方法的作用和前兩者類似,也是驅(qū)動生成器執(zhí)行,并在下一個 yield 處暫停。但它在調(diào)用的時候,需要傳遞一個異常進(jìn)去。 def gen(): try: yield 123 except ValueError as e: print(f"異常:{e}") yield 456 return "result"
g = gen() # 生成器在 yield 123 處暫停 g.__next__() # 向生成器傳遞一個異常 # 如果當(dāng)前生成器的暫停位置處無法捕獲傳遞的異常,那么會將異常拋出來 # 如果能夠捕獲,那么會驅(qū)動生成器執(zhí)行,并在下一個 yield 處暫停 # 當(dāng)前生成器位于 yield 123 處,而它所在的位置能夠捕獲異常 # 所以不會報錯,結(jié)果就是 456 會賦值給 val val = g.throw(ValueError("一個 ValueError")) """ 異常:一個 ValueError """ print(val) """ 456 """
關(guān)于生成器的 __next__、send、throw 三個方法的用法我們就說完了,還是比較簡單的。 生成器也是可以關(guān)閉的,我們來看一下。 def gen(): yield 123 yield 456 return "result"
g = gen() # 生成器在 yield 123 處停止 print(g.__next__()) # 123 # 關(guān)閉生成器 g.close() # 生成器一旦關(guān)閉,就代表執(zhí)行完畢了,它的棧幀會被重置為 None print(g.gi_frame) # None try: # 再次調(diào)用 __next__,會拋出 StopIteration g.__next__() except StopIteration as e: # 此時 e.value 為 None print(e.value) # None
無論是顯式地關(guān)閉生成器,還是正常情況下生成器執(zhí)行完畢,內(nèi)部的棧幀都會被重置為 None。而驅(qū)動一個已經(jīng)執(zhí)行結(jié)束的生成器,會拋出 StopIteration 異常,并且異常的 value 屬性為 None。 這里再來說一說 GeneratorExit 這個異常,如果我們關(guān)閉一個生成器(或者生成器被刪除時),那么會往里面扔一個 GeneratorExit 進(jìn)去。 def gen(): try: yield 123 except GeneratorExit as e: print("生成器被刪除了")
g = gen() # 生成器在 yield 123 處暫停 g.__next__() # 關(guān)閉生成器,會往里面扔一個 GeneratorExit g.close() """ 生成器被刪除了 """
這里我們捕獲了傳遞的 GeneratorExit,所以 print 語句執(zhí)行了,但如果沒有捕獲呢? def gen(): yield 123
g = gen() g.__next__() g.close()
此時無事發(fā)生,但是注意:如果是手動調(diào)用 throw 方法扔一個 GeneratorExit 進(jìn)去,異常還是會拋出來的。 那么問題來了,生成器為什么要提供這樣一種機制呢?直接刪就完了,干嘛還要往生成器內(nèi)部丟一個異常呢?答案是為了資源的清理和釋放。 在 Python 還未提供原生協(xié)程,以及 asyncio 還尚未流行起來的時候,很多開源的協(xié)程框架都是基于生成器實現(xiàn)的協(xié)程。而創(chuàng)建連接的邏輯,一般都會寫在 yield 后面。 def _create_connection(): # 一些邏輯 yield conn # 一些邏輯
但是這些連接在不用的時候,要不要進(jìn)行釋放呢?答案是肯定的,所以便可以這么做。 def _create_connection(): # 一些邏輯 try: yield conn except GeneratorExit: conn.close() # 一些邏輯
這樣當(dāng)我們關(guān)閉或刪除生成器的時候,就能夠自動對連接進(jìn)行釋放了。
不過還有一個需要注意的點,就是在捕獲 GeneratorExit 之后,不可以再執(zhí)行 yield,否則會拋出 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 方法時,如果沒有成功捕獲 GeneratorExit,那么生成器會直接關(guān)閉,不會有任何事情發(fā)生。但如果捕獲了 GeneratorExit,那么可以在對應(yīng)的語句塊里做一些資源清理邏輯,但不應(yīng)該再出現(xiàn) yield。 而上面的例子中出現(xiàn)了 yield,所以解釋器會拋出 RuntimeError,因為沒捕獲 GeneratorExit 還好,解釋器不會有什么抱怨。但如果捕獲了 GeneratorExit,說明我們知道生成器是被關(guān)閉了,既然知道,那里面還出現(xiàn) yield 的意義何在呢? 當(dāng)然啦,如果出現(xiàn)了 yield,但沒有執(zhí)行到,則不會拋 RuntimeError。 def gen(): try: yield 123 except GeneratorExit: print("生成器被刪除") return yield
g = gen() g.__next__() g.close() print("------------") """ 生成器被刪除 ------------ """
遇見 yield 之前就返回了,所以此時不會出現(xiàn) RuntimeError。 注意:GeneratorExit 繼承自 BaseException,它無法被 Exception 捕獲。
當(dāng)函數(shù)內(nèi)部出現(xiàn)了 yield 關(guān)鍵字,那么它就是一個生成器函數(shù),對于 yield from 而言亦是如此。那么問題來了,這兩者之間有什么區(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 對后面的值沒有要求,會直接將其返回。而 yield from 后面必須跟一個可迭代對象(否則報錯),然后每次返回可迭代對象的一個值。 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
除了要求必須跟一個可迭代對象,然后每次只返回一個值之外,其它表現(xiàn)和 yield 是類似的。而對于當(dāng)前這個例子來說,yield from [1, 2, 3] 等價于 for item in [1, 2, 3]: yield item。 所以有人覺得 yield from 貌似沒啥用啊,它完全可以用 for 循環(huán)加 yield 進(jìn)行代替。很明顯不是這樣的,yield from 背后做了非常多的事情,我們稍后說。 這里先出一道思考題:
這時候便可以通過 yield 和 yield from 來實現(xià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]
怎么樣,是不是很簡單呢? 如果單從語法上來看的話,會發(fā)現(xiàn) yield from 貌似沒什么特殊的地方,但其實 yield from 還可以作為委托生成器。委托生成器會在調(diào)用方和子生成器之間建立一個雙向通道,什么意思呢?我們舉例說明。 def gen(): yield 123 yield 456 return "result"
def middle(): res = yield from gen() print(f"接收到子生成器的返回值: {res}")
# middle 里面出現(xiàn)了 yield from gen() # 此時 middle() 便是委托生成器,gen() 是子生成器 g = middle()
# 而 yield from 會在調(diào)用方和子生成器之間建立一個雙向通道 # 兩者是可以互通的,調(diào)用 g.send、g.throw 都會直接傳遞給子生成器 print(g.__next__()) # 123 print(g.__next__()) # 456
# 問題來了,如果再調(diào)用一次 __next__ 會有什么后果呢? # 按照之前的理解,應(yīng)該會拋出 StopIteration print(g.__next__()) """ 接收到子生成器的返回值: result Traceback (most recent call last): File "...", line 21, in <module> print(g.__next__()) StopIteration """
在第三次調(diào)用 __next__ 的時候,確實拋了異常,但是委托生成器收到了子生成器的返回值。也就是說,委托生成器在調(diào)用方和子生成器之間建立了雙向通道,兩者是直接通信的,并且當(dāng)子生成器出現(xiàn) StopIteration 時,委托生成器還要負(fù)責(zé)兜底。 委托生成器會將子生成器拋出的 StopIteration 里面的 value 取出來,然后賦值給左側(cè)的變量 res,并在自己內(nèi)部繼續(xù)尋找 yield。 換句話說,當(dāng)子生成器 return 之后,委托生成器會拿到返回值,并將子生成器拋出的異常給捕獲掉。但是還沒完,因為還要找到下一個 yield,那么從哪里找呢?顯然是從委托生成器的內(nèi)部尋找,于是接下來就變成了調(diào)用方和委托生成器之間的通信。 如果在委托生成器內(nèi)部能找到下一個 yield,那么會將值返回給調(diào)用方。如果找不到,那么就重新構(gòu)造一個 StopIteration,將異常拋出去。此時異常的 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)用方和子生成器之間建立一個雙向通道,通道一旦建立,調(diào)用方可以和子生成器直接通信。雖然調(diào)用的是委托生成器的 __next__、send、throw 等方法,但影響的都是子生成器。 并且委托生成器還可以對子生成器拋出的 StopIteration 異常進(jìn)行兜底,會捕獲掉該異常,然后拿到返回值,這樣就無需手動捕獲子生成器的異常了。但問題是委托生成器還要找到下一個 yield,并將值返回給調(diào)用方,此時這個重?fù)?dān)就落在了它自己頭上。 如果找不到,還是要將異常拋出來的,只不過拋出的 StopIteration 是委托生成器構(gòu)建的。而子生成器拋出的 StopIteration,早就被委托生成器捕獲掉了。于是我們可以考慮在 yield from 的前面再加上一個 yield,這樣就不會拋異常了。 我們上面已經(jīng)了解了委托生成器的用法,不過問題來了,這玩意為啥會存在呢?上面的邏輯,即便不使用 yield from 也可以完成啊。
其實是因為我們上面的示例代碼比較簡單(為了演示用法),當(dāng)需求比較復(fù)雜時,將生成器內(nèi)部的部分操作委托給另一個生成器是有必要的,這也是委托生成器的由來。 而委托生成器不僅要能保證調(diào)用方和子生成器之間直接通信,還要能夠以一種優(yōu)雅的方式獲取子生成器的返回值,于是新的語法 yield from 就誕生了。 但其實 yield from 背后為我們做得事情還不止這么簡單,它不單單是建立雙向通道、獲取子生成器的返回值,它還會處理子生成器內(nèi)部出現(xiàn)的異常,詳細(xì)內(nèi)容可以查看 PEP380。 https://peps./pep-0380/
這里我們直接給出結(jié)論,并通過代碼演示一下。 1)子生成器 yield 后面的值,會直接返回給調(diào)用方;調(diào)用方 send 發(fā)送的值,也會直接傳給子生成器。 def gen(): res = yield 123 yield [res] return "result"
def middle(): yield (yield from gen())
g = middle() # 子生成器 yield 后面的值,會直接返回給調(diào)用方 print(g.__next__()) # 123 # 調(diào)用方 send 發(fā)送的值,也會直接傳給子生成器 print(g.send("小云同學(xué)")) # ['小云同學(xué)']
另外還要補充一個細(xì)節(jié),如果 yield from 一個已經(jīng)消耗完畢的生成器,會直接返回 None。 def gen(): yield 123 return "result"
def middle(): sub = gen() res = yield from sub yield res + " from gen()" # 到這里的話,sub = gen() 這個生成器已經(jīng)被消耗完畢了 # 如果我們繼續(xù) yield from 的話,會直接返回 None res = yield from sub yield f"res: {res}"
g = middle() print(g.__next__()) # 123 print(g.__next__()) # result from gen() # 此處執(zhí)行 g.__next__() 時 # 委托生成器內(nèi)部會執(zhí)行第二個 res = yield from sub # 但問題是 sub 之前就已經(jīng)被消耗完了,所以會直接返回 None,然后尋找下一個 yield print(g.__next__()) # res: None
所以不要對生成器做二次消費。 2)子生成器結(jié)束時,最后的 return value 等價于 raise StopIteration(value)。然后該異常會被 yield from 捕獲,并將 value 賦值給 yield from 左側(cè)的變量。并且在拿到子生成器的返回值時,委托生成器會繼續(xù)運行,尋找下一個 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 時會拋出 StopIteration # 然后在委托生成器內(nèi)部被捕獲,并將返回值賦給 res # 接著繼續(xù)尋找下一個 yield print(g.__next__()) # result from middle()
另外補充一點,生成器在 return 時,等價于拋出一個 StopIteration。但異常必須在 return 的時候隱式拋出,如果是在生成器內(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 """
此時會引發(fā)一個 RuntimeError。 3)如果子生成器在執(zhí)行的過程中,內(nèi)部出現(xiàn)了異常,那么會將異常丟給委托生成器。委托生成器會嘗試處理該異常,如果處理不了,那么再調(diào)用子生成器的 throw 方法將異常扔回去。 def gen(): yield 123 raise ValueError("出了個錯") return "result"
def middle(): yield from gen()
g = middle() print(g.__next__()) # 123 # 此時子生成器會拋出 ValueError,而委托生成器沒有異常捕獲邏輯,無法處理 # 于是會調(diào)用子生成器的 throw 方法,將異常重新扔回去,最終由調(diào)用方來處理 try: print(g.__next__()) # 123 except ValueError as e: print(e) # 出了個錯
那如果委托生成器可以處理子生成器拋出的異常呢? def gen(): yield 123 raise ValueError("出了個錯") return "result"
def middle(): try: yield from gen() except ValueError as e: yield f"異常:{e}" # 當(dāng)子生成器拋出異常時,它就已經(jīng)結(jié)束了 yield "result from middle()"
g = middle() print(g.__next__()) # 123 print(g.__next__()) # 異常:出了個錯 print(g.__next__()) # result from middle()
如果委托生成器可以處理子生成器拋出的異常,那么接下來就是調(diào)用方和委托生成器之間的事情了。 再比如我們將生成器 close 掉,看看結(jié)果會怎樣,我們知道它會 throw 一個 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)閉子生成器,會 throw 一個 GeneratorExit # 然后這個 GeneratorExit 會向上透傳給委托生成器 g.close() """ 子生成器結(jié)束了 """ # 注意:委托生成器也是同理 # 一旦捕獲了 GeneratorExit,后續(xù)不應(yīng)該再出現(xiàn) yield
yield from 算是 Python 里面特別難懂的一個語法了,但如果理解了 yield from,后續(xù)理解 await 就會簡單很多。 Python 里面還有一個生成器表達(dá)式,我們來看一下。 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á)式是在一個函數(shù)里面,那么生成器表達(dá)式周圍的小括號可以省略掉。 import random
d = [random.randint(1, 10) for _ in range(100)] # 我們想統(tǒng)計里面大于 5 的元素的總和 # 下面兩種做法都是可以的 print( sum((x for x in d if x > 5)), sum(x for x in d if x > 5) ) # 397 397
這兩種做法是等價的,字節(jié)碼完全一樣。
但要注意,生成器表達(dá)式還存在一些陷阱,一不小心就可能踩進(jìn)去。至于是什么陷阱呢?很簡單,一句話:使用生成器表達(dá)式創(chuàng)建生成器的時候,in 后面的變量就已經(jīng)確定了,但其它的變量則不會。舉個栗子: g = (巭孬嫑夯烎 for x in [1, 2, 3])
執(zhí)行這段代碼不會報錯,盡管 for 前面那一坨我們沒有定義,但不要緊,因為生成器是惰性執(zhí)行的。但如果我們調(diào)用了 g.__next__(),那么很明顯就會報錯了,會拋出 NameError。 g = (x for x in lst)
但是這段代碼會報錯:NameError: name 'lst' is not defined,因為 in 后面的變量在創(chuàng)建生成器的時候就已經(jīng)確定好了。而在創(chuàng)建生成器的時候,發(fā)現(xiàn) lst 沒有定義,于是拋出 NameError。 所以,陷阱就來了: i = 1 g = (x + i for x in [1, 2, 3]) i = 10 # 輸出的不是 (2, 3, 4) print(tuple(g)) # (11, 12, 13)
因為生成器只有在執(zhí)行的時候,才會去確定變量 i 究竟指向誰,而調(diào)用 tuple(g) 的時候 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),因為在創(chuàng)建生成器的時候,in 后面的變量就已經(jīng)確定了,這里會和 lst 指向同一個列表。而第三行改變的只是變量 lst 的指向,和生成器無關(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))
思考一下,上面代碼會打印啥?下面進(jìn)行分析: 初始的 g,可以看成是 (1, 2, 3, 4),因為 in 后面是啥,在創(chuàng)建生成器的時候就確定了; 第一次循環(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 會指向 10,所以打印結(jié)果就是 (21, 22, 23, 24)。 在 Python 還沒有引入原生協(xié)程的時候,很多開源框架都是基于生成器模擬的協(xié)程,最經(jīng)典的莫過于 Tornado。然而事實上,即便是原生協(xié)程,在底層也是基于生成器實現(xiàn)的。 async def native_coroutine(): return "古明地覺"
try: native_coroutine().__await__().__next__() except StopIteration as e: print(e.value) # 古明地覺
這里沒有創(chuàng)建事件循環(huán),而是直接驅(qū)動協(xié)程執(zhí)行。我們再演示一段代碼,看看讓生成器協(xié)程和原生協(xié)程混合使用會是什么效果。 import asyncio import time import types
async def some_task(): """ 某個耗時較長的任務(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"耗時:{end - start}")
asyncio.run(main()) """ ['task result from native coroutine', 'task result from generator coroutine'] 耗時:3.0016210079193115 """
從效果上來看,兩種方式是等價的。yield from 會驅(qū)動協(xié)程對象執(zhí)行,當(dāng)協(xié)程執(zhí)行 return 的時候,會拋出一個 StopIteration 異常。然后 yield from 再將異常捕獲掉,并取出里面的返回值。 但使用裝飾器 + yield from 這種方式不夠優(yōu)雅,并且 yield from 即用于生成器,又用于協(xié)程,容易給人造成困惑。為此 Python 從 3.5 開始引入了原生協(xié)程,使用 async def 定義協(xié)程,使用 await 驅(qū)動協(xié)程執(zhí)行。 關(guān)于協(xié)程的更多細(xì)節(jié),后續(xù)在介紹協(xié)程的時候再說,總之我們現(xiàn)在應(yīng)該使用原生協(xié)程,至于 yield from 就讓它留在歷史的塵埃中吧,我們只需要知道整個演進(jìn)過程即可。 以上我們就從 Python 的角度梳理了一遍生成器相關(guān)的知識,下一篇文章我們將從源碼的角度來分析生成器的具體實現(xiàn)。 對不起,沒有下一篇了,感謝大家對這個系列的支持。
|