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

PyInstaller und Cython verwenden, um eine ausführbare Python-Datei zu erstellen

Kompilieren Sie ausgewählte Module mit Cython und bündeln Sie Ihre Anwendung mit PyInstaller.

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

Sie haben eine Python -Anwendung erstellt und möchten sie distribute. Wahrscheinlich haben Sie sie in einer virtuellen Python Umgebung laufen. Aber Kunden haben dieses Setup nicht, manche haben vielleicht nicht einmal Python installiert.

Es gibt mehrere Programme, die Ihre Python -Anwendung in eine einzige ausführbare Datei umwandeln können. Hier verwende ich PyInstaller. Vielleicht möchten Sie auch einen Teil Ihres Codes schützen und/oder einige Vorgänge beschleunigen. Hierfür können wir Cython verwenden.

Spoiler: Das Hinzufügen statischer Cython -Typdeklarationen für zwei Variablen führte zu einer 50-fachen Leistungssteigerung.

Wie immer läuft auf meinem Entwicklungsrechner Ubuntu 20.04.

PyInstaller

PyInstaller ist ein Werkzeug, mit dem Sie Ihre Python -Dateien und alle Abhängigkeiten in einer einzigen ausführbaren Datei oder einem Verzeichnis bündeln können. Die gepackte Anwendung kann ohne Installation eines Python -Interpreters oder -Moduls ausgeführt werden.

Derzeit können Sie PyInstaller verwenden, um ausführbare Dateien für Linux, Windows und MacOS zu erstellen. Um eine ausführbare Datei für Linux zu erstellen, müssen Sie PyInstaller auf einem Linux -System ausführen, um eine ausführbare Datei für Windows zu erstellen, müssen Sie PyInstaller auf einem Windows -System ausführen, usw. PyInstaller läuft auch auf einem Raspberry Pi.

PyInstaller verwendet (Bibliotheks-)Dateien des Zielsystems, was bedeutet, dass eine für Windows 10 erstellte ausführbare Datei möglicherweise nicht auf Windows 8 läuft.

Derzeit unterstützt PyInstaller kein ARM. Es ist jedoch möglich, einen Bootloader für ARM zu erstellen, siehe 'Building the bootloader'. Sobald Sie dies getan haben, können Sie PyInstaller auf Ihrem ARM-System ausführen, um die ausführbare Datei zu erstellen.

Es ist auch möglich, PyInstaller mit einigen (Bibliotheks-)Dateien zu füttern und eine Cross-Kompilierung durchzuführen, z.B. einen ARM-Compiler auf Ihrem Entwicklungssystem zu verwenden; dies wäre großartig, aber ich habe dies nicht erforscht, noch gibt es viel darüber im Internet zu finden.

Die erzeugte ausführbare Datei ist nicht klein. Die Größe einer minimalen ausführbaren Datei beträgt etwa 7 MB. Sie sollten PyInstaller immer in einer virtuellen Umgebung ausführen, in der Sie ein Minimum an Paketen haben.

PyInstaller kann eine einzelne ausführbare Datei oder ein Verzeichnis mit Ihrer ausführbaren Datei und Bibliotheksdateien erstellen. Der Verzeichnisansatz ist etwas unübersichtlich, hat aber den Vorteil, dass Ihre Anwendung schneller startet, da viele Dateien nicht entpackt werden müssen. Wenn Sie mehrere ausführbare Dateien haben, die die gleichen Bibliotheken verwenden, können Sie sie in diesem Verzeichnis zusammenfassen.

Eine von PyInstaller erzeugte ausführbare Datei kann Antiviren-Software auslösen. In diesem Fall sollten Sie es in die Whitelist aufnehmen.

Cython

Von der Website: 'Cython ist ein optimierender statischer Compiler sowohl für die Programmiersprache Python als auch für die erweiterte Programmiersprache Cython (basierend auf Pyrex). Es macht das Schreiben von C-Erweiterungen für Python so einfach wie Python selbst.'

Ich habe eine Python -Anwendung und möchte sie mit distribute versehen. Ich kann dies mit PyInstaller tun, aber ich möchte auch einige Teile meines Python -Codes schützen lassen. Wenn Sie im Internet nach 'pyinstaller decompile' suchen, werden Sie sehen, dass es nicht so schwierig ist, den Code herauszubekommen, .pyc Bytecode ist leicht zu dekompilieren.

Da wir bereits ausführbare Dateien mit PyInstaller für verschiedene Zielsysteme erstellen müssen, können wir Cython verwenden, um einige Python -Dateien zu kompilieren, bevor wir PyInstaller starten. Bei Linux sind die kompilierten Dateien .so -Dateien, gemeinsam genutzte Bibliotheksdateien. Alles kann reverse-engineered werden, aber .so ist schwieriger als .pyc.

Und jetzt, wo wir Cython verwenden, können wir vielleicht auch die Leistung leicht verbessern.

Die Testanwendung

In einer virtuellen Umgebung installieren wir die Pakete PyInstaller und Cython:

  • pip install pyinstaller
  • pip install cython

Dann erstellen wir ein Verzeichnis 'project' mit der folgenden Struktur und Dateien:

.
├── 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 ist eine leere Datei. Die Methode 'process' in SomeClass1 enthält eine for loop , die eine Zuweisung vornimmt und 10000000 mal inkrementiert. In factory.py messen wir die Zeit, die für die Ausführung dieser Methode benötigt wird.

Ausführung mit Python

Um dies mit Python auszuführen, tun wir dies:

python run.py

Das Ergebnis ist:

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

Das bedeutet, dass es 320 Millisekunden dauert, bis der Vorgang abgeschlossen ist.

Erstellen einer ausführbaren Datei mit PyInstaller

Um eine ausführbare Datei zu erstellen:

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

Dies ergibt eine Menge an Ausgabe:

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.

Im Verzeichnis 'project' sind zwei neue Verzeichnisse hinzugefügt worden:

  • build
  • dist

Unsere ausführbare Datei befindet sich im Verzeichnis dist , sie ist fast 7 MB groß:

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

Wir führen es aus:

dist/myapp

Und das Ergebnis:

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

Dies ist etwa 20% langsamer als die Nicht-PyInstaller -Version.

Lassen Sie nun PyInstaller ein Verzeichnis mit Dateien erstellen, d.h. ohne die Option --onefile :

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

Wichtig: Das Format von --add-data ist <SRC:DST>. Das Trennzeichen ist ':' bei Linux und ';' bei Windows.

Unsere ausführbare Datei befindet sich im Verzeichnis dist/myapp, um sie auszuführen:

dist/myapp/myapp

Und das Ergebnis:

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

Die Verarbeitungszeit ist in etwa die gleiche wie mit der Option --onefile .

Hinzufügen von Cython

Zu Beginn lassen wir Cython nur eine Datei kompilieren:

app/some_package/some_classes.py

Dazu erstellen wir eine setup.py -Datei im Verzeichnis '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
    ),
)

Um zu kompilieren, führen wir aus:

python setup.py build_ext --inplace

Dies erzeugt ein build_cythonize Verzeichnis, aber was noch wichtiger ist, es erzeugt eine neue Datei in unserem Modulverzeichnis:

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

Die Datei 'some_classes.cpython-38-x86_64-linux-gnu.so' ist die kompilierte Version von some_classes.py! Beachten Sie, dass die Größe der kompilierten Datei 168 KB beträgt.

-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

Das Schöne an Cython ist, dass wir unsere Anwendung nun auf dieselbe Weise ausführen können, ohne etwas zu ändern:

python run.py

Das Ergebnis ist:

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

Sie läuft 4 mal schneller als ohne Cython!

Erstellen einer ausführbaren Datei mit PyInstaller unter Verwendung der kompilierten Datei

Da wir weitere Dinge hinzufügen müssen, erstellen wir ein Bash-Skript compile_and_bundle , das wir ausführen können:

#!/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

Das Skript ausführbar machen:

chmod 755 compile_and_bundle

Kompilieren und bündeln:

./compile_and_bundle

Wieder eine Menge Ausgaben, die mit enden:

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

Unsere ausführbare Datei befindet sich im Verzeichnis dist . Lasst es uns ausführen:

dist/myapp

Das Ergebnis ist:

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

Ohne irgendetwas zu ändern, haben wir eine ausführbare Datei erstellt, die 3 bis 4 Mal schneller läuft als die Version ohne Cython. Außerdem ist unser Code dadurch besser geschützt.

Die Python -Version von some_classes.py ist nicht in der ausführbaren Datei enthalten, nur die kompilierte Version ist hinzugefügt. Das ist zumindest meine Schlussfolgerung. Dies zeigt eine Reihe von Treffern:

grep -r factory.py build

Hier wird nichts angezeigt:

grep -r some_classes.py build

Viel schneller mit statischen Cython -Typendeklarationen

An bestimmten Stellen in unserem Code können wir statische Cython -Typendeklarationen hinzufügen. Dazu müssen wir zunächst Cython importieren. In some_classes.py fügen wir Typdeklarationen für 'i' und 'loops' hinzu.

# 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

Kompilieren und bündeln:

./compile_and_bundle

Unsere ausführbare Datei befindet sich im Verzeichnis dist . Starten wir es:

dist/myapp

Das Ergebnis ist:

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

Erstaunlich! Mit einer einfachen Änderung läuft unser Programm 50 Mal schneller!


Kompilieren aller Dateien eines Pakets

Ich wollte auch alle Dateien meines Pakets kompilieren. Aber ich konnte das nicht zum Laufen bringen. Ich habe es in setup.py versucht:

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

Das Ergebnis war:

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

Der einzige Weg, wie ich das zum Laufen bringen konnte, war, eine fast identische Datei compile.py im Verzeichnis 'app' zu erstellen.

# 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']
)

und dann im Verzeichnis 'app' ausführt:

python compile.py build_ext --inplace

Nun sind beide Dateien kompiliert. Wir können sie wie zuvor mit --add-binary hinzufügen.

Zusammenfassung

Dies war eine erstaunliche Reise. Wir haben gelernt, wie man plattformabhängige ausführbare Dateien erstellt. Und in der Zwischenzeit haben wir die Leistung unseres Programms fast ohne Aufwand gesteigert.

Wenn Ihr Programm hauptsächlich IO-gebunden ist, werden Sie natürlich keine große Veränderung bemerken, aber für CPU-gebundene Aufgaben kann der Geschwindigkeitszuwachs sehr bedeutsam sein (oder sogar essentiell).

Links / Impressum

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

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.