本文 AI 產出,尚未審核

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 只對不可變參數(如 intstrtuple)有效,若參數是可變物件需自行轉換成 hashable。


常見陷阱與最佳實踐

陷阱 說明 解法
忘記使用 functools.wraps 會導致 __name____doc__ 被覆寫,除錯與自動文件產生失效。 在 wrapper 前加 @functools.wraps(func)
裝飾器順序錯誤 多個裝飾器堆疊時,執行順序是 自下而上(最內層先執行)。 先思考每個裝飾器的前後置需求,必要時在測試中確認呼叫順序。
使用可變預設參數 若 wrapper 使用 def wrapper(args=[], **kwargs):,會產生共享列表。 永遠使用 None 作為預設值,並在函式內部初始化。
快取不適用於副作用函式 memoize 只適合純函式;若函式有 I/O、全域變數變更,快取會產生錯誤結果。 確認函式的 純度,或在快取前加入失效機制。
過度裝飾 把所有功能都寫成裝飾器會讓程式碼變得難以追蹤。 適度使用:將真正需要跨多個函式的共通邏輯抽出為裝飾器,其餘保持簡潔。

最佳實踐

  1. 保持裝飾器單一職責:每個裝飾器只做一件事(如日誌、計時、驗證),方便組合與測試。
  2. 寫單元測試:尤其是帶參數的裝飾器,測試不同參數組合與錯誤情況。
  3. 使用 functools.update_wrapperwraps,確保原始函式的 meta data 不會遺失。
  4. 避免在裝飾器內部做昂貴的運算,除非它本身就是效能相關的功能(如快取)。
  5. 文件化裝飾器:在 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. 資料科學:快取重複計算

在機器學習模型的特徵工程階段,某些計算成本高且結果不會變動(例如統計量),使用 @memoizefunctools.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 開發的各個層面(從腳本到大型框架)自如地抽象共通行為,讓程式碼更具彈性與可讀性。祝你在未來的專案中玩得開心、寫得更好! 🚀