Python-Anwendungen strukturieren, testen und warten
Um das Jahr 2000 lernte ich in einem Firmenpraktikum die Programmiersprache Python kennen. Meine Mentoren an der Universität schrieben damals Java, Delphi und Perl. Sie fragten mich: "Was ist das denn für eine Sprache? Nie davon gehört. Zeig uns doch mal etwas." Von anderen Entwicklern bekam ich auch mehrfach die wenig diplomatische Frage zu hören, wann ich denn eine "richtige Programmiersprache" lernen würde. In den 20 Jahren seitdem ist viel passiert: Python hat sich von einer wenig bekannten Programmiersprache zu einer bei Einsteigern und professionellen Entwicklern gleichermaßen beliebten entwickelt.
Der Tiobe-Index spricht für sich [1]. Python ist schon lange auf Augenhöhe mit C und Java und hat Ende 2021 den ersten Platz erobert. Selbst der schärfste Kritiker muss zugeben, dass Python viele Anwendungsbereiche durchdrungen hat. Aus Bereichen wie Datenanalyse, Machine Learning und Entwicklung von REST-APIs ist es kaum wegzudenken.
In dieser Zeit ist Python von einer Skriptsprache zu einer vollwertigen Programmiersprache gereift. Es gibt eine ausgezeichnete Auswahl von Entwicklungswerkzeugen, mit denen sich auch größere Programme gut pflegen lassen. Ob Python damit in kritischen Augen eine "richtige Programmiersprache" ist, vermag ich nicht zu sagen. Fest steht, dass darin richtige Programme geschrieben werden.
Meiner Meinung nach eignet sich Python als Primärsprache in IT-Projekten, aber auch als Zweitsprache für Nebenaufgaben. Die Grundlagen sind für erfahrene Entwickler schnell erlernbar. Die meisten Online-Materialien, Bücher und Kurse konzentrieren sich jedoch auf Syntax und Funktionsumfang. Über die Werkzeuge für professionelle Entwicklung findet sich in den Materialien für Einsteiger nur wenig.
Deshalb möchte ich in diesem Artikel einen Überblick über die wichtigsten Entwicklungswerkzeuge geben, mit denen sich moderne Python-Software ordentlich einrichten, entwickeln und pflegen lässt.
Editoren
Ein offensichtlich benötigtes Werkzeug zum Editieren von Python-Code ist ein geeigneter Editor. Hier gibt es derzeit vier Alternativen:
Eine einsteigerfreundliche Option ist Spyder, der Teil der Anaconda-Distribution ist [2]. Spyder ist ein leichtgewichtiger Editor, der eine schnelle Einarbeitung erlaubt. Herausragende Merkmale von Spyder sind eine ständig sichtbare Python-Konsole, der interaktive Variablen-Explorer und die Möglichkeit, Plots unmittelbar im Editor zu sehen. Durch die Zusammenarbeit mit den Anaconda-Paketen ist Spyder sehr gut für die Datenanalyse geeignet. Allerdings unterstützt Spyder das Schreiben größerer Softwareprojekte nur wenig.
Auch VSCode findet sich häufig auf den Bildschirmen von Python-Entwicklern [3]. Als Allround-Editor unterstützt VSCode von Hause aus auch das Editieren von Python. Vorteile von VSCode sind eine ausgezeichnete Integration von git sowie die Möglichkeit, über SSH und innerhalb von Docker-Containern zu editieren. Es ist möglich, Python-Code in VSCode auszuführen. Viele Entwicklungstools für Python (z. B. Linting, Testen, Refactoring) müssen als Plugins nachinstalliert werden. Auch die Ausführungsumgebung muss konfiguriert werden, was mit mehreren virtuellen Umgebungen schnell etwas unübersichtlich werden kann. Die Dokumentation ist über die einzelnen Plugins verteilt. Wem das – wie mir – zu umständlich ist, der kann VSCode nur zum Editieren verwenden und den Code im Terminal ausführen. VSCode ist eine ausgezeichnete Option für Projekte, in denen mehrere Programmiersprachen verwendet werden, und für Entwickler, die ohnehin schon sämtliche Tastenkürzel kennen.
PyCharm von JetBrains ist ohne Zweifel die am weitesten entwickelte IDE für Python [4]. Wer bereits mit IntelliJ oder CLion gearbeitet hat, wird viele Ähnlichkeiten entdecken. Bereits die freie Basisversion bietet einen reichhaltigen Funktionsumfang, der auch große Projekte strukturiert zu bearbeiten hilft: Vom Debugger über die git-Integration bis zur Verwaltung der Testumgebung ist alles dabei. In der kommerziellen Variante kommen weitere Features für Datenbankanbindungen, Refactoring und vieles mehr hinzu. Der einzige Nachteil von PyCharm ist die zu Beginn etwas steilere Lernkurve.
Ehrenvolle Erwähnung verdient Jupyter. Jupyter ist eine interaktive Umgebung ähnlich Matlab oder RStudio, mit der sich Python-Code im Browser editieren lässt. Das erstellte Format ist anstatt einer .py-Datei ein Jupyter Notebook, das neben dem Code auch formatierten Text, Tabellen, Diagramme, Bilder und mathematischen Formeln enthalten kann. Da Codeschnipsel sich einzeln ausführen lassen, sind Jupyter Notebooks relativ einfach zu debuggen und damit nützlich, um Python zu lernen oder mit neuen Bibliotheken zu experimentieren. Viele Organisationen bieten Jupyter serverseitig an – bekanntestes Beispiel ist Google Colab [5]. Ab einer gewissen Größe (etwa 100-200 Programmzeilen) werden Notebooks jedoch schnell unübersichtlich. Das den Notebooks zugrunde liegende Datenformat (JSON) hat bei der Arbeit mit git viele Nachteile. Für größere Programme taugt Jupyter daher nicht. Jupyter Notebooks sind aber für die Datenanalyse, für Simulationen und Machine-Learning-Modelle sowie im Unterricht sehr beliebt. Eine ausführliche deutschsprachige Einführung findet sich auf [6].
Virtuelle Umgebungen
Die Installation von Paketen in Python ist historisch eine leidige Sache. Es gab lange keine einheitliche Methode zum automatischen Auflösen von Paketabhängigkeiten, wie sie z. B. R und JavaScript schon lange besitzen. Die Situation hat sich in den letzten 10 Jahren erheblich verbessert, es sind aber noch einige Nachwehen spürbar. Ein für Python-Einsteiger oft verwirrender Aspekt sind die Details der Import-Mechanik in Python. Python sucht zu importierende Pakete und Module (Ordner und .py-Dateien) an mehreren Orten:
- dem aktuellen Arbeitsverzeichnis,
- einem Standardverzeichnis für Bibliotheken (hängt von der verwendeten Installation ab, meist site-packages/) und
- allen Verzeichnissen, die in der Umgebungsvariable PYTHONPATH aufgeführt sind (innerhalb von Python sys.path).
Um Projekte mit unterschiedlichen Abhängigkeiten voneinander zu trennen, ist es sinnvoll, mehrere alternative Paketverzeichnisse anzulegen. In Python nennt man dies eine virtuelle Umgebung. Es gibt mehrere Tools, um virtuelle Umgebungen anzulegen und zu pflegen. Leider sind diese nur bedingt miteinander kompatibel.
In neueren Python-Versionen fest eingebaut ist venv. Venv bietet die Grundfunktionalität über den Befehl:
python -m venv mein_projekt
Es wird ein Verzeichnis mein_projekt/ angelegt. Über das Shell-Skript mein_projekt/bin/activate kann die Umgebung aktiviert werden, d. h. die Pfadvariablen werden auf das Projektverzeichnis umgebogen. Neu installierte Bibliotheken werden dann im Verzeichnis dieser virtuellen Umgebung installiert. Auch ein Verweis auf den Python-Interpreter liegt dort. Dadurch ist die Trennung zu anderen vorhandenen virtuellen Umgebungen vollständig.
Sehr ähnlich zu venv funktioniert conda. Die Syntax und der Ort der Ablage unterscheidet sich leicht. conda ist etwas bequemer, falls man ohnehin bereits die Anaconda-Distribution nutzt.
Einen etwas anderen Ansatz verfolgt poetry. poetry erwartet vom Programmierer, sämtliche verwendeten Bibliotheken in der Datei pyproject.toml aufzuführen (was ohnehin eine gute Idee ist). Anschließend ermittelt poetry die exakten Versionen dieser Bibliotheken und sämtlicher Sekundärabhängigkeiten (diese hängen von der Python-Version, dem Betriebssystem und Abhängigkeiten der Pakete untereinander ab) und schreibt diese in eine Datei poetry.lock.
Die folgende Befehlsfolge initialisiert eine poetry-Umgebung und installiert die in pyproject.toml angegebenen Abhängigkeiten:
poetry new mein-projekt poetry install
Aktivieren lässt sich die Umgebung mit:
poetry shell
Der von poetry verwendete Prozess ist sehr sauber und ein echter Fortschritt zu pip, das je nach System bisweilen unterschiedliche Ergebnisse produziert. Die Auflösung der Abhängigkeiten ist je nach verwendeten Paketen ein längerer Prozess, der sich bei manchen Paketen auch schon einmal aufhängt. poetry ist noch relativ neu und ich erwarte, dass sich der Ansatz in den nächsten Jahren stabilisiert [7].
Generell sind virtuelle Umgebungen eine sehr gute Sache. Es ist jedoch etwas Vorsicht geboten! Es ist mir mehr als einmal passiert, dass ich mehrere Werkzeuge parallel verwendet habe und Python dadurch gründlich verwirren konnte. Glücklicherweise ist es leicht, eine virtuelle Umgebung wieder loszuwerden, nämlich indem man den erstellten Ordner wieder löscht. Abschließend ist zu bemerken, dass virtuelle Umgebungen vor allem ein Werkzeug zur lokalen Entwicklung sind. In einem build-Artefakt (z. B. einem Docker-Container) sind sie eher nicht notwendig. In einem git-Repository haben sie überhaupt nichts verloren.
Paketstruktur
Eine häufig gestellte Frage ist, wie die Dateien eines Python-Projekts strukturiert sein sollten. Sehen wir uns einmal die typische Struktur eines Python-Programms an (hier ein kleines Adventure, das bei mir regelmäßig als Beispiel herhalten muss [8]):
Die wichtigste Grundregel ist, Python-Module (also alle Dateien mit der Endung .py) nicht im Hauptverzeichnis des Projekts zu lassen, sondern sie in genau einem Ordner abzulegen. Dieser Unterordner ist das eigentliche Python-Paket. Der Name ist wichtig, da er vom Import-Mechanismus in Python verwendet wird. Heißt also der Paketordner space/, könnte man schreiben:
from space import planets
Python importiert dann die Datei space/planets.py. Alternativ könnte es aber auch ein Unterordner space/planets/ mit weiteren Python-Modulen sein. Auch Objekte aus den Dateien lassen sich direkt importieren:
from space.planets import Location
Die Order- und Dateinamen sind also recht wichtig. Die Stilkonvention PEP-8 empfiehlt, die Namen in snake_case, also kleinbuchstaben_mit_unterstrichen zu schreiben. Damit diese Imports funktionieren, ist es wichtig, Python das Hauptverzeichnis des Projekts mitzuteilen, in dem sich das Paket befindet. Dies kann über die Umgebungsvariable PYTHONPATH geschehen, oder indem man in PyCharm den Ordner als "source root" markiert. Auch der pip install-Befehl (s. u.) kümmert sich darum. Innerhalb des Pakets sollten die import-Anweisungen ebenfalls den vollständigen Namen des Pakets beinhalten. Relative Imports sind möglich, beim Testen kommt Python aber oft durcheinander.
Vom Hauptordner des Projekts zweigt ein Unterordner für die Testautomatisierung ab (tests/). Auch Ordner für Dokumentation und Konfiguration sollten auf dieser Ebene zu finden sein. Bisweilen repliziert der Test-Ordner die Namen im Paketordner, so dass Tests zu einzelnen Module leichter zu finden sind (z. B. tests/test_planets.py). Das ist aber nicht obligatorisch und auch eine Aufteilung, z. B. in Unit- und Integrationstests ist üblich.
Im Hauptordner schließlich finden sich die Konfigurationsdateien des Programms. Die Konfiguration eines modernen Projekts steht vor allem in der Datei pyproject.toml, in der sämtliche gängigen Python-Werkzeuge nach Einstellungen suchen. Ist diese ordentlich geschrieben, wird sonst nichts benötigt.
Ein etwas älterer Aufbau, der noch sehr häufig anzutreffen ist, legt die Metadaten des Projekts in der Datei setup.py ab, die Paketabhängigkeiten in requirements.txt, während alle anderen Werkzeuge ihre eigene Konfigurationsdatei mitbringen. Im Beispielprojekt habe ich beide Varianten aufgeführt [8].
Diese Konfigurationsdateien definieren unter anderem, welcher Ordner das eigentliche Python-Paket enthält. Dieses lässt sich über folgenden Befehl installieren:
pip install --editable .
Python registriert den in der Konfigurationsdatei angegebenen Ordner, so dass er sich aus Test-Code und anderen Python-Programmen importieren lässt. Mit
python setup.py dist
lässt sich eine Distributionsform des Programms zusammenstellen (unter Poetry mit poetry build). Der zugrunde liegende Prozess ist im Vergleich zum typischen Java- oder C++-Build recht einfach, da im wesentlichen Dateien kopiert werden.
Falls das Projekt etwas größer wird, kann es nötig sein, mehrere Python-Pakete parallel zu pflegen. Ob diese in einem Ordner src/, packages/ oder space-project/ stehen, ist egal. Vermeiden sollte man auf jeden Fall Ordnernamen wie space/space/, da dies leicht zu schwer nachvollziehbaren Import-Bugs führt.
Qualitätskontrolle
Bei der Arbeit in einem Python-Projekt sollten Maßnahmen zur Qualitätssicherung von Beginn an eingeplant werden. Diese können etwas mehr Raum einnehmen als in einem Java/C++-Projekt. Das liegt daran, dass Python keinen Compilierungsschritt kennt, in dem viele Fehler bereits abgefangen werden. Auch das weitgehende Fehlen strenger Kapselung macht es erforderlich, das Programm genau auf unerwünschte Effekte zu prüfen. Neben allgemein nützlichen Techniken wie Code-Reviews kennt Python eine Anzahl an Werkzeugen, um Defekte früh ausfindig zu machen.
Zunächst gibt es mehrere Linter, die Stilkontrollen durchführen und den Python-Code lesbar halten. Gebräuchlich ist vor allem das Tool flake8[9], das Konformität mit dem Python-Standard PEP8 überprüft. Mit black[10] lässt sich Quelltext automatisch auf PEP8 formatieren. Allerdings setzt black nicht alle Stilrichtlinien automatisch um (z. B. Namensgebung von Variablen und Funktionen). Das Tool isort[11] sortiert zusätzlich die import-Anweisungen in Python-Dateien erst nach Typ und dann alphabetisch. Bei einem Import-Block von einer oder mehreren Bildschirmseiten ist dies durchaus sinnvoll.
Diese Tools sind weitgehend komplementär. Es spricht daher nichts dagegen, flake8, black und isort gemeinsam zu verwenden, gerne auch im Rahmen einer CI-Pipeline.
Automatische Tests sind in Python hoch entwickelt.
Das Test-Framework pytest[12] hat sich in den letzten Jahren als Quasi-Standard etabliert. In der Grundfunktionalität lassen sich Tests als einfache Funktionen mit einem assert-Befehl implementieren:
def test_warp(space): """Reach Centauri after pressing '1' """ space.move(1) assert space.ship.location.name == "Centauri"
Entscheidend ist, dass diese Funktionen das Präfix test_ im Namen tragen und sich in einer Datei namens test_xxxx.py befinden. Dann ist pytest in der Lage, Tests in einem Verzeichnisbaum automatisch zu finden und mit einem kurzen Befehl auszuführen:
pytest
In pytest gibt es viele Möglichkeiten, den Testcode so einfach wie möglich zu gestalten. Mit fixtures lassen sich Objekte vor dem Test erzeugen und hinterher wieder aufräumen (das Objekt space im Beispiel ist eine fixture). Testparametrisierung erlaubt es, automatisch mehrere Testfälle aus Daten zu erzeugen, ohne dass dazu Code dupliziert werden muss. Schließlich lassen sich Objekte mit dem Modul unittest.mock durch temporäre Platzhalter ersetzen, so dass z. B. Datenbanken oder Netzwerkverbindungen für das Testen umgangen werden. Damit lässt sich kurzer strukturierter Testcode schreiben, der einfach zu warten ist.
Prüfen von Datentypen
Als einer der größten Nachteile von Python wird oft das Fehlen starker Typisierung genannt. In den letzen Jahren haben die Python-Entwickler hier nachgebessert, ohne die Flexibilität der dynamischen Typen zu opfern. Seit Python 3.5 verfügt die Sprache über Type Annotations, optionale Typangaben für Variablen und Signaturen von Funktionen. Die Type Annotations sind zunächst einmal funktionslos, sie sind also im Gegensatz zu anderen Sprachen als Dokumentation für Entwickler zu betrachten.
Es gibt aber einige Werkzeuge, die der Typannotation mehr Gewicht verleihen. Beispielsweise überprüft das Tool mypy[13] die in einem Programm verwendeten Datentypen und "beschwert sich", falls eine als int deklarierte Funktion eine Liste zurückliefert. mypy ist ein statisches Analysetool, es führt also den Code nicht aus.
Ist eine Typüberprüfung zur Laufzeit nötig, sollte man sich pydantic[14] genauer ansehen. Mit pydantic lassen sich Klassen als Datenstrukturen mit definierten Typen definieren, die automatisch überprüft werden. Diese Klassen bekommen automatisch einen Konstruktor und haben eine aussagekräftige String-Repräsentation. Der Code wird also nicht nur strenger, sondern auch kompakter:
from pydantic import BaseModel class Planet(Base): name: str population: int
Auch Funktionen lassen sich so überprüfen:
from pydantic import validate_arguments @validate_arguments def hyperspace_jump(destination: str, warp: int) ...
Zwar wird die Überprüfung erst zur Laufzeit vorgenommen, also wenn eine Instanz der Klasse erzeugt oder die Funktion aufgerufen wird. Die Validierung mit pydantic eignet sich aber hervorragend, um die Schnittstellen eines Programms abzusichern: Objekte, die in Datenbanken geschrieben werden, Requests und Responses einer Web-API und ähnliches.
Zusammenfassung
Die Aufführung ließe sich hier sicher noch fortsetzen. Zur besseren Übersicht sind die oben erwähnten Werkzeuge in der Tabelle noch einmal aufgeführt. Auch jeweils ein Modul zur Laufzeitanalyse, zum Erstellen von Dokumentation und zum Loggen hat dort Platz gefunden. Die meisten dieser Werkzeuge werden von einer großen und diversen Open-Source-Gemeinde entwickelt und gepflegt. Neuerungen erscheinen oft kurz nachdem jemand einen Bedarf für sich entdeckt hat. Es erfordert ein wenig Geduld, zu beobachten, welche der Werkzeuge langfristig Bestand haben.
Für etablierte Python-Werkzeuge habe ich einige Kochrezepte gesammelt [15]. Um über neuere Trends auf dem Laufenden zu bleiben, empfehle ich die Konferenzserien PyCon und EuroPython, deren Vorträge allesamt im Netz zu finden sind.
Tabelle 1: Ausgewählte Werkzeuge für die professionelle Python-Entwicklung
Entwicklungswerkzeug | Beschreibung |
Spyder | Einsteigerfreundlicher Editor, Teil der Anaconda-Distribution. |
VSCode | Allroundeditor mit guter Python-Unterstützung. |
PyCharm | Hoch entwickelte IDE, vergleichbar zu IntelliJ und CLion. |
Jupyter | Webbasierte Entwicklung für kurze Programme und Datenanalysen. |
venv | In Python eingebautes Modul für virtuelle Umgebungen. |
conda | In Anaconda eingebautes Werkzeug für virtuelle Umgebungen. |
poetry | Moderne Verwaltung von virtuellen Umgebungen und auflösen von Paketabhängigkeiten. |
pyproject.toml | Wichtigste Konfigurationsdatei eines Python-Projekts, die von zahlreichen Werkzeugen unterstützt wird. |
setup.py | Ältere Konfigurationsdatei, die ein Python-Projekt mit pip installierbar macht. |
flake8 | Linter, überprüft Konformität mit der Stilrichtlinie PEP8. |
black | Automatischer Codeformatierer, zu PEP8 konform. |
isort | Sortiert import-Anweisungen nach Kontext und alphabetisch. |
pytest | Das beliebteste Framework für automatische Tests unter Python. |
mypy | Statische Überprüfung von Typ-Annotationen. |
pydantic | Erlaubt Typüberprüfungen zur Laufzeit mit wenig zusätzlichem Code. |