19.6 Annotationen 

Die Elemente einer Funktionsschnittstelle, also die Funktionsparameter und der Rückgabewert, lassen sich mit Anmerkungen, sogenannten Annotationen, versehen:
def funktion(p1: Annotation1, p2: Annotation2) -> Annotation3:
Funktionskörper
Bei der Definition einer Funktion kann hinter jeden Parameter ein Doppelpunkt geschrieben werden, gefolgt von einer Annotation. Eine Annotation darf dabei ein beliebiger Python-Ausdruck sein. Die Angabe einer Annotation ist optional. Hinter der Parameterliste kann eine ebenfalls optionale Annotation für den Rückgabewert der Funktion geschrieben werden. Diese wird durch einen Pfeil (->) eingeleitet. Erst hinter dieser Annotation folgt der Doppelpunkt, der den Funktionskörper einleitet.
Annotationen ändern an der Ausführung einer Funktion nichts, man könnte sagen: Dem Python-Interpreter sind Annotationen egal. Das Interessante an ihnen ist, dass man sie über das Attribut __annotations__ des Funktionsobjekts auslesen kann. Da Annotationen beliebige Ausdrücke sein dürfen, kann der Programmierer hier also eine Information pro Parameter und Rückgabewert »speichern«, auf die er zu einem späteren Zeitpunkt – beispielsweise wenn die Funktion mit konkreten Parameterwerten aufgerufen wird – zurückkommt.
Dabei werden die Annotationen über das Attribut __annotations__ in Form eines Dictionarys zugänglich gemacht. Dieses Dictionary enthält die Parameternamen bzw. "return" für die Annotation des Rückgabewertes als Schlüssel und die jeweiligen Annotation-Ausdrücke als Werte. Für die oben dargestellte schematische Funktionsdefinition sieht dieses Dictionary also folgendermaßen aus:
funktion.__annotations__ =
{
"p1" : Annotation1,
"p2" : Annotation2,
"return" : Annotation3
}
Mit Function Annotations könnten Sie also beispielsweise eine Typüberprüfung an der Funktionsschnittstelle durchführen. Dazu definieren wir zunächst eine Funktion samt Annotationen:
def strmult(s: str, n: int) -> str:
return s*n
Die Funktion strmult hat die Aufgabe, einen String s n-mal hintereinander geschrieben zurückzugeben. Das geschieht durch Multiplikation von s und n.
Wir schreiben jetzt eine Funktion call, die dazu in der Lage ist, eine beliebige Funktion, deren Schnittstelle vollständig durch Annotationen beschrieben ist, aufzurufen bzw. eine Exception zu werfen, wenn einer der übergebenen Parameter einen falschen Typ hat:
def call(f, **kwargs):
for arg in kwargs:
if arg not in f.__annotations__:
raise TypeError("Parameter '{}'"
" unbekannt".format(arg))
if not isinstance(kwargs[arg], f.__annotations__[arg]):
raise TypeError("Parameter '{}'"
" hat ungültigen Typ".format(arg))
ret = f(**kwargs)
if type(ret) != f.__annotations__["return"]:
raise TypeError("Ungültiger Rückgabewert")
return ret
Die Funktion call bekommt ein Funktionsobjekt und beliebig viele Schlüsselwortparameter übergeben. Dann greift sie für jeden übergebenen Schlüsselwortparameter auf das Annotation-Dictionary des Funktionsobjekts f zu und prüft, ob ein Parameter dieses Namens überhaupt in der Funktionsdefinition von f vorkommt und – wenn ja – ob die für diesen Parameter übergebene Instanz den richtigen Typ hat. Ist eines von beidem nicht der Fall, wird eine entsprechende Exception geworfen.
Wenn alle Parameter korrekt übergeben wurden, wird das Funktionsobjekt f aufgerufen und der Rückgabewert gespeichert. Dessen Typ wird dann mit dem Datentyp verglichen, der in der Annotation für den Rückgabewert angegeben wurde; wenn er abweicht, wird eine Exception geworfen. Ist alles gut gegangen, wird der Rückgabewert der Funktion f von call durchgereicht:
>>> call(strmult, s="Hallo", n=3)
'HalloHalloHallo'
>>> call(strmult, s="Hallo", n="Welt")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Parameter 'n' hat ungültigen Typ
>>> call(strmult, s=13, n=37)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Parameter 's' hat ungültigen Typ
Um die Überprüfung auf den Rückgabewert testen zu können, muss natürlich die Definition der Funktion strmult verändert werden.