Explizite Typangaben in Python: Segen oder Fluch?

Die seit Python 3.6 erlaubten Typangaben für Variablen ist für manche Python-Entwickler eine sinnvolle Neuerung, für andere ein Sakrileg.

In Pocket speichern vorlesen Druckansicht 94 Kommentare lesen
Explizite Typangaben in Python: Segen oder Fluch?
Lesezeit: 8 Min.
Von
  • Gerhard Völkl
Inhaltsverzeichnis

Bisher haben Python-Entwickler über ihre C++- oder Java-Kollegen nur gelächelt, wenn diese bei jeder Variable und Funktion den zu erwartenden Datentyp angeben mussten. Seit Version 3.6 sind Typangaben bei Python ebenfalls möglich. Selbst Pythons Gründervater Guido van Rossum unterstützt das Vorgehen mit dem Type-Checker-Projekt mypy.

Der Typ einer Variablen in Python lässt sich bei der ersten Zuweisung festlegen:

a = "eins"
print(type(a))
<class 'str'>

Python weiß damit, dass es eine Variable a mit dem Wert "eins" gibt und sie vom Typ str ist – das ist eigentlich praktisch und bequem. Dafür, dass Python üblicherweise erst zur Laufzeit den Typ einer Variablen erfährt, gibt es den Fachbegriff dynamische Typisierung (dynamic typing). Das Gegenteil davon ist die statische Typisierung (static typing), bei der bereits beim Schreiben des Programms feststeht, von welchem Typ eine Variable ist. Beispiele hierfür sind Programmiersprachen wie C++, Java oder C#, bei denen jeweils folgende Codezeile legitim ist:

int i = 2;

An dieser Stelle folgt eine kleine Quizfrage: Welcher der zwei folgenden Befehle in Python erzeugt einen Fehler, wenn a wie im oberen Code bereits als str definiert ist?

b = a + 1 
c = a + foo(2)

Die erste Zeile führt in jedem Fall zu folgender Meldung:

TypeError: must be str, not int

Die Zahl 1 zu einem String zu addieren, ist nicht erlaubt. Bei der zweiten Programmzeile hängt es vom Rückgabewert der Funktion foo ab, ob es einen Fehler gibt. Ist der Rückgabewert eine Zahl, kommt es zum Programmabbruch. Bei einer Zeichenkette hängt Python den Inhalt der Variablen a mit dem Ergebnis der Funktion zusammen. Damit passt alles, da sich der Operator + auf Zeichenketten anwenden lässt.

Für den Fall

def foo(p):
return p/2

liefert die Funktion immer eine Zahl zurück. Damit führt die oben gezeigte Addition zu einer Fehlermeldung.

Ein Programm, das die beiden Zeilen enthält, läuft bis zu der ersten davon, um bei deren Verarbeitung mit einem Fehler abzubrechen. Nach der Korrektur dieser Zeile läuft es bis zur zweiten, um je nach Definition der Funktion wieder abzubrechen. Durch die dynamische Typisierung in Python ist erst beim Ausführen einer Zeile klar, ob sie einen Typfehler enthält. Bei statischer Typisierung wäre der Fehler beim Kompilieren aufgefallen statt im Betrieb beim Kunden.

Erheblich komplexer wird es, wenn die fehlerhaften Zeilen in Bedingungen versteckt sind und der Fehler erst auftritt, wenn die Voraussetzungen irgendwann erfüllt sind.

Bei kleinen Anwendungen ist das Verhalten kein Thema. Aber bei Programmen, die aus tausenden von Zeilen bestehen, ist der Ärger vorprogrammiert. Daher haben Macher von Python seit Version 3.6 die Option eingeführt, Variablen oder andere Programmobjekte mit Typinformationen versehen zu können (PEP 484 -- Type Hints):

a: str = "eins"

Bei einer Variablenzuweisung können Entwickler nach einem Doppelpunkt den gewünschten Typ angeben.

Bei einer Funktionsdefinition bekommt ein Parameter ebenfalls nach einem Doppelpunkt eine Typinformation. Den Rückgabewert können Entwickler nach einem Pfeil (->) mit einem Typ versehen:

def foo(p: float) -> float:
return p/2

Beim Hinzufügen der Type Hints zu den anfangs gezeigten beiden Zeilen würde sich zunächst bei der Ausführung überraschenderweise nichts ändern. Die zwei Abbrüche aufgrund der Fehler würden genauso erfolgen, denn Python sieht die Typinformationen in den aktuellen Versionen lediglich als Hinweis, und laut Sprachdefinition sollte sich nichts am Verhalten der Laufzeitumgebung ändern.

Der Einsatz der Type Hints ist erst wirklich interessant, wenn zusätzliche Programme wie Type-Checker (z. B. mypy) oder Entwicklungsumgebungen wie PyCharm daraus Nutzen ziehen.

An dem kostenlosen Type-Checker mypy ist Guido van Rossum beteiligt, einer der Erfinder von Python. Er ist aktuell bei Dropbox beschäftigt, einem Unternehmen das mit Python-Programmen arbeitet, die aus hunderttausenden Zeilen bestehen. Das erklärt vermutlich die Motivation für den Type-Checker.

Das Programm mypy lässt sich wie gewohnt mit PIP oder Anaconda (conda) installieren.

$ pip install mypy
$ conda install mypy

Um ein Python-Programm mit mypy zu prüfen, genügt folgender Befehl:

$ mypy test.py

Bei einem Programm, das die oben gezeigten, fehlerhaften Zeilen enthält, erzeugt mypy folgende Ausgabe:

test.py:6: error: \
Unsupported operand types for + ("str" and "int")

test.py:7: error: \
Unsupported operand types for + ("str" and "float")

Entwickler können sich damit die betroffenen Stellen vor der Auslieferung des Codes genauer ansehen. Damit sie mypy für Python-Programme einsetzen können, die nicht vollständig mit Typinformationen versehen sind, versucht das Tool auf dieselbe Art wie das Python-Laufzeitsystem, mögliche Typen abzuleiten (Type Inference). Bei Variablenzuweisungen funktioniert das gut, aber in anderen Fällen wird es schwierig. Wer eine hundertprozentige Zuverlässigkeit braucht, sollte die Typen explizit definieren.

Ein Großteil des Programmcodes kommt üblicherweise aus den Standardmodulen von Python, die Entwickler mit import einbinden. Um im Zusammenspiel mit ihnen eine Typprüfung zu ermöglichen, existieren sogenannte Stubs – Dateien, die lediglich die Typinformationen, aber keine ausführbaren Programmzeilen enthalten. Die Dateien sind im typeshed-Repository auf GitHub zu finden.

Beispielsweise würde der Stub für die in Python eingebauten Elemente (builtins) aussehen, wie der folgende Ausschnitt aus stubs/builtins.py:

True = ... # type: bool
False = ... # type: bool
...
def chr(code: int) -> str: ...

Die Entwickler von mypy übernehmen die aktuellen Dateien aus typeshed und liefern sie mit aus.

Die Entwicklungsumgebung PyCharm von JetBrains, die als Community-Edition kostenlos ist, arbeitet ebenfalls mit den Dateien von typeshed und expliziten Typinformationen. In der IDE erfolgt die Prüfung bereits während des Tippens und nicht erst im Anschluss wie bei einem Type-Checker.

PyCharm überprüft die Typen während der Eingabe.

Die Typinformationen der Standardmodule fließen durch die Ausgabe von PyCharm in den Entwicklungsprozess mit ein und sie verbessern die Autovervollständigung.

Für die Autovervollständigung greift PyCharm auf die Typinformationen zurück.

Neben den einfachen Datentypen können Entwickler in Python 3.6 Listen, Dictionaries oder komplexere Objekte mit expliziten Typinformationen versehen, wie folgende Beispiele zeigen:

l: List[str] = []   #Liste, die nur Strings enthält
d: Dict[str, int] #Dictionary mit Schlüsseln vom
#Typ str und Werten vom Typ int

t1: Tuple[int, int] #Tuple aus zwei Integer-Werten
t2: Tuple[int, ...] #Tuple aus beliebig vielen Integer-Werten

Damit Python diese komplexeren Typen erkennt, muss man sie aus dem Modul typing importieren:

from typing import List, Tuple, Dict

Folgender Code zeigt eine Funktion ohne Rückgabewert, die als Parameter ein iterierbares Objekt erwartet:

from typing import Iterable

def print_names(names: Iterable[str]) -> None:
for n in names:
print('Servus, {}'.format(n))