Einführung in Python Type Annotations

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 # Incompatible types in assignment (expression has type "None", variable has type "int") price: Optional[int] price = 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")) # error: "object" has no attribute "startswith" print(unknown_object // 0) # error: Unsupported operand types for // ("object" and "int") 

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") # Argument 1 to "hundreds" has incompatible type "str"; expected "Union[int, float]" 

Ü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) # Argument 1 to "append" of "list" has incompatible type "int"; expected "str" titles = ["hello", 1] # List item 1 has incompatible type "int"; expected "str" items: List = ["hello", 1] 

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") # Incompatible types in assignment (expression has type "str", variable has type "Tuple[int]") price_container = (1, 2) # Incompatible types in assignment (expression has type "Tuple[int, int]", variable has type "Tuple[int]") price_with_title: Tuple[int, str] = (1, "hello") prices: Tuple[int, ...] = (1, 2) prices = (1, ) prices = (1, "str") # Incompatible types in assignment (expression has type "Tuple[int, str]", variable has type "Tuple[int, ...]") something: Tuple = (1, 2, "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 # Incompatible types in assignment (expression has type "int", target has type "str") book_authors[1984] = "Orwell" # Invalid index type "int" for "Dict[str, str]"; expected type "str" 

Ä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 "" # No return value expected else: pass 

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" # Incompatible types in "yield" (actual type "str", expected type "int") 

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.

Source: https://habr.com/ru/post/de432656/


All Articles