裝飾器 是 Python 最具特色也最實用的語法糖之一,廣泛應用於日誌記錄、效能計時、權限驗證、快取、重試機制等橫切關注點。本文從閉包原理層層遞進,帶你徹底掌握裝飾器的設計與應用。
閉包讓內層函數能「記住」並存取外層函數的變數,即使外層函數已執行結束。
def outer(x):
y = [1, 2, 3] # 外層變數
def inner():
print(x) # 存取外層參數
print(y) # 存取外層區域變數
return inner # 回傳內層函數物件
x = 5 # 全域變數,不會被 inner 捕獲
f = outer(2) # outer 結束,但 inner 仍保有對 x=2, y=[1,2,3] 的引用
f() # 輸出:2 / [1, 2, 3]
# 檢視閉包捕獲的變數
print(f.__closure__) # (<cell at 0x...: int object at 0x...>, <cell at 0x...: list object at 0x...>)
關鍵點:inner 與其捕獲的環境變數(x、y)共同構成了閉包。外層函數結束後,這些變數不會被銷毀,因為 inner 仍持有引用。
裝飾器本質是一個可呼叫物件,接收一個函數,回傳一個新函數(通常包裝原函數並增加額外行為)。
def entrance(func):
def inner():
print(f"呼叫函數: {func.__name__}")
func() # 執行原函數
return inner
# 手動裝飾
def fun1():
print("fun1 執行中...")
fun1 = entrance(fun1) # 相當於 fun1 = inner
fun1() # 輸出:呼叫函數: fun1 / fun1 執行中...
@entrance
def fun2():
print("fun2 執行中...")
# 等價於 fun2 = entrance(fun2)
fun2()
基本版裝飾器無法處理帶參數的函數,需使用 *args、**kwargs:
def entrance(func):
def inner(*args, **kwargs):
print(f"呼叫函數: {func.__name__}, 參數: {args}, {kwargs}")
return func(*args, **kwargs) # 透傳參數並回傳結果
return inner
@entrance
def add(a, b):
return a + b
result = add(3, 5)
# 輸出:呼叫函數: add, 參數: (3, 5), {}
# result = 8
記錄函數呼叫名稱、參數、回傳值、執行時間:
import time
import functools
def logger(func):
@functools.wraps(func) # 保留原函數元資料(__name__, __doc__ 等)
def inner(*args, **kwargs):
start = time.perf_counter()
print(f"[LOG] {func.__name__} 開始執行 | 參數: {args} {kwargs}")
result = func(*args, **kwargs) # 執行原函數
elapsed = time.perf_counter() - start
print(f"[LOG] {func.__name__} 結束 | 耗時: {elapsed:.4f}s | 回傳: {result}")
return result
return inner
# 使用範例
@logger
def calculate_tax(price, rate=0.05):
"""計算含稅價格"""
time.sleep(0.1) # 模擬運算
return round(price * (1 + rate), 2)
@logger
def greet(name, *, formal=False):
prefix = "敬愛的" if formal else "哈囉"
return f"{prefix} {name}"
# 測試
print(calculate_tax(1000))
print(greet("小明", formal=True))
[LOG] calculate_tax 開始執行 | 參數: (1000,) {'rate': 0.05}
[LOG] calculate_tax 結束 | 耗時: 0.1003s | 回傳: 1050.0
1050.0
[LOG] greet 開始執行 | 參數: ('小明',) {'formal': True}
[LOG] greet 結束 | 耗時: 0.0001s | 回傳: 敬愛的 小明
敬愛的 小明
def retry(max_attempts=3, delay=1):
def decorator(func):
@functools.wraps(func)
def inner(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
raise
print(f"第 {attempt} 次失敗: {e}, {delay}s 後重試...")
time.sleep(delay)
return inner
return decorator
@retry(max_attempts=3, delay=0.5)
def unstable_api():
import random
if random.random() < 0.7:
raise ConnectionError("網路不穩")
return "成功"
print(unstable_api())
class Timer:
def __init__(self, func):
self.func = func
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
start = time.perf_counter()
result = self.func(*args, **kwargs)
print(f"{self.func.__name__} 耗時: {time.perf_counter() - start:.4f}s")
return result
@Timer
def slow():
time.sleep(0.5)
使用 functools.wraps 可保留 __name__、__doc__、__annotations__ 等屬性,讓 help()、IDE 提示、序列化正常運作。
| 陷阱 | 解決方案 |
|---|---|
忘記 @functools.wraps | 永遠加上,避免除錯、文檔、序列化異常 |
| 裝飾器順序錯誤 | @a @b def f() 等價 a(b(f)),由內而外執行 |
| 修改可變預設參數 | 勿在裝飾器內修改 **kwargs 直接引用的字典 |
| 非同步函數裝飾 | 需定義 async def inner 並 await func() |
| 過度使用導致堆疊過深 | 合併邏輯相關的裝飾器,或用中介軟體模式 |
@property、@classmethod、@staticmethod:類別方法轉換@functools.lru_cache:記憶化快取@dataclass:資料類別自動生成 __init__ 等@contextlib.contextmanager:產生器變上下文管理器@app.route、FastAPI @app.get:路由註冊@task、Dramatiq @actor:非同步任務裝飾器 = 閉包 + 高階函數 + 語法糖。掌握 *args/**kwargs 透傳、functools.wraps 保留元資料、帶參數裝飾器工廠模式,即可靈活處理日誌、快取、重試、計時、權限等橫切關注點,寫出乾淨、可復用、符合單一職責原則的 Python 程式碼。