functools.wraps有什么作用?

Eli*_*ght 588 python decorator functools

对另一个问题的答案的评论中,有人说他们不确定functools.wraps在做什么.所以,我问这个问题,以便在StackOverflow上有一个记录,供将来参考:究竟functools.wraps做了什么?

Eli*_*ght 978

当您使用装饰器时,您将一个功能替换为另一个功能.换句话说,如果你有一个装饰者

def logged(func):
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging
Run Code Online (Sandbox Code Playgroud)

然后当你说

@logged
def f(x):
   """does some math"""
   return x + x * x
Run Code Online (Sandbox Code Playgroud)

这跟说的完全一样

def f(x):
    """does some math"""
    return x + x * x
f = logged(f)
Run Code Online (Sandbox Code Playgroud)

并且您的函数f被替换为函数with_logging.不幸的是,这意味着,如果你再说

print(f.__name__)
Run Code Online (Sandbox Code Playgroud)

它会打印,with_logging因为这是你的新功能的名称.事实上,如果你查看docstring f,它将是空白的,因为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'
Run Code Online (Sandbox Code Playgroud)

  • 为什么我们需要`functools.wraps`来完成这项工作,它不应该只是装饰模式的一部分吗?你何时_not_想要使用@wraps? (75认同)
  • @wim:我写了一些装饰器,它们使用自己的`@ wraps`版本,以便对复制的值执行各种类型的修改或注释.从根本上说,它是Python哲学的扩展,显式优于隐式,特殊情况不足以破坏规则.(如果必须手动提供`@ wraps`,而不是使用某种特殊的退出机制,代码就会简单得多,语言也更容易理解.) (50认同)
  • @LucasMalor并非所有装饰器都包装它们装饰的功能.一些应用副作用,例如在某种查找系统中注册它们. (29认同)
  • 是的,我更喜欢避免使用装饰器模块,因为functools.wraps是标准库的一部分,因此不会引入另一个外部依赖项.但装饰模块确实解决了帮助问题,希望有一天也会有functools.wraps. (6认同)
  • 这里是一个例子,说明如果你不使用包装会发生什么:doctools测试可能会突然消失.这是因为doctools无法在装饰函数中找到测试,除非像wrapps()这样的东西将它们复制过来. (5认同)
  • 但为什么我们需要恢复函数名称、文档字符串等,并且看起来 @wraps 是装饰器模式的补丁。有什么危险,我们会失去什么?如果不存在正确的函数名称、文档字符串等,则无法提供哪些功能?难道以后发现这个问题就不能内置实现吗?哪些向后兼容性问题阻碍了语言本身的实现? (3认同)
  • 有一个使用装饰器模块的帮助问题的解决方案,并在此答案的评论中提到:http://stackoverflow.com/questions/1782843/python-decorator-problem-with-docstrings/1782888#1782888 (2认同)
  • @ssokolow:`特殊情况不足以破坏规则`:如果`wraps`是默认行为,那将是"规则".`wraps`对我来说更像是一个更明确的做事方式的补丁.也就是说,最好添加补丁而不是更改现有规则,并使用5个用户的分组创建Python 4. (2认同)
  • 编译器怎么会知道它是装饰器知道何时自动应用@wraps? (2认同)

XIA*_* LI 46

    \n
  1. 假设我们有这样的:简单装饰器,它接受函数\xe2\x80\x99s 输出并将其放入字符串中,后跟三个!!!!。
  2. \n
\n
def mydeco(func):\n    def wrapper(*args, **kwargs):\n        return f\'{func(*args, **kwargs)}!!!\'\n    return wrapper\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  1. 现在让\xe2\x80\x99s 用\xe2\x80\x9cmydeco\xe2\x80\x9d 装饰两个不同的函数:
  2. \n
\n
@mydeco\ndef add(a, b):\n    \'\'\'Add two objects together, the long way\'\'\'\n    return a + b\n\n@mydeco\ndef mysum(*args):\n    \'\'\'Sum any numbers together, the long way\'\'\'\n    total = 0\n    for one_item in args:\n        total += one_item\n    return total\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  1. 当运行 add(10,20), mysum(1,2,3,4) 时,它起作用了!
  2. \n
\n
>>> add(10,20)\n\'30!!!\'\n\n>>> mysum(1,2,3,4)\n\'10!!!!\'\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  1. 然而,name属性在定义函数时为我们提供了函数的名称,
  2. \n
\n
>>>add.__name__\n\'wrapper`\n\n>>>mysum.__name__\n\'wrapper\'\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  1. 更差
  2. \n
\n
>>> help(add)\nHelp on function wrapper in module __main__:\nwrapper(*args, **kwargs)\n\n>>> help(mysum)\nHelp on function wrapper in module __main__:\nwrapper(*args, **kwargs)\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  1. 我们可以通过以下方式部分修复:
  2. \n
\n
def mydeco(func):\n    def wrapper(*args, **kwargs):\n        return f\'{func(*args, **kwargs)}!!!\'\n    wrapper.__name__ = func.__name__\n    wrapper.__doc__ = func.__doc__\n    return wrapper\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  1. 现在我们再次运行步骤 5(第二次):
  2. \n
\n
>>> help(add)\nHelp on function add in module __main__:\n\nadd(*args, **kwargs)\n     Add two objects together, the long way\n\n>>> help(mysum)\nHelp on function mysum in module __main__:\n\nmysum(*args, **kwargs)\n    Sum any numbers together, the long way\n\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  1. 但我们可以使用 functools.wraps (decotator 工具)
  2. \n
\n
from functools import wraps\n\ndef mydeco(func):\n    @wraps(func)\n    def wrapper(*args, **kwargs):\n        return f\'{func(*args, **kwargs)}!!!\'\n    return wrapper\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  1. 现在再次运行步骤 5(第三次)
  2. \n
\n
>>> help(add)\nHelp on function add in module main:\nadd(a, b)\n     Add two objects together, the long way\n\n>>> help(mysum)\nHelp on function mysum in module main:\nmysum(*args)\n     Sum any numbers together, the long way\n
Run Code Online (Sandbox Code Playgroud)\n

参考

\n


Jos*_*osh 21

我经常为我的装饰者使用类而不是函数.我遇到了一些麻烦,因为一个对象不具有与函数相同的所有属性.例如,对象不具有该属性__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)
Run Code Online (Sandbox Code Playgroud)

此类将所有属性调用代理到正在装饰的函数.所以,你现在可以创建一个简单的装饰器来检查这两个参数是这样指定的:

class process_login(DecBase):
    def __call__(self, *args):
        if len(args) != 2:
            raise Exception("You can only specify two arguments")

        return self.func(*args)
Run Code Online (Sandbox Code Playgroud)

  • 正如来自@wraps的文档所说,@wraps只是functools.update_wrapper()的便捷功能。如果是类装饰器,则可以直接从__init __()方法调用`update_wrapper()`。因此,您根本不需要创建DecBase,只需在process_login的__init __()上添加以下行即可:update_wrapper(self,func)。就这样。 (5认同)

sma*_*rie 7

从python 3.5开始:

@functools.wraps(f)
def g():
    pass
Run Code Online (Sandbox Code Playgroud)

是的别名g = functools.update_wrapper(g, f)。它确实完成了三件事:

  • 它复制__module____name____qualname____doc__,和__annotations__属性fg。该默认列表位于其中WRAPPER_ASSIGNMENTS,您可以在functools源代码中看到它。
  • 会使用中的所有元素更新__dict__of 。(请参见源代码)gf.__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库相同的可靠技巧。


3rd*_*rdi 6

    \n
  1. 先决条件:您必须知道如何使用装饰器,特别是包装器。这个评论解释得有点清楚或者这个链接也解释得很好。

    \n
  2. \n
  3. 每当我们使用 For 例如:@wraps 后跟我们自己的包装函数。根据此链接中给出的详细信息中给出的详细信息,它说

    \n
  4. \n
\n
\n

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

\n

它相当于部分(update_wrapper,wrapped =wrapped,指派=指派,更新=更新)。

\n
\n

所以@wraps装饰器实际上调用了functools.partial(func[,*args][,**keywords])。

\n

functools.partial() 定义表示

\n
\n

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

\n
\n
>>> from functools import partial\n>>> basetwo = partial(int, base=2)\n>>> basetwo.__doc__ = \'Convert base 2 string to an int.\'\n>>> basetwo(\'10010\')\n18\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n


小智 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)
Run Code Online (Sandbox Code Playgroud)