装饰器本质是函数套函数,利用函数为一等对象特性,通过@语法糖将被装饰函数传入装饰器并替换原函数名;带参装饰器需三层嵌套;functools.wraps必须使用以保留原函数元信息;类装饰器需实现__call__并手动同步属性。

装饰器本质是函数套函数
python 装饰器没有魔法,它只是利用了函数是一等对象(first-class Object)这一特性:函数可以被赋值给变量、作为参数传入其他函数、在函数里定义并返回。所谓 @decorator 语法糖,底层就是把被装饰函数当作参数传给 decorator,再用返回值替换原函数名。
比如:
def my_decorator(func): def wrapper(*args, **kwargs): print("before") result = func(*args, **kwargs) print("after") return result return wrapper @my_decorator def say_hello(): print("hello")
等价于:
def say_hello(): print("hello") say_hello = my_decorator(say_hello)
关键点在于:my_decorator 必须返回一个可调用对象(通常是函数),且该对象要能接收和转发 say_hello 原本的参数 —— 否则调用会出错。
立即学习“Python免费学习笔记(深入)”;
带参数的装饰器需要三层嵌套
当你写 @decorator(arg=1) 时,Python 要求 decorator(arg=1) 的结果本身是一个装饰器(即接收函数并返回新函数)。所以必须多包一层:
- 最外层函数接收装饰器参数(如
arg),返回真正的装饰器 - 中间层是标准装饰器,接收被装饰函数,返回包装函数
- 最内层是实际执行逻辑的
wrapper
常见错误是漏掉某一层,导致 TypeError: 'function' object is not callable 或 missing 1 required positional argument: 'func'。
示例:
def repeat(times): def decorator(func): # ← 这才是真正的装饰器 def wrapper(*args, **kwargs): for _ in range(times): result = func(*args, **kwargs) return result return wrapper return decorator # ← 返回装饰器,不是 wrapper
functools.wraps 不是可选的,是必须的
如果不加 @functools.wraps(func),被装饰后的函数会丢失原始函数的元信息:__name__ 变成 'wrapper',__doc__ 为空,help() 查不到原说明,调试和反射类工具(如 flask 路由注册、pytest 参数提取)会出问题。
正确写法:
from functools import wraps def my_decorator(func): @wraps(func) # ← 必须加这一行 def wrapper(*args, *kwargs): return func(args, **kwargs) return wrapper
它本质上是把 func 的 __name__、__doc__、__module__ 等属性复制到 wrapper 上。不加的话,所有基于函数签名的自动化流程都可能静默失败。
类装饰器依赖 __call__ 方法
类也可以当装饰器用,前提是实现了 __call__。实例化时传入被装饰函数,调用时行为像函数:
class CountCalls: def __init__(self, func): self.func = func self.count = 0 # 注意:这里仍需 wraps,否则 __name__ 丢失 from functools import wraps self.__wrapped__ = func self.__name__ = func.__name__ self.__doc__ = func.__doc__ def __call__(self, *args, **kwargs): self.count += 1 return self.func(*args, **kwargs)
但要注意:类装饰器无法直接用 @functools.wraps(它是为函数设计的),得手动同步属性,或继承 functools.update_wrapper 的逻辑。否则 ide 提示、文档生成工具会识别不到原始函数信息。
装饰器的底层其实很直白,难的是在保持语义清晰的同时,不破坏函数的身份特征 —— 尤其是在组合多个装饰器、配合类型检查(mypy)或异步上下文时,元信息丢失和参数透传问题会立刻暴露。