Python Generator 與 Decorator

Python Generator 與 Decorator

這篇教學一次處理兩個讓 Python 程式碼「看起來很高級」的特性:Generator(產生器)用 yield 一次吐一筆資料、不佔記憶體;Decorator(裝飾器)用 @ 幫既有函式加功能、不動原始碼。兩者都是面試常客。

Generator:用 yield 一次吐一筆

函式裡用 yield 取代 return,它就變成產生器——呼叫時不會立刻執行,而是隨著 for 迴圈「要一筆、算一筆」:

def countdown(n):
    print("開始倒數")
    while n > 0:
        yield n        # 吐出一個值,暫停在這裡
        n -= 1

for i in countdown(3):
    print(i)

執行輸出:

開始倒數
3
2
1

注意「開始倒數」直到 for 開始要資料才印出——這就是惰性求值(lazy evaluation)。

為什麼需要 Generator:記憶體

比較兩種「產生一百萬個數字」的方式:

import sys

big_list = [i for i in range(1_000_000)]        # List:全部放進記憶體
big_gen = (i for i in range(1_000_000))         # Generator:要時才算

print(sys.getsizeof(big_list), "bytes")
print(sys.getsizeof(big_gen), "bytes")

執行輸出(數值依環境略有差異):

8448728 bytes
104 bytes

小括號的 (i for i in ...) 是 generator 運算式,長相跟 List Comprehension 一樣只差括號。處理大檔案逐行讀取(for line in f)正是同一個精神。

Decorator:用 @ 幫函式加功能

裝飾器是「吃一個函式、回傳加工版函式」的函式。經典範例:幫任何函式加上執行計時:

import time
import functools

def timer(func):
    @functools.wraps(func)                  # 保留原函式的名稱與說明
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)      # 執行原函式
        print(f"{func.__name__} 花了 {time.time() - start:.4f} 秒")
        return result
    return wrapper

@timer                                      # 等同 slow_add = timer(slow_add)
def slow_add(a, b):
    time.sleep(0.1)
    return a + b

print(slow_add(3, 5))

執行輸出(秒數依環境略有差異):

slow_add 花了 0.1001 秒
8

拆解:@timerslow_add 換成 wrapperwrapper 用第 20 篇的 *args, **kwargs 原封不動轉傳參數,所以任何函式都能被裝飾。

你早就用過的裝飾器

裝飾器出處
@dataclass第 30 篇,自動生成類別方法
@functools.wraps本篇,寫裝飾器的標配
@app.route("/")Flask/FastAPI 路由(網頁框架)
@pytest.fixturepytest 測試框架

讀懂「裝飾器=函式的加工器」,這些框架語法就不再神祕。

常見錯誤

1. Generator 用過一次就空了

產生器是「一次性」的,跑完第二次 for 什麼都拿不到。需要重複使用就重新呼叫產生器函式,或一開始就轉成 list()

2. 裝飾器忘了 return wrapper(或 return result)

timer 忘了 return wrapper:函式直接變 None;wrapper 忘了 return result:被裝飾的函式永遠回傳 None。兩個 return 都要在。

3. 沒加 @functools.wraps

被裝飾函式的 __name__、docstring 會變成 wrapper 的,除錯與文件工具都會錯亂。寫裝飾器時習慣性加上。

總結

Generator:yield 一次吐一筆、惰性求值省記憶體、一次性用完即空;Decorator:@ 語法把函式換成加工版、靠 *args/**kwargs 轉傳、functools.wraps 必加。第四階段到此完成——下一階段進入實戰應用,從 Python 操作 SQLite 資料庫開始,與本站 SQL 教學系列正式會合。

延伸閱讀