functools.wraps 有什么作用?
- 2024-12-16 08:35:00
- admin 原创
- 196
问题描述:
在对另一个问题的这个答案的评论中,有人说他们不确定functools.wraps
在做什么。所以,我问这个问题是为了在 StackOverflow 上留下记录以供将来参考:functools.wraps
到底做了什么?
解决方案 1:
当你使用装饰器时,你正在用另一个函数替换一个函数。换句话说,如果你有一个装饰器
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'
解决方案 2:
从python 3.5+开始:
@functools.wraps(f)
def g():
pass
是 的别名g = functools.update_wrapper(g, f)
。它的作用有三:
它将 的 、 、 、 和 属性复制到
__module__
上__name__
。__qualname__
此__doc__
默认__annotations__
列表f
在g
中,您可以在functools 源WRAPPER_ASSIGNMENTS
中看到它。它用来自的所有元素更新
__dict__
的。(参见源代码)g
`f.__dict__`WRAPPER_UPDATES
它设置
__wrapped__=f
了新的属性g
结果是g
看起来具有与 相同的名称、文档字符串、模块名称和签名f
。唯一的问题是,就签名而言,这实际上并不正确:默认情况下, 只是inspect.signature
遵循包装器链。您可以使用 进行检查,如文档inspect.signature(g, follow_wrapped=False)
中所述。这会带来令人讨厌的后果:
即使提供的参数无效,包装器代码也会执行。
包装器代码无法使用其名称从收到的 args、*kwargs 轻松访问参数。实际上,必须处理所有情况(位置、关键字、默认),因此必须使用类似 的东西
Signature.bind()
。
现在和装饰器之间有点混淆functools.wraps
,因为开发装饰器的一个非常常见的用例是包装函数。但两者都是完全独立的概念。如果您有兴趣了解两者的区别,我为两者都实现了辅助库:decopatch可轻松编写装饰器,makefun可提供签名保留的替代方案@wraps
。请注意,makefun
与著名的库相比,它依赖于相同的经过验证的技巧decorator
。
解决方案 3:
假设我们有这个:简单的装饰器,它接受函数的输出并将其放入字符串中,后跟三个!!!!。
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 (decotator 工具)
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
参考
解决方案 4:
我经常使用类而不是函数作为装饰器。我遇到了一些麻烦,因为对象不会具有函数所期望的所有相同属性。例如,对象不会具有属性__name__
。我遇到了一个特定的问题,很难追踪 Django 报告错误“对象没有属性' __name__
'”的位置。不幸的是,对于类样式的装饰器,我不相信 @wrap 可以完成这项工作。我改为创建了一个基本装饰器类,如下所示:
class DecBase(object):
func = None
def __init__(self, func):
self.__func = func
def __getattribute__(self, name):
if name == "func":
return super(DecBase, self).__getattribute__(name)
return self.func.__getattribute__(name)
def __setattr__(self, name, value):
if name == "func":
return super(DecBase, self).__setattr__(name, value)
return self.func.__setattr__(name, value)
此类将所有属性调用代理到被修饰的函数。因此,您现在可以创建一个简单的修饰器来检查是否指定了 2 个参数,如下所示:
class process_login(DecBase):
def __call__(self, *args):
if len(args) != 2:
raise Exception("You can only specify two arguments")
return self.func(*args)
解决方案 5:
先决条件:您必须知道如何使用装饰器,特别是包装器。此评论对此解释得比较清楚,或者此链接对此解释得也相当清楚。
每当我们使用 For 时,例如:@wraps 后跟我们自己的包装函数。根据此链接中提供的详细信息,它表示
functools.wraps 是在定义包装函数时调用 update_wrapper() 作为函数装饰器的便捷函数。
它相当于partial(update_wrapper,wrapped=wrapped,assigned=assigned,updated=updated)。
所以@wraps装饰器实际上调用了functools.partial(func,*args)。
functools.partial() 定义表示
partial() 用于部分函数应用,它“冻结”函数参数和/或关键字的某些部分,从而产生具有简化签名的新对象。例如,partial() 可用于创建一个可调用函数,其行为类似于 int() 函数,其中基本参数默认为两个:
>>> from functools import partial
>>> basetwo = partial(int, base=2)
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
>>> basetwo('10010')
18
这让我得出这样的结论:@wraps 调用 partial() 并将包装函数作为参数传递给它。partial() 最终返回简化版本,即包装函数内部的对象,而不是包装函数本身。
解决方案 6:
这是有关包装的源代码:
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
"""Update a wrapper function to look like the wrapped function
wrapper is the function to be updated
wrapped is the original function
assigned is a tuple naming the attributes assigned directly
from the wrapped function to the wrapper function (defaults to
functools.WRAPPER_ASSIGNMENTS)
updated is a tuple naming the attributes of the wrapper that
are updated with the corresponding attribute from the wrapped
function (defaults to functools.WRAPPER_UPDATES)
"""
for attr in assigned:
setattr(wrapper, attr, getattr(wrapped, attr))
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
# Return the wrapper so this can be used as a decorator via partial()
return wrapper
def wraps(wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
"""Decorator factory to apply update_wrapper() to a wrapper function
Returns a decorator that invokes update_wrapper() with the decorated
function as the wrapper argument and the arguments to wraps() as the
remaining arguments. Default arguments are as for update_wrapper().
This is a convenience function to simplify applying partial() to
update_wrapper().
"""
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)
解决方案 7:
简单来说,functools.wraps就是一个普通的函数,我们先来看一下官方给出的例子,结合源码我们可以看到更详细的实现,以及运行步骤如下:
wraps(f)返回一个对象,比如O1 。它是Partial 类的一个对象
下一步是@O1...,这是python中的装饰器符号。这意味着
包装器=O1.__call__(包装器)
检查__call__的实现,我们发现在此步骤之后,(左侧的)包装器将成为self.func(self.args, *args, newkeywords)产生的对象。检查new中O1的创建,我们知道self.func是函数update_wrapper。它使用参数args(右侧包装器)作为其第一个参数。检查update_wrapper的最后一步,可以看到返回了右侧包装器,其中某些属性根据需要进行了修改。
扫码咨询,免费领取项目管理大礼包!