Python 装饰器实战:从计时器到重试退避
在 Python 项目里,经常会遇到这样的需求:给函数加耗时统计、失败自动重试、打印调用日志,但又不想把这些与业务无关的代码塞进每个函数体。装饰器正是为此而生的工具。本文从一个计时器出发,写到带参数的重试装饰器,并聊聊实际使用时的注意事项。
一、装饰器到底是什么
装饰器的本质是一个“函数套函数”:它接收一个函数作为输入,返回一个新的函数。新函数在调用原函数的前后可以做额外的事,而原函数的业务逻辑一行都不用改。下面是最简单的形态:
def my_decorator(func):
def wrapper(*args, **kwargs):
print('调用前')
result = func(*args, **kwargs)
print('调用后')
return result
return wrapper
@my_decorator
def greet(name):
print(f'你好,{name}')
greet('小明')
# 输出:调用前 / 你好,小明 / 调用后
@ 语法只是语法糖,@my_decorator 等价于 greet = my_decorator(greet)。理解了这一点,后面的带参数装饰器就好理解了。
二、第一个实用装饰器:计时器
定位慢接口、慢任务时,计时器是最常用的装饰器之一:
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f'{func.__name__} 耗时 {elapsed:.3f} 秒')
return result
return wrapper
@timer
def slow_task():
time.sleep(0.5)
return 'done'
这里用到了 functools.wraps:它把原函数的 __name__、文档字符串等元信息复制到 wrapper 上。没有它,装饰后的函数名字会变成 wrapper,调试和日志都会受影响,这是一个容易忽略但很重要的细节。
三、带参数的装饰器:重试与指数退避
调用外部接口时,偶发的网络抖动很常见。比起在每个调用处写 try/except 循环,一个可配置的重试装饰器要干净得多:
import time
import random
from functools import wraps
def retry(max_attempts=3, base_delay=1.0, exceptions=(Exception,)):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
delay = base_delay
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == max_attempts:
raise
jitter = random.uniform(0, 0.5)
print(f'第 {attempt} 次失败:{e},{delay + jitter:.1f} 秒后重试')
time.sleep(delay + jitter)
delay *= 2 # 指数退避
return wrapper
return decorator
@retry(max_attempts=3, base_delay=1.0, exceptions=(ConnectionError,))
def fetch_data():
... # 发起网络请求
几个设计要点值得注意:第一,退避时间指数增长(1 秒、2 秒、4 秒),避免在服务端已经过载时继续用固定频率轰炸;第二,加一点随机抖动(jitter),防止大量客户端在同一时刻重试形成“惊群”;第三,只捕获你明确想重试的异常类型,默认捕获所有 Exception 会把参数错误、逻辑错误也掩盖掉。
四、装饰器叠加与使用建议
多个装饰器可以叠加使用,执行顺序是从下往上贴近函数、从上往下执行:
@timer
@retry(max_attempts=3)
def fetch_data():
...
# 实际效果:timer 包在最外层,统计的是包含重试在内的总耗时
实际使用时还有三点建议:其一,重试只适用于幂等操作(查询、按幂等键提交),对“扣款”“下单”这类非幂等操作自动重试可能造成重复执行;其二,装饰器里不要藏太重的逻辑,保持“可观测、可开关”,例如通过环境变量控制是否启用详细日志;其三,装饰器会改变调用栈,排障时记得看 wraps 是否保留了原函数信息。
五、小结
装饰器的学习曲线并不陡:先理解“函数可以作为参数和返回值”,再掌握“带参数的装饰器就是多一层闭包”,最后在实际项目里从计时、重试、日志这三个场景练起。把横切关注点从业务代码里抽出来,代码会立刻清爽很多。
- 点赞
- 收藏
- 关注作者
评论(0)