Канон / синтез

Девять корней на одной схеме

Корни — не список решений, а связный граф. Половина того, что кажется странностью Python, стоит не на одном корне, а на ребре между двумя.

Оглавление показывает корни в порядке чтения — линейно, потому что читать линейно. На самом деле они образуют граф: часть решений следует из предыдущих, часть ограничивает последующие. Сплошная стрелка — «отсюда следует», пунктирная — «это связывает руки».

Прямоугольники кликабельны — ведут на страницу корня. На узком экране схема прокручивается вбок.

1Рёбра словами

Схема бесполезна без текста: содержание здесь именно в рёбрах, а не в узлах.

1R1 → R3Значение — это объект в куче, а не битовое поле в стеке. Значит, кто-то должен решать, когда объект перестаёт быть нужным. Модель памяти — не отдельное решение, а обязательство, взятое вместе с объектной моделью.
2R1 → R2Класс — тоже объект, а неймспейс — обычный dict. Отсюда: поиск атрибута не может быть смещением в структуре, как в C++, — это просмотр словарей в момент обращения, кешируемый инлайн-кешем прямо в байткоде (урок 23).
3R2 → R4Протоколы — единственное исключение из процедуры поиска атрибута: дандеры читаются из слота типа напрямую. Исключение сделано ради скорости, и именно оно объясняет, почему дандер на инстансе не работает.
4R4 → R5Итерация — частный случай протокола, доведённый до предела: из __iter__ и __next__ выросли генераторы, а из генераторов — await.
5R2 → R7Модуль — объект со словарём-неймспейсом, лежащий в sys.modules. Импорт поэтому не подключение, а исполнение с кэшированием результата — девять фаз компиляции, разобранных в уроке 22.
6R3 → R6Ключевое ребро всего графа. Счётчик ссылок торчит в публичном C API: каждое расширение делает Py_INCREF без блокировки. Убрать GIL — значит сломать весь скомпилированный мир. Отсюда двадцать лет, которые заняло free-threading.
7R1 → R6Каждое число — объект с заголовком: миллион целых это 36 МБ вместо 8. Научные вычисления обязаны были уехать за границу языка — так C API получил свою главную нагрузку и своего главного клиента.
8R2 → R8Тип определяется в момент обращения и может измениться между двумя строками. Значит, надёжного статического вывода быть не может — не из-за недоделок mypy, а по построению языка.
9R2 → R9Порядок ключей dict был деталью реализации 3.6 и стал гарантией 3.7 — не потому, что так задумали, а потому, что на него уже оперлись. Канонический случай перехода 🔧 → 🔒.
10R3 → R9На детерминированное разрушение по счётчику опирается половина написанного Python-кода. Формально это по-прежнему деталь 🔧, и её нельзя ни убрать, ни пообещать.
11R6 → R9ABI расширений — причина, по которой free-threading сделан отдельной сборкой, а не флагом. Переключать поведение нельзя: колёса собраны под конкретный ABI.
12R8 → R9Типы остались необязательными именно потому, что обязательные потребовали бы слома. PEP 563 не стал умолчанием ровно по этой причине.

2Три вещи, которые видно только на графе

У GIL нет собственного корня. Он стоит на ребре 6: счётчик ссылок плюс публичный C API. Ни то ни другое по отдельности GIL не требует. Поэтому вопрос «почему не убрали GIL» некорректен — убирать надо было не GIL, а одно из двух решений под ним, и оба уже вросли в экосистему.

R9 — не корень, а сток. В него входят рёбра и не выходит ни одного. Это не решение о языке, это правило о том, как принимаются остальные решения. Поэтому у него нет глав.

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

Как проверить, что граф усвоен. Взять любую известную странность Python и назвать ребро, на котором она стоит. «Почему len(x), а не x.len()» — ребро 3. «Почему numpy параллелится потоками, а мой цикл нет» — рёбра 6 и 7. «Почему mypy пропускает баги» — ребро 8. «Почему порядок словаря можно использовать, а порядок множества нельзя» — ребро 9. Если ребро называется без раздумий, канон сработал.