functools.wraps做什么?

在对这个回答另一个问题的评论中,有人说他们不确定functools.wraps在做什么。所以,我问这个问题是为了在StackOverflow上记录下来以供将来参考:functools.wraps到底做了什么?

231220 次浏览

当您使用装饰器时,您正在用另一个函数替换一个函数。换句话说,如果您有一个装饰器

def logged(func):def with_logging(*args, **kwargs):print(func.__name__ + " was called")return func(*args, **kwargs)return with_logging

然后当你说

@loggeddef f(x):"""does some math"""return x + x * x

跟说的一模一样

def f(x):"""does some math"""return x + x * xf = logged(f)

你的函数f被替换为函数with_logging。不幸的是,这意味着如果你说

print(f.__name__)

它将打印with_logging,因为这是你的新函数的名称。事实上,如果你查看f的文档字符串,它将是空白的,因为with_logging没有文档字符串,所以你编写的文档字符串将不再存在。此外,如果你查看该函数的pydoc结果,它不会被列为接受一个参数x;相反,它会被列为接受*args**kwargs,因为这是with_logging需要的。

如果使用装饰器总是意味着丢失有关函数的这些信息,那将是一个严重的问题。这就是为什么我们有functools.wraps。这需要装饰器中使用的函数,并添加了复制函数名称、docstring、参数列表等的功能。由于wraps本身就是装饰器,以下代码做了正确的事情:

from functools import wrapsdef logged(func):@wraps(func)def with_logging(*args, **kwargs):print(func.__name__ + " was called")return func(*args, **kwargs)return with_logging
@loggeddef f(x):"""does some math"""return x + x * x
print(f.__name__)  # prints 'f'print(f.__doc__)   # prints 'does some math'

我经常为我的装饰器使用类,而不是函数。我在这方面遇到了一些麻烦,因为一个对象不会具有期望函数的所有相同属性。例如,一个对象不会具有属性__name__。我有一个具体的问题,很难追踪Django在哪里报告错误“对象没有属性'__name__'”。不幸的是,对于类风格的装饰器,我不相信@包装会完成这项工作。相反,我创建了一个基本装饰器类,如下所示:

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)
  1. 先决条件:您必须知道如何使用装饰器,特别是包装。这个评论解释得有点清楚,或者这个链接也解释得很好。

  2. 每当我们使用For eg:@包装后跟我们自己的包装函数时。根据这个链接中给出的细节,它说

functools.wraps是一个方便的函数,用于在定义包装函数时调用update_wrapper()作为函数装饰器。

它等价于部分(update_wrapper,包装=包装,分配=分配,更新=更新)。

所以@包装装饰器实际上会调用functools.partial(func[,*args][,**关键字])。

functools.partial()定义表示

部分()用于部分函数应用程序,它“冻结”函数参数和/或关键字的某些部分,从而产生具有简化签名的新对象。例如,部分()可用于创建一个行为类似int()函数的可调用对象,其中基参数默认为两个:

>>> from functools import partial>>> basetwo = partial(int, base=2)>>> basetwo.__doc__ = 'Convert base 2 string to an int.'>>> basetwo('10010')18

这让我得出这样的结论:@包装函数调用部分()并将包装函数作为参数传递给它。最后,部分()返回简化版本,即包装函数内部的对象,而不是包装函数本身。

简而言之,functools.wraps只是一个常规函数。让我们考虑一下这个官方例子。在源代码的帮助下,我们可以看到有关实现和运行步骤的更多细节如下:

  1. 包装(f)返回一个对象,比如O1。它是类局部的对象
  2. 下一步是@O1,它是python中的装饰符号。这意味着

包装器=O 1.__call__(包装器)

检查<强>__call__的实现,我们看到在这一步之后,(左手边)包装器成为self.func(*self.args,*参数,**新关键字)产生的对象检查__new__O1的创建,我们知道self.func是函数update_wrapper。它使用参数*args,右手边包装器作为其第一个参数。检查update_wrapper的最后一步,可以看到右手边包装器被返回,并根据需要修改了一些属性。

这是关于包装的源代码:

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 updatedwrapped is the original functionassigned is a tuple naming the attributes assigned directlyfrom the wrapped function to the wrapper function (defaults tofunctools.WRAPPER_ASSIGNMENTS)updated is a tuple naming the attributes of the wrapper thatare updated with the corresponding attribute from the wrappedfunction (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 decoratedfunction as the wrapper argument and the arguments to wraps() as theremaining arguments. Default arguments are as for update_wrapper().This is a convenience function to simplify applying partial() toupdate_wrapper()."""return partial(update_wrapper, wrapped=wrapped,assigned=assigned, updated=updated)

从python 3.5+开始:

@functools.wraps(f)def g():pass

g = functools.update_wrapper(g, f)的别名。它只做三件事:

  • 它将f__module____name____qualname____doc____annotations__属性复制到g上。这个默认列表在WRAPPER_ASSIGNMENTS中,您可以在函数工具库中看到它。
  • 它用f.__dict__中的所有元素更新g中的__dict__。(见源代码中的WRAPPER_UPDATES
  • 它在g上设置了一个新的__wrapped__=f属性

结果是g看起来与f具有相同的名称、docstring、模块名称和签名。唯一的问题是,关于签名,这实际上不是真的:只是inspect.signature默认遵循包装链。您可以使用inspect.signature(g, follow_wrapped=False)检查它,如doc中所述。这有恼人的后果:

  • 即使提供的参数无效,包装器代码也会执行。
  • 包装器代码无法从接收到的*args、**kwargs轻松使用其名称访问参数。实际上,必须处理所有情况(位置、关键字、默认),因此必须使用类似Signature.bind()的东西。

现在functools.wraps和装饰器之间有点混淆,因为开发装饰器的一个非常常见的用例是包装函数。但两者都是完全独立的概念。如果你有兴趣理解区别,我为两者实现了帮助库:Decopatch轻松编写装饰器,取乐@wraps提供签名保存替代品。请注意,makefun依赖于与著名的decorator库相同的经过验证的技巧。

  1. 假设我们有这个:简单装饰器,它接受函数的输出并将其放入一个字符串中,后跟三个!!!!。
def mydeco(func):def wrapper(*args, **kwargs):return f'{func(*args, **kwargs)}!!!'return wrapper
  1. 现在让我们用“mydeco”装饰两个不同的函数:
@mydecodef add(a, b):'''Add two objects together, the long way'''return a + b
@mydecodef mysum(*args):'''Sum any numbers together, the long way'''total = 0for one_item in args:total += one_itemreturn total
  1. 当运行add(10,20), mysum(1,2,3,4)时,它起作用了!
>>> add(10,20)'30!!!'
>>> mysum(1,2,3,4)'10!!!!'
  1. 但是,姓名属性在我们定义函数时为我们提供了函数的名称,
>>>add.__name__'wrapper`
>>>mysum.__name__'wrapper'
  1. 更糟
>>> help(add)Help on function wrapper in module __main__:wrapper(*args, **kwargs)
>>> help(mysum)Help on function wrapper in module __main__:wrapper(*args, **kwargs)
  1. 我们可以通过以下方式部分修复:
def mydeco(func):def wrapper(*args, **kwargs):return f'{func(*args, **kwargs)}!!!'wrapper.__name__ = func.__name__wrapper.__doc__ = func.__doc__return wrapper
  1. 现在我们再次运行步骤5(第2次):
>>> 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
  1. 但我们可以用functools.wraps(解码器工具)
from functools import wraps
def mydeco(func):@wraps(func)def wrapper(*args, **kwargs):return f'{func(*args, **kwargs)}!!!'return wrapper
  1. 现在再次运行步骤5(第3次)
>>> 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

参考