1. 包装
装饰器(decorator)是一种接收函数、再返回新函数的高阶函数。新函数通常会先执行一些附加逻辑,再调用原函数。
例如,很多函数都需要记录「开始执行」和「执行结束」。如果每个函数都手动写一遍,代码会重复。可以把这段公共逻辑放进装饰器:
01def log_call(function):02def wrapper():03print("开始执行")04result = function()05print("执行结束")06return result0708return wrapper0910def greet():11print("你好,Python")1213greet = log_call(greet)14greet()
log_call() 接收原来的 greet,返回一个新的 wrapper。重新赋值后,调用 greet() 实际上是在调用包装函数,原来的问候逻辑仍然由 function() 执行。
装饰器的重点是「在不修改原函数主体的情况下,为它增加行为」。日志、计时、权限检查、缓存和重试都适合抽成这种公共包装逻辑。
2. 语法
Python 提供 @ 语法,让装饰器的使用更直观:
01def log_call(function):02def wrapper():03print("开始执行")04result = function()05print("执行结束")06return result0708return wrapper0910@log_call11def greet():12print("你好,Python")1314greet()
函数定义前的 @log_call 等价于下面的重新赋值:
01def log_call(function):02def wrapper():03print("开始执行")04result = function()05print("执行结束")06return result0708return wrapper0910def greet():11print("你好,Python")1213greet = log_call(greet)14greet()
装饰器在函数定义完成时执行一次,而不是每次调用函数时重新执行。也就是说,模块加载过程中先创建 greet,再调用 log_call(greet) 得到新的函数;以后每次 greet() 都只调用已经生成的包装函数。
这也解释了装饰器的两个调用阶段:装饰阶段接收函数并返回包装函数,运行阶段才由包装函数接收真正的业务参数。
3. 参数
真实函数通常有位置参数、关键字参数和返回值,包装函数应该使用 *args 与 **kwargs 接收它们,并把结果返回给调用方:
01from functools import wraps0203def log_call(function):04@wraps(function)05def wrapper(*args, **kwargs):06print(f"开始执行:{function.__name__}")07result = function(*args, **kwargs)08print(f"执行结束:{function.__name__}")09return result1011return wrapper1213@log_call14def add(left, right):15return left + right1617print(add(2, right=3)) # 5
*args 收集位置参数,**kwargs 收集关键字参数;调用原函数时再次展开它们,才能保持原函数的调用方式。return result 也很重要,否则装饰后的函数会丢失原来的返回值。
@wraps(function) 来自 functools,它会把原函数的名称、文档字符串等信息复制到包装函数上。没有它时,add.__name__ 会变成 wrapper,调试、日志和框架读取函数信息时都不够准确。
包装函数还可以处理异常,但不要无意中吞掉异常。下面的 finally 无论成功还是失败都会执行,原异常仍然会继续抛出:
01from functools import wraps0203def log_result(function):04@wraps(function)05def wrapper(*args, **kwargs):06try:07return function(*args, **kwargs)08finally:09print(f"调用完成:{function.__name__}")1011return wrapper1213@log_result14def divide(left, right):15return left / right1617print(divide(6, 2))
4. 配置
有时装饰器本身也需要参数,例如希望调用函数三次。此时需要多包一层:最外层接收配置,中间层接收函数,最内层才接收业务参数:
01from functools import wraps0203def repeat(times):04def decorator(function):05@wraps(function)06def wrapper(*args, **kwargs):07result = None08for _ in range(times):09result = function(*args, **kwargs)1011return result1213return wrapper1415return decorator1617@repeat(3)18def greet(name):19print(f"你好,{name}")2021greet("小明")
@repeat(3) 并不是把数字直接传给 greet()。它会先执行 repeat(3),得到 decorator;再用 decorator(greet) 得到最终的 wrapper。调用 greet("小明") 时,wrapper 才会循环调用原函数三次。
装饰器配置还可以用于权限检查。把权限名称提前传入后,同一个装饰器工厂就能生成不同规则:
01from functools import wraps0203def require_role(required_role):04def decorator(function):05@wraps(function)06def wrapper(user, *args, **kwargs):07if user["role"] != required_role:08raise PermissionError("没有执行权限")0910return function(user, *args, **kwargs)1112return wrapper1314return decorator1516@require_role("admin")17def delete_article(user, article_id):18return f"已删除文章 {article_id}"1920admin = {"name": "小明", "role": "admin"}21print(delete_article(admin, 10))
在 FastAPI 项目中,鉴权、日志和限流也可以采用类似思路。不过框架通常已经提供了依赖注入或中间件机制,应该先使用框架的标准能力,再决定是否自定义装饰器。
5. 组合
一个函数可以同时使用多个装饰器。装饰器从下往上应用,但调用时从上往下进入:
01from functools import wraps0203def uppercase(function):04@wraps(function)05def wrapper(*args, **kwargs):06return function(*args, **kwargs).upper()0708return wrapper0910def add_prefix(function):11@wraps(function)12def wrapper(*args, **kwargs):13return "结果:" + function(*args, **kwargs)1415return wrapper1617@add_prefix18@uppercase19def message():20return "hello"2122print(message()) # 结果:HELLO
上面的写法等价于:
01from functools import wraps0203def uppercase(function):04@wraps(function)05def wrapper(*args, **kwargs):06return function(*args, **kwargs).upper()0708return wrapper0910def add_prefix(function):11@wraps(function)12def wrapper(*args, **kwargs):13return "结果:" + function(*args, **kwargs)1415return wrapper1617def message():18return "hello"1920message = add_prefix(uppercase(message))21print(message()) # 结果:HELLO
调用 message() 时,先进入外层的 add_prefix 包装函数;它内部再调用 uppercase 包装函数,最后才执行原始的 message。装饰器较多时,要注意顺序,因为交换顺序可能改变最终结果。
Python 自带和标准库中也有很多装饰器,例如类文章中介绍过的 @property、@classmethod、@staticmethod,以及数据类使用的 @dataclass。它们本质上都在函数或类定义完成时,对定义结果进行包装或改造。
异步函数需要使用异步包装函数,否则包装器无法正确等待原函数:
01from functools import wraps0203def log_async(function):04@wraps(function)05async def wrapper(*args, **kwargs):06print("异步任务开始")07result = await function(*args, **kwargs)08print("异步任务结束")09return result1011return wrapper
这类装饰器适合 FastAPI 的异步接口或异步 LangChain 调用。同步函数和异步函数不能直接使用同一种包装方式,需要保持 async 和 await 的调用模型一致。
6. 取舍
装饰器适合给多个函数增加相同的横切逻辑,但它也会增加调用链的间接层。看到一个被装饰的函数时,读者需要跳转到装饰器才能了解完整行为。
可以按照下面的原则判断是否使用装饰器:
- 多个函数都需要相同的日志、计时、权限或重试逻辑时,适合使用装饰器;
- 业务逻辑只出现一次,直接写在函数中通常更容易理解;
- 装饰器应该保持职责单一,不要在一个包装器里同时完成鉴权、缓存和异常转换;
- 尽量使用
@wraps,保留原函数的名称和文档信息; - 包装函数要正确传递参数、返回值和异常;
- 为异步函数编写装饰器时,要使用
async def和await; - FastAPI 等框架已经提供中间件或依赖注入时,优先采用框架的标准扩展点。
装饰器不是为了让代码看起来更高级,而是为了把重复的外围逻辑集中维护。只要增加的抽象让调用关系更难理解,就应该考虑改回普通函数或使用更明确的服务对象。