Прямой ответ
Декоратор — функция, принимающая другую функцию и возвращающая обёртку, поэтому поведение расширяется без изменения исходного кода.
import functools, time
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"{func.__name__}: {time.perf_counter() - start:.4f} с")
return result
return wrapper
@timed
def build_report(data):
...
| Аспект | Назначение |
|---|---|
functools.wraps |
Сохранение имени и документации обёрнутой функции |
*args, **kwargs |
Передача произвольных аргументов |
| Декоратор с аргументами | Дополнительный уровень вложенности |
Сохранение метаданных через functools.wraps обеспечивает корректное имя функции при отладке и в документации. О передаче аргументов — в статье про упаковку и распаковку.
Декораторы с аргументами и применение к классам
Два расширения базового механизма требуют внимания при использовании.
Декоратор с аргументами. Параметры декоратора передаются на внешнем уровне, поэтому добавляется ещё один уровень вложенности. Внешняя функция принимает параметры и возвращает декоратор.
Декоратор класса. Декоратор применяется к классу и возвращает изменённый класс либо обёртку, поэтому поведение всех экземпляров изменяется.
Практическое следствие: декоратор с аргументами отличается от декоратора без аргументов числом уровней вложенности, поэтому применение без скобок и со скобками даёт разный результат.
Отдельный аспект — порядок применения. Несколько декораторов применяются снизу вверх, поэтому ближайший к функции декоратор выполняется первым при вызове.
Три признака некорректного использования декораторов.
Отсутствие сохранения метаданных. Имя обёрнутой функции теряется, поэтому отладка и документация показывают имя обёртки.
Избыточная вложенность. Несколько уровней обёрток усложняют чтение кода и трассировку стека.
Декоратор с побочными эффектами при импорте. Действие выполняется при определении функции, а не при вызове, поэтому эффект наступает раньше ожидаемого.
О контекстных менеджерах как альтернативе для управления ресурсами — в статье про with; о декораторах на уровне класса — в статье про метаклассы и дескрипторы.
Декоратор выполняется при определении функции, поэтому тяжёлая работа в теле декоратора выполняется один раз, а не при каждом вызове.
Декораторы методов и доступ к экземпляру
Декоратор, применённый к методу класса, получает экземпляр первым аргументом, поэтому обёртка учитывает способ вызова.
Передача экземпляра. Обёртка принимает аргументы в упакованном виде, поэтому экземпляр передаётся первым элементом. Явное указание экземпляра в обёртке делает декоратор применимым только к методам.
Декоратор класса и декоратор метода. Декоратор класса изменяет поведение всех экземпляров, декоратор метода — поведение одного метода. Выбор определяется областью применения.
Кэширование результата. Обёртка может сохранять результат вызова, поэтому повторный вызов с теми же аргументами не выполняет работу. Кэш привязан к обёртке, поэтому различается для разных экземпляров.
Практическое следствие: обёртка метода принимает произвольные аргументы, поэтому применима к методу с любой сигнатурой.
Декораторы и тестирование обёрнутого кода
Обёртка изменяет способ вызова функции, поэтому тестирование учитывает поведение декоратора.
Тестирование обёрнутой функции. Тест вызывает функцию через обёртку, поэтому проверяется и поведение декоратора. Тест исходной функции без декоратора не покрывает обёртку.
Подмена обёрнутой функции. Обёртка передаёт управление исходной функции, поэтому подмена исходной функции влияет на результат теста.
Побочные эффекты обёртки. Обёртка, выполняющая запись в журнал либо учёт вызовов, требует проверки побочного эффекта в тесте.
Практическое следствие: декоратор является частью поведения функции, поэтому тестирование охватывает обёртку, а не только исходный код.
Практика: применение декораторов
1. Сохраняйте метаданные обёрнутой функции. Имя и документация требуются для отладки и генерации документации.
2. Передавайте произвольные аргументы. Обёртка принимает аргументы в упакованном виде, поэтому применима к любой функции.
3. Различайте декоратор с аргументами и без. Применение со скобками и без скобок даёт разный результат.
4. Учитывайте порядок применения. Декораторы применяются снизу вверх, поэтому порядок влияет на поведение.
5. Не выполняйте тяжёлую работу при каждом вызове. Работа, не зависящая от аргументов, выполняется один раз при определении.
6. Ограничивайте число уровней вложенности. Избыточные обёртки усложняют чтение кода и трассировку стека.
7. Проверяйте момент выполнения. Тело декоратора выполняется при определении функции, а не при вызове.
Рекомендации:
- Различайте декоратор и явную обёртку. Декоратор применяется при определении, явная обёртка — при вызове.
- Учитывайте влияние на трассировку стека. Каждый уровень обёртки добавляет кадр, поэтому стек вызовов удлиняется.
- Проверяйте применимость к методу класса. Декоратор метода получает экземпляр первым аргументом, поэтому обёртка учитывает это.
- Фиксируйте назначение декоратора. Декоратор с неочевидным поведением усложняет понимание кода.
- Пересматривайте при изменении обёрнутой функции. Изменение сигнатуры может сделать обёртку непригодной.
Практический сценарий: имя функции терялось в логах
Контекст: журнал выполнения показывал имя обёртки вместо имени обёрнутой функции, поэтому определить источник записи было нельзя.
- Диагностика. Декоратор возвращал обёртку без сохранения метаданных, поэтому имя функции заменялось именем обёртки. Записи журнала и трассировки стека содержали одно и то же имя для разных функций.
- Причина. Сохранение метаданных не выполнялось, поэтому обёртка заменяла имя, документацию и список аргументов обёрнутой функции.
- Действия. (а) Добавили сохранение метаданных во все декораторы проекта; (б) проверили, что имя и документация соответствуют обёрнутой функции; (в) сократили число уровней вложенности там, где обёртки дублировали поведение; (г) проверили момент выполнения тела декоратора и убедились в отсутствии тяжёлой работы при импорте; (д) зафиксировали требование сохранения метаданных в правилах разработки.
- Результат. Записи журнала содержат имя обёрнутой функции, поэтому источник записи определяется без дополнительного разбора.
Вывод: декоратор без сохранения метаданных заменяет имя обёрнутой функции, поэтому отладка и журнал теряют информацию об источнике. Сохранение метаданных обеспечивает корректное имя при любом числе обёрток.
FAQ
Что делает functools.wraps?
Копирует имя, документацию и список аргументов обёрнутой функции на обёртку, поэтому метаданные сохраняются при отладке и генерации документации.
Чем декоратор с аргументами отличается от обычного? Добавляется внешний уровень, принимающий параметры. Применение со скобками вызывает внешний уровень, поэтому результат отличается от применения без скобок.
В каком порядке применяются несколько декораторов? Снизу вверх: ближайший к функции декоратор применяется первым, поэтому при вызове он выполняется последним.
Когда выполняется тело декоратора? При определении функции, а не при вызове. Тяжёлая работа в теле декоратора выполняется один раз при импорте модуля.
Источники: Python glossary decorator, functools, function definitions — ссылки в шапке.