Python 課程 – 迭代與生成器(Iteration & Generators)
主題:生成器表達式(Generator Expressions)
簡介
在 Python 中,迭代是處理大量資料時最常見的手法。傳統的 list、tuple、dict 等容器在建立時會一次把所有元素全部放進記憶體,當資料量龐大時很容易造成記憶體不足。
為了解決這個問題,Python 引入了 生成器(generator):它在需要時才「產生」下一個值,具備 懶評估(lazy evaluation) 的特性。
雖然我們可以透過 def + yield 來手寫生成器函式,但在日常開發中,最常使用的仍是 生成器表達式(generator expression)——一種簡潔、可讀性高且效能優異的語法糖。掌握生成器表達式,能讓你在資料流處理、檔案讀寫、資料庫查詢等情境下,寫出既省記憶體又易維護的程式碼。
核心概念
什麼是生成器表達式?
生成器表達式的語法與列表推導式(list comprehension)非常相似,唯一的差別是 外層使用圓括號 () 而非方括號 []。
gen = (x * x for x in range(10))
上述程式會回傳一個 generator 物件,此物件本身不會立即計算 x * x,而是等到你真正迭代(例如使用 next()、for 迴圈、或傳給 sum())時才逐一產生結果。
重點:生成器表達式是 一次性 的資料流。當所有元素都被取走後,該 generator 便耗盡,無法再次使用,除非重新建立。
為什麼要使用生成器表達式?
| 項目 | 列表推導式 | 生成器表達式 |
|---|---|---|
| 記憶體使用 | 需要一次性把所有結果存入記憶體 | 只在需要時產生單一元素 |
| 執行速度 | 建立時即完成所有計算 | 取值時才計算,對於大型資料可減少不必要的運算 |
| 可迭代性 | 可直接使用 for、list()、sum() 等 |
同樣支援所有可迭代介面,但只能遍歷一次 |
基本語法結構
(expression for item in iterable if condition)
- expression:要產生的值(可以是任意 Python 表達式)
- item:迭代變數
- iterable:任何可迭代物件(list、tuple、dict、set、range、檔案物件等)
- if condition(可選):過濾條件,只保留符合條件的元素
程式碼範例
下面提供 5 個實用範例,從最簡單的概念到稍微進階的應用,並加上詳細註解說明。
1️⃣ 基礎平方數生成器
# 產生 0~9 的平方數,使用 generator expression
squares = (x * x for x in range(10))
# 逐一取值並印出
for s in squares:
print(s)
說明:range(10) 會產生 0~9 的整數,x * x 為每個數字的平方。迴圈每次執行時才計算下一個平方值。
2️⃣ 結合 if 條件過濾偶數的立方
# 只保留偶數,並計算其立方
even_cubes = (n ** 3 for n in range(20) if n % 2 == 0)
# 使用 sum() 直接求總和(sum 會自動迭代 generator)
total = sum(even_cubes)
print(f"0~19 偶數立方的總和 = {total}")
說明:if n % 2 == 0 只讓偶數通過,sum() 不會一次把所有立方值放進列表,而是逐一取值累加,記憶體需求極低。
3️⃣ 讀取大型文字檔並即時過濾關鍵字
# 假設有一個巨大的日誌檔案 logs.txt
def keyword_lines(filepath, keyword):
# 以 generator 方式逐行讀取,避免一次讀入整個檔案
return (line.rstrip() for line in open(filepath, encoding='utf-8')
if keyword in line)
# 只列出包含 "ERROR" 的行
for err in keyword_lines('logs.txt', 'ERROR'):
print(err)
說明:open() 回傳的檔案物件本身就是可迭代的,每次迭代只讀取一行。配合 generator expression 可以在不佔用大量記憶體的前提下即時過濾。
4️⃣ 結合多層迭代:笛卡爾積的懶生成
# 產生兩個集合的笛卡爾積 (a, b)
A = [1, 2, 3]
B = ['a', 'b', 'c']
cartesian = ((a, b) for a in A for b in B)
# 只取前 5 個組合示範
from itertools import islice
first_five = list(islice(cartesian, 5))
print(first_five) # [(1, 'a'), (1, 'b'), (1, 'c'), (2, 'a'), (2, 'b')]
說明:多層 for 可直接寫在 generator expression 中,產生的結果同樣是懶評估。itertools.islice 讓我們只取前幾個元素,避免一次產生全部組合。
5️⃣ 與 map、filter 混用:更彈性的資料管線
# 假設有一串原始資料
data = [5, 12, 7, 3, 20, 15]
# 先 filter 出大於 10 的,再 map 成字串,最後用 generator 表達式串接
processed = (str(x) for x in filter(lambda v: v > 10, data))
for item in processed:
print(item) # 12 20 15
說明:filter 和 map 本身已返回 generator(在 Python 3 以上),再套上一層 generator expression 可以把整個流程寫得更直觀且可讀性高。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| Generator 被重複使用 | 生成器只能迭代一次,第二次會直接結束,常見於把 generator 直接傳給 list() 再使用的情況。 |
若需要多次使用,重新建立 generator,或先轉成 list(僅在資料量允許的情況下)。 |
| 忘記關閉檔案 | 使用 open() 產生的檔案物件若未使用 with,在迭代完畢前可能不會關閉檔案。 |
使用 with open(...) as f: 包裹,或在 generator 裡自行 yield 後 finally 關閉。 |
| 過度嵌套 | 多層 for、if 會讓表達式變得難以閱讀。 |
當邏輯變複雜時,改寫成普通函式或生成器函式,保持可讀性。 |
| 忘記轉型 | 某些 API 只接受 序列(如 json.dump),直接傳入 generator 會出錯。 |
依需求 使用 list(gen) 或 tuple(gen) 轉型。 |
使用 sum() 時的精度問題 |
sum() 直接對浮點數 generator 求和,可能產生累積誤差。 |
若需要高精度,考慮使用 math.fsum() 或 decimal.Decimal。 |
最佳實踐
- 保持單一職責:生成器表達式應只負責「產生」資料,不要在裡面寫過多的副作用(如 I/O)。
- 使用
with管理資源:尤其是檔案、網路連線等,需要在 generator 結束時正確釋放。 - 適時使用
itertools:itertools.chain,islice,takewhile等工具可以與 generator 無縫結合,提升表達力。 - 明確命名:即使是匿名的 generator,也建議把它指派給具描述性的變數名稱,提升程式可讀性。
- 測試邊界情況:特別是空的 iterable、只有一筆資料的情況,確保程式不會因為
StopIteration而異常。
實際應用場景
| 場景 | 為何選擇生成器表達式 |
|---|---|
| 大資料檔案的逐行分析(如日誌、CSV) | 只佔用一行的記憶體,配合 if 條件即可即時過濾。 |
| 串流資料處理(Websocket、Kafka) | 資料來源是無限流,生成器能持續產生而不會阻塞記憶體。 |
| 計算統計指標(平均值、標準差) | 使用 sum()、statistics.mean() 等接受 iterable 的函式,直接把 generator 傳入,避免額外的列表建立。 |
| 組合演算法(笛卡爾積、排列組合) | 多層 for 直接寫在 expression 中,配合 itertools.islice 可只取部分結果,適合「先看一小部份」的需求。 |
| 資料庫查詢結果的分批處理 | 透過 DB‑API 的 cursor 迭代器與 generator 結合,實現「一次取一筆」的記憶體友好方式。 |
範例:假設我們要從資料庫一次讀取 10,000 筆訂單,並只把金額大於 1,000 的訂單寫入另一個檔案。
import sqlite3 conn = sqlite3.connect('shop.db') cur = conn.cursor() cur.execute('SELECT order_id, amount FROM orders') # 使用 generator expression 濾過金額 large_orders = (f"{oid},{amt}\n" for oid, amt in cur if amt > 1000) with open('large_orders.csv', 'w', encoding='utf-8') as f: f.writelines(large_orders) # writelines 會逐行寫入,無需一次載入全部資料這段程式只在需要時才把符合條件的列寫入檔案,記憶體使用量基本維持在 O(1)。
總結
生成器表達式是 Python 中 懶評估 的核心工具,讓我們在處理大量或無限資料時,能以 低記憶體、高效能 的方式撰寫清晰的程式碼。
- 語法與列表推導式相似,只是外層使用
()。 - 支援
if條件、 多層for、以及與itertools、map、filter等函式的無縫結合。 - 常見陷阱包括一次性迭代、資源未正確釋放以及過度嵌套,遵守最佳實踐即可避免。
在日常開發中,無論是 檔案流處理、資料庫分批讀寫,或是 演算法的組合產生,只要把「一次產生」的概念套用到 generator expression,就能顯著降低程式的記憶體 footprint,提升可擴展性。希望本篇文章能幫助你快速上手,並在實務專案中活用這項強大的語法特性。祝開發順利! 🚀