Декораторы с аргументами#

Иногда необходимо чтобы у декоратора была возможность принимать аргументы. В таком случае надо добавить еще один уровень вложенности для декоратора.

Самый базовый вариант декоратора с аргументами, когда функция не подменяется и аргументы функции не перехватываются. Тут к функции только добавляются атрибуты, которые указаны при вызове декоратора:

def add_mark(**kwargs):
    def decorator(func):
        for key, value in kwargs.items():
            setattr(func, key, value)
        return func
    return decorator


@add_mark(test=True, ordered=True)
def test_function(a, b):
    return a + b


In [73]: test_function.ordered
Out[73]: True

In [74]: test_function.test
Out[74]: True

Пошагово происходит следующее: сначала вызывается функция add_mark с соответствующими аргументами

decorate = add_mark(test=True, ordered=True)

Полученный результат будет декоратором, который ждет функцию как аргумент. То есть, то же самое можно сделать в два шага:

def add_mark(**kwargs):
    def decorator(func):
        for key, value in kwargs.items():
            setattr(func, key, value)
        return func
    return decorator

decorate = add_mark(test=True, ordered=True)

@decorate
def test_function(a, b):
    return a + b


In [73]: test_function.ordered
Out[73]: True

In [74]: test_function.test
Out[74]: True

Как только понадобится что-то делать с аргументами функции или добавить что-то до или после вызова функции, добавляется еще один уровень. Например, переделаем декоратор all_args_str таким образом, чтобы тип данных можно было передавать как аргумент. Декоратор all_args_str:

def all_args_str(func):
    @wraps(func)
    def wrapper(*args):
        if not all(isinstance(arg, str) for arg in args):
            raise ValueError('Все аргументы должны быть строками')
        return func(*args)
    return wrapper

Добавляем еще один уровень для добавления аргумента:

def restrict_args_type(required_type):
    def decorator(func):
        @wraps(func)
        def wrapper(*args):
            if not all(isinstance(arg, required_type) for arg in args):
                raise ValueError(f'Все аргументы должны быть {required_type.__name__}')
            return func(*args)
        return wrapper
    return decorator

Теперь, при применении декоратора, надо указывать какого типа должны быть аргументы:

In [89]: @restrict_args_type(str)
    ...: def to_upper(*args):
    ...:     result = [s.upper() for s in args]
    ...:     return result
    ...:

In [90]: to_upper('a', 2)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-90-b46c3ca71e5d> in <module>
----> 1 to_upper('a', 2)

<ipython-input-88-ea0c777e0f6e> in wrapper(*args)
      4             def wrapper(*args):
      5                 if not all(isinstance(arg, required_type) for arg in args):
----> 6                     raise ValueError(f'Все аргументы должны быть {required_type.__name__}')
      7                 return func(*args)
      8             return wrapper

ValueError: Все аргументы должны быть str

In [91]: to_upper('a', 'a')
Out[91]: ['A', 'A']

In [93]: @restrict_args_type(int)
    ...: def to_bin(*args):
    ...:     result = [bin(a) for a in args]
    ...:     return result
    ...:

In [94]: to_bin(1,2,3)
Out[94]: ['0b1', '0b10', '0b11']

In [95]: to_bin('a', 'b')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-95-e4007cc06928> in <module>
----> 1 to_bin('a', 'b')

<ipython-input-88-ea0c777e0f6e> in wrapper(*args)
      4             def wrapper(*args):
      5                 if not all(isinstance(arg, required_type) for arg in args):
----> 6                     raise ValueError(f'Все аргументы должны быть {required_type.__name__}')
      7                 return func(*args)
      8             return wrapper

ValueError: Все аргументы должны быть int

Также при необходимости можно сделать готовые декораторы для определенных типов данных:

In [96]: restrict_args_to_str = restrict_args_type(str)

In [97]: restrict_args_to_int = restrict_args_type(int)

In [98]: @restrict_args_to_str
    ...: def to_upper(*args):
    ...:     result = [s.upper() for s in args]
    ...:     return result
    ...:

In [99]: @restrict_args_to_int
    ...: def to_bin(*args):
    ...:     result = [bin(a) for a in args]
    ...:     return result
    ...: