Python 装饰器完全指南:从原理到实战,彻底搞懂这个优雅特性

如果你用 Python 写过 Web 框架、做过权限控制、或者接触过 Flask/Django,你一定见过 @login_required@app.route 这样的写法。这就是装饰器。很多人会用,但不一定真正理解它背后的原理。今天我们从头到尾把装饰器彻底搞清楚。

一、装饰器的本质

在理解装饰器之前,需要先明确一件事:Python 中函数是一等公民。这意味着函数可以像普通变量一样被传递、赋值、作为参数传入另一个函数,也可以作为返回值。

装饰器本质上就是一个接收函数作为参数、返回新函数的高阶函数。它的作用是在不修改原函数代码的前提下,给函数增加额外的功能。这符合软件设计中的”开闭原则”——对扩展开放,对修改关闭。

当你写 @my_decorator 放在函数定义上面时,Python 解释器实际上做的是:func = my_decorator(func)。这个语法糖让代码更简洁,但理解了这个等价关系,装饰器就没什么神秘的了。

二、从零手写一个装饰器

我们来写一个最简单的计时装饰器,记录函数的执行时间:

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:.4f}s')
        return result
    return wrapper

@timer
def process_data(n):
    time.sleep(n)
    return f'处理了 {n} 秒'

process_data(1)  # process_data 执行耗时: 1.0012s

这里有几个细节值得注意:

*args, **kwargs 让 wrapper 能接受任意参数,这样装饰器才能通用,不受原函数签名限制。

@wraps(func) 是 functools 提供的工具,它的作用是把原函数的 __name____doc__ 等元信息复制到 wrapper 上。如果不加这个,process_data.__name__ 会变成 'wrapper',在调试和日志中会造成困惑。

三、带参数的装饰器

有时候我们希望装饰器本身也能接受参数,比如 @retry(max_attempts=3)。这时候需要再包一层函数:

def retry(max_attempts=3, delay=1):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise  # 最后一次失败,抛出异常
                    print(f'第 {attempt + 1} 次失败: {e},{delay}s 后重试...')
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=3, delay=2)
def call_api(url):
    # 模拟不稳定的网络请求
    import random
    if random.random() < 0.7:
        raise ConnectionError('网络超时')
    return '请求成功'

理解这个三层嵌套的关键是:最外层 retry 接收装饰器参数,返回真正的装饰器 decoratordecorator 接收被装饰的函数,返回 wrapperwrapper 才是最终执行的函数。

四、装饰器的实际应用场景

装饰器在实际项目中的应用非常广泛,以下几个场景几乎每个项目都会用到:

权限验证:这是 Web 开发中最常见的用法。在视图函数上加一个 @login_required,就能在不修改业务逻辑的情况下,统一处理未登录的情况。Flask 的路由注册 @app.route、Django 的 @permission_required 都是这个思路。

缓存:Python 标准库的 @functools.lru_cache 就是一个缓存装饰器,能自动缓存函数的计算结果。对于计算密集型函数,加上这个装饰器可以大幅提升性能。

日志记录:在函数执行前后自动记录日志,包括入参、出参、执行时间、异常信息等,不需要在每个函数里手动写日志代码。

参数校验:在函数执行前自动校验参数类型和范围,不合法直接抛出异常,让业务代码更干净。

事务管理:数据库操作自动开启事务,成功提交,失败回滚,不需要在每个函数里写 try/except/commit/rollback。

五、类装饰器与装饰类

装饰器不只能装饰函数,也可以用类来实现装饰器,还可以装饰整个类。

用类实现装饰器时,需要实现 __call__ 方法,让类的实例可以像函数一样被调用。这种方式的好处是可以在装饰器中维护状态,比如统计函数被调用的次数:

class CallCounter:
    def __init__(self, func):
        wraps(func)(self)
        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)

@CallCounter
def say_hello():
    print('Hello!')

say_hello()  # say_hello 已被调用 1 次
say_hello()  # say_hello 已被调用 2 次
print(say_hello.count)  # 2

六、多个装饰器叠加的执行顺序

当一个函数上叠加了多个装饰器时,执行顺序是从下到上装饰,从上到下执行

也就是说,离函数最近的装饰器最先包裹函数,但在调用时最后执行。这个顺序有时候会影响结果,比如先做权限验证再做日志记录,和先做日志记录再做权限验证,行为是不同的。在使用多个装饰器时,要注意它们的顺序。

七、总结

装饰器是 Python 中实现横切关注点(Cross-Cutting Concerns)的优雅方式。它让你能在不侵入业务代码的前提下,统一处理日志、缓存、权限、重试等通用逻辑,让代码更加干净和可维护。

理解装饰器的关键是:函数是一等公民,装饰器是高阶函数,@decorator 只是语法糖。掌握了这个本质,无论多复杂的装饰器写法都能看懂。

更多 Python 实战内容,欢迎持续关注冉冉博客。

© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容