本文 AI 產出,尚未審核

Python 迭代與生成器(Iteration & Generators)

主題:yield from


簡介

在 Python 中,生成器(generator)讓我們可以用「懶評估」的方式逐一產出資料,極大降低記憶體使用量。而在多層生成器相互呼叫的情境下,傳統的 for item in sub_generator: yield item 常會寫得冗長且不易維護。Python 3.3 之後引入的 yield from,正是為了解決這類「生成器嵌套」的痛點。

yield from 不僅能把子生成器的所有值一次性傳遞給呼叫端,還會自動把 sendthrowclose 等控制訊號轉交給子生成器,讓我們可以以 更簡潔、更一致 的方式構建複雜的資料流或協程(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
必須手動迭代子生成器 自動迭代
不能直接傳遞 sendthrowclose 會自動代理
需要自行管理返回值 可直接取得 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 nyield 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 我們可以把 資料流 以「生成器」的形式串起來,且每個環節都能接受 sendthrow,形成高度可組合的管線。


常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記啟動子生成器 (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 層,必要時在外層加上 logdebug 訊息。
混用 returnyield 在同一生成器裡同時使用 return(返回值)與 yield(產出值)會讓閱讀者困惑。 若需要返回值,建議把返回邏輯放在最外層生成器,或使用 yield from 取得子生成器的返回值。

最佳實踐

  1. 以「管線」思維設計:將資料處理拆成小的生成器,每個生成器只負責單一職責,最外層使用 yield from 組合。
  2. 明確捕獲子生成器的返回值result = yield from sub() 能讓你在外層直接取得子生成器的結束狀態或計算結果。
  3. 使用 try/finally 保證資源釋放:在子生成器內部加入 try: ... finally: clean_up()yield from 會把 close() 正確傳遞下去。
  4. 避免在 yield from 前後混雜額外的 yield:若需要在兩段子生成器之間插入額外的值,請先結束前一段的 yield from,再寫新的 yieldyield from
  5. 寫測試:因為 yield from 牽涉到多層協程的控制流,單元測試(assert list(gen) == expected)能快速捕捉錯誤。

實際應用場景

場景 為什麼適合使用 yield from
大檔案逐行處理(如 log、CSV) 把檔案讀取、過濾、轉換三個步驟分別寫成生成器,外層只需 yield from filter_gen,記憶體佔用僅為單行大小。
網路串流(WebSocket、Kafka) 把接收、解碼、業務邏輯各自抽成生成器,yield from 讓訊號(如 close)自動向下傳遞,簡化錯誤處理。
協程框架(asyncio) 的底層實作 asyncioTask 內部使用 yield from 把子協程的結果傳回,理解它有助於自行撰寫自訂協程。
資料管線(ETL) ExtractTransformLoad 拆成三個生成器,使用 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 生成器家族中極具威力的語法糖,它不只是簡化迭代,更提供了 代理控制訊號取得子生成器返回值 以及 構建可組合管線 的能力。掌握它之後,我們可以:

  1. 用更少的程式碼寫出 多層迭代協程
  2. 資料流大檔案處理網路串流 等需要 懶評估 的場景中,保持低記憶體佔用且易於維護。
  3. pipeline 思維把複雜的業務邏輯拆解成單一職責的生成器,提升程式的可讀性與可測試性。

最後,建議在實務開發中 先從簡單的 yield from 替代 for … yield 開始,逐步探索 sendthrowreturn 的進階用法,並結合單元測試確保行為正確。如此一來,你的 Python 程式碼將能在效能、可維護性與可擴充性上,同時得到顯著提升。祝你玩得開心,寫出更優雅的生成器!