保留装饰功能的签名

时间:2020-03-06 14:51:49  来源:igfitidea点击:

假设我编写了一个装饰器,它执行了非常通用的操作。例如,它可能会将所有参数转换为特定类型,执行日志记录,实现备忘录等。

这是一个例子:

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

由于重复,这很糟糕。签名仍不会在自动生成的文档中正确显示。更新函数很容易,而不必更改文档字符串,也不会打错字。 [是的,我知道docstring已经复制了函数主体。请忽略此; 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的修饰函数?

还有其他方法吗?

解决方案

看一下装饰器模块,特别是装饰器装饰器,它可以解决此问题。

有一个带有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函数-现在已修复。

  • 安装装饰器模块:
$ pip install decorator
  • 修改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以上

从Python 3.4开始,来自stdlib的functools.wraps()保留签名:

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