保留修饰函数的签名
假设我写了一个装饰器,它可以做一些很通用的事情。比如,它可以把所有的参数转换成特定的类型,进行日志记录,或者实现缓存等等。
这里有个例子:
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 -- 在文档字符串中包含正确的签名:
def funny_function(x, y, z=3):
"""funny_function(x, y, z=3) -- computes x*y + 2*z"""
return x*y + 2*z
这个方法不好,因为会造成重复。自动生成的文档中仍然不会正确显示签名。更新函数时很容易忘记修改文档字符串,或者出现拼写错误。[而且,是的,我知道文档字符串已经重复了函数体。请忽略这一点;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吗?
还有其他方法吗?
8 个回答
这里有一个装饰器模块,里面有一个叫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
函数——现在已经修复了。
这个问题可以通过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 模块:
$ 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()
是标准库中的一个功能,从 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
。