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