Иногда бывает нужно передать аргументы декораторам. В шаблоне декоратора общего назначения, имя написанное после символа @
, относится к объекту функции, который может быть вызван другой функцией.
Чтобы получить декоратор, в который можно передать аргументы, нужно из функции с параметрами вернуть функциональный объект, который может действовать как декоратор.
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()
происходит несколько тонких вещей:
Определение внутренней функции decorator()
означает, что внешняя функция repeater()
будет ссылаться на объект функции decorator
. В простых декораторах без параметров, имя функции decorator()
используется без скобок для ссылки на объект функции.
@decorator def func(): ...
Добавление внешней функции необходимо при определении декораторов, которые принимают аргументы, что бы использовать скобки, для передачи параметров декоратора.
@repeater(repeat=3) def func(): ...
Аргумент 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__
использовать, как декоратор общего назначения (декоратор без аргументов). В общем, как говориться, все гениальное просто...
По сути, необходимо просто разделить этапы создания декоратора, принимающего аргументы:
То есть сделать то, что делают классы в 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 сек.
Этот код гораздо лучше читается чем декоратор с двумя вложенными функциями. Осталось только смириться с именем декоратора, начинающимся с большой буквы или с именем класса, начинающимся с маленькой.