Устройство интерпретатора Python

… in December 1989, I was looking for a “hobby” programming project that would keep me occupied during the week around Christmas. My office … would be closed, but I had a home computer, and not much else on my hands.

Гвидо ван Россум, предисловие к «Programming Python» (1-е изд.)

Так, из «хобби на рождественские каникулы», родился язык, на котором сегодня основана значительная часть научных вычислений.

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

Когда обработка данных, полученных в эксперименте, занимает часы вместо минут, причина почти всегда состоит не в «медленном Python», а в неудачно выбранной структуре данных или лишнем копировании.

Слайды к главе. Материал главы изложен также в первой части лекции «Python. Начало», продолжение лекции относится к трём следующим главам; слайды лекции доступны на сайте книги и в PDF.

Язык и его реализации

Python — язык, описанный спецификацией (The Python Language Reference), и у него несколько реализаций. Эталонной и самой распространённой является CPython, написанный на C: именно он устанавливается с python.org и из пакетов дистрибутивов Linux. PyPy компилирует часто выполняемые участки программы в машинный код во время работы (JIT), поэтому циклы на чистом Python выполняются в нём в несколько раз быстрее. MicroPython работает на микроконтроллерах с сотнями килобайт памяти, GraalPy — на виртуальной машине GraalVM.

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

Путь исходного текста

CPython не исполняет исходный текст напрямую. Лексический анализатор разбивает текст на токены: имена, числа, строки, операторы, отступы. Синтаксический анализатор строит из токенов абстрактное синтаксическое дерево (AST). Компилятор обходит дерево и порождает байт-код — последовательность инструкций виртуальной стековой машины, упакованную вместе с таблицами констант и имён в объект кода. Этот объект исполняет цикл вычисления интерпретатора, по одной инструкции за шаг. Каждый этап доступен из самого Python: модули tokenize и ast, встроенная функция compile и модуль dis.

Байт-код функции печатает модуль dis; здесь и далее приведён вывод CPython 3.12.

import dis

def area(r):
    return 3.14159 * r ** 2

dis.dis(area)
  3           0 RESUME                   0

  4           2 LOAD_CONST               1 (3.14159)
              4 LOAD_FAST                0 (r)
              6 LOAD_CONST               2 (2)
              8 BINARY_OP                8 (**)
             12 BINARY_OP                5 (*)
             16 RETURN_VALUE

Первый столбец — номер строки исходного текста, второй — смещение инструкции в байтах. Инструкции берут операнды с вершины стека значений и кладут туда результат: LOAD_CONST загружает константу, LOAD_FAST — локальную переменную, BINARY_OP снимает два значения и кладёт результат операции, RETURN_VALUE возвращает вершину стека вызывающему коду. RESUME в начале функции служит точкой, в которой интерпретатор проверяет запросы трассировки. Выражения из одних констант компилятор вычисляет заранее: в байт-коде присваивания x = 2 * 3.14159 остаётся одна константа 6.28318.

С версии 3.11 интерпретатор адаптивный (PEP 659): во время работы он заменяет общие инструкции специализированными. Уже после двух вызовов area(1.0) функция dis.dis(area, adaptive=True) показывает вместо второго BINARY_OP инструкцию BINARY_OP_MULTIPLY_FLOAT, умножающую именно вещественные числа. Байт-код не входит в спецификацию языка: набор инструкций меняется от версии к версии, поэтому вывод dis в материалах о других версиях выглядит иначе.

Модули и файлы .pyc

Каждый файл .py является модулем. Инструкция import circle находит файл circle.py, создаёт объект модуля, заносит его в словарь sys.modules и выполняет файл сверху вниз; имена, определённые в файле, становятся атрибутами модуля: circle.area. Повторный import находит модуль в sys.modules и файл заново не выполняет. Байт-код импортированного модуля сохраняется в каталоге __pycache__ в файле с тегом версии интерпретатора (circle.cpython-312.pyc) и используется повторно, пока исходный файл не изменился; запускаемый сценарий компилируется при каждом запуске.

У импортированного модуля атрибут __name__ равен его имени, а у файла, запущенного как сценарий, — строке "__main__". На этом основана идиома, позволяющая одному файлу служить и сценарием, и библиотекой функций: код под условием выполняется при запуске файла и не выполняется при его импорте.

def main():
    ...

if __name__ == "__main__":
    main()

Модули ищутся по списку каталогов sys.path, и после встроенных в интерпретатор модулей первым просматривается каталог запускаемого сценария. Поэтому файл random.py или numpy.py, созданный рядом со сценарием, подменяет одноимённый модуль стандартной библиотеки или установленного пакета, и импорт завершается непонятной ошибкой вида AttributeError: module 'random' has no attribute 'randint'. Имя собственного файла выбирается так, чтобы не совпадать с именами модулей.

Интерактивная оболочка

Интерактивная оболочка (REPL, read–eval–print loop) читает строку, выполняет её и печатает значение выражения; приглашение ... означает, что оболочка ждёт продолжения блока. Для исследования объектов служат функции help, dir, type и id, а значение последнего выражения сохраняется в переменной _. Команда python3 -i run.py выполняет сценарий и оставляет оболочку открытой со всеми его переменными, что удобно для разбора ошибки. В Python 3.13 оболочка получила многострочное редактирование и цветные сообщения об ошибках, а IPython и блокноты Jupyter дополняют её сохранением кода, результатов и графиков.