“它让装饰器不‘穿帮’,被装饰的函数还能假装自己是原来的函数。”
下面从 0 到源码,带你彻底看懂。
一、为什么需要 wraps?
装饰器会“偷梁换柱”
def dec(f):
def wrapper(*a, **kw):
return f(*a, **kw)
return wrapper
@dec
def add(x, y):
"""add two numbers"""
return x + y
print(add.__name__) # wrapper ← 名字被换掉了!
print(add.__doc__) # none ← 文档串也没了结果调试、ide 提示、sphinx 文档生成全都认错人。
functools.wraps 的作用
把原函数的 名字、文档、参数表、注解、模块名、属性字典 等关键元信息一次性拷贝到包装器上,解决“穿帮”问题。
二、最简正确姿势
from functools import wraps
def dec(f):
@wraps(f) # 就加这一行
def wrapper(*a, **kw):
"""wrapper doc"""
print('before')
return f(*a, **kw)
return wrapper
@dec
def add(x, y):
"""add two numbers"""
return x + y
print(add.__name__) # add
print(add.__doc__) # add two numbers
print(add.__annotations__) # 原函数注解也保留三、wraps 其实是个“偏函数”
源码等价于:
def wraps(wrapped,
assigned=wrapper_assignments, # 要拷贝的七大属性
updated=wrapper_updates): # __dict__ 如何合并
return partial(update_wrapper, # 返回一个“等待 wrapper 作为参数”的函数
wrapped=wrapped,
assigned=assigned,
updated=updated)所以@wraps(func) 先制造一个“待装饰器”,再把真正的 wrapper 传进去,完成 update_wrapper(wrapper, func)。
四、七大默认拷贝属性(wrapper_assignments)
__module____name____qualname__(3.3+ 限定名,嵌套类/函数需要)__doc____annotations__(3.10+ 包括 pep 563 延迟求值)__dict__的部分合并规则(见 wrapper_updates)__kwdefaults__、__defaults__默认参数值
五、自定义要保留/跳过的属性
my_attrs = ('__name__', '__doc__', '__my_flag__')
def my_wraps(f):
return wraps(f, assigned=my_attrs)
# 或者只想忽略某个属性
from functools import wrapper_assignments
skip_annotation = tuple(a for a in wrapper_assignments if a != '__annotations__')
def dec(f):
@wraps(f, assigned=skip_annotation)
def wrapper(*a, **kw):
...六、保留 wrapper 自己的属性(叠加)
有时既想保留原函数,又想给 wrapper 加新属性,用 updated 参数:
def dec(f):
@wraps(f, updated=()) # 不合并 __dict__,避免覆盖
def wrapper(*a, **kw):
...
wrapper.decorator = 'my_dec' # 新增标记
return wrapper七、带参数的装饰器 + wraps
from functools import wraps
def repeat(n): # ← 这一层接收参数
def dec(f):
@wraps(f)
def wrapper(*a, **kw):
for _ in range(n):
result = f(*a, **kw)
return result
return wrapper
return dec
@repeat(3)
def hello():
"""say hello"""
print('hi')
hello()
print(hello.__name__) # hello
print(hello.__doc__) # say hello关键点:@wraps(f) 一定要放在最内层 wrapper 上,而不是 dec 本身。
八、类装饰器 & 方法也能用
def count_calls(cls):
orig_init = cls.__init__
@wraps(orig_init) # 保留原 __init__ 签名
def new_init(self, *a, **kw):
self._cnt = 0
orig_init(self, *a, **kw)
cls.__init__ = new_init
return cls九、调试技巧:一眼看穿“真身”
>>> hello.__wrapped__ # wraps 自动加的秘密属性 <function hello at 0x...> # 指向原始函数 >>> inspect.signature(hello) # 拿到的是原签名,不会看到 wrapper 的 *a, **kw
十、常见坑速查
| 现象 | 原因 | 解决 |
|---|---|---|
| typeerror: wraps() missing 1 required positional argument | 把 @wraps 写成了 @wraps() | 正确写 @wraps(func) |
| 签名还是 wrapper 的 (*args, **kwargs) | 用了 wraps 但 ide 仍显示通用签名 | ide 缓存 / 需 inspect.signature 重新提取 |
| 保留不了自定义属性 | 默认 assigned 不包含 | 传自定义列表 |
十一、一句话总结
写装饰器就两步:
- 先 from functools import wraps
- 在 wrapper 函数正上方写 @wraps(被装饰函数)
到此这篇关于python @wraps()装饰器的使用的文章就介绍到这了,更多相关python @wraps()内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论