Декораторы Python на собеседовании

Написать декоратор просят редко, а объяснить, когда он выполняется и что делает с именем функции, — почти всегда. Разбираем, что проверяет этот вопрос.

Вопрос про декораторы может растянуться в цепочку уточняющих. Разработчик Авито, который сам проводит собеседования, опубликовал свою лесенку по этой теме, в ней тринадцать шагов: от «что такое декоратор» через «примените его без синтаксического сахара» и «а теперь с аргументом» до func.__doc__ и порядка выполнения при импорте. Ниже — четыре узла, на которые эта лесенка опирается; зная их, остальное можно вывести.

Всё, что сказано о поведении, относится к Python 3.

Декоратор — это присваивание §

Отправная точка задана документацией без всяких оговорок: синтаксис декоратора — это просто синтаксический сахар. Словарь языка приводит рядом два определения и называет их семантически эквивалентными: функция, определённая обычным образом и следом переприсвоенная как f = staticmethod(f), и та же функция под @staticmethod. Разницы между ними нет никакой.

Отсюда рабочее определение, из которого дальше выводится всё: декоратор — вызываемый объект, который получает функцию единственным аргументом, а его результат связывается с именем функции вместо неё самой. Именно так это сформулировано в справочнике языка, и в такой формулировке уже спрятаны ответы на половину лесенки.

Полезно проговорить следствие, которое легко проскочить: никакой магии в @ нет, и любой декоратор можно применить руками, записав то же присваивание. Способность сделать это на бумаге и есть содержание шага «примените без сахара».

Когда декоратор выполняется §

Справочник языка формулирует правило прямо: выражения декораторов вычисляются в момент определения функции, в области, содержащей это определение.

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

Из этого же правила выводится ответ на вопрос про параметризованный декоратор, до которого мы дойдём в следующей секции: @decorator(10) означает, что decorator(10) выполняется прямо при определении, и только его результат становится декоратором.

Вторая половина правила: вычисление происходит в области, содержащей определение. Декоратор видит имена того места, где стоит @.

Почему у параметризованного декоратора три уровня §

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

Справочник даёт преобразование дословно: функция под двумя декораторами, @f1(arg) сверху и @f2 снизу, примерно эквивалентна тому же определению плюс одна строка присваивания: func = f1(arg)(f2(func)).

Из этой строки читается сразу всё, что спрашивают. Порядок применения — изнутри наружу: сначала f2, ближайший к функции, потом результат отдаётся наружу. Три уровня вложенности параметризованного декоратора следуют из неё прямо: f1(arg) — это вызов, который должен вернуть декоратор; тот принимает функцию и возвращает обёртку. Отсюда и три уровня: функция от аргументов → функция от функции → обёртка.

Документация добавляет оговорку, которую стоит воспроизводить вместе с правилом: приведённая запись эквивалентна не полностью, исходная функция не связывается временно с именем func. То есть промежуточного состояния, в котором func указывает на недекорированную функцию, не существует.

Что происходит с именем функции §

Этот шаг в лесенке заходит через func.__doc__, и он проверяет, дочитано ли до конца правило из первой секции. Возвращённое декоратором значение связывается с именем функции. Значит, после декорирования имя указывает на обёртку со всеми её атрибутами.

Отсюда выводится весь набор последствий: __name__ покажет имя обёртки, __doc__ вернёт её докстроку, то есть, скорее всего, None, потому что докстроку внутренней обёртке обычно не пишут. Исходная функция никуда не делась, она захвачена замыканием, но по имени доступна уже обёртка.

Только теперь имеет смысл называть средство: functools.wraps — декоратор, который переносит эти атрибуты с исходной функции на обёртку. Механики он не меняет: имя по-прежнему связано с обёрткой, просто у обёртки теперь чужие метаданные. Это косметика поверх правила.

Практическая цена вопроса в том, что на потерянных метаданных ломаются вещи, которые их читают: инструменты документации, логи с именем функции, отладка. Везде, где что-то смотрит на __name__, появится wrapper.

Последние шаги лесенки, декоратор для корутины и один декоратор для обычной и асинхронной функции, — это уже разговор про asyncio: обёртка вокруг корутины сама должна быть корутиной, иначе результат перестанет быть ожидаемым объектом. Механика декоратора при этом прежняя, меняется только то, что возвращает обёртка.

Проверьте себя

Сначала проговорите ответ, потом сверьтесь с текстом.

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

Что проверяют: держите вы в голове механизм («вызываемое, результат которого связывается с именем») или описание внешнего вида: «функция, которая оборачивает другую функцию».

Частая ошибка: сказать «декоратор добавляет функции поведение». Это описание применения, и из него не выводится ни один следующий шаг.

Раздел «Декоратор — это присваивание»
Что напечатается при импорте модуля, если функцию ещё не вызывали?

Что проверяют: различаете вы момент определения и момент вызова. Вопрос устроен так, что ответ «ничего» возможен только при их смешении.

Частая ошибка: ответить «ничего, ведь функция не вызывалась». Тело функции действительно не выполнится, а вот выражение декоратора уже вычислено при определении.

Раздел «Когда декоратор выполняется»
Как написать декоратор, принимающий аргументы?

Что проверяют: выводите вы структуру из правила или воспроизводите шаблон по памяти. Правило даёт три уровня однозначно; шаблон запоминается и путается.

Частая ошибка: ограничиться двумя уровнями. Тогда аргумент декоратора попадёт туда, где ожидается декорируемая функция.

Раздел «Почему у параметризованного декоратора три уровня»
Что вернёт func.__doc__ после декорирования?

Что проверяют: доводите вы правило связывания имени до его следствий или знаете его формально.

Частая ошибка: ответить «докстроку исходной функции». По имени доступна обёртка, и вернётся её докстрока, обычно None.

Раздел «Что происходит с именем функции»

Проверьте эти темы вслух

В тренажёре вы отвечаете голосом, а разбор показывает, где ответ просел.