В практической ситуации декоратор заменяет метод вызываемым объектом: почему вызов через экземпляр может по...

В практической ситуации декоратор заменяет метод вызываемым объектом: почему вызов через экземпляр может потерять автоматическую передачу self?

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

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

Вызываемый объект не получает self автоматически, если его класс не реализует дескрипторный метод __get__. Обычная функция является дескриптором и при доступе через экземпляр превращается в связанный метод, а экземпляр класса с __call__ сам по себе таким механизмом не обладает.

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

В Python методы реализованы поверх общего протокола дескрипторов. Этот протокол позволяет объектам, размещённым в классе, изменять поведение доступа к атрибуту; на нём основано, в частности, автоматическое связывание функции с экземпляром.

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

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

Если декоратор возвращает экземпляр класса с методом __call__, при обращении к нему через экземпляр класса Python обычно возвращает тот же объект-декоратор. Python не вставляет экземпляр автоматически в аргументы __call__.

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

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

У функции есть метод __get__. Когда функция хранится в атрибуте класса и читается через экземпляр, этот метод создаёт связанный метод, который передаёт экземпляр первым аргументом.

Экземпляр вызываемого класса реализует только __call__. Вызов __call__ делает объект вызываемым, но не превращает его в дескриптор. Чтобы сохранить поведение метода, класс декоратора должен реализовать __get__ и вернуть объект, связанный с конкретным экземпляром.

import types class Decorator: def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): return self.func(*args, **kwargs) def __get__(self, instance, owner): if instance is None: return self return types.MethodType(self.__call__, instance) class Service: @Decorator def calculate(self, value): return value * 2 print(Service().calculate(3))

При обращении Service().calculate срабатывает Decorator.__get__, который связывает __call__ с экземпляром Service. Поэтому в __call__ первым аргументом оказывается объект Service, а затем переданное значение 3.

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

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

В сервисе декоратор аудита хранил счётчик вызовов и последнюю длительность операции. Сначала его реализовали как callable-объект: это было удобнее, чем замыкание, поскольку состояние имело именованные поля и легко тестировалось.

Рассматривались три варианта. Замыкание с обычной функцией сохраняло бы корректное связывание методов, но состояние было бы менее явно организовано. Callable-объект без __get__ сохранял удобную структуру состояния, однако ломал декорированные методы. Callable-объект с реализацией дескриптора сохранял оба свойства, но требовал аккуратно обработать доступ через класс, когда instance равен None.

Выбрали третий вариант: __get__ возвращал связанный вызов, а доступ через класс возвращал сам декоратор. В результате состояние аудита осталось централизованным, методы продолжили получать self, а вызов метода через класс сохранил стандартную семантику Python.

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

  1. Почему декоратор-функция обычно не сталкивается с этой проблемой?

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

  2. Что должен возвращать __get__ при обращении к декоратору через сам класс?

    Обычно при instance is None возвращают сам объект-декоратор. Это поддерживает обращения вроде Service.calculate, интроспекцию и вызовы через класс, где экземпляр передаётся явно. Если всегда возвращать объект, связанный с экземпляром, можно получить ошибку или некорректное поведение при доступе без экземпляра.

  3. Достаточно ли добавить __get__, если один экземпляр декоратора используется для методов разных экземпляров?

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