一、前言 #
嗨!拍拍君來了 ✨
你有沒有在別人的 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)。
這表示函式可以:
- 被指派給變數
- 當作參數傳給其他函式
- 從另一個函式回傳
# 函式可以指派給變數
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()
輸出:
嗨!
嗨!
嗨!
看起來有點複雜,但邏輯是清楚的:
repeat(times=3)回傳decoratordecorator(say_hi)回傳wrapper- 呼叫
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):
...
執行順序是由內到外(由下到上):
- 先套用
@retry→ 得到重試版 - 再套用
@log_calls→ 加上日誌 - 最後套用
@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 裡最能展現「優雅」的語法之一,一旦學會了,你會發現到處都能用它。但記住:不要為了炫技而濫用裝飾器——保持程式碼的可讀性永遠是第一優先。
下次當你看到 @ 的時候,就不會再覺得它是什麼神秘魔法了。它就是一件超能力外套,穿上去就能飛 🦸
拍拍君,下次見!