Как происходит поиск атрибутов в экземпляре класса и какие методы вызываются
Что проверяют
Знаете ли вы реальный алгоритм, а не упрощение «сначала экземпляр, потом класс». Главное здесь дескрипторы: без них не объяснить, почему property нельзя перекрыть атрибутом экземпляра, а метод можно. Следом просят сравнить __getattr__ и __getattribute__ или написать ленивый атрибут.
Как отвечать
Выражение obj.x превращается в вызов type(obj).__getattribute__(obj, "x"). По умолчанию object.__getattribute__ действует так:
- поиск в классе: имя ищется по
type(obj).__mro__. Если найден дескриптор данных (объект с__get__и__set__или__delete__, напримерpropertyили слот из__slots__), вызывается его__get__, и на этом всё; - словарь экземпляра: иначе проверяется
obj.__dict__; - не-дескриптор данных: если в классе нашёлся объект только с
__get__(обычная функция,classmethod,staticmethod), вызывается__get__. Так функция становится связанным методом; - обычный атрибут класса: возвращается как есть;
- запасной вариант: если ничего не нашлось и поднят
AttributeError, вызывается__getattr__, когда он определён.
Итого __getattribute__ срабатывает на каждое обращение, а __getattr__ только на промах. Запись идёт через __setattr__: дескриптор данных перехватывает её своим __set__, иначе значение попадает в obj.__dict__.
Хорошая деталь для финала: functools.cached_property это не-дескриптор данных. Первый вызов считает значение и кладёт его в __dict__ экземпляра, дальше словарь экземпляра выигрывает у дескриптора, и функция больше не вызывается.
Где спотыкаются
Внутри своего __getattribute__ обращаются к self.something и получают бесконечную рекурсию. Доступ к атрибутам там идёт через super().__getattribute__(name) или object.__getattribute__(self, name).
Думают, что магические методы ищутся тем же путём. Неявные вызовы вроде len(obj) или obj + 1 берут __len__ и __add__ прямо из типа, минуя словарь экземпляра и __getattribute__.
Вешают на __getattr__ всё подряд и забывают, что он маскирует опечатки: обращение к несуществующему атрибуту вместо AttributeError молча вернёт значение по умолчанию.