保留修饰函数的签名

144 投票
8 回答
27294 浏览
提问于 2025-04-11 09:23

假设我写了一个装饰器,它可以做一些很通用的事情。比如,它可以把所有的参数转换成特定的类型,进行日志记录,或者实现缓存等等。

这里有个例子:

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    return g

@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

>>> funny_function("3", 4.0, z="5")
22

到目前为止一切都很好。不过,有一个问题。被装饰的函数没有保留原始函数的文档说明:

>>> help(funny_function)
Help on function g in module __main__:

g(*args, **kwargs)

幸运的是,有一个解决办法:

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    g.__name__ = f.__name__
    g.__doc__ = f.__doc__
    return g

@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

这次,函数的名称和文档说明都是正确的:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(*args, **kwargs)
    Computes x*y + 2*z

但仍然有一个问题:函数的签名是错误的。信息“*args, **kwargs”几乎没有用。

该怎么办呢?我能想到两个简单但有缺陷的解决办法:

1 -- 在文档字符串中包含正确的签名:

def funny_function(x, y, z=3):
    """funny_function(x, y, z=3) -- computes x*y + 2*z"""
    return x*y + 2*z

这个方法不好,因为会造成重复。自动生成的文档中仍然不会正确显示签名。更新函数时很容易忘记修改文档字符串,或者出现拼写错误。[而且,是的,我知道文档字符串已经重复了函数体。请忽略这一点;funny_function只是一个随机的例子。]

2 -- 不使用装饰器,或者为每个特定的签名使用一个专用的装饰器:

def funny_functions_decorator(f):
    def g(x, y, z=3):
        return f(int(x), int(y), z=int(z))
    g.__name__ = f.__name__
    g.__doc__ = f.__doc__
    return g

这对于一组具有相同签名的函数是有效的,但在一般情况下没什么用。正如我一开始所说的,我希望能够完全通用地使用装饰器。

我在寻找一个完全通用且自动化的解决方案。

所以问题是:有没有办法在创建装饰的函数后编辑它的签名?

或者,我能否写一个装饰器,提取函数的签名,并在构造装饰的函数时使用这些信息,而不是使用“*kwargs, **kwargs”?我该如何提取这些信息?我应该如何构造装饰的函数——用exec吗?

还有其他方法吗?

8 个回答

9

这里有一个装饰器模块,里面有一个叫decorator的装饰器可以使用:

@decorator
def args_as_ints(f, *args, **kwargs):
    args = [int(x) for x in args]
    kwargs = dict((k, int(v)) for k, v in kwargs.items())
    return f(*args, **kwargs)

这样方法的签名和帮助信息就能被保留下来了:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(x, y, z=3)
    Computes x*y + 2*z

补充:J. F. Sebastian 指出我没有修改args_as_ints函数——现在已经修复了。

29

这个问题可以通过Python的标准库functools来解决,特别是里面的functools.wraps函数。这个函数的作用是“让一个包装函数看起来像被包装的函数”。不过,它的表现会根据Python的版本有所不同,下面会展示具体情况。根据提问中的例子,代码看起来会是这样的:

from functools import wraps

def args_as_ints(f):
    @wraps(f) 
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    return g


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

在Python 3中执行时,这段代码会产生以下结果:

>>> funny_function("3", 4.0, z="5")
22
>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(x, y, z=3)
    Computes x*y + 2*z

但是在Python 2中,它的一个缺点是不会更新函数的参数列表。在Python 2中执行时,它会产生:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(*args, **kwargs)
    Computes x*y + 2*z
106
  1. 安装 decorator 模块:

    $ pip install decorator
    
  2. 调整 args_as_ints() 的定义:

    import decorator
    
    @decorator.decorator
    def args_as_ints(f, *args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    
    @args_as_ints
    def funny_function(x, y, z=3):
        """Computes x*y + 2*z"""
        return x*y + 2*z
    
    print funny_function("3", 4.0, z="5")
    # 22
    help(funny_function)
    # Help on function funny_function in module __main__:
    # 
    # funny_function(x, y, z=3)
    #     Computes x*y + 2*z
    

Python 3.4 及以上版本

functools.wraps() 是标准库中的一个功能,从 Python 3.4 开始可以保留函数的签名:

import functools


def args_as_ints(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return func(*args, **kwargs)
    return wrapper


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z


print(funny_function("3", 4.0, z="5"))
# 22
help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
#     Computes x*y + 2*z

functools.wraps()至少 Python 2.5 版本 就有了,但在那个版本中它不能保留签名:

help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(*args, **kwargs)
#    Computes x*y + 2*z

注意:使用 *args, **kwargs 而不是 x, y, z=3

撰写回答