Вот она, тема, которую спрашивают почти на каждом Python-собеседовании уровня middle и выше. И вот она же тема, в которой чаще всего плавают, потому что выучили синтаксис @something, а что происходит на самом деле, не разбирали.
Начнём с аналогии, и она реально рабочая, не просто для красоты.
Представь человека. Обычного, без каких-либо дополнительных приспособлений. Теперь надень на него дождевик. Человек внутри остался ровно тем же самым человеком: тот же рост, тот же характер, то же лицо. Но теперь у него появилось новое свойство - дождь ему не страшен.
Сверху на дождевик можно надеть ещё и тёплую куртку. Человек всё ещё тот же самый, но теперь ему не страшен ещё и холод. А если вместо куртки надеть экзоскелет, человек сможет идти в гору без устали, при этом продолжая оставаться собой.
Вот декоратор работает ровно так же. Декоратор берёт функцию, "надевает" на неё дополнительное поведение, но саму функцию изнутри не меняет. Функция как считала свою логику, так и продолжает считать. Просто вокруг неё появляется обёртка, которая добавляет что-то новое: логирование, замер времени, проверку прав, повторные попытки при ошибке.
Прежде чем пользоваться синтаксисом @, важно увидеть, что декоратор - это просто функция высшего порядка. Никакой отдельной магии в языке для него нет, это тот же самый механизм, что мы разбирали два урока назад: функция принимает другую функцию и возвращает третью.
Вывод:
Надеваем дождевик: теперь дождь не страшен
Иду гулять
Снимаем дождевик
Разбираем по шагам, что тут происходит:
with_raincoat - это и есть декоратор. Обычная функция, которая принимает одну функцию (func) и возвращает другую (wrapper).with_raincoat определена wrapper - та самая "одежда". Она замыкает на себе func (помнишь замыкания из прошлого урока?) и умеет и напечатать что-то до вызова, и вызвать саму func, и напечатать что-то после.walk_outside = with_raincoat(walk_outside) - вот ключевая строчка. Мы взяли исходную функцию walk_outside, прогнали через декоратор и переприсвоили переменной walk_outside уже новую, обёрнутую версию. Старая функция никуда физически не делась, она просто теперь спрятана внутри wrapper как func.walk_outside(), на самом деле вызывается wrapper(), а она уже сама решает, когда и как вызвать оригинальную функцию.Обрати внимание на сигнатуру wrapper(*args, **kwargs). Это не случайность и не украшательство. Декоратор в общем случае не знает заранее, сколько аргументов и с какими именами будет принимать оборачиваемая функция. *args собирает все позиционные аргументы в кортеж, **kwargs все именованные в словарь, а при вызове func(*args, **kwargs) они точно так же распаковываются обратно. Это позволяет одному и тому же декоратору работать с абсолютно любой функцией, независимо от того, сколько у неё параметров.
Пример с более практичной нагрузкой, логирование вызовов:
Вывод:
Вызываем add с аргументами (2, 3), {}
add вернула 5
5
Это уже вполне реальный кусочек того, что называют cross-cutting concerns - сквозной функциональностью, которая нужна во множестве разных мест кода и не должна размазываться внутри каждой функции по отдельности. Логирование, замер времени, проверка доступа, кэширование - всё это классические кандидаты на декораторы, потому что сама бизнес-логика функции (в данном случае a + b) остаётся кристально чистой, а всё "обвязочное" поведение живёт отдельно, в обёртке.
Мы специально разберём декораторы по нарастающей сложности, потому что на собеседовании спрашивают именно так - от простого к сложному, проверяя, где ты застрянешь:
@ вместо ручного переприсваивания (следующий урок).functools.wraps.staticmethod/classmethod.__call__.wrapper(*args, **kwargs) нужен, чтобы декоратор работал с функцией любой сигнатуры, а не только с конкретным набором аргументов.func = decorator(func) - это ровно то, что происходит "под капотом" у синтаксиса @decorator, который разберём в следующем уроке.