快轉到主要內容
  1. 教學文章/

Python 裝飾器:讓你的函式穿上超能力外套

·7 分鐘· loading · loading · ·
Python Decorator 裝飾器 進階語法 設計模式
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 24: 本文

一、前言
#

嗨!拍拍君來了 ✨

你有沒有在別人的 Python 程式碼裡看過這種神奇的 @ 符號?

@login_required
def dashboard(request):
    return render(request, "dashboard.html")

或是 Flask 裡面的:

@app.route("/hello")
def hello():
    return "Hello World!"

這個 @ 就是 Python 的裝飾器(Decorator)。它可以在不修改原始函式的情況下,幫函式「穿上外套」,加上額外的功能——像是計時、快取、權限檢查等等。

裝飾器是 Python 中最優雅也最實用的進階語法之一,學會之後你的程式碼會更乾淨、更好維護。今天拍拍君就帶你從零開始,一步步搞懂裝飾器的所有秘密!

二、裝飾器的核心概念
#

在學裝飾器之前,我們要先理解一件事:在 Python 裡,函式是一等公民(First-class citizen)

這表示函式可以:

  1. 被指派給變數
  2. 當作參數傳給其他函式
  3. 從另一個函式回傳
# 函式可以指派給變數
def greet(name):
    return f"嗨,{name}!"

say_hi = greet
print(say_hi("拍拍君"))  # 嗨,拍拍君!

# 函式可以當參數
def apply(func, value):
    return func(value)

print(apply(greet, "拍拍醬"))  # 嗨,拍拍醬!

# 函式可以回傳函式
def make_greeter(greeting):
    def greeter(name):
        return f"{greeting}{name}!"
    return greeter

hello = make_greeter("你好")
print(hello("chatPTT"))  # 你好,chatPTT!

理解了這個,裝飾器的本質就很簡單了:裝飾器就是一個接收函式、回傳函式的函式

三、最簡單的裝飾器
#

讓我們來寫第一個裝飾器:

def my_decorator(func):
    def wrapper():
        print("✨ 函式執行前")
        func()
        print("✨ 函式執行後")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()

輸出:

✨ 函式執行前
Hello!
✨ 函式執行後

這裡 @my_decorator 等同於:

say_hello = my_decorator(say_hello)

所以 @ 語法只是語法糖,讓程式碼更好看而已。底層做的事情就是把原函式傳進去,拿到包裝後的新函式。

四、處理參數與回傳值
#

上面的裝飾器有個問題:被裝飾的函式不能有參數。我們用 *args**kwargs 來解決:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"📝 呼叫 {func.__name__},參數:{args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"✅ {func.__name__} 回傳:{result}")
        return result
    return wrapper

@my_decorator
def add(a, b):
    return a + b

@my_decorator
def greet(name, greeting="嗨"):
    return f"{greeting}{name}!"

print(add(3, 5))
print(greet("拍拍君", greeting="你好"))

輸出:

📝 呼叫 add,參數:(3, 5), {}
✅ add 回傳:8
8
📝 呼叫 greet,參數:('拍拍君',), {'greeting': '你好'}
✅ greet 回傳:你好,拍拍君!
你好,拍拍君!

保留原函式的身份: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 😱
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)。這是 Python 開發者的基本禮貌。如果你之前學過 functools,應該已經見過它了!

五、帶參數的裝飾器
#

有時候我們想讓裝飾器本身也能接受參數,例如:

@repeat(times=3)
def say_hi():
    print("嗨!")

這時候需要再包一層函式——三層套娃:

import functools

def repeat(times=1):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def say_hi():
    print("嗨!")

say_hi()

輸出:

嗨!
嗨!
嗨!

看起來有點複雜,但邏輯是清楚的:

  1. repeat(times=3) 回傳 decorator
  2. decorator(say_hi) 回傳 wrapper
  3. 呼叫 say_hi() 其實是呼叫 wrapper()

六、實戰常用裝飾器
#

6.1 計時器 ⏱️
#

import functools
import time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"⏱️ {func.__name__} 執行時間:{elapsed:.4f} 秒")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1.5)
    return "完成!"

print(slow_function())
⏱️ slow_function 執行時間:1.5012 秒
完成!

6.2 自動重試 🔄
#

API 呼叫失敗?網路不穩?加個 retry 裝飾器:

import functools
import time

def retry(max_attempts=3, delay=1.0):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    print(f"⚠️ 第 {attempt} 次嘗試失敗:{e}")
                    if attempt < max_attempts:
                        time.sleep(delay)
            raise last_exception
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5)
def unstable_api():
    import random
    if random.random() < 0.7:
        raise ConnectionError("伺服器忙碌中")
    return {"status": "ok"}

result = unstable_api()
print(result)

6.3 快取結果 💾
#

其實 Python 標準庫已經有 @functools.lru_cache(在 functools 教學 有詳細介紹),但自己寫一個有助於理解原理:

import functools

def simple_cache(func):
    cache = {}

    @functools.wraps(func)
    def wrapper(*args):
        if args in cache:
            print(f"💾 快取命中:{args}")
            return cache[args]
        result = func(*args)
        cache[args] = result
        return result

    wrapper.cache = cache  # 暴露 cache 方便除錯
    return wrapper

@simple_cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))  # 55
print(f"快取大小:{len(fibonacci.cache)}")  # 11

6.4 權限檢查 🔐
#

Web 框架中最常見的應用之一:

import functools

def require_role(role):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(user, *args, **kwargs):
            if user.get("role") != role:
                raise PermissionError(
                    f"需要 {role} 權限,目前是 {user.get('role')}"
                )
            return func(user, *args, **kwargs)
        return wrapper
    return decorator

@require_role("admin")
def delete_user(current_user, user_id):
    print(f"🗑️ 管理員 {current_user['name']} 刪除了使用者 {user_id}")

admin = {"name": "拍拍君", "role": "admin"}
guest = {"name": "chatPTT", "role": "guest"}

delete_user(admin, 42)   # ✅ 成功
delete_user(guest, 42)   # ❌ PermissionError

6.5 日誌記錄 📝
#

搭配 logging 模組 使用效果更好:

import functools
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        logger.info(f"呼叫 {func.__name__}({args}, {kwargs})")
        try:
            result = func(*args, **kwargs)
            logger.info(f"{func.__name__} 回傳 {result}")
            return result
        except Exception as e:
            logger.error(f"{func.__name__} 發生錯誤:{e}")
            raise
    return wrapper

@log_calls
def divide(a, b):
    return a / b

divide(10, 3)   # INFO 日誌
divide(10, 0)   # ERROR 日誌 + 拋出 ZeroDivisionError

七、堆疊多個裝飾器
#

裝飾器可以疊加使用:

@timer
@log_calls
@retry(max_attempts=3)
def fetch_data(url):
    ...

執行順序是由內到外(由下到上):

  1. 先套用 @retry → 得到重試版
  2. 再套用 @log_calls → 加上日誌
  3. 最後套用 @timer → 計算總時間(包含重試)

等同於:

fetch_data = timer(log_calls(retry(max_attempts=3)(fetch_data)))

💡 記憶口訣:裝飾器從最靠近函式的開始套用,但執行時外層先跑。就像穿衣服——最靠身體的先穿,但別人看到的是最外面那件。

八、用 class 實作裝飾器
#

除了函式,你也可以用 class 來寫裝飾器,利用 __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("拍拍君")  # 📊 say_hello 已被呼叫 1 次
say_hello("拍拍醬")  # 📊 say_hello 已被呼叫 2 次
say_hello("chatPTT")  # 📊 say_hello 已被呼叫 3 次

用 class 的好處是可以輕鬆保存狀態(像上面的 count),而且程式碼結構更清晰。

九、常見陷阱與注意事項
#

陷阱 1:裝飾 async 函式
#

如果你的函式是 async(學過 asyncio 的話),wrapper 也要是 async

import functools
import asyncio

def async_timer(func):
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = await func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"⏱️ {func.__name__}{elapsed:.4f} 秒")
        return result
    return wrapper

@async_timer
async def fetch_something():
    await asyncio.sleep(1)
    return "data"

asyncio.run(fetch_something())

陷阱 2:裝飾器在 import 時就執行
#

裝飾器在模組被 import 時就會執行(外層函式),不是在函式被呼叫時:

def register(func):
    print(f"📋 註冊 {func.__name__}")  # import 時就印出
    return func

@register
def my_func():
    pass

# 還沒呼叫 my_func(),但 "📋 註冊 my_func" 已經印了

這不一定是壞事——Flask 的 @app.route 就是利用這個特性來註冊路由的。

陷阱 3:別忘了 return
#

一個超容易犯的錯誤——wrapper 忘了 return

# ❌ 錯誤!
def bad_decorator(func):
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)  # 忘了 return!
    return wrapper

@bad_decorator
def add(a, b):
    return a + b

result = add(3, 5)
print(result)  # None 😱
# ✅ 正確
def good_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)  # 記得 return!
    return wrapper

十、Python 內建的實用裝飾器
#

Python 標準庫本身就提供了幾個好用的裝飾器:

裝飾器 用途 模組
@property 把方法變成屬性存取 內建
@staticmethod 靜態方法 內建
@classmethod 類別方法 內建
@functools.lru_cache 函式結果快取 functools
@functools.wraps 保留被裝飾函式的元資訊 functools
@dataclasses.dataclass 自動生成資料類別 dataclasses
@contextlib.contextmanager 把生成器變成 context manager contextlib
@typing.overload 型別提示用的多載宣告 typing

如果你對 @dataclasses.dataclass 有興趣,可以看拍拍君之前的 dataclasses 教學

結語
#

來回顧一下今天學了什麼:

  • 🎯 裝飾器的本質:接收函式、回傳函式的函式
  • 🔧 @functools.wraps:永遠記得加
  • 🧅 帶參數的裝飾器:三層套娃
  • 實戰模式:計時器、重試、快取、權限、日誌
  • 📚 堆疊順序:由下到上套用,由外到內執行
  • 🏗️ class 版裝飾器:用 __call__ 實作,適合需要保存狀態的場景

裝飾器是 Python 裡最能展現「優雅」的語法之一,一旦學會了,你會發現到處都能用它。但記住:不要為了炫技而濫用裝飾器——保持程式碼的可讀性永遠是第一優先。

下次當你看到 @ 的時候,就不會再覺得它是什麼神秘魔法了。它就是一件超能力外套,穿上去就能飛 🦸

拍拍君,下次見!

延伸閱讀
#

Python 學習 - 本文屬於一個選集。
§ 24: 本文

相關文章

MLX 入門教學:在 Apple Silicon 上跑機器學習
·4 分鐘· loading · loading
Python Mlx Apple-Silicon Machine-Learning Deep-Learning
FastAPI:Python 最潮的 Web API 框架
·5 分鐘· loading · loading
Python Fastapi Web Api Async
Docker for Python:讓你的程式在任何地方都能跑
·6 分鐘· loading · loading
Python Docker Container Devops 部署
Streamlit:用 Python 快速打造互動式資料應用
·8 分鐘· loading · loading
Python Streamlit Data-Visualization Web-App Dashboard
Python Logging:別再 print 了,用正經的方式記錄日誌吧
·6 分鐘· loading · loading
Python Logging Debug 標準庫
Pre-commit Hooks:讓壞 Code 連 Commit 的機會都沒有
·4 分鐘· loading · loading
Python Pre-Commit Git Linter Code-Quality Ruff Mypy