Команда Python for Devs подготовила перевод статьи о ключевых изменениях в Python 3.14. Автор разбирает релиз через призму внутреннего устройства интерпретатора и производительности: свободная многопоточность, конкурентные интерпретаторы, удалённая отладка, инкрементальная сборка мусора и новый Tail Calling интерпретатор.
Релиз Python с кодовым именем Pi (так названный потому, что версия 3.14 совпадает с цифрами числа π) наконец вышел. Вы можете самостоятельно ознакомиться со списком новых возможностей и ключевых изменений в release notes. В этом материале я хочу рассказать о пяти возможностях этого релиза, которые кажутся мне наиболее интересными — как Python-разработчику и как инженеру, которому нравится разбираться во внутреннем устройстве систем.
Свободная многопоточность в Python
С практической точки зрения сборка со свободной многопоточностью позволяет Python-программам одновременно использовать несколько CPU-ядер, обеспечивая настоящий параллельный запуск потоков для вычислительно нагруженных задач.
До Python 3.13 запуск нескольких потоков параллельно был невозможен из-за глобальной блокировки интерпретатора (GIL) — глобального мьютекса внутри интерпретатора Python. Перед выполнением на CPU поток должен захватить этот замок. В результате даже на мощной многоядерной машине процесс Python фактически использовал только одно ядро. В качестве обходного пути этого ограничения появились такие решения, как multiprocessing.
До выхода Python 3.13 был предложен PEP-703, цель которого — сделать GIL необязательным. В PEP описывался план изменений, позволяющий собирать версию Python без GIL с помощью специального флага сборки.
Эти изменения были приняты в релизе 3.14, и в итоге Python теперь поставляется в двух вариантах: один — по-прежнему с GIL, другой — без GIL. Если вы используете uv, установить обе версии можно следующими командами:
uv install cpython-3.14.0 #with the GIL
uv install cpython-3.14.0t #without the GILОбратите внимание, что сборка Python со свободной многопоточностью ломает ABI, и все сторонние пакеты, использующие C API CPython, необходимо пересобирать. Поэтому не все пакеты для научных вычислений будут сразу доступны для использования с этой версией.
Конкурентные интерпретаторы
Одной из самых захватывающих новинок релиза 3.14 стало появление модуля concurrent.interpreters в стандартной библиотеке. Он позволяет запускать несколько интерпретаторов Python параллельно внутри одного процесса Python. Это открывает ещё один вариант параллелизма в Python, даже несмотря на наличие GIL.
Реальные детали реализации довольно сложно объяснить в рамках одного текста — я сделаю это в отдельной статье. Но если вы читали мой материал о инициализации рантайма CPython, возможно, вы уже сможете связать отдельные фрагменты воедино. А пока — краткое резюме.
По умолчанию процесс Python содержит один главный интерпретатор и один главный поток. Теперь же появилась возможность динамически создавать несколько и��терпретаторов во время выполнения с помощью модуля concurrent.interpreters. Эти интерпретаторы, создаваемые на лету, также называют сабинтерпретаторами. Создать сабинтерпретатор очень просто — достаточно вызвать функцию create() из concurrent.interpreters.
import concurrent.interpreters
interp1 = concurrent.interpreters.create()После этого вызова внутри процесса Python уже работают два интерпретатора. Внутренне рантайм отслеживает их с помощью связанного списка объектов состояния интерпретатора. Состояние интерпретатора описывает внутреннее состояние выполнения конкретного интерпретатора. За счёт того, что каждому интерпретатору выделяется собственное состояние, рантайм изолирует их на уровне выполнения Python-кода.
Чтобы выполнить код в новом интерпретаторе, можно вызвать его метод call(). Например:
>>> def sum(a,b):
... return a + b
...
>>> interp1.call(sum, 10, 2)
12Однако это всё ещё не параллельное выполнение, поскольку в процессе Python работает только один поток. В этом случае рантайм просто переключает поток с выполнения кода в главном интерпретаторе на выполнение кода в сабинтерпретаторе.
Чтобы запускать код в интерпретаторе в собственном потоке, можно использовать метод call_in_thread(). Внутри он создаёт новый поток, который выполняет код в своём контексте. Это неблокирующий вызов, и получить результат напрямую нельзя. Поэтому для обмена данными между интерпретаторами необходимо использовать очередь, создаваемую с помощью метода concurrent.interpreters.create_queue(). Ниже приведён пример, объединяющий всё это вместе.
>>> def add(q, a, b):
... q.put(a+b)
...
... interp1 = concurrent.interpreters.create()
... queue = concurrent.interpreters.create_queue()
... t = interp1.call_in_thread(add, queue, 10, 2)
... result = queue.get()
... print(result)
...
12Здесь мы создаём очередь и передаём её в функцию add. Функция add кладёт результат в очередь. В главном интерпретаторе мы ожидаем результат, вызывая метод get() у очереди, который блокируется до тех пор, пока в очереди не появятся данные.
Если вам интересно, как всё это работает под капотом, дайте знать — в одном из следующих материалов мы можем подробно разобрать внутреннее устройство.
Поддержка удалённой отладки
Помимо конкурентности, в Python 3.14 появились серьёзные улучшения в инструментарии.
Отладка уже запущенных процессов Python всегда была болезненной. Чтобы отладить такой процесс с помощью отладчика, например pdb, приходилось вручную расставлять точки останова в коде, затем перезапускать процесс и ждать, пока выполнение снова дойдёт до нужного места. В продакшен-системах такой подход часто просто неприменим.
Новая возможность появилась именно для того, чтобы упростить этот сценарий: в Python 3.14 можно подключиться к уже работающему процессу с помощью команды python -m pdb -p <pid>, без необходимости его перезапуска.
С технической точки зрения интерпретатор CPython и раньше имел механизмы, позволяющие удалённым процессам подключаться к нему и исследовать состояние рантайма. Именно так работают удалённые профилировщики — такие как scalene, pyspy и другие. В рамках PEP-768 этот механизм был расширен, чтобы отладчики тоже могли подключаться к интерпретатору Python и выполнять отладку.
Теперь отладчик может присоединиться к процессу Python и изменить определённые поля во внутренних структурах данных рантайма, сигнализируя о начале отладки. Когда интерпретатор обнаруживает такой сигнал, он предоставляет отладочный приглашение, в котором можно устанавливать точки останова и работать как обычно.
Хотя pdb уже обновлён и поддерживает удалённую отладку, этот механизм также предоставляет API sys.remote_exec, благодаря которому внешние отладчики могут использовать эту возможность без необходимости низкоуровневой интеграции на C.
Инкрементальная сборка мусора
Дополняя улучшения в области конкурентности и отладки, описанные выше, эта возможность повышает стабильность и отзывчивость рантайма за счёт оптимизации работы сборщика мусора.
В одной из предыдущих статей я подробно разбирал стоимость полного сканирования кучи сборщиком мусора в CPython. Нетрудно догадаться, что это дорогостоящая операция, которая к тому же приводит к непредсказуемым задержкам в работе ваших API: пока работает GC, интерпретатор не выполняет никакой Python-код. Инкрементальная сборка мусора делает накладные расходы GC предсказуемыми, обеспечивая более плавную работу для нагрузок, чувствительных к задержкам.
Для начала разберёмся, как сборщик мусора работал до этого изменения. Существовало три поколения объектов, подлежащих сборке: молодое поколение, старое поколение и самое старое поколение. Для каждого поколения были настраиваемые пороги, определяющие, когда GC будет выполнять сканирование. Например, молодое поколение сканировалось, когда количество объектов в нём превышало 10 000.
Любой объект, переживший сканирование молодого поколения, продвигался в первое старое поколение. Первое старое поколение сканировалось после того, как молодое поколение было просканировано заданное число раз, например 10. В этот момент GC проверял и молодое поколение, и первое старое поколение. Объекты, пережившие сканирование первого старого поколения, продвигались во второе старое поколение (его также называют самым старым).
Самое старое поколение сканировалось после того, как первое старое поколение было просканировано заданное количество раз. При достижении этого порога GC выполнял полное сканирование кучи, то есть всех трёх поколений. Очевидно, что такая операция обходилась очень дорого.
Инкрементальная сборка мусора меняет этот подход. Количество поколений GC сокращено до двух: молодого и старого. В каждом цикле сборки мусора коллектор сканирует молодое поколение и лишь часть старого поколения. Благодаря этому объём работы GC в каждом цикле становится стабильным, а длительные паузы и всплески задержек, возникавшие из-за полного сканирования кучи, исчезают.
Tail Calling интерпретатор
Наконец, моё любимое изменение в этом релизе — Tail Calling интерпретатор. Это переработка цикла диспетчеризации байткода в виртуальной машине CPython, которая ускоряет выполнение Python-кода примерно на 5 %.
Цикл диспетчеризации байткода — это сердце интерпретатора: именно здесь исполняются инструкции байткода, полученные после компиляции вашей программы на Python. Чем быстрее работает этот цикл, тем быстрее выполняется программа, поэтому любые улучшения производительности в этой области особенно интересны. Я уже писал очень подробную статью о проектировании и реализации этого цикла в CPython, а сейчас готовлю ещё одну — уже про Tail Calling интерпретатор. Поэтому здесь ограничусь кратким обзором.
Ваша программа на Python компилируется в последовательность инструкций байткода. Например, следующий фрагмент показывает байткод для одной строки кода a + b. Цикл диспетчеризации последовательно проходит по этим инструкциям и выполняет их одну за другой.
>>> import dis
>>> dis.dis(”a + b”)
0 0 RESUME 0
1 2 LOAD_NAME 0 (a)
4 LOAD_NAME 1 (b)
6 BINARY_OP 0 (+)
10 RETURN_VALUEСамый очевидный способ реализовать такой цикл — использовать switch case. Проблема в том, что в Python сотни инструкций байткода, из-за чего этот switch становится огромным. Компиляторам сложно эффективно оптимизировать такие большие функции: например, они не могут оптимально распределять регистры, и часть ключевых переменных оказывается вытесненной в стек, что приводит к снижению производительности.
В CPython также существует реализация цикла диспетчеризации на основе computed goto, но она страдает от той же проблемы. Если вы не знакомы с таким подходом, рекомендую мою статью о проектировании и реализации цикла диспетчеризации CPython.
Такой подход и называется Tail Calling интерпретатором из-за того, как именно написаны эти функции. В конце они не возвращаются, а вызывают функцию, соответствующую следующей инструкции байткода. Для этого используется таблица указателей на функции, где следующая инструкция байткода служит индексом. Сигнатуры и возвращаемые значения у всех таких функций одинаковы, а поскольку вызовы происходят в самом конце функции, они являются хвостовыми (tail). Компилятор может оптимизировать такие вызовы и превратить их в переходы, устраняя накладные расходы на вызовы функций.
Повышение производительности здесь достигается по одной ключевой причине:
для обработки каждой инструкции байткода используются небольшие функции, которые компилятор может гораздо эффективнее оптимизировать и выполнять оптимальное распределение регистров.
В целом этот подход показал улучшение по сравнению с предыдущими реализациями на основе switch case и computed goto. Однако он требует поддержки оптимизации tail вызовов со стороны компилятора, которая присутствует не во всех компиляторах. Поэтому на данный момент эта возможность является опциональной: чтобы её использовать, необходимо собрать CPython из исходников с помощью поддерживаемого компилятора, например clang 19.
Подводя итоги
Хотя в этом релизе Python появилось множество других новых возможностей и улучшений, я выбрал именно эти темы из-за своего интереса к внутреннему устройству Python и вопросам производительности. Кроме того, такие изменения, как удалённая отладка и отказ от GIL, крайне интересны и с инженерной точки зрения. Их изучение даёт понимание, которое может помочь вам вырасти как инженеру.
Я планирую подробнее написать о некоторых из этих тем в будущих материалах. Но если вам хотелось бы, чтобы я разобрал что-то конкретное, дайте знать.
Русскоязычное сообщество про Python

Друзья! Эту статью подготовила команда Python for Devs — канала, где каждый день выходят самые свежие и полезные материалы о Python и его экосистеме. Подписывайтесь, чтобы ничего не пропустить!
