История вопроса
С принятием PEP 560 в Python 3.7 система типов потребовала способ использования обобщенных типов, таких как List[int] или Generic[T], в качестве базовых классов. До этого улучшения попытка получить наследование от параметризованного обобщения вызывала TypeError, так как эти объекты не были фактическими классами, что вынуждало разработчиков прибегать к сложным обходным решениям с метаклассами, что усложняло проектирование библиотек.
Проблема
Когда интерпретатор обрабатывает определение класса, он должен вычислить порядок разрешения методов (MRO) с использованием алгоритма линейной обработки C3. Этот алгоритм требует, чтобы все базовые классы были классами. Проблема возникает, когда базовый объект не является классом, а является обобщенным псевдонимом; интерпретатору необходим протокол, чтобы определить, какие реальные классы должны выступать вместо этого псевдонима во время построения MRO, не нарушая семантики наследования.
Решение
Python представил протокол __mro_entries__. Когда создание класса сталкивается с базой, имеющей этот метод, он вызывает base.__mro_entries__(original_bases) и ожидает получения кортежа классов в ответ. Эти классы заменяют оригинальную базу в расчете MRO. Например, typing.Generic реализует это, чтобы вернуть (Generic,), что позволяет ему функционировать как база, в то время как параметризированная логика остается отдельной.
from typing import Generic, TypeVar T = TypeVar('T') # Generic[T] не является классом, но __mro_entries__ позволяет ему действовать как таковой class Container(Generic[T]): pass # Container.__mro__ включает Generic, а не Generic[T] print(Container.__mro__) # (<class 'Container'>, <class 'typing.Generic'>, <class 'object'>)
Команда фреймворка должна была позволить пользователям определять модели данных, используя параметризованные обобщенные базы, такие как Model[UserType]. Их первоначальный подход использовал пользовательский метакласс для перехвата создания класса и извлечения параметров типов, но это заставляло пользователей вручную разрешать конфликты метаклассов при объединении фреймворка с моделями Django или SQLAlchemy.
Они рассматривали возможность использования декоратора класса для переписывания класса после определения, но этот подход нарушал статическую проверку типов и автозаполнение в IDE, поскольку преобразование происходило после того, как проверяющий типов анализировал исходный код. Другой альтернативой был __init_subclass__, но это не могло справиться с ситуацией, когда базовый класс сам не был классом.
Команда реализовала __mro_entries__ на своих обобщенных фабричных объектах. Когда пользователи писали class UserModel(Model[UserType]), экземпляр Model[UserType] возвращал (Model,) из своего метода __mro_entries__. Это позволяло классу корректно наследовать от Model, в то время как фабрика хранила конкретный параметр типа для проверки во время выполнения. Решение устранило конфликты метаклассов, сохранило полную поддержку IDE и поддерживало чистую иерархию наследования, удовлетворяющую алгоритму линейной обработки C3.
Влияет ли __mro_entries__ на проверку типов во время выполнения или поведение isinstance?
Кандидаты часто путают построение MRO с проверкой экземпляров. __mro_entries__ работает исключительно во время создания класса для построения кортежа __mro__. Он не влияет на проверки isinstance() или issubclass() во время выполнения. Эти операции полагаются на атрибуты __class__ и __bases__ существующих классов, а не на динамическую подстановку, произошедшую во время определения класса.
Почему __mro_entries__ возвращает кортеж, а не единственный класс?
Кортежный тип возврата учитывает сложные сценарии множественного наследования. Хотя обычно возвращается кортеж с одним элементом, например (Generic,), протокол позволяет обобщенному параметру подразумевать наследование от нескольких миксинов одновременно. Python распаковывает этот кортеж непосредственно в список баз для расчета MRO, поэтому возврат (A, B) эффективно заставляет класс наследовать от обоих A и B, а не от первоначальной небазовой базы.
Какую проверку выполняет Python на классах, возвращаемых __mro_entries__?
Интерпретатор строго проверяет, чтобы возвращаемые классы образовывали валидный граф наследования. Если кортеж содержит классы, которые создадут неконсистентный MRO — например, вводя конфликт ромбовидного наследования, который нарушает условия линейной обработки C3 — Python возбуждает TypeError во время создания класса. Эта проверка гарантирует, что динамическая подстановка не может обойти основные правила согласованности наследования языка.