📌 知识点简介
装饰器是 Python 最优雅的特性之一,但很多开发者停留在 @timer 这种基础用法。今天深入两层:带参数的装饰器 和 类装饰器,封装几个生产环境可直接复用的工具函数。
🔧 核心代码
1. 带参数的装饰器(三层嵌套)
from functools import wraps import time import logging logger = logging.getLogger(__name__) def retry(max_attempts=3, delay=1, exceptions=(Exception,)): """可配置重试装饰器""" def decorator(func): @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: raise logger.warning( f"{func.__name__} 第 {attempt} 次失败: {e}," f"{delay}s 后重试..." ) time.sleep(delay) return None # 不会执行到这里,仅供类型检查 return wrapper return decorator def timed(unit="s", precision=4): """可配置单位的计时装饰器""" scale = {"s": 1, "ms": 1000, "μs": 1_000_000} def decorator(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = (time.perf_counter() - start) * scale.get(unit, 1) print(f"[⏱] {func.__name__} 耗时: {elapsed:.{precision}f} {unit}") return result return wrapper return decorator
2. 类装饰器(带状态)
class CountCalls: """统计函数调用次数的类装饰器""" def __init__(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) class Singleton: """将任意类变为单例的装饰器""" def __init__(self, cls): self.cls = cls self._instance = None def __call__(self, *args, **kwargs): if self._instance is None: self._instance = self.cls(*args, **kwargs) return self._instance
3. 装饰器堆叠 & functools.wraps 的重要性
def debug_info(func): """打印传入/传出参数的调试装饰器""" @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) print(f"[🔍] 调用: {func.__name__}({signature})") result = func(*args, **kwargs) print(f"[🔍] 返回: {result!r}") return result return wrapper # 堆叠使用(执行顺序从下往上,从里到外) @timed(unit="ms") @debug_info def fetch_user(user_id: int) -> dict: return {"id": user_id, "name": "Alice"}
🧪 使用示例
# ---- 重试装饰器 ---- @retry(max_attempts=3, delay=0.5, exceptions=(ConnectionError, TimeoutError)) def fetch_data(url: str) -> str: # 模拟不稳定网络请求 import random if random.random() < 0.7: raise ConnectionError("网络波动") return f"data from {url}" res = fetch_data("https://api.example.com/data") print(res) # ---- 计时装饰器 ---- @timed(unit="ms") def heavy_compute(n: int) -> int: return sum(i * i for i in range(n)) heavy_compute(10_000_000) # ---- 计数装饰器 ---- @CountCalls def greet(name: str) -> str: return f"Hello, {name}!" print(greet("Alice")) # [📊] greet 已调用 1 次 print(greet("Bob")) # [📊] greet 已调用 2 次 # ---- 单例装饰器 ---- @Singleton class Database: def __init__(self, host: str): self.host = host db1 = Database("localhost") db2 = Database("remote") # 不会重新初始化 print(db1 is db2) # True print(db1.host) # "localhost"(首次创建的值) # ---- 函数元数据保留 ---- @debug_info def simple_add(a: int, b: int) -> int: """将两个数相加""" return a + b # 没有 @wraps 的话,这里会显示 wrapper 的信息 print(f"函数名: {simple_add.__name__}") # simple_add print(f"文档: {simple_add.__doc__}") # 将两个数相加
⚠️ 注意事项 / 避坑指南
| 坑点 | 说明 | 解决 |
|---|---|---|
| 元数据丢失 | 不加 @wraps(func) 会导致 __name__、__doc__、__annotations__ 全被覆盖 |
每个 wrapper 上务必加 @wraps(func) |
| 多层装饰器顺序 | @A @B 等价于 A(B(func)),A 在最外层 |
把耗时/日志放外面,重试/缓存放里面 |
类装饰器的 __init__ 签名 |
class-based 的 __init__ 接收的是函数,容易和类本身的 __init__ 混淆 |
用函数装饰器做复杂场景,类装饰器做简单计数/单例 |
| 装饰器参数类型检查 | 三层嵌套的装饰器容易把参数写混淆 | 善用 functools.partial 简化,或是使用 @dataclass 包装配置 |
| 性能开销 | 每个装饰器调用都增加函数调用栈 | 生产环境适量使用,高频路径避免多层装饰器堆叠 |
简化带参装饰器的技巧(减少嵌套)
from functools import partial def retry(func=None, *, max_attempts=3, delay=1): """同时支持 @retry 和 @retry(max_attempts=5) 两种用法""" if func is None: return partial(retry, max_attempts=max_attempts, delay=delay) @wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, max_attempts + 1): try: return func(*args, **kwargs) except Exception as e: if attempt == max_attempts: raise time.sleep(delay) return None return wrapper # 无括号用法 @retry def api_call_1(): ... # 带参数用法 @retry(max_attempts=5, delay=2) def api_call_2(): ...
📝 总结
装饰器是 Python 的"语法糖核弹"------用得好,代码复用率和可读性暴涨;用不好,层层嵌套让调试想哭。掌握带参装饰器、类装饰器、wraps 保护元数据、以及 partial 简化嵌套这四个要点,日常开发中无论是埋点、缓存、重试还是权限校验,都能信手拈来。