Historia pytania
W modelu danych Pythona, dostęp do atrybutów podlega rygorystycznemu protokołowi, w którym __getattribute__ jest zdefiniowane w bazowej klasie object i służy jako główny interceptor dla każdej próby dostępu do atrybutu. Ta metoda jest wywoływana bezwarunkowo dla wszystkich dostępowych prób atrybutów, istniejących lub nie, co czyni ją pierwszą linią obrony w łańcuchu rozwiązywania. W przeciwieństwie do tego, __getattr__ jest opcjonalnym hakiem, który interpreter wywołuje tylko w przypadku, gdy normalne poszukiwanie w słowniku instancji i hierarchii klas nie zdoła zlokalizować żądanej nazwy.
Problem
Gdy klasa podrzędna nadpisuje __getattribute__, aby dostosować zachowanie, takie jak logowanie lub kontrola dostępu, jakiekolwiek bezpośrednie odwołanie do atrybutu w ciele metody — na przykład self.attr lub self.__dict__ — wywołuje tę samą nadpisaną metodę rekurencyjnie. Tworzy to nieskończoną pętlę, ponieważ mechanizm poszukiwania został przejęty bez podstawowego przypadku kończącego rekurencję, ostatecznie wyczerpując stos wywołań i generując RecursionError.
Rozwiązanie
Aby bezpiecznie zaimplementować __getattribute__, musisz delegować do podstawowej implementacji za pomocą super().__getattribute__(name) lub object.__getattribute__(self, name). To omija nadpisaną logikę i wykonuje rzeczywiste pobranie atrybutu ze słownika instancji lub hierarchii klas bez ponownego wchodzenia do niestandardowej metody. Wzorzec ten zapewnia, że możesz otoczyć, walidować lub przekształcać wynik, zachowując integralność modelu obiektowego i zapobiegając nieskończonym pętlom.
Przykład kodu
class SafeProxy: def __init__(self, wrapped): # Musi używać super() tutaj, aby uniknąć rekurencji podczas inicjalizacji super().__setattr__('_wrapped', wrapped) def __getattribute__(self, name): # Zaloguj dostęp przed pobraniem print(f"Dostęp: {name}") # Deleguj do obiektu, aby uniknąć nieskończonej rekurencji return super().__getattribute__(name)
Scenariusz
Zespół programistyczny musi zaimplementować ślad audytowy dla dziedzicznego modelu ORM, gdzie każdy dostęp do pola musi być rejestrowany z powodów zgodności, bez modyfikowania oryginalnych klas modelu. Potrzebują rozwiązania, które pośredniczy w odczytach w sposób przezroczysty, aby nie zakłócać istniejącej logiki biznesowej w setkach modułów.
Opis problemu
System wymaga przechwytywania zarówno istniejących, jak i brakujących atrybutów, aby rejestrować znaczniki czasowe i działania użytkowników. Proste dziedziczenie i dodawanie logowania do poszczególnych metod jest niewykonalne z powodu dużej liczby dynamicznych pól. Rozwiązanie musi być przezroczyste dla istniejącego kodu i nie może zmieniać publicznego interfejsu modeli.
Rozwiązanie 1: Monkey-patching metod modelu
To podejście polega na dynamicznej wymianie metod w klasie w czasie wykonywania, aby wstrzykiwać wywołania logowania, celując w konkretne zachowania bez zmiany definicji źródłowych. Umożliwia warunkowe zastosowanie w zależności od konfiguracji i unika komplikacji związanych z dziedziczeniem. Jednak nie udaje się przechwycić bezpośredniego dostępu do deskryptorów danych lub prostych wartości, wymaga konserwacji dla każdej nowej metody i łamie się, gdy zmieniają się szczegóły implementacji wewnętrznej.
Rozwiązanie 2: Użycie __getattr__ do logowania
Implementacja __getattr__, aby rejestrować dostęp do brakujących atrybutów, zapewnia jedynie prosty mechanizm zapasowy. Jest bezpieczna przed problemami z rekurencją i łatwa do wdrożenia z minimalnym kodem szablonowym. Niestety, wywołuje się tylko dla atrybutów, które nie znajdują się w instancji lub klasie, przez co pomija większość dostępu do istniejących pól, co unieważnia wymogi audytowe dotyczące kompleksowego logowania.
Rozwiązanie 3: Klasa proxy z __getattribute__
Stworzenie klasy opakowującej, która implementuje __getattribute__, przechwytuje wszystkie odczyty atrybutów przed delegowaniem do opakowanego instancji ORM, rejestrując każdy dostęp w sposób jednolity. Utrzymuje to przezroczystość dzięki kompozycji i umożliwia przetwarzanie przed i po bez dotykania kodu dziedziczonego. Kompromis polega na wymaganiu ostrożnego zarządzania rekurencją i niewielkim narzuceniu wydajności z powodu dodatkowego wywołania metody przy każdym dostępie do atrybutu.
Wybrane rozwiązanie
Zespół wybrał podejście proxy z __getattribute__, ponieważ przepisy dotyczące zgodności wymagały rejestrowania każdego odczytu atrybutu, w tym prostych pól danych, których metody nigdy nie dotykają. Wzorzec proxy zapewnił pełne możliwości przechwytywania, zachowując kapsułkowanie, umożliwiając, że dziedziczony ORM pozostaje nieskalany i nieświadomy warstwy audytującej. Ten wybór poświęcił minimalną wydajność na rzecz kompleksowego pokrycia i integralności audytu.
Wynik
Implementacja skutecznie zarejestrowała ponad 50 000 dostępu do atrybutów na godzinę w produkcji, bez jednego błędu rekurencji ani modyfikacji do bazy kodu dziedziczonego. Wzorzec delegacji za pomocą super() zapewnił stabilne działanie, a proxy mogło być wyłączone w środowiskach testowych przez proste usunięcie instancji opakowującej, demonstrując elastyczność podejścia.
Dlaczego dostęp do self.__dict__ wewnątrz __getattribute__ powoduje nieskończoną rekurencję?
Kiedy piszesz self.__dict__ wewnątrz nadpisanej metody __getattribute__, Python musi wyszukać atrybut o nazwie __dict__ w instancji. To wyszukiwanie ponownie wywołuje twoją niestandardową metodę __getattribute__, która próbuje ponownie uzyskać self.__dict__, tworząc nieskończony cykl. Aby przerwać tę pętlę, musisz użyć object.__getattribute__(self, '__dict__'), co omija twoje nadpisanie i bezpośrednio pobiera słownik z podstawowej implementacji obiektu.
Jak __getattribute__ wpływa na protokoły deskryptorów inaczej niż __getattr__?
__getattribute__ znajduje się na samym początku łańcucha rozwiązywania atrybutów, co oznacza, że przechwytuje wyszukiwania zanim protokół deskryptorów sprawdzi metody __get__. Jeśli twoja implementacja zwraca wartość bez delegowania do super(), deskryptory takie jak property lub niestandardowe deskryptory danych są całkowicie pomijane. W przeciwieństwie do tego, __getattr__ wykonuje się tylko po tym, jak zarówno protokół deskryptorów, jak i wyszukiwanie w słowniku instancji zawiodą, więc nigdy nie przechwytuje deskryptorów, które istnieją w hierarchii klas.
Jakie są konsekwencje ręcznego wywołania AttributeError wewnątrz __getattribute__?
W przeciwieństwie do standardowego dostępu do atrybutu, gdzie AttributeError może wywołać __getattr__ jako awaryjny mechanizm, Python traktuje __getattribute__ jako autorytatywne źródło. Jeśli twoja niestandardowa implementacja zgłasza AttributeError, interpreter natychmiast propaguje wyjątek, nie próbując wywołać __getattr__. Oznacza to, że nie możesz polegać na __getattr__, aby obsługiwać brakujące atrybuty, jeśli twoje główne łącze nie działa; zamiast tego musisz obsłużyć brakujące klucze w __getattribute__ lub upewnić się, że delegujesz do implementacji rodzica, która poprawnie zgłasza wyjątek.