首页 文章

保留装饰功能的签名

提问于
浏览
95

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

这是一个例子:

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 - 在docstring中包含正确的签名:

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?

还有其他方法吗?

6 回答

  • 9
    $ 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 .

  • 13

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

    你可以使用decorator moduledecorator 装饰器:

    @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 功能 - 它现在已修复 .

  • 6

    看一下decorator模块 - 特别是decorator装饰器,它解决了这个问题 .

  • 0

    第二种选择:

    • 安装包装模块:

    $ 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
    
  • 8

    如上所述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 .

相关问题