При обращении к обычному методу через экземпляр Python автоматически передаёт объект экземпляра первым аргу...

При обращении к обычному методу через экземпляр Python автоматически передаёт объект экземпляра первым аргументом. Какой механизм связывает функцию класса с этим экземпляром?

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

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

Это делает дескрипторный протокол. Функция, размещённая в классе, является дескриптором: при доступе через экземпляр её метод __get__ создаёт связанный метод, в котором экземпляр сохраняется как первый аргумент self.

При доступе через сам класс функция не связывается с конкретным экземпляром. Поэтому вызов через класс требует явно передать объект, который станет self.

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

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

Такой подход решает задачу автоматического связывания поведения с объектом без копирования функции в каждый экземпляр. Логика метода хранится один раз в классе, а при обращении к ней Python формирует объект, содержащий функцию и конкретный экземпляр.

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

Функция в теле класса сама по себе не является связанным методом. Если не учитывать это различие, можно неправильно объяснить, откуда берётся self, почему вызов через класс требует дополнительного аргумента и почему экземпляр иногда может заменить метод атрибутом с тем же именем.

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

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

При поиске атрибута в классе Python видит объект-функцию. У функции есть метод __get__, поэтому она участвует в дескрипторном протоколе. Доступ через экземпляр вызывает примерно такую логику: дескриптор получает экземпляр и класс, после чего возвращает связанный метод.

Связанный метод хранит ссылку на исходную функцию в __func__ и на экземпляр в __self__. При вызове он передаёт __self__ первым аргументом, поэтому параметр self не нужно указывать вручную.

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

Обычная функция является не data-дескриптором, потому что определяет __get__, но не определяет __set__ или __delete__. Поэтому атрибут экземпляра с таким же именем может перекрыть метод класса. Data-дескрипторы, например свойства, имеют приоритет над словарём экземпляра.

class User: def greet(self): return self.name u = User() u.name = "Анна" print(User.greet) print(u.greet()) print(u.greet.__self__ is u) print(u.greet.__func__ is User.greet)

В этом примере User.greet — функция, а u.greet — связанный метод. Функция не дублируется в объекте u; создаётся представление, которое хранит функцию и экземпляр.

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

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

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

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

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

  1. Создаётся ли новая функция при каждом обращении к методу экземпляра?

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

  1. Почему атрибут экземпляра может перекрыть обычный метод класса?

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

  1. Чем отличаются обычный метод, метод класса и статический метод с точки зрения связывания?

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