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

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

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

PyInstaller onefile против onedir: что выбрать

Один exe или exe с папкой _internal: как onefile и onedir влияют на старт, обновление, антивирусы и поиск файлов данных — с механикой бутстрапа на живом коде.

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

Сборка из прошлого урока дала exe и папку _internal рядом — это onedir, вариант по умолчанию. Флаг --onefile клеит всё содержимое в один исполняемый файл. Выбор кажется косметикой, но он меняет скорость старта, способ обновления, отношение антивирусов и даже то, где программа ищет свои файлы данных. Разберём обе формы изнутри.

onedir: всё уже на диске

В onedir exe — маленький стартер, а рядом лежит папка _internal: библиотеки, байткод, данные. Запуск мгновенный: распаковывать ничего не нужно, всё уже на месте. Соберём такую структуру и посмотрим на неё целиком:

структура onedir
import os, shutil

for d in ('dist/mytool', 'dist/mytool/_internal'):
    os.makedirs(d, exist_ok=True)
with open('dist/mytool/mytool.exe', 'wb') as f:
    f.write(b'MZ')
with open('dist/mytool/_internal/runtime.dll', 'wb') as f:
    f.write(b'runtime')
with open('dist/mytool/_internal/reports.dat', 'wb') as f:
    f.write(b'data')

shown = []
for root, dirs, files in os.walk('dist'):
    for name in files:
        shown.append(root.replace(os.sep, '/') + '/' + name)
print('\n'.join(sorted(shown)))
shutil.rmtree('dist')
Вывод
dist/mytool/_internal/reports.dat
dist/mytool/_internal/runtime.dll
dist/mytool/mytool.exe

Имена условные: в настоящей _internal сотни файлов с платформенными именами. Суть в другом: пользователь получает папку целиком, и переносить надо её всю — exe без _internal не работает.

onefile: один файл ценой распаковки

При сборке с --onefile всё содержимое приклеивается к exe. При каждом запуске бутстрап распаковывает содержимое во временную папку и запускает программу уже оттуда. Проиграем этот сценарий на чистом Python:

бутстрап onefile: распаковка и запуск
import zipfile, tempfile, os, shutil

with zipfile.ZipFile('mytool.exe', 'w') as z:
    z.writestr('__main__.py', "print('Отчёт готов: 3 позиции')\n")
    z.writestr('helper.py', "FACTOR = 2\n")

stage = tempfile.mkdtemp()
with zipfile.ZipFile('mytool.exe') as z:
    z.extractall(stage)

print('Распаковано во временную папку:', ', '.join(sorted(os.listdir(stage))))
entry = open(os.path.join(stage, '__main__.py'), encoding='utf-8').read()
exec(compile(entry, '__main__.py', 'exec'), {'__name__': '__main__'})
print('Временная папка ещё на месте:', os.path.exists(stage))

shutil.rmtree(stage)
os.remove('mytool.exe')
print('Бутстрап убрал за собой:', not os.path.exists(stage))
Вывод
Распаковано во временную папку: __main__.py, helper.py
Отчёт готов: 3 позиции
Временная папка ещё на месте: True
Бутстрап убрал за собой: True

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

сколько операций распаковки у каждой формы
import zipfile, os

with zipfile.ZipFile('mytool.exe', 'w') as z:
    for i in range(1, 5):
        z.writestr('mod%d.py' % i, 'x = %d\n' % i)

onedir_ops = 0
onefile_ops = 0
with zipfile.ZipFile('mytool.exe') as z:
    for info in z.infolist():
        z.read(info.filename)
        onefile_ops += 1
os.remove('mytool.exe')

print('Операций распаковки у onedir:', onedir_ops)
print('Операций распаковки у onefile:', onefile_ops)
Вывод
Операций распаковки у onedir: 0
Операций распаковки у onefile: 4

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

Критерийonefileonedir
РаспространениеОдин exeПапка с exe и _internal
СтартДольше: распаковка каждый разМгновенный
АнтивирусыПретензий большеПретензий меньше
ОбновлениеЗаменить один файлЗаменить папку целиком
ОтладкаСложно: всё скрытоВидно, что где лежит

Флаги, которые меняют поведение

Обе формы задаются флагами командной строки, а поведение — ещё несколькими ключами:

терминал: два способа сборки
pyinstaller main.py --onefile
pyinstaller main.py --onedir
Это команды терминала, поэтому блок не запускается в браузерной песочнице: сборка выполняется нативным PyInstaller на реальной машине.
терминал: полезные флаги
pyinstaller main.py --onefile --noconsole --icon app.ico --name mytool
pyinstaller main.py --add-data "config.json;."
Терминальные команды, в песочнице не выполняются. --noconsole убирает окно консоли у GUI-приложения, --name задаёт имя exe. Разделитель в --add-data на Windows — точка с запятой, на macOS и Linux — двоеточие.

Где программа: sys.frozen и путь к данным

Внутри собранного exe у модуля sys появляется атрибут frozen — так программа узнаёт, что она собрана, и может скорректировать пути. В песочнице этого атрибута нет, поэтому механику покажем на имитации собранной среды:

код для собранного приложения
import sys, os

def base_dir():
    if getattr(sys, 'frozen', False):
        return os.path.dirname(sys.executable)
    return os.path.dirname(os.path.abspath(__file__))
Атрибут sys.frozen существует только внутри exe от PyInstaller, поэтому блок не запускается в песочнице — рядом механика показана на имитации.
как exe находит своё
import os, sys

def base_dir(sysmod):
    if getattr(sysmod, 'frozen', False):
        return os.path.dirname(sysmod.executable)
    return 'папка со скриптом'

print('Обычный Python ищет данные:', base_dir(sys))

fake = type(sys)('sys')
fake.frozen = True
fake.executable = 'C:/apps/mytool.exe'
print('Собранный exe ищет данные:', base_dir(fake))
Вывод
Обычный Python ищет данные: папка со скриптом
Собранный exe ищет данные: C:/apps

А теперь классическая ловушка onefile: open с относительным путём. При разработке файл лежит рядом со скриптом и всё находится. В собранном onefile текущая папка другая, а данные — во временной папке распаковки:

относительный путь: разработка против onefile
import os, tempfile, shutil

os.makedirs('project', exist_ok=True)
with open('project/config.json', 'w', encoding='utf-8') as f:
    f.write('{"mode": "fast"}')

with open('project/config.json', encoding='utf-8') as f:
    print('При разработке найден:', f.read())

stage = tempfile.mkdtemp()
old = os.getcwd()
os.chdir(stage)
try:
    with open('project/config.json', encoding='utf-8') as f:
        print('В onefile найден:', f.read())
except FileNotFoundError:
    print('В onefile: FileNotFoundError')
os.chdir(old)
shutil.rmtree(stage)
shutil.rmtree('project')
Вывод
При разработке найден: {"mode": "fast"}
В onefile: FileNotFoundError

Упаковщик готов. Дальше — Nuitka: вместо упаковки байткода он переводит программу в C и собирает нативно, а альтернативные упаковщики — py2exe и cx_Freeze — идут тем же путём, что PyInstaller, но с другим рецептом сборки.

Чему верить: размер папки или размер файла

Частая путаница: сравнивают размер папки onedir с размером файла onefile и делают вывод, что onefile «вместительнее». На самом деле внутри onefile лежит тот же onedir-набор, просто запакованный загрузчиком: при запуске он распаковывается во временную папку и исполняется оттуда. Разница в размере на диске — не разница в содержимом, а плата за упаковку.

Из этого следуют два практических правила. Первое: onedir стартует быстрее, потому что не тратит время на распаковку — для программ, которые запускаются часто и работают долго (панель оператора, монитор), это заметнее, чем экономия на размере. Второе: onefile удобнее для распространения по почте или через веб — один файл вместо папки с сотнями компонентов, и у пользователя меньше шансов запустить не тот файл.

СценарийЧто выбратьПочему
Рассылка заказчикуonefileодин файл проще передать и проверить
Ежедневный рабочий инструментonedirмгновенный старт, без распаковки в temp
Программа с тяжёлыми даннымиonedirраспаковка больших данных занимает время
Автозапуск на слабой машинеonedirменьше дисковых операций при старте

Отдельно стоит сказать про --noconsole: флаг убирает чёрное окно терминала у GUI-программ. Но если убрать его у консольного инструмента, пользователь потеряет и вывод — программа отработает молча, и проверить её будет невозможно. Правило простое: --noconsole только для программ с собственным окном интерфейса.

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

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

import zipfile, os

with zipfile.ZipFile('one.exe', 'w') as z:
    z.writestr('a.py', 'x = 1\n')
    z.writestr('b.py', 'x = 2\n')

ops = 0
with zipfile.ZipFile('one.exe') as z:
    for info in z.infolist():
        z.read(info.filename)
        ops += 1
os.remove('one.exe')
print('Распаковано записей:', ops)
import sys

print('frozen в обычном Python:', getattr(sys, 'frozen', False))
import zipfile, tempfile, os, shutil

with zipfile.ZipFile('t.exe', 'w') as z:
    z.writestr('m.py', 'x = 1\n')
    z.writestr('n.py', 'x = 2\n')

stage = tempfile.mkdtemp()
with zipfile.ZipFile('t.exe') as z:
    z.extractall(stage)
print(sorted(os.listdir(stage)))
shutil.rmtree(stage)
os.remove('t.exe')
Проверь себя
0 / 7

1. Что делает флаг --onefile?

2. Почему onefile стартует медленнее onedir?

3. Где onefile держит файлы данных во время работы?

4. Как программа понимает, что она собрана в exe?

5. Чем опасен флаг --noconsole?

6. Приложение с config.json, библиотеками и автозапуском по расписанию. Что выбрать?

7. Какой разделитель в --add-data на Windows?

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

Напиши функцию data_path(name, sysmod): в обычной среде она возвращает имя файла как есть, а в собранном приложении (sysmod.frozen истинно) — путь рядом с exe. Проверь на обычном sys и на имитации exe. Ожидаемый вывод: два пути.

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

Чем onefile отличается от onedir?

onefile — один exe: удобно распространять, но при каждом запуске содержимое распаковывается во временную папку. onedir — exe и папка _internal: старт мгновенный, содержимое видно, но переносится вся папка.

Урок 10: PyInstaller — сборка exe

Программа в onefile не находит config.json. Что делать?

Относительный open('config.json') смотрит в текущую папку, а данные onefile держит во временной. Внутри собранного приложения берите путь из sys._MEIPASS, а пользовательские файлы кладите рядом с exe и находите через dirname(sys.executable).

Когда лучше onedir, а не onefile?

Когда важен быстрый старт (автозапуски, службы, частые перезапуски), когда приложение держит данные рядом с собой и когда хочется видеть содержимое бандла при отладке. Также у onedir меньше трений с антивирусами.

Что делает --noconsole и когда он нужен?

Убирает окно консоли у GUI-приложения на Windows. Нужен для оконных программ; обязательное условие — писать логи в файл, потому что print в таком приложении уходит в никуда вместе с сообщениями об ошибках.

Урок 12: Nuitka — альтернатива для скорости

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

«onefile оплачивает удобство одного файла распаковкой при каждом запуске.»

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

TelegramVK

Что дальше

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

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

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