При обращении к обычному методу через экземпляр Python автоматически передаёт объект экземпляра первым аргументом. Какой механизм связывает функцию класса с этим экземпляром?
Это делает дескрипторный протокол. Функция, размещённая в классе, является дескриптором: при доступе через экземпляр её метод __get__ создаёт связанный метод, в котором экземпляр сохраняется как первый аргумент self.
При доступе через сам класс функция не связывается с конкретным экземпляром. Поэтому вызов через класс требует явно передать объект, который станет self.
Python строит доступ к атрибутам вокруг единого протокола объектов, а не вокруг отдельного специального правила только для методов. Это позволяет одинаковым механизмом реализовывать методы, свойства, методы классов, статические методы и другие управляемые атрибуты.
Такой подход решает задачу автоматического связывания поведения с объектом без копирования функции в каждый экземпляр. Логика метода хранится один раз в классе, а при обращении к ней Python формирует объект, содержащий функцию и конкретный экземпляр.
Функция в теле класса сама по себе не является связанным методом. Если не учитывать это различие, можно неправильно объяснить, откуда берётся self, почему вызов через класс требует дополнительного аргумента и почему экземпляр иногда может заменить метод атрибутом с тем же именем.
Ошибочное понимание особенно опасно при работе с дескрипторами, моками и динамической заменой атрибутов. Оно также приводит к неверным ожиданиям относительно того, создаётся ли новая функция для каждого экземпляра.
При поиске атрибута в классе Python видит объект-функцию. У функции есть метод __get__, поэтому она участвует в дескрипторном протоколе. Доступ через экземпляр вызывает примерно такую логику: дескриптор получает экземпляр и класс, после чего возвращает связанный метод.
Связанный метод хранит ссылку на исходную функцию в __func__ и на экземпляр в __self__. При вызове он передаёт __self__ первым аргументом, поэтому параметр self не нужно указывать вручную.
При доступе через класс экземпляр отсутствует: дескриптор получает None и возвращает исходную функцию. Это позволяет явно вызвать её с объектом, который будет использован как self.
Обычная функция является не data-дескриптором, потому что определяет __get__, но не определяет __set__ или __delete__. Поэтому атрибут экземпляра с таким же именем может перекрыть метод класса. Data-дескрипторы, например свойства, имеют приоритет над словарём экземпляра.
В этом примере User.greet — функция, а u.greet — связанный метод. Функция не дублируется в объекте u; создаётся представление, которое хранит функцию и экземпляр.
В тесте разработчик заменил метод конкретного экземпляра обычной функцией и ожидал автоматической передачи self. Но функция, записанная непосредственно в словарь экземпляра, не проходит поиск через класс и не получает автоматическое связывание, поэтому вызов может завершиться ошибкой о недостающем аргументе.
Можно заменить метод в самом классе: это изменит поведение всех экземпляров, что удобно для глобального патча, но опасно из-за побочных эффектов. Можно присвоить экземпляру уже связанный метод — это ограничит изменение одним объектом, однако потребует явного использования средств привязки и усложнит отладку.
Обычно для тестов выбирают явную подмену через механизм моков или присваивают экземпляру функцию с заранее понятной сигнатурой. Такое решение делает область действия изменения очевидной и не маскирует различие между функцией класса и связанным методом.
Нет. Исходная функция обычно хранится в классе один раз. При обращении Python получает связанный объект метода; этот объект может создаваться заново, но исполняемая функция остаётся общей для экземпляров.
Обычная функция — не data-дескриптор. При поиске атрибута data-дескрипторы имеют приоритет, затем проверяется словарь экземпляра, а затем находятся обычные атрибуты класса и не-data-дескрипторы. Поэтому значение в словаре экземпляра с тем же именем может скрыть обычный метод.
Обычный метод связывается с экземпляром и получает его первым аргументом. classmethod связывается с классом и получает класс первым аргументом, независимо от того, обращаются к нему через класс или экземпляр. staticmethod отключает автоматическую передачу экземпляра или класса и возвращает вызываемую функцию без такого связывания.