PyInstaller en Cython gebruiken om een Python executable te maken
Compileer geselecteerde modules met Cython en bundel uw applicatie met PyInstaller.
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
Lees meer
Compile Cython Distribution PyInstaller
Recent
- Database UUID primaire sleutels van je webapplicatie verbergen
- Don't Repeat Yourself (DRY) met Jinja2
- SQLAlchemy, PostgreSQL, maximum aantal rijen per user
- Toon de waarden in SQLAlchemy dynamische filters
- Veilige gegevensoverdracht met Public Key versleuteling en pyNaCl
- rqlite: een alternatief voor SQLite met hoge beschikbaarheid en distributed
Meest bekeken
- Met behulp van Python's pyOpenSSL om SSL-certificaten die van een host zijn gedownload te controleren
- Gebruik van UUIDs in plaats van Integer Autoincrement Primary Keys met SQLAlchemy en MariaDb
- Maak verbinding met een dienst op een Docker host vanaf een Docker container
- PyInstaller en Cython gebruiken om een Python executable te maken
- SQLAlchemy: Gebruik van Cascade Deletes om verwante objecten te verwijderen
- Flask RESTful API verzoekparametervalidatie met Marshmallow-schema's