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

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

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

py_compile и compileall: компилируем Python сами

Два инструмента стандартной библиотеки компилируют код без запуска: py_compile — один файл, compileall — целый каталог. Собираем pyc вручную и ловим ошибки компиляции.

Редакция Питоники

В первом уроке мы выяснили, что компиляция происходит при каждом запуске и импорте, а второй урок разобрал устройство pyc-файла. Остался вопрос практики: когда компилировать нужно самим. Ответов три: проверка синтаксиса большого проекта без запуска ни одной строки, прогрев кэша перед выкладкой на сервер и сборка дистрибутива, где pyc едут вместе с исходниками. Компиляция без запуска — это проверка синтаксиса всего проекта за секунды: ни одна строка кода не исполнилась. Стандартная библиотека закрывает всё это двумя модулями: py_compile компилирует один файл, compileall — каталог целиком.

py_compile: один файл

Функция py_compile.compile принимает путь к исходнику, компилирует его и пишет pyc туда же, куда его положил бы обычный импорт — в __pycache__ рядом. Возвращаемое значение — путь к созданному файлу:

компиляция одного файла
import glob
import os
import py_compile

with open('tool_demo.py', 'w', encoding='utf-8') as f:
    f.write('def run():\n    return 1 + 1\n')

pyc = py_compile.compile('tool_demo.py')
print('исходник на месте:', os.path.exists('tool_demo.py'))
print('pyc создан:', os.path.exists(pyc))
print('компилятор вернул строку-путь:', isinstance(pyc, str))

for p in glob.glob('__pycache__/*.pyc'):
    os.remove(p)
if os.path.isdir('__pycache__'):
    os.rmdir('__pycache__')
os.remove('tool_demo.py')
Вывод
исходник на месте: True
pyc создан: True
компилятор вернул строку-путь: True

Три факта о поведении: компиляция не трогает исходник, результат действительно появился и путь вернулся строкой. Имя файла внутри __pycache__ содержит метку версии интерпретатора, поэтому мы проверяем существование, а не печатаем путь: на CPython 3.12 и 3.13 хвосты имени разные. Если имя нужно своё — есть параметр cfile.

свой путь результата через cfile
import os
import py_compile

with open('pack_demo.py', 'w', encoding='utf-8') as f:
    f.write('VERSION = 1\n')

py_compile.compile('pack_demo.py', cfile='pack_compiled.pyc')
print('компиляция по своему пути сработала:', os.path.exists('pack_compiled.pyc'))

os.remove('pack_compiled.pyc')
os.remove('pack_demo.py')
Вывод
компиляция по своему пути сработала: True

Параметр cfile отправил результат по указанному пути — файл появился в текущем каталоге, а не в __pycache__. Так собирают pyc для дистрибутивов с заранее известной структурой каталогов. Назвали файл сами — значит, и метку версии в имени можно не тащить: интерпретатор всё равно сверит magic-число из заголовка (пятый урок).

Ошибки компиляции: doraise

Главный сценарий ручной компиляции — проверка кода без запуска. Файл с ошибкой синтаксиса компилятор замечает мгновенно, но по умолчанию py_compile не считает это ЧП: он печатает ошибку в поток ошибок и возвращает None. Чтобы ошибка стала исключением, включают doraise=True:

ловим ошибку компиляции
import os
import py_compile

with open('cracked_demo.py', 'w', encoding='utf-8') as f:
    f.write('x = (1 +\n')

try:
    py_compile.compile('cracked_demo.py', doraise=True)
    print('скомпилировалось без ошибок')
except py_compile.PyCompileError:
    print('поймали PyCompileError: файл с ошибкой')

os.remove('cracked_demo.py')
Вывод
поймали PyCompileError: файл с ошибкой

Незакрытая скобка превратилась в исключение PyCompileError, которое скрипт ловит и обрабатывает: так строятся проверки всего проекта в CI. Без doraise тот же файл проскочил бы молча — функция вернула None, и если код не проверяет возвращаемое значение, битый файл уехал бы в продакшен незамеченным.

compileall: каталог целиком

Для проекта из десятков файлов вызывать py_compile по одному — мучение. compileall.compile_dir проходит каталог рекурсивно и компилирует каждый .py файл. Возвращаемое значение — True, если всё скомпилировалось; посчитать результат можно любым способом, здесь — количеством pyc в кэше:

компиляция каталога
import compileall
import glob
import os
import shutil

shutil.rmtree('pkg_demo', ignore_errors=True)
os.makedirs('pkg_demo')
for name in ('alpha', 'beta', 'gamma'):
    with open('pkg_demo/' + name + '.py', 'w', encoding='utf-8') as f:
        f.write('N = 1\n')

result = compileall.compile_dir('pkg_demo', quiet=1)
print('каталог скомпилирован:', result)
print('pyc собрано:', len(glob.glob('pkg_demo/__pycache__/*.pyc')))

shutil.rmtree('pkg_demo')
Вывод
каталог скомпилирован: True
pyc собрано: 3

Три модуля — три pyc, по одному на каждый исходник. Параметр quiet=1 убрал стандартный дот-репорт компилятора (по точке на файл), чтобы вывод остался читаемым. Когда понадобится перекомпилировать всё принудительно, даже если кэш кажется свежим, — параметр force=True: по умолчанию compileall пропускает файлы, у которых pyc уже актуален.

компиляция одного файла силами compileall
import compileall
import glob
import os
import shutil

os.makedirs('single_demo', exist_ok=True)
with open('single_demo/report.py', 'w', encoding='utf-8') as f:
    f.write('M = 2\n')

ok = compileall.compile_file('single_demo/report.py', quiet=1)
print('одиночный файл скомпилирован:', ok)
print('pyc в кэше модуля:', len(glob.glob('single_demo/__pycache__/*.pyc')))

shutil.rmtree('single_demo')
Вывод
одиночный файл скомпилирован: True
pyc в кэше модуля: 1

У compileall есть и функция для одного файла — compile_file. Она удобна, когда скрипт уже импортирует compileall, а отдельный py_compile ради одного вызова тянуть не хочется. Имя __pycache__ внутри single_demo — то же правило: кэш живёт рядом с исходником.

Командная строка

Оба модуля работают и из консоли — как скрипты через python -m. Это форма, в которой их чаще всего и используют в инструкциях по сборке:

компиляция из командной строки
python -m py_compile app.py
python -m compileall мой_проект/
Командная строка недоступна в браузерной песочнице — команды выполняются на своей машине. python -m py_compile молчит при успехе и падает при ошибке компиляции; python -m compileall печатает дот-репорт и создаёт __pycache__ во всём дереве каталога.
несколько файлов одной командой
python -m py_compile app.py settings.py utils.py
Командная строка недоступна в песочнице. py_compile в консоли принимает несколько файлов за раз; при успехе молчит, при ошибке компиляции падает с traceback на первом же битом файле — имена файлов перечисляются через пробел.
тихий режим compileall в консоли
python -m compileall -q мой_проект/
Командная строка недоступна в песочнице. Флаг -q убирает дот-репорт: команда печатает только итоговую строку со счётчиком скомпилированных файлов — удобно в скриптах сборки.
ИнструментОбъектТипичный вызовОсобенность
py_compileОдин файлpy_compile.compile('app.py', doraise=True)Ошибки — только с doraise
compileallКаталог рекурсивноcompileall.compile_dir('pkg', quiet=1)Пропускает свежий кэш; force=True пересобирает
python -m py_compileОдин файл из консолиpython -m py_compile app.pyМолчит при успехе
python -m compileallКаталог из консолиpython -m compileall .Печатает дот-репорт
  • py_compile.compile компилирует один файл и возвращает путь к pyc в __pycache__;
  • cfile задаёт свой путь результата — удобно для сборки дистрибутивов;
  • doraise=True превращает SyntaxError в PyCompileError: без него ошибка печатается в stderr и функция возвращает None;
  • compileall.compile_dir компилирует каталог рекурсивно, compile_file — один файл, force=True игнорирует свежий кэш;
  • из консоли работают обе формы: python -m py_compile и python -m compileall.

Ручная компиляция — про кэш и проверку синтаксиса. Дальше по разделу: как прочитать байткод глазами — дизассемблер dis в четвёртом уроке, почему pyc строго привязан к версии интерпретатора — пятый урок про magic-число, а про то, как интерпретатор сам управляет этим кэшем — шестой урок про __pycache__.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

import py_compile

with open('quiz_mod.py', 'w', encoding='utf-8') as f:
    f.write('x = (2 +\n')
try:
    py_compile.compile('quiz_mod.py', doraise=True)
    print('ok')
except py_compile.PyCompileError:
    print('fail')
import compileall, glob, os
os.makedirs('quiz_pkg', exist_ok=True)
for name in ('a', 'b'):
    with open('quiz_pkg/' + name + '.py', 'w', encoding='utf-8') as f:
        f.write('N = 1\n')
compileall.compile_dir('quiz_pkg', quiet=1)
print(len(glob.glob('quiz_pkg/__pycache__/*.pyc')))
import shutil
shutil.rmtree('quiz_pkg')
import py_compile
res = py_compile.compile('no_such_file.py', doraise=True)
print('готово')
Проверь себя
0 / 7

1. Куда py_compile.compile кладёт результат по умолчанию?

2. Что делает doraise=True в py_compile.compile?

3. Чем compileall.compile_dir отличается от py_compile.compile?

4. compileall пропустил файл при повторном запуске. Почему?

5. Как скомпилировать каталог проекта из консоли?

6. Исполняет ли py_compile.compile код из файла?

7. Зачем прогревают кэш командой python -m compileall перед деплоем?

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

Скомпилируйте файл notes.py с содержимым TITLE = 8 по своему пути build_notes.pyc (параметр cfile) и напечатайте, что файл собран. За собой уберите и pyc, и исходник.

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

Как скомпилировать Python-файл в pyc?

Один файл — py_compile.compile('app.py') из стандартной библиотеки или в консоли python -m py_compile app.py. Каталог целиком — compileall.compile_dir('проект/') или python -m compileall. Результат — pyc в __pycache__ рядом с исходником.

Урок 2: устройство pyc-файла

Как проверить синтаксис Python-файла, не запуская его?

Скомпилировать: py_compile.compile('app.py', doraise=True) — код не исполняется, но ошибки синтаксиса поднимутся как PyCompileError. Для всего проекта — compileall.compile_dir с doraise: ни одна строка не выполнится.

Зачем запускать python -m compileall перед выкладкой?

Это прогрев кэша: все модули компилируются один раз на машине сборки, и на сервере импорты сразу идут по готовым pyc. Критично на холодном старте и там, где каталоги read-only и записать pyc на месте невозможно.

py_compile.compile молча вернул None — что не так?

Файл содержит SyntaxError: без doraise=True py_compile печатает ошибку в stderr и возвращает None, не бросая исключений. Добавьте doraise=True — получите PyCompileError, который можно поймать и показать.

Можно ли распространять программу как набор pyc вместо исходников?

Технически да — pyc той же версии Python импортируется без исходника. Но это не защита: декомпиляторы для каждой версии восстанавливают большую часть кода. Для сборки самостоятельных программ есть PyInstaller и Nuitka — им посвящены уроки 10-12.

Урок 10: PyInstaller

Понравился урок? Сошлитесь на него

«Компиляция без запуска — это проверка синтаксиса всего проекта за секунды: ни одна строка кода не исполнилась.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

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

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

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