Какие подводные камни есть у декораторов в Python
Что проверяют
Понимаете ли вы, что декоратор это функция, которая при определении подменяет одну функцию другой, и какие последствия у этой подмены. Следом часто просят написать декоратор с параметрами или объяснить, почему после декоратора сломалась асинхронная функция.
Как отвечать
Начните с механики: @deco над def f равносильно f = deco(f), и выполняется это один раз, при импорте модуля. Дальше подводные камни:
- потеря метаданных: обёртка заменяет
__name__,__doc__и сигнатуру. Ломаются логи и всё, что читает сигнатуру: FastAPI, Click, фикстуры pytest. Лечится@functools.wraps(func), он заодно кладёт оригинал в__wrapped__; - код при импорте: всё, что написано в теле декоратора вне обёртки, выполняется при загрузке модуля, и подключение к базе там срабатывает уже на
import; - параметры: декоратор с аргументами это три уровня вложенности, и
@retryбез скобок передаст функцию туда, где ждали число попыток; - порядок: декораторы применяются снизу вверх, верхний оказывается внешним.
@cacheнад@login_requiredвернёт закэшированный ответ, не проверив права; - async: обычная обёртка над
async defвозвращает корутину, не дождавшись её. Таймер измерит ноль,try/exceptне поймает ошибку. Нужнаasync def wrapperсawait, выбор ветки черезinspect.iscoroutinefunction; - общее состояние: переменные в замыкании декоратора общие для всех вызовов и потоков;
lru_cacheна методе:selfпопадает в ключ кэша, и объекты не освобождаются, пока жив кэш. Аргументы при этом должны быть хэшируемыми;- типизация: чтобы mypy и IDE видели настоящую сигнатуру, обёртку типизируют через
ParamSpecиTypeVar.
Где спотыкаются
Забывают functools.wraps и потом не понимают, почему во всех логах функция называется wrapper.
Проверяют декоратор только на синхронных функциях, а в проекте на FastAPI почти всё асинхронное.
Рассуждают о порядке сверху вниз, как читается код. На вопрос «что выполнится первым» лучше отвечать примером из двух декораторов.