Как Python исполняет код: байткод и виртуальная машина
Питон исполняет программу в два этапа: компилятор переводит исходник в байткод, виртуальная машина выполняет его по инструкциям. Достаём оба этапа на поверхность и вызываем компилятор сами.
Редакция Питоники
Питон называют интерпретируемым языком: запустил файл — и программа работает, без явного шага сборки, как в C. Но слово «интерпретируемый» скрывает половину правды. Перед исполнением любой программы Python компилирует её исходник в байткод — набор простых команд для виртуальной машины. Этот этап никогда не выключается: он происходит при каждом запуске скрипта, при каждом импорте модуля, в каждой строке, которую вы вводите в консоли. Раздел «Компиляция» начинается с этого слоя, потому что без него непонятно, откуда берутся pyc-файлы, зачем нужно magic-число и что показывают инструменты вроде PyInstaller и Nuitka.
Два этапа: компиляция и исполнение
Когда вы запускаете python app.py, происходит две вещи подряд. Сначала компилятор CPython читает текст программы и переводит его в байткод — промежуточный язык не для процессора, а для самой машины Python. Потом виртуальная машина (VM) берёт байткод и выполняет его инструкция за инструкцией. Обычная программа ни на одном этапе не отличается от любой другой:
| Этап | Кто делает | Что на входе | Что на выходе |
|---|---|---|---|
| Компиляция | Компилятор CPython | Текст исходника (.py) | Байткод в кодовом объекте |
| Исполнение | Виртуальная машина | Байткод | Результат программы |
prices = [120, 90, 60]
total = sum(prices)
print('Итого:', total)
Итого: 270
Три строки напечатали итог — и за это время сработали оба этапа: компиляция превратила текст в байткод, VM его исполнила. Нам просто никогда не показывали промежуточный результат: он живёт в памяти и умирает после запуска. Тот же запуск в командной строке выглядит буднично, но прячет ровно эту пару этапов:
python app.py
Именно этот невидимый объект Python умеет сохранять на диск — получится знакомый по проектам файл с расширением .pyc, но об этом во втором уроке про устройство pyc.
Компилятор доступен из кода
Компиляция — не тайна интерпретатора, а обычная встроенная функция compile: она принимает строку с кодом и возвращает кодовый объект. Так можно увидеть результат первого этапа своими глазами:
source = 'total = 40 + 2\nprint(total)'
code = compile(source, '<пример>', 'exec')
print('тип результата compile:', type(code).__name__)
print('имя кодового объекта:', code.co_name)
exec(code)
тип результата compile: code имя кодового объекта: <module> 42
Функция compile получила три аргумента: текст программы, имя файла для отчётов об ошибках (мы поставили метку <пример>, потому что файла нет) и режим компиляции. Вернула она объект типа code — кодовый объект, упаковку байткода с именем и константами. Функция exec — это вход в виртуальную машину: она взяла кодовый объект и исполнила его, и напечаталось 42. Компиляция и исполнение разделены: между ними можно хранить, передавать и проверять код.
Кодовый объект: упаковка программы
Кодовый объект — это контейнер для скомпилированной программы: внутри байткод, константы, имена переменных и служебные данные. У каждой функции он тоже есть и лежит в атрибуте __code__. Посмотрим на самые полезные атрибуты:
def add(a, b):
return a + b
code = add.__code__
print('имя:', code.co_name)
print('аргументов:', code.co_argcount)
print('локальные имена:', code.co_varnames)
имя: add
аргументов: 2
локальные имена: ('a', 'b')Функция — это не только код, но и паспорт: имя, число аргументов, список локальных имён. Всё это компилятор собрал ещё на этапе компиляции, до первого вызова. Проверка isinstance с классом types.CodeType отвечает на вопрос «кодовый ли это объект», а co_consts показывает константы, зашитые в байткод:
import types
def add(a, b):
return a + b
print('константы:', add.__code__.co_consts)
print('это кодовый объект:', isinstance(add.__code__, types.CodeType))
константы: (None,) это кодовый объект: True
None в константах — это значение по умолчанию, которое вернёт функция без явного return. Ничего лишнего: константы — только то, что реально встретилось в коде. Атрибуты co_name, co_varnames, co_consts стабильны и читаются одинаково на любой версии Python — на них и строятся надёжные инструменты анализа кода.
Три режима компиляции
Третий аргумент compile — режим, и он должен совпадать с тем, что вы компилируете. Режим exec — для программы из любых инструкций, eval — для одного выражения с результатом, single — для одного действия с немедленной печатью, как в интерактивной консоли:
| Режим | Для чего | Что вернёт exec/eval |
|---|---|---|
| exec | Программа: присваивания, циклы, функции | всегда None |
| eval | Одно выражение с результатом | значение выражения |
| single | Одна команда интерактивной консоли | None, но печатает результат |
mode_eval = compile('2 ** 10', '<задача>', 'eval')
print('eval вернул:', eval(mode_eval))
mode_exec = compile('side = 7\nprint(side * 2)', '<задача>', 'exec')
result = exec(mode_exec)
print('exec вернул:', result)
eval вернул: 1024 14 exec вернул: None
Обратите внимание на порядок строк: exec напечатал 14 изнутри, а вернул None. Режим eval — единственный, чьё исполнение даёт значение: выражение вычислилось, и eval его отдал. Путаница «почему exec не возвращает результат» исчезает, если помнить: exec исполняет инструкции, а инструкции не имеют значения — значение имеет выражение.
Заглянем внутрь: инструкции VM
Байткод — это список простых команд: положить константу на стек, сложить, сохранить имя. Дизассемблер dis печатает эти команды построчно, и уже в первой программе виден фокус компилятора:
import dis
dis.dis(compile('total = 40 + 2', '<пример>', 'exec'))
0 RESUME 0
1 LOAD_CONST 0 (42)
STORE_NAME 0 (total)
RETURN_CONST 1 (None)В исходнике было 40 + 2, а в байткоде — только LOAD_CONST 42: компилятор сам посчитал константы и положил готовый результат. Это первое правило чтения байткода: он не повторяет исходник дословно, а показывает то, что осталось после оптимизаций. Инструкция LOAD_CONST кладёт значение на стек, STORE_NAME сохраняет его в переменную total, RETURN_CONST завершает модуль. Полный разбор команд — в уроке про дизассемблер dis.
Собираем оба этапа в одну программу
Финальный опыт урока: программа живёт в строке, компилятор даёт кодовый объект, VM его исполняет. Ровно это происходит с каждым файлом .py при запуске — просто исходник приходит из файла, а не из строки:
program = '''
def double(n):
return n * 2
print('удвоенное:', double(21))
'''
code = compile(program, '<программа>', 'exec')
exec(code)
print('готово')
удвоенное: 42 готово
- питон исполняет программу в два этапа: компиляция исходника в байткод и исполнение байткода виртуальной машиной;
- compile переводит строку с кодом в кодовый объект и не исполняет её;
- exec запускает кодовый объект на VM, eval вычисляет выражение и возвращает значение;
- у каждой функции байткод лежит в атрибуте __code__: имя, аргументы, константы читаются оттуда напрямую;
- байткод не копирует исходник: компилятор оптимизирует, например сворачивает константы.
Теперь понятно, что Python сохраняет на диск, когда кладёт рядом со скриптом файл .pyc: тот самый кодовый объект, сериализованный в байты. Его устройство — заголовок с magic-числом и marshal-блок — второй урок раздела. А если нужен инструмент сборки, а не теория — третий урок про py_compile и compileall покажет, как компилировать файлы и каталоги одной командой.
Сначала предскажи ответ в голове — это главный навык программиста.
mode = compile('2 ** 3', '<x>', 'eval')
r = eval(mode)
print(r * 2)
code = compile('x = 5', '<x>', 'exec')
print(type(code).__name__)
print(code.co_name)
mode = compile('print(1 + 1)', '<x>', 'exec')
result = exec(mode)
print(result)
1. Что происходит между чтением исходника и его исполнением в Python?
2. Что возвращает функция compile?
3. Чем exec отличается от eval при исполнении кодового объекта?
4. Где у функции лежит её кодовый объект?
5. В исходнике было 40 + 2, а в байткоде LOAD_CONST 42. О чём это говорит?
6. Какой режим нужен для compile, чтобы вычислить выражение '2 ** 10' и получить результат?
7. Кто исполняет байткод после компиляции?
Дана строка source с программой из двух строк. Скомпилируйте её через compile с режимом exec и именем '<задание>', исполните через exec — программа сама напечатает результат.
Python компилируется или интерпретируется?
И то и другое. Компилятор CPython переводит исходник в байткод, а виртуальная машина исполняет байткод инструкция за инструкцией. Отличие от C в том, что компиляция спрятана: она происходит при каждом запуске и импорте, без отдельного шага сборки.
Урок 2: устройство pyc-файлаЧто такое байткод простыми словами?
Это список простых команд, понятных виртуальной машине Python: положить константу на стек, сложить два значения, сохранить имя переменной. Байткод не машинный код — процессор его не исполняет, его исполняет сама программа-интерпретатор.
Что делает виртуальная машина CPython?
Читает байткод по одной инструкции и выполняет её: кладёт значения на стек, выполняет операции, сохраняет результаты в переменные. Стековая модель объясняет названия инструкций: LOAD кладёт на стек, STORE снимает и сохраняет.
Урок 4: дизассемблер disКак посмотреть байткод своей программы?
Скомпилируйте исходник через compile и передайте кодовый объект в dis.dis — дизассемблер напечатает все инструкции. У функций достаточно dis.dis(имя_функции): байткод лежит в атрибуте __code__.
Урок 4: дизассемблер disЗачем программисту знать про байткод?
Это отвечает на практические вопросы: почему рядом со скриптом появляются pyc-файлы, почему pyc не работает на другой версии Python, что именно ускоряет PyPy и что переносят PyInstaller и zipapp. Верхний слой Python перестаёт быть магией.
Понравился урок? Сошлитесь на него
«Перед исполнением любой программы Python компилирует её исходник в байткод — набор простых команд для виртуальной машины.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Компиляция · Урок 2
pyc-файл: устройство скомпилированного файла Python
pyc — это кодовый объект, упакованный в байты: 16 байт заголовка с magic-числом, временем и размером исходника, дальше marshal-блок с байткодом. Разбираем файл до последнего байта.
Компиляция · Урок 4
dis: дизассемблер байткода Python
dis печатает байткод функции инструкция за инструкцией: какая команда, над какими значениями, куда прыгает условие. Учимся читать колонки вывода и понимать стековую машину.
Функции · Урок 1
Функции в Python: первая def
Первая функция: задаём блоку кода имя через def и вызываем по скобкам. Разбираем объявление на части — имя, двоеточие, тело с отступом — и видим, зачем функция вместо копипасты.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
aiogram · Урок 4
FSM в aiogram: машина состояний для пошаговых диалогов
Учим бот вести диалог по шагам: состояния как таблица переходов, анкета «имя — возраст — город» на тренажёре, а потом по-настоящему — StatesGroup, MemoryStorage и FSMContext.
машина состояний aiogramмашина состояний телеграм бот
re · Урок 1
Регулярные выражения Python с нуля: первый шаблон и модуль re
Первый шаблон, который находит любую дату в тексте, а не одну конкретную: import re, re.search, raw-строки и объект Match — всё, чтобы перестать бояться регулярки.
объект match python
Компиляция · Урок 3
py_compile и compileall: компилируем Python сами
Два инструмента стандартной библиотеки компилируют код без запуска: py_compile — один файл, compileall — целый каталог. Собираем pyc вручную и ловим ошибки компиляции.
python компиляция файлакомпиляция модулей python