Python 迭代與生成器(Iteration & Generators)
主題:yield from
簡介
在 Python 中,生成器(generator)讓我們可以用「懶評估」的方式逐一產出資料,極大降低記憶體使用量。而在多層生成器相互呼叫的情境下,傳統的 for item in sub_generator: yield item 常會寫得冗長且不易維護。Python 3.3 之後引入的 yield from,正是為了解決這類「生成器嵌套」的痛點。
yield from 不僅能把子生成器的所有值一次性傳遞給呼叫端,還會自動把 send、throw、close 等控制訊號轉交給子生成器,讓我們可以以 更簡潔、更一致 的方式構建複雜的資料流或協程(coroutine)管線。掌握 yield from,是從「基礎生成器」走向「高階協程」的關鍵一步。
核心概念
1. yield from 的語法與基本行為
def outer():
# 直接把 inner 產生的所有值交給呼叫端
yield from inner()
yield from <iterable>會 遍歷<iterable>,把每個元素交給外層的yield。- 同時,它會把
send()、throw()、close()等方法的呼叫代理給<iterable>(若<iterable>本身是生成器的話)。 yield from會返回子生成器的 最終返回值(return的值),這在構建協程時非常有用。
2. 為什麼要使用 yield from?
傳統寫法 (for + yield) |
yield from |
|---|---|
| 必須手動迭代子生成器 | 自動迭代 |
不能直接傳遞 send、throw、close |
會自動代理 |
| 需要自行管理返回值 | 可直接取得 return 結果 |
| 程式碼較冗長,易出錯 | 簡潔且安全 |
3. 基本範例
範例 1:最簡單的子生成器代理
def numbers():
for i in range(5):
yield i
def wrapper():
# 直接把 numbers() 的所有值交給呼叫端
yield from numbers()
for n in wrapper():
print(n) # 輸出 0 1 2 3 4
重點:
wrapper不需要自己寫for n in numbers(): yield n,yield from已幫我們完成。
範例 2:傳遞 send 給子生成器
def echo():
while True:
value = yield # 接收外部的值
print(f"Echo: {value}")
def controller():
# 把外部的 send 直接傳給 echo
yield from echo()
gen = controller()
next(gen) # 啟動生成器,跑到第一個 yield
gen.send("Hello") # 直接送到 echo 裡
gen.send("World")
gen.close() # 關閉整條生成鏈
說明:使用
yield from後,我們不需要在controller裡寫value = sub_gen.send(v),send會自動穿透。
範例 3:取得子生成器的返回值(return)
def sub():
yield 1
yield 2
return "完成" # 返回值
def main():
result = yield from sub() # result 會是 "完成"
print("子生成器回傳:", result)
for _ in main():
pass
# 輸出:
# 1
# 2
# 子生成器回傳: 完成
技巧:在協程中,子生成器的返回值常用來傳遞狀態或結果,
yield from讓取得變得自然。
範例 4:嵌套多層 yield from
def level3():
yield "L3-1"
yield "L3-2"
def level2():
yield from level3()
yield "L2-1"
def level1():
yield "L1-0"
yield from level2()
yield "L1-1"
for item in level1():
print(item)
# 輸出: L1-0 L3-1 L3-2 L2-1 L1-1
觀察:每一層只需寫一次
yield from,即可把全部子層的值串接起來,程式結構非常清晰。
範例 5:建立生成器管線(pipeline)
def source(data):
"""產生原始資料"""
for x in data:
yield x
def filter_even():
"""只保留偶數"""
while True:
value = yield
if value % 2 == 0:
yield value
def square():
"""把接收到的值平方"""
while True:
value = yield
yield value * value
def pipeline(data):
"""把三個生成器串成一條管線"""
# 建立子生成器
f_even = filter_even()
next(f_even) # 啟動
sq = square()
next(sq)
# 使用 yield from 依序傳遞
for item in source(data):
# 先送到 filter_even,若被過濾則不產生值
try:
filtered = f_even.send(item)
except StopIteration:
continue
if filtered is None:
continue
# 再送到 square
result = sq.send(filtered)
yield result
for v in pipeline(range(1, 11)):
print(v) # 輸出 4 16 36 64 100(2,4,6,8,10 的平方)
說明:透過
yield from我們可以把 資料流 以「生成器」的形式串起來,且每個環節都能接受send、throw,形成高度可組合的管線。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記啟動子生成器 (next(sub_gen)) |
yield from 會自動呼叫 sub_gen.__next__(),但若自行使用 sub_gen.send() 前未先啟動,會拋 TypeError: can't send non-None value to a just-started generator。 |
用 yield from 時不必自行 next();若自行呼叫子生成器,記得先 next()。 |
| 子生成器拋出例外未被捕獲 | yield from 會把外層的 throw() 直接傳給子生成器,若子生成器未處理,會向上冒泡。 |
在子生成器內使用 try/except 捕獲,或在外層使用 try/except 包住 yield from 呼叫。 |
| 返回值被忽略 | 很多人只把 yield from 當作「迭代」使用,忽略了它會返回子生成器的 return 值。 |
若需要子生成器的結果,務必把 result = yield from sub() 儲存或使用。 |
| 過度嵌套導致除錯困難 | 雖然 yield from 能簡化層層迭代,但過度嵌套會讓堆疊追蹤變長。 |
保持每條管線的長度在 2–3 層,必要時在外層加上 log 或 debug 訊息。 |
混用 return 與 yield |
在同一生成器裡同時使用 return(返回值)與 yield(產出值)會讓閱讀者困惑。 |
若需要返回值,建議把返回邏輯放在最外層生成器,或使用 yield from 取得子生成器的返回值。 |
最佳實踐
- 以「管線」思維設計:將資料處理拆成小的生成器,每個生成器只負責單一職責,最外層使用
yield from組合。 - 明確捕獲子生成器的返回值:
result = yield from sub()能讓你在外層直接取得子生成器的結束狀態或計算結果。 - 使用
try/finally保證資源釋放:在子生成器內部加入try: ... finally: clean_up(),yield from會把close()正確傳遞下去。 - 避免在
yield from前後混雜額外的yield:若需要在兩段子生成器之間插入額外的值,請先結束前一段的yield from,再寫新的yield或yield from。 - 寫測試:因為
yield from牽涉到多層協程的控制流,單元測試(assert list(gen) == expected)能快速捕捉錯誤。
實際應用場景
| 場景 | 為什麼適合使用 yield from |
|---|---|
| 大檔案逐行處理(如 log、CSV) | 把檔案讀取、過濾、轉換三個步驟分別寫成生成器,外層只需 yield from filter_gen,記憶體佔用僅為單行大小。 |
| 網路串流(WebSocket、Kafka) | 把接收、解碼、業務邏輯各自抽成生成器,yield from 讓訊號(如 close)自動向下傳遞,簡化錯誤處理。 |
| 協程框架(asyncio) 的底層實作 | asyncio 的 Task 內部使用 yield from 把子協程的結果傳回,理解它有助於自行撰寫自訂協程。 |
| 資料管線(ETL) | 把 Extract、Transform、Load 拆成三個生成器,使用 yield from 串成完整流程,且每個階段都能接受 send 進行動態參數調整。 |
| 遞迴演算法(樹、圖遍歷) | 透過 yield from 把子樹的遍歷結果直接回傳,寫出的程式碼比手動 for 更簡潔且易讀。 |
範例:遞迴遍歷二元樹
class Node:
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
def inorder(node):
if node is None:
return
# 左子樹
yield from inorder(node.left)
# 本節點
yield node.value
# 右子樹
yield from inorder(node.right)
# 建立簡易樹
root = Node(4,
Node(2, Node(1), Node(3)),
Node(6, Node(5), Node(7)))
print(list(inorder(root))) # [1, 2, 3, 4, 5, 6, 7]
總結
yield from 是 Python 生成器家族中極具威力的語法糖,它不只是簡化迭代,更提供了 代理控制訊號、取得子生成器返回值 以及 構建可組合管線 的能力。掌握它之後,我們可以:
- 用更少的程式碼寫出 多層迭代 與 協程。
- 在 資料流、大檔案處理、網路串流 等需要 懶評估 的場景中,保持低記憶體佔用且易於維護。
- 以 pipeline 思維把複雜的業務邏輯拆解成單一職責的生成器,提升程式的可讀性與可測試性。
最後,建議在實務開發中 先從簡單的 yield from 替代 for … yield 開始,逐步探索 send、throw、return 的進階用法,並結合單元測試確保行為正確。如此一來,你的 Python 程式碼將能在效能、可維護性與可擴充性上,同時得到顯著提升。祝你玩得開心,寫出更優雅的生成器!