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

@ как синтаксический сахар

Частота на собеседованиях: Средняя - проверяют, понимаешь ли ты, что @ не магияСпрашивают в: Т-Банк, Яндекс

В прошлом уроке мы применяли декоратор вручную: walk_outside = with_raincoat(walk_outside). Работает отлично, но выглядит громоздко, особенно если декораторов несколько. Разработчики Python подумали о том же самом и добавили значок @ как чистый синтаксический сахар. Сахар в том смысле, что он не добавляет никакой новой возможности языку, а просто делает запись короче и приятнее для глаза.

Смотри, это одно и то же

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

Вывод:

Дождевик надет Иду гулять

А вот полностью эквивалентная запись без @:

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

Вывод:

Дождевик надет Иду гулять 2

То есть строчка @with_raincoat прямо над def walk_outside(): означает буквально следующее: "определи функцию walk_outside, а затем сразу же прогони её через with_raincoat и результат сохрани обратно в переменную walk_outside". Никакой отдельной магии интерпретатора здесь нет, это всё тот же вызов функции, просто записанный компактнее и ближе к месту, где функция объявляется. Такую запись гораздо проще читать: сразу видно, что функция декорирована, не нужно искать переприсваивание где-то ниже по файлу.

Важная деталь: декоратор должен существовать до момента использования

Раз @with_raincoat это на самом деле вызов функции with_raincoat в момент объявления walk_outside, то with_raincoat должна быть уже определена к этому моменту. Написать @with_raincoat выше того места, где сама функция with_raincoat определена, не получится, будет NameError. Это тот же самый принцип, что и с любой другой функцией в Python: нельзя вызвать то, что ещё не существует в текущем пространстве имён.

Первый намёк на проблему, которую решим в следующем уроке

Смотри внимательно на этот кусок:

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

Вывод:

wrapper

Хм. Мы декорировали функцию walk_outside, а её имя вдруг стало wrapper. Это не баг Python, это прямое следствие того, что мы обсуждали: walk_outside теперь на самом деле указывает на функцию wrapper из тела декоратора, а исходная walk_outside спрятана внутри как func и снаружи никак не видна напрямую. Для отладки и интроспекции кода это не самая приятная ситуация: если разработчик вызовет help(walk_outside) или посмотрит стектрейс, увидит там wrapper, а не осмысленное имя исходной функции. Именно эту проблему решает functools.wraps, о нём - в следующем уроке.

@ применяется не только к твоим собственным декораторам

Ты наверняка уже видел @ в чужом коде до этого курса, просто не задумывался, что это тот же самый механизм. Например, встроенный декоратор @staticmethod:

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

Вывод:

Гав!

staticmethod - это тоже функция (точнее, класс-декоратор в стандартной библиотеке), которая берёт метод и превращает его в такой, который можно вызывать без создания экземпляра класса и без неявной передачи self. Работает по тому же принципу, что наш with_raincoat, просто логика внутри другая. К декораторам класса и декораторам методов мы ещё вернёмся отдельно, в уроках 11 и 12.

Проверь себя

  • @decorator над def func(): эквивалентно func = decorator(func) сразу после определения функции.
  • Это чистый синтаксический сахар, никакой отдельной магии интерпретатора для @ не существует.
  • Декоратор должен быть определён до того места в коде, где применяется @decorator.
  • После декорирования func.__name__ меняется на имя обёртки - это побочный эффект, который решает functools.wraps.