Historia de la pregunta
El protocolo __mro_entries__ se introdujo en Python 3.7 a través de PEP 560 ("Soporte básico para el módulo de tipado y tipos genéricos"). Antes de esta mejora, los alias genéricos como typing.List[int] no podían utilizarse como clases base en definiciones de clases porque type.__new__ requería estrictamente que todas las bases fueran instancias de type. Esta limitación obligó al módulo typing a depender de frágiles trucos de metaclase que eran difíciles de mantener y causaban problemas de rendimiento. El protocolo fue diseñado para desacoplar la expresión sintáctica de una base de su contribución semántica al gráfico de herencia, permitiendo un mejor soporte para genéricos y patrones de fábricas.
El problema
Cuando CPython procesa una definición de clase, debe calcular el Orden de Resolución de Métodos (MRO) utilizando el algoritmo de linealización C3 para asegurar una jerarquía de búsqueda de métodos consistente y predecible. Si un objeto base no es una clase (por ejemplo, un genérico parametrizado o un objeto de configuración), el intérprete carece de la información de tipo necesaria para colocar la nueva clase correctamente dentro del árbol de herencia. Ignorar tales objetos rompería las verificaciones isinstance y las cadenas super(), mientras que rechazarlos completamente impediría patrones de metaprogramación potentes. El desafío principal era permitir que estos objetos no clase declararan qué clases concretas representaban lógicamente durante la fase de construcción de la clase.
La solución
Python ahora examina cada elemento en la tupla de bases para un método __mro_entries__(self, bases) durante la creación de la clase. Si este método existe, se invoca con la tupla original de bases, y debe retornar una tupla de clases reales para sustituir el objeto en el cálculo de MRO. Las clases devueltas se tratan como si hubieran sido enumeradas explícitamente como bases. Este mecanismo permite que una instancia actúe como un marcador de posición transparente que se resuelve en clases concretas en el momento de la definición.
class ConfigurableMixin: def __init__(self, feature): self.feature = feature def __mro_entries__(self, bases): # Inyectar dinámicamente las clases base según la configuración if self.feature == "logging": return (LoggingSupport,) return (BaseFeature,) class LoggingSupport: def log(self, msg): print(msg) class BaseFeature: pass # La instancia es reemplazada por LoggingSupport en el MRO class Service(ConfigurableMixin("logging")): pass print(LoggingSupport in Service.__mro__) # True
En un gran marco web asincrónico, los desarrolladores necesitaban crear una fábrica DatabaseMixin que, al ser instanciada con una URL de base de datos específica (por ejemplo, DatabaseMixin("postgresql://")), inyectaría automáticamente tanto ConnectionPool como AsyncSession como clases base en la clase de servicio del usuario. La dificultad era que DatabaseMixin(...) devolvía una instancia de objeto simple, no una clase, sin embargo, necesitaba participar en el MRO como si el desarrollador hubiera escrito explícitamente class UserService(ConnectionPool, AsyncSession).
Solución 1: Metaclase Personalizada
Un enfoque involucró crear una metaclase que escaneaba la tupla bases en __new__, identificaba las instancias de DatabaseMixin, y las reemplazaba con las clases objetivo antes de llamar a super().__new__. Esto permitió un control preciso pero introdujo el problema de "conflicto de metaclases": cualquier servicio que utilizara esta metaclase no podría heredar de otras clases que definieran sus propias metaclases, como ciertas clases base de ORM. Además, la depuración se volvía difícil porque la sintaxis de definición de clases ocultaba transformaciones complejas, y las trazas de pila apuntaban a los internals de la metaclase en lugar de al código del usuario.
Solución 2: Decoración de Clase Post-Creación
Otra opción fue usar un decorador de clase aplicado después de que la clase fue creada. El decorador copiaría manualmente los métodos de ConnectionPool y AsyncSession en la nueva clase o usaría type.__setattr__ para inyectarlos. Aunque esto evitó la viralidad de la metaclase, rompió fundamentalmente el modelo de herencia de Python: isinstance(UserService(), ConnectionPool) devolvería False, y las llamadas a super() dentro de los métodos copiados se resolverían incorrectamente porque el MRO no contenía realmente las clases padre. Esto llevó a errores sutiles donde las utilidades del marco no reconocían a los servicios como capaces de bases de datos.
Solución 3: Protocolo __mro_entries__
El equipo eligió implementar __mro_entries__ en el objeto devuelto por DatabaseMixin. El método devolvía (ConnectionPool, AsyncSession) en función de la URL analizada. Esta solución se integró sin problemas con la maquinaria nativa de creación de clases de CPython. El MRO se calculó correctamente, las verificaciones isinstance funcionaron de manera natural y no hubo conflictos de metaclases. La instancia de la fábrica actuó como un marcador de posición declarativo que se disolvió en la estructura de herencia adecuada durante la construcción de la clase, preservando la semántica de super() y la compatibilidad con la herencia múltiple.
El resultado fue una API limpia e intuitiva donde los desarrolladores podían escribir class OrderService(DatabaseMixin(postgres_url)): y recibir automáticamente capacidades de agrupación de conexiones y gestión de sesiones con resolución de métodos correcta, pleno soporte de IDE y cero sobrecarga en tiempo de ejecución o conflictos de herencia.
¿Cómo maneja la linealización C3 las posibles duplicaciones cuando __mro_entries__ expande una base en clases ya presentes en otra parte de la lista de herencia?
Cuando __mro_entries__ devuelve una clase que también aparece en otros lugares de las bases (por ejemplo, si una fábrica se expande a (BaseA,) y otra base explícita es Derived(BaseA)), el algoritmo C3 de Python trata la tupla expandida como la lista base efectiva. Luego, el algoritmo fusiona estas listas manteniendo el orden de precedencia local y asegurando la monotonía. Dado que C3 está diseñado para manejar ancestros comunes, BaseA aparece solo una vez en el MRO final, posicionado después de todas las clases que dependen de él pero antes de object. Los candidatos a menudo creen erróneamente que esto crea un conflicto o una entrada duplicada, pero el proceso de linealización naturalmente deduplica mientras mantiene la restricción de "hijos antes que padres", asegurando una resolución de métodos consistente.
¿Por qué __mro_entries__ no puede acceder a la clase que se está creando y qué error específico ocurre si intenta hacerlo?
Durante la creación de la clase, type.__new__ llama a __mro_entries__ en los objetos base antes de que el objeto de clase mismo sea instanciado. El diccionario de espacios de nombres existe, pero el objeto de clase aún no tiene identidad. Si la implementación intenta acceder a los atributos de la clase prospectiva (por ejemplo, haciendo referencia al nombre de la clase desde un alcance externo o intentando inspeccionar bases como si ya estuvieran vinculados a la nueva clase), se generará un NameError o AttributeError porque la vinculación aún no existe. Los candidatos frecuentemente asumen que pueden inspeccionar el estado final de la clase o __dict__ para tomar decisiones dinámicas, pero el método solo recibe la tupla de bases originales como argumento y debe confiar en su propio estado interno para determinar el valor de retorno.
¿Registrar un objeto con __mro_entries__ como una subclase virtual de un ABC a través de abc.ABCMeta.register() hace que el ABC aparezca en el MRO?
No. El registro de subclases virtuales es un mecanismo en tiempo de ejecución que pobla una caché interna dentro del ABC para las verificaciones de isinstance() y issubclass(). No altera el atributo __mro__ de la subclase. Cuando se define MyClass(MyObject()) y MyObject() devuelve (ConcreteBase,) a través de __mro_entries__, solo aparece ConcreteBase en MyClass.__mro__. Si ConcreteBase se registra como una subclase virtual de MyABC, entonces isinstance(MyClass(), MyABC) devuelve True, pero MyABC no estará presente en MyClass.__mro__. Los candidatos a menudo confunden la subclase virtual con la verdadera herencia, lo que lleva a confusiones sobre por qué las llamadas a super() o las inspecciones de MRO no reflejan la relación con el ABC, o por qué los métodos definidos en el ABC no están disponibles a través de la herencia.