W wczesnych wersjach Python rozwiązywanie atrybutów opierało się na prostej wyszukiwarce w głąb przez słownik instancji, a następnie przez hierarchię klas. To podejście okazało się niewystarczające do implementacji solidnego zachowania przypominającego właściwości, gdzie obliczone wartości musiały przechwytywać zarówno odczyty, jak i zapisy bez dwuznaczności. Wprowadzenie nowych klas w Python 2.2 ustaliło protokół deskriptorów, klasyfikując deskriptory w zależności od obecności __set__ lub __delete__, aby rozwiązać konflikty priorytetowe.
Bez ścisłej zasady priorytetu interpreter nie mógł zdecydować, czy lokalne przechowywanie instancji powinno dominować nad definicjami na poziomie klasy, czy odwrotnie. Jeśli słowniki instancji zawsze miałyby priorytet, właściwości nie mogłyby walidować przypisania, ponieważ wartości byłyby przechowywane bezpośrednio w __dict__. Z drugiej strony, jeśli atrybuty klas zawsze by dominowały, normalne zmienne instancji byłyby niedostępne, gdyby nazwy kolidowały z metodami lub innymi atrybutami klasy.
Algorytm wyszukiwania atrybutów w Python nakłada obowiązek, aby deskriptory danych — te definiujące __set__ lub __delete__ — miały priorytet nad słownikami instancji, podczas gdy deskriptory nienależące do danych (definiujące tylko __get__) ustępują słownikom instancji. Taki projekt pozwala @property wymuszać logikę walidacji poprzez przechwytywanie zapisów, podczas gdy zwykłe funkcje lub właściwości cache'owane pozostają możliwe do nadpisania na poziomie instancji bez skomplikowanego metaprogramowania.
Zespół deweloperski budował warstwę walidacji danych o wysokiej przepustowości dla platformy handlowej. Potrzebowali trwałych pól, które ściśle walidowały nadchodzące dane rynkowe w odniesieniu do wymogów regulacyjnych, zapewniając, że nie mogą być przypisane nieprawidłowe wartości. Dodatkowo potrzebowali obliczanych metryk, które mogłyby być buforowane na poziomie instancji, aby uniknąć kosztownego przeliczenia wskaźników zmienności podczas intensywnych burstów handlowych.
Jednym z rozważanych podejść było wdrożenie wszystkich atrybutów jako właściwości za pomocą dekoratora @property. To zapewniło wszechstronny kontrolę walidacji przez przechwytywanie każdej operacji zapisu za pomocą metody setter właściwości. Jednak ten projekt uniemożliwił systemowi ominięcie walidacji podczas ładowania zserializowanych danych z zaufanych wewnętrznych pamięci podręcznych, tworząc niepotrzebne obciążenie obliczeniowe podczas operacji odtwarzania zbiorczego.
Inną opcją było nadpisanie __setattr__ w klasie bazowej w celu centralizacji logiki walidacji w jednej metodzie. Chociaż ta kontrola centralna oferowała jeden punkt modyfikacji reguł walidacji, wprowadzała kruche logiki rozgałęzienia do rozróżnienia między permanentnymi polami wymagającymi walidacji a tymczasowymi pamięciami obliczeniowymi. Co więcej, to podejście zakłócało standardowe wzorce dostępu do atrybutów oczekiwane przez biblioteki serializacji stron trzecich, powodując błędy integracji.
Wybrane rozwiązanie wykorzystało bezpośrednio dychotomię protokołu deskriptorów, aby zaspokoić oba wymagania bez obciążenia centralizacją. Zespół wdrożył ValidatedField jako deskriptor danych z metodą __set__, która wymuszała ograniczenia typu i zakresu, zapewniając, że zawsze przechwytywał przypisania niezależnie od stanu instancji, ponieważ deskriptory danych mają priorytet nad słownikami instancji. Dla obliczonych metryk stworzyli CachedMetric jako deskriptor nienależący do danych, implementujący tylko __get__, pozwalając słownikowi instancji na zaciemnienie deskriptora po obliczeniu i lokalnym przechowaniu wartości, co omijało przeliczenie podczas kolejnych dostępn.
Ta architektura zapewniła ścisłą walidację dla zewnętrznych wejść, jednocześnie pozwalając na elastyczne, wydajne buforowanie wartości pochodnych. System z powodzeniem przetwarzał dane rynkowe o wysokiej objętości bez zatorów walidacyjnych podczas nawadniania pamięci podręcznych. Testy wykazały 40% redukcję obciążenia walidacji w scenariuszach odtwarzania historycznego w porównaniu do podejścia opartego tylko na właściwościach, przy zachowaniu pełnej zgodności regulacyjnej dla wchłaniania danych na żywo.
Czy usunięcie atrybutu omija deskriptor danych, jeśli deskriptor nie ma metody __delete__?
Gdy deskriptor danych implementuje __set__, ale pomija __delete__, próba usunięcia atrybutu za pomocą del obj.attr nie wraca do słownika instancji. Python nadal uznaje obiekt za deskriptor danych z powodu obecności __set__, a operacja usuwania wywoła AttributeError, wskazując, że atrybut nie może zostać usunięty. Aby umożliwić usunięcie, deskriptor musi jawnie zdefiniować __delete__, aby usunąć wartość z instancji, lub klasa musi implementować niestandardową logikę usuwania; mechanizm wyszukiwania nigdy nie sprawdza słownika instancji dla atrybutów deskriptorów danych podczas operacji usuwania.
Dlaczego super().attribute wydaje się ignorować deskriptory danych zdefiniowane w bieżącej klasie?
Proxy super() implementuje mechanizm współpracy w dziedziczeniu wielokrotnym, który zaczyna wyszukiwanie w Kolejności Rozwiązywania Metod (MRO) w klasie następującej po bieżącej klasie w hierarchii. Ponieważ deskriptor jest zdefiniowany w samej bieżącej klasie, super() pomija go podczas wyszukiwania. Jednak jeśli klasa rodzicielska definiuje deskriptor danych o tej samej nazwie, super() go znajdzie i zastosuje standardowe zasady priorytetu deskriptorów danych, wywołując __get__ z odpowiednią instancją i klasą właściciela. To działanie pochodzi z punktu początkowego MRO, a nie z jakiegokolwiek szczególnego wyjątku dla deskriptorów w obiektach proxy super.
Jak __slots__ wykorzystują protokół deskriptorów do wymuszania ograniczeń przechowywania?
Gdy klasa definiuje __slots__, interpreter Python automatycznie tworzy specjalizowane wewnętrzne deskriptory (zwykle obiekty member_descriptor na poziomie C) dla każdej nazwy slotu i umieszcza je w słowniku klasy. Te deskriptory implementują zarówno __get__, jak i __set__, co czyni je deskriptorami danych, które mają priorytet nad każdą próbą przechowywania wartości w konwencjonalnym słowniku instancji. Ponieważ instancje klas z slots zazwyczaj nie mają __dict__, chyba że "__dict__" jest wyraźnie zawarte na liście slotów, protokół deskriptorów zapewnia, że wszystkie odczyty i zapisy dla atrybutów z slots są kierowane przez te deskriptory na poziomie C, wymuszając bezpieczeństwo typów i wydajność pamięci poprzez zapobieganie losowemu przypisaniu atrybutów.