本文 AI 產出,尚未審核

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

主題:生成器函式(yield


簡介

在日常的 Python 開發中,我們常會需要 遍歷大量資料延遲計算,或是 建立無窮序列。如果直接把所有結果一次性產生並存入記憶體,不僅會浪費資源,還可能因為資料量過大而導致程式崩潰。
yield 正是為了 在需要時才產生資料保持最小的記憶體佔用 而設計的關鍵語法。它讓函式變成 生成器(generator),在迭代過程中「暫停」與「恢復」執行,提供一個 懶惰(lazy) 的資料流。

掌握 yield 不僅能寫出更高效的程式,還能讓你在 資料處理、串流、協程 等場景中得心應手。以下的教學會一步步帶你從概念到實作,並說明常見的陷阱與最佳實踐,幫助你在實務上快速上手。


核心概念

1. 什麼是生成器函式?

當一個函式內部使用了 yield,Python 會把它視為 生成器函式。呼叫此函式不會直接執行程式碼,而是回傳一個 生成器物件(generator object)。只有在對這個物件進行迭代(如 for 迴圈、next())時,函式內部才會逐步執行,遇到 yield 時暫停,並把當前的值「產出」給呼叫端。

def countdown(n):
    """從 n 倒數到 0,每次產出一個整數"""
    while n >= 0:
        yield n      # 暫停,回傳 n
        n -= 1       # 恢復後繼續執行

重點yield 會把「目前的計算結果」交給外部,同時保留函式的執行狀態(包括局部變數、程式計數器等),下次迭代時從暫停的地方繼續。


2. yieldreturn 的差別

return yield
回傳方式 結束函式,回傳單一值 暫停函式,回傳一個序列中的值
執行狀態 完全結束,所有局部變數釋放 保留狀態,下一次可從中斷點繼續
使用情境 需要一次性得到結果 需要逐步產出大量或無限資料

3. 生成器的「懶惰」特性

生成器只在 需要 時才計算下一個值,這意味著:

  • 記憶體友好:不必一次性把所有資料放入列表或陣列。
  • 可處理無限序列:例如斐波那契數列、自然數等,理論上可以永遠產出。
  • 支援管線(pipeline):多個生成器可以串接,形成資料流的「管道」處理。

程式碼範例

以下提供 5 個實用範例,從基礎到進階,說明 yield 在不同情境下的應用。

範例 1:簡易的範圍生成器(取代 range

def my_range(start, stop, step=1):
    """自訂版的 range,使用 yield 實作"""
    i = start
    while i < stop:
        yield i
        i += step

# 使用方式
for num in my_range(0, 5):
    print(num)   # 0 1 2 3 4

說明my_range 像內建的 range,但可以在迭代過程中加入額外的邏輯(例如日誌、條件過濾)。


範例 2:讀取大型文字檔的逐行生成器

def read_large_file(filepath, encoding='utf-8'):
    """一次只讀取一行,適合處理 GB 級別的檔案"""
    with open(filepath, 'r', encoding=encoding) as f:
        for line in f:
            yield line.rstrip('\n')   # 去除換行符號

# 範例:統計檔案中出現次數最多的單字
from collections import Counter

counter = Counter()
for line in read_large_file('big_data.txt'):
    counter.update(line.split())

print(counter.most_common(10))

說明:使用 yield 能避免一次性把整個檔案載入記憶體,對大檔案的資料分析尤為重要。


範例 3:無限斐波那契序列

def fibonacci():
    """產生無限的斐波那契數列"""
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# 只取前 10 個
import itertools
for num in itertools.islice(fibonacci(), 10):
    print(num)   # 0 1 1 2 3 5 8 13 21 34

說明while True 配合 yield 可建立 無窮生成器,使用 itertools.islice 或自行的計數控制取樣。


範例 4:生成器管線(Pipeline)— 文字清理 → 小寫 → 去除停用詞

STOP_WORDS = {"the", "is", "at", "which", "on"}

def clean_text(lines):
    for line in lines:
        # 移除標點符號
        cleaned = ''.join(ch for ch in line if ch.isalnum() or ch.isspace())
        yield cleaned

def to_lower(lines):
    for line in lines:
        yield line.lower()

def remove_stopwords(lines):
    for line in lines:
        words = [w for w in line.split() if w not in STOP_WORDS]
        yield ' '.join(words)

# 組合管線
raw = ["The quick, brown fox!", "Jumped over the lazy dog."]
pipeline = remove_stopwords(to_lower(clean_text(raw)))

for processed in pipeline:
    print(processed)
# 輸出:
# quick brown fox
# jumped over lazy dog

說明:每個函式都是一個 生成器,相互串接形成資料處理的流水線,保持 記憶體使用最低,且易於擴充。


範例 5:協程(Coroutine)簡易示範 – 使用 yield 接收資料

def accumulator():
    """累加器協程,使用 send() 接收數值並回傳累計結果"""
    total = 0
    while True:
        value = yield total      # 暫停並回傳 total,等待外部 send()
        if value is None:        # 收到 None 表示結束
            break
        total += value

# 使用方式
gen = accumulator()
next(gen)            # 啟動協程,取得第一次回傳的 0
print(gen.send(10)) # 10
print(gen.send(5))  # 15
print(gen.send(-2)) # 13
gen.close()         # 結束協程

說明yield 不僅能產出值,也能 接收外部傳入的資料(透過 send()),這是 Python 協程的基礎概念,對於非阻塞 I/O、事件驅動程式非常有用。


常見陷阱與最佳實踐

陷阱 說明 解決方案 / 最佳實踐
忘記 next() 或迭代 直接呼叫生成器函式卻沒有迭代,會得到一個 generator 物件而非結果。 立即使用 forlist()next() 取得值。
try/finally 中忘記關閉檔案 使用 yield 產生檔案行時,如果在迭代途中拋出例外,檔案可能不會關閉。 使用 with 語句包住檔案,或在生成器裡加入 try...finally 釋放資源。
產生無限迭代卻未加限制 無限生成器若被不慎放入 list() 會導致記憶體耗盡。 永遠在外層加上迭代限制(如 itertools.islice、計數變數)。
在生成器內部修改外部可變物件 產生器會保持對外部變數的引用,若外部變數被改變,生成器的行為可能難以預測。 盡量在生成器內部自行管理狀態,或使用 copy.deepcopy 以避免副作用。
使用 yield 於多執行緒環境 生成器本身不是執行緒安全的,同時多執行緒存取同一生成器會產生競爭條件。 使用 queue.Queueasyncio 取代多執行緒共享生成器。

最佳實踐小結

  1. 保持生成器單一職責:每個生成器只負責一件事(例如清理、過濾),方便組合與測試。
  2. 使用 with 管理資源:檔案、網路連線等外部資源應在生成器內部使用 with,確保即使迭代提前結束也能正確釋放。
  3. 避免在生成器內部做大量計算:若計算成本高,考慮將計算抽離,讓生成器只負責資料傳遞。
  4. 適時使用 itertools:Python 標準庫的 itertools 提供大量高效的生成器工具(如 chainislicefilterfalse),能減少自製代碼。
  5. 寫測試:生成器的懶惰特性可能隱藏錯誤,使用 list(gen)pytestlist 斷言可以驗證產出序列。

實際應用場景

場景 為何適合使用生成器
大數據 ETL(Extract‑Transform‑Load) 逐筆讀取、清理、寫入,避免一次載入全部資料。
網路爬蟲(Streaming Scraping) 逐頁抓取、即時解析,減少記憶體佔用,並可在抓取過程中即時中斷。
即時日志分析 以生成器流式讀取 log 檔,配合正則表達式過濾,實現「邊讀邊處理」。
機器學習資料前處理 產生批次(batch)資料給模型訓練,支援 epoch 重複迭代而不重複載入全部樣本。
協程與非阻塞 I/Oasyncio yield(在 Python 3.5+ 以前)或 await(現代)皆可實作事件驅動的資料流。
遊戲或動畫的幀生成 產生每一幀的狀態,允許在渲染過程中動態調整,而不必一次產生全部幀。

範例:在機器學習中,我們常用 yield 實作資料生成器,讓模型在每個 epoch 只載入必要的 batch:

def batch_generator(X, y, batch_size=32, shuffle=True):
    """Yield a batch of (X, y) each iteration"""
    n = len(X)
    indices = list(range(n))
    if shuffle:
        random.shuffle(indices)

    for start in range(0, n, batch_size):
        end = start + batch_size
        batch_idx = indices[start:end]
        yield X[batch_idx], y[batch_idx]

# Keras 訓練範例
model.fit(batch_generator(train_X, train_y), 
          steps_per_epoch=len(train_X)//32, 
          epochs=10)

總結

  • yield 讓函式變成 生成器,具備 懶惰產出保持執行狀態 的特性。
  • 相較於一次性返回完整資料,生成器在 記憶體使用、處理無限序列、建構資料管線 等方面有明顯優勢。
  • 透過 多個小型生成器的組合,可以打造高可讀、易維護且效能良好的資料流處理系統。
  • 在實務開發中,常見於 大檔案處理、ETL、機器學習批次生成、協程 等領域。
  • 注意 資源釋放、迭代限制、執行緒安全 等陷阱,遵循最佳實踐即可寫出既安全又高效的程式碼。

掌握 yield 後,你將能在 Python 中更靈活地控制資料流向,寫出 既省資源又易擴充 的程式。祝你在寫程式的路上,玩得開心、寫得漂亮! 🚀