Einführung in Python-Typen¶
Python hat Unterstützung für optionale „Typhinweise“ (Englisch: „Type Hints“). Auch „Typ Annotationen“ genannt.
Diese „Typhinweise“ oder -Annotationen sind eine spezielle Syntax, die es erlaubt, den Typ einer Variablen zu deklarieren.
Durch das Deklarieren von Typen für Ihre Variablen können Editoren und Tools bessere Unterstützung bieten.
Dies ist lediglich eine schnelle Anleitung / Auffrischung über Pythons Typhinweise. Sie deckt nur das Minimum ab, das nötig ist, um diese mit FastAPI zu verwenden ... was tatsächlich sehr wenig ist.
FastAPI basiert vollständig auf diesen Typhinweisen, sie geben der Anwendung viele Vorteile und Möglichkeiten.
Aber selbst wenn Sie FastAPI nie verwenden, wird es für Sie nützlich sein, ein wenig darüber zu lernen.
Hinweis
Wenn Sie ein Python-Experte sind und bereits alles über Typhinweise wissen, überspringen Sie dieses Kapitel und fahren Sie mit dem nächsten fort.
Motivation¶
Fangen wir mit einem einfachen Beispiel an:
def get_full_name(first_name, last_name):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
Dieses Programm gibt aus:
John Doe
Die Funktion macht Folgendes:
- Nimmt einen
first_name
undlast_name
. - Schreibt den ersten Buchstaben eines jeden Wortes groß, mithilfe von
title()
. - Verkettet sie mit einem Leerzeichen in der Mitte.
def get_full_name(first_name, last_name):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
Bearbeiten Sie es¶
Es ist ein sehr einfaches Programm.
Aber nun stellen Sie sich vor, Sie würden es selbst schreiben.
Irgendwann sind die Funktions-Parameter fertig, Sie starten mit der Definition des Körpers ...
Aber dann müssen Sie „diese Methode aufrufen, die den ersten Buchstaben in Großbuchstaben umwandelt“.
War es upper
? War es uppercase
? first_uppercase
? capitalize
?
Dann versuchen Sie es mit dem langjährigen Freund des Programmierers, der Editor-Autovervollständigung.
Sie geben den ersten Parameter der Funktion ein, first_name
, dann einen Punkt (.
) und drücken Strg+Leertaste
, um die Vervollständigung auszulösen.
Aber leider erhalten Sie nichts Nützliches:
Typen hinzufügen¶
Lassen Sie uns eine einzelne Zeile aus der vorherigen Version ändern.
Wir ändern den folgenden Teil, die Parameter der Funktion, von:
first_name, last_name
zu:
first_name: str, last_name: str
Das war's.
Das sind die „Typhinweise“:
def get_full_name(first_name: str, last_name: str):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
Das ist nicht das gleiche wie das Deklarieren von Defaultwerten, wie es hier der Fall ist:
first_name="john", last_name="doe"
Das ist eine andere Sache.
Wir verwenden Doppelpunkte (:
), nicht Gleichheitszeichen (=
).
Und das Hinzufügen von Typhinweisen ändert normalerweise nichts an dem, was ohne sie passieren würde.
Aber jetzt stellen Sie sich vor, Sie sind wieder mitten in der Erstellung dieser Funktion, aber mit Typhinweisen.
An derselben Stelle versuchen Sie, die Autovervollständigung mit „Strg+Leertaste“ auszulösen, und Sie sehen:
Hier können Sie durch die Optionen blättern, bis Sie diejenige finden, bei der es „Klick“ macht:
Mehr Motivation¶
Sehen Sie sich diese Funktion an, sie hat bereits Typhinweise:
def get_name_with_age(name: str, age: int):
name_with_age = name + " is this old: " + age
return name_with_age
Da der Editor die Typen der Variablen kennt, erhalten Sie nicht nur Code-Vervollständigung, sondern auch eine Fehlerprüfung:
Jetzt, da Sie wissen, dass Sie das reparieren müssen, konvertieren Sie age
mittels str(age)
in einen String:
def get_name_with_age(name: str, age: int):
name_with_age = name + " is this old: " + str(age)
return name_with_age
Deklarieren von Typen¶
Sie haben gerade den Haupt-Einsatzort für die Deklaration von Typhinweisen gesehen. Als Funktionsparameter.
Das ist auch meistens, wie sie in FastAPI verwendet werden.
Einfache Typen¶
Sie können alle Standard-Python-Typen deklarieren, nicht nur str
.
Zum Beispiel diese:
int
float
bool
bytes
def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes):
return item_a, item_b, item_c, item_d, item_d, item_e
Generische Typen mit Typ-Parametern¶
Es gibt Datenstrukturen, die andere Werte enthalten können, wie etwa dict
, list
, set
und tuple
. Die inneren Werte können auch ihren eigenen Typ haben.
Diese Typen mit inneren Typen werden „generische“ Typen genannt. Es ist möglich, sie mit ihren inneren Typen zu deklarieren.
Um diese Typen und die inneren Typen zu deklarieren, können Sie Pythons Standardmodul typing
verwenden. Es existiert speziell für die Unterstützung dieser Typhinweise.
Neuere Python-Versionen¶
Die Syntax, welche typing
verwendet, ist kompatibel mit allen Versionen, von Python 3.6 aufwärts zu den neuesten, inklusive Python 3.9, Python 3.10, usw.
Mit der Weiterentwicklung von Python kommen neuere Versionen heraus, mit verbesserter Unterstützung für Typannotationen, und in vielen Fällen müssen Sie gar nicht mehr das typing
-Modul importieren, um Typannotationen zu schreiben.
Wenn Sie eine neuere Python-Version für Ihr Projekt wählen können, werden Sie aus dieser zusätzlichen Vereinfachung Nutzen ziehen können.
In der gesamten Dokumentation gibt es Beispiele, welche kompatibel mit unterschiedlichen Python-Versionen sind (wenn es Unterschiede gibt).
Zum Beispiel bedeutet „Python 3.6+“, dass das Beispiel kompatibel mit Python 3.6 oder höher ist (inklusive 3.7, 3.8, 3.9, 3.10, usw.). Und „Python 3.9+“ bedeutet, es ist kompatibel mit Python 3.9 oder höher (inklusive 3.10, usw.).
Wenn Sie über die neueste Version von Python verfügen, verwenden Sie die Beispiele für die neueste Version, diese werden die beste und einfachste Syntax haben, zum Beispiel, „Python 3.10+“.
Liste¶
Definieren wir zum Beispiel eine Variable, die eine list
von str
– eine Liste von Strings – sein soll.
Deklarieren Sie die Variable mit der gleichen Doppelpunkt-Syntax (:
).
Als Typ nehmen Sie list
.
Da die Liste ein Typ ist, welcher innere Typen enthält, werden diese von eckigen Klammern umfasst:
def process_items(items: list[str]):
for item in items:
print(item)
Von typing
importieren Sie List
(mit Großbuchstaben L
):
from typing import List
def process_items(items: List[str]):
for item in items:
print(item)
Deklarieren Sie die Variable mit der gleichen Doppelpunkt-Syntax (:
).
Als Typ nehmen Sie das List
, das Sie von typing
importiert haben.
Da die Liste ein Typ ist, welcher innere Typen enthält, werden diese von eckigen Klammern umfasst:
from typing import List
def process_items(items: List[str]):
for item in items:
print(item)
Tipp
Die inneren Typen in den eckigen Klammern werden als „Typ-Parameter“ bezeichnet.
In diesem Fall ist str
der Typ-Parameter, der an List
übergeben wird (oder list
in Python 3.9 und darüber).
Das bedeutet: Die Variable items
ist eine Liste – list
– und jedes der Elemente in dieser Liste ist ein String – str
.
Tipp
Wenn Sie Python 3.9 oder höher verwenden, müssen Sie List
nicht von typing
importieren, Sie können stattdessen den regulären list
-Typ verwenden.
Auf diese Weise kann Ihr Editor Sie auch bei der Bearbeitung von Einträgen aus der Liste unterstützen:
Ohne Typen ist das fast unmöglich zu erreichen.
Beachten Sie, dass die Variable item
eines der Elemente in der Liste items
ist.
Und trotzdem weiß der Editor, dass es sich um ein str
handelt, und bietet entsprechende Unterstützung.
Tupel und Menge¶
Das Gleiche gilt für die Deklaration eines Tupels – tuple
– und einer Menge – set
:
def process_items(items_t: tuple[int, int, str], items_s: set[bytes]):
return items_t, items_s
from typing import Set, Tuple
def process_items(items_t: Tuple[int, int, str], items_s: Set[bytes]):
return items_t, items_s
Das bedeutet:
- Die Variable
items_t
ist eintuple
mit 3 Elementen, einemint
, einem weiterenint
und einemstr
. - Die Variable
items_s
ist einset
, und jedes seiner Elemente ist vom Typbytes
.
Dict¶
Um ein dict
zu definieren, übergeben Sie zwei Typ-Parameter, getrennt durch Kommas.
Der erste Typ-Parameter ist für die Schlüssel des dict
.
Der zweite Typ-Parameter ist für die Werte des dict
:
def process_items(prices: dict[str, float]):
for item_name, item_price in prices.items():
print(item_name)
print(item_price)
from typing import Dict
def process_items(prices: Dict[str, float]):
for item_name, item_price in prices.items():
print(item_name)
print(item_price)
Das bedeutet:
- Die Variable
prices
ist eindict
:- Die Schlüssel dieses
dict
sind vom Typstr
(z. B. die Namen der einzelnen Artikel). - Die Werte dieses
dict
sind vom Typfloat
(z. B. der Preis jedes Artikels).
- Die Schlüssel dieses
Union¶
Sie können deklarieren, dass eine Variable einer von verschiedenen Typen sein kann, zum Beispiel ein int
oder ein str
.
In Python 3.6 und höher (inklusive Python 3.10) können Sie den Union
-Typ von typing
verwenden und die möglichen Typen innerhalb der eckigen Klammern auflisten.
In Python 3.10 gibt es zusätzlich eine neue Syntax, die es erlaubt, die möglichen Typen getrennt von einem vertikalen Balken (|
) aufzulisten.
def process_item(item: int | str):
print(item)
from typing import Union
def process_item(item: Union[int, str]):
print(item)
In beiden Fällen bedeutet das, dass item
ein int
oder ein str
sein kann.
Vielleicht None
¶
Sie können deklarieren, dass ein Wert ein str
, aber vielleicht auch None
sein kann.
In Python 3.6 und darüber (inklusive Python 3.10) können Sie das deklarieren, indem Sie Optional
vom typing
Modul importieren und verwenden.
from typing import Optional
def say_hi(name: Optional[str] = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
Wenn Sie Optional[str]
anstelle von nur str
verwenden, wird Ihr Editor Ihnen dabei helfen, Fehler zu erkennen, bei denen Sie annehmen könnten, dass ein Wert immer eine String (str
) ist, obwohl er auch None
sein könnte.
Optional[Something]
ist tatsächlich eine Abkürzung für Union[Something, None]
, diese beiden sind äquivalent.
Das bedeutet auch, dass Sie in Python 3.10 Something | None
verwenden können:
def say_hi(name: str | None = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
from typing import Optional
def say_hi(name: Optional[str] = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
from typing import Union
def say_hi(name: Union[str, None] = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
Union
oder Optional
verwenden?¶
Wenn Sie eine Python-Version unterhalb 3.10 verwenden, hier ist mein sehr subjektiver Standpunkt dazu:
- 🚨 Vermeiden Sie
Optional[SomeType]
- Stattdessen ✨ verwenden Sie
Union[SomeType, None]
✨.
Beide sind äquivalent und im Hintergrund dasselbe, aber ich empfehle Union
statt Optional
, weil das Wort „optional“ impliziert, dass dieser Wert, zum Beispiel als Funktionsparameter, optional ist. Tatsächlich bedeutet es aber nur „Der Wert kann None
sein“, selbst wenn der Wert nicht optional ist und benötigt wird.
Ich denke, Union[SomeType, None]
ist expliziter bezüglich seiner Bedeutung.
Es geht nur um Wörter und Namen. Aber diese Worte können beeinflussen, wie Sie und Ihre Teamkollegen über den Code denken.
Nehmen wir zum Beispiel diese Funktion:
from typing import Optional
def say_hi(name: Optional[str]):
print(f"Hey {name}!")
Der Parameter name
ist definiert als Optional[str]
, aber er ist nicht optional, Sie können die Funktion nicht ohne diesen Parameter aufrufen:
say_hi() # Oh, nein, das löst einen Fehler aus! 😱
Der name
Parameter wird immer noch benötigt (nicht optional), weil er keinen Default-Wert hat. name
akzeptiert aber dennoch None
als Wert:
say_hi(name=None) # Das funktioniert, None is gültig 🎉
Die gute Nachricht ist, dass Sie sich darüber keine Sorgen mehr machen müssen, wenn Sie Python 3.10 verwenden, da Sie einfach |
verwenden können, um Vereinigungen von Typen zu definieren:
def say_hi(name: str | None):
print(f"Hey {name}!")
Und dann müssen Sie sich nicht mehr um Namen wie Optional
und Union
kümmern. 😎
Generische Typen¶
Diese Typen, die Typ-Parameter in eckigen Klammern akzeptieren, werden generische Typen oder Generics genannt.
Sie können die eingebauten Typen als Generics verwenden (mit eckigen Klammern und Typen darin):
list
tuple
set
dict
Verwenden Sie für den Rest, wie unter Python 3.8, das typing
-Modul:
Union
Optional
(so wie unter Python 3.8)- ... und andere.
In Python 3.10 können Sie als Alternative zu den Generics Union
und Optional
den vertikalen Balken (|
) verwenden, um Vereinigungen von Typen zu deklarieren, das ist besser und einfacher.
Sie können die eingebauten Typen als Generics verwenden (mit eckigen Klammern und Typen darin):
list
tuple
set
dict
Verwenden Sie für den Rest, wie unter Python 3.8, das typing
-Modul:
Union
Optional
- ... und andere.
List
Tuple
Set
Dict
Union
Optional
- ... und andere.
Klassen als Typen¶
Sie können auch eine Klasse als Typ einer Variablen deklarieren.
Nehmen wir an, Sie haben eine Klasse Person
, mit einem Namen:
class Person:
def __init__(self, name: str):
self.name = name
def get_person_name(one_person: Person):
return one_person.name
Dann können Sie eine Variable vom Typ Person
deklarieren:
class Person:
def __init__(self, name: str):
self.name = name
def get_person_name(one_person: Person):
return one_person.name
Und wiederum bekommen Sie die volle Editor-Unterstützung:
Beachten Sie, das bedeutet: „one_person
ist eine Instanz der Klasse Person
“.
Es bedeutet nicht: „one_person
ist die Klasse genannt Person
“.
Pydantic Modelle¶
Pydantic ist eine Python-Bibliothek für die Validierung von Daten.
Sie deklarieren die „Form“ der Daten als Klassen mit Attributen.
Und jedes Attribut hat einen Typ.
Dann erzeugen Sie eine Instanz dieser Klasse mit einigen Werten, und Pydantic validiert die Werte, konvertiert sie in den passenden Typ (falls notwendig) und gibt Ihnen ein Objekt mit allen Daten.
Und Sie erhalten volle Editor-Unterstützung für dieses Objekt.
Ein Beispiel aus der offiziellen Pydantic Dokumentation:
from datetime import datetime
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str = "John Doe"
signup_ts: datetime | None = None
friends: list[int] = []
external_data = {
"id": "123",
"signup_ts": "2017-06-01 12:22",
"friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
from datetime import datetime
from typing import Union
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str = "John Doe"
signup_ts: Union[datetime, None] = None
friends: list[int] = []
external_data = {
"id": "123",
"signup_ts": "2017-06-01 12:22",
"friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
from datetime import datetime
from typing import List, Union
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str = "John Doe"
signup_ts: Union[datetime, None] = None
friends: List[int] = []
external_data = {
"id": "123",
"signup_ts": "2017-06-01 12:22",
"friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
Info
Um mehr über Pydantic zu erfahren, schauen Sie sich dessen Dokumentation an.
FastAPI basiert vollständig auf Pydantic.
Viel mehr von all dem werden Sie in praktischer Anwendung im Tutorial - Benutzerhandbuch sehen.
Tipp
Pydantic verhält sich speziell, wenn Sie Optional
oder Union[Etwas, None]
ohne einen Default-Wert verwenden. Sie können darüber in der Pydantic Dokumentation unter Required fields mehr erfahren.
Typhinweise mit Metadaten-Annotationen¶
Python bietet auch die Möglichkeit, zusätzliche Metadaten in Typhinweisen unterzubringen, mittels Annotated
.
In Python 3.9 ist Annotated
ein Teil der Standardbibliothek, Sie können es von typing
importieren.
from typing import Annotated
def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
return f"Hello {name}"
In Versionen niedriger als Python 3.9 importieren Sie Annotated
von typing_extensions
.
Es wird bereits mit FastAPI installiert sein.
from typing_extensions import Annotated
def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
return f"Hello {name}"
Python selbst macht nichts mit Annotated
. Für Editoren und andere Tools ist der Typ immer noch str
.
Aber Sie können Annotated
nutzen, um FastAPI mit Metadaten zu versorgen, die ihm sagen, wie sich ihre Anwendung verhalten soll.
Wichtig ist, dass der erste Typ-Parameter, den Sie Annotated
übergeben, der tatsächliche Typ ist. Der Rest sind Metadaten für andere Tools.
Im Moment müssen Sie nur wissen, dass Annotated
existiert, und dass es Standard-Python ist. 😎
Später werden Sie sehen, wie mächtig es sein kann.
Tipp
Der Umstand, dass es Standard-Python ist, bedeutet, dass Sie immer noch die bestmögliche Entwickler-Erfahrung in ihrem Editor haben, sowie mit den Tools, die Sie nutzen, um ihren Code zu analysieren, zu refaktorisieren, usw. ✨
Und ebenfalls, dass Ihr Code sehr kompatibel mit vielen anderen Python-Tools und -Bibliotheken sein wird. 🚀
Typhinweise in FastAPI¶
FastAPI macht sich diese Typhinweise zunutze, um mehrere Dinge zu tun.
Mit FastAPI deklarieren Sie Parameter mit Typhinweisen, und Sie erhalten:
- Editorunterstützung.
- Typ-Prüfungen.
... und FastAPI verwendet dieselben Deklarationen, um:
- Anforderungen zu definieren: aus Anfrage-Pfadparametern, Abfrageparametern, Header-Feldern, Bodys, Abhängigkeiten, usw.
- Daten umzuwandeln: aus der Anfrage in den erforderlichen Typ.
- Daten zu validieren: aus jeder Anfrage:
- Automatische Fehler generieren, die an den Client zurückgegeben werden, wenn die Daten ungültig sind.
- Die API mit OpenAPI zu dokumentieren:
- Die dann von den Benutzeroberflächen der automatisch generierten interaktiven Dokumentation verwendet wird.
Das mag alles abstrakt klingen. Machen Sie sich keine Sorgen. Sie werden all das in Aktion sehen im Tutorial - Benutzerhandbuch.
Das Wichtigste ist, dass FastAPI durch die Verwendung von Standard-Python-Typen an einer einzigen Stelle (anstatt weitere Klassen, Dekoratoren usw. hinzuzufügen) einen Großteil der Arbeit für Sie erledigt.
Info
Wenn Sie bereits das ganze Tutorial durchgearbeitet haben und mehr über Typen erfahren wollen, dann ist eine gute Ressource der „Cheat Sheet“ von mypy
.