裝飾器是 Python 最具表達力的特性之一,實現了橫切關注點(日誌、計時、事務、權限等)與業務邏輯的分離。本文從語法糖本質講起,逐層遞進至帶參裝飾器、類裝飾器、多層嵌套、functools.wraps 保護元數據等進階主題,並給出生產級最佳實踐模式。
@decorator
def func(): ...
# 等價於
func = decorator(func)
decorator 是一個可調用對象,接收原函數,返回新函數(包裝器)。
def simple_decorator(func):
def wrapper():
print("調用前")
func()
print("調用後")
return wrapper
@simple_decorator
def greet():
print("Hello")
greet()
# 輸出:
# 調用前
# Hello
# 調用後
functools.wraps直接包裝會導致 __name__、__doc__、__annotations__ 丟失,影響調試與自動化工具。
from functools import wraps
def logged(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[LOG] 調用 {func.__name__}")
return func(*args, **kwargs)
return wrapper
*args, **kwargsdef universal_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"參數: {args}, {kwargs}")
return func(*args, **kwargs)
return wrapper
@universal_decorator
def add(a, b=10):
return a + b
add(1) # 參數: (1,), {}
add(2, b=20) # 參數: (2,), {'b': 20}
def repeat(times):
"""返回一個裝飾器,使被裝飾函數執行 times 次"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
results = []
for _ in range(times):
results.append(func(*args, **kwargs))
return results
return wrapper
return decorator
@repeat(3)
def say_hi(name):
return f"Hi {name}"
print(say_hi("Alice")) # ['Hi Alice', 'Hi Alice', 'Hi Alice']
調用鏈:@repeat(3) → repeat(3) 返回 decorator → decorator(say_hi) 返回 wrapper。
__call__ 與上下文管理class Timer:
def __init__(self, label=""):
self.label = label
self.start = 0
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
import time
self.start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - self.start
print(f"[{self.label}] {func.__name__} 耗時 {elapsed:.4f}s")
return result
return wrapper
# 額外支援 with 語句
def __enter__(self):
import time
self.start = time.perf_counter()
return self
def __exit__(self, *exc):
import time
print(f"[{self.label}] 區塊耗時 {time.perf_counter() - self.start:.4f}s")
# 作為函數裝飾器
@Timer("API")
def fetch_data():
pass
# 作為上下文管理器
with Timer("DB"):
pass
@a
@b
@c
def f(): ...
# 等價於
f = a(b(c(f)))
# 調用順序:a.wrapper → b.wrapper → c.wrapper → f
import threading
from functools import wraps
def synchronized(lock):
"""參數化裝飾器:接受任意實現 acquire/release 的鎖對象"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
lock.acquire()
try:
return func(*args, **kwargs)
finally:
lock.release()
return wrapper
return decorator
my_lock = threading.RLock()
class Service:
@synchronized(my_lock)
def critical_section(self):
pass
class MyClass:
@classmethod
@logged # 先應用 logged,再應用 classmethod
def foo(cls):
pass
@staticmethod
@logged # 同理
def bar():
pass
@property
@logged # property 必須在最外層
def baz(self):
return 1
順序原則:離函數定義最近的裝飾器最先執行。
| 陷阱 | 解決方案 |
|---|---|
忘記 @wraps 導致元數據丟失 | 始終在 wrapper 上使用 @wraps(func) |
| 裝飾器改變函數簽名,破壞類型提示 | 使用 typing.ParamSpec + Concatenate (Python 3.10+) 保留簽名 |
類裝飾器未正確處理 self/cls | wrapper 使用 *args, **kwargs 透傳,不假設參數結構 |
| 多線程下共享狀態導致競態 | 裝飾器內部狀態用 threading.local 或鎖保護 |
| 遞歸函數被裝飾導致重複包裝 | 用 functools.lru_cache 或檢測 func.__wrapped__ 避免 |
@property、@classmethod、@staticmethod@functools.lru_cache(maxsize=128):記憶化緩存@functools.singledispatch:單分發泛函數@contextlib.contextmanager:生成器變上下文管理器@dataclasses.dataclass:自動生成 __init__ 等@typing.overload:類型提示重載裝飾器是「函數接收函數,返回函數」的語法糖,核心價值在於關注點分離與代碼復用。掌握三層嵌套模板、wraps 保護、類裝飾器靈活性,即可輕鬆實現日誌、計時、重試、限流、事務、權限驗證等通用邏輯,讓業務代碼保持純粹。