假设我编写了一个非常通用的装饰器。例如,它可能会将所有参数转换为特定类型,执行日志记录,实现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?
还有其他方法吗?
答案 0 :(得分:70)
安装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
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
。
答案 1 :(得分: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
答案 2 :(得分:9)
您可以使用decorator module 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. Sebastian指出我没有修改args_as_ints
函数 - 它现在已修复。
答案 3 :(得分:8)
答案 4 :(得分:6)
第二个选项:
$ easy_install 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
答案 5 :(得分:1)
如上文makefun
中所述;如果您担心外观(true
和help
)方面的签名,那么使用inspect.signature
完全可以。
如果您担心行为方面的签名(特别是在参数不匹配的情况下,functools.wraps
),TypeError
不会保留它。为此,您应该使用functools.wraps
,或者将其核心引擎概括为this post about functools.wraps
。
decorator
另请参阅{{3}}。