假设我编写了一个装饰器来做一些非常通用的东西.例如,它可能会将所有参数转换为特定类型,执行日志记录,实现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?
还有其他方法吗?
安装装饰模块:
$ 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()
来自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()
至少从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
.
这是通过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
有一个装饰器模块,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塞巴斯蒂安指出我没有修改args_as_ints
功能 - 它现在已修复.
看看装饰模块 - 特别是装饰器装饰器,它解决了这个问题.
第二种选择:
安装包装模块:
$ 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