如果你用 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 接收装饰器参数,返回真正的装饰器 decorator;decorator 接收被装饰的函数,返回 wrapper;wrapper 才是最终执行的函数。
四、装饰器的实际应用场景
装饰器在实际项目中的应用非常广泛,以下几个场景几乎每个项目都会用到:
权限验证:这是 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 实战内容,欢迎持续关注冉冉博客。














暂无评论内容