Storia della domanda
Con l'adozione di PEP 560 in Python 3.7, il sistema dei tipi ha richiesto un modo per utilizzare tipi generici come List[int] o Generic[T] come classi base. Prima di questo miglioramento, tentare di ereditare da un generico parametrizzato resultava in un TypeError poiché questi oggetti non erano classi effettive, costringendo gli sviluppatori a ricorrere a complessi workaround di metaclassi che complicavano il design della libreria.
Il problema
Quando l'interprete elabora una definizione di classe, deve calcolare l'Ordine di Risoluzione dei Metodi (MRO) utilizzando l'algoritmo di linearizzazione C3. Questo algoritmo richiede che tutte le basi siano classi. La difficoltà sorge quando un oggetto base non è una classe ma un alias generico; l'interprete ha bisogno di un protocollo per determinare quali classi reali dovrebbero sostituire questo alias durante la costruzione del MRO senza compromettere la semantica dell'ereditarietà.
La soluzione
Python ha introdotto il protocollo __mro_entries__. Quando la creazione di una classe incontra una base con questo metodo, chiama base.__mro_entries__(original_bases) e si aspetta in cambio una tupla di classi. Queste classi sostituiscono la base originale nel calcolo del MRO. Ad esempio, typing.Generic implementa questo per restituire (Generic,), permettendo di funzionare come base mentre la logica parametrizzata rimane separata.
from typing import Generic, TypeVar T = TypeVar('T') # Generic[T] non è una classe, ma __mro_entries__ permette che agisca come tale class Container(Generic[T]): pass # Container.__mro__ include Generic, non Generic[T] print(Container.__mro__) # (<class 'Container'>, <class 'typing.Generic'>, <class 'object'>)
Un team di framework ha dovuto consentire agli utenti di definire modelli di dati utilizzando basi generiche parametrizzate come Model[UserType]. Il loro approccio iniziale utilizzava una metaclass personalizzata per intercettare la creazione della classe ed estrarre i parametri di tipo, ma questo costringeva gli utenti a risolvere manualmente i conflitti di metaclassi quando combinavano il framework con modelli Django o SQLAlchemy.
Hanno considerato l'uso di un decoratore di classe per riscrivere la classe dopo la definizione, ma questo approccio ha compromesso il controllo statico dei tipi e il completamento automatico IDE perché la trasformazione avveniva dopo che il controllore dei tipi aveva analizzato il codice sorgente. Un'altra alternativa prevedeva __init_subclass__, ma questo non poteva gestire il caso in cui la base stessa non fosse una classe.
Il team ha implementato __mro_entries__ sui loro oggetti di fabbrica generici. Quando gli utenti scrivevano class UserModel(Model[UserType]), l'istanza Model[UserType] restituiva (Model,) dal suo metodo __mro_entries__. Questo ha consentito alla classe di ereditare correttamente da Model mentre la fabbrica memorizzava il parametro di tipo specifico per la convalida a runtime. La soluzione ha eliminato i conflitti di metaclassi, preservato il pieno supporto IDE e mantenuto una gerarchia di ereditarietà pulita che soddisfaceva l'algoritmo di linearizzazione C3.
__mro_entries__ influisce sul controllo dei tipi a runtime o sul comportamento di isinstance?
I candidati spesso confondono la costruzione del MRO con il controllo delle istanze. __mro_entries__ opera esclusivamente durante la creazione della classe per costruire la tupla __mro__. Non ha alcun effetto su isinstance() o su controlli issubclass() a runtime. Quelle operazioni si basano sugli attributi __class__ e __bases__ delle classi esistenti, non sulla sostituzione dinamica avvenuta durante la fase di definizione della classe.
Perché __mro_entries__ restituisce una tupla invece di una singola classe?
Il tipo di ritorno tupla accoglie scenari complessi di ereditarietà multipla. Sebbene comunemente restituisca una tupla a un elemento come (Generic,), il protocollo consente a un parametro generico di implicare l'ereditarietà da più mixin simultaneamente. Python decomprime questa tupla direttamente nella lista delle basi per il calcolo del MRO, quindi restituire (A, B) rende effettivamente la classe ereditare sia da A che da B invece che dalla base non-classe originale.
Quale validazione esegue Python sulle classi restituite da __mro_entries__?
L'interprete valida rigorosamente che le classi restituite formino un grafo di ereditarietà valido. Se la tupla contiene classi che creerebbero un MRO incoerente, come l'introduzione di un conflitto di ereditarietà a diamante che viola i vincoli di linearizzazione C3, Python solleva un TypeError durante la creazione della classe. Questa validazione garantisce che la sostituzione dinamica non possa eludere le regole fondamentali di coerenza dell'ereditarietà del linguaggio.