保留装饰功能的签名


111

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

这是一个例子:

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构造修饰的函数?

还有其他方法吗?


1
从来没有说过时。我或多或少想知道inspect.Signature添加什么到装饰函数上。
NightShadeQueen 2015年

Answers:


79
  1. 安装装饰器模块:

    $ 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起,来自stdlib的命令就保留签名:

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


您的答案不是第一个,但到目前为止是最全面的:-)实际上,我更喜欢一个不涉及第三方模块的解决方案,但是查看装饰器模块的源代码,它很简单,我可以只需复制即可。
Fredrik Johansson,

1
@MarkLodato:functools.wraps()已经在Python 3.4+中保留了签名(如答案中所述)。您是说设置wrapper.__signature__对较早版本有帮助吗?(您测试了哪些版本?)
jfs

1
@MarkLodato:help()在Python 3.4上显示正确的签名。您为什么认为functools.wraps()已损坏,而不是IPython?
jfs

1
@MarkLodato:如果我们必须编写代码来修复它,它就坏了。给出help()正确的结果后,问题是应该修复哪个软件:functools.wraps()还是IPython?无论如何,手动分配__signature__充其量只是一种解决方法-这不是长期解决方案。
jfs

1
看起来在python 3.4中inspect.getfullargspec()仍然没有返回正确的签名functools.wraps,您必须使用它inspect.signature()
Tuukka Mustonen

16

这是通过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

不确定是否是Sphinx,但是当包装函数是类的方法时,这似乎不起作用。Sphinx继续报告装饰器的呼叫签名。
alphabetasoup

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

编辑:塞巴斯蒂安(JF Sebastian)指出,我没有修改args_as_ints功能-现在已修复。



6

第二种选择:

  1. 安装包装模块:

$ easy_install包装

包裹有奖励,保留班级签名。


import wrapt
import inspect

@wrapt.decorator def args_as_ints(wrapped, instance, args, kwargs): if instance is None: if inspect.isclass(wrapped): # Decorator was applied to a class. return wrapped(*args, **kwargs) else: # Decorator was applied to a function or staticmethod. return wrapped(*args, **kwargs) else: if inspect.isclass(instance): # Decorator was applied to a classmethod. return wrapped(*args, **kwargs) else: # Decorator was applied to an instancemethod. return wrapped(*args, **kwargs) @args_as_ints def funny_function(x, y, z=3): """Computes x*y + 2*z""" return x * y + 2 * z >>> funny_function(3, 4, 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

2

如上文在jfs的回答中所述:如果您担心外观(helpinspect.signature)方面的签名,那么使用functools.wraps就很好了。

如果您担心行为方面的签名(特别是TypeError在参数不匹配的情况下),请functools.wraps不要保留它。decorator为此,您应该使用它,或者将其核心引擎(称为)推广makefun

from makefun import wraps

def args_as_ints(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("wrapper executes")
        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"))
# wrapper executes
# 22

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

funny_function(0)  
# observe: no "wrapper executes" is printed! (with functools it would)
# TypeError: funny_function() takes at least 2 arguments (1 given)

另请参阅有关的帖子functools.wraps


1
另外,inspect.getfullargspec也不通过调用保留结果functools.wraps
laike9m

感谢您提供有用的附加评论@ laike9m!
smarie
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.