关于python:保留修饰函数的签名

Preserving signatures of decorated functions

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

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

到目前为止一切都很好。然而,有一个问题。修饰函数不保留原始函数的文档:

1
2
3
4
>>> help(funny_function)
Help on function g in module __main__:

g(*args, **kwargs)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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

这次,函数名和文档是正确的:

1
2
3
4
5
>>> help(funny_function)
Help on function funny_function in module __main__:

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

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

怎么办?我可以想到两个简单但有缺陷的解决方法:

1——在docstring中包含正确的签名:

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

这很糟糕,因为重复。在自动生成的文档中,签名仍然无法正确显示。很容易更新函数,忘记更改docstring或输入错误。[是的,我知道docstring已经复制了函数体。请忽略这一点;有趣的_函数只是一个随机的例子。]

2——不使用修饰符,也不为每个特定的签名使用特殊用途的修饰符:

1
2
3
4
5
6
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
    $ pip install decorator
  • 调整args_as_ints()的定义:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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

  • Python3.4+

    stdlib中的functools.wraps()保留了自python 3.4以来的签名:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    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之后可用,但它不保留那里的签名:

    1
    2
    3
    4
    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


    这是通过python的标准库functools和特定的functools.wraps函数来解决的,该函数旨在"更新包装函数,使其看起来像包装函数"。但是,它的行为取决于Python版本,如下所示。应用于问题中的示例,代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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中执行时,将产生以下结果:

    1
    2
    3
    4
    5
    6
    7
    >>> 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

    它的唯一缺点是,在Python2中,它不更新函数的参数列表。在python 2中执行时,它将生成:

    1
    2
    3
    4
    5
    >>> help(funny_function)
    Help on function funny_function in module __main__:

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


    有一个decorator模块带有decoratordecorator,您可以使用:

    1
    2
    3
    4
    5
    @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)

    然后保留该方法的签名和帮助:

    1
    2
    3
    4
    5
    >>> 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函数——它现在已经修复了。


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


    第二种选择:

  • 安装包装模块:
  • $easy_安装wrapt

    Wrapt有奖励,保留类签名。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    import wrapt
    import inspect
    </p>

    @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


    正如上面JFS的回答中所评论的那样;如果您关心的是外观方面的签名(helpinspect.signature,那么使用functools.wraps是完全正确的。

    如果您关心签名的行为(特别是在参数不匹配的情况下,TypeError),functools.wraps不会保留签名。您最好使用decorator,或者我对其核心引擎makefun的概括。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    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的帖子。