跳到主要內容
技術

用 Functional Programming 的角度理解 Python Decorator

從一等函式、高階函式、閉包出發,揭開 Python decorator 的本質,讓你不只會用,更能看懂它背後的函數式思維。

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 內建的 mapfiltersorted 都是高階函式:

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 回傳的是一個函數,而這個函數「閉合」了變數 ndoubletriple 各自記住了自己的 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 也只是多幾層函數而已。