週六有四百張訂單,為了求和先把它們全塞進一個列表,收銀臺就開始卡。生成器一次只交出一張。yield 之間到底保留了什麼?
生成器函式一次產生一個值,並記住自己停在哪裡。因此它可以在不把全部值放進記憶體的情況下處理很長甚至無限的序列。關鍵在 yield:它暫停函式,而不是永久回傳。 你還會用 yield from 委託、觀察惰性執行,並用 close() 觸發清理。
一次交出一個值
函式執行到 yield 時,會把一個值交還給呼叫方並暫停。下一次請求會從這一行之後繼續。迴圈可以消費整個序列,而不必事先知道會產出多少個值。
def countdown(n):
while n > 0:
yield n
n -= 1
for value in countdown(3):
print(value)會暫停的函式
用 next 主動取值
呼叫生成器函式不會立刻執行函式體,而是回傳一個生成器物件。每次呼叫 next 才請求一個值。沒有更多值時,next 會拋出 StopIteration,for 迴圈會自動處理它。
def numbers():
yield 1
yield 2
gen = numbers()
print(next(gen))
print(next(gen))按需求值
使用生成器表達式
生成器表達式使用圓括號,而不是列表推導式的方括號。它惰性地產生值。需要查看或重複使用完整結果時,再用 list() 包起來。
squares = (n * n for n in range(4))
print(list(squares))惰性推導式
yield from 委託一個流
yield from 把控制權交給另一個可迭代物件,並轉發每個值。它替代巢狀的 for/yield,讓組合關係更明確。內層生成器耗盡後,外層生成器繼續執行。
def drinks():
yield "latte"
yield "tea"
def menu():
yield "menu"
yield from drinks()
yield "done"
print(list(menu()))從子生成器轉發值
生成器流水線保持惰性
建立生成器不會執行函式體。只有消費者請求下一個值時,值才流動,所以流水線可以提前停止。日誌等副作用發生在消費時,而不是構造流水線時。
def source():
print("source start")
for n in [1, 2, 3]:
print("yield", n)
yield n
pipeline = (n * 10 for n in source())
print("before consume")
print(next(pipeline))
print("after first")
print(list(pipeline))消費者拉動之前什麼都不執行
close() 會執行 finally
呼叫 close() 會在暫停的 yield 處拋出 GeneratorExit,並執行 finally。消費者提前停止時,生成器因此能釋放檔案或鎖。通常 with 更清楚,但 close() 解釋了清理契約。
def resource():
try:
yield "open"
yield "still open"
finally:
print("cleanup")
gen = resource()
print(next(gen))
gen.close()
print("closed")提前停止也能清理
本課要點
- yield 會暫停生成器並產生一個值。
- 呼叫生成器函式會回傳生成器物件。
- next 取出一個值;StopIteration 表示結束。
- 生成器表達式使用圓括號,而且只能消費一次。
- 生成器惰性執行;yield from 組合流;close() 觸發 finally 清理。
常見錯誤
- 生成器耗盡後還試圖重複使用它。
- 以為第一次呼叫 next 之前函式體就會執行。
- 序列太大無法放入記憶體時,仍然使用列表推導式。
- 建立生成器後就期待它的副作用立刻執行。
寫一個依次產出 3、2、1 的生成器,再列印 list(countdown(3))。
動手試試
改改變數、先預測輸出,再執行程式碼。第一次點擊「執行程式碼」時會下載 Python 執行時,可能需要幾秒鐘。
練習本課
12 道題,作答後立即看到解釋。