Geschiedenis van de vraag
Het __mro_entries__ protocol werd geïntroduceerd in Python 3.7 via PEP 560 ("Kernondersteuning voor het typing-module en generieke types"). Voordat deze verbetering werd doorgevoerd, konden generieke aliassen zoals typing.List[int] niet worden gebruikt als basis classes in class-definities omdat type.__new__ strikt vereiste dat alle basen instanties van type moesten zijn. Deze beperking dwong het typing-module om afhankelijk te zijn van fragiele metaclass hacks die moeilijk te onderhouden waren en prestatieproblemen veroorzaakten. Het protocol is ontworpen om de syntactische expressie van een basis los te koppelen van de semantische bijdrage aan de erfelijkheidsgrafiek, waardoor schonere ondersteuning voor generics en factory-patronen mogelijk is.
Het probleem
Wanneer CPython een class-definitie verwerkt, moet het de Method Resolution Order (MRO) berekenen met behulp van het C3-lineariseringsalgoritme om een consistente en voorspelbare methode-opzoekhiërarchie te waarborgen. Als een basisobject geen klasse is (bijvoorbeeld een geparametriseerde generic of een configuratieobject), mist de interpreter de nodige type-informatie om de nieuwe klasse correct binnen de erfelijkheidsboom te plaatsen. Het simpelweg negeren van dergelijke objecten zou isinstance-controles en super()-ketens breken, terwijl het ze outright afwijzen krachtige metaprogrammeringspatronen zou verhinderen. De kernuitdaging was om deze niet-klasobjecten toe te staan te declareren welke concrete klassen zij logisch vertegenwoordigen tijdens de klassenconstructiefase.
De oplossing
Python inspecteert nu elk item in de bases-tuple voor een __mro_entries__(self, bases)-methode tijdens het maken van een klasse. Als deze methode bestaat, wordt deze aangeroepen met de originele bases-tuple, en deze moet een tuple van werkelijke klassen retourneren ter vervanging van het object in de MRO-berekening. De geretourneerde klassen worden vervolgens behandeld alsof ze expliciet als bases waren vermeld. Dit mechanisme stelt een instantie in staat om als een transparante plaatsvervanger op te treden die op het moment van definitie naar concrete klassen resolveert.
class ConfigurableMixin: def __init__(self, feature): self.feature = feature def __mro_entries__(self, bases): # Dynamisch basis classes injecteren op basis van configuratie if self.feature == "logging": return (LoggingSupport,) return (BaseFeature,) class LoggingSupport: def log(self, msg): print(msg) class BaseFeature: pass # De instantie wordt vervangen door LoggingSupport in de MRO class Service(ConfigurableMixin("logging")): pass print(LoggingSupport in Service.__mro__) # Waar
In een groot asynchroon webframework moesten ontwikkelaars een DatabaseMixin-fabriek maken die, wanneer deze werd geïnstantieerd met een specifieke database-URL (bijv. DatabaseMixin("postgresql://")), automatisch zowel ConnectionPool als AsyncSession als basis classes in de gebruikersserviceklasse zou injecteren. Het probleem was dat DatabaseMixin(...) een gewoon objectexemplaar retourneerde, geen klasse, terwijl het moest deelnemen aan de MRO alsof de ontwikkelaar expliciet had geschreven class UserService(ConnectionPool, AsyncSession).
Oplossing 1: Aangepaste Metaclass
Een benadering hield in dat er een metaclass werd gemaakt die de bases-tuple in __new__ doorzocht, DatabaseMixin-instanties identificeerde en deze verving door de doelklassen voordat super().__new__ werd aangeroepen. Dit gaf nauwkeurige controle maar introduceerde het "metaclass conflictoprobleem": elke service die deze metaclass gebruikte, kon niet van andere klassen erven die hun eigen metaclasses definieerden, zoals bepaalde ORM-basis klassen. Bovendien werd debugging moeilijk omdat de syntaxis van de class-definitie complexe transformaties verstopte, en stacktraces naar de interne metaclass verwezen in plaats van naar gebruikerscode.
Oplossing 2: Post-Creatie Class Decoratie
Een andere optie was om een class-decorator te gebruiken die werd toegepast na het creëren van de klasse. De decorator zou handmatig methoden van ConnectionPool en AsyncSession op de nieuwe klasse kopiëren of type.__setattr__ gebruiken om ze in te voegen. Hoewel dit de metaclass-virulentie vermeed, brak het de erfelijkheidsmodel van Python fundamenteel: isinstance(UserService(), ConnectionPool) zou False retourneren, en super()-aanroepen binnen de gekopieerde methoden zouden onjuist oplossen omdat de MRO de ouderklassen niet werkelijk bevatte. Dit leidde tot subtiele bugs waarbij framework-hulpmiddelen diensten niet als database-geschikt herkenden.
Oplossing 3: __mro_entries__ Protocol
Het team koos ervoor om __mro_entries__ te implementeren op het object dat door DatabaseMixin werd geretourneerd. De methode retourneerde (ConnectionPool, AsyncSession) op basis van de geparseerde URL. Deze oplossing integreerde naadloos met de native class-creatie-machinerie van CPython. De MRO werd correct berekend, isinstance-controles werkten natuurlijk, en er waren geen metaclass-conflicten. De fabrieksinstantie fungeerde als een declaratieve plaatsvervanger die tijdens de classconstructie opgelost werd in de juiste erfelijkheidsstructuur, waarbij super()-semantiek en compatibiliteit met meerdere erfelijkheid behouden bleven.
Het resultaat was een schone, intuïtieve API waar ontwikkelaars konden schrijven class OrderService(DatabaseMixin(postgres_url)): en automatisch verbinding pooling en sessiebeheer konden ontvangen met correcte methode-resolutie, volledige IDE-ondersteuning, en geen runtime overhead of erfelijkheidsconflicten.
Hoe behandelt C3-linearizatie potentiële duplicaten wanneer __mro_entries__ een basis uitbreidt naar klassen die elders in de erfelijkheidslijst al aanwezig zijn?
Wanneer __mro_entries__ een klasse retourneert die ook ergens anders in de basen voorkomt (bijvoorbeeld, als één fabriek uitbreidt naar (BaseA,) en een andere expliciete basis Derived(BaseA) is), behandelt Python's C3-algoritme de uitgebreide tuple als de effectieve basislijst. Het algoritme voegt deze lijsten samen terwijl het de lokale prioriteitsvolgorde behoudt en monotoniciteit waarborgt. Omdat C3 is ontworpen om gemeenschappelijke voorouders te behandelen, verschijnt BaseA slechts één keer in de uiteindelijke MRO, gepositioneerd na alle klassen die ervan afhangen maar vóór object. Kandidaten geloven vaak ten onrechte dat dit een conflict of dubbele invoer creëert, maar het linearisatieproces dedupliceert van nature terwijl het de "kinderen voor ouders"-restrictie handhaaft, waardoor consistente methode-resolutie gegarandeerd is.
Waarom kan __mro_entries__ de klasse die wordt aangemaakt niet bereiken, en welke specifieke fout treedt op als het dat probeert?
Tijdens het maken van een klasse roept type.__new__ __mro_entries__ aan op de basisobjecten voordat het klasseobject zelf is geïnstantieerd. De namespace-dictionary bestaat, maar het klasseobject heeft nog geen identiteit. Als de implementatie probeert eigenschappen van de toekomstige klasse te benaderen (bijvoorbeeld, door de klassenaam uit een externe scope te refereren of te proberen bases te inspecteren alsof ze al aan de nieuwe klasse zijn gebonden), zal dit een NameError of AttributeError veroorzaken omdat de binding nog niet bestaat. Kandidaten veronderstellen vaak dat ze de uiteindelijke staat van de klasse of __dict__ kunnen inspecteren om dynamische beslissingen te nemen, maar de methode ontvangt alleen de tuple van originele basen als argument en moet vertrouwen op zijn eigen interne toestand om de retourwaarde te bepalen.
Geeft het registreren van een object met __mro_entries__ als een virtuele subclass van een ABC via abc.ABCMeta.register() de ABC in de MRO?
Nee. Virtuele subclassregistratie is een runtime-mechanisme dat een interne cache binnen de ABC voor isinstance() en issubclass()-controles bevolkt. Het verandert de __mro__-attribuut van de subclass niet. Wanneer MyClass(MyObject()) wordt gedefinieerd en MyObject() retourneert (ConcreteBase,) via __mro_entries__, verschijnt alleen ConcreteBase in MyClass.__mro__. Als ConcreteBase is geregistreerd als een virtuele subclass van MyABC, dan retourneert isinstance(MyClass(), MyABC) True, maar MyABC zal niet aanwezig zijn in MyClass.__mro__. Kandidaten verwarren vaak virtuele subclassing met echte erfelijkheid, wat leidt tot verwarring over waarom super()-aanroepen of MRO-inspectie de ABC-relatie niet reflecteert, of waarom methoden die op de ABC zijn gedefinieerd niet via erfelijkheid beschikbaar zijn.