装饰器如何工作,常用哪些?
未标记
考查重点:包装调用与函数契约。装饰器接原函数并返回包装对象,调用前后加日志/计时等;透传返回异常并用wraps保元信息
本题目录
参考答案
核心回答
Python 装饰器是在函数或类定义时接收被装饰对象,并把返回结果重新绑定到原名称的一种机制。最常见的写法是返回一个包装函数,在调用原函数前后加入计时、日志、鉴权或缓存等能力,同时透传参数、返回值和异常。
@deco 作用于函数 f,可以近似理解成定义后执行 f = deco(f)。装饰器不一定只能是函数,也可以是可调用对象;返回结果也不一定仍是普通函数,例如属性或类方法装饰器会改变访问和绑定方式。
一个计时装饰器怎么写
下面是同步函数的参考实现,使用 perf_counter 计量经过时间。*args、**kwargs 透传参数,return 透传结果,finally 让失败调用也执行计时输出;没有捕获并吞掉原业务异常。
from functools import wraps
from time import perf_counter
def timed(func):
@wraps(func)
def wrapper(*args, **kwargs):
started = perf_counter()
try:
return func(*args, **kwargs)
finally:
elapsed = perf_counter() - started
print(f"{func.__name__}: {elapsed:.6f}s")
return wrapper
@timed
def total(a: int, b: int, *, scale: int = 1) -> int:
"""按比例计算合计。"""
return (a + b) * scale
assert total(2, 3, scale=4) == 20
assert total.__name__ == "total"
assert total.__doc__ == "按比例计算合计。"
assert total.__wrapped__(2, 3, scale=4) == 20
@wraps(func) 会复制函数名、文档等元信息,并设置指向被包装函数的 __wrapped__,有利于调试与内省。它不会替你调用原函数、补上漏写的 return,也不会把包装器的真实参数列表改成原函数的列表。需要给静态类型检查器保留参数类型时,可以进一步使用 ParamSpec 等类型标注;复制元信息与类型检查是不同层次。
示例中的输出只用于演示计时。实际记录时还需避免日志失败覆盖业务异常,并明确统计的是单层包装、整个调用还是重试后的总耗时,不能只看一个数字就当作业务性能结论。
装饰发生在什么时候,多个装饰器如何排序
装饰发生在执行函数定义语句时,通常就是导入模块或执行到相应定义时,而不是每次调用业务函数时。包装函数内部的代码才通常在后续调用时执行。带参数的 @deco(option) 则先调用工厂取得装饰器,再用它装饰目标对象。
若从上到下写 @outer、@inner,近似展开为 f = outer(inner(f)):先应用离函数近的 inner,再应用 outer。如果装饰器表达式本身有副作用,例如两个工厂调用,表达式按源码从上到下求值,随后才由内向外应用,不能把这两件事都概括成“从下往上执行”。
对于常见的前后包装,调用时依次进入 outer、进入 inner、执行原函数,再退出 inner、退出 outer。顺序会改变语义,例如鉴权放在缓存外层,可以在命中缓存前校验权限;缓存键仍需包含必要的身份和权限范围。不能只因为两种排列都能运行,就认为效果相同。
异步函数为什么要单独处理
普通同步包装器调用 async def 函数,只会取得协程对象。如果此时就结束计时,统计到的是创建协程的时间,实际等待和执行还没有完成。下面的异步版本在包装器中 await 原函数,覆盖到异步调用完成或退出的时刻。
import asyncio
from functools import wraps
from time import perf_counter
def timed_async(func):
@wraps(func)
async def wrapper(*args, **kwargs):
started = perf_counter()
try:
return await func(*args, **kwargs)
finally:
elapsed = perf_counter() - started
print(f"{func.__name__}: {elapsed:.6f}s")
return wrapper
@timed_async
async def async_total(a, b, *, scale=1):
await asyncio.sleep(0)
return (a + b) * scale
assert asyncio.run(async_total(2, 3, scale=4)) == 20
此处计量的是经过时间,会包含等待,不能当成纯 CPU 时间。异步异常和取消仍向上传播,finally 执行清理或记录。wraps 本身不会把同步包装器变成协程函数;生成器、异步生成器的逐次迭代也有自己的生命周期,不能直接套这两个例子就声称覆盖了全部执行时间。
常用哪些装饰器
@property:把方法作为属性访问的接口,可配合 setter 等控制写入与校验。@classmethod:方法接收调用时相应的类作为隐式第一个参数,常用于替代构造入口。@staticmethod:不自动绑定实例或类,适合放在类命名空间内的独立逻辑。@functools.wraps:编写包装器时保留被包装对象的元信息。@functools.lru_cache、@functools.cache:缓存可复用的计算结果。需要可哈希参数,并考虑失效和内存;不应把有副作用操作或普通协程对象直接当作可重复缓存的结果。- 自定义的计时、日志、参数校验和鉴权装饰器,以及框架提供的路由、注册类装饰器:具体返回类型、调用方式和顺序,要看各自契约。
常见追问
闭包和装饰器有什么关系?
上面的包装函数会保存对 func 的引用,带参数的版本还会保存配置,这通常通过闭包完成。闭包是实现手段,装饰器是定义时应用可调用对象并重新绑定的机制,二者并不等同。
忘记返回包装函数或业务结果会怎样?
外层忘记返回包装函数,原名称通常会被绑定成 None;包装函数忘记返回原调用结果,调用者就会拿到 None。使用 wraps 也不会修复这两类错误。
是否任何重试、缓存都适合做成装饰器?
只有当副作用、重试条件、缓存键和失效规则清晰时才合适。一个通用包装不应悄悄把业务调用变成多次扣款,或把不同用户的结果混用。复杂流程需要显式表达状态,不能全部藏进多层装饰。
对应教程
《17-Tools 工具调用》 的“使用 @tool 装饰器”展示了将函数转为框架工具的应用,帮助理解装饰结果未必仍按原函数方式使用;Python 的装饰时机、元信息和同步/异步包装机制可结合本文与语言规范学习。
参考资料
- 国内:廖雪峰:装饰器:借鉴普通、带参数包装与函数重新绑定和 wraps 示例,解释正文中装饰器工厂、定义时装饰与调用时执行包装的区别。
- 国内:骆昊:函数高级应用:参考计时逻辑提取为装饰器的过程,说明正文示例中位置参数、关键字参数、返回值和异常怎样透传。
- 国内:dongwm:Python 3.10 新增的类型系统特性:借鉴 PEP 612 的装饰器例子,解释正文中通用参数包装的静态类型信息损失及 ParamSpec 的作用,区分类型标注与元信息保留。
- 国外:Python 3.14:functools:
update_wrapper、wraps与缓存装饰器的契约,明确__wrapped__和元信息的用途,也说明哪些结果不适合普通函数缓存。 - 国外:Python 3.14:Function definitions 与 Coroutines:依据函数定义、装饰器表达式求值、嵌套应用与协程函数语义,解释正文中装饰时机、堆叠顺序和异步包装必须 await 的原因。


