Preserving signatures of decorated functions
假设我写了一个装饰器来做一些非常普通的事情。例如,它可以将所有参数转换为特定类型、执行日志记录、实现内存化等。
下面是一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 |
到目前为止一切都很好。然而,有一个问题。修饰函数不保留原始函数的文档:
1 2 3 4 | >>> help(funny_function) Help on function g in module __main__: g(*args, **kwargs) |
号
幸运的是,有一个解决方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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 |
这次,函数名和文档是正确的:
1 2 3 4 5 | >>> help(funny_function) Help on function funny_function in module __main__: funny_function(*args, **kwargs) Computes x*y + 2*z |
。
但是仍然有一个问题:函数签名是错误的。信息"*args,**kwargs"几乎是无用的。
怎么办?我可以想到两个简单但有缺陷的解决方法:
1——在docstring中包含正确的签名:
1 2 3 | 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已经复制了函数体。请忽略这一点;有趣的_函数只是一个随机的例子。]
2——不使用修饰符,也不为每个特定的签名使用特殊用途的修饰符:
1 2 3 4 5 6 | 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?
还有其他方法吗?
安装装饰模块:
1 | $ pip install decorator |
调整
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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 |
。
Python3.4+
stdlib中的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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 |
1 2 3 4 5 | help(funny_function) # Help on function funny_function in module __main__: # # funny_function(*args, **kwargs) # Computes x*y + 2*z |
。
注意:用
这是通过python的标准库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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中执行时,将产生以下结果:
1 2 3 4 5 6 7 | >>> 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中,它不更新函数的参数列表。在python 2中执行时,它将生成:
1 2 3 4 5 | >>> help(funny_function) Help on function funny_function in module __main__: funny_function(*args, **kwargs) Computes x*y + 2*z |
有一个decorator模块带有
1 2 3 4 5 | @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) |
然后保留该方法的签名和帮助:
1 2 3 4 5 | >>> 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指出我没有修改
看看decorator模块——特别是decorator decorator,它解决了这个问题。
第二种选择:
$easy_安装wrapt
Wrapt有奖励,保留类签名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | import wrapt import inspect </p> @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 |
。
正如上面JFS的回答中所评论的那样;如果您关心的是外观方面的签名(
如果您关心签名的行为(特别是在参数不匹配的情况下,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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) |
。
另见关于