03 / 72
ТеорияБазовыйMiddle

Декоратор как аналогия и первая реализация руками

Частота на собеседованиях: Высокая - декораторы спрашивают почти на каждом Python-собеседовании middle+Спрашивают в: Т-Банк, Яндекс, Сбер, Авито, Big Tech-style

Вот она, тема, которую спрашивают почти на каждом Python-собеседовании уровня middle и выше. И вот она же тема, в которой чаще всего плавают, потому что выучили синтаксис @something, а что происходит на самом деле, не разбирали.

Начнём с аналогии, и она реально рабочая, не просто для красоты.

Декоратор - это одежда

Представь человека. Обычного, без каких-либо дополнительных приспособлений. Теперь надень на него дождевик. Человек внутри остался ровно тем же самым человеком: тот же рост, тот же характер, то же лицо. Но теперь у него появилось новое свойство - дождь ему не страшен.

Сверху на дождевик можно надеть ещё и тёплую куртку. Человек всё ещё тот же самый, но теперь ему не страшен ещё и холод. А если вместо куртки надеть экзоскелет, человек сможет идти в гору без устали, при этом продолжая оставаться собой.

Вот декоратор работает ровно так же. Декоратор берёт функцию, "надевает" на неё дополнительное поведение, но саму функцию изнутри не меняет. Функция как считала свою логику, так и продолжает считать. Просто вокруг неё появляется обёртка, которая добавляет что-то новое: логирование, замер времени, проверку прав, повторные попытки при ошибке.

Пишем декоратор руками, без @

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

python
Загрузка редактора...

Вывод:

Надеваем дождевик: теперь дождь не страшен Иду гулять Снимаем дождевик

Разбираем по шагам, что тут происходит:

  1. with_raincoat - это и есть декоратор. Обычная функция, которая принимает одну функцию (func) и возвращает другую (wrapper).
  2. Внутри with_raincoat определена wrapper - та самая "одежда". Она замыкает на себе func (помнишь замыкания из прошлого урока?) и умеет и напечатать что-то до вызова, и вызвать саму func, и напечатать что-то после.
  3. walk_outside = with_raincoat(walk_outside) - вот ключевая строчка. Мы взяли исходную функцию walk_outside, прогнали через декоратор и переприсвоили переменной walk_outside уже новую, обёрнутую версию. Старая функция никуда физически не делась, она просто теперь спрятана внутри wrapper как func.
  4. Когда мы вызываем walk_outside(), на самом деле вызывается wrapper(), а она уже сама решает, когда и как вызвать оригинальную функцию.

Зачем нужны *args и **kwargs в обёртке

Обрати внимание на сигнатуру wrapper(*args, **kwargs). Это не случайность и не украшательство. Декоратор в общем случае не знает заранее, сколько аргументов и с какими именами будет принимать оборачиваемая функция. *args собирает все позиционные аргументы в кортеж, **kwargs все именованные в словарь, а при вызове func(*args, **kwargs) они точно так же распаковываются обратно. Это позволяет одному и тому же декоратору работать с абсолютно любой функцией, независимо от того, сколько у неё параметров.

Пример с более практичной нагрузкой, логирование вызовов:

python
Загрузка редактора...

Вывод:

Вызываем add с аргументами (2, 3), {} add вернула 5 5

Это уже вполне реальный кусочек того, что называют cross-cutting concerns - сквозной функциональностью, которая нужна во множестве разных мест кода и не должна размазываться внутри каждой функции по отдельности. Логирование, замер времени, проверка доступа, кэширование - всё это классические кандидаты на декораторы, потому что сама бизнес-логика функции (в данном случае a + b) остаётся кристально чистой, а всё "обвязочное" поведение живёт отдельно, в обёртке.

Прогрессия усложнения, которую мы дальше пройдём

Мы специально разберём декораторы по нарастающей сложности, потому что на собеседовании спрашивают именно так - от простого к сложному, проверяя, где ты застрянешь:

  1. Декоратор как функция высшего порядка (то, что мы только что сделали).
  2. Синтаксический сахар @ вместо ручного переприсваивания (следующий урок).
  3. Проблема потери метаданных функции и functools.wraps.
  4. Декоратор с параметрами - декоратор, который сам принимает аргументы.
  5. Практические декораторы: кэш, rate limiter, retry.
  6. Декоратор класса целиком.
  7. Декоратор метода класса, с нюансами про staticmethod/classmethod.
  8. Несколько декораторов подряд - порядок применения.
  9. Декоратор с состоянием через класс с __call__.
  10. И на вершине сложности - метаклассы, класс, который порождает другие классы примерно так же, как декоратор порождает обёрнутую функцию.

Проверь себя

  • Декоратор не меняет исходную функцию изнутри, он оборачивает её в дополнительное поведение снаружи - как одежда на человеке.
  • Декоратор это функция высшего порядка: принимает функцию, возвращает другую функцию (обёртку).
  • wrapper(*args, **kwargs) нужен, чтобы декоратор работал с функцией любой сигнатуры, а не только с конкретным набором аргументов.
  • func = decorator(func) - это ровно то, что происходит "под капотом" у синтаксиса @decorator, который разберём в следующем уроке.