保留装饰功能的签名

问题:保留装饰功能的签名

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

这是一个例子:

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

还有其他方法吗?

Suppose I have written a decorator that does something very generic. For example, it might convert all arguments to a specific type, perform logging, implement memoization, etc.

Here is an example:

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

Everything well so far. There is one problem, however. The decorated function does not retain the documentation of the original function:

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

g(*args, **kwargs)

Fortunately, there is a workaround:

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

This time, the function name and documentation are correct:

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

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

But there is still a problem: the function signature is wrong. The information “*args, **kwargs” is next to useless.

What to do? I can think of two simple but flawed workarounds:

1 — Include the correct signature in the docstring:

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

This is bad because of the duplication. The signature will still not be shown properly in automatically generated documentation. It’s easy to update the function and forget about changing the docstring, or to make a typo. [And yes, I’m aware of the fact that the docstring already duplicates the function body. Please ignore this; funny_function is just a random example.]

2 — Not use a decorator, or use a special-purpose decorator for every specific signature:

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

This works fine for a set of functions that have identical signature, but it’s useless in general. As I said in the beginning, I want to be able to use decorators entirely generically.

I’m looking for a solution that is fully general, and automatic.

So the question is: is there a way to edit the decorated function signature after it has been created?

Otherwise, can I write a decorator that extracts the function signature and uses that information instead of “*kwargs, **kwargs” when constructing the decorated function? How do I extract that information? How should I construct the decorated function — with exec?

Any other approaches?


回答 0

  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

  1. Install decorator module:

    $ pip install decorator
    
  2. Adapt definition of 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 preserves signatures since 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() is available at least since Python 2.5 but it does not preserve the signature there:

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

Notice: *args, **kwargs instead of x, y, z=3.


回答 1

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

This is solved with Python’s standard library functools and specifically functools.wraps function, which is designed to “update a wrapper function to look like the wrapped function“. It’s behaviour depends on Python version, however, as shown below. Applied to the example from the question, the code would look like:

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

When executed in Python 3, this would produce the following:

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

Its only drawback is that in Python 2 however, it doesn’t update function’s argument list. When executed in Python 2, it will produce:

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

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

回答 2

有一个带装饰器的装饰器模块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功能-现在已修复。

There is a decorator module with decorator decorator you can use:

@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)

Then the signature and help of the method is preserved:

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

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

EDIT: J. F. Sebastian pointed out that I didn’t modify args_as_ints function — it is fixed now.


回答 3

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

Take a look at the decorator module – specifically the decorator decorator, which solves this problem.


回答 4

第二种选择:

  1. 安装包装模块:

$ easy_install包装

包裹有奖励,保留Class签名。


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

Second option:

  1. Install wrapt module:

$ easy_install wrapt

wrapt have a bonus, preserve class signature.


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

回答 5

如上文在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

As commented above in jfs’s answer ; if you’re concerned with signature in terms of appearance (help, and inspect.signature), then using functools.wraps is perfectly fine.

If you’re concerned with signature in terms of behavior (in particular TypeError in case of arguments mismatch), functools.wraps does not preserve it. You should rather use decorator for that, or my generalization of its core engine, named 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)

See also this post about functools.wraps.