ЗАМЕР
bench/mro/order.py
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/python/mro
- Как запустить
python3.11 bench/mro/order.py python3.12 bench/mro/order.py python3.13 bench/mro/order.py python3.14 bench/mro/order.py
Запись прогона
Замеры для урока «MRO и множественное наследование»
| Скрипт | Что делает |
|---|---|
order.py |
наблюдения без времени: слияние C3 по шагам и сверка с __mro__ на семи иерархиях, отказ C3 дословным текстом, куда ведёт super() у экземпляров разных классов, оборванная цепочка, кооперативный __init__, ячейка __class__ |
practice.py |
источник ответов двух практических задач урока |
Время поиска по длинному MRO здесь не меряется заново: оно снято в
bench/attr-lookup/mro_cache.py (с кешем атрибутов типа и без), а влияние
переопределённого mro() — в bench/attr-lookup/mro_override.py. Урок
ссылается на те записи прогонов.
python3.11 bench/mro/order.py
python3.12 bench/mro/order.py
python3.13 bench/mro/order.py
python3.14 bench/mro/order.py
Записи в runs/: order.txt — 3.13.7, остальные версии — с суффиксом.
Что получилось
Ручной C3 совпал с интерпретатором на всех семи иерархиях — ромб и шесть
иерархий с несколькими базами, одна из них по мотивам примера из
Doc/howto/mro.rst. Реализация в скрипте и есть
то правило, которое урок цитирует: голова первого списка, если её нет ни в
одном хвосте.
Вывод одинаков на 3.11–3.14, кроме одного места. На 3.11.15 и 3.12.3
текст TypeError об отказе C3 содержит перевод строки: «Cannot create a
consistent method resolution» и «order (MRO) for bases X, Y» печатаются двумя
строками. На 3.13.7 и 3.14.7 — одной. Это видно простым diff записей.
Ячейка __class__ есть у функции, объявленной в теле класса
(co_freevars == ('__class__',)), и отсутствует у функции, приписанной
классу снаружи: super() без аргументов в ней падает с
RuntimeError: super(): __class__ cell not found на всех четырёх версиях.
Практика урока
practice.py — источник ответов двух задач, runs/practice.txt — дословная
запись его прогона на 3.13.7; сборка сверяет с ней ответы
(scripts/validate-practice.mjs).
Вторая задача — отношение времени o.m при длине MRO 51 и 2. Это частное двух
величин около 30 нс, и оно шумит: на четырёх версиях вышло 1,0–1,1
(runs/practice-3.11.txt и соседние). Поэтому у задачи широкий допуск, а
утверждение урока строится не на нём, а на наклоне из mro_cache.py:
+0,02 нс на уровень с кешем против +3,31 без.
Скрипт
254 строк"""Порядок поиска метода при множественном наследовании: что строит C3 и куда ведёт super().
Времени здесь нет — сравниваются порядки, последовательности вызовов и
тексты исключений. Время поиска по длинному MRO уже снято в
bench/attr-lookup/mro_cache.py, и урок ссылается на тот прогон.
ШЕСТЬ СЮЖЕТОВ:
1. C3 РУКАМИ. Слияние линеаризаций расписано по шагам и сверено с тем, что
построил интерпретатор. Совпадение проверяется на семи иерархиях, а не
на одной: реализация, совпавшая на ромбе, могла бы разойтись на классе с
тремя базами.
2. ОТКАЗ. Две иерархии, для которых порядка нет, и дословный текст
исключения. Шаги слияния печатаются до места, где «хорошей головы» нет.
3. SUPER() — СЛЕДУЮЩИЙ В MRO ЭКЗЕМПЛЯРА, А НЕ РОДИТЕЛЬ. Один и тот же класс
`Left` вызывает `super().greet()`, и адресат этого вызова зависит от того,
экземпляр какого класса перед нами.
4. ЦЕПОЧКА РВЁТСЯ МОЛЧА. Один класс в ромбе не вызывает super() — и метод
соседа не выполняется вовсе, без ошибки и предупреждения.
5. КООПЕРАТИВНЫЙ __init__. Именованные аргументы, которые каждый класс
снимает свои и передаёт остальное дальше, и что будет, если остаток
дойдёт до object.
6. ОТКУДА super() БЕЗ АРГУМЕНТОВ ЗНАЕТ КЛАСС. Компилятор кладёт ячейку
`__class__` (PEP 3135) в функцию, объявленную в теле класса, — если она
использует super() или __class__; метод без них ячейки не получает. У
функции, объявленной снаружи и приписанной классу позже, ячейки нет.
Запускать: python3.11 / 3.12 / 3.13 / 3.14.
"""
import sys
import types
print("PY", sys.version.split()[0])
print()
def names(seq):
return [c.__name__ for c in seq]
def c3(cls, log=None):
"""Линеаризация C3 по формуле из Doc/howto/mro.rst.
L[C(B1 ... BN)] = C + merge(L[B1] ... L[BN], B1 ... BN)
Берётся голова первого списка; если она не встречается в ХВОСТЕ ни одного
из списков, она уходит в результат и вычёркивается отовсюду. Иначе
пробуется голова следующего списка. Если хорошей головы нет — порядка нет.
"""
bases = list(cls.__bases__)
seqs = [c3(b) for b in bases] + [bases]
out = [cls]
step = 0
while True:
seqs = [s for s in seqs if s]
if not seqs:
return out
for s in seqs:
head = s[0]
if not any(head in other[1:] for other in seqs):
break
else:
if log is not None:
log.append(
f" шаг {step + 1}: хорошей головы нет среди "
+ ", ".join(s[0].__name__ for s in seqs)
)
raise TypeError("C3: нет согласованного порядка")
step += 1
if log is not None:
lists = " ".join("[" + " ".join(names(s)) + "]" for s in seqs)
log.append(f" шаг {step}: {lists} -> берём {head.__name__}")
out.append(head)
seqs = [[c for c in s if c is not head] for s in seqs]
# --- 1. C3 рукой и интерпретатором ---------------------------------------------
print("1) C3 по шагам: class D(Left, Right), общий предок Base")
class Base:
def greet(self):
return ["Base"]
class Left(Base):
def greet(self):
return ["Left"] + super().greet()
class Right(Base):
def greet(self):
return ["Right"] + super().greet()
class D(Left, Right):
def greet(self):
return ["D"] + super().greet()
log = []
mine = c3(D, log)
print("\n".join(log))
print(" вручную: ", names(mine))
print(" D.__mro__: ", names(D.__mro__))
print()
# Ещё шесть иерархий: совпадение на одном ромбе ничего не доказывает.
class A: pass
class B: pass
class C: pass
class K1(A, B, C): pass
class K2(B, C): pass
class K3(A, C): pass
class Z(K1, K2, K3): pass # по мотивам примера из Doc/howto/mro.rst, упрощён
class P(A): pass
class Q(P, B): pass
class R2(Q, K2): pass
agree = 0
checked = [D, K1, K2, K3, Z, Q, R2]
for cls in checked:
if c3(cls) == list(cls.__mro__):
agree += 1
print(f" ручной C3 совпал с __mro__ на {agree} иерархиях из {len(checked)}")
print(" Z.__mro__:", names(Z.__mro__))
print()
# --- 2. Отказ -----------------------------------------------------------------
print("2) когда порядка нет")
class X: pass
class Y: pass
class XY(X, Y): pass
class YX(Y, X): pass
try:
class Impossible(XY, YX):
pass
except TypeError as e:
print(f" class Impossible(XY, YX) -> TypeError: {e}")
try:
class Backwards(Base, Left):
pass
except TypeError as e:
print(f" class Backwards(Base, Left) -> TypeError: {e}")
def c3_bases(name, bases):
"""Ручной C3 для баз, из которых класс создать нельзя: вместо класса —
простой объект с `__name__` и `__bases__`, этого слиянию достаточно."""
view = types.SimpleNamespace(__name__=name, __bases__=tuple(bases))
log = []
try:
c3(view, log)
except TypeError:
pass
return log
print(" ручное слияние для Impossible(XY, YX):")
print("\n".join(c3_bases("Impossible", [XY, YX])))
print()
# --- 3. super() — следующий в MRO экземпляра ------------------------------------
print("3) куда ведёт super() внутри Left.greet")
print(" Left().greet() ->", Left().greet())
print(" D().greet() ->", D().greet())
print(" один и тот же super() в Left: у экземпляра Left следующий — Base,")
print(" у экземпляра D следующий — Right, которого Left не наследует")
print()
# --- 4. Цепочка рвётся молча ----------------------------------------------------
print("4) один класс не вызывает super()")
class QuietLeft(Base):
def greet(self):
return ["QuietLeft"] # super() не вызван
class Broken(QuietLeft, Right):
def greet(self):
return ["Broken"] + super().greet()
print(" Broken.__mro__: ", names(Broken.__mro__))
print(" Broken().greet() ->", Broken().greet())
print(" Right и Base стоят в MRO, но не выполнились: цепочку оборвал QuietLeft")
print()
# --- 5. Кооперативный __init__ ---------------------------------------------------
print("5) кооперативный __init__: каждый снимает своё, остальное передаёт дальше")
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
it = Item(name="box", size=3)
print(" Item.__mro__:", names(Item.__mro__))
print(f" Item(name='box', size=3) -> name={it.name!r}, size={it.size}")
try:
Item(name="box", size=3, colour="red")
except TypeError as e:
print(f" лишний аргумент colour доехал до object -> TypeError: {e}")
print()
# --- 6. Откуда super() без аргументов знает класс ---------------------------------
print("6) super() без аргументов: ячейка __class__")
cell = Left.greet.__closure__[0]
print(" Left.greet.__code__.co_freevars:", Left.greet.__code__.co_freevars)
print(" содержимое ячейки:", cell.cell_contents.__name__)
class Plain:
def no_super(self):
return 1
print(" метод без super() и __class__, co_freevars:", Plain.no_super.__code__.co_freevars)
def outside(self):
return super().greet()
Left.outside = outside
try:
Left().outside()
except RuntimeError as e:
print(f" функция, объявленная вне класса -> RuntimeError: {e}")