Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 1 из 20 Средний 15 мин 100 XP

Как 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
Командная строка недоступна в песочнице. Под капотом происходят оба этапа урока: компилятор переводит 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, но печатает результат
eval и exec на одном компиляторе
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 печатает эти команды построчно, и уже в первой программе виден фокус компилятора:

дизассемблер программы total = 40 + 2
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)
Дизассемблер не запускается в песочнице: вывод зависит от версии Python. Показан вывод CPython 3.13 — в 3.12 и других версиях набор инструкций и вид колонок отличаются. Подробно dis разобран в уроке 4.

В исходнике было 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)
Проверь себя
0 / 7

1. Что происходит между чтением исходника и его исполнением в Python?

2. Что возвращает функция compile?

3. Чем exec отличается от eval при исполнении кодового объекта?

4. Где у функции лежит её кодовый объект?

5. В исходнике было 40 + 2, а в байткоде LOAD_CONST 42. О чём это говорит?

6. Какой режим нужен для compile, чтобы вычислить выражение '2 ** 10' и получить результат?

7. Кто исполняет байткод после компиляции?

Карточки терминов
Запомнено: 0 / 8
Практика

Дана строка source с программой из двух строк. Скомпилируйте её через compile с режимом exec и именем '<задание>', исполните через exec — программа сама напечатает результат.

practice.py
Вопросы и ответы по уроку

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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.