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, порядок
разрешения методов.
При одиночном наследовании он очевиден:
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 — слияние с одним правилом
Интуиция подсказывает «слева направо, вглубь»: сначала вся ветка Left вместе
с её предками, потом вся ветка Right. Для ромба она даёт D, Left, Base, Right, Base — и метод Base выиграл бы у метода Right. 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 плюс слияние линеаризаций родителей и списка родителей
А слияние — тем самым правилом, которое выполнено руками выше:
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 и удалите из всех списков слияния
У этой строгости есть имя — монотонность: если в порядке класса 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() вызывает метод
родителя». Документация говорит другое:
The search starts from the class right after the type.
Поиск начинается с класса, стоящего сразу после type.
«После» — в каком списке? В 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
Хороший дизайн требует, чтобы у таких реализаций была одинаковая сигнатура вызова во всех случаях
На практике это именованные аргументы: каждый класс снимает свои и передаёт остальное дальше.
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):
pass5) кооперативный __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
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.3 | C3 становится алгоритмом MRO для классов нового стиля. Сам алгоритм придуман не для Python — для языка Dylan (Barrett и соавторы, 1996). Правило 2.2 для классов нового стиля документ Симионато разбирает на примере и показывает, что оно нарушало монотонность, — поэтому его и заменили. | |
| 3.0 | PEP 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 не менялся. |
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
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()Практика · оцените
Проверьте себя
class D(Left, Right), где Left и Right наследуют Base. У Left нет метода m, у Right и Base есть. Чей m вызовет D().m()?
Чем измерено
Числа этого урока получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Порядок поиска метода — это список, и он у каждого класса свой:
__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); без кеша поиск линейный.
На самом деле
- Он возвращает посредника, и поиск через него идёт по MRO ЭКЗЕМПЛЯРА, начиная после класса, где написан метод. Для
Left().greet()это действительно родительBase, а дляD().greet()тот жеsuper()вLeftведёт вRight— соседа, которогоLeftне наследует. Документация: The search starts from the class right after the type. - Обход в глубину дал бы для ромба
D, Left, Base, Right, Base, и методBaseвыиграл бы у методаRight. C3 уводит общего предка в хвост:D, Left, Right, Base, object. Правило одно — класс стоит раньше всех своих предков, а порядок баз из объявления сохраняется. - Не может и не пытается. Если базы требуют противоположного порядка —
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 нс на уровень, — а кеш сбивается присваиванием в класс, чего обычный код после импорта не делает.
- Молча — для Python это обычный метод. Классы после него стоят в MRO и не выполняются: в прогоне
Broken().greet()вернул['Broken', 'QuietLeft'], хотяRightиBaseв его MRO есть.
По версиям
- 2.3
- C3 становится алгоритмом MRO для классов нового стиля. Сам алгоритм придуман не для Python — для языка Dylan (Barrett и соавторы, 1996). Правило 2.2 для классов нового стиля документ Симионато разбирает на примере и показывает, что оно нарушало монотонность, — поэтому его и заменили.<
- 3.0
- PEP 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 не менялся.<
Что разобрано
- База: у каждого класса есть список, по которому ищется метод
- Механизм 1: C3 — слияние с одним правилом
- Механизм 2: когда порядка нет, класса не будет
- Механизм 3: super() — следующий в MRO экземпляра, а не родитель
- Глубже: откуда super() знает класс и сколько стоит длинный MRO
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- История версий
- Практика
- Проверьте себя
- Чем измерено
Источники и что читать дальше
4 ИСТОЧНИКА
- 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
- Встроенные функции — 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
- 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/
- A Monotonic Superclass Linearization for DylanИсточник. Барретт, Кассельс, Хаар, Мун, Плейфорд, Уизингтон, OOPSLA 1996. Работа, в которой алгоритм C3 и появился, — для языка Dylan, а не для Python; Python взял его в 2.3. Нужна, чтобы назвать свойство, ради которого C3 такой строгий: монотонность — если C1 стоит раньше C2 в порядке класса, то и в порядке любого его наследника.https://doi.org/10.1145/236337.236343