Как Python разрешает обращение к атрибуту объекта, если одноимённые атрибуты есть у экземпляра и его класса?

Как Python разрешает обращение к атрибуту объекта, если одноимённые атрибуты есть у экземпляра и его класса?

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

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

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

Если атрибут не найден, Python может вызвать __getattr__. Однако переопределённый __getattribute__ получает возможность вмешаться ещё раньше и изменить стандартный порядок.

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

Модель объектов Python объединяет атрибуты экземпляров, пространства имён классов и наследование. Такой подход позволяет обращаться к данным и поведению единообразно через точечную нотацию.

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

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

Одинаковое имя может находиться в словаре экземпляра и в классе. Если не знать приоритетов, можно ошибочно ожидать, что присваивание атрибута экземпляру всегда скроет значение класса.

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

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

Упрощённый порядок стандартного поиска таков:

  1. Метод __getattribute__ экземпляра получает запрос.
  2. В классе и его базовых классах по MRO ищется дескриптор данных — объект с __get__ и __set__ или __delete__. Если он найден, вызывается его __get__, а словарь экземпляра не имеет приоритета.
  3. Если дескриптора данных нет, проверяется словарь экземпляра.
  4. Затем используется атрибут класса или базового класса. Если это дескриптор без __set__ и __delete__, то есть дескриптор без данных, вызывается его __get__; так обычно связываются методы с экземпляром.
  5. При отсутствии результата вызывается __getattr__, если он определён.

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

class User: role = "class role" @property def name(self): return "computed" user = User() user.role = "instance role" print(user.role) # instance role print(user.name) # computed

role берётся из словаря экземпляра, потому что это обычный атрибут класса. name контролируется свойством, являющимся дескриптором данных, поэтому наличие одноимённой записи в экземпляре не изменяет результат доступа.

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

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

В ORM-модели поле status должно возвращать нормализованное значение и запрещать произвольную запись. Разработчик добавляет status в словарь экземпляра и ожидает, что это временно заменит значение свойства.

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

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

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

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

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

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

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

  1. Как наследование влияет на поиск атрибута класса?

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