Python 課程 — 函式(Functions)
主題:裝飾器(Decorator)
簡介
在 Python 中,裝飾器是一種非常強大且彈性的語法結構,讓我們可以在不改變原始函式程式碼的前提下,為函式「加上」額外的功能。這種「在函式外層包一層」的做法,不僅提升了程式碼的可讀性與可維護性,也讓重複性的前後處理(例如記錄日誌、效能計時、權限驗證)能夠集中管理。
對於 初學者,裝飾器可能看起來有點抽象;但只要掌握了「函式是第一等公民」的概念,並了解 閉包(closure)與 可呼叫物件(callable)之間的關係,就能輕鬆上手。對 中級開發者而言,裝飾器更是實務開發中不可或缺的工具,常見於 Web 框架、測試套件以及資料科學工作流程中。
本篇文章將從基本概念說起,逐步帶出實作方式、常見陷阱與最佳實踐,最後以幾個真實的應用場景作結,幫助你在日常開發中靈活運用裝飾器。
核心概念
1. 裝飾器是「函式的函式」
在 Python 中,函式本身可以被當作 變數、參數、回傳值。裝飾器正是利用這一特性,接受一個函式作為輸入,回傳一個包裝過的函式(wrapper),而這個 wrapper 會在原函式執行前後插入自訂的行為。
def my_decorator(func):
def wrapper(*args, **kwargs):
# 前置處理
print("執行前")
result = func(*args, **kwargs) # 呼叫原函式
# 後置處理
print("執行後")
return result
return wrapper
使用 @my_decorator 語法糖,就等同於:
@my_decorator
def greet(name):
print(f"Hello, {name}!")
# 等同於
greet = my_decorator(greet)
2. functools.wraps:保留原函式資訊
直接回傳 wrapper 會導致原函式的 __name__、__doc__ 等屬性被覆蓋,這會影響除錯與自動文件產生。functools.wraps 能自動把這些屬性「搬」回 wrapper。
import functools
def my_decorator(func):
@functools.wraps(func) # <--- 重要
def wrapper(*args, **kwargs):
print("前置")
return func(*args, **kwargs)
return wrapper
3. 帶參數的裝飾器
若要讓裝飾器本身接受參數,需要再多包一層函式,形成「三層結構」:
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def say_hi():
print("Hi!")
上述 repeat 會把 say_hi 呼叫 三次。
4. 裝飾類別(Class Decorator)
裝飾器不一定要是函式,類別也可以實作 __call__ 方法,變成可呼叫物件,進而當作裝飾器使用。
class CountCalls:
def __init__(self, func):
self.func = func
self.calls = 0
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
self.calls += 1
print(f"第 {self.calls} 次呼叫")
return self.func(*args, **kwargs)
@CountCalls
def add(a, b):
return a + b
程式碼範例
以下提供 五個 常見且實用的裝飾器範例,從基礎到進階,皆附上說明與註解。
範例 1️⃣ 記錄執行時間(Timer)
import time, functools
def timer(func):
"""測量函式執行時間的裝飾器"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"⏱ {func.__name__} 執行時間: {end - start:.4f} 秒")
return result
return wrapper
@timer
def slow_sum(n):
total = 0
for i in range(n):
total += i
return total
slow_sum(10_000_000)
重點:使用
time.perf_counter()可取得高解析度的時間,適合測試效能。
範例 2️⃣ 檢查參數類型(Type Checker)
import functools
def type_check(expected_type):
"""檢查第一個參數是否符合預期類型"""
def decorator(func):
@functools.wraps(func)
def wrapper(arg, *args, **kwargs):
if not isinstance(arg, expected_type):
raise TypeError(f"第一個參數必須是 {expected_type.__name__}")
return func(arg, *args, **kwargs)
return wrapper
return decorator
@type_check(int)
def square(x):
"""傳回 x 的平方"""
return x * x
print(square(5)) # 正常
# print(square('5')) # 會拋出 TypeError
技巧:只檢查第一個參數,實際開發中可根據需求檢查全部參數或使用
inspect.signature取得參數資訊。
範例 3️⃣ 記錄日誌(Logging)
import logging, functools
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
def log_call(func):
"""在呼叫前後寫入 INFO 級別日誌"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
logging.info(f"呼叫 {func.__name__},參數: args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
logging.info(f"{func.__name__} 回傳 {result!r}")
return result
return wrapper
@log_call
def multiply(a, b):
return a * b
multiply(3, 7)
實務:在大型系統中,透過裝飾器集中管理日誌,可避免在每個函式內手動寫
logging造成的重複程式碼。
範例 4️⃣ 權限驗證(Permission)
import functools
# 假設有一個全域變數記錄當前使用者的角色
CURRENT_USER_ROLE = "guest"
def require_role(role):
"""只有符合指定角色的使用者才能執行"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if CURRENT_USER_ROLE != role:
raise PermissionError(f"需要角色 {role},但目前是 {CURRENT_USER_ROLE}")
return func(*args, **kwargs)
return wrapper
return decorator
@require_role("admin")
def delete_user(user_id):
print(f"使用者 {user_id} 已被刪除")
# delete_user(123) # 會拋出 PermissionError
提醒:在真實環境中,角色資訊通常來自 session、token 或資料庫,裝飾器只負責判斷,不負責取得。
範例 5️⃣ 快取結果(Memoization)
import functools
def memoize(func):
"""簡易的函式快取裝飾器,適合純函式 (pure function)"""
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args in cache:
return cache[args] # 直接回傳快取值
result = func(*args)
cache[args] = result
return result
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print([fibonacci(i) for i in range(10)]) # 快速計算
說明:
memoize只對不可變參數(如int、str、tuple)有效,若參數是可變物件需自行轉換成 hashable。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解法 |
|---|---|---|
忘記使用 functools.wraps |
會導致 __name__、__doc__ 被覆寫,除錯與自動文件產生失效。 |
在 wrapper 前加 @functools.wraps(func)。 |
| 裝飾器順序錯誤 | 多個裝飾器堆疊時,執行順序是 自下而上(最內層先執行)。 | 先思考每個裝飾器的前後置需求,必要時在測試中確認呼叫順序。 |
| 使用可變預設參數 | 若 wrapper 使用 def wrapper(args=[], **kwargs):,會產生共享列表。 |
永遠使用 None 作為預設值,並在函式內部初始化。 |
| 快取不適用於副作用函式 | memoize 只適合純函式;若函式有 I/O、全域變數變更,快取會產生錯誤結果。 |
確認函式的 純度,或在快取前加入失效機制。 |
| 過度裝飾 | 把所有功能都寫成裝飾器會讓程式碼變得難以追蹤。 | 適度使用:將真正需要跨多個函式的共通邏輯抽出為裝飾器,其餘保持簡潔。 |
最佳實踐:
- 保持裝飾器單一職責:每個裝飾器只做一件事(如日誌、計時、驗證),方便組合與測試。
- 寫單元測試:尤其是帶參數的裝飾器,測試不同參數組合與錯誤情況。
- 使用
functools.update_wrapper或wraps,確保原始函式的 meta data 不會遺失。 - 避免在裝飾器內部做昂貴的運算,除非它本身就是效能相關的功能(如快取)。
- 文件化裝飾器:在 docstring 中說明接受的參數、可能拋出的例外,讓使用者一目了然。
實際應用場景
1. Web 框架中的路由與授權
在 Flask、FastAPI 等框架裡,@app.route、@app.get 本質上就是路由裝飾器,同時常會配合 @login_required、@admin_only 等授權裝飾器,將 HTTP 請求的前置檢查抽離出視圖函式。
from flask import Flask, request, abort
import functools
app = Flask(__name__)
def login_required(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if not request.headers.get("Authorization"):
abort(401)
return func(*args, **kwargs)
return wrapper
@app.route("/secret")
@login_required
def secret_page():
return "只有已登入使用者能看到"
2. 測試框架的前置與後置設定
pytest 提供的 @pytest.fixture 其實也是一種裝飾器,用來在測試執行前建立測試環境(如資料庫連線、暫存檔),測試結束後自動清理。
import pytest
@pytest.fixture
def tmp_file(tmp_path):
p = tmp_path / "data.txt"
p.write_text("sample")
yield p
p.unlink() # 測試結束自動刪除
3. 資料科學:快取重複計算
在機器學習模型的特徵工程階段,某些計算成本高且結果不會變動(例如統計量),使用 @memoize 或 functools.lru_cache 能顯著縮短實驗時間。
from functools import lru_cache
import numpy as np
@lru_cache(maxsize=128)
def compute_covariance(matrix_tuple):
matrix = np.array(matrix_tuple)
return np.cov(matrix)
# 只會計算一次,之後直接快取
cov1 = compute_covariance(tuple(np.random.rand(100, 5).flatten()))
cov2 = compute_covariance(tuple(np.random.rand(100, 5).flatten())) # 重新計算
4. CLI 工具的統一錯誤處理
當開發命令列介面時,常需要捕捉例外、印出友善訊息並設定退出代碼。使用裝飾器可以把這段「樣板」程式碼抽離。
import sys, functools
def cli_error_handler(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"錯誤: {e}", file=sys.stderr)
sys.exit(1)
return wrapper
@cli_error_handler
def main():
# 可能拋出例外的程式碼
...
if __name__ == "__main__":
main()
總結
- 裝飾器 是 Python 中「函式即第一等公民」的直接應用,讓我們能在不改動原始程式碼的情況下,靈活地加入前置、後置或取代行為。
- 透過
functools.wraps、閉包 與 可呼叫物件,我們可以寫出 可讀、可重用、可測試 的程式碼。 - 常見的實務需求——效能計時、日誌記錄、權限驗證、結果快取——都可以用裝飾器快速實現,並在大型專案中保持程式碼的乾淨與一致性。
- 使用裝飾器時要留意 順序、元資料保留、快取的適用範圍,並遵守「單一職責」的設計原則,才能避免陷阱、提升維護性。
掌握了裝飾器的概念與寫法,你就能在 Python 開發的各個層面(從腳本到大型框架)自如地抽象共通行為,讓程式碼更具彈性與可讀性。祝你在未來的專案中玩得開心、寫得更好! 🚀