使用functools.wraps在装饰器链中保留确切的功能签名

时间:2020-01-22 15:24:37

标签: python python-3.x python-decorators functools

在Python 3.4+中,functools.wraps保留了它包装的函数的签名。不幸的是,如果创建的装饰器要堆叠在一起,则序列中的第二个(或更高版本)装饰器将看到的通用*args**kwargs签名包装器,并且在装饰器序列的底部始终不保留原始功能的签名。这是一个例子。

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()

这给

-------------------------------------------------------------------------
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'

如果您切换装饰器的顺序,则'y'会得到相同的键错误。

我尝试在第二个装饰器中将wraps(func)替换为wraps(func.__wrapped__),但这仍然行不通(更不用说它要求程序员明确知道他们要在装饰器堆栈中的哪个位置工作给定包装器功能)。

我也查看了inspect.signature(foo),这似乎是对的,但是我发现这是因为inspect.signature的{​​{1}}参数默认为{{1} },因此它以某种方式知道遵循包装函数的顺序,但是显然,用于调用follow_wrapped的常规方法调用框架不会遵循相同的协议来解析外部装饰包装的args和kwargs。

我怎样才能让True忠实地通过签名,以便foo()(可以这么说)总是忠实地复制wraps的签名?

3 个答案:

答案 0 :(得分:2)

您实际上没有向函数foo传递任何参数,因此*args**kwargs对于两个装饰器都是空的。如果您传递参数,装饰器将正常工作

foo(x=2, y = 3) # prints 5

您可以尝试使用inspect

获取默认函数参数

答案 1 :(得分:1)

不使用inspect就无法真正获得默认值,并且还需要考虑位置参数(*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)

此打印

4
5
12

答案 2 :(得分:0)

您的诊断不正确;实际上,functools.wraps保留了双重修饰函数的签名:

>>> import inspect
>>> inspect.signature(foo)
<Signature (x=1, y=3)>

我们还可以观察到,使用错误的签名调用函数不是问题,因为这将引发TypeError,而不是KeyError

似乎给人的印象是,仅使用一个装饰器时,kwargs将填充参数默认值。这根本不会发生:

def test_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print('args:', args)
        print('kwargs:', kwargs)
        return func(*args, **kwargs)
    return wrapper

@test_decorator
def foo(x=1):
    print('x:', x)

输出为:

>>> foo()
args: ()
kwargs: {}
x: 1

如您所见,即使仅使用一个装饰器,argskwargs都不会接收该参数的默认值。它们都是空的,因为foo()调用了包装函数,没有位置参数,也没有关键字参数。


实际的问题是您的代码有逻辑错误。装饰器validate_xvalidate_y期望将参数作为关键字参数传递,但实际上它们可能作为位置参数传递或根本不传递(因此将应用默认值),在这种情况下,{ {1}中将没有{1}}和/或'x'

没有简单的方法可以使装饰器使用可以作为关键字或位置传递的参数。如果您将参数设为仅关键字,则可以在验证它们之前测试'y'中的kwargs还是'x'

'y'

通常最好显式地kwargs错误而不是使用def validate_x(func): @wraps(func) def wrapper(*args, **kwargs): if 'x' in kwargs and kwargs['x'] > 2: raise ValueError('Invalid x, should be <= 2, was ' + str(x)) return func(*args, **kwargs) return wrapper @validate_x def bar(*, x=1): # keyword-only arg, prevent passing as positional arg ... ,因为您的程序can be run with assert disabled

还请注意,有可能在默认raise无效的情况下声明类似assert的函数。这不会引发任何错误,因为装饰器未检查默认参数值。