Интерпретатор, байт-код и .pyc
Интерпретатор, байт-код и .pyc
В прошлом уроке мы запустили python3 hello.py и получили вывод. На этом разница между «исходником» и «выполнением» закончилась - всё произошло автоматически. В этом уроке заглянем под капот: что именно делает CPython между чтением файла и появлением результата в терминале.
Не интерпретатор и не компилятор
Распространённое заблуждение - что Python «просто исполняет код построчно». На самом деле этапов несколько:
- Лексический анализ - исходник разбивается на токены (ключевые слова, идентификаторы, литералы)
- Парсинг - токены складываются в AST (Abstract Syntax Tree)
- Компиляция в байт-код - AST превращается в инструкции для виртуальной машины
- Исполнение байт-кода - CPython VM выполняет инструкции
Этапы 1-3 происходят один раз и закешированы. Этап 4 - это и есть то, что мы обычно называем «выполнением Python-программы».
Смотрим байт-код вживую
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 компилирует его в байт-код и сохраняет рядом:
Имя файла кодирует версию интерпретатора (cpython-312 = CPython 3.12). Это важно: байт-код Python 3.11 и 3.12 не совместим, поэтому файлы версионированы.
Когда CPython проверяет нужно ли пересобирать байт-код, он сравнивает время модификации .py и .pyc. Если исходник свежее - перекомпилирует. Поэтому при разработке можно не думать об этом кеше.
Shebang в Unix-скриптах
На Linux и macOS Python-скрипты можно запускать как обычные программы:
./hello.py
Для этого нужны две вещи:
- Shebang - первая строка файла указывает интерпретатор:
#!/usr/bin/env python3
print("Hello, World!")
- Права на исполнение:
chmod +x hello.py
./hello.py
В 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 однопоточный» - упрощение; реальность чуть сложнее.
Мини-задание
- Создай
dis_demo.py:
import dis
def square(x):
return x * x
dis.dis(square)
Запусти и изучи вывод. Найди инструкцию BINARY_OP.
- Создай
module_test.py:
def hello():
return "module says hello"
print(f"Файл выполняется напрямую: __name__ = {__name__}")
Запусти его: python3 module_test.py. Затем в REPL импортируй его: import module_test. Сравни вывод.
- Запусти встроенный HTTP-сервер в текущей директории:
python3 -m http.server 8000
Открой http://localhost:8000 в браузере. Это пример как -m запускает целый модуль как программу.
Что дальше
Поняли как CPython компилирует и исполняет код, и как структурировать файл чтобы он работал и как скрипт, и как модуль. В следующем уроке - виртуальные окружения: зачем они нужны, как создать и почему ни один проект не должен ставить пакеты в системный Python.