Интерпретатор, байт-код и .pyc

Интерпретатор, байт-код и .pyc

В прошлом уроке мы запустили python3 hello.py и получили вывод. На этом разница между «исходником» и «выполнением» закончилась - всё произошло автоматически. В этом уроке заглянем под капот: что именно делает CPython между чтением файла и появлением результата в терминале.

Не интерпретатор и не компилятор

Распространённое заблуждение - что Python «просто исполняет код построчно». На самом деле этапов несколько:

  1. Лексический анализ - исходник разбивается на токены (ключевые слова, идентификаторы, литералы)
  2. Парсинг - токены складываются в AST (Abstract Syntax Tree)
  3. Компиляция в байт-код - AST превращается в инструкции для виртуальной машины
  4. Исполнение байт-кода - CPython VM выполняет инструкции

Этапы 1-3 происходят один раз и закешированы. Этап 4 - это и есть то, что мы обычно называем «выполнением Python-программы».

Понимание этой схемы помогает интерпретировать ошибки и поведение. Например, `SyntaxError` возникает на этапе 2 (парсинг) - программа даже не начала исполняться. А `NameError` или `ZeroDivisionError` - на этапе 4, когда конкретная строка выполняется. Про иерархию исключений и их обработку - в уроке про [try/except](./15-errors-exceptions.md).

Смотрим байт-код вживую

Python даёт инструмент для просмотра скомпилированного байт-кода - модуль dis:

import dis

def add(a, b):
    return a + b

dis.dis(add)

Запусти этот файл - получишь что-то вроде:

  4           RESUME                   0
              LOAD_FAST                0 (a)
              LOAD_FAST                1 (b)
              BINARY_OP                0 (+)
              RETURN_VALUE

Это и есть инструкции виртуальной машины: загрузи a в стек, загрузи b, применить бинарную операцию +, вернуть результат. Похоже на ассемблер, только для CPython VM, а не для процессора.

Файлы .pyc и кеш

При первом импорте модуля CPython компилирует его в байт-код и сохраняет рядом:

Структура проекта с pycache: myproject/ содержит main.py, utils.py и директорию pycache/ со скомпилированными .pyc файлами

Имя файла кодирует версию интерпретатора (cpython-312 = CPython 3.12). Это важно: байт-код Python 3.11 и 3.12 не совместим, поэтому файлы версионированы.

Файлы `.pyc` - бинарные, в них нет смысла лезть руками. Если что-то сломалось - проще удалить всю папку `__pycache__/`. CPython пересоздаст её при следующем запуске.

Когда CPython проверяет нужно ли пересобирать байт-код, он сравнивает время модификации .py и .pyc. Если исходник свежее - перекомпилирует. Поэтому при разработке можно не думать об этом кеше.

Shebang в Unix-скриптах

На Linux и macOS Python-скрипты можно запускать как обычные программы:

./hello.py

Для этого нужны две вещи:

  1. Shebang - первая строка файла указывает интерпретатор:
#!/usr/bin/env python3
print("Hello, World!")
  1. Права на исполнение:
chmod +x hello.py
./hello.py
Прямая ссылка вроде `#!/usr/bin/python3` хрупкая - на разных системах путь к интерпретатору отличается. `env` ищет `python3` в `PATH`, что работает универсально. Это стандартная практика для всех скриптовых языков на Unix.

В Windows shebang игнорируется (исключение - launcher py.exe, который умеет читать первую строку). Для кроссплатформенности скрипты обычно запускают явным python3 script.py.

name == "main"

Если в hello.py есть код вне функций - он выполнится при любом запуске. Это иногда нежелательно: при import hello мы не хотим запускать программу, нам нужны только её функции.

Идиома:

def greet(name):
    print(f"Привет, {name}!")

if __name__ == "__main__":
    greet("мир")

Когда файл запускается напрямую (python3 hello.py), переменная __name__ равна строке "__main__", и блок исполняется. Когда файл импортируется (import hello), __name__ равна "hello", и блок пропускается. Как устроены импорты и пакеты целиком - в отдельном уроке.

Это аналог func main() в Go, но не обязательный - просто идиома для скриптов, которые могут быть и исполняемыми, и импортируемыми.

Опции командной строки

CPython понимает много флагов. Самые полезные:

ФлагЧто делает
python3 script.pyЗапуск файла
python3 -c "print(1+1)"Выполнить одну строку кода
python3 -m module_nameЗапустить модуль как скрипт (например, python3 -m http.server)
python3 -i script.pyВыполнить скрипт и остаться в REPL с его контекстом
python3 -O script.pyОптимизация: убирает assert из байт-кода
python3 -VПоказать версию

Особенно полезен -m: запускает любой установленный модуль как программу. Например, python3 -m venv myenv создаёт виртуальное окружение, python3 -m pip install requests ставит пакет. Флаг -i пригодится при отладке - о нём в уроке про REPL и pdb.

Виртуальная машина и GIL

CPython VM однопоточная в строгом смысле: за раз исполняется только одна инструкция байт-кода. Параллелизм через threading ограничен GIL (Global Interpreter Lock) - блокировкой, которая защищает внутренние структуры VM от гонок.

Это не значит что Python не умеет в параллелизм. Это значит:

  • Для IO-bound задач (запросы, файлы) GIL отдаётся другим потокам на время блокировки - параллелизм работает
  • Для CPU-bound задач (вычисления) GIL мешает - используют multiprocessing или async

Подробнее разберём в Модуле 7: GIL и потоки и multiprocessing. Сейчас важно понимать: «Python однопоточный» - упрощение; реальность чуть сложнее.

Мини-задание

  1. Создай dis_demo.py:
import dis

def square(x):
    return x * x

dis.dis(square)

Запусти и изучи вывод. Найди инструкцию BINARY_OP.

  1. Создай module_test.py:
def hello():
    return "module says hello"

print(f"Файл выполняется напрямую: __name__ = {__name__}")

Запусти его: python3 module_test.py. Затем в REPL импортируй его: import module_test. Сравни вывод.

  1. Запусти встроенный HTTP-сервер в текущей директории:
python3 -m http.server 8000

Открой http://localhost:8000 в браузере. Это пример как -m запускает целый модуль как программу.

Что дальше

Поняли как CPython компилирует и исполняет код, и как структурировать файл чтобы он работал и как скрипт, и как модуль. В следующем уроке - виртуальные окружения: зачем они нужны, как создать и почему ни один проект не должен ставить пакеты в системный Python.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.