Проект: standalone-инструмент от исходника до pyz-архива
Финальный проект раздела: инструмент отчёта по продажам проходит путь от исходника до проверенного tasks.pyz. Проверка компиляции, zipapp с __main__.py, запуск архива, инвентаризация содержимого и чек-лист сборки.
Редакция Питоники
Финальный проект собирает раздел в одну рабочую цепочку. Инструмент — отчёт по продажам: список сделок, сумма и лучшая сделка. Требования к сборке жёсткие: синтаксис проверен до упаковки, архив запускается сам, входная точка на месте, лишнего внутри нет — и весь путь повторяем от нуля за один прогон. Всё уже было по кусочкам: py_compile из урока 3, zipapp из урока 8, проверки существования из всех предыдущих уроков. Осталось соединить и проверить.
| Шаг | Что делаем | Инструмент |
|---|---|---|
| 1 | Исходник tool.py и проверка компиляции | py_compile с doraise |
| 2 | Репетиция логики на живых данных | Чистый запуск функций |
| 3 | Сборка tasks.pyz с __main__.py и запуск | zipapp + runpy |
| 4 | Инвентаризация собранного архива | zipfile |
| 5 | Сборка как повторяемая функция | Одна функция build_pyz |
Шаг 1: исходник и проверка компиляции
Синтаксическая ошибка, обнаруженная заказчиком на чистой машине, стоит дороже всех сборок мира — поэтому проверка компиляции идёт первой. py_compile.compile с флагом doraise=True превращает ошибку компиляции в исключение PyCompileError, которое мы ловим и превращаем в честный вердикт:
import os
import shutil
import py_compile
BUILD = 'build'
if os.path.exists(BUILD):
shutil.rmtree(BUILD)
os.makedirs(BUILD)
tool_source = '''SALES = [120, 90, 240, 60, 310]
def summarize(values):
total = sum(values)
best = max(values)
return total, best
def report():
total, best = summarize(SALES)
print('продаж всего:', total)
print('лучшая сделка:', best)
'''
with open(os.path.join(BUILD, 'tool.py'), 'w', encoding='utf-8') as f:
f.write(tool_source)
check_file = os.path.join(BUILD, 'tool-checked.pyc')
ok = True
try:
py_compile.compile(os.path.join(BUILD, 'tool.py'), cfile=check_file, doraise=True)
except py_compile.PyCompileError:
ok = False
print('исходник записан:', os.path.exists(os.path.join(BUILD, 'tool.py')))
print('синтаксис в порядке:', ok)
print('проверочный pyc на месте:', os.path.exists(check_file))
shutil.rmtree(BUILD)
print('каталог build убран за собой:', not os.path.exists(BUILD))
исходник записан: True синтаксис в порядке: True проверочный pyc на месте: True каталог build убран за собой: True
Три проверки — три True, и каталог после проверки убран. Деталь, которая отличает грамотную сборку: параметр cfile задаёт проверочному файлу собственное имя. Без него Python положил бы результат в __pycache__ под именем с меткой версии — нам же важно не имя, а факт: компиляция прошла, синтаксис годен. Проверочный pyc после себя мы не сохраняем: настоящий архив соберёт свои исходники сам.
python -m py_compile tool.py
python -m zipapp build -o tasks.pyz
python tasks.pyz
Шаг 2: репетиция логики
Прежде чем паковать, логику проверяют вживую: сборка сама по себе ничего не проверяет — она честно пакует и хорошее, и сломанное. Функции инструмента маленькие, и их репетиция — просто вызов с живыми данными:
SALES = [120, 90, 240, 60, 310]
def summarize(values):
total = sum(values)
best = max(values)
return total, best
total, best = summarize(SALES)
print('продаж всего:', total)
print('лучшая сделка:', best)
продаж всего: 820 лучшая сделка: 310
Восемьсот двадцать и триста десять — ожидаемый отчёт. Эти же две строки появятся из архива на шаге 3: совпадение вывода до и после упаковки — ещё одна проверка, уже смысловая. Логика годна, пакуем.
Шаг 3: сборка tasks.pyz с __main__.py
zipapp из урока 8 превращает каталог в запускаемый zip. Два условия работоспособности: внутри обязан лежать __main__.py — входная точка, которую исполняет запуск архива, — и модули, которые он импортирует, должны быть рядом, чтобы Python нашёл их внутри через zipimport. Собираем и сразу запускаем:
import os
import shutil
import zipapp
import runpy
BUILD = 'build'
PYZ = 'tasks.pyz'
for stale in (BUILD, PYZ):
if os.path.isdir(stale):
shutil.rmtree(stale)
elif os.path.exists(stale):
os.remove(stale)
os.makedirs(BUILD)
tool_source = '''SALES = [120, 90, 240, 60, 310]
def summarize(values):
total = sum(values)
best = max(values)
return total, best
def report():
total, best = summarize(SALES)
print('продаж всего:', total)
print('лучшая сделка:', best)
'''
main_source = '''import tool
tool.report()
print('готово: tasks.pyz работает')
'''
with open(os.path.join(BUILD, 'tool.py'), 'w', encoding='utf-8') as f:
f.write(tool_source)
with open(os.path.join(BUILD, '__main__.py'), 'w', encoding='utf-8') as f:
f.write(main_source)
zipapp.create_archive(BUILD, target=PYZ)
print('архив создан:', os.path.exists(PYZ))
print()
print('запуск tasks.pyz:')
runpy.run_path(PYZ, run_name='__main__')
shutil.rmtree(BUILD)
os.remove(PYZ)
print('рабочие файлы убраны:', not os.path.exists(PYZ) and not os.path.exists(BUILD))
архив создан: True запуск tasks.pyz: продаж всего: 820 лучшая сделка: 310 готово: tasks.pyz работает рабочие файлы убраны: True
Вывод совпал с репетицией: отчёт тот же, и финальная строка подтверждает, что работа шла именно из архива. runpy.run_path — программный эквивалент команды python tasks.pyz: он открывает архив, находит __main__.py и исполняет его, добавив содержимое архива в путь поиска модулей — поэтому import tool находит tool.py внутри. Файлы с артефактами убраны за собой: чистая песочница — то же правило, что и чистая сборка.
Шаг 4: инвентаризация собранного
Запуск показал, что архив работает; инвентаризация показывает, из чего он состоит. Читаем содержимое через zipfile и проверяем три факта: входная точка внутри, модуль внутри, мусора внутри нет. Список сортируем — содержимое архива и так стабильное, а сортировка делает отчёт независимым от порядка файлов на диске:
import os
import shutil
import zipfile
import zipapp
BUILD = 'build'
PYZ = 'tasks.pyz'
if os.path.isdir(BUILD):
shutil.rmtree(BUILD)
if os.path.exists(PYZ):
os.remove(PYZ)
os.makedirs(BUILD)
with open(os.path.join(BUILD, 'tool.py'), 'w', encoding='utf-8') as f:
f.write('''SALES = [120, 90, 240, 60, 310]
def summarize(values):
total = sum(values)
best = max(values)
return total, best
def report():
total, best = summarize(SALES)
print('продаж всего:', total)
print('лучшая сделка:', best)
''')
with open(os.path.join(BUILD, '__main__.py'), 'w', encoding='utf-8') as f:
f.write('''import tool
tool.report()
''')
zipapp.create_archive(BUILD, target=PYZ)
with zipfile.ZipFile(PYZ) as z:
contents = sorted(z.namelist())
print('состав tasks.pyz:')
for name in contents:
print(' -', name)
print('entrypoint __main__.py внутри:', '__main__.py' in contents)
print('модуль tool.py внутри:', 'tool.py' in contents)
print('лишних pyc внутри:', any(name.endswith('.pyc') for name in contents))
shutil.rmtree(BUILD)
os.remove(PYZ)
print('инвентаризация окончена, сборка убрана:', not os.path.exists(PYZ) and not os.path.exists(BUILD))
состав tasks.pyz: - __main__.py - tool.py entrypoint __main__.py внутри: True модуль tool.py внутри: True лишних pyc внутри: False инвентаризация окончена, сборка убрана: True
Состав архива ровно тот, что задуман: два файла, входная точка и модуль, ни одного pyc. Проверка лишних pyc — не паранойя, а правило: в каталоге сборки легко оказываются старые артефакты, и zipapp честно упакует их тоже. Отчёт об инвентаризации — документ, который отправляется вместе с архивом: получатель видит состав до распаковки.
Шаг 5: сборка как повторяемая функция
Ручная цепочка хороша один раз. Настоящая сборка — это функция: передал исходники, получил архив, и никакого ручного состояния между запусками. Сборка должна быть повторяемой: одна функция, чистый каталог, один файл на выходе.
import os
import shutil
import zipapp
def build_pyz(folder, target):
if os.path.isdir(folder):
shutil.rmtree(folder)
os.makedirs(folder)
with open(os.path.join(folder, 'tool.py'), 'w', encoding='utf-8') as f:
f.write('''def main():
print('инструмент готов')
''')
with open(os.path.join(folder, '__main__.py'), 'w', encoding='utf-8') as f:
f.write('''import tool
tool.main()
''')
zipapp.create_archive(folder, target=target)
shutil.rmtree(folder)
return os.path.exists(target)
print('сборка прошла:', build_pyz('build', 'demo.pyz'))
print('каталог build убран функцией:', not os.path.isdir('build'))
os.remove('demo.pyz')
print('архив demo.pyz убран после проверки:', not os.path.exists('demo.pyz'))
сборка прошла: True каталог build убран функцией: True архив demo.pyz убран после проверки: True
Функция начинается с чистки каталога — это делает её безопасной для повторного запуска: старые артефакты не переживают сборку. Возврат os.path.exists — булев факт успеха, по которому вызывающий код решает, что дальше. Именно так выглядит сборка в автоматизации: непрерывная интеграция вызывает одну функцию и по True или False решает судьбу сборки.
Проверка pyc без исходника
Последняя проверка из арсенала раздела — убедиться, что скомпилированный файл читается сам по себе, без исходника. Заголовок pyc занимает шестнадцать байт: magic-число версии, флаги, метка времени и размер исходника; за ними — marshal-блок кодового объекта из урока 7. Срез откусывает заголовок, marshal возвращает объект:
import marshal
with open('build/tool-checked.pyc', 'rb') as f:
data = f.read()
code = marshal.loads(data[16:])
print(type(code).__name__)
print('имена внутри:', code.co_names)
Кодовый объект вернулся из байтов целиком — с именами функций и константами. Если такое чтение получилось, pyc собран корректно, и декомпиляция из урока 15 с ним справится тем более. Для сборки это финальная точка: формат подтверждён.
Финальный чек-лист сборки
- исходник компилируется: py_compile с doraise прошёл без PyCompileError;
- репетиция сделана: функции дали ожидаемый вывод до упаковки;
- входная точка на месте: __main__.py лежит в корне архива;
- модули рядом: импорты __main__.py находят файлы внутри архива;
- мусора нет: ни pyc, ни __pycache__, ни черновиков внутри архива;
- архив запускается: python tasks.pyz печатает ожидаемый отчёт;
- сборка повторяема: одна функция, чистый каталог, один файл на выходе;
- рабочие файлы убраны: остался только tasks.pyz и ничего лишнего.
Раздел пройден весь: от первого байткода в уроке 1 до собранного и проверенного архива. На руках — рабочая цепочка сборки: проверка компиляции, репетиция, zipapp с __main__.py, инвентаризация и чек-лист, который переживёт любой следующий проект. Дальше сборка понадобится сразу: исключения и тесты в pytest — темы, с которыми инструменты собирают по-настоящему надёжными.
Сначала предскажи ответ в голове — это главный навык программиста.
import zipfile
print('__main__.py' in ['tool.py', '__main__.py'])
print(sorted(['tool.py', '__main__.py']))
def summarize(values):
total = sum(values)
best = max(values)
return total, best
total, best = summarize([120, 90, 240])
print('продаж всего:', total)
print('лучшая сделка:', best)
files = ['tool.py', 'notes.txt', '__main__.py']
clean = [f for f in files if not f.endswith('.txt')]
print(sorted(clean))
print('мусорных txt нет:', not any(f.endswith('.txt') for f in clean))
1. Зачем в проекте py_compile с doraise=True до сборки?
2. Зачем проверочному pyc задают собственное имя через cfile?
3. Что делает runpy.run_path('tasks.pyz')?
4. Почему при инвентаризации список файлов сортируют?
5. О чём говорит проверка лишних pyc внутри архива?
6. Почему сборку оформляют функцией, а не последовательностью команд?
7. Чем tasks.pyz отличается от exe из урока про PyInstaller?
8. Что означает срез data[16:] при чтении pyc?
Соберите ядро инструмента: функция summarize(values) возвращает пару из суммы и максимума. Прогоните на списке [120, 90, 240] и напечатайте две строки отчёта.
Как собрать запускаемый pyz-архив из своего скрипта?
Положить скрипт и __main__.py с входной точкой в один каталог и вызвать zipapp.create_archive(каталог, target='tasks.pyz'). Запуск — python tasks.pyz; входная точка находит модули внутри архива через zipimport.
Урок 8: zipappЗачем проверять компиляцию, если архив всё равно соберётся?
Сборка пакует всё подряд: синтаксическая ошибка всплывёт у получателя при запуске. py_compile с doraise=True ловит её на шаге сборки и превращает в исключение PyCompileError — вердикт до упаковки.
Урок 3: py_compile и compileallКак убедиться, что в архив не попал мусор?
Инвентаризация через zipfile: читаете namelist, сортируете и проверяете три факта — __main__.py на месте, модули на месте, ни одного pyc или черновика. Начинать сборку с чистки каталога — правило, а не привычка.
Чем pyz-архив лучше и хуже exe?
pyz легче, собирается одним вызовом и переносится между системами с Python. Хуже тем, что Python на машине получателя должен быть установлен: автономный exe с интерпретатором внутри собирает PyInstaller.
Урок 10: PyInstallerКак сделать сборку повторяемой?
Завернуть в функцию: очистка каталога сборки в начале, запись исходников, zipapp, возврат os.path.exists как факта успеха. Один вызов — один результат, никаких ручных состояний между запусками; на этом строятся автоматические сборки.
Понравился урок? Сошлитесь на него
«Сборка должна быть повторяемой: одна функция, чистый каталог, один файл на выходе.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Компиляция · Урок 8
zipapp: скрипт-архив .pyz — программа в одном файле
Один файл вместо папки: собираем запускаемый zip-архив с точкой входа __main__.py, дописываем shebang и разбираем, когда .pyz достаточно без exe.
Компиляция · Урок 3
py_compile и compileall: компилируем Python сами
Два инструмента стандартной библиотеки компилируют код без запуска: py_compile — один файл, compileall — целый каталог. Собираем pyc вручную и ловим ошибки компиляции.
Компиляция · Урок 10
PyInstaller: сборка exe из Python-скрипта
От скрипта к exe: PyInstaller анализирует импорты, кладёт интерпретатор, байткод и данные в один файл. Собираем бандл по шагам и проверяем его на чистой машине.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
pytest · Урок 20
Финальный проект: полный тест-сьют модуля работы с заказами
Выпускной: модуль заказов со скидками, классом Order, JSON и валютой — и полный сьют из шестнадцати тестов, который держит его весь целиком и гоняется одной командой.
pytest финальный проектpytest проект пример
json · Урок 20
Проект: каталог книг — от URL до файла
Финал раздела: один скрипт собирает каталог книг — параметры в URL, разбор ответа, фильтр по рейтингу, сортировка по цене и проверенный файл на диске.
python проект json api каталогpython проект пример для начинающих
Компиляция · Урок 12
Nuitka: компиляция Python-кода в C
От упаковки к компиляции: как Nuitka переводит программу в C, что становится машинным кодом, а что остаётся PyObject — и где ждать ускорения.
nuitka standalone