Интерпретатор — это программа, которая читает исходный код и выполняет его по ходу дела, не собирая заранее отдельный машинный файл. Компилятор, наоборот, переводит всю программу в машинный код до запуска, и дальше работает уже готовый результат. Граница не всегда резкая: многие современные среды сначала переводят код в промежуточный байткод, а потом либо интерпретируют его, либо на лету компилируют горячие участки (это и есть JIT).
Содержание
- Интерпретатор, компилятор и JIT: развожу термины
- Как работает интерпретатор: от текста к выполнению
- Минимальный интерпретатор арифметики
- Байткод и виртуальная машина на примере CPython
- JIT: компиляция во время исполнения
- Интерпретатор и компилятор: сравнение
- Плюсы и минусы интерпретаторов
- Выводы
- Где применяется / связь с практикой
- FAQ
Ниже разведу три близких термина, пройду интерпретацию по шагам (лексер, парсер, AST, байткод, виртуальная машина), соберу маленький рабочий интерпретатор и посмотрю, как устроены CPython, V8, PHP и Ruby.
Интерпретатор, компилятор и JIT: развожу термины
Эти три слова часто путают, поэтому сразу мини-словарь. Один и тот же язык может использовать сразу несколько подходов.
- Компилятор (AOT, ahead-of-time) — переводит всю программу в машинный код заранее. На выходе отдельный файл (например
.exeили ELF), который запускается без исходника и без самого компилятора. Так работают C и C++ через GCC или Clang, Go, Rust. - Интерпретатор — выполняет программу во время запуска, разбирая и исполняя ее по частям. Отдельного машинного файла не создает, для запуска нужен сам интерпретатор на устройстве.
- JIT (just-in-time) — гибрид: код исполняется интерпретатором, но часто вызываемые («горячие») участки компилируются в машинный код прямо во время работы и дальше идут быстрее.
Важное уточнение: «интерпретируемый язык» — это свойство реализации, а не самого языка. Python по спецификации не запрещает компиляцию; просто самая распространенная реализация (CPython) устроена как интерпретатор байткода.
Как работает интерпретатор: от текста к выполнению
Исходный код — это просто строка символов. Чтобы ее выполнить, интерпретатор проходит несколько этапов.
- Лексический анализ (лексер). Текст режется на токены: числа, имена, операторы, скобки. Строка
2 + 3превращается в последовательностьNUMBER(2),PLUS,NUMBER(3). - Синтаксический анализ (парсер). Из токенов строится дерево — AST (abstract syntax tree, абстрактное синтаксическое дерево). Оно отражает структуру: у сложения два потомка-операнда.
- Выполнение. Дальше два основных пути. Либо интерпретатор обходит AST и вычисляет узлы напрямую (tree-walking), либо сначала переводит AST в компактный байткод, который исполняет виртуальная машина.
Tree-walking проще в реализации, но медленнее. Байткод плюс виртуальная машина — основной подход в промышленных языках, потому что байткод удобнее оптимизировать и быстрее исполнять.
Минимальный интерпретатор арифметики
Соберу законченный tree-walking интерпретатор выражений. Парсинг я переиспользую из стандартного модуля ast, а сам обход дерева пишу руками — это и есть ядро интерпретации.
import ast
import operator
OPS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
}
def interpret(expr: str):
tree = ast.parse(expr, mode="eval").body # шаг парсера: строим AST
def ev(node): # шаг выполнения: обходим дерево
if isinstance(node, ast.Constant):
return node.value
if isinstance(node, ast.BinOp):
left = ev(node.left)
right = ev(node.right)
return OPS[type(node.op)](left, right)
raise ValueError("неизвестный узел")
return ev(node=tree)
print(interpret("2 + 3 * 4")) # ожидаю 14, а не 20: приоритет умножения задан деревом
В консоли появится 14. Обратите внимание: порядок действий нигде не прописан в моем коде — его задает структура AST, где умножение оказалось глубже сложения. Именно поэтому этап парсинга так важен: он фиксирует смысл до выполнения.
Если подать сломанное выражение, разбор упадет уже на этапе парсера:
print(interpret("2 +"))
SyntaxError: invalid syntax
Исправление — подавать синтаксически полное выражение, например 2 + 0. Ошибка возникла на этапе парсинга, до вычислений: часть ошибок интерпретатор ловит при разборе, часть уже во время исполнения.
Байткод и виртуальная машина на примере CPython
Настоящий Python не обходит AST напрямую — он компилирует его в байткод, а исполняет байткод стековая виртуальная машина. Увидеть байткод можно модулем dis.
import dis
def add(a, b):
return a + b
dis.dis(add)
2 RESUME 0
3 LOAD_FAST a
LOAD_FAST b
BINARY_OP 0 (+)
RETURN_VALUE
Каждая строка — инструкция байткода. LOAD_FAST кладет переменную на стек, BINARY_OP берет два верхних значения и складывает, RETURN_VALUE возвращает результат. Виртуальная машина CPython в цикле читает эти инструкции одну за другой и меняет стек. Точный набор опкодов и оформление вывода зависят от версии CPython, поэтому у себя вывод может отличаться в деталях.
Байткод модуля Python кешируется на диск в каталоге __pycache__ (файлы .pyc), чтобы не разбирать исходник заново при импорте. Это по-прежнему не машинный код процессора — его исполняет виртуальная машина.
JIT: компиляция во время исполнения
Чистая интерпретация байткода удобна, но проигрывает в скорости заранее скомпилированному машинному коду. JIT сокращает разрыв: среда наблюдает за выполнением и переводит горячие участки в машинный код на лету.
- JavaScript, V8 (Chrome, Node.js). Движок сначала гонит байткод через интерпретатор Ignition, а часто исполняемые функции оптимизирующие компиляторы (Maglev, TurboFan) переводят в машинный код. Если предположения оптимизации нарушаются, движок делает деоптимизацию и возвращается к байткоду.
- PHP (Zend Engine). Исходник компилируется в опкоды, OPcache их кеширует; с PHP 8.0 доступен JIT для горячего кода.
- Ruby (CRuby). Байткод исполняет виртуальная машина YARV; YJIT добавляет JIT-компиляцию.
- Python (CPython). Исторически JIT в стандартной сборке не было; экспериментальный JIT появился в недавних версиях и по умолчанию выключается. Точные номера версий и статус стоит сверять по официальным заметкам о релизе.
Вывод: деление на «интерпретируемые» и «компилируемые» языки условно. У реальных движков внутри и разбор кода, и байткод, и JIT одновременно.
Интерпретатор и компилятор: сравнение
Ниже сводка отличий. Речь про чистые случаи AOT-компилятора и интерпретатора байткода; JIT-среды сочетают свойства обеих колонок.
| Признак | Компилятор (AOT) | Интерпретатор |
|---|---|---|
| Когда переводится код | целиком до запуска | по частям во время запуска |
| Результат | отдельный машинный файл | отдельного файла нет |
| Нужен ли инструмент при запуске | нет, файл самостоятелен | да, нужен сам интерпретатор |
| Скорость выполнения | обычно выше | обычно ниже (сглаживает JIT) |
| Старт после правки кода | нужна пересборка | запуск сразу, без сборки |
| Где искать ошибку | много ошибок на этапе сборки | часть ошибок только при исполнении |
| Переносимость | под каждую платформу свой бинарник | один исходник на разных платформах |
Плюсы и минусы интерпретаторов
Сильные стороны:
- Быстрый цикл правка-запуск: сборку ждать не нужно, удобно для разработки и обучения.
- Переносимость: один исходник работает везде, где есть интерпретатор нужной версии.
- Гибкость: интерактивная оболочка (REPL), выполнение кода, собранного на лету, простая пошаговая отладка.
Ограничения:
- Скорость обычно ниже, чем у заранее скомпилированного машинного кода; JIT сокращает, но не всегда убирает разрыв.
- Для запуска нужен установленный интерпретатор подходящей версии.
- Исходный код по умолчанию распространяется в открытом виде.
Утверждения про «меньше памяти» или «всегда медленнее» из старых обзоров обманчивы: реальные потребление и скорость зависят от движка, версии и наличия JIT, а не от самого факта интерпретации.
Выводы
- Интерпретатор выполняет код во время запуска и не создает отдельный машинный файл; компилятор переводит программу в машинный код заранее.
- Интерпретация проходит по шагам: лексер режет текст на токены, парсер строит AST, дальше идет обход дерева или исполнение байткода виртуальной машиной.
- JIT — это гибрид: горячие участки компилируются в машинный код на лету (V8, PHP 8, Ruby YJIT).
- «Интерпретируемый язык» — характеристика реализации, а не языка: CPython, V8, Zend и CRuby совмещают байткод и JIT.
- Деление языков на компилируемые и интерпретируемые условно; смотреть надо на конкретный движок и его версию.
Где применяется / связь с практикой
Понимание того, как интерпретатор разбирает и исполняет код, помогает читать сообщения об ошибках (что упало на разборе, а что при выполнении), осознанно пользоваться REPL и модулем dis, объяснять разницу в скорости между Python и C. Python — удобная точка входа именно потому, что интерпретатор дает мгновенный цикл правка-запуск и интерактивную оболочку.
Освойте тему на практике
Если хочется системно освоить язык и заодно разобраться, как устроено его выполнение, посмотрите курс Python Basic — там теория подкреплена практикой. Проверить формат и уровень подачи можно на бесплатных открытых уроках перед стартом.
FAQ
Python компилируется или интерпретируется?
И то, и другое. CPython сначала компилирует исходник в байткод (файлы .pyc), а затем исполняет байткод виртуальной машиной. Отдельного машинного файла под процессор при этом не создается.
Чем байткод отличается от машинного кода?
Машинный код исполняет процессор напрямую, он привязан к архитектуре. Байткод — это инструкции для виртуальной машины, они переносимы, но их исполняет программа-интерпретатор, а не железо.
Всегда ли интерпретируемый код медленнее компилируемого?
Не всегда и не во столько раз, как кажется. JIT в V8, PHP и Ruby компилирует горячие участки в машинный код, а на задачах ввода-вывода разница часто незаметна. Точную оценку дает только замер на конкретной нагрузке.



