Python 使用functools.wrapps在装饰器链中保留精确的函数签名
在Python 3.4+中,Python 使用functools.wrapps在装饰器链中保留精确的函数签名,python,python-3.x,python-decorators,functools,Python,Python 3.x,Python Decorators,Functools,在Python 3.4+中,functools.wrapps保留它所包装的函数的签名。不幸的是,如果您创建的装饰器要堆叠在彼此的顶部,则第二个(或更高)装饰器序列中的decorator将看到包装器的泛型*args和**kwargs签名,而不是在decorator序列的底部一直保留原始函数的签名。这里有一个例子 from functools import wraps def validate_x(func): @wraps(func) def wrapper(*args,
functools.wrapps
保留它所包装的函数的签名。不幸的是,如果您创建的装饰器要堆叠在彼此的顶部,则第二个(或更高)装饰器序列中的decorator将看到包装器的泛型*args
和**kwargs
签名,而不是在decorator序列的底部一直保留原始函数的签名。这里有一个例子
from functools import wraps
def validate_x(func):
@wraps(func)
def wrapper(*args, **kwargs):
assert kwargs['x'] <= 2
return func(*args, **kwargs)
return wrapper
def validate_y(func):
@wraps(func)
def wrapper(*args, **kwargs):
assert kwargs['y'] >= 2
return func(*args, **kwargs)
return wrapper
@validate_x
@validate_y
def foo(x=1, y=3):
print(x + y)
# call the double wrapped function.
foo()
从functools导入包装
def validate_x(函数):
@包装(func)
def包装(*args,**kwargs):
断言kwargs['x']=2
返回函数(*args,**kwargs)
返回包装器
@验证
@验证
def foo(x=1,y=3):
打印(x+y)
#调用双包装函数。
foo()
这给
-------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-5-69c17467332d> in <module>
22
23
---> 24 foo()
<ipython-input-5-69c17467332d> in wrapper(*args, **kwargs)
4 @wraps(func)
5 def wrapper(*args, **kwargs):
----> 6 assert kwargs['x'] <= 2
7 return func(*args, **kwargs)
8 return wrapper
KeyError: 'x'
-------------------------------------------------------------------------
KeyError回溯(最近一次呼叫最后一次)
在里面
22
23
--->24 foo()
包装中(*args,**kwargs)
4@wrapps(func)
5 def包装(*args,**kwargs):
---->6 assert kwargs['x']您实际上没有向函数foo
传递任何参数,因此*args
和**kwargs
对于这两个修饰符都是空的。如果您传递参数,则装饰器将正常工作
foo(x=2,y=3)#打印5
您可以尝试使用获取默认函数参数如果不使用
检查就无法真正获取默认值,并且还需要考虑位置参数(*args
)与关键字参数(**kwargs
)。因此,如果数据存在,则对其进行规范化如果数据丢失,则检查函数
import inspect
from functools import wraps
def get_default_args(func):
signature = inspect.signature(func)
return {
k: v.default
for k, v in signature.parameters.items()
if v.default is not inspect.Parameter.empty
}
def validate_x(func):
@wraps(func)
def wrapper(*args, **kwargs):
if args and not kwargs and len(args) == 2:
kwargs['x'] = args[0]
kwargs['y'] = args[1]
args = []
if not args and not kwargs:
kwargs = get_default_args(func)
assert kwargs['x'] <= 2
return func(*args, **kwargs)
return wrapper
def validate_y(func):
@wraps(func)
def wrapper(*args, **kwargs):
if args and not kwargs and len(args) == 2:
kwargs['x'] = args[0]
kwargs['y'] = args[1]
args = []
if not args and not kwargs:
kwargs = get_default_args(func)
assert kwargs['y'] >= 2
return func(*args, **kwargs)
return wrapper
@validate_x
@validate_y
def foo(x=1, y=3):
print(x + y)
# call the double wrapped function.
foo()
# call with positional args
foo(1, 4)
# call with keyword args
foo(x=2, y=10)
你的诊断是错误的;实际上,
functools.wrapps
保留了双修饰函数的签名:
导入检查
>>>检查签名(foo)
我们还可以观察到,使用错误的签名调用函数不是问题,因为这会引发TypeError
,而不是KeyError
您似乎有这样的印象:当只使用一个装饰器时,kwargs
将填充参数默认值。这根本不会发生:
def测试装饰器(func):
@包装(func)
def包装(*args,**kwargs):
打印('args:',args)
打印('kwargs:',kwargs)
返回函数(*args,**kwargs)
返回包装器
@测试装饰器
def foo(x=1):
打印('x:',x)
输出为:
>>foo()
args:()
kwargs:{}
x:1
如您所见,args
和kwargs
都不会收到参数的默认值,即使只使用了一个装饰符。它们都是空的,因为foo()
调用包装函数时没有位置参数和关键字参数
实际的问题是您的代码有一个逻辑错误。装饰程序
validate\u x
和validate\u y
希望参数作为关键字参数传递,但实际上它们可能作为位置参数传递,也可能根本不传递(因此默认值将适用),在这种情况下'x'
和/或'y'
不会出现在kwargs
中
没有简单的方法可以让你的装饰者使用一个可以作为关键字或位置传递的参数;如果只使用arguments关键字,则可以在验证它们之前测试'x'
或'y'
是否在kwargs
中
def validate_x(func):
@包装(func)
def包装(*args,**kwargs):
如果kwargs中的“x”和kwargs['x']>2:
raise VALUERROR('Invalid x,应该注意,在我的示例中,内部装饰程序,例如@validate_y
工作得很好。它使用了wrapped函数中的kwargs
,没有任何inspect
操作。错误只发生在包装(…)的外部装饰程序上
另一个修饰函数。当我在调试器中运行你的代码时,args和kwargs都是空的。这是因为默认值是在调用函数时填充的,而不是以前。这就是为什么我添加了args
->kwargs
的规范化,并且需要检查以获得默认值。这就是为什么对于外部装饰者是正确的,但对于内部装饰者似乎不是正确的。@ely内部装饰者@validate\u y
在您的示例中也不起作用;只是您没有给它一个失败的机会,因为@validate\u x
首先失败。无论是使用一个还是两个装饰者,行为都是相同的;无论哪种方式它引发了一个KeyError
。我已经在我的问题中解决了这个问题。请注意,inspect.signature
有一个默认为True的follow\u wrapped
参数,但是函数调用机制中使用的实际签名是不同的,而不是遵循wrapp。包装的实际签名是(*args,**kwargs)
因为您就是这样声明的。正如我所说,错误不在签名中,而是在装饰程序的逻辑中。您正在无条件地访问kwargs['x']
当kwargs
不能保证包含'x'
作为键时。如果签名是问题所在,那么调用foo
时会出现TypeError
错误的参数,而不是函数体中的KeyError
;函数体执行的事实意味着参数are根据签名可以接受。打个比方,如果您定义了def wrapper(*args,**kwargs):raise ValueError()
,那么foo()
将引发ValueError,因为这是包装函数的声明行为;调用修饰函数引发错误并不意味着其签名无效
4
5
12