История вопроса
Протокол __mro_entries__ был введен в Python 3.7 через PEP 560 ("Основная поддержка модуля типизации и обобщенных типов"). До этого улучшения обобщенные алиасы, такие как typing.List[int], не могли использоваться в качестве базовых классов в определения классов, так как type.__new__ строго требовал, чтобы все базовые классы были экземплярами type. Это ограничение заставило модуль typing полагаться на хрупкие трюки с метаклассами, которые было трудно поддерживать и которые вызывали проблемы с производительностью. Протокол был разработан для того, чтобы отделить синтаксическое выражение базового класса от его семантического вклада в граф наследования, обеспечивая более чистую поддержку обобщений и паттернов фабрик.
Проблема
Когда CPython обрабатывает определение класса, он должен вычислить порядок разрешения методов (MRO) с помощью алгоритма линейной инициализации C3, чтобы обеспечить согласованную и предсказуемую иерархию поиска методов. Если базовый объект не является классом (например, параметризованный обобщенный тип или объект конфигурации), интерпретатор не имеет необходимой информации о типе, чтобы правильно разместить новый класс в дереве наследования. Простое игнорирование таких объектов нарушало бы проверки isinstance и цепочки super(), в то время как их полное отклонение мешало бы мощным метапрограммным паттернам. Основной задачей было позволить этим не-классовым объектам объявить, какие конкретные классы они логически представляют во время создания класса.
Решение
Python теперь проверяет каждый элемент в кортеже базовых классов на наличие метода __mro_entries__(self, bases) во время создания класса. Если этот метод существует, он вызывается с оригинальным кортежем базовых классов, и он должен вернуть кортеж фактических классов для замены объекта в расчете MRO. Возвращаемые классы затем обрабатываются так, будто они были явно указаны как базовые. Этот механизм позволяет экземпляру функционировать как прозрачный заполнитель, который разрешается в конкретные классы на этапе определения.
class ConfigurableMixin: def __init__(self, feature): self.feature = feature def __mro_entries__(self, bases): # Динамически инжектируем базовые классы на основе конфигурации if self.feature == "logging": return (LoggingSupport,) return (BaseFeature,) class LoggingSupport: def log(self, msg): print(msg) class BaseFeature: pass # Экземпляр заменяется LoggingSupport в MRO class Service(ConfigurableMixin("logging")): pass print(LoggingSupport in Service.__mro__) # True
В большом асинхронном веб-фреймворке разработчики должны были создать фабрику DatabaseMixin, которая, когда создается с определенным URL базы данных (например, DatabaseMixin("postgresql://")), автоматически инжектировала бы как ConnectionPool, так и AsyncSession в качестве базовых классов в пользовательский класс сервиса. Проблема заключалась в том, что DatabaseMixin(...) возвращал простой экземпляр объекта, а не класс, однако он должен был участвовать в MRO, как если бы разработчик явно написал class UserService(ConnectionPool, AsyncSession).
Решение 1: Пользовательский метакласс
Один из методов заключался в создании метакласса, который просматривал кортеж bases в __new__, идентифицировал экземпляры DatabaseMixin и заменял их целевыми классами перед вызовом super().__new__. Это обеспечивало точный контроль, но вызывало проблему "конфликта метаклассов": любой сервис, использующий этот метакласс, не мог наследоваться от других классов, которые определили свои собственные метаклассы, таких как определенные базовые классы ORM. Кроме того, отладка становилась сложной, так как синтаксис определения класса скрывал сложные преобразования, а трассировки стека указывали на внутренние элементы метакласса, а не на пользовательский код.
Решение 2: Декорирование класса после создания
Другой вариант заключался в использовании декоратора класса, применяемого после создания класса. Декоратор вручную копировал бы методы из ConnectionPool и AsyncSession в новый класс или использовал type.__setattr__ для их инъекции. Хотя это избегало вирусности метакласса, это в корне нарушало модель наследования Python: isinstance(UserService(), ConnectionPool) возвращал бы False, а вызовы super() внутри скопированных методов разрешались бы неправильно, потому что MRO на самом деле не содержал родительские классы. Это приводило к тонким ошибкам, когда утилиты фреймворка не распознавали сервисы как способные к работе с базами данных.
Решение 3: Протокол __mro_entries__
Команда выбрала реализацию __mro_entries__ на объекте, возвращаемом DatabaseMixin. Метод возвращал (ConnectionPool, AsyncSession) на основе разобранного URL. Это решение интегрировалось с механикой создания классов в CPython. MRO рассчитывался правильно, проверки isinstance работали естественно, и не было конфликтов метаклассов. Экземпляр фабрики действовал как декларативный заполнитель, который растворялся в правильной наследственной структуре во время создания класса, сохраняя семантику super() и совместимость с множественным наследованием.
Результатом стал чистый, интуитивно понятный API, где разработчики могли писать class OrderService(DatabaseMixin(postgres_url)): и автоматически получать возможности пула соединений и управления сессиями с правильным разрешением методов, полной поддержкой IDE и нулевыми накладными расходами на выполнение или конфликтами наследования.
Как C3 линейная инициализация обрабатывает потенциальные дубликаты, когда __mro_entries__ расширяет базу на классы, уже присутствующие где-то в списке наследования?
Когда __mro_entries__ возвращает класс, который также появляется в других базах (например, если одна фабрика расширяется до (BaseA,), а другой явно указанный базовый класс Derived(BaseA)), алгоритм C3 Python рассматривает расширенный кортеж как эффективный список баз. Алгоритм затем объединяет эти списки, сохраняя локальный порядок приоритета и обеспечивая монотонность. Поскольку C3 разработан для обработки общих предков, BaseA появляется только один раз в окончательном MRO, расположенный после всех классов, которые зависят от него, но перед object. Кандидаты часто ошибочно полагают, что это создает конфликт или дублирующую запись, но процесс линейной инициализации естественным образом устраняет дубликаты, сохраняя ограничение "дочерние классы перед родительскими", обеспечивая согласованное разрешение методов.
Почему __mro_entries__ не может получить доступ к создаваемому классу и какая конкретная ошибка возникает, если она пытается это сделать?
Во время создания класса type.__new__ вызывает __mro_entries__ на базовых объектах перед тем, как сам объект класса будет создан. Словарь пространства имен существует, но у объекта класса еще нет идентичности. Если реализация пытается получить доступ к атрибутам предполагаемого класса (например, ссылаясь на имя класса из внешней области видимости или пытаясь осмотреть bases, как если бы они уже были связаны с новым классом), это приведет к возникновению NameError или AttributeError, потому что связывание еще не существует. Кандидаты часто предполагают, что могут просмотреть окончательное состояние класса или __dict__, чтобы принимать динамические решения, но метод получает только кортеж оригинальных базовых классов в качестве аргумента и должен полагаться на свое внутреннее состояние, чтобы определить возвращаемое значение.
Приведение объекта с __mro_entries__ в качестве виртуального подкласса ABC с помощью abc.ABCMeta.register() вызывает появление ABC в MRO?
Нет. Регистрация виртуального подкласса является механизмом времени выполнения, который заполняет внутренний кэш внутри ABC для проверок isinstance() и issubclass(). Это не изменяет атрибут __mro__ подкласса. Когда MyClass(MyObject()) определен и MyObject() возвращает (ConcreteBase,) через __mro_entries__, только ConcreteBase появляется в MyClass.__mro__. Если ConcreteBase зарегистрирован как виртуальный подкласс MyABC, тогда isinstance(MyClass(), MyABC) вернет True, но MyABC не будет присутствовать в MyClass.__mro__. Кандидаты часто смешивают виртуальное наследование с истинным наследованием, что приводит к путанице о том, почему вызовы super() или инспекции MRO не отражают отношения ABC, или почему методы, определенные в ABC, недоступны через наследование.