Python 装饰器实战:从计时器到重试退避

举报
yd_269685704 发表于 2026/11/01 10:10:10 2026/11/01
【摘要】 在 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 是否保留了原函数信息。

五、小结

装饰器的学习曲线并不陡:先理解“函数可以作为参数和返回值”,再掌握“带参数的装饰器就是多一层闭包”,最后在实际项目里从计时、重试、日志这三个场景练起。把横切关注点从业务代码里抽出来,代码会立刻清爽很多。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。