Menu

Декораторы в Python: @-функции, аргументы и functools.wraps

Что такое декораторы Python на самом деле, как написать свой и паттерны (аргументы, стекинг, wraps), ради которых к ним стоит тянуться.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Декоратор - это функция, которая оборачивает функцию

Фраза звучит абстрактно, но механика простая. Декоратор принимает функцию и возвращает функцию. Возвращённая обычно вызывает исходную, накручивая сверху дополнительное поведение.

Самый короткий пример:

shout - декоратор. Он принимает функцию (greet), строит новую функцию (wrapper), которая вызывает оригинал и повышает регистр результата, и возвращает её. Переприсваивание greet = shout(greet) подменяет оригинал на обёрнутую версию.

Этот паттерн переприсваивания настолько частый, что Python дал ему отдельный синтаксис.

@ - сахар для переприсваивания

@name строкой выше def эквивалентно name = name(...) сразу после определения функции:

@shout читается как «примени декоратор shout к этой функции». Python запускает greet = shout(greet) сразу после def - та же механика, меньше набора.

Как увидишь @name - мысленно заменяй на function = name(function). Это всё, что означает синтаксис.

Обработка аргументов

У большинства функций есть аргументы. Пригодный декоратор пробрасывает их дальше. Идиома - *args, **kwargs, способ Python принять любые аргументы: обёртке не должно быть важно, что ждёт обёрнутая функция:

*args забирает все позиционные аргументы. **kwargs - все keyword-аргументы. Обёртка пробрасывает всё в обёрнутую функцию без изменений и делает дополнительную работу, ради которой декоратор и нужен, - здесь поднимает регистр результата.

Такую форму принимает большинство реальных декораторов.

Более полезный пример: тайминг

Выводим, сколько занимает функция:

Паттерн - «запустить что-то до вызова, запустить что-то после» - то, чем в итоге занимается большинство декораторов. Логирование, проверки авторизации, повторы и валидация - одной формы.

Сохраняем оригинал: functools.wraps

Декорирование функции её заменяет, и обёрнутая функция теряет исходные атрибуты __name__ и __doc__:

greet.__name__ теперь "wrapper", docstring исчез. Это ломает help(), трейсбэки и любые инструменты, инспектирующие функцию.

Ремонт - одна строка: @functools.wraps(func) на внутренней функции копирует метаданные.

Всегда ставь @wraps(func) на внутреннюю функцию. Это ничего не стоит и избавляет от удивлённых отладочных сессий позже.

Декораторы с аргументами

Иногда самому декоратору нужна настройка - «повтори функцию до 3 раз», «логируй на уровне DEBUG». Это ещё один слой вложенности: внешняя функция принимает аргументы и возвращает декоратор.

Три слоя звучит как много. Читай изнутри наружу:

  1. repeat(times=3) - вызов функции. Возвращает decorator.
  2. decorator - собственно декоратор, принимает функцию и возвращает обёрнутую.
  3. wrapper - обёрнутая функция, работающая в момент вызова.

Эта форма лежит в основе @retry(times=5), @cache(maxsize=100) и фреймворковых декораторов вроде @app.route("/users"). Как увидел трёхслойный паттерн - вся семья читается одинаково.

Стекинг декораторов

К функции можно применить несколько декораторов. Они стекаются снизу вверх: ближайший к def срабатывает первым:

Сначала оборачивает add_exclaim, прибавляя !. Потом сверху - shout, повышающий регистр. Вывод - HI, ROSA!.

Порядок важен. Если стек перевернуть, восклицательный знак добавится после повышения регистра - визуально здесь то же самое, но представь декоратор, форматирующий JSON: выполнить его до или после декоратора, логирующего вход, даст совсем разные результаты.

Встроенные декораторы, которые встретишь

У Python и его стандартной библиотеки есть декораторы, с которыми ты встретишься в реальном коде:

  • @property превращает метод в вычисляемый атрибут.
  • @staticmethod помечает метод, не использующий self или cls.
  • @classmethod получает класс как cls вместо экземпляра - отлично для альтернативных конструкторов.
  • @functools.lru_cache мемоизирует результаты, и повторные вызовы с теми же аргументами попадают в кэш.

Фреймворковые декораторы (@app.route, @pytest.fixture, @dataclass) работают по той же механике. Ничего особенного - просто функции, оборачивающие функции.

Когда писать, а когда не писать

Пиши декоратор, когда хочешь применить одно и то же поведение к многим функциям - тайминг, логирование, повторы, проверки доступа. Смысл в том, чтобы держать это поведение вне тела функции.

Пропусти декоратор, когда:

  • Поведение принадлежит одной конкретной функции. Положи его в функцию.
  • Нужен только для тестирования. Фикстура или параметр понятнее.
  • Тянет настакать их четыре-пять. В этот момент управление потоком прячется в цепочке декораторов - читателю придётся разматывать каждый слой, чтобы увидеть, что реально выполняется. Простой хелпер читается лучше.

Декораторы - острый инструмент. Используется хорошо - код сухой, намерение очевидное. Плохо - код скрывает, что делает. Склоняйся к «очевидному», когда решаешь, тянуться ли.

Дальше: аннотации типов

Декораторы - частое место, где встречаешь type hints в дикой природе: функции wrapper часто аннотируют свои сигнатуры. Type hints - маленькая фича, которая быстро окупается, и она на следующей странице.

Часто задаваемые вопросы

Что такое декоратор в Python?

Декоратор - это функция, которая принимает другую функцию и возвращает новую, обычно обёрнутую дополнительным поведением. Применяется через @decorator_name строкой выше def. Синтаксис @ - это сокращение для func = decorator_name(func).

Для чего используются декораторы?

Чтобы добавить поведение вокруг функции, не трогая её тело - логирование, тайминг, кэш, проверки авторизации, валидация входа, повторы. Фреймворки активно ими пользуются: @app.route(...) во Flask, @pytest.fixture в pytest, встроенные @property и @staticmethod.

Можно ли написать свой декоратор?

Да. Декоратор - это просто функция, принимающая функцию и возвращающая функцию. Большинство собственных декораторов оборачивают исходный вызов в маленькую внутреннюю функцию, которая что-то делает до, после или вокруг него. На внутреннюю функцию вешай functools.wraps, чтобы сохранить имя и docstring оригинала.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ