Einführung in Python-Typanmerkungen

    Einleitung



    Illustrationsautorin - Magdalena Tomczyk


    Zweiter Teil


    Python ist eine Sprache mit dynamischer Typisierung und ermöglicht es uns, mit Variablen verschiedener Typen ziemlich frei zu arbeiten. Beim Schreiben von Code gehen wir jedoch irgendwie davon aus, welche Variablentypen verwendet werden (dies kann durch eine Einschränkung des Algorithmus oder eine Geschäftslogik verursacht werden). Und für den korrekten Betrieb des Programms ist es wichtig, so schnell wie möglich Fehler bei der Übertragung von Daten des falschen Typs zu finden.


    Das Beibehalten der Idee der dynamischen Ententypisierung in modernen Versionen von Python (3.6+) unterstützt Anmerkungen für die Typen von Variablen, Klassenfeldern, Argumenten und Rückgabewerten von Funktionen:



    Typanmerkungen werden vom Python-Interpreter einfach gelesen und nicht mehr verarbeitet, sind jedoch für die Verwendung mit Code von Drittanbietern verfügbar und hauptsächlich für statische Analysegeräte vorgesehen.


    Ich heiße Andrey Tikhonov und bin in Lamoda an der Entwicklung des Backends beteiligt.


    In diesem Artikel möchte ich die Grundlagen der Verwendung von Typanmerkungen erläutern und typische Beispiele ansehen, die von Paketanmerkungen implementiert werden typing.


    Tools, die Anmerkungen unterstützen


    Typanmerkungen werden von vielen Python-IDEs unterstützt, die falschen Code hervorheben oder während des Tippvorgangs Hinweise geben.


    Zum Beispiel sieht Pycharm so aus:


    Fehler beim Hervorheben



    Tipps:



    Typanmerkungen werden auch von Konsolenlinern 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 die gleiche 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 Analysatoren kann variieren. Zum Beispiel behandeln mypy und pycharm die Änderung des Variablentyps auf unterschiedliche Weise. In den Beispielen werde ich mich auf die Ausgabe von mypy konzentrieren.


    In einigen Beispielen kann der Code beim Start ausnahmslos funktionieren, kann jedoch aufgrund der Verwendung von Variablen des falschen Typs logische Fehler enthalten. Und in einigen Beispielen kann es nicht einmal ausgeführt werden.


    Die Grundlagen


    Im Gegensatz zu älteren Versionen von Python werden Typanmerkungen nicht in Kommentaren oder Docstring geschrieben, sondern direkt in den Code. Zum einen bricht es die Rückwärtskompatibilität, zum anderen bedeutet es eindeutig, dass es Teil des Codes ist und entsprechend verarbeitet werden kann.


    Im einfachsten Fall enthält die Annotation den direkt erwarteten Typ. Komplexere Fälle werden unten diskutiert. Wenn die Basisklasse als Annotation angegeben ist, können Instanzen ihrer Erben als Werte übergeben werden. Sie können jedoch nur die Features verwenden, die in der Basisklasse implementiert sind.


    Anmerkungen zu Variablen werden mit einem Doppelpunkt nach dem Bezeichner geschrieben. Danach kann es zu einer Initialisierung des Wertes kommen. Zum Beispiel


    price: int = 5
    title: str

    Funktionsparameter werden wie Variablen kommentiert, und der Rückgabewert wird nach dem Pfeil -> und vor dem abschließenden Doppelpunkt angezeigt  . Zum Beispiel


    defindent_right(s: str, width: int) -> str:return" " * (max(0, width - len(s))) + s

    Bei Klassenfeldern müssen Anmerkungen bei der Definition einer Klasse explizit angegeben werden. Analysatoren können sie jedoch automatisch basierend auf der __init__Methode ausgeben. In diesem Fall stehen sie jedoch während der Programmausführung nicht zur Verfügung. Weitere Informationen zum Arbeiten mit Anmerkungen in Runtime finden Sie im zweiten Teil des Artikels.


    classBook:
        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 der Verwendung von Datenklassen Feldtypen in der Klasse angegeben werden. Lesen Sie mehr über Datenklasse


    Eingebaute Typen


    Obwohl Sie Standardtypen als Anmerkungen verwenden können, sind viele nützliche Informationen im Modul verborgen typing.


    Optional


    Wenn Sie eine Variable mit einem Typ markieren intund versuchen, sie zuzuweisen None, wird ein Fehler angezeigt:


    Incompatible types in assignment (expression has type "None", variable has type "int")


    In diesem Fall enthält das Modul eine OptionalTypanmerkung, die den jeweiligen Typ angibt. 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

    Any


    Manchmal möchten Sie die möglichen Typen einer Variablen nicht einschränken. Zum Beispiel, wenn es wirklich nicht wichtig ist oder wenn Sie die Verarbeitung verschiedener Typen auf eigene Faust planen. In diesem Fall können Sie die Annotation verwenden Any. Der folgende Code mypy wird nicht schwören:


    unknown_item: Any = 1
    print(unknown_item)
    print(unknown_item.startswith("hello"))
    print(unknown_item // 0)

    Die Frage kann sich stellen, warum nicht verwendet werden object? In diesem Fall wird jedoch davon ausgegangen, dass mindestens ein Objekt übertragen werden kann, es kann jedoch nur als Instanz behandelt werden object.


    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 alle, sondern nur einige Typen verwendet werden dürfen, können Sie die Annotation typing.Unionmit der Liste der Typen in eckigen Klammern verwenden.


    defhundreds(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]"

    Die Zusammenfassung ist übrigens Optional[T]gleichwertig Union[T, None], obwohl eine solche Aufzeichnung nicht empfohlen wird.


    Sammlungen


    Der Typ-Anmerkungsmechanismus unterstützt den Generics-Mechanismus ( Generics , genauer im zweiten Teil des Artikels), der die Angabe von Elementtypen ermöglicht, die in Containern für Container gespeichert sind.


    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 sie für eine solche Annotation nicht mehr geeignet. Dafür gibt es typing.List. Auf dieselbe Weise, 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 ähnlicher Elemente enthält. Aber zur gleichen Zeit gibt es keine Einschränkungen für Beschriftungselemente: Sie können die verwenden Any, Optional, Listund andere. Wenn der Elementtyp nicht angegeben ist, wird davon ausgegangen Any.


    Neben der Liste gibt es ähnliche Anmerkungen zu den Sets: typing.Setund typing.FrozenSet.


    Tuples


    Im Gegensatz zu Listen werden Tupel häufig für Elemente unterschiedlichen Typs verwendet. Die Syntax ist mit einem Unterschied ähnlich: Der Typ jedes Elements des Tupels wird in eckigen Klammern separat angegeben.


    Wenn Sie ein Tupel verwenden möchten, das der Liste ähnlich ist: Speichern Sie eine unbekannte Anzahl desselben Elementtyps, können Sie die Ellipse ( ...) verwenden.


    Annotationen Tupleohne Angabe von Elementtypen funktionieren auf dieselbe Weise.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 verwendet typing.Dict. Separat werden der Schlüsseltyp und der Werttyp mit Anmerkungen versehen:


    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 verwendet typing.DefaultDictundtyping.OrderedDict


    Das Ergebnis der Funktion


    Um den Typ des Ergebnisses der Funktion anzugeben, können Sie eine beliebige Anmerkung verwenden. Es gibt jedoch einige Sonderfälle.


    Wenn die Funktion nichts zurückgibt (z. B. like print), ist das Ergebnis immer gleich None. Für Anmerkungen verwenden wir auch None.


    Gültige Optionen zum Ausführen einer solchen Funktion sind: eine explizite Rückgabe None, eine Rückgabe ohne Angabe eines Wertes und eine Beendigung ohne Aufruf return.


    defnothing(a: int) -> None:if a == 1:
            returnelif a == 2:
            returnNoneelif a == 3:
            return""# No return value expectedelse:
            pass

    Wenn die Funktion niemals die Kontrolle zurückgibt (z. B. wie sys.exit), sollten Sie die Annotation verwenden NoReturn:


    defforever() -> NoReturn:whileTrue:
            pass

    Wenn dies eine Generatorfunktion ist, das heißt, ihr Körper enthält einen Operator yield, können Sie die Annotation für die zurückgegebene verwenden Iterable[T], oder Generator[YT, ST, RT]:


    defgenerate_two() -> Iterable[int]:yield1yield"2"# Incompatible types in "yield" (actual type "str", expected type "int")

    Anstelle des Schlusses


    Für viele Situationen im Typisierungsmodul gibt es geeignete Typen, jedoch werde ich nicht alles berücksichtigen, da das Verhalten dem betrachteten ähnlich ist.
    Zum Beispiel gibt es Iteratoreine generische Version für collections.abc.Iterator, typing.SupportsIntum anzuzeigen, dass das Objekt die Methode unterstützt __int__, oder Callablefür Funktionen und Objekte, die die Methode unterstützen__call__


    Der Standard definiert auch das Format von Anmerkungen in Form von Kommentaren und Stub-Dateien, die nur Informationen für statische Analysatoren enthalten.


    Im nächsten Artikel möchte ich auf den Mechanismus von Generika und die Verarbeitung von Anmerkungen zur Laufzeit eingehen.


    Jetzt auch beliebt: