保留装饰函数的签名

Posted

技术标签:

【中文标题】保留装饰函数的签名【英文标题】:Preserving signatures of decorated functions 【发布时间】:2010-09-13 22:51:09 【问题描述】:

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

这是一个例子:

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

还有其他方法吗?

【问题讨论】:

从未说过“过时”。我或多或少想知道inspect.Signature 在处理装饰函数时添加了什么。 【参考方案1】:

    安装decorator模块:

    $ 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+

functools.wraps() from stdlib 自 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() 可用 at least since 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

【讨论】:

你的不是第一个答案,而是迄今为止最全面的 :-) 我实际上更喜欢不涉及第三方模块的解决方案,但查看装饰器模块的源代码,这很简单足以让我复制它。 @MarkLodato:functools.wraps() 已经在 Python 3.4+ 中保留了签名(如答案中所述)。你的意思是设置wrapper.__signature__ 对早期版本有帮助吗? (您测试了哪些版本?) @MarkLodato: help() 在 Python 3.4 上显示了正确的签名。为什么你认为 functools.wraps() 坏了而不是 IPython? @MarkLodato:如果我们必须编写代码来修复它,它就会被破坏。鉴于help() 产生了正确的结果,问题是应该修复哪个软件:functools.wraps() 还是 IPython?无论如何,手动分配__signature__ 充其量只是一种解决方法——它不是一个长期的解决方案。 看起来 inspect.getfullargspec() 在 python 3.4 中仍然没有为 functools.wraps 返回正确的签名,您必须改用 inspect.signature()【参考方案2】:

这可以通过 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 继续报告装饰器的调用签名。 functools.wraps 不完整。 inspect.getfullargspec(func) 仍然返回装饰器函数的签名而不是包装函数。【参考方案3】:

您可以使用带有decorator 装饰器的decorator module:

@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 函数——现在已修复。

【讨论】:

【参考方案4】:

看看decorator 模块——特别是decorator 装饰器,它解决了这个问题。

【讨论】:

【参考方案5】:

第二个选项:

    安装包装模块:

$ easy_install wrapt

wrapt 有一个奖励,保留类签名。


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

【讨论】:

【参考方案6】:

正如上面在jfs's answer 中评论的那样;如果您在外观方面关心签名(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)

另见this post about functools.wraps

【讨论】:

另外,inspect.getfullargspec 的结果不会通过调用functools.wraps 来保存。 感谢@laike9m 提供有用的附加评论!【参考方案7】:
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

这修正了名称和文档。为了保留函数签名,wrapg.__name__ = f.__name__, g.__doc__ = f.__doc__ 使用在完全相同的位置。

wraps 本身就是一个装饰器。我们将闭包内部函数传递给该装饰器,它将修复元数据。但是如果我们只将内部函数传递给wraps,它就不会知道从哪里复制元数据。它需要知道需要保护哪个函数的元数据。它需要知道原来的功能。

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=wraps(f)(g)
    return g

wraps(f) 将返回一个以g 作为参数的函数。这将返回闭包并分配给g,然后我们将其返回。

【讨论】:

【参考方案8】:
from inspect import signature


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)
    sig = signature(f)
    g.__signature__ = sig
    g.__doc__ = f.__doc__
    g.__annotations__ = f.__annotations__
    g.__name__ = f.__name__
    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

我想添加那个答案(因为这首先出现在谷歌中)。检查模块能够获取函数的签名,以便可以将其保存在装饰器中。但这还不是全部。如果你想修改签名,你可以这样做:

from inspect import signature, Parameter, _ParameterKind


def foo(a: int, b: int) -> int:
    return a + b

sig = signature(foo)
sig._parameters = dict(sig.parameters)
sig.parameters['c'] = Parameter(
    'c', _ParameterKind.POSITIONAL_OR_KEYWORD, 
    annotation=int
)
foo.__signature__ = sig

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

foo(a: int, b: int, c: int) -> int

为什么要改变函数的签名?

拥有关于您的函数和方法的足够文档非常有用。如果您使用 *args, **kwargs 语法,然后从 kwargs 弹出参数用于装饰器中的其他用途,则该关键字参数将无法正确记录,因此会修改函数的签名。

【讨论】:

以上是关于保留装饰函数的签名的主要内容,如果未能解决你的问题,请参考以下文章

创建装饰器时保留元信息

如何更正装饰函数签名和类型提示?

创建一个结合两个函数而不指定原始函数的调用签名的装饰器

如何使用 python-decorator 包来装饰类方法?

python_如何定义带参数的装饰器?

Python进阶精华-编写装饰器为被包装的函数添加参数