第 27 課 · 進階 · 20 分鐘

Python 裝飾器

用可復用的行為包裝函式,而不修改函式體。

裝飾器包裝函式functools.wraps@ 語法

每張訂單都要過同樣的三步:驗證、收款、列印。Maya 想給這條路徑計時,又不想改它。包裝函式該放在哪裡?

裝飾器是一個接收函式並回傳替代函式的函式。@ 語法是在定義時應用這種替代的簡潔寫法。包裝函式適合做日誌、計時、驗證和權限檢查,但應保留原函式的元資料。 你還會寫帶參數的裝飾器、轉發參數,並理解疊加順序。

函式可以作為值傳遞

函式也是物件。可以把它存進變數、傳給另一個函式,或作為回傳值。裝飾器正是利用這一點:接收原函式,並在它外面構造一個新的可呼叫物件。

def greet():
    return "hello"

def call(func):
    return func()

print(call(greet))

把函式作為參數

用 @ 包裝函式

包裝函式通過閉包記住 func,並在被包裝函式呼叫時執行它。把 @announce 放在 greet 上方,等價於在定義之後寫 greet = announce(greet)。

def announce(func):
    def wrapper():
        return "Before: " + func()
    return wrapper

@announce
def greet():
    return "hello"

print(greet())

裝飾器語法

保留原函式的名字

包裝函式替換了原函式物件,所以它的 __name__ 通常會變成 wrapper。functools.wraps 會把重要元資料複製回來。打算長期使用的裝飾器都應該加上它。

from functools import wraps

def announce(func):
    @wraps(func)
    def wrapper():
        return func().upper()
    return wrapper

@announce
def greet():
    return "hello"

print(greet())
print(greet.__name__)

保留函式身份

裝飾器工廠回傳裝飾器

裝飾器需要設定時,在外面再加一層函式。repeat(3) 回傳真正的裝飾器,後者再接收函式。三層容易混淆;只有行為確實需要參數時才用工廠。

def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            return [func(*args, **kwargs) for _ in range(times)]
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    return f"hello {name}"

print(greet("Ada"))

設定在外,函式在內

轉發參數和回傳值

包裝函式應接受 *args 和 **kwargs,並回傳原函式的結果。否則裝飾器會悄悄改變函式介面。再配合 functools.wraps,偵錯和文件仍能看到原來的名字。

def trace(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(func.__name__, args, kwargs, "->", result)
        return result
    return wrapper

@trace
def add(a, b=0):
    return a + b

print(add(2, b=3))

保持介面靈活,保留回傳值

疊加順序是從下往上

離 def 最近的裝飾器先應用。@bold 在 @upper 上方,意味著 upper 先包裝函式,然後 bold 再包裝結果。預測輸出時從下往上讀,雖然程式碼是從上往下寫的。

def bold(func):
    def wrapper():
        return "<b>" + func() + "</b>"
    return wrapper

def upper(func):
    def wrapper():
        return func().upper()
    return wrapper

@bold
@upper
def title():
    return "menu"

print(title())

下面的裝飾器先執行

本課要點

  • 裝飾器接收一個函式,並回傳一個可呼叫物件。
  • @deco 放在 def f 上方,等價於 f = deco(f)。
  • 內部包裝函式會在被裝飾函式呼叫時執行。
  • functools.wraps 會保留原函式的元資料。
  • 工廠回傳裝飾器;包裝函式轉發參數;疊加順序從下往上。

常見錯誤

  • 裝飾器忘記回傳包裝函式。
  • 在定義包裝函式時就呼叫原函式,而不是在包裝函式內部呼叫。
  • 忘記 functools.wraps,導致原名和文件字串丟失。
  • 裝飾器需要參數時,忘記多寫一層函式。
輪到你

寫一個裝飾器,在函式回傳的字串兩邊加上方括號。

動手試試

改改變數、先預測輸出,再執行程式碼。第一次點擊「執行程式碼」時會下載 Python 執行時,可能需要幾秒鐘。

瀏覽器內 Python本機執行 · 無需帳號
首次執行會從 CDN 下載 Pyodide。修改後點擊「執行程式碼」。

練習本課

12 道題,作答後立即看到解釋。

查看 12 道題答案開始練習
← 生成器上下文管理器 →