functools.wraps 的作用是什么?
在这篇关于另一个问题的回答的评论中,有人提到他们不太明白 functools.wraps
是干什么的。所以,我在这里提这个问题,是为了在 StackOverflow 上留个记录,方便以后查阅:functools.wraps
到底是做什么的呢?
7 个回答
- 假设我们有一个简单的装饰器,它会把一个函数的输出放进一个字符串里,然后加上三个惊叹号!!!!。
def mydeco(func):
def wrapper(*args, **kwargs):
return f'{func(*args, **kwargs)}!!!'
return wrapper
- 现在我们用“mydeco”来装饰两个不同的函数:
@mydeco
def add(a, b):
'''Add two objects together, the long way'''
return a + b
@mydeco
def mysum(*args):
'''Sum any numbers together, the long way'''
total = 0
for one_item in args:
total += one_item
return total
- 当我们运行 add(10,20) 和 mysum(1,2,3,4) 的时候,它们都能正常工作!
>>> add(10,20)
'30!!!'
>>> mysum(1,2,3,4)
'10!!!!'
- 但是,name 属性会给我们函数的名字,当我们定义它的时候,
>>>add.__name__
'wrapper`
>>>mysum.__name__
'wrapper'
- 更糟糕的是
>>> help(add)
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
>>> help(mysum)
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
- 我们可以部分修复这个问题:
def mydeco(func):
def wrapper(*args, **kwargs):
return f'{func(*args, **kwargs)}!!!'
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
return wrapper
- 现在我们再运行第5步(第二次):
>>> help(add)
Help on function add in module __main__:
add(*args, **kwargs)
Add two objects together, the long way
>>> help(mysum)
Help on function mysum in module __main__:
mysum(*args, **kwargs)
Sum any numbers together, the long way
- 但我们可以使用 functools.wraps(一个装饰器工具)
from functools import wraps
def mydeco(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f'{func(*args, **kwargs)}!!!'
return wrapper
- 现在再运行第5步(第三次):
>>> help(add)
Help on function add in module main:
add(a, b)
Add two objects together, the long way
>>> help(mysum)
Help on function mysum in module main:
mysum(*args)
Sum any numbers together, the long way
从 Python 3.5 版本开始:
@functools.wraps(f)
def g():
pass
这个是 g = functools.update_wrapper(g, f)
的别名。它主要做三件事:
- 它把
f
的一些属性,比如__module__
、__name__
、__qualname__
、__doc__
和__annotations__
,复制到g
上。这些默认的属性在WRAPPER_ASSIGNMENTS
中,你可以在 functools 的源代码中看到。 - 它用
f.__dict__
中的所有元素更新g
的__dict__
。(可以查看源代码中的WRAPPER_UPDATES
) - 它在
g
上设置一个新的__wrapped__=f
属性。
结果是,g
看起来和 f
有相同的名字、文档字符串、模块名和签名。唯一的问题是,关于签名这一点其实并不完全正确:只是 inspect.signature
默认会跟随包装链。你可以通过使用 inspect.signature(g, follow_wrapped=False)
来检查这一点,具体可以参考 文档。这会带来一些麻烦的后果:
- 即使提供的参数无效,包装代码也会执行。
- 包装代码不能轻松通过名称访问参数,因为它是从接收到的
*args
和**kwargs
中来的。实际上,你需要处理所有情况(位置参数、关键字参数、默认参数),因此需要使用类似Signature.bind()
的方法。
现在,functools.wraps
和装饰器之间有点混淆,因为开发装饰器时常常需要包装函数。但这两者其实是完全独立的概念。如果你想了解它们的区别,我实现了两个辅助库:decopatch 用于轻松编写装饰器,以及 makefun 用于提供一个保留签名的 @wraps
替代方案。请注意,makefun
依赖于与著名的 decorator
库相同的有效技巧。
当你使用装饰器的时候,其实是在用一个函数替换另一个函数。换句话说,如果你有一个装饰器
def logged(func):
def with_logging(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return with_logging
那么当你写
@logged
def f(x):
"""does some math"""
return x + x * x
这就等于在说
def f(x):
"""does some math"""
return x + x * x
f = logged(f)
这时候,你的函数 f
就被 with_logging
这个函数替代了。不幸的是,这样一来,如果你再写
print(f.__name__)
它会打印出 with_logging
,因为这是你新函数的名字。实际上,如果你查看 f
的文档字符串,它会是空的,因为 with_logging
没有文档字符串,所以你写的文档字符串就不见了。此外,如果你查看这个函数的 pydoc 结果,它不会显示接受一个参数 x
;而是会显示接受 *args
和 **kwargs
,因为这就是 with_logging
接受的参数。
如果使用装饰器总是意味着失去这些关于函数的信息,那就会是个大问题。这就是我们需要 functools.wraps
的原因。它可以把装饰器中使用的函数的一些信息,比如函数名、文档字符串、参数列表等,复制过来。而且因为 wraps
本身也是一个装饰器,所以下面的代码就能正确处理这个问题:
from functools import wraps
def logged(func):
@wraps(func)
def with_logging(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return with_logging
@logged
def f(x):
"""does some math"""
return x + x * x
print(f.__name__) # prints 'f'
print(f.__doc__) # prints 'does some math'