PythonprogramowanieStarszy programista Python

Przez jaki mechanizm dynamicznej substytucji obiekt nieklasowy **Python**a określa swoje klasy uczestniczące podczas pojawienia się na liście dziedziczenia klas, wpływając tym samym na obliczenia kolejności rozwiązywania metod?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź na pytanie

Historia pytania

Wraz z przyjęciem PEP 560 w Pythonie 3.7, system typów wymagał sposobu używania typów generycznych, takich jak List[int] czy Generic[T], jako klas bazowych. Przed tym ulepszeniem próba dziedziczenia z parametryzowanego typu ogólnego kończyła się błędem TypeError, ponieważ te obiekty nie były rzeczywistymi klasami, zmuszając programistów do stosowania skomplikowanych obejść metaklas, co komplikowało projekt bibliotek.

Problem

Gdy interpreter przetwarza definicję klasy, musi obliczyć kolejność rozwiązywania metod (MRO) za pomocą algorytmu C3. Ten algorytm wymaga, aby wszystkie bazy były klasami. Problem pojawia się, gdy obiekt bazowy nie jest klasą, ale aliasem generycznym; interpreter potrzebuje protokołu, aby określić, które rzeczywiste klasy powinny zastąpić ten alias podczas konstrukcji MRO, nie łamiąc przy tym semantyki dziedziczenia.

Rozwiązanie

Python wprowadził protokół __mro_entries__. Gdy tworzenie klasy napotka bazę z tą metodą, wywołuje base.__mro_entries__(original_bases) i oczekuje krotki klas jako zwrot. Te klasy zastępują oryginalną bazę w obliczeniach MRO. Na przykład, typing.Generic implementuje to, zwracając (Generic,), co pozwala mu funkcjonować jako baza, podczas gdy logika parametryzowana pozostaje oddzielona.

from typing import Generic, TypeVar T = TypeVar('T') # Generic[T] nie jest klasą, ale __mro_entries__ pozwala jej działać jak jedna class Container(Generic[T]): pass # Container.__mro__ zawiera Generic, a nie Generic[T] print(Container.__mro__) # (<class 'Container'>, <class 'typing.Generic'>, <class 'object'>)

Sytuacja z życia

Zespół tworzący framework musiał umożliwić użytkownikom definiowanie modeli danych przy użyciu parametryzowanych baz generycznych, takich jak Model[UserType]. Ich początkowe podejście polegało na użyciu niestandardowej metaklasy do przechwytywania tworzenia klasy i wydobywania parametrów typów, ale zmuszało to użytkowników do ręcznego rozwiązywania konfliktów metaklas przy łączeniu frameworka z modelami Django lub SQLAlchemy.

Rozważali użycie dekoratora klasowego do przepisania klasy po definicji, ale to podejście łamało statyczne sprawdzanie typów i autouzupełnianie IDE, ponieważ transformacja następowała po przeanalizowaniu źródłowego kodu przez sprawdzacz typów. Inną alternatywą było użycie __init_subclass__, ale to nie mogło obsłużyć przypadku, gdy baza sama w sobie nie była klasą.

Zespół wdrożył __mro_entries__ na swoich obiektach fabryki generycznej. Kiedy użytkownicy pisali class UserModel(Model[UserType]), instancja Model[UserType] zwracała (Model,) z jej metody __mro_entries__. To pozwoliło klasie prawidłowo dziedziczyć po Model, podczas gdy fabryka przechowywała konkretny parametr typu dla walidacji w czasie wykonywania. Rozwiązanie wyeliminowało konflikty metaklas, zachowało pełne wsparcie IDE i utrzymało czystą hierarchię dziedziczenia, która zaspokajała wymagania algorytmu C3.

Co często przeoczają kandydaci

Czy __mro_entries__ wpływa na sprawdzanie typów w czasie wykonywania lub zachowanie isinstance?

Kandydaci często mylą konstrukcję MRO ze sprawdzaniem instancji. __mro_entries__ działa wyłącznie podczas tworzenia klasy, aby zbudować krotkę __mro__. Nie ma wpływu na sprawdzanie isinstance() lub issubclass() w trakcie działania. Te operacje polegają na atrybutach __class__ i __bases__ istniejących klas, a nie na dynamicznej substytucji, która miała miejsce w fazie definicji klasy.

Dlaczego __mro_entries__ zwraca krotkę zamiast pojedynczej klasy?

Typ zwracanej krotki uwzględnia złożone scenariusze dziedziczenia wielokrotnego. Chociaż zwykle zwraca krotkę jednolitym elemencie, taką jak (Generic,), protokół pozwala, aby parametr generyczny implikował dziedziczenie z wielu mixinów jednocześnie. Python rozpakowuje tę krotkę bezpośrednio do listy baz dla obliczeń MRO, więc zwrócenie (A, B) skutecznie powoduje, że klasa dziedziczy zarówno z A, jak i B, zamiast z pierwotnej bazy, która nie była klasą.

Jaką walidację Python przeprowadza na klasach zwróconych przez __mro_entries__?

Interpreter rygorystycznie weryfikuje, że zwrócone klasy tworzą ważną graf dziedziczenia. Jeśli krotka zawiera klasy, które mogłyby stworzyć niespójną MRO — na przykład wprowadzając konflikt dziedziczenia diamentowego, który narusza zasady liniaryzacji C3 — Python zgłasza błąd TypeError podczas tworzenia klasy. Ta walidacja zapewnia, że dynamiczna substytucja nie może obejść podstawowych zasad spójności dziedziczenia w języku.