Deep Engineering

ЗАМЕР

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}")