Программирование PythonФункции и декораторыPython-разработчик серверных приложений

Зачем обёртке декоратора нужен атрибут wrapped ?

Зачем обёртке декоратора нужен атрибут __wrapped__?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Атрибут __wrapped__ связывает обёртку с исходной функцией. Благодаря этому инструменты интроспекции, например inspect.signature() и inspect.unwrap(), могут обнаружить исходный вызываемый объект вместо того, чтобы видеть только техническую обёртку.

Обычно атрибут устанавливает functools.wraps. Он улучшает совместимость декораторов с отладчиками, документацией, тестовыми инструментами и фреймворками, но не превращает обёртку в исходную функцию и не отменяет её поведение.

Исторический контекст

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

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

Постановка проблемы

Без __wrapped__ интроспекция обычно видит параметры самой обёртки, например *args и **kwargs, а не реальные параметры исходной функции. Это может нарушить генерацию документации, автоматическую передачу зависимостей, проверку сигнатур и работу инструментов отладки.

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

Подробное решение

functools.wraps — это удобная форма применения functools.update_wrapper к внутренней функции. Помимо переноса ряда атрибутов, update_wrapper устанавливает у обёртки __wrapped__, ссылающийся на исходную функцию.

from functools import wraps import inspect def logged(func): @wraps(func) def wrapper(*args, **kwargs): print("вызов") return func(*args, **kwargs) return wrapper @logged def add(left: int, right: int) -> int: """Складывает два числа.""" return left + right print(inspect.signature(add)) # (left: int, right: int) -> int print(inspect.unwrap(add) is add.__wrapped__) # True

При вызове inspect.signature(add) функция инспекции по умолчанию проходит по цепочке __wrapped__ и получает сигнатуру исходной функции. inspect.unwrap позволяет пройти эту цепочку явно; если декораторов несколько, каждый корректно созданный слой может указывать на предыдущий.

__wrapped__ не меняет порядок выполнения и не удаляет логику декоратора. Вызов всё равно сначала попадает в обёртку. Также атрибут не гарантирует полное сохранение всех возможных пользовательских атрибутов: wraps копирует стандартный набор метаданных и обновляет словарь атрибутов по правилам update_wrapper.

Использовать wraps следует почти всегда при написании функционального декоратора. Исключения возможны, когда намеренно требуется скрыть исходную функцию от интроспекции или создать собственную модель метаданных, но это должно быть осознанным решением.

Ситуация из практики

В веб-приложении декоратор авторизации оборачивает обработчики маршрутов. Без wraps все обработчики выглядят для инструмента регистрации как функция с произвольными *args и **kwargs; генерация документации и автоматическая проверка параметров становятся неточными.

Рассматривались два варианта. Можно вручную присваивать __name__, __doc__ и другие атрибуты, но это хрупко: легко забыть __wrapped__, а набор метаданных сложнее поддерживать. Можно было отказаться от обёртки и реализовать авторизацию в отдельном слое диспетчера, но это потребовало бы изменить архитектуру и усложнило локальное применение правила.

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

Что кандидаты часто упускают

  1. Достаточно ли скопировать __name__ и __doc__, чтобы inspect.signature увидел исходную сигнатуру?

Нет. Эти атрибуты влияют на отображаемое имя и документацию, но не описывают связь обёртки с исходной функцией. Для стандартного перехода к исходному объекту нужен __wrapped__ или явно заданный механизм получения сигнатуры, например __signature__.

  1. Означает ли наличие __wrapped__, что декоратор можно обойти обычным вызовом?

Нет. __wrapped__ — это доступная ссылка на объект для интроспекции, а не автоматический переключатель поведения. Обычный вызов обёрнутой функции по-прежнему выполняет код декоратора. Однако код, который сознательно обращается к __wrapped__, может вызвать исходную функцию напрямую, поэтому публикация этой ссылки должна учитываться при проектировании API и модели безопасности.

  1. Что произойдёт с цепочкой при нескольких декораторах, если один из них не использует functools.wraps?

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