📌 知识点简介
装饰器是 Python 最优雅的特性之一,但很多开发者停留在 @timer 这种基础用法。今天深入两层:带参数的装饰器 和 类装饰器,封装几个生产环境可直接复用的工具函数。
🔧 核心代码
1. 带参数的装饰器(三层嵌套)
python
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. 类装饰器(带状态)
python
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 的重要性
python
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"}
🧪 使用示例
python
# ---- 重试装饰器 ----
@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 包装配置 |
| 性能开销 | 每个装饰器调用都增加函数调用栈 | 生产环境适量使用,高频路径避免多层装饰器堆叠 |
简化带参装饰器的技巧(减少嵌套)
python
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 简化嵌套这四个要点,日常开发中无论是埋点、缓存、重试还是权限校验,都能信手拈来。