037、装饰器三部曲(二):带参装饰器、类装饰器与 functools.wraps

037、装饰器三部曲(二):带参装饰器、类装饰器与 functools.wraps

一个让我熬夜到凌晨三点的Bug

上周五晚上,我正美滋滋地准备下班,突然收到线上告警:某个核心API的响应时间从200ms飙升到了5秒。我赶紧翻日志,发现所有被装饰器包装过的函数,它们的__name__属性全变成了wrapper。更诡异的是,某个带参数的装饰器,参数传进去后居然被“吞掉”了,导致配置完全失效。

这种问题,十有八九是装饰器写崩了。我一边骂骂咧咧地打开IDE,一边心想:要是当初写装饰器的时候,把functools.wraps和参数传递的细节搞清楚,也不至于现在被拉出来加班。

带参装饰器:别被“两层嵌套”吓到

先看一个最常见的场景:你想写一个装饰器,能控制是否打印日志,或者指定日志级别。

# 别这样写!参数会丢失
def log(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"[{level}] 调用 {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@log("INFO")
def say_hello(name):
    return f"Hello, {name}"

这段代码看起来没问题,但如果你在wrapper里想访问level,它确实能访问到——因为闭包捕获了外层变量。真正坑人的是,当你需要动态修改level时,你会发现闭包里的level是只读的,除非你用nonlocal声明。

这里踩过坑:我曾经写过一个带参装饰器,用来控制缓存过期时间。参数传进去后,在wrapper里修改了level变量,结果Python报错UnboundLocalError。解决方案很简单:在wrapper里用nonlocal level声明一下。

def cache(expire=300):
    def decorator(func):
        cache_data = {}
        def wrapper(*args, **kwargs):
            nonlocal expire  # 这里踩过坑,不加nonlocal会报错
            # 缓存逻辑...
            return func(*args, **kwargs)
        return wrapper
    return decorator

类装饰器:用__call__实现更优雅的状态管理

函数装饰器写多了,你会发现一个问题:当装饰器需要维护状态(比如统计调用次数、缓存数据)时,用函数闭包会显得很别扭。这时候,类装饰器就派上用场了。

class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0
    
    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"第 {self.count} 次调用 {self.func.__name__}")
        return self.func(*args, **kwargs)

@CountCalls
def greet(name):
    return f"Hi, {name}"

类装饰器的好处是:状态变量(比如count)直接挂在实例上,不用纠结闭包里的变量作用域。但别这样写:如果你在__init__里做了耗时操作,比如读取配置文件,那每次装饰一个函数都会执行一次——这通常不是你想要的。

更骚的操作是:用类装饰器实现带参数的装饰器。这时候需要三层嵌套:类初始化接收参数,__call__接收函数,再返回一个包装函数。

class Retry:
    def __init__(self, max_retries=3):
        self.max_retries = max_retries
    
    def __call__(self, func):
        def wrapper(*args, **kwargs):
            for i in range(self.max_retries):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"第 {i+1} 次重试失败: {e}")
            raise Exception("重试耗尽")
        return wrapper

@Retry(max_retries=5)
def unstable_api():
    import random
    if random.random() < 0.7:
        raise ConnectionError("网络波动")
    return "成功"

functools.wraps:救命的“身份恢复术”

回到开头的那个Bug。为什么被装饰的函数,__name__会变成wrapper?因为装饰器本质上是用wrapper替换了原函数。Python解释器不知道你包装了谁,它只知道现在这个函数叫wrapper

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("before")
        result = func(*args, **kwargs)
        print("after")
        return result
    return wrapper

@my_decorator
def test():
    """这是一个测试函数"""
    pass

print(test.__name__)  # 输出: wrapper
print(test.__doc__)   # 输出: None

这会导致什么问题?如果你用inspect模块做参数检查,或者用help()查看文档,全都会乱套。更严重的是,某些框架(比如Flask的路由注册)依赖函数的__name__来生成URL,这时候装饰器就会破坏路由映射。

解决方案就是functools.wraps。它会把原函数的__name____doc____module____annotations__等属性复制到wrapper上。

from functools import wraps

def my_decorator(func):
    @wraps(func)  # 这里加一行,救你一命
    def wrapper(*args, **kwargs):
        print("before")
        result = func(*args, **kwargs)
        print("after")
        return result
    return wrapper

@my_decorator
def test():
    """这是一个测试函数"""
    pass

print(test.__name__)  # 输出: test
print(test.__doc__)   # 输出: 这是一个测试函数

别这样写:有些人图省事,只在最外层的装饰器上加@wraps,内层嵌套的装饰器就不加了。结果就是,多层装饰器叠加后,__name__依然会丢失。正确的做法是:每个wrapper函数上都要加@wraps

实战:一个带参数、带状态、保留元数据的完整装饰器

把上面所有知识点揉在一起,写一个生产级别的装饰器:

from functools import wraps
import time

class RateLimiter:
    """限流装饰器,支持自定义速率和单位"""
    
    def __init__(self, max_calls=10, period=1.0):
        self.max_calls = max_calls
        self.period = period
        self.calls = []
    
    def __call__(self, func):
        @wraps(func)  # 保留原函数信息
        def wrapper(*args, **kwargs):
            now = time.time()
            # 清理过期记录
            self.calls = [t for t in self.calls if now - t < self.period]
            
            if len(self.calls) >= self.max_calls:
                raise Exception(f"超过限流阈值: {self.max_calls}次/{self.period}秒")
            
            self.calls.append(now)
            return func(*args, **kwargs)
        return wrapper

@RateLimiter(max_calls=5, period=2.0)
def api_request(url):
    """发送API请求"""
    return f"请求 {url} 成功"

这个装饰器:支持参数配置、维护调用记录状态、保留原函数的元数据。如果你在线上看到类似的代码,大概率是踩过坑之后重构出来的。

个人经验:什么时候该用哪种装饰器

  • 简单日志、计时、权限校验:用函数装饰器,轻量、直观。
  • 需要维护状态(计数、缓存、限流):用类装饰器,状态管理更清晰。
  • 装饰器本身需要参数:函数装饰器用三层嵌套,类装饰器用__init__+__call__
  • 任何装饰器:只要写了wrapper,就加上@wraps(func),这是最低成本的防御性编程。

最后说一句:别迷信“一行代码实现XX”的装饰器。真正生产环境用的装饰器,往往需要处理异常、日志、超时、重试、限流等复杂逻辑。把基础打牢,遇到问题才能快速定位——就像我那个凌晨三点的Bug,最后发现只是少了个@wraps

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值