Декораторы Python позволяют добавлять к функции общие поведения, такие как измерение времени выполнения, повторные попытки, кэширование или проверка аргументов, без изменения её внутреннего кода. В руководстве, опубликованном на Machine Learning Mastery, автор подробно объясняет их работу и показывает, как создавать надёжные декораторы, не скрывающие исключения.
Исходной точкой является модуль обработки данных, состоящий из трёх функций, которые со временем обросли побочными задачами. Согласно руководству, одна функция проверяет существование пути перед загрузкой эмбеддингов, другая повторяет свои запросы в цикле с помощью time.sleep(), а третья измеряет своё время выполнения и сохраняет результаты в словаре. Этот вспомогательный код, добавленный вручную, различается от функции к функции, и каждая новая функция рискует привнести свой вариант.
Декоратор — простое присваивание
Декоратор — это функция, которая принимает другую функцию в качестве аргумента и возвращает обёртку (wrapper), то есть код, выполняемый до и после исходного вызова. Руководство напоминает, что синтаксис @ эквивалентен переприсваиванию имени функции, что в глоссарии Python называется синтаксическим сахаром. Таким образом, декоратор выполняется только один раз — в момент определения функции.
Обёртка сохраняет исходную функцию благодаря замыканию (closure) — механизму, при котором внутренняя функция сохраняет доступ к переменным внешней функции. Параметры *args и **kwargs передают аргументы, а результат возвращается как есть. Однако без дополнительных мер обёртка заменяет имя и документацию функции. Поэтому руководство рекомендует использовать functools.wraps, который копирует эти метаданные, а также атрибут __wrapped__, позволяющий получить доступ к недекорированной функции при тестировании.
Четыре типа вспомогательного поведения
Декоратор для измерения времени опирается на time.perf_counter() — таймер, предназначенный для вычисления длительности, а не дат. Помещённый в блок try/finally, он выводит время выполнения даже при сбое функции, при этом исключение передаётся вызывающему коду.
Декоратор для повторных попыток является фабрикой, принимающей параметры, такие как max_attempts, delay и типы исключений для перезапуска. Он повторяет попытку только для настроенных исключений, чтобы ошибка программирования не трактовалась как сетевая проблема. При последней попытке голый raise повторно вызывает исходное исключение. Однако руководство отмечает ограничение: этот подход безопасен только для вызовов, которые можно выполнить дважды без последствий. Платёж или отправка сообщения не соответствуют этому критерию.
Для кэширования руководство рекомендует functools.lru_cache, который сохраняет результаты последних вызовов в пределах maxsize (например, 256). Его ограничения связаны с реализацией: аргументы должны быть хэшируемыми, а функция, возвращающая изменяемый объект, может привести к неожиданным эффектам. Наконец, для валидации используется inspect.signature(), чтобы связать аргументы с именами параметров, даже когда они передаются позиционно или со значениями по умолчанию, а затем применяются простые правила перед вызовом.
Руководство завершается предостережением: декоратор не должен определять бизнес-логику функции. Когда поведение зависит от данных, его лучше разместить внутри функции или в модели данных для валидации. При наложении нескольких декораторов важен порядок их применения, поскольку самый верхний является самой внешней обёрткой.
