Deep Engineering
Средний·Опубликовано·3.11 · 3.12 · 3.13 · 3.14·25 МИН

MRO и множественное наследование: C3, ромб и куда на самом деле ведёт super()

Порядок поиска метода — не «слева направо, вглубь», а линеаризация C3, и выводится она из одного правила слияния. Из того же правила следуют и ромб, в котором общий предок уходит в хвост, и TypeError, которым Python отказывается создавать класс. А super() ведёт не к родителю, а к следующему в MRO экземпляра — и один и тот же метод зовёт разных соседей в разных иерархиях.

Полное техническое изложение

TL;DR

Порядок поиска метода — это список, и он у каждого класса свой: __mro__. При одиночном наследовании он очевиден — класс, родитель, дед. При множественном его строит алгоритм C3, и правило у него одно: берём голову первого списка, если её нет в хвосте ни одного другого, иначе — голову следующего. Из этого правила следуют два факта, которые стоит знать наизусть. В ромбе D(Left, Right) общий предок уходит в хвост: D, Left, Right, Base, object, и метод, которого нет у Left, берётся у Right, а не у Base. А если базы требуют противоположного порядка, хорошей головы нет, и Python отказывается создавать класс: TypeError: Cannot create a consistent method resolution order (MRO).

Дальше — то, что отличает знающего от читавшего. super() ведёт не к родителю, а к следующему классу в MRO экземпляра. Метод Left.greet написан один раз, но у экземпляра Left его super() ведёт в Base, а у экземпляра D — в Right, которого Left не наследует и о котором не знает. Поэтому в ромбе, где super() зовут все, каждый метод выполняется ровно один раз. И поэтому же цепочка кончается молча на классе, который super() не вызвал: соседи стоят в MRO и не выполняются, без ошибки. В CPython длина MRO на повторный поиск почти не влияет: кеш атрибутов типа отвечает за 10,8–11,9 нс при длинах от 2 до 51 (3.13.7); без кеша поиск линейный.

Порог входа
Перед уроком достаточно понимать
  • как объявляют класс и наследуют его от другого;
  • что метод, которого нет у класса, ищется у родителя;
  • что класс можно унаследовать сразу от нескольких: class D(Left, Right).
Заранее знать не нужно
  • __mro__, линеаризация C3, конфликт порядка баз;
  • super() в ромбе, кооперативный __init__, ячейка __class__, кеш атрибутов типа.

База: у каждого класса есть список, по которому ищется метод

Когда пишут obj.greet(), Python ищет greet не «где-то в иерархии», а по конкретному списку классов, по порядку, и берёт первое совпадение. Список лежит у класса в атрибуте __mro__ — method resolution order, порядок разрешения методов.

При одиночном наследовании он очевиден:

PYTHON
class Base:
    def greet(self):
        return ["Base"]
 
class Left(Base):
    def greet(self):
        return ["Left"] + super().greet()
 
print(Left.__mro__)   # (Left, Base, object)

Класс, его родитель, родитель родителя — и в конце object, от которого наследуется всё. Ответ уровня Junior на вопрос «что такое MRO» ровно такой: это порядок, в котором Python ищет атрибут по классам иерархии; посмотреть его можно в Class.__mro__.

Вопрос становится интересным, когда родителей больше одного, и особенно когда у них есть общий предок — ромб.

Механизм 1: C3 — слияние с одним правилом

контракт языкаПравило C3 — часть языка с Python 2.3; порядок и текст отказа сверены прогоном bench/mro/order.py на 3.11–3.14.

Интуиция подсказывает «слева направо, вглубь»: сначала вся ветка Left вместе с её предками, потом вся ветка Right. Для ромба она даёт D, Left, Base, Right, Base — и метод Base выиграл бы у метода Right. Python делает не так:

PYTHON
class Right(Base):
    def greet(self):
        return ["Right"] + super().greet()
 
class D(Left, Right):
    def greet(self):
        return ["D"] + super().greet()
 
print([c.__name__ for c in D.__mro__])
# ['D', 'Left', 'Right', 'Base', 'object']

Общий предок Base ушёл в хвост, после обоих наследников. Откуда это берётся, проще всего увидеть руками. C3 сливает несколько списков: порядки родителей — у Left это Left, Base, object, у Right — Right, Base, object — и сам список баз Left, Right. Правило одно: берём голову первого списка, если её нет в хвосте ни одного другого; иначе пробуем голову следующего. Взятый класс вычёркивается из всех списков, и всё повторяется.

Скрипт bench/mro/order.py делает ровно это, печатает слияние по шагам и сверяет результат с __mro__ интерпретатора — совпало на семи иерархиях из семи:

1) C3 по шагам: class D(Left, Right), общий предок Base
   шаг 1: [Left Base object]  [Right Base object]  [Left Right]  ->  берём Left
   шаг 2: [Base object]  [Right Base object]  [Right]  ->  берём Right
   шаг 3: [Base object]  [Base object]  ->  берём Base
   шаг 4: [object]  [object]  ->  берём object
   вручную:         ['D', 'Left', 'Right', 'Base', 'object']
   D.__mro__:       ['D', 'Left', 'Right', 'Base', 'object']

   ручной C3 совпал с __mro__ на 7 иерархиях из 7

Весь ромб решается на втором шаге. Первым стоит список Left, и его голова теперь Base — но Base лежит в хвосте списка Right. Брать её нельзя, пока не взят Right. Так правило и выражает то, что от порядка требуется: класс стоит раньше своих предков (поэтому Base после обоих наследников) и порядок баз в объявлении сохраняется (поэтому Left раньше Right).

Формально документация записывает то же самое одной строкой:

the linearization of C is the sum of C plus the merge of the linearizations of the parents and the list of the parents

перевод

линеаризация C — это C плюс слияние линеаризаций родителей и списка родителей

— The Python 2.3 Method Resolution Order

А слияние — тем самым правилом, которое выполнено руками выше:

take the head of the first list, i.e L[B1][0]; if this head is not in the tail of any of the other lists, then add it to the linearization of C and remove it from the lists in the merge

перевод

возьмите голову первого списка, то есть L[B1][0]; если эта голова не входит в хвост ни одного из остальных списков, добавьте её в линеаризацию C и удалите из всех списков слияния

— The Python 2.3 Method Resolution Order

У этой строгости есть имя — монотонность: если в порядке класса C1 стоит раньше C2, то так же будет в порядке любого его наследника. Добавив подкласс, нельзя случайно переставить методы у предков.

Механизм 2: когда порядка нет, класса не будет

Слияние не всегда удаётся. Два класса с противоположным порядком одних и тех же баз — и хорошей головы на каком-то шаге нет:

2) когда порядка нет
   class Impossible(XY, YX) -> TypeError: Cannot create a consistent method resolution order (MRO) for bases X, Y
   class Backwards(Base, Left) -> TypeError: Cannot create a consistent method resolution order (MRO) for bases Base, Left
   ручное слияние для Impossible(XY, YX):
   шаг 1: [XY X Y object]  [YX Y X object]  [XY YX]  ->  берём XY
   шаг 2: [X Y object]  [YX Y X object]  [YX]  ->  берём YX
   шаг 3: хорошей головы нет среди X, Y

XY требует, чтобы X шёл раньше Y, YX — наоборот. Голова каждого списка лежит в хвосте другого, и Python не выбирает «разумный» вариант, а отказывает:

it is impossible to construct the merge, Python 2.3 will refuse to create the class C and will raise an exception (построить слияние невозможно, Python 2.3 откажется создавать класс C и возбудит исключение).

Второй пример короче и выглядит невинно: class Backwards(Base, Left). Предок объявлен раньше своего же потомка — а C3 требует, чтобы потомок шёл раньше предка. Исправление — поменять базы местами.

Важно, когда это происходит: в момент выполнения class, то есть при импорте модуля. До вызова метода дело не доходит — модуль не загрузится.

Механизм 3: super() — следующий в MRO экземпляра, а не родитель

контракт языкаПоведение super() задано документацией встроенных функций; прогон bench/mro/order.py, разделы 3–5, одинаков на 3.11–3.14.

Привычная формулировка — «super() вызывает метод родителя». Документация говорит другое:

The search starts from the class right after the type.

перевод

Поиск начинается с класса, стоящего сразу после type.

— Встроенные функции — super

«После» — в каком списке? В MRO объекта, у которого вызван метод, а не класса, где написан super(). Точная формулировка такая: super(Left, obj) возвращает не класс и не вызов, а посредника — объект, поиск атрибутов через который идёт по type(obj).__mro__, начиная с класса после Left. Сам super() ничего не вызывает: вызов — это .greet() у найденного через него метода. Короткое «super() ведёт к следующему в MRO экземпляра» — сжатая запись этой формулы. Из неё и складывается поведение ромба:

3) куда ведёт super() внутри Left.greet
   Left().greet()  -> ['Left', 'Base']
   D().greet()     -> ['D', 'Left', 'Right', 'Base']

Left.greet написан один раз. У экземпляра Left следующий после Left — Base. У экземпляра D следующий после Left — Right, которого Left не наследует и о котором не знает. Каждый метод в ромбе выполнился ровно один раз, Base — в самом конце. Это гарантия кооперативной цепочки, а не языка: она держится, пока super() зовут все её участники. Ради неё super() так и устроен: при вызове родителя по имени Base выполнился бы дважды, из каждой ветки.

Обратная сторона — цепочка держится на том, что super() зовут все. Достаточно одному классу его не вызвать:

4) один класс не вызывает super()
   Broken.__mro__:  ['Broken', 'QuietLeft', 'Right', 'Base', 'object']
   Broken().greet() -> ['Broken', 'QuietLeft']

Right и Base стоят в MRO, но не выполнились. Ни ошибки, ни предупреждения: цепочка просто кончилась на QuietLeft. Для Python это не ошибка, а обычный метод — и в конце цепочки так и задумано: Base.greet здесь super() не зовёт, и на нём цепочка заканчивается. Ошибкой это становится, когда иерархия задумана кооперативной, а класс в её середине выпадает из договора: если Right.__init__ открывал соединение или регистрировал объект, этого не произошло.

Кооперативный __init__

Для __init__ из этого следует практическое правило. Ни один класс в цепочке не знает, кто стоит после него, — значит, не знает и того, какие аргументы тому нужны. Документация формулирует требование так:

Good design dictates that such implementations have the same calling signature in every case

перевод

Хороший дизайн требует, чтобы у таких реализаций была одинаковая сигнатура вызова во всех случаях

— Встроенные функции — super

На практике это именованные аргументы: каждый класс снимает свои и передаёт остальное дальше.

PYTHON
class Named:
    def __init__(self, *, name, **rest):
        self.name = name
        super().__init__(**rest)
 
class Sized:
    def __init__(self, *, size, **rest):
        self.size = size
        super().__init__(**rest)
 
class Item(Named, Sized):
    pass
5) кооперативный __init__: каждый снимает своё, остальное передаёт дальше
   Item.__mro__: ['Item', 'Named', 'Sized', 'object']
   Item(name='box', size=3) -> name='box', size=3
   лишний аргумент colour доехал до object -> TypeError: object.__init__() takes exactly one argument (the instance to initialize)

Последняя строка — полезная, а не досадная: аргумент, который никто не снял, доезжает до object.__init__ и падает. Опечатка в имени аргумента не проглатывается молча.

Глубже: откуда super() знает класс и сколько стоит длинный MRO

наблюдение замераbench/mro/order.py, раздел 6, и bench/attr-lookup/mro_cache.py, CPython 3.13.7 и 3.14.7. Времена сняты на одной машине; содержательны не сами наносекунды, а наклон: есть ли рост с длиной MRO.

super() без аргументов — не магия, а ячейка. Здесь стоит различать два уровня. Контракт языка, заданный PEP 3135: super() без аргументов работает в функции, объявленной в теле класса, и берёт этот класс. Как именно это сделано — деталь реализации CPython, и её показывает прогон: компилятор кладёт в такую функцию скрытую свободную переменную __class__ — если функция использует super() или __class__. PEP формулирует это шире:

Every function will have a cell named class that contains the class object that the function is defined in (У каждой функции будет ячейка с именем __class__, содержащая объект класса, в котором функция определена).

6) super() без аргументов: ячейка __class__
   Left.greet.__code__.co_freevars: ('__class__',)
   содержимое ячейки: Left
   метод без super() и __class__, co_freevars: ()
   функция, объявленная вне класса -> RuntimeError: super(): __class__ cell not found

co_freevars и сама ячейка — то, что видно в CPython, а не обещание языка; опираться в коде стоит на контракт. Из него — первый аргумент super(): класс, в котором написан метод. Второй — self. MRO берётся у type(self), поиск начинается после __class__. И из этого же — ограничение: функция, объявленная снаружи и приписанная классу позже, ячейки не имеет, и super() без аргументов в ней падает. Там пишут явную форму super(Left, self).

Длина MRO на повторный вызов почти не влияет. Поиск по списку классов — линейный, и без кеша это видно: каждый уровень добавляет 3,31 нс на 3.13.7, и при длине 51 обращение стоит 180,8 нс. Но у типа есть кеш атрибутов: на повторное обращение он отвечает, не проходя список вовсе.

PY 3.13.7 | лучшее из 100 чередующихся кругов по 20000 обращений

  длина __mro__   кеш горячий   кеш сбивается   (цена присваивания)
              2        10.8 нс          18.7 нс               26.7 нс
              6        10.9 нс          29.2 нс               27.4 нс
             21        11.0 нс          87.4 нс               26.8 нс
             51        11.9 нс         180.8 нс               26.8 нс

  на уровень MRO без кеша: +3.31 нс
  на уровень MRO с кешем:  +0.02 нс
  при длине 51: без кеша 181 нс, с кешем 12 нс

Кеш сбивается присваиванием в класс — поэтому для сравнения его сбивают на каждой итерации нарочно. В обычном коде классы после импорта не меняются, и работает левый столбец. Подробно кеш разобран в статье о том, что происходит на o.x.

Как отвечать на собеседовании

Короткий ответ: MRO — порядок, в котором Python ищет атрибут по классам иерархии; он лежит в Class.__mro__ и строится алгоритмом C3. C3 ставит класс раньше его предков и сохраняет порядок баз, поэтому в ромбе D(Left, Right) общий предок уходит в хвост: D, Left, Right, Base, object. А super() — посредник: поиск через него идёт по MRO экземпляра, начиная после класса, где написан метод, и это не обязательно родитель.

Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.

Если интервьюер копает глубже

Покажите ромб и два вывода одного и того же метода: у экземпляра Left его super() ведёт в Base, у экземпляра D — в Right. Это одна строчка кода и самое убедительное доказательство, что «super — это родитель» неверно. Отсюда же следствие: в кооперативной иерархии super() зовут все, а аргументы передают именованными с **rest, потому что следующий в цепочке неизвестен заранее. Тогда каждый метод цепочки выполняется ровно один раз — это свойство такого договора, а не правило Python.

Про отказ стоит сказать точно: TypeError: Cannot create a consistent method resolution order возникает при определении класса, то есть при импорте, например из-за предка, объявленного раньше потомка, — class C(Base, Child). Чинится перестановкой баз.

И если спросят про цену — в CPython длина MRO на повторный вызов почти не влияет: горячий кеш атрибутов типа отвечает за 10,8–11,9 нс при длинах от 2 до 51 (3.13.7). Линейный рост, 3,31 нс на уровень, появляется, только когда кеш сбивают присваиванием в класс.

Дальше спросят

Спросят дальше

А как вызвать метод конкретного предка в обход MRO?

Короткий ответ

Явно: Base.greet(self). Это обычная функция, взятая у класса, и self передаётся руками. Но в кооперативной иерархии это ломает главное свойство super() — Base выполнится дважды, если до него доберётся ещё и цепочка super() из соседней ветки.

Спросят дальше

Почему super() работает без аргументов?

Короткий ответ

Компилятор кладёт в метод, который использует super(), ячейку __class__ с классом, где метод объявлен, а self — первый аргумент метода. У функции, приписанной классу снаружи, ячейки нет: super() в ней падает с RuntimeError: super(): __class__ cell not found, и нужна явная форма super(Left, self).

Спросят дальше

Что будет, если один класс в цепочке не вызовет super()?

Короткий ответ

Цепочка кончится на нём, и классы после него в MRO не выполнятся — молча. В прогоне у Broken(QuietLeft, Right) вывод ['Broken', 'QuietLeft']: Right и Base стоят в MRO и не вызваны. Python считает это обычным методом; проблемой это становится, только если иерархия задумана кооперативной.

Частые заблуждения

Утверждение

super() вызывает метод родительского класса

На самом деле

Он возвращает посредника, и поиск через него идёт по MRO ЭКЗЕМПЛЯРА, начиная после класса, где написан метод. Для Left().greet() это действительно родитель Base, а для D().greet() тот же super() в Left ведёт в Right — соседа, которого Left не наследует. Документация: The search starts from the class right after the type (Поиск начинается с класса, стоящего сразу после type).

Утверждение

MRO — это обход в глубину слева направо

На самом деле

Обход в глубину дал бы для ромба D, Left, Base, Right, Base, и метод Base выиграл бы у метода Right. C3 уводит общего предка в хвост: D, Left, Right, Base, object. Правило одно — класс стоит раньше всех своих предков, а порядок баз из объявления сохраняется.

Утверждение

Python всегда может выстроить порядок, в крайнем случае как-нибудь

На самом деле

Не может и не пытается. Если базы требуют противоположного порядка — class Impossible(XY, YX) или предок раньше потомка, class C(Base, Left), — Python падает с TypeError: Cannot create a consistent method resolution order (MRO) прямо при определении класса.

Утверждение

Длинная иерархия — медленный вызов метода

На самом деле

В CPython с горячим кешем атрибутов типа — нет: 10,8 нс при длине MRO 2 и 11,9 нс при длине 51 (3.13.7). Линейный рост есть только без кеша — 3,31 нс на уровень, — а кеш сбивается присваиванием в класс, чего обычный код после импорта не делает.

Утверждение

Если класс не вызвал super(), Python об этом сообщит

На самом деле

Молча — для Python это обычный метод. Классы после него стоят в MRO и не выполняются: в прогоне Broken().greet() вернул ['Broken', 'QuietLeft'], хотя Right и Base в его MRO есть.

История версий

ВерсияИзменениеЧто это значит для кода
2.3C3 становится алгоритмом MRO для классов нового стиля. Сам алгоритм придуман не для Python — для языка Dylan (Barrett и соавторы, 1996). Правило 2.2 для классов нового стиля документ Симионато разбирает на примере и показывает, что оно нарушало монотонность, — поэтому его и заменили.
3.0PEP 3135: super() без аргументов. Держится на ячейке __class__, которую компилятор кладёт в функции, объявленные в теле класса. Классов старого стиля больше нет — C3 для всех.
3.13Текст TypeError об отказе C3 печатается одной строкой. Проверено: на 3.11.15 и 3.12.3 в сообщении стоит перевод строки — Cannot create a consistent method resolution и order (MRO) for bases X, Y выходят двумя строками. На поведение не влияет, но ломает тесты, сверяющие текст исключения целиком.
3.14Метакласс, переопределяющий mro(), лишает класс кеша атрибутов типа — даже если возвращает ровно super().mro(). Замер bench/attr-lookup/mro_override.py: 15,3 / 63,8 / 118,4 нс при длинах 2 / 21 / 51 против 6,5 / 6,4 / 6,4 нс у обычного класса; на 3.13.7 эффекта нет. Сам порядок C3 не менялся.

Практика

Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.

Практика · что напечатает

Ромб: A и B наследуют Base, C наследует A и B. Методы A и B зовут super(). Что напечатает этот код?
class Base:
  def hello(self):
      print("Base")


class A(Base):
  def hello(self):
      print("A")
      super().hello()


class B(Base):
  def hello(self):
      print("B")
      super().hello()


class C(A, B):
  pass


C().hello()

Практика · оцените

Обращение к методу o.m у объекта, в MRO которого 2 класса, и у объекта с 51 классом в MRO; метод объявлен у самого верхнего. Во сколько раз дороже второе?
раза

Проверьте себя

Вопрос 1 из 4

class D(Left, Right), где Left и Right наследуют Base. У Left нет метода m, у Right и Base есть. Чей m вызовет D().m()?

Чем измерено

Числа этого урока получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.

Источники и что читать дальше

4 ИСТОЧНИКА

  1. The Python 2.3 Method Resolution OrderОфициальная документация. Первоисточник правила, по которому строится MRO. Мишель Симионато. Формула: «the linearization of C is the sum of C plus the merge of the linearizations of the parents and the list of the parents» (линеаризация C — это C плюс слияние линеаризаций родителей и списка родителей). Правило слияния: «take the head of the first list, i.e L[B1][0]; if this head is not in the tail of any of the other lists, then add it to the linearization of C and remove it from the lists in the merge» (возьмите голову первого списка, то есть L[B1][0]; если эта голова не входит в хвост ни одного из остальных списков, добавьте её в линеаризацию C и удалите из всех списков слияния). И отказ: «it is impossible to construct the merge, Python 2.3 will refuse to create the class C and will raise an exception» (построить слияние невозможно, Python 2.3 откажется создавать класс C и возбудит исключение).https://docs.python.org/3/howto/mro.html
  2. Встроенные функции — superОфициальная документация. Откуда взято, что super() ведёт не к родителю: «The search starts from the class right after the type» (Поиск начинается с класса, стоящего сразу после type), и пример: «if __mro__ of object_or_type is D -> B -> C -> A -> object and the value of type is B, then super() searches C -> A -> object» (если __mro__ у object_or_type — это D -> B -> C -> A -> object, а type — это B, то super() ищет в C -> A -> object). Там же правило кооперативных методов: «Good design dictates that such implementations have the same calling signature in every case» (Хороший дизайн требует, чтобы у таких реализаций была одинаковая сигнатура вызова во всех случаях).https://docs.python.org/3.14/library/functions.html#super
  3. PEP 3135 — New SuperPEP. Кэлвин Спилман, Тим Делани, Ли Райан; Final, Python 3.0. Документ, которым появился super() без аргументов, и механизм, на котором он держится: «Every function will have a cell named __class__ that contains the class object that the function is defined in» (У каждой функции будет ячейка с именем __class__, содержащая объект класса, в котором функция определена). Прогон уточняет: ячейка появляется у функции, объявленной в теле класса и использующей super() или __class__; у метода без них её нет, у функции, приписанной классу снаружи, — тоже, и super() в ней падает с RuntimeError.https://peps.python.org/pep-3135/
  4. A Monotonic Superclass Linearization for DylanИсточник. Барретт, Кассельс, Хаар, Мун, Плейфорд, Уизингтон, OOPSLA 1996. Работа, в которой алгоритм C3 и появился, — для языка Dylan, а не для Python; Python взял его в 2.3. Нужна, чтобы назвать свойство, ради которого C3 такой строгий: монотонность — если C1 стоит раньше C2 в порядке класса, то и в порядке любого его наследника.https://doi.org/10.1145/236337.236343