PythonprogramowanieStarszy programista Python

W jaki sposób instancja **Python** może określić swoje logiczne klasy bazowe do udziału w obliczeniach porządku rozwiązywania metod?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź na pytanie

Historia pytania

Protokół __mro_entries__ został wprowadzony w Python 3.7 za pośrednictwem PEP 560 ("Podstawowe wsparcie dla modułu typing i typów ogólnych"). Przed tym ulepszeniem, ogólne aliasy, takie jak typing.List[int], nie mogły być używane jako klasy bazowe w definicjach klas, ponieważ type.__new__ ściśle wymagał, aby wszystkie bazy były instancjami type. To ograniczenie zmusiło moduł typing do polegania na kruchych hackach metaklas, które były trudne do utrzymania i powodowały problemy z wydajnością. Protokół został zaprojektowany w celu odłączenia składniowego wyrażenia bazy od jej semantycznego wkładu do grafu dziedziczenia, umożliwiając czystsze wsparcie dla typów ogólnych i wzorców fabrycznych.

Problem

Kiedy CPython przetwarza definicję klasy, musi obliczyć porządek rozwiązywania metod (MRO) za pomocą algorytmu C3, aby zapewnić spójną i przewidywalną hierarchię wyszukiwania metod. Jeśli obiekt bazowy nie jest klasą (na przykład, zweryfikowany ogólny lub obiekt konfiguracyjny), interpreter nie ma potrzebnych informacji o typie, aby prawidłowo umieścić nową klasę w drzewie dziedziczenia. Proste ignorowanie takich obiektów złamałoby kontrole isinstance i łańcuchy super(), podczas gdy ich całkowite odrzucenie uniemożliwiłoby potężne wzorce metaprogramowania. Kluczowym wyzwaniem było umożliwienie tym obiektom, które nie są klasami, zadeklarowania, które konkretne klasy logicznie reprezentują podczas fazy budowania klasy.

Rozwiązanie

Python teraz bada każdy element w krotce bazowych, szukając metody __mro_entries__(self, bases) podczas tworzenia klasy. Jeśli ta metoda istnieje, jest wywoływana z oryginalną krotką baz, a musi zwrócić krotkę rzeczywistych klas, które zastąpią obiekt w obliczeniach MRO. Zwrócone klasy są traktowane tak, jakby zostały wyraźnie wymienione jako klasy bazowe. Ten mechanizm pozwala instancji działać jako przezroczysty placeholder, który rozwiązuje się w konkretne klasy w momencie definicji.

class ConfigurableMixin: def __init__(self, feature): self.feature = feature def __mro_entries__(self, bases): # Dynamicznie wstrzykuj klasy bazowe na podstawie konfiguracji if self.feature == "logging": return (LoggingSupport,) return (BaseFeature,) class LoggingSupport: def log(self, msg): print(msg) class BaseFeature: pass # Instancja jest zastępowana przez LoggingSupport w MRO class Service(ConfigurableMixin("logging")): pass print(LoggingSupport in Service.__mro__) # True

Sytuacja z życia

W dużym asynchronicznym frameworku webowym, deweloperzy musieli stworzyć fabrykę DatabaseMixin, która, gdy została zainicjowana z określonym adresem URL bazy danych (np. DatabaseMixin("postgresql://")), automatycznie wstrzykiwałaby zarówno ConnectionPool, jak i AsyncSession jako klasy bazowe do klasy usługi użytkownika. Trudność polegała na tym, że DatabaseMixin(...) zwracała prostą instancję obiektu, a nie klasę, jednak musiała uczestniczyć w MRO tak, jakby deweloper wyraźnie napisał class UserService(ConnectionPool, AsyncSession).

Rozwiązanie 1: Niestandardowa Metaklasa Jednym z podejść było stworzenie metaklasy, która skanowała krotkę bases w __new__, identyfikowała instancje DatabaseMixin i zastępowała je klasami docelowymi przed wywołaniem super().__new__. To umożliwiło precyzyjną kontrolę, ale wprowadziło problem "konfliktu metaklas": każda usługa wykorzystująca tę metaklasę nie mogła dziedziczyć z innych klas, które definiowały własne metaklasy, takich jak niektóre klasy bazowe ORM. Dodatkowo, debugowanie stało się trudne, ponieważ składnia definicji klas ukrywała złożone transformacje, a ślady stosu wskazywały na wewnętrzności metaklas, a nie kod użytkownika.

Rozwiązanie 2: Dekoracja Klasy Po Utworzeniu Inną opcją było użycie dekoratora klasy stosowanego po utworzeniu klasy. Dekorator ręcznie kopiowałby metody z ConnectionPool i AsyncSession do nowej klasy lub używałby type.__setattr__, aby je wstrzyknąć. Chociaż unikało to wirusowości metaklas, zasadniczo łamało model dziedziczenia Python: isinstance(UserService(), ConnectionPool) zwracałoby False, a wywołania super() w skopiowanych metodach byłyby rozwiązywane nieprawidłowo, ponieważ MRO w rzeczywistości nie zawierało klas nadrzędnych. Prowadziło to do subtelnych błędów, gdzie narzędzia frameworkowe nie rozpoznawały usług jako zdolnych do działania z bazą danych.

Rozwiązanie 3: Protokół __mro_entries__ Zespół zdecydował się zaimplementować __mro_entries__ dla obiektu zwróconego przez DatabaseMixin. Metoda zwracała (ConnectionPool, AsyncSession) na podstawie analizowanego URL. To rozwiązanie zintegrowało się płynnie z natywną maszynerią tworzenia klas CPython. MRO zostało obliczone poprawnie, kontrole isinstance działały naturalnie, a żadne konflikty metaklas nie wystąpiły. Instancja fabryki działała jako deklaracyjny placeholder, który niszczył się w odpowiednią strukturę dziedziczenia podczas budowy klasy, zachowując semantykę super() i kompatybilność z wieloma dziedziczeniami.

Rezultatem był czysty, intuicyjny interfejs API, w którym deweloperzy mogli pisać class OrderService(DatabaseMixin(postgres_url)): i automatycznie otrzymywać funkcje puli połączeń i zarządzania sesjami z poprawnym rozwiązywaniem metod, pełnym wsparciem IDE i zerowym narzutem w czasie wykonywania oraz konfliktami dziedziczenia.

Co często umyka kandydatom

Jak C3 linearization radzi sobie z potencjalnymi duplikatami, gdy __mro_entries__ rozwija bazę w klasy, które już są obecne w liście dziedziczenia?

Kiedy __mro_entries__ zwraca klasę, która również występuje gdzie indziej w bazach (na przykład, jeśli jedna fabryka rozwija się do (BaseA,), a inna jawna baza to Derived(BaseA)), algorytm C3 w Python traktuje rozszerzoną krotkę jako efektywną listę baz. Algorytm następnie łączy te listy, zachowując lokalną kolejność pierwszeństwa i zapewniając monotonność. Ponieważ C3 jest zaprojektowany do obsługi wspólnych przodków, BaseA pojawia się tylko raz w końcowym MRO, umieszczony po wszystkich klasach, które na nim polegają, ale przed object. Kandydaci często błędnie wierzą, że tworzy to konflikt lub duplikat, ale proces linearizacji naturalnie usuwa duplikaty, zachowując ograniczenie "dzieci przed rodzicami", co zapewnia spójną rozwiązywalność metod.

Dlaczego __mro_entries__ nie może uzyskać dostępu do tworzonych klas, a jaki błąd występuje, jeśli próbuje to zrobić?

Podczas tworzenia klasy, type.__new__ wywołuje __mro_entries__ na obiektach bazowych przed utworzeniem samego obiektu klasy. Słownik przestrzeni nazw istnieje, ale obiekt klasy nie ma jeszcze tożsamości. Jeśli implementacja próbuje uzyskać dostęp do atrybutów przyszłej klasy (na przykład, odwołując się do nazwy klasy z zewnętrznego zakresu lub próbując zbadać bases tak, jakby były już związane z nową klasą), spowoduje to podniesienie NameError lub AttributeError, ponieważ powiązanie jeszcze nie istnieje. Kandydaci często zakładają, że mogą zbadać ostateczny stan klasy lub __dict__, aby podejmować dynamiczne decyzje, ale metoda otrzymuje tylko krotkę oryginalnych baz jako argument i musi opierać się na własnym stanie wewnętrznym, aby określić wartość zwracaną.

Czy rejestrowanie obiektu z __mro_entries__ jako wirtualnej klasy podrzędnej ABC przez abc.ABCMeta.register() powoduje, że ABC pojawia się w MRO?

Nie. Rejestracja wirtualnych klas podrzędnych to mechanizm działania w czasie wykonywania, który wypełnia wewnętrzną pamięć podręczną w ABC dla kontroli isinstance() i issubclass(). Nie zmienia to atrybutu __mro__ klasy podrzędnej. Kiedy definiowana jest MyClass(MyObject()), a MyObject() zwraca (ConcreteBase,) za pośrednictwem __mro_entries__, tylko ConcreteBase pojawia się w MyClass.__mro__. Jeśli ConcreteBase jest zarejestrowane jako wirtualna klasa podrzędna MyABC, wtedy isinstance(MyClass(), MyABC) zwraca True, ale MyABC nie będzie obecne w MyClass.__mro__. Kandydaci często mylą wirtualne dziedziczenie z prawdziwym dziedziczeniem, co prowadzi do dezorientacji co do tego, dlaczego wywołania super() lub inspekcja MRO nie odzwierciedlają relacji ABC ani dlaczego metody zdefiniowane w ABC nie są dostępne w drodze dziedziczenia.