Python wprowadził moduł abc w wersji 2.6, aby usystematyzować Abstrakcyjne Klasy Bazowe, umożliwiając strukturalne typowanie poza tradycyjnym typowaniem wskazującym. Kluczowym mechanizmem jest metoda klasy __subclasshook__, którą maszyna abc wywołuje, gdy issubclass() nie znajduje kandydatów w MRO ABC. Metoda ta otrzymuje kandydacką klasę i zwraca True, False lub NotImplemented, co pozwala na wirtualną rejestrację bez dziedziczenia.
Problem polega na tym, że __subclasshook__ często musi zweryfikować, czy kandydat implementuje określone metody lub atrybuty. Bez warunków ochronnych, jeśli hak wewnętrznie wywołuje issubclass() lub podobne sprawdzenia, które prowadzą z powrotem do tego samego ABC, uruchamia to nieskończoną rekurencję. Obowiązkowa ochrona wymaga sprawdzenia if cls is MyABC na początku metody, co zapewnia, że hak waliduje tylko konkretne ABC, które je definiuje, a nie podklasy tego ABC.
from abc import ABC, abstractmethod class Drawable(ABC): @abstractmethod def draw(self): pass @classmethod def __subclasshook__(cls, C): # Ochrona przed rekurencją: zajmuj się tylko Drawable bezpośrednio if cls is not Drawable: return NotImplemented # Sprawdzenie strukturalne: czy zachowuje się jak Drawable? if hasattr(C, "draw") and callable(getattr(C, "draw")): return True return NotImplemented class Circle: def draw(self): print("Rysowanie okręgu") # Weryfikacja wirtualnych podklas bez dziedziczenia assert issubclass(Circle, Drawable)
Nasz zespół budował zintegrowaną platformę analityczną, która musiała obsługiwać wiele baz danych. Zdefiniowaliśmy ABC DatabaseDriver z metodami takimi jak connect(), execute(), i close(). Chcieliśmy jednak wspierać istniejące biblioteki bazodanowe innych firm (jak psycopg2 czy pymongo) bez ich forkowania lub opakowywania w klasy adaptera.
Pierwszym rozwiązaniem, które rozważaliśmy, było ścisłe dziedziczenie wzorca adaptera. Stworzylibyśmy klasy opakowujące jak Psycopg2Adapter(DatabaseDriver), które kapsułkowałyby połączenia od stron trzecich. To zapewniłoby doskonałe bezpieczeństwo typów i wsparcie analizy statycznej. Jednak stworzyło to znaczące obciążenie konserwacyjne dla każdego przekazywania metod i wprowadziło dodatkową warstwę pośrednictwa podczas działania.
Drugim podejściem było czyste typowanie wskazujące z inspekcją atrybutów w czasie działania. Po prostu założyliśmy, że każdy obiekt posiadający metody connect i execute jest ważnym sterownikiem. Chociaż to oferowało maksymalną elastyczność i brak kodu opakowującego, cicho zawiodło, gdy sygnatury metod były niekompatybilne. Dodatkowo, statyczne analizatory typów, takie jak mypy, nie mogły walidować tych kontraktów, co prowadziło do opóźnionej detekcji błędów w środowiskach produkcyjnych.
Wybraliśmy trzecie rozwiązanie: implementację __subclasshook__ w naszym ABC DatabaseDriver, aby rejestrować wirtualne podklasy. To wyeliminowało potrzebę klas opakowujących, zachowując jednocześnie ścisłą walidację isinstance i pozwalając klasom od stron trzecich przechodzić kontrolę typów bez modyfikacji. Warunek ochronny zapewniał, że sprawdzanie podklasy DatabaseDriver w związku z samym sobą nie spowoduje nieskończonych pętli.
Wynik był 40% redukcją kodu boilderplate adapterów i płynne wsparcie autouzupełniania w IDE. System mógł teraz akceptować surowe połączenia bazodanowe z bibliotek, które nic nie wiedziały o naszym ABC, zachowując jednocześnie ścisłą walidację w czasie działania i gwarancje typowania strukturalnego.
Dlaczego __subclasshook__ musi sprawdzać if cls is MyABC przed wykonaniem sprawdzeń strukturalnych, a co się stanie, jeśli ten warunek zostanie pominięty?
Bez tej ochrony, wywołanie issubclass(SubClass, MyABC) wyzwala MyABC.__subclasshook__(SubClass). Jeśli hak wewnętrznie sprawdza issubclass(SubClass, MyABC), aby zweryfikować dziedziczenie, tworzy to natychmiastową nieskończoną rekurencję. Maszyna abc języka Python wywołuje hak tylko dla dokładnej klasy, która go definiuje, ale sprawdzenia strukturalne często prowadzą z powrotem do tego samego zapytania. Stos szybko przepełnia się bez ochrony, aby zapewnić, że hak waliduje tylko konkretne ABC, które definiuje.
Jak wirtualne dziedziczenie przez register() różni się od __subclasshook__ pod względem wydajności i zmienności?
register() dodaje klasę do wewnętrznej pamięci podręcznej (_abc_cache) natychmiast, co czyni kolejne sprawdzenia O(1) przez wyszukiwanie w zbiorze. W przeciwieństwie do tego, __subclasshook__ wykonuje dowolny kod Pythona przy każdym wywołaniu issubclass, chyba że jest w pamięci podręcznej, tworząc obciążenie obliczeniowe. Dodatkowo, register() jest trwałe przez czas życia procesu i działa na wbudowanych typach jak list. Z kolei __subclasshook__ umożliwia dynamiczną, warunkową logikę opartą na możliwościach w czasie działania, ale działa tylko dla zdefiniowanych przez użytkownika ABC.
Jaka jest interakcja między __subclasshook__ a metodą __instancecheck__ w niestandardowych metaklasach?
Gdy wywoływane jest isinstance(obj, MyABC), Python najpierw konsultuje metaklasę instancji __instancecheck__. Jeśli jest niedostępna lub dająca wnioski, przechodzi do issubclass(type(obj), MyABC), co wyzwala __subclasshook__. Kandydaci często przeoczają, że __subclasshook__ bierze udział tylko w sprawdzaniach klas, a nie bezpośrednich sprawdzaniach instancji. Również przeoczają, że zwrócenie NotImplemented pozwala na kontynuowanie sprawdzenia przez MRO, umożliwiając współprace z wieloma zarzutami w złożonych hierarchiach.