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

В подклассе одноимённым обычным атрибутом заменили унаследованное свойство: почему чтение экземпляра перестало вызывать getter?

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

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

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

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

Дескрипторы позволяют классу управлять доступом к атрибутам через специальные методы. На этом механизме построены property, связанные методы, classmethod и многие инструменты ORM.

Такой подход отделяет интерфейс атрибута от способа хранения значения: клиент обращается к obj.value, а класс может вычислять значение, проверять его или выполнять побочные действия. Для наследования это означает, что дочерний класс может заменить поведение атрибута обычным значением или новым дескриптором.

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

Наличие свойства в базовом классе не гарантирует, что оно будет участвовать в доступе к атрибуту экземпляра. Одноимённое объявление в подклассе может полностью изменить поведение: getter, setter и проверка значения базового свойства перестанут вызываться.

Это особенно опасно при расширении моделей, настроек или API: разработчик может считать, что переопределяет только значение по умолчанию, но фактически отключить валидацию или вычисление, реализованные свойством.

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

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

Далее Python проверяет найденный объект. Если это дескриптор данных, например property, вызывается его __get__. Если найден обычный атрибут, у экземпляра дополнительно проверяется __dict__; при наличии там такого имени возвращается значение экземпляра, иначе — найденное значение класса.

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

class Parent: @property def value(self): print("getter Parent") return 10 class Child(Parent): value = "class value" obj = Child() print(obj.value) # class value obj.__dict__["value"] = "instance value" print(obj.value) # instance value

В первом обращении getter Parent.value не вызывается: в Child уже найден обычный атрибут value. Во втором обращении значение экземпляра перекрывает обычный атрибут класса.

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

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

В базовой модели свойство status нормализует строку и запрещает недопустимые значения. В наследнике разработчик объявил status = "new", рассчитывая задать значение по умолчанию. В результате проверки базового свойства исчезли, а экземпляры стали принимать произвольные значения через __dict__.

Возможны три решения:

  • оставить обычный атрибут — просто, но поведение свойства полностью теряется;
  • повторно объявить property в подклассе — сохраняет контроль, но требует явно поддерживать логику getter и setter;
  • изменить базовый дизайн, например хранить состояние во внутреннем атрибуте и задать значение по умолчанию через конструктор — обычно прозрачнее, но может потребовать изменения API.

Практично оставить status свойством, а начальное значение задавать через внутреннее поле, например _status. Так сохраняются единые правила валидации для базового класса и всех наследников.

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

  1. Всегда ли атрибут подкласса скрывает свойство базового класса?

Да, если атрибут подкласса найден первым по MRO. Неважно, является ли унаследованное свойство дескриптором данных: до него поиск не дойдёт. Исключение по сути только одно — найденный атрибут подкласса сам может делегировать доступ базовому свойству или быть новым дескриптором с нужной логикой.

  1. Что произойдёт, если в подклассе объявить одноимённый дескриптор?

Будет использован дескриптор подкласса, потому что именно он является первым найденным определением имени. Его __get__, __set__ или __delete__ могут полностью заменить поведение базового свойства. Наследуемый дескриптор автоматически не объединяется с новым.

  1. Можно ли восстановить базовое свойство через super()?

Да, но только явно внутри переопределённого свойства или метода. Обращение к super().value ищет value после текущего класса в MRO и поэтому может получить дескриптор базового класса. Простое наличие атрибута в подклассе не создаёт такого делегирования автоматически.