Объясните механизм: в какой момент Python вызывает __getattr__ при обращении к атрибуту объекта?
__getattr__ вызывается как резервный обработчик после того, как стандартный поиск атрибута завершился исключением AttributeError. Он не участвует в обычном успешном поиске и не вызывается для каждого обращения к атрибуту.
Если сам __getattr__ не может предоставить значение, он должен выбросить AttributeError; тогда getattr и обычный доступ через точку считают атрибут отсутствующим.
В объектной модели Python доступ к атрибутам можно расширять через специальные методы. Такой подход решает задачу динамического предоставления атрибутов без предварительного хранения каждого значения в экземпляре.
Это полезно для ленивых вычислений, прокси-объектов, адаптеров к внешним источникам данных и поддержки устаревших имён атрибутов. При этом обычный механизм доступа сохраняется, а дополнительная логика включается только при неудачном поиске.
Если считать, что __getattr__ вызывается при любом обращении к атрибуту, можно случайно реализовать дорогостоящую или рекурсивную логику. Если вместо AttributeError возвращать None для неизвестных имён, ошибки в написании атрибутов будут незаметно превращаться в некорректные значения.
Особенно опасно безусловно перехватывать все имена: это может скрыть реальные ошибки в свойствах, дескрипторах или переопределённом __getattribute__. Код, использующий hasattr, также может скрыть исключение AttributeError, возникшее внутри вычисления свойства, приняв его за отсутствие атрибута.
Обычный доступ через точку сначала запускает механизм __getattribute__. Если он успешно находит атрибут, __getattr__ не вызывается. Только при завершении поиска с AttributeError Python предоставляет возможность обработать имя через __getattr__.
Минимальный пример:
Для settings.timeout стандартный поиск не находит атрибут, после чего вызывается __getattr__, возвращающий значение. Для settings.retries обработчик сам выбрасывает AttributeError, поэтому обращение завершается ошибкой.
__getattr__ отличается от __getattribute__ областью применения. __getattribute__ участвует в каждом обращении к атрибуту и потому требует особой осторожности: обращение к атрибуту внутри него может вызвать рекурсию. __getattr__ является более узким и обычно безопасным расширением для отсутствующих имён.
Метод должен проверять имя явно и не перехватывать его без необходимости. Если значение вычисляется лениво, следует отдельно решить вопрос кэширования: без кэша вычисление повторяется при каждом обращении, а с кэшем нужно учитывать устаревание данных и потокобезопасность.
В клиенте к внешнему сервису нужно поддержать несколько виртуальных атрибутов, значения которых загружаются по запросу. Рассматривались два варианта: заранее заполнить все значения при создании объекта или реализовать __getattr__.
Предварительная загрузка проще для отладки и делает обращения предсказуемыми, но увеличивает время создания объекта, расход памяти и число сетевых запросов. __getattr__ позволяет загружать только реально используемые значения, однако требует явного списка допустимых имён, обработки ошибок сети и решения о кэшировании.
Был выбран __getattr__ с проверкой имени и кэшем успешно загруженных значений. Неизвестные имена приводят к AttributeError, поэтому опечатки не маскируются, а редко используемые данные не загружаются заранее.
Что произойдёт, если __getattr__ вернёт None для любого неизвестного имени?
Доступ к несуществующему атрибуту формально станет успешным и вернёт None. Это нарушает ожидаемый контракт объектной модели: getattr с отсутствующим значением, hasattr и обычные проверки наличия начнут вести себя misleadingly, а опечатки могут обнаружиться только значительно позже.
Корректнее обрабатывать только поддерживаемые имена, а для остальных выбрасывать AttributeError с информативным сообщением. Возврат None допустим лишь тогда, когда None действительно является значением конкретного существующего виртуального атрибута.
Будет ли вызван __getattr__, если внутри свойства возникнет AttributeError?
Потенциально да: если общий механизм доступа завершится AttributeError, Python может передать управление __getattr__. Поэтому AttributeError, случайно выброшенный из тела свойства из-за ошибки в его логике, способен быть воспринят как отсутствие самого свойства.
Это одна из причин не использовать AttributeError как универсальный способ сигнализировать о любой внутренней проблеме. Внутри свойства следует сохранять исходную семантику ошибок или преобразовывать их в более подходящее исключение, если ошибка не означает отсутствие атрибута.
Сработает ли __getattr__, если обратиться к нему напрямую через __getattribute__?
Автоматический резервный вызов относится к обычному механизму доступа объекта, например к выражению с точкой или функции getattr. Прямой вызов obj.__getattribute__("name") сам по себе не является повторным запуском полного механизма с автоматическим вызовом __getattr__ после ошибки.
Это различие важно при переопределении __getattribute__: для обращения к базовой реализации обычно используют super().__getattribute__(name), а логику резервного поиска размещают отдельно в __getattr__. Смешивание этих уровней может привести к рекурсии или к обходу ожидаемого fallback-механизма.