Geschichte der Frage
Das Protokoll __mro_entries__ wurde in Python 3.7 über PEP 560 ("Kernunterstützung für das Typmodul und generische Typen") eingeführt. Vor dieser Verbesserung konnten generische Aliase wie typing.List[int] nicht als Basisklassen in Klassendefinitionen verwendet werden, da type.__new__ strikt verlangte, dass alle Basen Instanzen von type sind. Diese Einschränkung zwang das Modul typing, sich auf fragile Metaklassen-Hacks zu verlassen, die schwer zu warten waren und Leistungsprobleme verursachten. Das Protokoll wurde entwickelt, um die syntaktische Darstellung einer Basis von ihrem semantischen Beitrag zum Vererbungsgrafen zu entkoppeln und so eine sauberere Unterstützung für Generics und Fabrikmuster zu ermöglichen.
Das Problem
Wenn CPython eine Klassendefinition verarbeitet, muss es die Methodenauflösungsreihenfolge (MRO) mit dem C3-Linerarisierungsalgorithmus berechnen, um eine konsistente und vorhersehbare Hierarchie für die Methodenauffindung zu gewährleisten. Wenn ein Basisobjekt keine Klasse ist (zum Beispiel ein parameterisiertes Generikum oder ein Konfigurationsobjekt), fehlen dem Interpreter die notwendigen Typinformationen, um die neue Klasse korrekt im Vererbungshierarchiebaum zu platzieren. Würde man solche Objekte einfach ignorieren, würde das isinstance-Prüfungen und super()-Ketten brechen, während eine völlige Ablehnung diese leistungsstarken Metaprogrammierungsmuster verhindern würde. Die zentrale Herausforderung bestand darin, diesen Nicht-Klassen-Objekten zu ermöglichen, anzugeben, welche konkreten Klassen sie während der Klassenerstellungsphase logisch repräsentieren.
Die Lösung
Python untersucht jetzt jedes Element im Basen-Tupel auf eine Methode __mro_entries__(self, bases) während der Klassenerstellung. Wenn diese Methode existiert, wird sie mit dem ursprünglichen Basen-Tupel aufgerufen und muss ein Tupel tatsächlicher Klassen zurückgeben, um das Objekt in der MRO-Berechnung zu ersetzen. Die zurückgegebenen Klassen werden dann so behandelt, als ob sie ausdrücklich als Basen aufgeführt wären. Diese Mechanik erlaubt einer Instanz, als transparenter Platzhalter zu fungieren, der zur Definitionzeit auf konkrete Klassen aufgelöst wird.
class ConfigurableMixin: def __init__(self, feature): self.feature = feature def __mro_entries__(self, bases): # Dynamisch Basisklassen basierend auf Konfiguration injizieren if self.feature == "logging": return (LoggingSupport,) return (BaseFeature,) class LoggingSupport: def log(self, msg): print(msg) class BaseFeature: pass # Die Instanz wird in der MRO durch LoggingSupport ersetzt class Service(ConfigurableMixin("logging")): pass print(LoggingSupport in Service.__mro__) # True
In einem großen asynchronen Web-Framework mussten Entwickler eine DatabaseMixin-Fabrik erstellen, die, wenn sie mit einer bestimmten Datenbank-URL (z.B. DatabaseMixin("postgresql://")) instanziiert wurde, sowohl ConnectionPool als auch AsyncSession automatisch als Basisklassen in die Benutzerdienstklasse injiziert. Das Problem war, dass DatabaseMixin(...) eine einfache Objektinstanz zurückgab, keine Klasse, die jedoch an der MRO teilnehmen musste, als ob der Entwickler ausdrücklich geschrieben hätte class UserService(ConnectionPool, AsyncSession).
Lösung 1: Benutzerdefinierte Metaklasse
Ein Ansatz bestand darin, eine Metaklasse zu erstellen, die das Basen-Tupel in __new__ durchscannt, DatabaseMixin-Instanzen identifiziert und sie vor dem Aufruf von super().__new__ durch die Zielklassen ersetzt. Dies erlaubte präzise Kontrolle, brachte jedoch das Problem des „Metaklassenkonflikts“ mit sich: Jeder Dienst, der diese Metaklasse verwendete, konnte nicht von anderen Klassen erben, die ihre eigenen Metaklassen definierten, wie es bei bestimmten ORM-Basisklassen der Fall ist. Außerdem wurde das Debuggen schwierig, da die Syntax der Klassendefinition komplexe Transformationen verbarg und Stack-Traces auf die Interna der Metaklasse verwiesen, anstatt auf den Benutzer-Code.
Lösung 2: Klassen-Dekoration nach der Erstellung
Eine weitere Option war die Verwendung eines Klassen-Dekorators, der nach der Erstellung der Klasse angewendet wurde. Der Dekorator würde manuell Methoden von ConnectionPool und AsyncSession auf die neue Klasse kopieren oder type.__setattr__ verwenden, um sie zu injizieren. Während dies die Metaklassen-Viralität vermied, brach es grundlegend das Python-Vererbungsmodell: isinstance(UserService(), ConnectionPool) würde False zurückgeben, und super()-Aufrufe innerhalb der kopierten Methoden würden falsch aufgelöst, da die MRO tatsächlich nicht die Elternklassen enthielt. Dies führte zu subtilen Fehlern, bei denen Framework-Dienstprogramme die Dienste nicht als datenbankfähige Dienste anerkannten.
Lösung 3: Protokoll __mro_entries__
Das Team entschied sich, __mro_entries__ auf dem Objekt zu implementieren, das von DatabaseMixin zurückgegeben wurde. Die Methode gab (ConnectionPool, AsyncSession) basierend auf der analysierten URL zurück. Diese Lösung integrierte sich nahtlos in die nativen Klassenerstellungsmechanismen von CPython. Die MRO wurde korrekt berechnet, isinstance-Prüfungen funktionierten natürlich, und es gab keine Metaklassenkonflikte. Die Fabrikinstanz fungierte als deklarativer Platzhalter, der sich während der Klassenerstellung in die richtige Vererbungsstruktur auflöste und die Semantik von super() und die Kompatibilität mit Mehrfachvererbung bewahrte.
Das Ergebnis war eine saubere, intuitive API, bei der Entwickler schreiben konnten class OrderService(DatabaseMixin(postgres_url)): und automatisch Verbindungs-Pooling und Sitzungsmanagementfähigkeiten mit korrekter Methodenauflösung, voller IDE-Unterstützung und null Laufzeitaufwand oder Vererbungsproblemen erhielten.
Wie behandelt C3-Linerarisierung potenzielle Duplikate, wenn __mro_entries__ eine Basis in Klassen erweitert, die bereits andernorts in der Vererbungsliste vorhanden sind?
Wenn __mro_entries__ eine Klasse zurückgibt, die auch an anderer Stelle in den Basen erscheint (zum Beispiel, wenn eine Fabrik auf (BaseA,) erweitert und eine andere explizite Basis Derived(BaseA) ist), behandelt der C3-Algorithmus von Python das erweiterte Tupel als die effektive Basenliste. Der Algorithmus fusioniert dann diese Listen, während er die lokale Vorrangordnung bewahrt und Monotonie sicherstellt. Da C3 dafür ausgelegt ist, gemeinsame Vorfahren zu behandeln, erscheint BaseA nur einmal in der endgültigen MRO, positioniert nach allen Klassen, die davon abhängen, aber vor object. Kandidaten glauben oft fälschlicherweise, dass dies einen Konflikt oder einen doppelten Eintrag verursacht, aber der Linerarisierungsprozess dedupliziert auf natürliche Weise, während er die Einschränkung „Kind vor Eltern“ aufrechterhält und eine konsistente Methodenauflösung gewährleistet.
Warum kann __mro_entries__ nicht auf die zu schaffende Klasse zugreifen, und welcher spezifische Fehler tritt auf, wenn es versucht, dies zu tun?
Während der Klassenerstellung ruft type.__new__ __mro_entries__ für die Basisobjekte auf, bevor das Klassenobjekt selbst instanziiert wird. Das Namensraum-Wörterbuch existiert, aber das Klassenobjekt hat noch keine Identität. Wenn die Implementierung versucht, Attribute der vorgesehenen Klasse zuzugreifen (zum Beispiel, indem sie den Klassennamen aus einem äußeren Gültigkeitsbereich referenziert oder versucht, bases zu inspizieren, als wären sie bereits an die neue Klasse gebunden), wird ein NameError oder AttributeError ausgelöst, da die Bindung noch nicht existiert. Kandidaten nehmen häufig an, dass sie den endgültigen Zustand oder __dict__ der Klasse inspizieren können, um dynamische Entscheidungen zu treffen, aber die Methode erhält nur das Tupel der ursprünglichen Basen als Argument und muss sich auf ihren eigenen internen Zustand verlassen, um den Rückgabewert zu bestimmen.
Führt die Registrierung eines Objekts mit __mro_entries__ als virtuelle Unterklasse eines ABC über abc.ABCMeta.register() dazu, dass das ABC in der MRO erscheint?
Nein. Die Registrierung virtueller Unterklassen ist ein Laufzeitmechanismus, der einen internen Cache im ABC für isinstance()- und issubclass()-Prüfungen bevölkert. Sie verändert nicht das Attribut __mro__ der Unterklasse. Wenn MyClass(MyObject()) definiert wird und MyObject() über __mro_entries__ (ConcreteBase,) zurückgibt, erscheint nur ConcreteBase in MyClass.__mro__. Wenn ConcreteBase als virtuelle Unterklasse von MyABC registriert ist, dann gibt isinstance(MyClass(), MyABC) True zurück, aber MyABC wird nicht in MyClass.__mro__ vorhanden sein. Kandidaten verwechselt häufig die virtuelle Unterklassifizierung mit echter Vererbung, was zu Verwirrung darüber führt, warum super()-Aufrufe oder MRO-Inspektionen die ABC-Beziehung nicht widerspiegeln oder warum Methoden, die im ABC definiert sind, nicht über Vererbung verfügbar sind.