angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

PyInstaller en Cython gebruiken om een Python executable te maken

Compileer geselecteerde modules met Cython en bundel uw applicatie met PyInstaller.

6 oktober 2021 Bijgewerkt 6 oktober 2021
post main image
https://www.pexels.com/nl-nl/@polina-kovaleva

U heeft een Python applicatie gemaakt en wilt deze distribueren. U heeft het waarschijnlijk draaien in een Python virtuele omgeving. Maar klanten hebben deze setup niet, sommigen hebben misschien niet eens Python geïnstalleerd.

Er zijn verschillende programma's die uw Python applicatie kunnen omzetten in een enkel uitvoerbaar bestand. Hier gebruik ik PyInstaller. Het kan ook zijn dat je een deel van je code wilt beschermen en/of sommige handelingen wilt versnellen. Hiervoor kunnen we Cython gebruiken.

Spoiler: Het toevoegen van statische Cython type declaraties voor twee variabelen gaf een 50-voudige prestatieverhoging.

Zoals altijd draait mijn ontwikkel machine op Ubuntu 20.04.

PyInstaller

PyInstaller is een hulpmiddel om je Python bestanden en al zijn afhankelijkheden in een enkele executable of map te bundelen. De verpakte app kan draaien zonder een Python interpreter of modules te installeren.

Op dit moment kunt u PyInstaller gebruiken om uitvoerbare bestanden te bouwen voor Linux, Windows en MacOS. Om een uitvoerbaar bestand voor Linux te maken, moet u PyInstaller op een Linux systeem draaien, om een uitvoerbaar bestand voor Windows te maken, moet u PyInstaller op een Windows systeem draaien, enz. PyInstaller draait ook op een Raspberry Pi.

PyInstaller gebruikt (bibliotheek)bestanden van het doelsysteem, wat betekent dat een uitvoerbaar bestand dat voor Windows 10 is gemaakt misschien niet op Windows 8 draait.

Momenteel ondersteunt PyInstaller ARM niet. Het is echter mogelijk om een bootloader voor ARM te maken, zie 'Building the bootloader'. Als u dit gedaan heeft, kunt u PyInstaller op uw ARM systeem draaien om de executable te bouwen.

Het is ook mogelijk om PyInstaller te voeden met enkele (bibliotheek) bestanden, en cross-compilatie te doen, b.v. een ARM compiler op uw ontwikkelsysteem te gebruiken, dit zou geweldig zijn maar ik heb dit niet onderzocht, en er is ook niet veel over te vinden op het internet.

Het gegenereerde uitvoerbare bestand is niet klein. De grootte van een minimaal uitvoerbaar bestand is ongeveer 7 MB. U zou PyInstaller altijd in een Virtuele Omgeving moeten draaien waar u een minimum aan pakketten heeft.

PyInstaller kan een enkel uitvoerbaar bestand maken of een directory die uw uitvoerbaar bestand en bibliotheekbestanden bevat. De directory-benadering is een beetje rommelig, maar heeft het voordeel dat uw applicatie sneller start omdat veel bestanden niet uitgepakt hoeven te worden. Als je meer uitvoerbare bestanden hebt die dezelfde bibliotheken gebruiken, kun je ze samenvoegen in deze directory.

Een uitvoerbaar bestand gegenereerd door PyInstaller kan antivirus software triggeren. In dit geval moet je het whitelisten.

Cython

Van de website: 'Cython is een optimaliserende statische compiler voor zowel de Python programmeertaal als de uitgebreide Cython programmeertaal (gebaseerd op Pyrex). Het maakt het schrijven van C uitbreidingen voor Python net zo gemakkelijk als Python zelf.

Ik heb een Python applicatie en wil deze distribueren. Ik kan dit doen met PyInstaller, maar ik wil ook sommige delen van mijn Python code beschermd hebben. Als je op het internet zoekt naar 'pyinstaller decompile' zul je zien dat het niet zo moeilijk is om de code eruit te krijgen, .pyc bytecode is eenvoudig te decompileren.

Omdat we al uitvoerbare bestanden moeten bouwen met PyInstaller voor verschillende doelsystemen, kunnen we Cython gebruiken om enkele Python bestanden te compileren voordat we PyInstaller starten. Voor Linux zijn de gecompileerde bestanden .so bestanden, gedeelde bibliotheek bestanden. Alles kan reverse-engineered worden, maar .so is moeilijker dan .pyc.

En nu we Cython gebruiken, kunnen we misschien ook gemakkelijk de prestaties verbeteren.

De test applicatie

In een Virtuele Omgeving installeren we de pakketten PyInstaller en Cython:

  • pip install pyinstaller
  • pip install cython

Dan maken we een directory 'project' met de volgende structuur en bestanden:

.
├── app
│   ├── factory.py
│   └── some_package
│       ├── __init__.py
│       └── some_classes.py
├── README
└── run.py
# run.py
from app.factory import run

if __name__ == '__main__':
    run()
# app/factory.py
import datetime
from app.some_package import SomeClass1

def run():
    print('Running ...')
    some_class1 = SomeClass1()
    ts = datetime.datetime.now()
    loops = some_class1.process()
    print('Ready in: {} seconds, loops = {}'.\
        format((datetime.datetime.now() - ts).total_seconds(), loops))
# app/some_package/__init__.py
from .some_classes import SomeClass1
# app/some_package/some_classes.py

class SomeClass1:
    def process(self):
        print('Processing ...')
        a = 'a string'
        loops = 0
        for i in range(10000000):
            b = a
            loops += 1
        return loops

README is een leeg bestand. De methode 'process' in SomeClass1 bevat een for loop die een opdracht uitvoert en 10000000 keer incrementeert. In factory.py meten we de tijd die nodig is om deze methode uit te voeren.

Uitvoeren met Python

Om dit met Python uit te voeren doen we:

python run.py

Het resultaat is:

Running ...
Processing ...
Ready in: 0.325362 seconds, loops = 10000000

Dit betekent dat het 320 milliseconden duurt om te voltooien.

Maak uitvoerbaar bestand met PyInstaller

Om een uitvoerbaar bestand te maken:

pyinstaller --onefile --name="myapp" --add-data="README:README" run.py

Dit geeft een heleboel uitvoer:

23 INFO: PyInstaller: 4.5.1
23 INFO: Python: 3.8.10
29 INFO: Platform: Linux-5.11.0-37-generic-x86_64-with-glibc2.29
...
4996 INFO: Building EXE from EXE-00.toc completed successfully.

In de 'project' directory zijn twee nieuwe directories toegevoegd:

  • build
  • dist

Onze executable staat in de dist directory, hij is bijna 7 MB groot:

-rwxr-xr-x 1 peter peter 6858088 okt  6 11:05 myapp

Om het uit te voeren:

dist/myapp

En het resultaat:

Running ...
Processing ...
Ready in: 0.422389 seconds, loops = 10000000

Dit is ongeveer 20% langzamer dan de niet-PyInstaller versie.

Laat nu PyInstaller een directory met bestanden maken, dus zonder de --onefile optie:

pyinstaller --name="myapp" --add-data="README:README" run.py

Belangrijk: Het formaat van --add-data is <SRC:DST>. Het scheidingsteken is ':' op Linux en ';' op Windows.

Onze executable staat in de dist/myapp directory, om hem uit te voeren:

dist/myapp/myapp

En het resultaat:

Running ...
Processing ...
Ready in: 0.423248 seconds, loops = 10000000

De verwerkingstijd is ongeveer hetzelfde als met de --onefile optie.

Voeg Cython toe

Om te beginnen laten we Cython maar één bestand compileren:

app/some_package/some_classes.py

Hiervoor maken we een 'setup.py' bestand aan in de 'project' directory:

# setup.py
from setuptools import find_packages, setup
from setuptools.extension import Extension

from Cython.Build import cythonize
from Cython.Distutils import build_ext

setup(
    name="myapp",
    version='0.100',
    ext_modules = cythonize(
        [
            Extension("app.some_package.some_classes", 
                ["app/some_package/some_classes.py"]),
        ],
        build_dir="build_cythonize",
        compiler_directives={
            'language_level' : "3",
            'always_allow_keywords': True,
        }
    ),
    cmdclass=dict(
        build_ext=build_ext
    ),
)

Om te compileren, voeren we uit:

python setup.py build_ext --inplace

Dit creëerde een build_cythonize directory, maar nog belangrijker, het creëerde een nieuw bestand in onze module directory:

└── some_package
    ├── __init__.py
    ├── some_classes.cpython-38-x86_64-linux-gnu.so
    └── some_classes.py

Het bestand 'some_classes.cpython-38-x86_64-linux-gnu.so' is de gecompileerde versie van some_classes.py! Merk op dat de grootte van het gecompileerde bestand 168 KB is.

-rw-rw-r-- 1 peter peter     68 okt  6 10:31 __init__.py
-rwxrwxr-x 1 peter peter 168096 okt  6 12:48 some_classes.cpython-38-x86_64-linux-gnu.so
-rw-rw-r-- 1 peter peter    293 okt  6 12:39 some_classes.py

Het mooie van Cython is dat we nu onze applicatie op dezelfde manier kunnen draaien zonder iets te veranderen:

python run.py

Het resultaat is:

Running ...
Processing ...
Ready in: 0.079608 seconds, loops = 10000000

Hij draait 4 keer sneller dan zonder Cython!

Maak executable met PyInstaller met behulp van het gecompileerde bestand

Omdat we nog meer dingen moeten toevoegen maken we een Bash script compile_and_bundle dat we kunnen uitvoeren:

#!/usr/bin/bash

app_name="myapp"
echo "building for app = ${app_name}"

# cleanup
rm -R dist
rm -R build
rm -R "${app_name}.spec"
find . | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf

# compile
python setup.py build_ext --inplace

# bundle
pyinstaller \
    --onefile \
    --name "${app_name}" \
	--add-data="README:README" \
	--add-binary="app/some_package/some_classes.cpython-38-x86_64-linux-gnu.so:app/some_package/" \
    run.py

Maak het script uitvoerbaar:

chmod 755 compile_and_bundle

Compileren en bundelen:

./compile_and_bundle

Weer een heleboel uitvoer eindigend met:

...
064 INFO: Building EXE from EXE-00.toc completed successfully.

Onze executable staat in de dist directory. Laten we het uitvoeren:

dist/myapp

Het resultaat is:

Running ...
Processing ...
Ready in: 0.128354 seconds, loops = 10000000

Zonder iets te veranderen hebben we een uitvoerbaar bestand gemaakt dat 3 tot 4 keer sneller draait dan de versie zonder Cython. Het beschermt onze code ook beter.

De Python versie van some_classes.py is niet opgenomen in het uitvoerbare programma, alleen de gecompileerde versie is toegevoegd. Tenminste, dat is wat ik concludeer. Dit laat een aantal hits zien:

grep -r factory.py build

Dit laat niets zien:

grep -r some_classes.py build

Veel sneller met statische Cython type declaraties

Op bepaalde plaatsen in onze code kunnen we statische Cython type declaraties toevoegen. Om dit te doen moeten we eerst Cython importeren. In some_classes.py voegen we type declaraties toe voor 'i' en 'loops'.

# app/some_package/some_classes.py
import cython

class SomeClass1:
    def process(self):
        i: cython.int # here
        loops: cython.int # and here
        print('Processing ...')
        a = 'a string'
        loops = 0
        for i in range(10000000):
            b = a
            loops += 1
        return loops

Compileren en bundelen:

./compile_and_bundle

Onze executable staat in de dist directory. Laten we het uitvoeren:

dist/myapp

Het resultaat is:

Running ...
Processing ...
Ready in: 0.002335 seconds, loops = 10000000

Verbazingwekkend! Met een simpele verandering, draait ons programma 50 keer sneller!


Alle bestanden van een pakket compileren

Ik wilde ook alle bestanden van mijn package compileren. Maar ik kon dit niet werkend krijgen. Ik probeerde het in 'setup.py':

        [
            Extension("app.some_package.some_classes.*", 
                ["app/some_package/some_classes/*.py"]),
        ],

Het resultaat was:

ValueError: 'app/some_package/some_classes/*.py' doesn't match any files

De enige manier waarop ik dit aan de praat kon krijgen is door een bijna identiek bestand compile.py aan te maken in de 'app' directory.

# app/compile.py
from setuptools import find_packages, setup
from setuptools.extension import Extension

from Cython.Build import cythonize
from Cython.Distutils import build_ext

setup(
    name="packages",
    version='0.100',
    ext_modules=cythonize(
        [
           Extension('some_package.*', ['some_package/*.py']),
        ],
        build_dir="build_cythonize",
        compiler_directives={
            'language_level' : "3",
            'always_allow_keywords': True,
        },
    ),
    cmdclass=dict(
        build_ext=build_ext
    ),
    packages=['some_package']
)

en dan uit te voeren in de 'app' directory:

python compile.py build_ext --inplace

Nu zijn beide bestanden gecompileerd. We kunnen ze toevoegen met --add-binary zoals voorheen.

Samenvatting

Dit was een verbazingwekkende reis. We hebben geleerd hoe we platform afhankelijke executables kunnen bouwen. En in de tussentijd hebben we de performance van ons programma verhoogd met bijna geen moeite.

Natuurlijk, als je programma voornamelijk IO-bound is dan zul je niet veel verandering merken, maar voor CPU-bound taken kan de toename in snelheid zeer significant zijn (of zelfs essentieel).

Links / credits

Compiling Python Code with Cython
https://ron.sh/compiling-python-code-with-cython

Cython
https://en.wikipedia.org/wiki/Cython

PyInstaller Manual
https://pyinstaller.readthedocs.io/en/stable/

Using Cython to protect a Python codebase
https://bucharjan.cz/blog/using-cython-to-protect-a-python-codebase.html

Using PyInstaller to Easily Distribute Python Applications
https://realpython.com/pyinstaller-python

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.