Python – 迭代與生成器
單元:迭代與生成器(Iteration & Generators)
主題:for 迴圈的可迭代物件機制
簡介
在 Python 中,for 迴圈是最常用的控制流程之一。它之所以能夠遍歷 列表、字典、集合、字串,甚至自訂的資料結構,全部仰賴「可迭代物件(iterable)」的概念。了解 for 迴圈背後的可迭代機制,不只能寫出更簡潔的程式碼,還能在需要大量資料處理、串流 (stream) 或記憶體受限的環境中,靈活運用 生成器(generator) 來提升效能。
本篇文章將從 可迭代協定(iteration protocol)、迭代器(iterator)、生成器 三個層面,逐步說明 for 迴圈是如何取得資料、如何停止迭代,以及在實務開發中常見的陷阱與最佳實踐。
核心概念
1. 什麼是可迭代物件?
在 Python 中,只要一個物件實作了 __iter__() 方法,或同時實作了 __getitem__()(且索引從 0 開始),就被視為 可迭代。for 迴圈在執行時會先呼叫 iter(obj),這個內建函式會:
- 嘗試呼叫
obj.__iter__(),若成功則回傳一個 迭代器(iterator)物件。 - 若
__iter__不存在,則改用舊式的序列協定:呼叫obj.__getitem__(0), obj.__getitem__(1) …,直到拋出IndexError為止。
迭代器本身必須實作兩個方法:
| 方法 | 目的 |
|---|---|
__iter__() |
回傳自身,讓迭代器本身也是可迭代的 |
__next__() |
產生下一個元素,若沒有更多元素則拋出 StopIteration |
重點:
for迴圈不直接操作資料容器,而是透過 迭代器協定 把資料「逐一」取出。
2. for 迴圈的執行流程
以下以簡化的流程圖說明 for 迴圈的內部機制:
for target in iterable:
body
iterable→iter(iterable)→ 取得 iterator。- 呼叫
next(iterator)取得第一個值,賦給target。 - 執行
body。 - 重複第 2 步,直到
next()拋出StopIteration。
這個機制使得 for 迴圈可以 懶惰求值(lazy evaluation),只在需要時才產生下一個元素,對於大資料集或無限序列特別有用。
3. 常見的可迭代類型
| 類型 | 是否可迭代 | 取得方式 |
|---|---|---|
list、tuple、set |
✅ | iter(obj) 直接返回內建迭代器 |
dict |
✅ | 預設迭代鍵 (for k in d:),也可 d.items()、d.values() |
str |
✅ | 逐字元迭代 |
range |
✅ | 產生整數序列,記憶體友好 |
| 自訂類別 | ✅(需實作 __iter__ 或 __getitem__) |
依需求返回自訂迭代器 |
file 物件 |
✅ | 逐行讀取,實作了迭代器協定 |
4. 程式碼範例
下面提供 5 個實用範例,示範不同可迭代物件與自訂迭代器的使用方式。每段程式碼皆附上說明註解,方便初學者快速掌握概念。
範例 1:遍歷字典的鍵、值、項目
# 直接遍歷鍵 (預設行為)
person = {"name": "Alice", "age": 30, "city": "Taipei"}
for key in person:
print(f"鍵: {key}") # name, age, city
# 同時取得鍵與值
for key, value in person.items():
print(f"{key} => {value}")
技巧:若只需要值,使用
person.values();若要同時取得鍵和值,person.items()最直觀。
範例 2:使用 range 產生大數列,配合 for 懶惰求值
# 產生 0~999_999 的整數序列,且不會一次佔用全部記憶體
for i in range(1_000_000):
if i % 200_000 == 0:
print(f"已處理 {i} 筆資料")
說明:
range物件本身就是一個 惰性序列,只在next()被呼叫時才計算下一個值。
範例 3:自訂可迭代類別(實作 __iter__)
class Countdown:
"""倒數計時器,從 start 迭代到 0"""
def __init__(self, start: int):
self.start = start
def __iter__(self):
# 回傳一個迭代器物件,本例直接回傳自身
self.current = self.start
return self
def __next__(self):
if self.current < 0:
raise StopIteration
value = self.current
self.current -= 1
return value
# 使用方式
for n in Countdown(5):
print(n, end=' ') # 5 4 3 2 1 0
重點:
__iter__必須回傳 迭代器本身(或其他迭代器),而__next__必須在結束時拋出StopIteration。
範例 4:生成器函式(generator function)簡化迭代器實作
def fibonacci(limit: int):
"""產生前 limit 個 Fibonacci 數字"""
a, b = 0, 1
for _ in range(limit):
yield a # 產生一個值,暫停函式狀態
a, b = b, a + b
# 直接在 for 迴圈中使用
for num in fibonacci(10):
print(num, end=' ') # 0 1 1 2 3 5 8 13 21 34
優點:利用
yield,Python 會自動為我們建立符合迭代器協定的物件,省去手寫__iter__/__next__的繁瑣。
範例 5:檔案逐行讀取(避免一次載入整個檔案)
# 假設 data.txt 含有大量文字資料
with open('data.txt', encoding='utf-8') as f:
for line_number, line in enumerate(f, start=1):
# 只處理包含關鍵字的行
if 'Error' in line:
print(f"[{line_number}] {line.strip()}")
說明:
file物件本身即為可迭代物件,for迴圈會一次讀取一行,極大降低記憶體使用。
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 建議的解決方案 |
|---|---|---|
直接修改迭代中的容器(如在 for 內 list.append()) |
迭代次數不如預期,甚至拋出 RuntimeError |
若需修改,先 建立副本(for x in lst[:])或改用 列表推導式 |
忘記 StopIteration 於自訂迭代器 |
迴圈永遠不會結束,導致程式卡住 | 必須在 __next__ 中正確拋出 StopIteration |
使用 range 產生巨大列表(如 list(range(10**9))) |
記憶體瞬間爆炸 | 直接使用 range 本身或改用 生成器表達式:(i for i in range(10**9)) |
在生成器內部捕捉 StopIteration |
會使外層 for 無法正確終止 |
只在最外層捕捉,或使用 try/except 包住 next() 呼叫 |
| 檔案未關閉 | 檔案資源泄漏,Windows 上可能鎖檔 | 使用 with 語句 確保自動關閉 |
最佳實踐小結
- 盡量使用內建的可迭代物件(
list,dict,range,file),它們已最佳化。 - 需要大量資料或無限序列時,優先考慮生成器(
yield或生成器表達式)。 - 保持迭代過程的純粹性:不要在迭代中改變容器結構,若必須改變,請使用副本或分段處理。
- 使用
enumerate、zip、itertools等工具,可讓多序列同步迭代更簡潔且安全。
實際應用場景
| 場景 | 為何使用 for + 可迭代機制 |
範例簡述 |
|---|---|---|
| 資料清理(CSV、Log) | 逐行讀取,避免一次載入整個檔案 | for line in open('log.txt'): ... |
| 批次 API 呼叫 | 產生請求參數的序列,使用 range 或生成器控制速率 |
for page in range(1, 101): fetch(page) |
| 大數據分析 | 以 itertools.islice 配合生成器分批處理 |
for chunk in itertools.islice(data_gen, 0, 1000): ... |
| 演算法實作(如 BFS、DFS) | 使用 deque 或 list 作為可迭代的工作隊列 |
for node in queue: |
| 即時串流處理(WebSocket、Kafka) | 產生無限資料流,使用 while True: yield |
def stream(): while True: yield get_message() |
總結
for迴圈之所以能遍歷各式各樣的資料結構,核心在於 可迭代協定:只要物件實作__iter__(或舊式__getitem__),就能交給iter()產生迭代器。- 迭代器本身必須提供
__next__,並在結束時拋出StopIteration。 - 生成器 是建立迭代器的最簡潔方式,
yield讓函式在每次產生值後暫停,實現「懶惰求值」。 - 在實務開發中,正確使用可迭代物件能 降低記憶體佔用、提升程式可讀性,同時避免常見的迭代陷阱(如在迭代中修改容器)。
掌握了 for 迴圈的可迭代機制後,無論是處理小型清單還是大規模資料流,都能以 簡潔、可靠 的方式撰寫 Python 程式。祝你在未來的專案中玩得開心、寫得更好!