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

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

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

Проект: 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, которое мы ловим и превращаем в честный вердикт:

шаг 1: исходник и проверка компиляции
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
Команды терминала для настоящей машины: проверка компиляции, сборка архива из каталога build и запуск полученного tasks.pyz. В песочнице файлов с командами нет — вся цепочка ниже исполняется кодом на странице.

Шаг 2: репетиция логики

Прежде чем паковать, логику проверяют вживую: сборка сама по себе ничего не проверяет — она честно пакует и хорошее, и сломанное. Функции инструмента маленькие, и их репетиция — просто вызов с живыми данными:

шаг 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. Собираем и сразу запускаем:

шаг 3: сборка архива и запуск
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 и проверяем три факта: входная точка внутри, модуль внутри, мусора внутри нет. Список сортируем — содержимое архива и так стабильное, а сортировка делает отчёт независимым от порядка файлов на диске:

шаг 4: инвентаризация tasks.pyz
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: сборка как повторяемая функция

Ручная цепочка хороша один раз. Настоящая сборка — это функция: передал исходники, получил архив, и никакого ручного состояния между запусками. Сборка должна быть повторяемой: одна функция, чистый каталог, один файл на выходе.

шаг 5: build_pyz — сборка одной функцией
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 возвращает объект:

чтение pyc без исходника
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 с шага 1 сохранился: он печатает code и кортеж имён SALES, summarize, report. В песочнице блок не запускается — проверочный pyc там убран за собой сразу после проверки, а его байтовое содержимое зависит от версии Python.

Кодовый объект вернулся из байтов целиком — с именами функций и константами. Если такое чтение получилось, 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))
Проверь себя
0 / 8

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?

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

Соберите ядро инструмента: функция summarize(values) возвращает пару из суммы и максимума. Прогоните на списке [120, 90, 240] и напечатайте две строки отчёта.

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

Как собрать запускаемый 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

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

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

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