Utiliser PyInstaller et Cython pour créer un exécutable Python
Compilez les modules sélectionnés avec Cython et regroupez votre application avec PyInstaller.
Vous avez créé une application Python et vous voulez la distribuer. Vous l'avez probablement exécutée dans un environnement virtuel Python . Mais les clients n'ont pas cette configuration, certains peuvent même ne pas avoir installé Python .
Il existe plusieurs programmes qui peuvent convertir votre application Python en un seul fichier exécutable. Ici, j'utilise PyInstaller. Vous pouvez également vouloir protéger une partie de votre code et/ou accélérer certaines opérations. Pour cela, nous pouvons utiliser Cython.
Spoiler : L'ajout de déclarations de types statiques Cython pour deux variables a permis de multiplier les performances par 50.
Comme toujours, ma machine de développement tourne sous Ubuntu 20.04.
PyInstaller
PyInstaller est un outil pour regrouper vos fichiers Python et toutes ses dépendances dans un seul exécutable ou répertoire. L'application packagée peut fonctionner sans installer d'interpréteur ou de modules Python .
Actuellement, vous pouvez utiliser PyInstaller pour construire des exécutables pour Linux, Windows et MacOS. Pour créer un exécutable pour Linux, il faut exécuter PyInstaller sur un système Linux , pour créer un exécutable pour Windows, il faut exécuter PyInstaller sur un système Windows , etc. PyInstaller fonctionne également sur un Raspberry Pi.
PyInstaller utilise des fichiers (bibliothèques) du système cible, ce qui signifie qu'un exécutable créé pour Windows 10 peut ne pas fonctionner sur Windows 8.
Actuellement, PyInstaller ne supporte pas ARM. Cependant, il est possible de créer un chargeur de démarrage pour ARM, voir 'Building the bootloader'. Une fois que vous avez fait cela, vous pouvez exécuter PyInstaller sur votre système ARM pour construire l'exécutable.
Il est également possible d'alimenter PyInstaller avec certains fichiers (bibliothèques), et de faire une compilation croisée, par exemple en utilisant un compilateur ARM sur votre système de développement, ce serait génial mais je n'ai pas fait de recherches à ce sujet, et il n'y a pas grand chose à trouver sur Internet.
Le fichier exécutable généré n'est pas petit. La taille d'un fichier exécutable minimal est d'environ 7 Mo. Vous devriez toujours exécuter PyInstaller dans un environnement virtuel où vous avez un minimum de paquets.
PyInstaller peut créer un seul exécutable ou un répertoire contenant votre exécutable et les fichiers de bibliothèque. L'approche par répertoire est un peu désordonnée, mais elle a l'avantage de permettre un démarrage plus rapide de votre application, car il n'est pas nécessaire de décompresser de nombreux fichiers. Si vous avez plusieurs exécutables qui utilisent les mêmes bibliothèques, vous pouvez les regrouper dans ce répertoire.
Un exécutable généré par PyInstaller peut déclencher un logiciel antivirus. Dans ce cas, vous devez le mettre sur liste blanche.
Cython
Du site web : Cython est un compilateur statique optimisant pour le langage de programmation Python et le langage de programmation étendu Cython (basé sur Pyrex). Il rend l'écriture d'extensions C pour Python aussi facile que Python lui-même.''
J'ai une application Python et je veux la distribuer. Je peux le faire avec PyInstaller, mais je veux aussi que certaines parties de mon code Python soient protégées. Si vous cherchez 'pyinstaller decompile' sur Internet, vous verrez qu'il n'est pas si difficile d'extraire le code, le bytecode .pyc est facile à décompiler.
Puisque nous devons déjà construire des exécutables avec PyInstaller pour différents systèmes cibles, nous pouvons utiliser Cython pour compiler quelques fichiers Python avant de lancer PyInstaller. Pour Linux, les fichiers compilés sont des fichiers .so , des fichiers de bibliothèque partagée. Tout peut faire l'objet d'une rétro-ingénierie, mais .so est plus difficile que .pyc.
Et maintenant que nous utilisons Cython, nous pouvons peut-être aussi améliorer facilement les performances.
L'application de test
Dans un environnement virtuel, nous installons les paquets PyInstaller et Cython :
- pip install pyinstaller
- pip install cython
Ensuite nous créons un répertoire 'project' avec la structure et les fichiers suivants :
.
├── 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 est un fichier vide. La méthode 'process' dans SomeClass1 contient une for loop qui fait une affectation et l'incrémente 10000000 fois. Dans factory.py , nous mesurons le temps nécessaire à l'exécution de cette méthode.
Exécution avec Python
Pour exécuter cette méthode avec Python nous faisons :
python run.py
Le résultat est :
Running ...
Processing ...
Ready in: 0.325362 seconds, loops = 10000000
Cela signifie qu'il faut 320 millisecondes pour terminer.
Créer un exécutable avec PyInstaller
Pour créer un exécutable :
pyinstaller --onefile --name="myapp" --add-data="README:README" run.py
Cela donne beaucoup de résultats :
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.
Dans le répertoire 'project' deux nouveaux répertoires ont été ajoutés :
- build
- dist
Notre exécutable se trouve dans le répertoire dist , il fait presque 7 Mo :
-rwxr-xr-x 1 peter peter 6858088 okt 6 11:05 myapp
L'exécuter :
dist/myapp
Et le résultat :
Running ...
Processing ...
Ready in: 0.422389 seconds, loops = 10000000
C'est environ 20% plus lent que la version non-PyInstaller .
Maintenant, laissons PyInstaller créer un répertoire avec des fichiers, c'est à dire sans l'option --onefile :
pyinstaller --name="myapp" --add-data="README:README" run.py
Important : Le format de --add-data est <SRC:DST>. Le séparateur est ':' sur Linux et ';' sur Windows.
Notre exécutable se trouve dans le répertoire dist/myapp, pour l'exécuter :
dist/myapp/myapp
Et le résultat :
Running ...
Processing ...
Ready in: 0.423248 seconds, loops = 10000000
Le temps de traitement est à peu près le même qu'avec l'option --onefile .
Ajouter Cython
Pour commencer, nous ne laissons Cython compiler qu'un seul fichier :
app/some_package/some_classes.py
Pour cela, nous créons un fichier setup.py dans le répertoire 'project' :
# 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
),
)
Pour compiler, nous exécutons :
python setup.py build_ext --inplace
Cela a créé un répertoire build_cythonize mais plus important encore, cela a créé un nouveau fichier dans notre répertoire de modules :
└── some_package
├── __init__.py
├── some_classes.cpython-38-x86_64-linux-gnu.so
└── some_classes.py
Le fichier 'some_classes.cpython-38-x86_64-linux-gnu.so' est la version compilée de some_classes.py ! Notez que la taille du fichier compilé est de 168 KB.
-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
La beauté de Cython est que nous pouvons maintenant exécuter notre application de la même manière sans rien changer :
python run.py
Le résultat est le suivant :
Running ...
Processing ...
Ready in: 0.079608 seconds, loops = 10000000
Elle tourne 4 fois plus vite que sans Cython !
Créer un exécutable avec PyInstaller en utilisant le fichier compilé
Comme nous devons ajouter plus de choses, nous créons un script Bash compile_and_bundle que nous pouvons exécuter :
#!/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
Rendre le script exécutable :
chmod 755 compile_and_bundle
Compiler et empaqueter :
./compile_and_bundle
Encore une fois beaucoup de sortie se terminant par :
...
064 INFO: Building EXE from EXE-00.toc completed successfully.
Notre exécutable est dans le répertoire dist . Exécutons-le :
dist/myapp
Le résultat est le suivant :
Running ...
Processing ...
Ready in: 0.128354 seconds, loops = 10000000
Sans rien changer, nous avons créé un exécutable qui tourne 3 à 4 fois plus vite que la version sans Cython. Il protège également mieux notre code.
La version Python de some_classes.py n'est pas incluse dans l'exécutable, seule la version compilée est ajoutée. C'est du moins ce que je conclus. Ceci montre un certain nombre de résultats :
grep -r factory.py build
Ceci ne montre rien :
grep -r some_classes.py build
Beaucoup plus rapide avec les déclarations de type statiques Cython
À certains endroits de notre code, nous pouvons ajouter des déclarations de types statiques Cython . Pour ce faire, nous devons d'abord importer Cython. Dans some_classes.py , nous ajoutons les déclarations de type pour 'i' et '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
Compilez et regroupez :
./compile_and_bundle
Notre exécutable est dans le répertoire dist . Exécutons-le :
dist/myapp
Le résultat est :
Running ...
Processing ...
Ready in: 0.002335 seconds, loops = 10000000
Incroyable ! Avec un simple changement, notre programme s'exécute 50 fois plus vite !
Compilation de tous les fichiers d'un paquet
Je voulais aussi compiler tous les fichiers de mon paquet. Mais je n'ai pas réussi à le faire fonctionner. J'ai essayé dans setup.py :
[
Extension("app.some_package.some_classes.*",
["app/some_package/some_classes/*.py"]),
],
Le résultat était le suivant :
ValueError: 'app/some_package/some_classes/*.py' doesn't match any files
La seule façon de faire fonctionner ce programme est de créer un fichier presque identique compile.py dans le répertoire 'app' .
# 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']
)
puis en l'exécutant dans le répertoire 'app' :
python compile.py build_ext --inplace
Maintenant les deux fichiers sont compilés. Nous pouvons les ajouter en utilisant --add-binary comme avant.
Résumé
Ce fut un voyage étonnant. Nous avons appris à construire des exécutables dépendants de la plateforme. Et pendant ce temps, nous avons augmenté les performances de notre programme avec presque aucun effort.
Bien sûr, si votre programme est principalement lié aux entrées/sorties, vous ne remarquerez pas beaucoup de changement, mais pour les tâches liées à CPU, l'augmentation de la vitesse peut être très importante (voire essentielle).
Liens / crédits
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
En savoir plus...
Compile Cython Distribution PyInstaller
Récent
- Un commutateur de base de données avec HAProxy et HAProxy Runtime API
- Docker Swarm rolling updates
- Masquer les clés primaires de la base de données UUID de votre application web
- Don't Repeat Yourself (DRY) avec Jinja2
- SQLAlchemy, PostgreSQL, nombre maximal de lignes par user
- Afficher les valeurs des filtres dynamiques SQLAlchemy
Les plus consultés
- Utilisation des Python's pyOpenSSL pour vérifier les certificats SSL téléchargés d'un hôte
- Utiliser PyInstaller et Cython pour créer un exécutable Python
- Réduire les temps de réponse d'un Flask SQLAlchemy site web
- Connexion à un service sur un hôte Docker à partir d'un conteneur Docker
- Utiliser UUIDs au lieu de Integer Autoincrement Primary Keys avec SQLAlchemy et MariaDb
- SQLAlchemy : Utilisation de Cascade Deletes pour supprimer des objets connexes