Einführung

Illustration von Magdalena Tomczyk
Zweiter Teil
Python ist eine Sprache mit dynamischer Typisierung und ermöglicht es uns, Variablen verschiedener Typen frei zu bearbeiten. Beim Schreiben von Code wird jedoch auf die eine oder andere Weise davon ausgegangen, welche Arten von Variablen verwendet werden (dies kann durch eine Einschränkung des Algorithmus oder der Geschäftslogik verursacht werden). Und für den korrekten Betrieb des Programms ist es wichtig, dass wir so früh wie möglich Fehler im Zusammenhang mit der Übertragung von Daten des falschen Typs finden.
Unter Beibehaltung der Idee der dynamischen Ententypisierung in modernen Versionen von Python (3.6+) werden Anmerkungen zu Variablentypen, Klassenfeldern, Argumenten und Rückgabewerten von Funktionen unterstützt:
Typanmerkungen werden einfach vom Python-Interpreter gelesen und nicht mehr verarbeitet, stehen jedoch für die Verwendung mit Code von Drittanbietern zur Verfügung und sind hauptsächlich für die Verwendung durch statische Analysegeräte vorgesehen.
Mein Name ist Andrey Tikhonov und ich bin bei Lamoda in der Backend-Entwicklung tätig.
In diesem Artikel möchte ich die Grundlagen der Verwendung von Typanmerkungen erläutern und die typischen Beispiele betrachten, die durch die typing
Anmerkungen implementiert werden.
Anmerkungswerkzeuge
Typanmerkungen werden von vielen Python-IDEs unterstützt, die während der Eingabe falschen Code hervorheben oder Hinweise geben.
So sieht es beispielsweise in Pycharm aus:
Fehlerhervorhebung

Tipps:

Typanmerkungen werden auch von Konsolenlintern verarbeitet.
Hier ist die Ausgabe von Pylint:
$ pylint example.py ************* Module example example.py:7:6: E1101: Instance of 'int' has no 'startswith' member (no-member)
Aber für dieselbe Datei, die mypy gefunden hat:
$ mypy example.py example.py:7: error: "int" has no attribute "startswith" example.py:10: error: Unsupported operand types for // ("str" and "int")
Das Verhalten verschiedener Analysegeräte kann variieren. Zum Beispiel behandeln mypy und pycharm den Typ einer Variablen unterschiedlich. Weiter in den Beispielen werde ich mich auf die Ausgabe von mypy konzentrieren.
In einigen Beispielen kann der Code beim Start ausnahmslos ausgeführt werden, kann jedoch aufgrund der Verwendung von Variablen des falschen Typs logische Fehler enthalten. In einigen Beispielen wird es möglicherweise nicht einmal ausgeführt.
Die Grundlagen
Im Gegensatz zu älteren Versionen von Python werden Typanmerkungen nicht in Kommentaren oder Dokumentzeichenfolgen geschrieben, sondern direkt im Code. Dies beeinträchtigt zum einen die Abwärtskompatibilität, zum anderen bedeutet dies eindeutig, dass es Teil des Codes ist und entsprechend verarbeitet werden kann
Im einfachsten Fall enthält die Anmerkung den direkt erwarteten Typ. Komplexere Fälle werden unten diskutiert. Wenn die Basisklasse als Annotation angegeben ist, ist es akzeptabel, Instanzen ihrer Nachkommen als Werte zu übergeben. Sie können jedoch nur die Funktionen verwenden, die in der Basisklasse implementiert sind.
Anmerkungen für Variablen werden nach dem Doppelpunkt nach dem Bezeichner geschrieben. Danach kann der Wert initialisiert werden. Zum Beispiel
price: int = 5 title: str
Funktionsparameter werden wie Variablen mit Anmerkungen versehen, und der Rückgabewert wird nach dem Pfeil ->
und vor dem letzten Doppelpunkt angezeigt. Zum Beispiel
def indent_right(s: str, width: int) -> str: return " " * (max(0, width - len(s))) + s
Bei Klassenfeldern müssen beim Definieren einer Klasse Anmerkungen explizit angegeben werden. Analysatoren können sie jedoch automatisch basierend auf der Methode __init__
In diesem Fall sind sie jedoch zur Laufzeit nicht verfügbar. Weitere Informationen zum Arbeiten mit Anmerkungen zur Laufzeit finden Sie im zweiten Teil des Artikels
class Book: title: str author: str def __init__(self, title: str, author: str) -> None: self.title = title self.author = author b: Book = Book(title='Fahrenheit 451', author='Bradbury')
Übrigens müssen bei Verwendung der Datenklasse Feldtypen in der Klasse angegeben werden. Mehr zur Datenklasse
Eingebaute Typen
Obwohl Sie Standardtypen als Anmerkungen verwenden können, sind im typing
viele nützliche Dinge verborgen.
Optional
Wenn Sie die Variable mit dem Typ int
markieren und versuchen, sie None
zuzuweisen, tritt ein Fehler auf:
Incompatible types in assignment (expression has type "None", variable has type "int")
In solchen Fällen wird im Typisierungsmodul eine Optional
Anmerkung mit einem bestimmten Typ bereitgestellt. Bitte beachten Sie, dass der Typ der optionalen Variablen in eckigen Klammern angegeben ist.
from typing import Optional amount: int amount = None
Beliebig
Manchmal möchten Sie die möglichen Typen einer Variablen nicht einschränken. Zum Beispiel, wenn es wirklich nicht wichtig ist oder wenn Sie vorhaben, verschiedene Typen selbst zu verarbeiten. In diesem Fall können Sie die Annotation Any
. Mypy schwört nicht auf den folgenden Code:
unknown_item: Any = 1 print(unknown_item) print(unknown_item.startswith("hello")) print(unknown_item // 0)
Es kann sich die Frage stellen, warum nicht object
? In diesem Fall wird jedoch davon ausgegangen, dass jedes Objekt zwar übertragen werden kann, jedoch nur als object
zugegriffen werden kann.
unknown_object: object print(unknown_object) print(unknown_object.startswith("hello"))
Union
In Fällen, in denen nicht nur beliebige, sondern nur einige Typen verwendet werden müssen, können Sie die Annotation typing.Union
mit einer Liste von Typen in eckigen Klammern verwenden.
def hundreds(x: Union[int, float]) -> int: return (int(x) // 100) % 10 hundreds(100.0) hundreds(100) hundreds("100")
Übrigens entspricht die Optional[T]
-Anmerkung Union[T, None]
, obwohl ein solcher Eintrag nicht empfohlen wird.
Sammlungen
Der Typanmerkungsmechanismus unterstützt den generischen Mechanismus ( Generika , mehr im zweiten Teil des Artikels), mit dem die darin enthaltenen Elementtypen für Container angegeben werden können.
Listen
Um anzuzeigen, dass eine Variable eine Liste enthält, können Sie den Listentyp als Anmerkung verwenden. Wenn Sie jedoch angeben möchten, welche Elemente die Liste enthält, ist eine solche Anmerkung nicht mehr geeignet. Es wird typing.List
. typing.List
dafür. Ähnlich wie wir den Typ einer optionalen Variablen angegeben haben, geben wir den Typ der Listenelemente in eckigen Klammern an.
titles: List[str] = ["hello", "world"] titles.append(100500)
Es wird angenommen, dass die Liste eine unbestimmte Anzahl von Elementen desselben Typs enthält. Es gibt jedoch keine Einschränkungen für die Annotation eines Elements: Sie können Any
, Optional
, List
und andere verwenden. Wenn der Elementtyp nicht angegeben ist, wird davon ausgegangen, dass es sich um Any
.
Zusätzlich zur Liste gelten ähnliche Anmerkungen für Mengen: typing.Set
und typing.FrozenSet
.
Tupel
Tupel werden im Gegensatz zu Listen häufig für heterogene Elemente verwendet. Die Syntax ist mit einem Unterschied ähnlich: Die eckigen Klammern geben den Typ jedes Elements des Tupels einzeln an.
Wenn Sie ein Tupel ähnlich der Liste verwenden möchten: Speichern Sie eine unbekannte Anzahl von Elementen desselben Typs. Verwenden Sie dazu die Auslassungspunkte ( ...
).
Annotation Tuple
ohne Angabe von Elementtypen funktioniert ähnlich wie Tuple[Any, ...]
price_container: Tuple[int] = (1,) price_container = ("hello")
Wörterbücher
Für Wörterbücher wird typing.Dict
verwendet. Der Schlüsseltyp und der Werttyp werden separat kommentiert:
book_authors: Dict[str, str] = {"Fahrenheit 451": "Bradbury"} book_authors["1984"] = 0
Ähnlich verwendete typing.DefaultDict
und typing.OrderedDict
Funktionsergebnis
Sie können eine beliebige Anmerkung verwenden, um die Art des Funktionsergebnisses anzugeben. Es gibt jedoch einige Sonderfälle.
Wenn eine Funktion nichts zurückgibt (z. B. print
), lautet das Ergebnis immer None
. Für Anmerkungen verwenden wir auch None
.
Gültige Optionen zum Beenden einer solchen Funktion wären: explizit None
, ohne Angabe eines Werts zurückgeben und beenden, ohne return
aufzurufen.
def nothing(a: int) -> None: if a == 1: return elif a == 2: return None elif a == 3: return ""
Wenn die Funktion niemals zurückgegeben wird (z. B. sys.exit
), sollten Sie die Annotation sys.exit
verwenden:
def forever() -> NoReturn: while True: pass
Wenn es sich um eine Generatorfunktion handelt, Iterable[T]
dass ihr Hauptteil eine yield
enthält, können Sie die Iterable[T]
oder Generator[YT, ST, RT]
für die zurückgegebene Funktion verwenden:
def generate_two() -> Iterable[int]: yield 1 yield "2"
Anstelle einer Schlussfolgerung
Für viele Situationen gibt es geeignete Typen im Typisierungsmodul, ich werde jedoch nicht alles berücksichtigen, da das Verhalten dem betrachteten ähnlich ist.
Beispielsweise gibt es einen Iterator
als generische Version für collections.abc.Iterator
, typing.SupportsInt
um anzugeben, dass das Objekt die Methode Callable
unterstützt, oder Callable
für Funktionen und Objekte, die die Methode __call__
Der Standard definiert auch das Format von Anmerkungen in Form von Kommentaren und Stub-Dateien, die Informationen nur für statische Analysatoren enthalten.
Im nächsten Artikel möchte ich auf den Mechanismus der Arbeit von Generika und die Verarbeitung von Anmerkungen zur Laufzeit eingehen.