Python 裝飾器 Decorator 從入門到進階:用實例搞懂這個神奇語法
什麼是 Python 裝飾器?
如果你學 Python 學到一定程度,一定會在別人的程式碼或框架裡看到 @ 這個符號,像是 Flask 的 @app.route、pytest 的 @pytest.fixture。這些就是裝飾器(Decorator)。
裝飾器本質上是一個函式,它接受另一個函式作為參數,然後回傳一個新的函式。聽起來有點抽象,但只要理解兩個核心概念——一等函式(First-class Functions)和閉包(Closure)——裝飾器就會變得非常直觀。
這篇文章會帶你從零開始,一步步搞懂裝飾器的原理,並學會在實際專案中應用它。如果你還沒設定好開發環境,可以先看看 Python 虛擬環境教學,確保你有乾淨的執行環境。
一等函式:函式也是物件
Python 中,函式是一等公民(First-class Citizen),意思是函式可以像任何其他物件一樣被傳遞、賦值、甚至當作回傳值。
def greet(name):
return f"Hello, {name}!"
# 函式可以賦值給變數
say_hello = greet
print(say_hello("Alice")) # Hello, Alice!
# 函式可以作為參數傳入另一個函式
def execute(func, value):
return func(value)
print(execute(greet, "Bob")) # Hello, Bob!
# 函式可以作為回傳值
def get_greeter():
return greet
greeter = get_greeter()
print(greeter("Charlie")) # Hello, Charlie!
這個特性是裝飾器存在的基礎。
閉包:函式記住了外部的變數
閉包是指一個內部函式,它可以存取並「記住」定義它時所在的外部作用域中的變數,即使外部函式已經執行完畢。
def outer(message):
# message 是外部函式的區域變數
def inner():
# inner 可以存取 message,即使 outer 已結束
print(f"Message: {message}")
return inner # 回傳的是函式物件,不是呼叫結果
say_hi = outer("Hi there!")
say_hi() # Message: Hi there!
say_bye = outer("Goodbye!")
say_bye() # Message: Goodbye!
注意 outer 回傳的是 inner(沒有括號),也就是函式物件本身。當 say_hi() 被呼叫時,它還是能存取 message,因為閉包把這個變數「封存」起來了。
從零開始寫一個裝飾器
有了以上概念,我們來親手寫第一個裝飾器。假設我們想在執行任何函式前後都印出一行分隔線:
def my_decorator(func):
def wrapper():
print("=" * 30)
func() # 呼叫原本的函式
print("=" * 30)
return wrapper
def say_hello():
print("Hello, World!")
# 手動套用裝飾器
decorated = my_decorator(say_hello)
decorated()
# ==============================
# Hello, World!
# ==============================
這就是裝飾器的本質:my_decorator 接收 say_hello,回傳一個增強版的 wrapper 函式。
@語法糖:讓程式碼更優雅
Python 提供了 @ 符號作為語法糖(Syntactic Sugar),讓套用裝飾器更直觀:
def my_decorator(func):
def wrapper():
print("=" * 30)
func()
print("=" * 30)
return wrapper
@my_decorator # 等同於 say_hello = my_decorator(say_hello)
def say_hello():
print("Hello, World!")
say_hello()
# ==============================
# Hello, World!
# ==============================
@my_decorator 放在函式定義上方,Python 會自動把 say_hello = my_decorator(say_hello) 這件事做掉,語義完全相同,但程式碼更清晰。
處理帶參數的函式:*args 和 **kwargs
上面的例子中,被裝飾的函式沒有任何參數。實際上大多數函式都有參數,我們需要讓 wrapper 能夠傳遞這些參數:
def my_decorator(func):
def wrapper(*args, **kwargs): # 接收任意參數
print(f"呼叫函式: {func.__name__}")
result = func(*args, **kwargs) # 傳遞給原始函式
print(f"函式執行完畢")
return result # 記得回傳結果!
return wrapper
@my_decorator
def add(a, b):
return a + b
@my_decorator
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(add(3, 5)) # 呼叫函式: add → 8
print(greet("Alice")) # 呼叫函式: greet → Hello, Alice!
functools.wraps:保留原函式的身份
有個細節很重要。使用裝飾器後,函式的 __name__、__doc__ 等屬性會被 wrapper 覆蓋掉:
def my_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def add(a, b):
"""兩數相加"""
return a + b
print(add.__name__) # wrapper(不是 add!)
print(add.__doc__) # None(文件字串也不見了!)
這在除錯或使用某些框架時會造成問題。解決方法是使用 functools.wraps:
import functools
def my_decorator(func):
@functools.wraps(func) # 這行很重要!
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def add(a, b):
"""兩數相加"""
return a + b
print(add.__name__) # add ✓
print(add.__doc__) # 兩數相加 ✓
寫裝飾器時,永遠記得加上 @functools.wraps(func)。這是業界標準做法。
帶參數的裝飾器
有時候我們希望裝飾器本身也能接收參數,例如 @retry(times=3)。這時需要再多一層函式:
import functools
def repeat(times):
"""讓函式重複執行 times 次的裝飾器"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def say_hi():
print("Hi!")
say_hi()
# Hi!
# Hi!
# Hi!
結構變成三層:最外層接收裝飾器的參數,中間層是真正的裝飾器,最內層是 wrapper。@repeat(times=3) 相當於 say_hi = repeat(times=3)(say_hi)。
實用範例:計時裝飾器
這是最常用的裝飾器之一,用來測量函式的執行時間:
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
elapsed = end - start
print(f"{func.__name__} 執行時間: {elapsed:.4f} 秒")
return result
return wrapper
@timer
def slow_function():
time.sleep(1.5)
return "完成"
result = slow_function()
# slow_function 執行時間: 1.5023 秒
實用範例:日誌記錄裝飾器
import functools
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(message)s")
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
logging.info(f"呼叫 {func.__name__}({signature})")
try:
result = func(*args, **kwargs)
logging.info(f"{func.__name__} 回傳: {result!r}")
return result
except Exception as e:
logging.error(f"{func.__name__} 拋出例外: {e}")
raise
return wrapper
@log_calls
def divide(a, b):
return a / b
divide(10, 2) # INFO: 呼叫 divide(10, 2) → 回傳 5.0
divide(10, 0) # ERROR: divide 拋出例外: division by zero
實用範例:重試裝飾器
在呼叫外部 API 或網路請求時,加上自動重試邏輯非常實用:
import functools
import time
import random
def retry(max_attempts=3, delay=1.0, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == max_attempts:
print(f"已達最大重試次數 ({max_attempts}),放棄")
raise
print(f"第 {attempt} 次嘗試失敗: {e},{delay} 秒後重試...")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5, exceptions=(ConnectionError,))
def fetch_data(url):
# 模擬不穩定的網路請求
if random.random() < 0.7:
raise ConnectionError("網路連線失敗")
return f"從 {url} 取得的資料"
try:
data = fetch_data("https://api.example.com/data")
print(data)
except ConnectionError:
print("最終仍然失敗")
實用範例:快取裝飾器(Memoization)
Python 內建就有 functools.lru_cache,但自己實作一個有助於理解原理:
import functools
def memoize(func):
cache = {} # 閉包中的快取字典
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# 沒有快取時,fibonacci(40) 需要大量遞迴
# 有了快取,幾乎瞬間完成
print(fibonacci(40)) # 102334155
# Python 內建版本,更完整(支援 maxsize 和 LRU 淘汰)
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci_builtin(n):
if n <= 1:
return n
return fibonacci_builtin(n - 1) + fibonacci_builtin(n - 2)
類別裝飾器(Class Decorators)
裝飾器也可以用類別來實作,通過定義 __call__ 方法讓類別的實例可以被當作函式呼叫:
import functools
class CountCalls:
"""計算函式被呼叫次數的裝飾器"""
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} 已被呼叫 {self.count} 次")
return self.func(*args, **kwargs)
@CountCalls
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Alice") # say_hello 已被呼叫 1 次
say_hello("Bob") # say_hello 已被呼叫 2 次
say_hello("Charlie")# say_hello 已被呼叫 3 次
print(say_hello.count) # 3(可以直接存取狀態!)
疊加多個裝飾器
可以在同一個函式上套用多個裝飾器,執行順序是由下到上套用,由上到下執行:
import functools
def bold(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return "" + func(*args, **kwargs) + ""
return wrapper
def italic(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return "" + func(*args, **kwargs) + ""
return wrapper
@bold # 第二個套用(最外層)
@italic # 第一個套用(最內層)
def greet(name):
return f"Hello, {name}!"
print(greet("Alice")) # Hello, Alice!
# 等同於 bold(italic(greet))("Alice")
加入 Type Hints 讓裝飾器更安全
Python 3.10+ 可以用 ParamSpec 和 TypeVar 為裝飾器加上完整的型別提示:
import functools
import time
from typing import TypeVar, Callable, Any
from collections.abc import Callable
# Python 3.10+ 推薦寫法
from typing import ParamSpec, TypeVar
P = ParamSpec('P')
T = TypeVar('T')
def timer(func: Callable[P, T]) -> Callable[P, T]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__}: {elapsed:.4f}s")
return result
return wrapper
@timer
def add(a: int, b: int) -> int:
return a + b
result = add(1, 2) # IDE 會正確推斷 result 的型別為 int
真實世界的裝飾器:Flask 路由與 pytest
現在你理解了裝飾器的原理,回頭看這些框架的用法就豁然開朗了:
# Flask 路由裝飾器
from flask import Flask
app = Flask(__name__)
@app.route('/api/users', methods=['GET'])
def get_users():
return {"users": []}
# app.route 是一個帶參數的裝飾器工廠
# 等同於:get_users = app.route('/api/users', methods=['GET'])(get_users)
# pytest fixture 裝飾器
import pytest
@pytest.fixture
def database_connection():
conn = create_connection()
yield conn # 提供 fixture 值
conn.close() # 測試後清理
@pytest.fixture(scope="module") # 帶參數的版本
def shared_resource():
return expensive_setup()
如果你在做 Web API 開發,可以參考 FastAPI REST API 教學,FastAPI 也大量使用了裝飾器模式(@app.get、@app.post 等)。在自動化腳本中,裝飾器同樣很有用,像是 Python Excel 自動化教學中就可以用計時裝飾器來監控批次處理的效能。
裝飾器常見陷阱與最佳實踐
陷阱 1:忘記加 @functools.wraps
一定要加,否則函式的 __name__、__doc__、__annotations__ 都會遺失,導致除錯困難。
陷阱 2:裝飾器在模組載入時就執行
registry = []
def register(func):
registry.append(func) # 模組一載入就會執行這行!
return func
@register
def my_func():
pass
print(registry) # [] # 尚未呼叫 my_func 就已註冊
這有時是預期行為(例如 Flask 的路由就是這樣),但要注意別在裝飾器中做有副作用的操作。
最佳實踐總結
- 永遠使用
@functools.wraps(func) - wrapper 一定要
return result,否則原函式的回傳值會消失 - 帶參數的裝飾器用三層函式結構
- 為裝飾器加上 Type Hints(Python 3.10+ 用 ParamSpec)
- 裝飾器應該只做一件事,保持單一職責
小結
裝飾器是 Python 中非常優雅的設計模式,掌握它能讓你的程式碼更模組化、更易維護。從本文學到的關鍵概念:
- 一等函式:函式可以作為參數和回傳值
- 閉包:內部函式可以存取外部函式的變數
- 基本裝飾器:接收函式、回傳增強版函式
@functools.wraps:保留原函式的 metadata- 帶參數的裝飾器:三層函式結構
- 實用裝飾器:計時、日誌、重試、快取
建議你把這些範例都實際跑過一遍,然後嘗試在自己的專案中加入一個計時或日誌裝飾器,你會發現它立刻讓程式碼變得更乾淨、更專業。
繼續閱讀
Python 虛擬環境教學:venv、virtualenv、conda 完整比較與實戰指南
完整比較 Python 三大虛擬環境工具 venv、virtualenv 與 conda 的差異與使用場景。從基本操作到最佳實踐,帶你建立乾淨的 Python 開發環境,告別套件衝突的噩夢。
相關文章
你可能也喜歡
探索其他領域的精選好文