Назад к блогу

Python 3.15: ленивые импорты, frozendict, sentinel и переработанный JIT

Python 3.15: ленивые импорты, frozendict, sentinel и переработанный JIT

В Python 3.15 появились ленивые импорты, встроенные `frozendict` и `sentinel`, отдельный пакет для профилирования и переработанный JIT-компилятор — один из самых насыщенных релизов за последние годы. Разбираем, как работает механизм отложенной загрузки модулей, зачем понадобились новые базовые типы и что эти изменения дают в повседневной разработке.

9 октября 2026 года вышел стабильный релиз Python 3.15.0 — новейший крупный релиз языка. В него вошли PEP 810 с явными ленивыми импортами, новые встроенные типы frozendict и sentinel, выделенный пакет profiling, переработанный JIT-компилятор и включённые по умолчанию frame pointers. Ниже — что именно изменилось, как это работает и что из этого следует для инженеров.

Что именно изменилось

Ленивые импорты через soft-ключевое слово lazy

Импорт можно пометить ключевым словом lazy, и тогда модуль не загружается в момент выполнения инструкции — вместо него создаётся ленивый прокси-объект, привязанный к имени, а фактическая загрузка происходит при первом обращении к этому имени. Ключевое слово разрешено только на глобальном (модульном) уровне: не внутри функций, тел классов, блоков try и не в import *. Инструкции from __future__ import ленивыми быть не могут — это директивы парсера и компилятора. Синтаксически lazy может предшествовать import или from.

При lazy from ... import каждое импортируемое имя привязывается к своему ленивому прокси. Первое обращение к любому из них запускает загрузку всего модуля, но реифицирует только это конкретное имя — остальные остаются прокси до обращения к ним.

реификация использует состояние системы импорта (sys.path, sys.meta_path, sys.path_hooks, __import__) на момент реификации, а не на момент выполнения инструкции lazy import. До реификации лениво импортированный модуль не появляется в sys.modules; ленивые объекты модулей перечисляются в множестве sys.lazy_modules. После реификации модуль попадает в sys.modules и удаляется из sys.lazy_modules — даже если остаются другие нереифицированные ленивые ссылки на него. Если реификация не удалась, ленивый объект не заменяется, а последующие обращения повторяют попытку.

Управление ленивыми импортами

Глобальный режим задаётся тремя способами с таким приоритетом: программный вызов выше опции командной строки -X lazy_imports=<mode>, которая выше переменной окружения PYTHON_LAZY_IMPORTS=<mode>. По умолчанию режим "normal". Режимы:

  • "normal" — ленивыми становятся только явно помеченные импорты;
  • "all" — все импорты уровня модуля становятся потенциально ленивыми, кроме импортов внутри try и from ... import *;
  • "none" — ни один импорт не ленивый, даже помеченный ключевым словом, фильтр не вызывается, поведение эквивалентно обычному import.

Текущий режим можно узнать программно. Фильтр задаётся программно: функция-фильтр принимает имя импортирующего модуля, полное имя импортируемого модуля и fromlist и сообщает, следует ли сделать импорт ленивым. Фильтр срабатывает в точке выполнения инструкции, а не в точке реификации, и может вызываться параллельно; его можно снять. Это позволяет исключать модули с известными побочными эффектами.

Механизм __lazy_modules__

Модуль может объявить переменную __lazy_modules__ в глобальной области — обычно это множество строк с полными именами модулей. При каждом выполнении оператора import Python проверяет через __contains__, входит ли полное имя импортируемого модуля в этот объект; если да, импорт обрабатывается как помеченный lazy. From-импорты из такого модуля тоже становятся ленивыми, а импорты подмодулей — не обязательно. На версиях до 3.15 атрибут просто игнорируется, и импорты выполняются немедленно, так что код остаётся совместимым со старыми интерпретаторами. Рекомендуется определять __lazy_modules__ один раз до любых импортов, но поскольку проверка идёт при каждом импорте, его можно менять между операторами.

Новые встроенные типы и модули

Добавлен неизменяемый тип frozendict в builtins. Он не допускает изменения после создания, не является подклассом dict, наследуется напрямую от object, хешируем при хешируемости всех ключей и значений, сохраняет порядок вставки, но сравнение порядок не учитывает.

Добавлен тип sentinel в builtins для создания уникальных sentinel-значений с кратким представлением. Sentinel-объекты сохраняют идентичность при копировании, поддерживают использование в выражениях типов с оператором | и сериализуются через pickle, если импортируемы по модулю и имени.

Появился модуль profiling, объединяющий встроенные инструменты профилирования под единым пространством имён: profiling.tracing (детерминированная трассировка вызовов, перемещённая из инструмента детерминированной трассировки) и profiling.sampling (новый статистический сэмплирующий профилировщик Tachyon). Инструмент детерминированной трассировки остаётся псевдонимом для обратной совместимости, а старый модуль профилирования объявлен устаревшим и будет удалён в Python 3.17.

Также добавлен модуль math.integer с математическими функциями для целочисленных аргументов.

Frame pointers по умолчанию

CPython теперь собирается с frame pointers на поддерживающих платформах через флаги -fno-omit-frame-pointer и -mno-omit-leaf-frame-pointer. Это ускоряет и делает надёжнее разворачивание нативного стека для системных профилировщиков, отладчиков, инструментов анализа падений и eBPF-инструментов наблюдаемости. Флаги доступны через sysconfig, поэтому расширения, собираемые инструментами, которые используют конфигурацию сборки Python, наследуют frame pointers. Распространение намеренное: смешанное профилирование Python и нативного кода требует непрерывной цепочки указателей кадра через интерпретатор, расширения, встраивающие приложения и нативные библиотеки. Один нативный компонент без frame pointers может сломать разворачивание стека для всего процесса Python, поэтому сторонние системы сборки, компилирующие C, C++, Rust или другой нативный код без наследования флагов Python, должны включать эквивалентные флаги сами.

JIT-компилятор

JIT в 3.15 поддерживает значительно больше байткод-операций и control flow, чем в 3.14. Простое создание Python-объектов теперь понимается JIT, перегруженные операции и генераторы поддерживаются частично. Это стало возможным благодаря переработанному фронтенду трассировки, который записывает фактические пути исполнения кода, а не оценивает их, как предыдущая реализация.

В оптимизатор добавлена базовая форма распределения регистров: JIT избегает части стековых операций и работает с регистрами, что даёт более эффективные трассы за счёт отказа от чтения и записи в память. Выполняется больше постоянного распространения — когда JIT видит, что пользовательский код даёт константы, код упрощается. Счётчики ссылок избегаются там, где это безопасно: отслеживая уникальные ссылки на объекты, оптимизатор устраняет обновления счётчиков и выполняет операции на месте для int и float. Генератор машинного кода выдаёт лучший код для x86-64 и AArch64 на macOS и Linux; в целом ожидается меньшее потребление памяти под сгенерированный машинный код и более эффективный код по сравнению с 3.14.

Публикация JIT-кода для профилировщиков и отладчиков

JIT-код публикуется для внешних инструментов: сначала он передаётся в perf, затем, если включены соответствующие макросы, регистрируется для GDB и GNU backtrace. Для perf публикация идёт только при совпадении активных callbacks, и teardown-дескриптора у неё нет. Для GDB строится минимальный in-memory ELF с секциями .text, .eh_frame, .shstrtab, .strtab, .symtab, где .text копирует сам код, а .eh_frame — переданные unwind-данные.

Генерация DWARF .eh_frame диспетчеризуется по флагу absolute_addr: при 0 строится PC-relative FDE для perf с кодировкой DWRF_EH_PE_pcrel | DWRF_EH_PE_sdata4, при 1 — absolute FDE для GDB с DWRF_EH_PE_absptr. В GDB-варианте CIE описывает steady-state кадр (CFA = %rbp+16 / x29+16), а FDE не добавляет CFI — это корректно для executor stencils. При отмене регистрации код снимается с учёта для GNU backtrace и GDB, затем освобождается структура.

Удаления

В разделе Removed удалены функция получения версии Java из модуля platform (устаревшая с Python 3.13) и модули sre_compile, sre_constants, sre_parse. Также удалены недокументированные функции ctypes и glob, класс CGIHTTPRequestHandler и флаг --cgi из python -m http.server, метод загрузки модуля у Loader и его подклассов, параметр package у функции доступа к ресурсам importlib.resources, проверка зарезервированных имён путей в pathlib, параметр check_home у проверки сборки Python в sysconfig, поддержка произвольных аргументов в C-реализации RLock, codeobject.co_lnotab, typing.no_type_check_decorator, методы работы с метками у классов Wave_read и Wave_write, а также метод загрузки модуля в zipimport.

Устаревания и запланированные удаления

Импорт ByteString из collections.abc и из typing теперь вызывает предупреждение об устаревании; сам collections.abc.ByteString запланирован к удалению в Python 3.17. Создание Struct без обязательного аргумента и повторная инициализация уже созданного Struct устарели и будут удалены в Python 3.20. Система политик asyncio устарела и будет удалена в Python 3.16 — включая классы asyncio.AbstractEventLoopPolicy, asyncio.DefaultEventLoopPolicy, asyncio.WindowsSelectorEventLoopPolicy, asyncio.WindowsProactorEventLoopPolicy и функции получения и установки политики цикла событий. Константы calendar.January и calendar.February устарели и заменены на calendar.JANUARY и calendar.FEBRUARY.

Изменения C API

Появились новые функции для создания модуля из spec и initfunc, создания tuple из массива, вывода объекта в stderr для отладки и набор функций, безопасных для использования в tp_traverse. Реализована PEP 820 с унифицированной системой слотов; при этом прежние функции создания типов и модулей из spec мягко устарели. Поле cval типа комплексного числа устарело — вместо него следует использовать функции преобразования комплексных чисел. Также мягко устарели функции арифметики над комплексными числами.

Предыстория

До 3.15 ленивые импорты решались несколькими способами. Стандартная библиотека предоставляет готовый класс, который позволяет импортам на уровне модуля работать «в основном» так же, как inline-импорты. Другой распространённый приём — переносить импорты внутрь функций, но это требует больше работы при реализации и сопровождении, может быть сведено на нет одним случайным импортом верхнего уровня и скрывает полный набор зависимостей модуля. Анализ стандартной библиотеки показал, что около 17% всех импортов вне тестов (почти 3500 импортов в 730 файлах) уже размещены внутри функций или методов именно для отсрочки выполнения. Существует и сторонний пакет lazy_loader — ещё одна реализация ленивых импортов; импорты, используемые только для статической проверки типов, — ещё один источник потенциально ненужных импортов.

Эти подходы не устраивали авторов: нет чёткого стандарта, есть накладные расходы во время выполнения в неожиданных местах или, что хуже, во время интроспекции. Готовый класс из стандартной библиотеки должен разрешить спецификацию модуля до создания ленивого загрузчика, что вносит накладные расходы, снижающие выгоду от ленивой загрузки. Он работает на уровне механизма импорта, а не даёт синтаксис уровня языка, поэтому нет канонического способа для линтеров и проверщиков типов распознавать ленивые импорты. Наконец, он требует значительного шаблонного кода, включая ручное манипулирование спецификациями модулей, загрузчиками и sys.modules, что делает его непрактичным для обычных случаев, когда нужно лениво импортировать несколько модулей.

Почему это сделали

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

Основная проблема: импорт первого модуля часто запускает каскад импортов и оптимистично загружает множество зависимостей, которые могут никогда не использоваться. Для инструментов с подкомандами даже запуск с --help может загружать десятки ненужных модулей и занимать несколько секунд.

Явный синтаксис с soft-ключевым словом lazy выбран потому, что ленивые импорты имеют иную семантику, чем обычные: ошибки и побочные эффекты возникают при первом использовании, а не в самом операторе импорта. Важно, чтобы ленивость была видна прямо в месте импорта, а не скрыта в глобальной конфигурации или удалённых объявлениях уровня модуля, — это даёт локальное рассуждение о поведении импорта. Механизм гранулярный: он вводится явным синтаксисом для отдельных импортов, а не глобальным флагом, что позволяет внедрять его постепенно.

Вариант с декораторами был отклонён: декораторы в Python предназначены для обёртывания и преобразования вызываемых объектов, а не операторов; разрешение декораторов на операторах импорта открыло бы дверь многим другим декораторам операторов, значительно расширяя синтаксис языка, а декораторы, связанные с импортом, создали бы проблему начальной загрузки — их нужно было бы либо импортировать, либо сделать встроенными. Изменение import на ленивый по умолчанию находится вне области действия PEP 810, поскольку из обсуждения PEP 690 ясно, что это довольно спорная идея.

Что это меняет на практике

Производительность ленивых импортов

В PEP 810 указано, что накладные расходы минимальны: после первого использования, если импорт не упал, накладных расходов нет благодаря адаптивному интерпретатору, оптимизирующему медленный путь. Есть небольшая одноразовая стоимость создания прокси-объекта. Реификация стоит столько же, сколько обычный импорт; постоянных потерь производительности нет. Бенчмарки pyperformance показывают нейтральность по производительности, когда ленивые импорты не используются.

Специализация наступает после 2–3 вызовов. До неё используются инструкции LOAD_GLOBAL и LOAD_ATTR, после трёх вызовов — LOAD_GLOBAL_MODULE и LOAD_ATTR_MODULE, которые являются оптимизированными быстрыми путями без накладных расходов на проверку ленивых импортов. Наблюдать специализацию можно через dis.dis(use_json, adaptive=True).

Тайминг ошибок, побочные эффекты и циклические импорты

Ошибки, которые при обычном импорте возникли бы сразу, теперь проявляются при использовании ленивого имени. Побочные эффекты лениво импортируемых модулей выполняются при первом использовании привязки, а не при импорте модуля. Порядок импорта модулей может отличаться от порядка их появления в коде, поскольку модули импортируются при первом использовании. При ошибке во время реификации замена ленивого объекта не происходит, и последующие использования повторяют всю реификацию.

Ленивые импорты не решают автоматически проблему циклических импортов: они могут помочь только если циклическая ссылка не используется во время инициализации модуля. Если один модуль обращается к другому во время импорта, ошибка всё равно возникнет.

Портирование C-расширений

Авторы расширений и нативных библиотек с собственными системами сборки должны обеспечить целостность unwind chain, обычно добавляя -fno-omit-frame-pointer и подобные флаги в CFLAGS. В C API появились новые функции для создания модуля из спецификации и функции инициализации, создания кортежа из массива, отладочного вывода объекта в поток ошибок, а также набор функций, безопасных для использования при обходе объектов сборщиком мусора. Введена PEP 820 — унифицированная система слотов для C API, при этом ряд функций создания типов и модулей мягко устарел. Поле с C-представлением комплексного числа устарело — вместо него следует использовать функции преобразования комплексных чисел между Python и C; функции арифметики над комплексными числами также мягко устарели.

Ограничения и открытые вопросы

Авторы PEP 810 признают, что lazy from __future__ import feature не работает, поскольку future-импорты — это директивы парсера и компилятора. Для аннотаций типов и TYPE_CHECKING ленивые импорты устраняют обычную необходимость в защитных проверках. Wildcard-импорты всегда выполняются немедленно, поэтому их следует избегать.

Обращение к ленивому импорту через обычный доступ к атрибуту запускает реификацию, а получение списка имён модуля специально обрабатывается так, чтобы реификации не происходило. Инструменты вроде isort и black потребуют обновлений для распознавания ключевого слова lazy, но изменения должны быть минимальными, так как структура импорта остаётся прежней.

В анонсе релиза отмечена проблема совместимости с macOS 27.0: при запуске IDLE или других GUI-приложений, использующих модуль tkinter, эти приложения могут зависнуть при использовании команды меню, открывающей диалог (например, команд About IDLE, Settings и Open Module в IDLE), что приводит к появлению вращающегося пляжного мяча и необходимости принудительного завершения. Проблема вызвана изменением поведения операционной системы в macOS 27.0, которое, как считается, затрагивает все текущие версии графического тулкита Tk и, следовательно, модуль tkinter во всех текущих версиях Python. Пользователям, зависящим от Tk-приложений (таких как IDLE) на macOS, рекомендуется рассмотреть возможность отложить установку macOS 27.0 до появления обходного решения в Tk или macOS либо проверить, что рабочий процесс приложения не затронут; обновления следует отслеживать в issue #158053.

Артефакты релиза

На странице релиза перечислены артефакты с размерами: XZ-сжатый tarball исходников — 34.0 MB, Gzipped tarball исходников — 41.7 MB, Android embeddable package (aarch64) — 23.1 MB, Android embeddable package (x86_64) — 23.4 MB, iOS XCframework — 80.6 MB, macOS installer — 84.9 MB, Windows installer (64-bit) — 43.3 MB, Windows installer (32-bit) — 41.6 MB, Windows installer (ARM64) — 42.6 MB, Windows embeddable package (64-bit) — 13.1 MB, Windows embeddable package (32-bit) — 11.4 MB, Windows embeddable package (ARM64) — 12.4 MB, Windows release manifest — 15.0 KB. В разделе «More resources» перечислены онлайн-документация, PEP 790 (расписание релиза 3.15), сообщение об ошибках на GitHub и возможность финансовой поддержки Python. Также упоминается игра whatsnewt — TUI-текстовый квест по новым возможностям Python 3.15, запускаемый командой uvx --python 3.15 whatsnewt.

Источники