functools.wraps 的作用是什么?

993 投票
7 回答
283367 浏览
提问于 2025-04-11 17:54

在这篇关于另一个问题的回答的评论中,有人提到他们不太明白 functools.wraps 是干什么的。所以,我在这里提这个问题,是为了在 StackOverflow 上留个记录,方便以后查阅:functools.wraps 到底是做什么的呢?

7 个回答

49
  1. 假设我们有一个简单的装饰器,它会把一个函数的输出放进一个字符串里,然后加上三个惊叹号!!!!。
def mydeco(func):
    def wrapper(*args, **kwargs):
        return f'{func(*args, **kwargs)}!!!'
    return wrapper
  1. 现在我们用“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
  1. 当我们运行 add(10,20) 和 mysum(1,2,3,4) 的时候,它们都能正常工作!
>>> add(10,20)
'30!!!'

>>> mysum(1,2,3,4)
'10!!!!'
  1. 但是,name 属性会给我们函数的名字,当我们定义它的时候,
>>>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步(第二次):
>>> 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步(第三次):
>>> 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

参考链接

68

从 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 库相同的有效技巧。

1590

当你使用装饰器的时候,其实是在用一个函数替换另一个函数。换句话说,如果你有一个装饰器

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'

撰写回答