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

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.

6 octobre 2021 Mise à jour 6 octobre 2021
post main image
https://www.pexels.com/nl-nl/@polina-kovaleva

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

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.