Сообщить об ошибке.

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

Иногда бывает нужно передать аргументы декораторам. В шаблоне декоратора общего назначения, имя написанное после символа @, относится к объекту функции, который может быть вызван другой функцией.

Чтобы получить декоратор, в который можно передать аргументы, нужно из функции с параметрами вернуть функциональный объект, который может действовать как декоратор.

def func_param(param=4):
    def decorator(func):
        # Создать и вернуть функцию-обертку
        ...
    return decorator

Обычно декоратор создает и возвращает внутреннюю функцию-обертку, следовательно полный пример даст внутреннюю функцию внутри внутренней функции. Звучит запутанно (смотрите ниже как проще создать декоратор с аргументами, используя класс)... Для тех, кто хочет понять, как все это работает, будем разбираться на реальном примере.

Создаваемый декоратор @repeater() будет повторять декорируемую функцию, переданное в качестве аргумента, количество раз.

import functools

def repeater(repeat=1):
    """Повторение выполнения кода"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for i in range(repeat):
                print(f'{i+1}: ', end='')
                val = func(*args, **kwargs)
            return val
        return wrapper
    return decorator

Самая внешняя функция repeater(), которая принимает аргументы, возвращает ссылку на функцию декоратор decorator().

В самой функции repeater() происходит несколько тонких вещей:

  1. Определение внутренней функции decorator() означает, что внешняя функция repeater() будет ссылаться на объект функции decorator. В простых декораторах без параметров, имя функции decorator() используется без скобок для ссылки на объект функции.

    @decorator
    def func():
       ...
    

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

    @repeater(repeat=3)
    def func():
       ...
    
  2. Аргумент repeat, явно не используется в самой функции repeater(), но при передаче параметра создается замыкание, где значение repeat сохраняется до тех пор, пока оно не будет использовано позже функцией wrapper().

Применим декоратор repeater() к функции say():

@repeater(repeat=3)
def say(name):
    print (f 'Hello {name}!')

say('Андрей')
# 1: Hello Андрей!
# 2: Hello Андрей!
# 3: Hello Андрей!

Следующий пример будет создавать требуемую задержку выполнения кода. Такое поведение иногда требуется для мониторинга доступности какого нибудь ресурса. Декоратор назовем delayed.

import functools
import time

def delayed(delay=1):
    """Задержка перед вызовом функции"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f'Спим {delay} сек.')
            time.sleep(delay)
            val = func(*args, **kwargs)
            return val
        return wrapper
    return decorator

@delayed(delay=0.5)
def countdown(int_num):
    if int_num < 1:
        exit(0)
    else:
        print(int_num)
    countdown(int_num - 1)    
        
countdown(3)
# Спим 0.5 сек.
# 3
# Спим 0.5 сек.
# 2
# Спим 0.5 сек.
# 1
# Спим 0.5 сек.

Декоратор с аргументами в виде класса.

Выше, описывался способ написания декоратора с аргументами, при составлении которого можно запутаться. Так и было принято писать такие декораторы, пока кто-то не наткнулся на возможность использования классов в виде декораторов. Изучая эту возможность, придумал передавать методу __init__ аргументы декоратора, а метод __call__ использовать, как декоратор общего назначения (декоратор без аргументов). В общем, как говориться, все гениальное просто...

По сути, необходимо просто разделить этапы создания декоратора, принимающего аргументы:

  1. Запомнить переданные в декоратор аргументы;
  2. Создать вызываемый декоратор общего назначения и используя область видимости, манипулировать переданными аргументами.

То есть сделать то, что делают классы в Python, следовательно можно не париться с двумя вложенными функциями.

Перепишем декоратор @delayed() с использованием класса:

from functools import wraps
from time import sleep

class Delayed:
    # запоминаем аргументы декоратора
    def __init__(self, delay=1):
        self._delay = delay

    # декоратор общего назначения
    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f'Спим {self._delay} сек.')
            sleep(self._delay)
            val = func(*args, **kwargs)
            return val
        return wrapper

@Delayed(delay=0.5)
def countdown(int_num):
    if int_num < 1:
        exit(0)
    else:
        print(int_num)
    countdown(int_num - 1)    
        
countdown(3) 
# Спим 0.5 сек.
# 3
# Спим 0.5 сек.
# 2
# Спим 0.5 сек.
# 1
# Спим 0.5 сек.

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