Python decorator 是很多人「會用但說不清楚」的語法特性。本文不從「語法糖」開始講,而是從 Functional Programming(FP) 的角度切入,讓你理解 decorator 的本質不是魔法,而是幾個簡單概念的組合。
前置概念:函數在 FP 裡是什麼?
在 FP 的世界裡,函數是 first-class citizen(一等公民),意思是:函數可以被當成值來使用——賦值給變數、傳入另一個函數、從函數裡回傳。
Python 完全支援這件事:
def greet(name):
return f"Hello, {name}"
say_hi = greet # 函數賦值給變數
print(say_hi("Alice")) # Hello, Alice
greet 不過是個指向函數物件的名字。say_hi 和它指向同一個函數。
高階函式(Higher-Order Function)
FP 裡另一個核心概念是 高階函式:接受函數作為參數,或回傳函數的函數。
Python 內建的 map、filter、sorted 都是高階函式:
nums = [1, 2, 3, 4, 5]
squares = list(map(lambda x: x ** 2, nums))
# [1, 4, 9, 16, 25]
我們也可以自己寫一個:
def apply_twice(f, x):
return f(f(x))
def add_one(n):
return n + 1
apply_twice(add_one, 3) # 5
apply_twice 接受一個函數 f,把它連續套用兩次。這就是高階函式。
閉包(Closure)
要理解 decorator,還需要理解 閉包。
閉包是一個函數,它「記住」了定義它時的外部環境變數,即使外部函數已經執行完畢。
def make_multiplier(n):
def multiply(x):
return x * n # n 來自外部函式的 scope
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
double(5) # 10
triple(5) # 15
make_multiplier 回傳的是一個函數,而這個函數「閉合」了變數 n。double 和 triple 各自記住了自己的 n。
Decorator 的本質:回傳函數的高階函式
現在把上面三個概念接起來。Decorator 就是:
一個接受函數、回傳(新)函數的高階函式。
來手寫一個最簡單的 decorator:
def log_call(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"Done")
return result
return wrapper
def add(a, b):
return a + b
add = log_call(add) # 把 add 包一層
add(1, 2)
# Calling add
# Done
# 3
注意最後這一行:
add = log_call(add)
這就是 decorator 做的事。Python 的 @ 語法不過是讓這行更好看:
@log_call
def add(a, b):
return a + b
兩者完全等價。@log_call 等於 add = log_call(add)。
wrapper 裡面用 *args, **kwargs
wrapper 用 *args, **kwargs 是為了讓這個 decorator 能套在任意簽名的函數上,不論原函數接受幾個參數。這是讓 decorator 通用的標準寫法。
保留原函數的 metadata:functools.wraps
有個問題:包完之後,add.__name__ 變成 wrapper 了。
print(add.__name__) # wrapper(錯誤)
FP 裡講求 referential transparency(引用透明)——一個函數的替換不應該改變程式的行為。Decorator 把原函數替換成 wrapper,卻讓 metadata 跑掉,這是個側效應。
標準做法是用 functools.wraps:
import functools
def log_call(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"Done")
return result
return wrapper
@functools.wraps(func) 本身也是一個 decorator——它把 func 的 __name__、__doc__ 等 metadata 複製到 wrapper 上。Decorator 套 Decorator,這很 FP。
帶參數的 Decorator:Currying 的影子
有時候我們希望 decorator 本身也能接受參數,例如:
@retry(times=3)
def fetch_data():
...
這需要多加一層函數:
import functools
def retry(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Attempt {attempt} failed: {e}")
raise RuntimeError(f"Failed after {times} attempts")
return wrapper
return decorator
現在的調用鏈是:
retry(times=3) → 回傳 decorator
decorator(fetch_data) → 回傳 wrapper
@retry(times=3) 等價於:
fetch_data = retry(times=3)(fetch_data)
這個「函數回傳函數、再接受函數」的結構,和 FP 裡的 currying(柯里化) 非常相似——把多參數函數轉成一系列單參數函數的串接。
Decorator 組合:函數合成(Function Composition)
FP 裡有個概念叫 function composition(函數合成):(f ∘ g)(x) = f(g(x)),把兩個函數串接成一個新函數。
多個 decorator 疊加就是函數合成:
@log_call
@retry(times=3)
def fetch_data():
...
等價於:
fetch_data = log_call(retry(times=3)(fetch_data))
執行順序是由內而外:先套 retry,再套 log_call。Decorator 的堆疊就是函數合成,只是語法讓它看起來像裝飾,而非數學式。
小結
| FP 概念 | 在 Decorator 的體現 |
|---|---|
| 一等函式 | 函數可傳入、可回傳 |
| 高階函式 | Decorator 本身接受並回傳函數 |
| 閉包 | wrapper 閉合了 func |
| Currying | 帶參數的 decorator 多一層函數 |
| 函數合成 | 多個 decorator 疊加 |
Python decorator 不是特殊語法,而是這些 FP 概念在實務中的自然落地。當你下次看到 @ 時,腦中浮現的應該是:「這是一個回傳函數的高階函式,透過閉包記住了原本的函數。」
理解了本質,再複雜的 decorator 也只是多幾層函數而已。