本文 AI 產出,尚未審核
Python – 函式式編程(Functional Programming)
裝飾器再探(functools.wraps)
簡介
在 Python 中,裝飾器(decorator) 是一種非常好用的語法糖,讓我們可以在不改變原始函式本體的情況下,為函式「加上」額外的行為。
然而,在實作裝飾器的過程中,最常見的問題就是「函式的原始資訊(名稱、說明文件、簽名)會被隱藏」——這對除錯、文件產生、IDE 補全等都會造成困擾。
functools.wraps 正是為了解決這個問題而設計的,它會把 原始函式的元資料(__name__、__doc__、__module__、__annotations__ 等)正確地複製到包裝後的函式上。
本文將從概念說明、實作範例、常見陷阱與最佳實踐,一直到真實的應用情境,完整介紹 functools.wraps 的使用方式,讓你在寫裝飾器時既安全又好維護。
核心概念
1️⃣ 為什麼需要 functools.wraps?
普通的裝飾器會回傳一個 全新函式,而不是原本的函式本身。舉例:
def simple_decorator(func):
def wrapper(*args, **kwargs):
print("Before call")
result = func(*args, **kwargs)
print("After call")
return result
return wrapper
使用 @simple_decorator 包裝後的函式,__name__ 會變成 wrapper,__doc__ 會變成 None,這在:
- 除錯:堆疊追蹤會顯示
wrapper,不易辨識是哪一個原始函式。 - 文件生成:自動產生的 API 文件會失去說明文字。
- IDE 補全:簽名資訊遺失,開發者必須自行查閱原始程式碼。
functools.wraps 只需要一行裝飾,就能把這些資訊「搬回」包裝函式。
import functools
def better_decorator(func):
@functools.wraps(func) # ← 這行是關鍵
def wrapper(*args, **kwargs):
print("Before call")
return func(*args, **kwargs)
return wrapper
2️⃣ functools.wraps 的底層原理
functools.wraps 本身是一個 裝飾器工廠,它返回 functools.update_wrapper 的簡化版:
def wraps(wrapped,
assigned=WRAPPER_ASSIGNMENTS,
updated=WRAPPER_UPDATES):
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)
assigned:決定哪些屬性會被直接賦值(預設('__module__', '__name__', '__qualname__', '__doc__', '__annotations__'))。updated:決定哪些屬性會被更新(預設('__dict__',),即把原函式的__dict__合併到 wrapper 中)。
了解這些參數可以在特殊需求下自行調整(例如保留自訂屬性)。
3️⃣ 常見的裝飾器範例
以下提供 5 個實務上常見 且搭配 functools.wraps 的範例,每個範例都附有說明與執行結果示意。
3.1 記錄執行時間(Timing Decorator)
import time
import functools
def timing(func):
"""測量函式執行時間的裝飾器"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
value = func(*args, **kwargs)
end = time.perf_counter()
print(f"[Timing] {func.__name__} 執行了 {end - start:.4f}s")
return value
return wrapper
@timing
def compute(n: int) -> int:
"""簡單的累加運算"""
total = 0
for i in range(n):
total += i
return total
# 使用範例
compute(1_000_000)
輸出
[Timing] compute 執行了 0.0321s
functools.wraps讓compute.__doc__仍保留 「簡單的累加運算」,而不是wrapper的說明。
3.2 日誌記錄(Logging Decorator)
import logging
import functools
logging.basicConfig(level=logging.INFO)
def logger(level=logging.INFO):
"""依照指定等級記錄函式呼叫資訊"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
logging.log(level, f"呼叫 {func.__name__},參數: {args}, {kwargs}")
result = func(*args, **kwargs)
logging.log(level, f"{func.__name__} 回傳 {result!r}")
return result
return wrapper
return decorator
@logger(logging.DEBUG)
def greet(name: str, *, prefix="Hello"):
"""向使用者打招呼"""
return f"{prefix}, {name}!"
greet("Alice", prefix="Hi")
日誌
DEBUG:root:呼叫 greet,參數: ('Alice',), {'prefix': 'Hi'} DEBUG:root:greet 回傳 'Hi, Alice!'
- 透過
@functools.wraps,greet.__name__與greet.__doc__都不會被覆蓋。
3.3 快取(Caching)— lru_cache 的自訂封裝
import functools
def cached(maxsize=128):
"""自訂快取裝飾器,內部使用 lru_cache"""
def decorator(func):
@functools.wraps(func)
@functools.lru_cache(maxsize=maxsize)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
return decorator
@cached(maxsize=64)
def fib(n: int) -> int:
"""計算第 n 個 Fibonacci 數字(遞迴版)"""
if n < 2:
return n
return fib(n-1) + fib(n-2)
print(fib(35))
輸出
9227465
fib.__doc__仍然是 「計算第 n 個 Fibonacci 數字」,且functools.lru_cache已經被正確套用。
3.4 驗證參數(Argument Validation)
import functools
def validate_positive(func):
"""確保所有位置參數皆為正整數"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
for i, v in enumerate(args):
if isinstance(v, int) and v <= 0:
raise ValueError(f"第 {i+1} 個參數必須為正整數, got {v}")
return func(*args, **kwargs)
return wrapper
@validate_positive
def multiply(a: int, b: int) -> int:
"""回傳兩數相乘的結果"""
return a * b
# multiply(3, -5) # 會拋出 ValueError
multiply(4, 7) # 正常回傳 28
- 使用
@functools.wraps後,multiply.__signature__(由inspect.signature取得)仍保持原樣,方便在自動化測試或 API 文件產生時使用。
3.5 裝飾類別方法(Class Method Decorator)
import functools
def method_logger(func):
"""記錄類別方法的呼叫與回傳值"""
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
cls_name = self.__class__.__name__
print(f"[{cls_name}] 呼叫 {func.__name__},參數: {args}, {kwargs}")
result = func(self, *args, **kwargs)
print(f"[{cls_name}] {func.__name__} 回傳 {result!r}")
return result
return wrapper
class Calculator:
@method_logger
def add(self, x, y):
"""回傳 x + y"""
return x + y
c = Calculator()
c.add(5, 3)
輸出
[Calculator] 呼叫 add,參數: (5, 3), {} [Calculator] add 回傳 8
@functools.wraps讓Calculator.add.__doc__仍為 「回傳 x + y」,而不是wrapper。
常見陷阱與最佳實踐
| 陷阱 | 可能的結果 | 解決方式 |
|---|---|---|
忘記加 @functools.wraps |
__name__、__doc__、__module__ 皆被覆寫,除錯訊息不明確。 |
一定在 wrapper 函式上方寫 @functools.wraps(original_func)。 |
直接返回 wrapper 而非 functools.update_wrapper |
只保留 __name__,其他屬性仍遺失。 |
使用 functools.wraps(其內部呼叫 update_wrapper),或自行呼叫 functools.update_wrapper(wrapper, original_func)。 |
| 在裝飾器內部使用可變預設值 | 多次呼叫時共享同一個狀態,導致奇怪的行為。 | 使用 None 作為預設值,再在函式內部初始化(如 cache = {})。 |
裝飾器接受參數時忘記兩層 @functools.wraps |
只包裝了最內層 wrapper,外層仍未保留原始資訊。 | 兩層都需要 @functools.wraps(外層包裝內層函式)。 |
使用 functools.wraps 後仍看不到簽名 |
inspect.signature 只顯示 *args, **kwargs。 |
需要 functools.wraps + functools.update_wrapper(..., assigned=('__signature__',)) 或使用 decorator 第三方套件。 |
最佳實踐清單
- 永遠使用
@functools.wraps,除非你真的不需要保留任何原始資訊。 - 保持 wrapper 的簽名與原函式相同(使用
*args, **kwargs),若需要更精確的簽名,可參考functools.wraps(..., assigned=('__signature__',))。 - 將裝飾器寫成可重用的函式(接受參數的情況使用三層結構),提升可測試性與可讀性。
- 加入型別註解(
typing),讓 IDE 能正確推斷回傳值與參數型別。 - 使用
functools.update_wrapper直接操作時,明確指定assigned與updated,避免意外覆寫自訂屬性。
實際應用場景
| 場景 | 為何需要 functools.wraps |
範例 |
|---|---|---|
| Web 框架的路由註冊(如 Flask、FastAPI) | 框架透過 __name__ 產生 URL,若被覆寫會產生錯誤路徑。 |
@app.route('/login') @functools.wraps(view_func) def wrapper(...): |
| 自動產生 API 文件(Swagger、OpenAPI) | 文件生成工具會抓 __doc__ 與型別註解,失去資訊會導致文件不完整。 |
@dataclass @functools.wraps 用於服務層函式。 |
| 測試框架的 Mock/Spy | 測試時需要取得原始函式的 __module__ 以定位來源,wraps 可保持正確。 |
@patch('module.func') 搭配自訂裝飾器。 |
| 企業級日誌與監控 | 需要在日誌中顯示真正的函式名稱與說明,才能快速定位問題。 | 前文的 Logging Decorator。 |
| 函式快取與記憶化 | functools.lru_cache 需要正確的函式簽名來決定快取鍵值。 |
前文的 cached 範例。 |
總結
- 裝飾器 為 Python 函式式編程提供了強大的「行為注入」能力,但如果不妥善保留原始函式的元資料,會造成除錯與文件維護的困擾。
functools.wraps只是一行簡單的裝飾器,卻能自動把 名稱、說明文件、模組、註解與__dict__從原始函式搬回包裝函式,使得 堆疊追蹤、IDE 補全與自動化文件生成 都保持正確。- 在實務開發中,幾乎所有自訂裝飾器(日誌、計時、驗證、快取、授權等)都應該 配合
functools.wraps使用,並遵守「保持簽名、避免可變預設值、明確指定assigned」的最佳實踐。
掌握了 functools.wraps 後,你的裝飾器將不再是「黑盒」——它們既能提供彈性的功能增強,又不會隱藏原本的程式碼意圖,真正做到 可讀、可維、可測。祝你在 Python 的函式式編程旅程中寫出更乾淨、更易維護的程式!