Как передаются параметры в функцию-декоратор в Python
Что проверяют
Понимаете ли вы, что @decorator(arg) это не особый синтаксис, а вызов функции, результат которого и становится декоратором. Следом обычно просят написать декоратор, который работает и как @retry, и как @retry(times=3), или спрашивают, когда вычисляются аргументы.
Как отвечать
Запись @repeat(3) над def f равна f = repeat(3)(f). Отсюда три уровня вложенности:
- фабрика
repeat(times): принимает параметры и возвращает сам декоратор; - декоратор
decorator(func): принимает функцию и возвращает обёртку; - обёртка
wrapper(*args, **kwargs): выполняется при каждом вызовеfи видитtimesиfuncчерез замыкание.
Обёртку помечают @functools.wraps(func): без неё теряются __name__, __doc__ и __wrapped__, ломаются логи, документация и интроспекция вроде inspect.signature.
Фабрика и декоратор выполняются один раз, в момент определения функции, обычно при импорте модуля. Значение times фиксируется тогда же, а не при каждом вызове.
Чтобы поддержать обе формы, пишут def retry(func=None, *, times=3): если func передан, декоратор применили без скобок, иначе возвращают functools.partial(retry, times=times). Keyword-only параметры здесь защищают от путаницы, когда первым позиционным аргументом случайно оказывается функция.
Альтернатива вложенности это класс: параметры в __init__, функция в __call__. Удобно, если у декоратора есть состояние. Для типизации обёртки с Python 3.10 есть ParamSpec, он сохраняет сигнатуру для mypy и IDE.
Где спотыкаются
Путают @retry и @retry(). Без скобок фабрика получает функцию вместо параметра, и вызов f() ведёт себя странно: возвращает функцию вместо результата или падает с TypeError.
Забывают return в обёртке, и декорированная функция всегда возвращает None.
Держат в замыкании изменяемое состояние, например счётчик вызовов, и удивляются, что он общий для всех вызовов и потоков. Для присваивания внутри обёртки нужен nonlocal, а для многопоточного кода ещё и блокировка.