在另一个问题的答案评论中,有人说他们不确定functools.wraps
在做什么。所以,我提出这个问题,以便在 StackOverflow 上留下记录供将来参考: functools.wraps
具体是做什么?
使用装饰器时,您正在用另一个函数替换一个函数。换句话说,如果您有一个装饰器
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
的docstring,它将为空,因为with_logging
没有docstring,因此您编写的docstring将不再存在。此外,如果您查看该函数的pydoc结果,则不会将其列为接受一个参数x
的函数;相反,它将被列为接受*args
和**kwargs
,因为这就是with_logging
需要的。
如果使用装饰器总是意味着失去关于函数的这些信息,那么这将是一个严重的问题。这就是为什么我们有functools.wraps
的原因。它接受在装饰器中使用的函数,并添加复制函数名称、docstring、参数列表等功能。而且由于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'
从 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源代码中看到。g
的 __dict__
来自于 f.__dict__
中的所有元素。(请看源代码中的 WRAPPER_UPDATES
)g
上设置一个新的__wrapped__=f
属性其结果是g
看起来具有与f
相同的名称、文档字符串、模块名称和签名。唯一的问题是关于签名,实际上这不是真的:只是因为inspect.signature
默认跟随包装器链。您可以通过使用inspect.signature(g, follow_wrapped=False)
来验证它,如文档中所述。这带来了烦人的后果:
Signature.bind()
之类的东西。现在functools.wraps
和装饰器之间有些混淆,因为开发装饰器的一个非常常见的用例是包装函数。但两者完全独立的概念。如果您想了解差异,我为两者都实现了辅助库: decopatch 用于轻松编写装饰器,makefun 则提供了保留签名的替代@wraps
。请注意,makefun
依赖于与著名的decorator
库相同的成熟技巧。
def mydeco(func):
def wrapper(*args, **kwargs):
return f'{func(*args, **kwargs)}!!!'
return wrapper
@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)
'30!!!'
>>> mysum(1,2,3,4)
'10!!!!'
>>>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
>>> 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
from functools import wraps
def mydeco(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f'{func(*args, **kwargs)}!!!'
return wrapper
>>> 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
我经常使用类来作为装饰器,而不是函数。但由于对象没有函数所期望的所有属性,这给我带来了一些困扰。例如,对象不会有属性__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)
@wraps
文档的说明,@wraps
只是一个使用方便的函数,它调用了functools.update_wrapper()
。在类装饰器的情况下,您可以直接从__init__()
方法中调用update_wrapper()
。因此,您根本不需要创建DecBase
类,只需在process_login
的__init__()
方法中添加以下代码行:update_wrapper(self, func)
。就这样。 - FabianoFlask
中使用add_url_route
时,需要(在某些情况下?)提供的view_func
函数具有__name__
属性。如果提供的函数实际上是一个装饰方法,即使使用了 functools.wraps
装饰器,该属性也不再存在。 - Joëlupdate_wrapper
而不是@wraps
就可以完成工作 :) - Joël每当我们使用例如:@wraps 紧接着我们自己的包装器函数时。根据此链接中给出的详细信息,它说
functools.wraps 是一个方便的函数,用于在定义包装器函数时作为函数装饰器调用update_wrapper()。
它等同于partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)。
因此,@wraps 装饰器实际上会调用 functools.partial(func[,*args][, **keywords])。
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()返回简化版本即包装函数内部的对象而不是包装函数本身。
这是有关包装的源代码:
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)
简而言之,functools.wraps 只是一个普通函数。让我们看看官方示例。通过源代码的帮助,我们可以了解到实现和运行步骤的更多细节如下:
wrapper=O1.__call__(wrapper)
检查__call__的实现,我们可以看到在这一步之后,(左侧)wrapper变成了由self.func(*self.args, *args, **newkeywords)产生的对象。检查__new__中O1的创建,我们知道self.func是函数update_wrapper。它使用参数*args,右侧的wrapper作为其第一个参数。检查update_wrapper的最后一步,可以看到右侧的wrapper被返回,并根据需要修改了一些属性。
functools.wraps
进行这项工作,难道它不应该一开始就作为装饰器模式的一部分吗?何时您不想使用 @wraps? - wim@wraps
,以执行各种类型的修改或注释。基本上,这是Python哲学的扩展,即显式优于隐式,并且特例不足以打破规则。(如果必须手动提供@wraps
而不是使用某种特殊的退出机制,则代码要简单得多,语言更容易理解。) - ssokolow