05 / 72
ТеорияСреднийMiddle

functools.wraps и потерянные метаданные функции

Частота на собеседованиях: Средняя - классический code review вопросСпрашивают в: Т-Банк, Авито

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

Показываем проблему целиком

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

Вывод:

wrapper None

Смотри, что мы потеряли. Имя функции стало wrapper вместо add. А docstring - строка документации """Складывает два числа.""" - вообще пропала, стала None. И это не мелочь для галочки. Это реально влияет на работу инструментов: help(add) покажет бесполезную информацию про wrapper, автодокументация (Sphinx и подобные) сгенерирует неправильные описания, отладчик в стектрейсе покажет имя wrapper вместо осмысленного add, а инструменты вроде IDE, которые подсказывают сигнатуру функции при наборе кода, собьются с толку.

Решение: functools.wraps

В стандартной библиотеке для этого есть готовый инструмент, functools.wraps. Сам он тоже декоратор, просто применяется не к твоей бизнес-функции, а к обёртке внутри твоего декоратора:

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

Вывод:

add2 Складывает два числа.

functools.wraps(func) копирует на wrapper служебные атрибуты оригинальной функции: __name__, __doc__, __module__, __qualname__ и некоторые другие. Снаружи всё выглядит так, будто декорирования как будто и не было - имя и документация на месте, а поведение всё равно расширено.

Бонус: wrapped и сохранённая сигнатура

functools.wraps даёт ещё одну полезную вещь - атрибут __wrapped__, который хранит ссылку на исходную, недекорированную функцию:

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

Вывод:

<function add2 at 0x1052c3530> (a, b)

Модуль inspect умеет "смотреть сквозь" __wrapped__ и достаёт настоящую сигнатуру (a, b), а не (*args, **kwargs) от обёртки. Это то, что реально используют внешние инструменты: автогенераторы документации, статические анализаторы, дебаггеры.

Почему на собеседовании это спрашивают

Это классический вопрос на code review: "вот декоратор без functools.wraps, что с ним не так". Правильный ответ не просто "надо добавить functools.wraps", а объяснение, что именно ломается без него: метаданные функции (имя, докстринг, сигнатура), от которых зависят инструменты интроспекции, тестирования и документации. Хорошее эмпирическое правило: если пишешь декоратор, который не является строго одноразовым внутренним хаком, всегда добавляй @functools.wraps(func) над wrapper. Это стоит одну строчку и почти никогда не имеет недостатков.

Проверь себя

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

Дальше усложняем: что если декоратору самому нужны параметры, например @retry(times=3). Для этого понадобится ещё один уровень вложенности функций.