保留修饰函数的签名

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

这里有一个例子:

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,或者输入错误。[ 是的,我知道 docstring 已经复制了函数体。请忽略这一点; fun _ 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?

还有别的办法吗?

22068 次浏览

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

有一个 装饰模块装饰模块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

编辑: J.F. 塞巴斯蒂安指出,我没有修改 args_as_ints功能-它现在是固定的。

  1. 安装 装潢师模块:

    $ 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()是可用的 至少从 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. 安装 wrapt 模块:

$easy _ install wrapt

包装有奖金,保留类签名。


进口包装纸进口包装纸
进口检验

@ wrapt。装潢师 Def args _ as _ int (包装,实例,args,kwargs) : 如实例为无: 如果 spect.isclass (包装) : # Decorator 应用于一个类。 返回包装(* args,* * kwargs) 其他: # Decorator 应用于函数或静态方法。 返回包装(* args,* * kwargs) 其他: 如果 spect.isclass (实例) : # Decorator 应用于类方法。 返回包装(* args,* * kwargs) 其他: # Decorator 应用于实例方法。 返回包装(* args,* * kwargs) @ args _ as _ int Def fun _ function (x,y,z = 3) : “计算 x * y + 2 * z” 返回 x * y + 2 * z > > > fun _ function (3,4,z = 5)) 22英镑 > > > help (fun _ function) 在 module _ _ main _ _ 中函数 fun _ function 的帮助: Fun _ function (x,y,z = 3) 计算 x * y + 2 * z

这个问题可以通过 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

在 Python3中执行时,会产生以下结果:

>>> 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中,它不更新函数的参数列表。在 Python2中执行时,它将产生:

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


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

正如上面在 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的文章

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然后我们返回它。

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 中的参数以供其他用途,那么关键字参数将不会被正确地记录,因此,需要修改函数的签名。