Сохраните точную сигнатуру функции в цепочке декораторов, используя functools.wraps - PullRequest
0 голосов
/ 22 января 2020

В Python 3.4+, functools.wraps сохраняет сигнатуру функции, которую оборачивает. К сожалению, если вы создаете декораторы, которые должны быть расположены друг над другом, второй (или более поздний) декоратор в последовательности будет видеть сигнатуру c *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 имеет параметр follow_wrapped, который по умолчанию равен True, поэтому он каким-то образом знает, следовать последовательности обернутых функций, но очевидно, что обычная структура вызова метода для вызова foo() не будет следовать этому же протоколу для аргументов разрешения и kwargs внешней декорированной оболочки.

Как я могу просто иметь wraps верно проходить через подпись, чтобы wraps(wraps(wraps(wraps(f)))) (так сказать) всегда точно повторял подпись f?

Ответы [ 3 ]

2 голосов
/ 22 января 2020

Вы на самом деле не передаете никакие аргументы функции foo, поэтому *args и **kwargs пусты для обоих декораторов. Если вы передадите аргументы, декораторы будут работать нормально

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

Вы можете попытаться получить аргументы функции по умолчанию, используя inspect

1 голос
/ 22 января 2020

Вы не можете получить значения по умолчанию без использования 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
0 голосов
/ 22 января 2020

Ваш диагноз неверен; фактически, 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

Итак, как вы можете видеть, ни args, ни kwargs не получают значения по умолчанию для аргумента, даже когда используется только один декоратор. Они оба пусты, потому что foo() вызывает функцию-оболочку без позиционных аргументов и без аргументов ключевых слов.


Фактическая проблема заключается в том, что в вашем коде есть логическая ошибка. Декораторы validate_x и validate_y ожидают, что аргументы будут переданы в качестве аргументов ключевого слова, но на самом деле они могут передаваться как позиционные аргументы или вообще не передаваться (поэтому применяются значения по умолчанию), в этом случае '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
    ...

Обычно лучше явно raise сообщить об ошибке, вместо использования assert, потому что ваша программа может быть запущена с assert отключено .

Остерегайтесь также, что возможно объявить функцию, такую ​​как @validate_x def baz(*, x=5): ..., где по умолчанию x является недействительным. Это не вызовет никакой ошибки, поскольку значение аргумента по умолчанию не проверяется декоратором.

...