История вопроса
В модели данных Python доступ к атрибутам следует строгому протоколу, где __getattribute__ определён в базовом классе object и служит основным перехватчиком для каждого поиска атрибутов. Этот метод вызывается без условий для всех доступов к атрибутам, существующим или нет, что делает его первой линией защиты в цепочке разрешения. В отличие от этого, __getattr__ является необязательным хуком, который интерпретатор вызывает только тогда, когда обычный поиск по словарю экземпляра и иерархии классов не может найти запрашиваемое имя.
Проблема
Когда подкласс переопределяет __getattribute__, чтобы настроить поведение, такое как логирование или контроль доступа, любой прямой доступ к атрибуту внутри тела метода — такой как self.attr или self.__dict__ — рекурсивно вызывает тот же переопределённый метод. Это создаёт бесконечный цикл, потому что механизм поиска был перехвачен без базового случая для завершения рекурсии, в конечном итоге исчерпывая стек вызовов и вызывая RecursionError.
Решение
Чтобы безопасно реализовать __getattribute__, необходимо делегировать вызов базовой реализации с помощью super().__getattribute__(name) или object.__getattribute__(self, name). Это обходит переопределённую логику и осуществляет фактическое извлечение атрибута из словаря экземпляра или иерархии классов без повторного входа в пользовательский метод. Шаблон обеспечивает возможность обертывания, валидации или преобразования результата, сохраняя целостность модели объекта и предотвращая бесконечные циклы.
Пример кода
class SafeProxy: def __init__(self, wrapped): # Необходимо использовать super() здесь, чтобы избежать рекурсии при инициализации super().__setattr__('_wrapped', wrapped) def __getattribute__(self, name): # Логируем доступ перед извлечением print(f"Доступ к: {name}") # Делегируем объекту, чтобы избежать бесконечной рекурсии return super().__getattribute__(name)
Сценарий
Команде разработчиков необходимо реализовать аудиторскую трассировку для устаревшей модели ORM, где каждый доступ к полям должен быть зафиксирован по причинам соблюдения норм, не внося изменения в оригинальные классы модели. Им требуется решение, которое перехватывает считывания прозрачно, чтобы не нарушать существующую бизнес-логику в сотнях модулей.
Описание проблемы
Система требует перехвата как существующих, так и отсутствующих атрибутов, чтобы записывать временные метки и действия пользователей. Простое создание подкласса и добавление логирования в отдельные методы неосуществимо из-за большого количества динамических полей. Решение должно быть прозрачно для существующего кода и не может изменять публичный интерфейс моделей.
Решение 1: Монки-патчинг методов модели
Этот подход включает в себя динамическую замену методов в классе во время выполнения, чтобы внедрить вызовы логирования, целенаправленно нацеливаясь на специфическое поведение, не изменяя исходные определения. Он позволяет условное применение на основе конфигурации и избегает осложнений с наследованием. Однако он не перехватывает прямой доступ к дескрипторам данных или простым значениям, требует обслуживания для каждого нового метода и ломается при изменении внутренних деталей реализации.
Решение 2: Использование __getattr__ для логирования
Реализация __getattr__ для логирования доступа к отсутствующим атрибутам предоставляет лишь простое резервное решение. Оно защищено от проблем с рекурсией и легко реализуется с минимальным количеством повторяющегося кода. К сожалению, оно срабатывает только для атрибутов, не найденных в экземпляре или классе, пропуская большинство обращений к существующим полям, что противоречит требованиям аудита на всеобъемлющее логирование.
Решение 3: Класс-прокси с __getattribute__
Создание обёрточного класса, который реализует __getattribute__, перехватывает все считывания атрибутов перед делегированием на экземпляр обёрнутой ORM, захватывая каждый доступ одинаково. Это сохраняет прозрачность через композицию и позволяет предварительной и последующей обработке без вмешательства в устаревший код. Недостатком является необходимость в тщательном управлении рекурсией и небольшое замедление работы из-за дополнительного вызова метода при каждом доступе к атрибуту.
Выбранное решение
Команда выбрала подход с прокси с __getattribute__, потому что нормативные требования обязывали фиксировать каждое чтение атрибута, включая простые поля данных, к которым методы никогда не обращаются. Шаблон прокси обеспечивал полные возможности перехвата, сохраняя инкапсуляцию, позволяя устаревшей ORM оставаться неповреждённой и неосведомлённой о слое аудита. Этот выбор пожертвовал минимальной производительностью ради всеобъемлющего охвата и целостности аудита.
Результат
Реализация успешно зафиксировала более 50 000 доступов к атрибутам в час в производственной среде без единой ошибки рекурсии или изменения устаревшей кодовой базы. Шаблон делегирования с использованием super() обеспечил стабильную работу, и прокси можно было отключить в тестовых средах, просто удалив инстанцирование обёртки, что демонстрирует гибкость подхода.
Почему доступ к self.__dict__ внутри __getattribute__ вызывает бесконечную рекурсию?
Когда вы пишете self.__dict__ внутри переопределённого метода __getattribute__, Python должен выполнить поиск атрибута с именем __dict__ на экземпляре. Этот поиск снова вызывает ваш пользовательский метод __getattribute__, который снова пытается получить доступ к self.__dict__, создавая бесконечный цикл. Чтобы разорвать этот цикл, вы должны использовать object.__getattribute__(self, '__dict__'), что обходит ваше переопределение и извлекает словарь непосредственно из базовой реализации объекта.
Как __getattribute__ влияет на протоколы дескрипторов иначе, чем __getattr__?
__getattribute__ находится в самом начале цепочки разрешения атрибутов, что означает, что он перехватывает поиски до проверки протокола дескрипторов для методов __get__. Если ваша реализация возвращает значение без делегирования на super(), дескрипторы, такие как property или пользовательские дескрипторы данных, полностью обходятся. В отличие от этого, __getattr__ выполняется только после того, как как поиск в протоколе дескрипторов, так и поиск в словаре экземпляра завершились неудачно, поэтому он никогда не перехватывает дескрипторы, которые существуют в иерархии классов.
Каковы последствия ручного возбуждения AttributeError внутри __getattribute__?
В отличие от стандартного доступа к атрибуту, где AttributeError может вызвать __getattr__ в качестве резервного варианта, Python рассматривает __getattribute__ как авторитетный источник. Если ваша пользовательская реализация вызывает AttributeError, интерпретатор немедленно передаёт исключение, не пытаясь вызвать __getattr__. Это означает, что вы не можете полагаться на __getattr__ для обработки отсутствующих атрибутов, если ваш основной хук не сработал; вместо этого вам нужно обрабатывать отсутствующие ключи внутри __getattribute__ или убедиться, что вы делегируете к родительской реализации, которая правильно вызывает исключение.