python 保留装饰函数的签名
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/147816/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Preserving signatures of decorated functions
提问by Fredrik Johansson
Suppose I have written a decorator that does something very generic. For example, it might convert all arguments to a specific type, perform logging, implement memoization, etc.
假设我写了一个装饰器来做一些非常通用的事情。例如,它可能将所有参数转换为特定类型、执行日志记录、实现记忆等。
Here is an example:
下面是一个例子:
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
Everything well so far. There is one problem, however. The decorated function does not retain the documentation of the original function:
到目前为止一切都很好。然而,有一个问题。修饰函数不保留原函数的文档:
>>> help(funny_function)
Help on function g in module __main__:
g(*args, **kwargs)
Fortunately, there is a workaround:
幸运的是,有一个解决方法:
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
This time, the function name and documentation are correct:
这一次,函数名称和文档是正确的:
>>> help(funny_function)
Help on function funny_function in module __main__:
funny_function(*args, **kwargs)
Computes x*y + 2*z
But there is still a problem: the function signature is wrong. The information "*args, **kwargs" is next to useless.
但是还有一个问题:函数签名是错误的。信息 "*args, **kwargs" 几乎没用。
What to do? I can think of two simple but flawed workarounds:
该怎么办?我可以想到两个简单但有缺陷的解决方法:
1 -- Include the correct signature in the docstring:
1 -- 在文档字符串中包含正确的签名:
def funny_function(x, y, z=3):
"""funny_function(x, y, z=3) -- computes x*y + 2*z"""
return x*y + 2*z
This is bad because of the duplication. The signature will still not be shown properly in automatically generated documentation. It's easy to update the function and forget about changing the docstring, or to make a typo. [And yes, I'm aware of the fact that the docstring already duplicates the function body. Please ignore this; funny_function is just a random example.]
由于重复,这很糟糕。签名仍不会在自动生成的文档中正确显示。很容易更新函数而忘记更改文档字符串或输入错误。[是的,我知道文档字符串已经复制了函数体。请忽略这一点;有趣的功能只是一个随机的例子。]
2 -- Not use a decorator, or use a special-purpose decorator for every specific signature:
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
This works fine for a set of functions that have identical signature, but it's useless in general. As I said in the beginning, I want to be able to use decorators entirely generically.
这适用于一组具有相同签名的函数,但一般来说是无用的。正如我在开头所说的,我希望能够完全通用地使用装饰器。
I'm looking for a solution that is fully general, and automatic.
我正在寻找一种完全通用且自动的解决方案。
So the question is: is there a way to edit the decorated function signature after it has been created?
所以问题是:有没有办法在创建后编辑装饰函数签名?
Otherwise, can I write a decorator that extracts the function signature and uses that information instead of "*kwargs, **kwargs" when constructing the decorated function? How do I extract that information? How should I construct the decorated function -- with exec?
否则,我是否可以编写一个装饰器来提取函数签名并在构造装饰函数时使用该信息而不是“*kwargs, **kwargs”?我如何提取该信息?我应该如何使用 exec 构造装饰函数?
Any other approaches?
还有其他方法吗?
采纳答案by jfs
Install decoratormodule:
$ pip install decorator
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
安装装饰器模块:
$ 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+
蟒蛇 3.4+
functools.wraps()
from stdlibpreserves signatures since Python 3.4:
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()
is available at least since Python 2.5but it does not preserve the signature there:
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
Notice: *args, **kwargs
instead of x, y, z=3
.
注意:*args, **kwargs
而不是x, y, z=3
.
回答by Timur
This is solved with Python's standard library functools
and specifically functools.wraps
function, which is designed to "update a wrapper function to look like the wrapped function". It's behaviour depends on Python version, however, as shown below. Applied to the example from the question, the code would look like:
这可以通过 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
When executed in Python 3, this would produce the following:
在 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
Its only drawback is that in Python 2 however, it doesn't update function's argument list. When executed in Python 2, it will produce:
它唯一的缺点是在 Python 2 中,它不更新函数的参数列表。在 Python 2 中执行时,它将产生:
>>> help(funny_function)
Help on function funny_function in module __main__:
funny_function(*args, **kwargs)
Computes x*y + 2*z
回答by DzinX
There is a decorator modulewith decorator
decorator you can use:
您可以使用带有装饰器的装饰器模块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)
Then the signature and help of the method is preserved:
然后保留方法的签名和帮助:
>>> help(funny_function)
Help on function funny_function in module __main__:
funny_function(x, y, z=3)
Computes x*y + 2*z
EDIT: J. F. Sebastian pointed out that I didn't modify args_as_ints
function -- it is fixed now.
编辑:JF Sebastian 指出我没有修改args_as_ints
函数——现在已经修复了。
回答by Brian
回答by macm
Second option:
第二种选择:
- Install wrapt module:
- 安装 wrapt 模块:
$ easy_install wrapt
$ easy_install 包装
wrapt have a bonus, preserve class signature.
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
回答by smarie
As commented above in jfs's answer; if you're concerned with signature in terms of appearance (help
, and inspect.signature
), then using functools.wraps
is perfectly fine.
正如上面在jfs 的回答中所评论的;如果您关心签名的外观(help
, 和inspect.signature
),那么使用functools.wraps
完全没问题。
If you're concerned with signature in terms of behavior (in particular TypeError
in case of arguments mismatch), functools.wraps
does not preserve it. You should rather use decorator
for that, or my generalization of its core engine, named makefun
.
如果您关注行为方面的签名(特别是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)
See also this post about functools.wraps
.
另请参阅有关functools.wraps
.