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 取代多執行緒共享生成器。 |
最佳實踐小結
- 保持生成器單一職責:每個生成器只負責一件事(例如清理、過濾),方便組合與測試。
- 使用
with管理資源:檔案、網路連線等外部資源應在生成器內部使用with,確保即使迭代提前結束也能正確釋放。 - 避免在生成器內部做大量計算:若計算成本高,考慮將計算抽離,讓生成器只負責資料傳遞。
- 適時使用
itertools:Python 標準庫的itertools提供大量高效的生成器工具(如chain、islice、filterfalse),能減少自製代碼。 - 寫測試:生成器的懶惰特性可能隱藏錯誤,使用
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 中更靈活地控制資料流向,寫出 既省資源又易擴充 的程式。祝你在寫程式的路上,玩得開心、寫得漂亮! 🚀