【python小技巧5】一種特殊的迭代器——生成器

上一章我們討論了迭代器。在迭代器里,有一種很常用,那就是生成器(Generator)。
開始前的自測:
目錄
生成器是指?
生成器函數(shù)
生成器對象
生成器表達式
再講 yield
一、生成器?
生成器可以指:生成器函數(shù)、生成器對象、生成器表達式
統(tǒng)稱生成器,容易弄混,后面我們盡量使用全稱??1?
它們的共同點是按需產生數(shù)據值,從而避免一次性將大量數(shù)據存儲在內存中
一句話:生成器函數(shù)是一個函數(shù),調用產生一個生成器對象。生成器表達式的值是一個生成器對象。

二、生成器函數(shù)
生成器函數(shù),顧名思義,本質上是一個函數(shù)。該函數(shù)可以像普通函數(shù)一樣調用,但是返回值是一個生成器對象。
比如,我給你一個函數(shù):
當我調用 f(3) 之后,打印一個 "n: 3"?并且返回一個 4。這很簡單,對吧?但如果我將 return 改為 yield:
調用 f(3),并沒有輸出 "n: 3",返回值是一個 generator object,即生成器對象。
當我調用 next(a)?時才會打印?"n: 3"、返回 4
它的特點有:
函數(shù)內有必須?yield?關鍵字(python 解釋器依靠這一條來區(qū)分生成器函數(shù)和普通函數(shù));可以有 return,如果沒有 return,會自動在最末尾 return None。
調用后不會直接執(zhí)行函數(shù)內的代碼。
返回的不是函數(shù)內的 return 值,而是一個生成器對象。
對生成器調用 next() 函數(shù)時會運行函數(shù),直到運行到 yield 關鍵字就把后面的值作為 next() 函數(shù)的返回值返回出去(特例:一個單獨的 yield 等同于 yield None),然后函數(shù)暫停,記住運行的位置,暫時退出函數(shù)。下一次調用 next() 時,從上次的位置繼續(xù)運行函數(shù)。
運行完函數(shù)或遇到 return 時,則觸發(fā) StopIteration,return 的值就是?StopIteration 異常里的值(value 屬性)。

三、生成器對象
一定是迭代器、可迭代
迭代器是本身
有兩種獲得方式:
調用生成器函數(shù)
使用生成器表達式
屬性:
gi_code
:生成器運行時關聯(lián)的 code 對象。gi_frame
:返回生成器所在的幀(frame)對象。gi_running
:返回生成器對象是否正在迭代。布爾值。gi_yieldfrom
:如果該生成器是一個委派子生成器,則返回其對應的子生成器;否則返回 None。(詳見下文第五節(jié))方法
close
()
:在生成器函數(shù)暫停的位置引發(fā) GeneratorExit。如果之后生成器函數(shù)正常退出、關閉或引發(fā) GeneratorExit (由于未捕獲該異常) 則關閉并返回其調用者。
如果生成器產生了一個值,關閉會引發(fā) RuntimeError。
如果生成器引發(fā)任何其他異常,它會被傳播給調用者。
如果生成器已經由于異?;蛘M顺鰟t close() 不會做任何事。
send(value)
:恢復執(zhí)行并向生成器函數(shù)“發(fā)送”一個值。value 參數(shù)將成為當前 yield 表達式的結果。
send() 方法會返回生成器所產生的下一個值,或者如果生成器沒有產生下一個值就退出則會引發(fā) StopIteration。
當調用 send() 來啟動生成器時(第一次),它必須以 None 作為調用參數(shù),因為這時沒有可以接收值的 yield 表達式;否則會引發(fā)一個?TypeError("can't send non-None value to a just-started generator")。
throw(type[, value[, traceback]])
:在生成器暫停的位置引發(fā)?type
?類型的異常,并返回該生成器函數(shù)所產生的下一個值。如果生成器沒有產生下一個值就退出,則將引發(fā)?
StopIteration
?異常。如果生成器函數(shù)沒有捕獲傳入的異常,或引發(fā)了另一個異常,則該異常會被傳播給調用者。
__next__()
: 產生下一個值,與 send(None) 等價。一般不直接調用。

四、生成器表達式
A generator expression is a compact generator notation in parentheses.?2?
生成器表達式會產生一個新的生成器對象。 其句法與推導式相同,區(qū)別在于它是用圓括號而不是用方括號或花括號括起來的。
我的理解:生成器表達式就是遍歷一個對象,對遍歷出來的值進行判斷或處理。
寫法:
先寫一個小括號 `()`
在里面寫上用來填充這個“序列”的“代表元素”,不僅可以是變量本身 `x`,也可以做一些處理,比如 `foo(x)`
后邊必須是一個 for 循環(huán)遍歷一個對象,e.g. `(foo(x) for x in [1, 2, 3, 4, 5])`
再后面,就是可選的部分了??梢允瞧渌?for 循環(huán),也可以是 if 語句,也可以有多層嵌套,每一個 for 或者 if 語句的嵌套范圍直到生成器表達式末尾
再看文章開頭那個問題,顯然它的值是一個生成器對象。如果轉化為列表的話 ?3?,代碼等價于:
不難看出,它的作用是將 [0, 10) 范圍內能被 3 整除的數(shù)重復三遍的。
開始講點細節(jié)上的東西:
生成器表達式中使用的變量會在迭代下一個時被求值,即與普通生成器相同;但是,最左側?
for
?子句內的可迭代對象是會被立即求值的。正是由于這個特性,使它比列表推導式更省內存。圓括號在只附帶一個參數(shù)的調用中可以省略。e.g.
只能迭代一次,再迭代就 StopIteration 啦!

五、再講 yield
首先,yield 關鍵字只能用在函數(shù)里。
其次,也是很多人不知道的冷知識,yield 不只是語句,也可以是表達式!這個 yield 表達式的值就是生成器對象被調用 .send() 方法被傳入的值。一個單獨的 next(gen_obj) 或者 gen_obj.__next__() 等價于 gen_obj.send(None)。記?。?span id="s0sssss00s" class="color-pink-03">使用 yield 表達式記得加括號!
e.g. 函數(shù)內 `print(yield x)` 是非法的!得寫成 `print((yield x))`,外層括號是 print 函數(shù)的調用,內層屬于 yield 表達式,這個不像生成器表達式一樣可以省略!
yield from? 沒錯,生成器函數(shù)中還可以將“生成”這個活委派給子生成器 (Subgenerator)
當使用 yield from <expr> 時,所提供的表達式必須是一個可迭代對象。 迭代該可迭代對象所產生的值會被直接傳遞給當前生成器方法的調用者。 任何通過 send() 傳入的值以及任何通過 throw() 傳入的異常如果有適當?shù)姆椒▌t會被傳給下層迭代器。 如果不是這種情況,那么 send() 將引發(fā) AttributeError 或 TypeError,而 throw() 將立即引發(fā)所轉入的異常。
END

注釋:
?1??一般說的生成器通常指生成器對象
?2? 摘自 https://docs.python.org/3.8/reference/expressions.html
?3? 只是方便理解
參考資料:
https://docs.python.org/3.8/reference/expressions.html
https://docs.python.org/3.8/tutorial/classes.html
https://docs.python.org/3.8/howto/functional.html
https://docs.python.org/3.8/reference/datamodel.html
https://docs.python.org/3.8/reference/simple_stmts.html
https://peps.python.org/pep-0380
部分內容使用 ChatGPT 編寫。
以上內容如有錯誤,歡迎指出!