СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

| сохранено

H Как преподнести проект на Python рядовому пользователю без компиляторов и костылей, или подстраиваем велосипед под седока в черновиках Из песочницы

Здравствуй, читатель.

Я люблю Python таким, какой он есть: шустрый, лаконичный, имеет из-под коробки очень много полезных вещей (например, можно узнать, является ли число палиндромом всего в 1 строку).
На нем в данный момент пишу клеточный автомат — упрощенное воссоздание жизни в чашке Петри. 5 классов живых существ, наследственность, влияние «силы водорода» на жизнь и развитие клетки. Но это уже другая история.

Собственно, это не осталось незамеченным со стороны окружающих. Появились люди, которые хотели «погонять» за свою колонию цианобактерий и вывести их в элиту своей чашки, вытеснив других. Но тут проявляется, по мнению некоторых, не очень хорошая особенность данного ЯП — он (почти) не компилируем — приходится с собой таскать 80 мегабайт дополнительного груза. А что если…

Цель — сделать Python более добрым по отношению к простому смертному пользователю, то есть:
— не принуждаем юзера ставить среду;
— не заставляем качать большие файлы (> 25 мб);
— не посылаем танцевать с бубнами, «нажми там, затем нарисуй пятиконечную звезду»;
— не теряем скорость выполнения приложения. Лишь чуток подождать в начале запуска.

Для своих целей я избрал вот такую алхимию:

1) Берем девственно чистый Python (в статье используется Python 3.4, но вас ничто не сдерживает использовать более поздние\ранние версии), без Tkinter, IDLE, pydoc, тестов;

Спойлер


2) Добавляем в него дополнительные (не стандартные, + нами специально изъятые) нужные модули, использующиеся в приложении, добавляем их в "/lib/" директорию. Помещаем в отдельную директорию папку с интерпретатором + исполняемый скрипт;
3) Сжимаем;
Осторожно, нецензурная лексика!


4) Создаем на компилируемом ЯП (самое простое — pascal) маленькую программку, которая расшакалит распакует это дело уже на компьютере конечного пользователя и запустит;
5) Задача, выполнена, my master!

Пункты 1 и 2. Чистим Python


Этот пункт можно проделать и самостоятельно. Выполняем их для уменьшения размера готового велосипеда. (В развернутом состоянии интерпретатор у меня весит 60,9 мб, в сжатом — всего 11,9 мб, то есть сжатие составляет 80,5%, что немаловажно).
Осторожно, нецензурная лексика!


Ссылка на Dropbox. (12 мб)

Затем складываем zip архив, файл (.py или .pyc) и сопутствующие файлы (картинки, ресурсы и проч.) в одну папку. Важно! Основной файл python должен называться main.py или main.pyc во избежание путаницы с другими .py или .pyc файлами вашего проекта.

Получится что-то похожее:

dir_test/
… python.zip
… main.py
… image.bmp
… README.txt

Пункт 3


Теперь сжимаем всё содержимое папки (!!!) в zip файл. Зачем мы снова архивируем интерпретатор? Интерпретатор мы сжимаем с высокой степенью сжатия и, если мы при каждой пробе будем заново архивировать его, то будет уходить довольно много времени.

Пункт 4


Теперь остается написать программу на pascal/delphi:
Код :
program python;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils, System.Zip, ShellAPI;

var a, path : string;
    zip : TZipFile;
    i, s, p : integer;
begin
  try
  // если уже проводилась распаковка...
    if fileexists('.sourse\\main.py') or fileexists('.sourse\\main.pyc') then begin
      writeln('Starting...');
      if fileexists('.sourse\\main.py') then ShellExecute(0, nil, PChar('"'+ExtractFilePath(ParamStr(0))+'\\.sourse\\python\\py.exe"'), PChar(string('"'+ExtractFilePath(ParamStr(0))+'\\.sourse\\main.py"')), '', 1)

      else if fileexists('.sourse\\main.pyc') then ShellExecute(0, nil, PChar('"'+ExtractFilePath(ParamStr(0))+'\\.sourse\\python\\py.exe"'), PChar(string('"'+ExtractFilePath(ParamStr(0))+'\\.sourse\\main.pyc"')), '', 1)
      else begin
        write('File not found! Press any key...');
        read(i);
      end;

      exit();
    end;

    //распаковываем zip-файл
    zip := TZipFile.Create;
    zip.UTF8Support := True;
    s := 0; // индикатор наличия main файла
    p := 0; //индикатор наличия python
    zip.Open('sourse.zip', zmRead);
    for I := 0 to zip.FileCount - 1 do
    begin
      writeln(zip.FileNames[i], ' extracting...');
      if zip.FileNames[i] = 'main.py' then s := 1;
      if zip.FileNames[i] = 'main.pyc' then s := 2;
      if zip.FileNames[i] = 'python.zip' then p := 1;
      zip.Extract(zip.FileNames[i], '.sourse');
    end;
    zip.Close;
    DeleteFile('sourse.zip');
    //начинаем работу с распаковкой пайтона
    write('Extracting Python, it may take a few seconds...');
    zip.Open('.sourse\\python.zip', zmRead);
    zip.ExtractAll('.sourse\\python');
    write('Done!'+#10#13 +'Starting...');
    zip.Destroy();
    DeleteFile('.sourse\\python.zip');
    //проверки
    if s = 0 then      //если в ходе распаковки основной файл не был обнаружен
        begin
           write('No .py or .pyc file!', #10#13, 'Press any key...');
           read(a);
           exit();
        end;
    if p = 0 then      //если в ходе распаковки python не был обнаружен
        begin
           write('No Python!', #10#13, 'Press any key...');
           read(a);
           exit();
        end;
    //сам запуск
    if s = 1 then ShellExecute(0, nil, PChar('"'+ExtractFilePath(ParamStr(0))+'\\.sourse\\python\\py.exe"'), PChar(string('"'+ExtractFilePath(ParamStr(0))+'\\.sourse\\main.py"')), '', 1)    //PChar(string(ExtractFilePath(ParamStr(0))+'\\.sourse\\main.py'))
    else ShellExecute(0, nil, PChar('"'+ExtractFilePath(ParamStr(0))+'\\.sourse\\python\\py.exe"'), PChar(string('"'+ExtractFilePath(ParamStr(0))+'\\.sourse\\main.pyc"')), '', 1);    //PChar(string(ExtractFilePath(ParamStr(0))+'\\.sourse\\main.py'))

    exit();
  except
  //на случай, если что случится
    on E: Exception do begin
      Writeln(E.ClassName, ': ', E.Message);
      write('Broken! Press any button...');
      read(a);
    end;
  end;
end.


Программа весит 559 кб, что очень недурно.

Для проверки напишем скрипт-пробник на python:
Скрипт python
print('Привет! Я - переносной скрипт python и я занимаю в запакованном виде менее 20 мб!')
print('Сейчас я попытаюсь импортировать модуль random...')

try:
    import random
    print('Хех, мне это удалось!')
    print('Держи псевдослучайное число :', random.randrange(0,100))
except:
    print('%( Не получилось')

ragnarok = input('Нажмите любую кнопку...')



Всё это весит в запакованном виде 12 мегабайт (но по увеличению количества кода вес не будет существенно меняться) и работает быстро — по тестам первая распаковка длится не более 4 секунд, а последующие запуски — менее полсекунды.
В развернутом виде мы получаем 60,9 мегабайта.

Ссылка на тест:
https://www.dropbox.com/s/17xdpeju7u0oieh/python%20test.zip?dl=0 (Dropbox, 12 мб)

Подведем итог


Мы способны полноценно запускать python приложения на компьютерах под управлением Windows, где не стоит интерпретатор Python (также в ситуациях с установленной 2.x версией), увеличили мобильность приложения, не теряя скорости выполнения.

Спасибо, что дочитали мою статью. У меня мало опыта в таком деле, но я буду стараться! Следующей будет статья про клеточный автомат.

комментарии (28)

+10
ApeCoder ,  
+4
maratvildan ,  
по мне так www.pyinstaller.org/ удобней
+1
ApeCoder ,  
чем?
–10
+1 –11
maratvildan ,  
ничем. просто я им пользуюсь. обсуждение продолжать не буду. я предоставил ссылку автору, если он про него не знает.
+3
datacompboy ,  
Мне его удалось нормально под вайном заставить работать.
+1
SAKrisT ,  
просто интересно. Зачем? :)
+3
datacompboy ,  
программа используется в 95% под виндой, а разрабатываю я на линухе.
регулярно ребутиться для сборки надоедает, запускать винду в виртуалке долго и неудобно (для py2exe я так делал).
теперь у меня удобно собирается просто по одной команде make win виндовая сборка.
+2
ForeverYoung ,  
Не поделитесь рецептом?
0
datacompboy ,  
Хорошо, на днях распишу.
0
Mingun ,   * (был изменён)
У меня, когда я выбирал утилиту для перегонки питона в exe-шник, неприятно поразило то, что, вопреки заверениям, py2exe генерирует exe-шник, зависимый от какого-то рантайма msvc, причем докладывание в папку той DLL, на которую указал Dependency Walker, проблему не решило — «Приложение некорректно настроено бла-бла-бла».

PyInstaller создал мне чистый exe-шник, не зависящий ни от чего, кроме kernel32, user32 и еще парочки системных библиотек (не припомню названия), от которых зависят все (ну ладно, собираемые обычным образом) виндовые программы.

Из минусов — только для 2.x ветки, но для моей задачи использование третьего питона было не принципиальным.
0
Mingun ,  
Да, еще вспомнил, почему-то PyInstaller плохо дружит с юникодом (может сейчас ситуация и поменялась, утилита писалась где-то год назад), во всяком случае, у меня, если .py-исходник содержал не-ASCII символы (даже если просто комментарии), то собранный exe-шник падал с ошибкой декодирования. Пришлось отказаться от русского языка в утилите.
0
HotIceCream ,  
сборка в один файл.
0
nosuchip ,  
Вместо тысячи строк кода.
0
Koiki ,  
Весь минус заключается в том, что он актуален только для 2.х
Хотя, py2exe заметно удобнее будет.

Но в свою защиту следует упомянуть, что скорость работы скрипта будет аналогична скорости работы при запуске с помощью полноценного интерпретатора.
0
ApeCoder ,   * (был изменён)
pypi.python.org/pypi/py2exe/0.9.2.0

P.S. просто поискал
0
alkresin ,  
И какой примерно выходит размер исполняемого файла?
0
ApeCoder ,   * (был изменён)
www.py2exe.org/index.cgi/OptimizingSize

Here is an example — my setup script, which resulted in a total size of 2.15 MB, 2.01 of which are the Python DLL (which of course can't be excluded).
0
kAIST ,  
У меня получалось сжать итоговую сборку программы до 3.5 мегабайт. Сюда же кстати был включен gui на tkinter и еще кое какие модули. Использовал дополнительно сжатие всех dll и pyd файлов upx'ом.
+7
wait ,  
Программа весит 559 кб, что очень недурно.

Как четверть интерпретатора Python — да уж, недурно…
+3
Hertz ,  
Я один не понял, что за полиморфные числа такие?
0
+1 –1
Koiki ,  
Полиморфные числа не изменяют своего значения при чтении в обратном порядке.
171, 4, 6556, 24542 — полиморфные числа.

Также есть полиморфные слова и полиморфные фразы.
0
Koiki ,  
Каюсь, моя ошибка. Исправил, спасибо!
0
Bytamine ,  
У вас профессиональная деформация словарного запаса.
+29
SLY_G ,  
Нецензурная лексика здесь лишняя.
–6
+1 –7
boombick ,  
Плюс.
0
+1 –1
pomme ,  
Зачем я это прочитал?
+1
unity_ultra_hardcore ,  
Вы правда находите эти картинки остроумными?