Python 装饰器的原理是什么?为什么要用 functools.wraps?
简化版
装饰器本质上是一个接收函数并返回新函数的高阶函数。@decorator 等价于 func = decorator(func);functools.wraps 用来保留原函数的名称、文档、注解等元信息,避免调试、日志和框架识别出问题。
详细版
最简单的装饰器:
def log(func):
def wrapper(*args, **kwargs):
print("before")
result = func(*args, **kwargs)
print("after")
return result
return wrapper
@log
def add(a, b):
return a + b
等价于:
def add(a, b):
return a + b
add = log(add)
调用 add(1, 2) 时,实际调用的是 wrapper(1, 2),wrapper 内部再调用原来的 add。
推荐写法:
from functools import wraps
def log(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"call {func.__name__}")
return func(*args, **kwargs)
return wrapper
不用 wraps 时,装饰后的函数名会变成 wrapper,__doc__、__annotations__ 等信息也可能丢失。
完整版教学
一、装饰器解决什么问题
装饰器用于在不修改原函数代码的前提下,为函数增加通用能力。
常见场景:
- 日志记录;
- 权限校验;
- 性能计时;
- 缓存;
- 事务控制;
- 重试;
- 参数校验。
例如业务函数只关心业务:
def get_user(user_id):
return query_user(user_id)
如果每个函数都手写日志、耗时统计、异常处理,业务代码会被横切逻辑污染。装饰器可以把这些通用逻辑抽出去。
二、函数是一等对象
装饰器能成立,是因为 Python 中函数是一等对象。函数可以:
- 赋值给变量;
- 作为参数传给另一个函数;
- 作为返回值返回;
- 放进列表或字典。
def hello():
return "hello"
f = hello
print(f())
装饰器正是利用了“函数可以作为参数和返回值”这个特性。
三、@语法的真实含义
下面代码:
@log
def add(a, b):
return a + b
等价于:
def add(a, b):
return a + b
add = log(add)
所以装饰器不是运行时每次调用前才“临时包一下”,而是在函数定义完成后,立即把函数对象传给装饰器,并把返回值重新绑定给原函数名。
这也是为什么装饰器可以改变函数行为:
def always_zero(func):
def wrapper(*args, **kwargs):
return 0
return wrapper
装饰后,函数名已经指向 wrapper,不是原始函数。
四、为什么 wrapper 要接收 *args 和 **kwargs
如果装饰器只写固定参数:
def wrapper():
return func()
那它只能装饰无参函数。为了让装饰器适配各种函数签名,通常写:
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
这表示调用者传来的所有位置参数和关键字参数,都原样转交给原函数。
同时要记得返回原函数结果:
result = func(*args, **kwargs)
return result
如果忘了 return,被装饰函数的返回值会变成 None。
五、functools.wraps 的作用
不用 wraps:
def log(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@log
def add(a: int, b: int) -> int:
"""add two numbers"""
return a + b
print(add.__name__) # wrapper
这对调试、文档生成、日志、Web 框架路由、测试工具都不友好。
使用 wraps:
from functools import wraps
def log(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
wraps(func) 会把原函数的一些元信息复制到 wrapper 上,并设置 __wrapped__,让工具有机会追溯到原函数。
六、装饰器的副作用和注意点
写装饰器时常见问题:
- 忘记返回
wrapper; wrapper忘记返回原函数结果;- 不使用
wraps导致元信息丢失; - 装饰器吞异常后没有明确处理策略;
- 装饰异步函数时写成同步 wrapper,导致协程没有被正确 await。
异步函数装饰器要写成:
from functools import wraps
def log_async(func):
@wraps(func)
async def wrapper(*args, **kwargs):
print("before")
return await func(*args, **kwargs)
return wrapper
七、常见误区与追问
记忆钩子:装饰器不是“给函数加注释”,而是把函数名重新绑定到另一个可调用对象;
wraps则是在换壳以后尽量保留原函数身份。
- 误区:装饰器会在每次函数调用时重新装饰一次。
@log的应用发生在函数定义完成后,等价于add = log(add);之后每次调用add(),调用的是已经返回的wrapper。 - 追问:为什么 wrapper 通常要写
*args, **kwargs? 装饰器往往要适配不同签名的函数,透传位置参数和关键字参数能保持通用性;如果 wrapper 写死参数,就只能装饰特定形态的函数。 - 误区:装饰器里调用了原函数就一定保留返回值。 如果
wrapper里写了func(*args, **kwargs)却忘记return,调用方拿到的就是None,这是业务装饰器里很常见的隐藏 bug。 - 追问:
functools.wraps到底复制了什么? 它基于update_wrapper复制__name__、__doc__、__module__、__annotations__等元信息,并设置__wrapped__方便inspect等工具追溯原函数。 - 误区:用了 wraps 就完全等同于原函数。
wraps主要保留元信息,不会自动修复 wrapper 的调用语义;异常处理、异步 await、返回值、参数校验仍要由装饰器代码正确实现。 - 追问:装饰 async 函数时为什么要特别小心? 原函数是协程函数时,wrapper 也应是
async def并await func(...);否则可能返回一个未被 await 的协程对象,导致逻辑没有真正执行。
八、加强记忆
装饰器就是“函数进,函数出”,@log 等价于 func = log(func)。wrapper 负责增强逻辑,并用 *args, **kwargs 透传参数;wraps 负责保留原函数身份信息。面试讲清这三层,装饰器原理就稳了。