本文 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. yield 與 return 的差別

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 物件而非結果。 立即使用 for、list() 或 next() 取得值。
在 try/finally 中忘記關閉檔案 使用 yield 產生檔案行時,如果在迭代途中拋出例外,檔案可能不會關閉。 使用 with 語句包住檔案,或在生成器裡加入 try...finally 釋放資源。
產生無限迭代卻未加限制 無限生成器若被不慎放入 list() 會導致記憶體耗盡。 永遠在外層加上迭代限制(如 itertools.islice、計數變數)。
在生成器內部修改外部可變物件 產生器會保持對外部變數的引用,若外部變數被改變,生成器的行為可能難以預測。 盡量在生成器內部自行管理狀態,或使用 copy.deepcopy 以避免副作用。
使用 yield 於多執行緒環境 生成器本身不是執行緒安全的,同時多執行緒存取同一生成器會產生競爭條件。 使用 queue.Queue 或 asyncio 取代多執行緒共享生成器。

最佳實踐小結

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

實際應用場景

場景 為何適合使用生成器
大數據 ETL(Extract‑Transform‑Load) 逐筆讀取、清理、寫入,避免一次載入全部資料。
網路爬蟲(Streaming Scraping) 逐頁抓取、即時解析,減少記憶體佔用,並可在抓取過程中即時中斷。
即時日志分析 以生成器流式讀取 log 檔,配合正則表達式過濾,實現「邊讀邊處理」。
機器學習資料前處理 產生批次(batch)資料給模型訓練,支援 epoch 重複迭代而不重複載入全部樣本。
協程與非阻塞 I/O(asyncio) 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 中更靈活地控制資料流向,寫出 既省資源又易擴充 的程式。祝你在寫程式的路上,玩得開心、寫得漂亮! 🚀