Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 484 – Typenhinweise

Autor:
Guido van Rossum <guido at python.org>, Jukka Lehtosalo <jukka.lehtosalo at iki.fi>, Łukasz Langa <lukasz at python.org>
BDFL-Delegate:
Mark Shannon
Discussions-To:
Python-Dev Liste
Status:
Final
Typ:
Standards Track
Thema:
Typisierung
Erstellt:
29-Sep-2014
Python-Version:
3.5
Post-History:
16-Jan-2015, 20-Mar-2015, 17-Apr-2015, 20-May-2015, 22-May-2015
Resolution:
Python-Dev Nachricht

Inhaltsverzeichnis

Zusammenfassung

PEP 3107 führte Syntax für Funktionsannotationen ein, aber die Semantik wurde bewusst offen gelassen. Es gab inzwischen genügend externe Nutzung für statische Typanalyse, sodass die Community von einem Standardvokabular und Basistools innerhalb der Standardbibliothek profitieren würde.

Dieser PEP führt ein provisorisches Modul ein, um diese Standarddefinitionen und -werkzeuge bereitzustellen, zusammen mit einigen Konventionen für Situationen, in denen Annotationen nicht verfügbar sind.

Beachten Sie, dass dieser PEP weiterhin ausdrücklich ANDERE Verwendungen von Annotationen NICHT verbietet, noch (oder verbietet) irgendeine bestimmte Verarbeitung von Annotationen vorschreibt, selbst wenn diese dieser Spezifikation entsprechen. Er ermöglicht lediglich eine bessere Koordination, so wie PEP 333 für Web-Frameworks.

Zum Beispiel ist hier eine einfache Funktion, deren Argument- und Rückgabetyp in den Annotationen deklariert sind

def greeting(name: str) -> str:
    return 'Hello ' + name

Während diese Annotationen zur Laufzeit über das übliche Attribut __annotations__ verfügbar sind, _findet keine Typenprüfung zur Laufzeit statt_. Stattdessen geht der Vorschlag von der Existenz eines separaten Offline-Typenprüfers aus, den Benutzer nach Belieben über ihren Quellcode laufen lassen können. Im Wesentlichen fungiert ein solcher Typenprüfer als sehr leistungsfähiger Linter. (Obwohl es für einzelne Benutzer natürlich möglich wäre, einen ähnlichen Prüfer zur Laufzeit für die Durchsetzung von Design By Contract oder JIT-Optimierungen einzusetzen, sind diese Werkzeuge noch nicht so ausgereift.)

Der Vorschlag ist stark von mypy inspiriert. Zum Beispiel kann der Typ „Sequenz von Ganzzahlen“ als Sequence[int] geschrieben werden. Die eckigen Klammern bedeuten, dass keine neue Syntax zur Sprache hinzugefügt werden muss. Das Beispiel hier verwendet einen benutzerdefinierten Typ Sequence, der aus einem reinen Python-Modul typing importiert wird. Die Notation Sequence[int] funktioniert zur Laufzeit, indem __getitem__() in der Metaklasse implementiert wird (aber ihre Bedeutung ist hauptsächlich für einen Offline-Typenprüfer).

Das Typsystem unterstützt Unions, generische Typen und einen speziellen Typ namens Any, der konsistent mit (d. h. zuweisbar von und zu) allen Typen ist. Dieses letztere Merkmal stammt aus der Idee des graduellen Typisierens. Graduelles Typisieren und das vollständige Typsystem werden in PEP 483 erklärt.

Andere Ansätze, von denen wir uns inspirieren ließen oder mit denen unser Ansatz verglichen und kontrastiert werden kann, werden in PEP 482 beschrieben.

Begründung und Ziele

PEP 3107 fügte Unterstützung für beliebige Annotationen zu Teilen einer Funktionsdefinition hinzu. Obwohl den Annotationen damals keine Bedeutung zugewiesen wurde, gab es immer ein implizites Ziel, sie für Typ-Hinting zu verwenden, was in diesem PEP als erster möglicher Anwendungsfall aufgeführt ist.

Dieser PEP zielt darauf ab, eine Standard-Syntax für Typ-Annotationen bereitzustellen, die Python-Code für einfachere statische Analyse und Refactoring, potenzielle Laufzeit-Typenprüfung und (vielleicht, in einigen Kontexten) Code-Generierung unter Nutzung von Typinformationen öffnet.

Von diesen Zielen ist die statische Analyse die wichtigste. Dazu gehört die Unterstützung für Offline-Typenprüfer wie mypy sowie die Bereitstellung einer Standardnotation, die von IDEs für Code-Vervollständigung und Refactoring verwendet werden kann.

Non-goals

Während das vorgeschlagene Typing-Modul einige Bausteine für die Laufzeit-Typenprüfung enthalten wird – insbesondere die Funktion get_type_hints() –, müssten Drittanbieterpakete entwickelt werden, um spezifische Laufzeit-Typenprüfungsfunktionen zu implementieren, beispielsweise unter Verwendung von Dekoratoren oder Metaklassen. Die Verwendung von Typ-Hinweisen zur Leistungsoptimierung bleibt als Übung für den Leser.

Es sollte auch betont werden, dass **Python eine dynamisch typisierte Sprache bleiben wird und die Autoren keinerlei Wunsch haben, Typ-Hinweise jemals obligatorisch zu machen, auch nicht per Konvention.**

Die Bedeutung von Annotationen

Jede Funktion ohne Annotationen sollte von jedem Typenprüfer als die allgemeinste mögliche Art behandelt oder ignoriert werden. Funktionen mit dem Dekorator @no_type_check sollten als ohne Annotationen behandelt werden.

Es wird empfohlen, aber nicht vorgeschrieben, dass geprüfte Funktionen Annotationen für alle Argumente und den Rückgabetyp haben. Für eine geprüfte Funktion ist die Standardannotation für Argumente und den Rückgabetyp Any. Eine Ausnahme bildet das erste Argument von Instanz- und Klassenmethoden. Wenn es nicht annotiert ist, wird angenommen, dass es den Typ der enthaltenden Klasse für Instanzmethoden und einen Typobjekttyp, der dem enthaltenden Klassenobjekt entspricht, für Klassenmethoden hat. Zum Beispiel hat in Klasse A das erste Argument einer Instanzmethode den impliziten Typ A. In einer Klassenmethode kann der genaue Typ des ersten Arguments mit der verfügbaren Typnotation nicht dargestellt werden.

(Beachten Sie, dass der Rückgabetyp von __init__ mit -> None annotiert werden sollte. Der Grund dafür ist subtil. Wenn __init__ eine Rückgabenotations von -> None annimmt, würde das bedeuten, dass eine argumentlose, unannotierte __init__-Methode immer noch typgeprüft werden sollte? Anstatt dies mehrdeutig zu lassen oder eine Ausnahme von der Ausnahme einzuführen, sagen wir einfach, dass __init__ eine Rückgabenotation haben sollte; das Standardverhalten ist somit dasselbe wie bei anderen Methoden.)

Ein Typenprüfer wird erwartet, den Körper einer geprüften Funktion auf Konsistenz mit den gegebenen Annotationen zu prüfen. Die Annotationen können auch verwendet werden, um die Korrektheit von Aufrufen zu prüfen, die in anderen geprüften Funktionen vorkommen.

Von Typenprüfern wird erwartet, dass sie versuchen, so viele Informationen wie nötig abzuleiten. Die Mindestanforderung ist, die eingebauten Dekoratoren @property, @staticmethod und @classmethod zu verarbeiten.

Typdefinitions-Syntax

Die Syntax nutzt Annotationen im PEP 3107-Stil mit einer Reihe von Erweiterungen, die in den folgenden Abschnitten beschrieben werden. In seiner Grundform wird Typ-Hinting verwendet, indem Funktionsannotations-Slots mit Klassen gefüllt werden

def greeting(name: str) -> str:
    return 'Hello ' + name

Dies besagt, dass der erwartete Typ des Arguments name str ist. Analog ist der erwartete Rückgabetyp str.

Ausdrücke, deren Typ eine Unterklasse eines bestimmten Argumenttyps ist, werden ebenfalls für dieses Argument akzeptiert.

Akzeptable Typenhinweise

Typ-Hinweise können eingebaute Klassen sein (einschließlich derer, die in der Standardbibliothek oder in externen Erweiterungsmodulen definiert sind), abstrakte Basisklassen, Typen, die im Modul types verfügbar sind, und benutzerdefinierte Klassen (einschließlich derer, die in der Standardbibliothek oder in externen Modulen definiert sind).

Während Annotationen normalerweise das beste Format für Typ-Hinweise sind, gibt es Zeiten, in denen es angemessener ist, sie durch einen speziellen Kommentar darzustellen, oder in einer separat verteilten Stub-Datei. (Siehe unten für Beispiele.)

Annotationen müssen gültige Ausdrücke sein, die ohne Ausnahme ausgewertet werden, wenn die Funktion definiert wird (siehe unten für Vorwärtsdeklarationen).

Annotationen sollten einfach gehalten werden, da statische Analysewerkzeuge die Werte möglicherweise nicht interpretieren können. Dynamisch berechnete Typen werden wahrscheinlich nicht verstanden. (Dies ist eine absichtlich etwas vage Anforderung, spezifische Ein- und Ausschlüsse können in zukünftigen Versionen dieses PEP nach Bedarf hinzugefügt werden, basierend auf der Diskussion.)

Zusätzlich zu den oben genannten können die folgenden unten definierten speziellen Konstrukte verwendet werden: None, Any, Union, Tuple, Callable, alle ABCs und Platzhalter für konkrete Klassen, die aus typing exportiert werden (z. B. Sequence und Dict), Typvariablen und Typ-Aliase.

Alle neu eingeführten Namen, die zur Unterstützung der in den folgenden Abschnitten beschriebenen Funktionen verwendet werden (wie Any und Union), sind im Modul typing verfügbar.

Verwendung von None

Wenn None in einem Typ-Hinweis verwendet wird, gilt es als äquivalent zu type(None).

Typ-Aliase

Typ-Aliase werden durch einfache Variablendeklarationen definiert

Url = str

def retry(url: Url, retry_count: int) -> None: ...

Beachten Sie, dass wir empfehlen, Aliasnamen großzuschreiben, da sie benutzerdefinierte Typen darstellen, die (wie benutzerdefinierte Klassen) typischerweise so benannt werden.

Typ-Aliase können so komplex sein wie Typ-Hinweise in Annotationen – alles, was als Typ-Hinweis akzeptabel ist, ist auch in einem Typ-Alias akzeptabel

from typing import TypeVar, Iterable, Tuple

T = TypeVar('T', int, float, complex)
Vector = Iterable[Tuple[T, T]]

def inproduct(v: Vector[T]) -> T:
    return sum(x*y for x, y in v)
def dilate(v: Vector[T], scale: T) -> Vector[T]:
    return ((x * scale, y * scale) for x, y in v)
vec = []  # type: Vector[float]

Dies ist äquivalent zu

from typing import TypeVar, Iterable, Tuple

T = TypeVar('T', int, float, complex)

def inproduct(v: Iterable[Tuple[T, T]]) -> T:
    return sum(x*y for x, y in v)
def dilate(v: Iterable[Tuple[T, T]], scale: T) -> Iterable[Tuple[T, T]]:
    return ((x * scale, y * scale) for x, y in v)
vec = []  # type: Iterable[Tuple[float, float]]

Callable

Frameworks, die Callback-Funktionen mit bestimmten Signaturen erwarten, können mit Callable[[Arg1Type, Arg2Type], ReturnType] typ-hinted werden. Beispiele

from typing import Callable

def feeder(get_next_item: Callable[[], str]) -> None:
    # Body

def async_query(on_success: Callable[[int], None],
                on_error: Callable[[int, Exception], None]) -> None:
    # Body

Es ist möglich, den Rückgabetyp eines Callables zu deklarieren, ohne die Aufrufsignatur anzugeben, indem eine literale Ellipse (drei Punkte) für die Argumentliste eingesetzt wird

def partial(func: Callable[..., str], *args) -> Callable[..., str]:
    # Body

Beachten Sie, dass keine eckigen Klammern um die Ellipse stehen. Die Argumente des Callbacks sind in diesem Fall vollständig uneingeschränkt (und Schlüsselwortargumente sind zulässig).

Da die Verwendung von Callbacks mit Schlüsselwortargumenten nicht als üblicher Anwendungsfall angesehen wird, gibt es derzeit keine Unterstützung für die Angabe von Schlüsselwortargumenten mit Callable. Ebenso gibt es keine Unterstützung für die Angabe von Callback-Signaturen mit einer variablen Anzahl von Argumenten eines bestimmten Typs.

Da typing.Callable eine Doppelrolle als Ersatz für collections.abc.Callable spielt, wird isinstance(x, typing.Callable) implementiert, indem auf isinstance(x, collections.abc.Callable) zurückgegriffen wird. isinstance(x, typing.Callable[...]) wird jedoch nicht unterstützt.

Generics

Da Typinformationen über Objekte, die in Containern gespeichert sind, nicht generell statisch abgeleitet werden können, wurden abstrakte Basisklassen erweitert, um die Subskription zu unterstützen, um erwartete Typen für Container-Elemente anzugeben. Beispiel

from typing import Mapping, Set

def notify_by_email(employees: Set[Employee], overrides: Mapping[str, str]) -> None: ...

Generics können parametrisiert werden, indem eine neue Factory in typing namens TypeVar verwendet wird. Beispiel

from typing import Sequence, TypeVar

T = TypeVar('T')      # Declare type variable

def first(l: Sequence[T]) -> T:   # Generic function
    return l[0]

In diesem Fall ist der Vertrag, dass der zurückgegebene Wert mit den Elementen der Sammlung konsistent ist.

Ein TypeVar()-Ausdruck muss immer direkt einer Variablen zugewiesen werden (er sollte nicht Teil eines größeren Ausdrucks sein). Das Argument für TypeVar() muss ein String sein, der mit dem Variablennamen übereinstimmt, dem er zugewiesen wird. Typvariablen dürfen nicht neu definiert werden.

TypeVar unterstützt die Beschränkung parametrischer Typen auf eine feste Menge möglicher Typen (Hinweis: Diese Typen können nicht durch Typvariablen parametrisiert werden). Zum Beispiel können wir eine Typvariable definieren, die sich nur über str und bytes erstreckt. Standardmäßig erstreckt sich eine Typvariable über alle möglichen Typen. Beispiel für die Beschränkung einer Typvariable

from typing import TypeVar, Text

AnyStr = TypeVar('AnyStr', Text, bytes)

def concat(x: AnyStr, y: AnyStr) -> AnyStr:
    return x + y

Die Funktion concat kann entweder mit zwei str-Argumenten oder zwei bytes-Argumenten aufgerufen werden, aber nicht mit einer Mischung aus str- und bytes-Argumenten.

Es sollten mindestens zwei Beschränkungen vorhanden sein, falls überhaupt; die Angabe einer einzelnen Beschränkung ist nicht zulässig.

Unterklassen von Typen, die durch eine Typvariable beschränkt sind, sollten im Kontext der Typvariable als ihre jeweiligen explizit aufgeführten Basistypen behandelt werden. Betrachten Sie dieses Beispiel

class MyStr(str): ...

x = concat(MyStr('apple'), MyStr('pie'))

Der Aufruf ist gültig, aber die Typvariable AnyStr wird auf str und nicht auf MyStr gesetzt. Tatsächlich wird der abgeleitete Typ des Rückgabewerts, der x zugewiesen wird, ebenfalls str sein.

Zusätzlich ist Any ein gültiger Wert für jede Typvariable. Betrachten Sie Folgendes

def count_truthy(elements: List[Any]) -> int:
    return sum(1 for elem in elements if elem)

Dies ist äquivalent zum Weglassen der generischen Notation und dem einfachen Sagen von elements: List.

Benutzerdefinierte generische Typen

Sie können eine Basisklasse Generic einbeziehen, um eine benutzerdefinierte Klasse als generisch zu definieren. Beispiel

from typing import TypeVar, Generic
from logging import Logger

T = TypeVar('T')

class LoggedVar(Generic[T]):
    def __init__(self, value: T, name: str, logger: Logger) -> None:
        self.name = name
        self.logger = logger
        self.value = value

    def set(self, new: T) -> None:
        self.log('Set ' + repr(self.value))
        self.value = new

    def get(self) -> T:
        self.log('Get ' + repr(self.value))
        return self.value

    def log(self, message: str) -> None:
        self.logger.info('{}: {}'.format(self.name, message))

Generic[T] als Basisklasse definiert, dass die Klasse LoggedVar einen einzigen Typparameter T akzeptiert. Dies macht T auch als Typ innerhalb des Klassenkörpers gültig.

Die Basisklasse Generic verwendet eine Metaklasse, die __getitem__ definiert, sodass LoggedVar[t] als Typ gültig ist

from typing import Iterable

def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None:
    for var in vars:
        var.set(0)

Ein generischer Typ kann beliebig viele Typvariablen haben, und Typvariablen können beschränkt sein. Dies ist gültig

from typing import TypeVar, Generic
...

T = TypeVar('T')
S = TypeVar('S')

class Pair(Generic[T, S]):
    ...

Jedes Typvariablenargument für Generic muss eindeutig sein. Dies ist daher ungültig

from typing import TypeVar, Generic
...

T = TypeVar('T')

class Pair(Generic[T, T]):   # INVALID
    ...

Die Basisklasse Generic[T] ist in einfachen Fällen redundant, in denen Sie eine andere generische Klasse unterklassifizieren und Typvariablen für ihre Parameter angeben

from typing import TypeVar, Iterator

T = TypeVar('T')

class MyIter(Iterator[T]):
    ...

Diese Klassendefinition ist äquivalent zu

class MyIter(Iterator[T], Generic[T]):
    ...

Sie können Mehrfachvererbung mit Generic verwenden

from typing import TypeVar, Generic, Sized, Iterable, Container, Tuple

T = TypeVar('T')

class LinkedList(Sized, Generic[T]):
    ...

K = TypeVar('K')
V = TypeVar('V')

class MyMapping(Iterable[Tuple[K, V]],
                Container[Tuple[K, V]],
                Generic[K, V]):
    ...

Das Unterklassifizieren einer generischen Klasse ohne Angabe von Typparametern nimmt Any für jede Position an. Im folgenden Beispiel ist MyIterable nicht generisch, erbt aber implizit von Iterable[Any]

from typing import Iterable

class MyIterable(Iterable):  # Same as Iterable[Any]
    ...

Generische Metaklassen werden nicht unterstützt.

Geltungsbereichsregeln für Typvariablen

Typvariablen folgen normalen Namensauflösungsregeln. Im Kontext der statischen Typenprüfung gibt es jedoch einige Sonderfälle

  • Eine in einer generischen Funktion verwendete Typvariable kann im selben Codeblock als unterschiedliche Typen interpretiert werden. Beispiel
    from typing import TypeVar, Generic
    
    T = TypeVar('T')
    
    def fun_1(x: T) -> T: ...  # T here
    def fun_2(x: T) -> T: ...  # and here could be different
    
    fun_1(1)                   # This is OK, T is inferred to be int
    fun_2('a')                 # This is also OK, now T is str
    
  • Eine in einer Methode einer generischen Klasse verwendete Typvariable, die mit einer der Variablen übereinstimmt, die diese Klasse parametrisieren, ist immer an diese Variable gebunden. Beispiel
    from typing import TypeVar, Generic
    
    T = TypeVar('T')
    
    class MyClass(Generic[T]):
        def meth_1(self, x: T) -> T: ...  # T here
        def meth_2(self, x: T) -> T: ...  # and here are always the same
    
    a = MyClass()  # type: MyClass[int]
    a.meth_1(1)    # OK
    a.meth_2('a')  # This is an error!
    
  • Eine in einer Methode verwendete Typvariable, die nicht mit einer der Variablen übereinstimmt, die die Klasse parametrisieren, macht diese Methode zu einer generischen Funktion für diese Variable
    T = TypeVar('T')
    S = TypeVar('S')
    class Foo(Generic[T]):
        def method(self, x: T, y: S) -> S:
            ...
    
    x = Foo()               # type: Foo[int]
    y = x.method(0, "abc")  # inferred type of y is str
    
  • Ungebundene Typvariablen sollten nicht in den Körpern von generischen Funktionen oder in Klassenkörpern außerhalb von Methodendefinitionen vorkommen
    T = TypeVar('T')
    S = TypeVar('S')
    
    def a_fun(x: T) -> None:
        # this is OK
        y = []  # type: List[T]
        # but below is an error!
        y = []  # type: List[S]
    
    class Bar(Generic[T]):
        # this is also an error
        an_attr = []  # type: List[S]
    
        def do_something(x: S) -> S:  # this is OK though
            ...
    
  • Eine generische Klassendefinition, die innerhalb einer generischen Funktion vorkommt, sollte keine Typvariablen verwenden, die die generische Funktion parametrisieren
    from typing import List
    
    def a_fun(x: T) -> None:
    
        # This is OK
        a_list = []  # type: List[T]
        ...
    
        # This is however illegal
        class MyGeneric(Generic[T]):
            ...
    
  • Eine generische Klasse, die in einer anderen generischen Klasse verschachtelt ist, kann nicht dieselben Typvariablen verwenden. Der Geltungsbereich der Typvariablen der äußeren Klasse deckt die innere nicht ab
    T = TypeVar('T')
    S = TypeVar('S')
    
    class Outer(Generic[T]):
        class Bad(Iterable[T]):       # Error
            ...
        class AlsoBad:
            x = None  # type: List[T] # Also an error
    
        class Inner(Iterable[S]):     # OK
            ...
        attr = None  # type: Inner[T] # Also OK
    

Instanziierung generischer Klassen und Typentfernung

Benutzerdefinierte generische Klassen können instanziiert werden. Angenommen, wir schreiben eine Node-Klasse, die von Generic[T] erbt

from typing import TypeVar, Generic

T = TypeVar('T')

class Node(Generic[T]):
    ...

Um Node-Instanzen zu erstellen, rufen Sie Node() auf, genau wie bei einer regulären Klasse. Zur Laufzeit ist der Typ (Klasse) der Instanz Node. Aber welchen Typ hat sie für den Typenprüfer? Die Antwort hängt davon ab, wie viele Informationen im Aufruf verfügbar sind. Wenn der Konstruktor (__init__ oder __new__) T in seiner Signatur verwendet und ein entsprechendes Argument übergeben wird, wird der Typ des entsprechenden Arguments/der entsprechenden Argumente substituiert. Andernfalls wird Any angenommen. Beispiel

from typing import TypeVar, Generic

T = TypeVar('T')

class Node(Generic[T]):
    x = None  # type: T # Instance attribute (see below)
    def __init__(self, label: T = None) -> None:
        ...

x = Node('')  # Inferred type is Node[str]
y = Node(0)   # Inferred type is Node[int]
z = Node()    # Inferred type is Node[Any]

Falls der abgeleitete Typ [Any] verwendet, der beabsichtigte Typ jedoch spezifischer ist, können Sie einen Typ-Kommentar (siehe unten) verwenden, um den Typ der Variablen zu erzwingen, z.B.

# (continued from previous example)
a = Node()  # type: Node[int]
b = Node()  # type: Node[str]

Alternativ können Sie einen spezifischen konkreten Typ instanziieren, z.B.

# (continued from previous example)
p = Node[int]()
q = Node[str]()
r = Node[int]('')  # Error
s = Node[str](0)   # Error

Beachten Sie, dass der Laufzeittyp (Klasse) von p und q immer noch nur Node ist – Node[int] und Node[str] sind unterscheidbare Klassenobjekte, aber die Laufzeitklasse der durch ihre Instanziierung erstellten Objekte speichert die Unterscheidung nicht. Dieses Verhalten wird als „Typentfernung“ bezeichnet; es ist üblich in Sprachen mit Generics (z.B. Java, TypeScript).

Die Verwendung von generischen Klassen (parametrisiert oder nicht) zum Zugriff auf Attribute führt zu einem Fehler bei der Typenprüfung. Außerhalb des Klassendefinitionskörpers kann ein Klassenattribut nicht zugewiesen werden und kann nur durch den Zugriff über eine Klasseninstanz abgerufen werden, die kein Instanzattribut mit demselben Namen hat

# (continued from previous example)
Node[int].x = 1  # Error
Node[int].x      # Error
Node.x = 1       # Error
Node.x           # Error
type(p).x        # Error
p.x              # Ok (evaluates to None)
Node[int]().x    # Ok (evaluates to None)
p.x = 1          # Ok, but assigning to instance attribute

Generische Versionen von abstrakten Sammlungen wie Mapping oder Sequence und generische Versionen von eingebauten Klassen – List, Dict, Set und FrozenSet – können nicht instanziiert werden. Konkrete benutzerdefinierte Unterklassen davon und generische Versionen konkreter Sammlungen können jedoch instanziiert werden

data = DefaultDict[int, bytes]()

Beachten Sie, dass man statische Typen und Laufzeitklassen nicht verwechseln sollte. Der Typ wird auch in diesem Fall entfernt und der obige Ausdruck ist nur eine Abkürzung für

data = collections.defaultdict()  # type: DefaultDict[int, bytes]

Es wird nicht empfohlen, die indizierte Klasse (z. B. Node[int]) direkt in einem Ausdruck zu verwenden – die Verwendung eines Typ-Alias (z. B. IntNode = Node[int]) ist stattdessen vorzuziehen. (Erstens hat das Erstellen der indizierten Klasse, z. B. Node[int], Laufzeitkosten. Zweitens ist die Verwendung eines Typ-Alias lesbarer.)

Beliebige generische Typen als Basisklassen

Generic[T] ist nur als Basisklasse gültig – es ist kein richtiger Typ. Benutzerdefinierte generische Typen wie LinkedList[T] aus dem obigen Beispiel und eingebaute generische Typen und ABCs wie List[T] und Iterable[T] sind jedoch sowohl als Typen als auch als Basisklassen gültig. Zum Beispiel können wir eine Unterklasse von Dict definieren, die Typargumente spezialisiert

from typing import Dict, List, Optional

class Node:
    ...

class SymbolTable(Dict[str, List[Node]]):
    def push(self, name: str, node: Node) -> None:
        self.setdefault(name, []).append(node)

    def pop(self, name: str) -> Node:
        return self[name].pop()

    def lookup(self, name: str) -> Optional[Node]:
        nodes = self.get(name)
        if nodes:
            return nodes[-1]
        return None

SymbolTable ist eine Unterklasse von dict und eine Unterart von Dict[str, List[Node]].

Wenn eine generische Basisklasse eine Typvariable als Typargument hat, macht dies die definierte Klasse generisch. Zum Beispiel können wir eine generische LinkedList-Klasse definieren, die iterierbar und ein Container ist

from typing import TypeVar, Iterable, Container

T = TypeVar('T')

class LinkedList(Iterable[T], Container[T]):
    ...

Nun ist LinkedList[int] ein gültiger Typ. Beachten Sie, dass wir T mehrfach in der Basisklassenliste verwenden können, solange wir nicht dieselbe Typvariable T mehrfach innerhalb von Generic[...] verwenden.

Betrachten Sie auch das folgende Beispiel

from typing import TypeVar, Mapping

T = TypeVar('T')

class MyDict(Mapping[str, T]):
    ...

In diesem Fall hat MyDict einen einzelnen Parameter, T.

Abstrakte generische Typen

Die von Generic verwendete Metaklasse ist eine Unterklasse von abc.ABCMeta. Eine generische Klasse kann eine ABC sein, indem sie abstrakte Methoden oder Eigenschaften enthält, und generische Klassen können auch ABCs als Basisklassen ohne Metaklassenkonflikt haben.

Typvariablen mit oberer Schranke

Eine Typvariable kann eine obere Schranke mit bound=<type> angeben (Hinweis: <type> kann selbst nicht durch Typvariablen parametrisiert werden). Dies bedeutet, dass ein tatsächlicher Typ, der für die Typvariable substituiert wird (explizit oder implizit), eine Unterklasse des Schrankentyps sein muss. Beispiel

from typing import TypeVar, Sized

ST = TypeVar('ST', bound=Sized)

def longer(x: ST, y: ST) -> ST:
    if len(x) > len(y):
        return x
    else:
        return y

longer([1], [1, 2])  # ok, return type List[int]
longer({1}, {1, 2})  # ok, return type Set[int]
longer([1], {1, 2})  # ok, return type Collection[int]

Eine obere Schranke kann nicht mit Typbeschränkungen kombiniert werden (wie bei AnyStr, siehe Beispiel oben); Typbeschränkungen führen dazu, dass der abgeleitete Typ _genau_ einer der Beschränkungstypen ist, während eine obere Schranke nur verlangt, dass der tatsächliche Typ eine Unterklasse des Schrankentyps ist.

Kovarianz und Kontravarianz

Betrachten Sie eine Klasse Employee mit einer Unterklasse Manager. Angenommen, wir haben jetzt eine Funktion mit einem Argument, das mit List[Employee] annotiert ist. Sollten wir in der Lage sein, diese Funktion mit einer Variablen vom Typ List[Manager] als Argument aufzurufen? Viele Leute würden „ja, natürlich“ antworten, ohne die Konsequenzen zu bedenken. Aber ohne mehr über die Funktion zu wissen, sollte ein Typenprüfer einen solchen Aufruf ablehnen: Die Funktion könnte eine Employee-Instanz zur Liste hinzufügen, was den Typ der Variablen im Aufrufer verletzen würde.

Es stellt sich heraus, dass ein solches Argument _kontravariant_ wirkt, während die intuitive Antwort (die korrekt ist, wenn die Funktion ihr Argument nicht mutiert!) verlangt, dass das Argument _kovariant_ wirkt. Eine längere Einführung in diese Konzepte finden Sie auf Wikipedia und in PEP 483; hier zeigen wir nur, wie das Verhalten eines Typenprüfers gesteuert wird.

Standardmäßig werden generische Typen für alle Typvariablen als _invariant_ betrachtet, was bedeutet, dass Werte für Variablen, die mit Typen wie List[Employee] annotiert sind, exakt mit der Typannotation übereinstimmen müssen – keine Unterklassen oder Oberklassen des Typparameters (in diesem Beispiel Employee) sind erlaubt.

Um die Deklaration von Containertypen zu erleichtern, bei denen kovariante oder kontravariante Typenprüfung akzeptabel ist, akzeptieren Typvariablen Schlüsselwortargumente covariant=True oder contravariant=True. Höchstens eines davon kann übergeben werden. Mit solchen Variablen definierte generische Typen gelten als kovariant oder kontravariant in der entsprechenden Variable. Konventionell wird empfohlen, Namen, die auf _co enden, für mit covariant=True definierte Typvariablen und Namen, die auf _contra enden, für mit contravariant=True definierte zu verwenden.

Ein typisches Beispiel beinhaltet die Definition einer unveränderlichen (oder schreibgeschützten) Containerklasse

from typing import TypeVar, Generic, Iterable, Iterator

T_co = TypeVar('T_co', covariant=True)

class ImmutableList(Generic[T_co]):
    def __init__(self, items: Iterable[T_co]) -> None: ...
    def __iter__(self) -> Iterator[T_co]: ...
    ...

class Employee: ...

class Manager(Employee): ...

def dump_employees(emps: ImmutableList[Employee]) -> None:
    for emp in emps:
        ...

mgrs = ImmutableList([Manager()])  # type: ImmutableList[Manager]
dump_employees(mgrs)  # OK

Die schreibgeschützten Collection-Klassen in typing sind alle kovariant in ihrer Typparameter deklariert (z. B. Mapping und Sequence). Die mutablen Collection-Klassen (z. B. MutableMapping und MutableSequence) sind invariant deklariert. Das einzige Beispiel für einen kontravarianten Typ ist der Generator-Typ, der kontravariant im send()-Argumenttyp ist (siehe unten).

Hinweis: Kovarianz oder Kontravarianz ist keine Eigenschaft einer Typparameter, sondern eine Eigenschaft einer generischen Klasse, die mit dieser Variablen definiert wurde. Varianz ist nur auf generische Typen anwendbar; generische Funktionen haben diese Eigenschaft nicht. Letztere sollten nur mit Typparametern ohne die Schlüsselwortargumente covariant oder contravariant definiert werden. Zum Beispiel ist das folgende Beispiel in Ordnung

from typing import TypeVar

class Employee: ...

class Manager(Employee): ...

E = TypeVar('E', bound=Employee)

def dump_employee(e: E) -> None: ...

dump_employee(Manager())  # OK

während das folgende verboten ist

B_co = TypeVar('B_co', covariant=True)

def bad_func(x: B_co) -> B_co:  # Flagged as error by a type checker
    ...

Der Zahlenturm

PEP 3141 definiert den numerischen Turm von Python, und das stdlib-Modul numbers implementiert die entsprechenden ABCs (Number, Complex, Real, Rational und Integral). Es gibt einige Probleme mit diesen ABCs, aber die eingebauten konkreten numerischen Klassen complex, float und int sind allgegenwärtig (besonders die beiden letzteren :-).

Anstatt zu verlangen, dass Benutzer import numbers schreiben und dann numbers.Float usw. verwenden, schlägt diese PEP eine einfache Abkürzung vor, die fast genauso effektiv ist: Wenn ein Argument mit dem Typ float annotiert ist, ist ein Argument vom Typ int akzeptabel; ähnlich sind für ein Argument, das mit complex annotiert ist, Argumente vom Typ float oder int akzeptabel. Dies behandelt keine Klassen, die die entsprechenden ABCs implementieren, oder die Klasse fractions.Fraction, aber wir glauben, dass diese Anwendungsfälle äußerst selten sind.

Vorwärtsdeklarationen

Wenn ein Typ-Hint Namen enthält, die noch nicht definiert wurden, kann diese Definition als Zeichenkettenliteral ausgedrückt werden, um später aufgelöst zu werden.

Eine Situation, in der dies häufig vorkommt, ist die Definition einer Container-Klasse, wobei die zu definierende Klasse in der Signatur einiger Methoden vorkommt. Zum Beispiel funktioniert der folgende Code (der Anfang einer einfachen Binärbaum-Implementierung) nicht

class Tree:
    def __init__(self, left: Tree, right: Tree):
        self.left = left
        self.right = right

Um dies zu beheben, schreiben wir

class Tree:
    def __init__(self, left: 'Tree', right: 'Tree'):
        self.left = left
        self.right = right

Das Zeichenkettenliteral sollte einen gültigen Python-Ausdruck enthalten (d. h. compile(lit, '', 'eval') sollte ein gültiges Code-Objekt sein) und es sollte ohne Fehler ausgewertet werden, sobald das Modul vollständig geladen wurde. Der lokale und globale Namensraum, in dem es ausgewertet wird, sollte derselbe Namensraum sein, in dem Standardargumente derselben Funktion ausgewertet würden.

Darüber hinaus sollte der Ausdruck als gültiger Typ-Hint analysierbar sein, d. h. er unterliegt den Regeln aus dem Abschnitt Akzeptable Typ-Hints oben.

Es ist zulässig, Zeichenkettenliterale als Teil eines Typ-Hints zu verwenden, zum Beispiel

class Tree:
    ...
    def leaves(self) -> List['Tree']:
        ...

Ein häufiger Anwendungsfall für Vorwärtsreferenzen ist, wenn z. B. Django-Modelle in Signaturen benötigt werden. Typischerweise ist jedes Modell in einer separaten Datei und hat Methoden, die Argumente annehmen, deren Typ andere Modelle umfasst. Aufgrund der Art und Weise, wie zirkuläre Importe in Python funktionieren, ist es oft nicht möglich, alle benötigten Modelle direkt zu importieren

# File models/a.py
from models.b import B
class A(Model):
    def foo(self, b: B): ...

# File models/b.py
from models.a import A
class B(Model):
    def bar(self, a: A): ...

# File main.py
from models.a import A
from models.b import B

Unter der Annahme, dass main zuerst importiert wird, schlägt dies mit einem ImportError in der Zeile from models.a import A in models/b.py fehl, das aus models/a.py importiert wird, bevor a die Klasse A definiert hat. Die Lösung besteht darin, zu Modul-only-Imports zu wechseln und die Modelle anhand ihres Namens _modul_._klasse_ zu referenzieren

# File models/a.py
from models import b
class A(Model):
    def foo(self, b: 'b.B'): ...

# File models/b.py
from models import a
class B(Model):
    def bar(self, a: 'a.A'): ...

# File main.py
from models.a import A
from models.b import B

Union-Typen

Da die Akzeptanz einer kleinen, begrenzten Menge erwarteter Typen für ein einzelnes Argument üblich ist, gibt es eine neue spezielle Fabrik namens Union. Beispiel

from typing import Union

def handle_employees(e: Union[Employee, Sequence[Employee]]) -> None:
    if isinstance(e, Employee):
        e = [e]
    ...

Ein von Union[T1, T2, ...] gefakter Typ ist eine Obermenge aller Typen T1, T2 usw., so dass ein Wert, der ein Element eines dieser Typen ist, für ein Argument akzeptabel ist, das mit Union[T1, T2, ...] annotiert ist.

Ein häufiger Fall von Union-Typen sind optionale Typen. Standardmäßig ist None kein gültiger Wert für jeden Typ, es sei denn, ein Standardwert von None wurde in der Funktionsdefinition bereitgestellt. Beispiele

def handle_employee(e: Union[Employee, None]) -> None: ...

Als Abkürzung für Union[T1, None] können Sie Optional[T1] schreiben; zum Beispiel ist das obige äquivalent zu

from typing import Optional

def handle_employee(e: Optional[Employee]) -> None: ...

Eine frühere Version dieser PEP erlaubte Typ-Prüfern, einen optionalen Typ anzunehmen, wenn der Standardwert None ist, wie in diesem Code

def handle_employee(e: Employee = None): ...

Dies wäre als äquivalent zu behandelt worden

def handle_employee(e: Optional[Employee] = None) -> None: ...

Dies ist nicht mehr das empfohlene Verhalten. Typ-Prüfer sollten dazu übergehen, die explizite Angabe des optionalen Typs zu verlangen.

Unterstützung für Singleton-Typen in Unions

Eine Singleton-Instanz wird häufig verwendet, um einen speziellen Zustand zu markieren, insbesondere in Situationen, in denen None ebenfalls ein gültiger Wert für eine Variable ist. Beispiel

_empty = object()

def func(x=_empty):
    if x is _empty:  # default argument value
        return 0
    elif x is None:  # argument was provided and it's None
        return 1
    else:
        return x * 2

Um präzise Typen in solchen Situationen zu ermöglichen, sollte der Benutzer den Union-Typ in Verbindung mit der Klasse enum.Enum aus der Standardbibliothek verwenden, damit Typfehler statisch abgefangen werden können

from typing import Union
from enum import Enum

class Empty(Enum):
    token = 0
_empty = Empty.token

def func(x: Union[int, None, Empty] = _empty) -> int:

    boom = x * 42  # This fails type check

    if x is _empty:
        return 0
    elif x is None:
        return 1
    else:  # At this point typechecker knows that x can only have type int
        return x * 2

Da die Unterklassen von Enum nicht weiter unterklassifiziert werden können, kann der Typ der Variablen x in allen Zweigen des obigen Beispiels statisch abgeleitet werden. Der gleiche Ansatz ist anwendbar, wenn mehr als ein Singleton-Objekt benötigt wird: Man kann eine Aufzählung verwenden, die mehr als einen Wert hat

class Reason(Enum):
    timeout = 1
    error = 2

def process(response: Union[str, Reason] = '') -> str:
    if response is Reason.timeout:
        return 'TIMEOUT'
    elif response is Reason.error:
        return 'ERROR'
    else:
        # response can be only str, all other possible values exhausted
        return 'PROCESSED: ' + response

Der Typ Any

Eine spezielle Art von Typ ist Any. Jeder Typ ist konsistent mit Any. Er kann als ein Typ betrachtet werden, der alle Werte und alle Methoden hat. Beachten Sie, dass Any und der eingebaute Typ object völlig unterschiedlich sind.

Wenn der Typ eines Werts object ist, lehnt der Typ-Prüfer fast alle Operationen darauf ab, und die Zuweisung zu einer Variablen (oder die Verwendung als Rückgabewert) eines spezialisierteren Typs ist ein Typfehler. Auf der anderen Seite, wenn ein Wert den Typ Any hat, erlaubt der Typ-Prüfer alle Operationen darauf, und ein Wert vom Typ Any kann einer Variablen (oder als Rückgabewert) eines eingeschränkteren Typs zugewiesen werden.

Ein Funktionsparameter ohne Annotation wird als mit Any annotiert angenommen. Wenn ein generischer Typ ohne Angabe von Typparametern verwendet wird, werden diese als Any angenommen

from typing import Mapping

def use_map(m: Mapping) -> None:  # Same as Mapping[Any, Any]
    ...

Diese Regel gilt auch für Tuple, im Annotationskontext ist er äquivalent zu Tuple[Any, ...] und wiederum zu tuple. Ebenso ist ein bloßer Callable in einer Annotation äquivalent zu Callable[..., Any] und wiederum zu collections.abc.Callable

from typing import Tuple, List, Callable

def check_args(args: Tuple) -> bool:
    ...

check_args(())           # OK
check_args((42, 'abc'))  # Also OK
check_args(3.14)         # Flagged as error by a type checker

# A list of arbitrary callables is accepted by this function
def apply_callbacks(cbs: List[Callable]) -> None:
    ...

Der Typ NoReturn

Das Modul typing stellt einen speziellen Typ NoReturn bereit, um Funktionen zu annotieren, die niemals normal zurückkehren. Zum Beispiel eine Funktion, die bedingungslos eine Ausnahme auslöst

from typing import NoReturn

def stop() -> NoReturn:
    raise RuntimeError('no way')

Die Annotation NoReturn wird für Funktionen wie sys.exit verwendet. Statische Typ-Prüfer stellen sicher, dass Funktionen, die als NoReturn annotiert sind, niemals wirklich zurückkehren, weder implizit noch explizit

import sys
from typing import NoReturn

  def f(x: int) -> NoReturn:  # Error, f(0) implicitly returns None
      if x != 0:
          sys.exit(1)

Die Prüfer erkennen auch, dass der Code nach Aufrufen solcher Funktionen nicht erreichbar ist und verhalten sich entsprechend

# continue from first example
def g(x: int) -> int:
    if x > 0:
        return x
    stop()
    return 'whatever works'  # Error might be not reported by some checkers
                             # that ignore errors in unreachable blocks

Der Typ NoReturn ist nur als Rückgabeannotation von Funktionen gültig und wird als Fehler betrachtet, wenn er an anderer Stelle vorkommt

from typing import List, NoReturn

# All of the following are errors
def bad1(x: NoReturn) -> int:
    ...
bad2 = None  # type: NoReturn
def bad3() -> List[NoReturn]:
    ...

Der Typ von Klassenobjekten

Manchmal möchten Sie über Klassenobjekte sprechen, insbesondere über Klassenobjekte, die von einer gegebenen Klasse erben. Dies kann als Type[C] geschrieben werden, wobei C eine Klasse ist. Zur Klarstellung: Während C (wenn als Annotation verwendet) sich auf Instanzen der Klasse C bezieht, bezieht sich Type[C] auf Unterklassen von C. (Dies ist eine ähnliche Unterscheidung wie zwischen object und type.)

Zum Beispiel, nehmen wir an, wir haben die folgenden Klassen

class User: ...  # Abstract base for User classes
class BasicUser(User): ...
class ProUser(User): ...
class TeamUser(User): ...

Und nehmen wir an, wir haben eine Funktion, die eine Instanz einer dieser Klassen erstellt, wenn man ihr ein Klassenobjekt übergibt

def new_user(user_class):
    user = user_class()
    # (Here we could write the user object to a database)
    return user

Ohne Type[] wäre das Beste, was wir tun könnten, um new_user() zu annotieren

def new_user(user_class: type) -> User:
    ...

Mit Type[] und einem Typvariablen mit oberer Schranke können wir jedoch viel mehr tun

U = TypeVar('U', bound=User)
def new_user(user_class: Type[U]) -> U:
    ...

Wenn wir nun new_user() mit einer spezifischen Unterklasse von User aufrufen, wird ein Typ-Prüfer den korrekten Typ des Ergebnisses ableiten

joe = new_user(BasicUser)  # Inferred type is BasicUser

Der Wert, der Type[C] entspricht, muss ein tatsächliches Klassenobjekt sein, das eine Unterklasse von C ist, keine spezielle Form. Mit anderen Worten, im obigen Beispiel wird der Aufruf von z. B. new_user(Union[BasicUser, ProUser]) vom Typ-Prüfer abgelehnt (zusätzlich zum Fehlschlagen zur Laufzeit, da man keine Union instanziieren kann).

Beachten Sie, dass es zulässig ist, eine Union von Klassen als Parameter für Type[] zu verwenden, wie in

def new_non_team_user(user_class: Type[Union[BasicUser, ProUser]]):
    user = new_user(user_class)
    ...

Das tatsächlich zur Laufzeit übergebene Argument muss jedoch immer noch ein konkretes Klassenobjekt sein, z. B. im obigen Beispiel

new_non_team_user(ProUser)  # OK
new_non_team_user(TeamUser)  # Disallowed by type checker

Type[Any] wird ebenfalls unterstützt (siehe unten für seine Bedeutung).

Type[T], wobei T eine Typvariable ist, ist erlaubt, wenn das erste Argument einer Klassenmethode annotiert wird (siehe relevanter Abschnitt).

Andere spezielle Konstrukte wie Tuple oder Callable sind als Argument für Type nicht zulässig.

Es gibt einige Bedenken hinsichtlich dieser Funktion: Wenn beispielsweise new_user() user_class() aufruft, impliziert dies, dass alle Unterklassen von User dies in ihrer Konstruktorsignatur unterstützen müssen. Dies ist jedoch nicht einzigartig für Type[]: Klassenmethoden haben ähnliche Bedenken. Ein Typ-Prüfer sollte Verstöße gegen solche Annahmen kennzeichnen, aber standardmäßig sollten Konstruktoraufrufe, die der Konstruktorsignatur in der angegebenen Basisklasse (User im obigen Beispiel) entsprechen, erlaubt sein. Ein Programm, das eine komplexe oder erweiterbare Klassenhierarchie enthält, könnte dies auch durch die Verwendung einer Factory-Klassenmethode handhaben. Eine zukünftige Überarbeitung dieser PEP kann bessere Möglichkeiten zur Behandlung dieser Bedenken einführen.

Wenn Type parametrisiert wird, erfordert es genau einen Parameter. Plain Type ohne Klammern ist äquivalent zu Type[Any] und dies wiederum zu type (die Wurzel der Metaklassen-Hierarchie von Python). Diese Äquivalenz motiviert auch den Namen Type im Gegensatz zu Alternativen wie Class oder SubType, die während der Diskussion dieser Funktion vorgeschlagen wurden; dies ähnelt der Beziehung zwischen z. B. List und list.

Bezüglich des Verhaltens von Type[Any] (oder Type oder type), bietet der Zugriff auf Attribute einer Variablen mit diesem Typ nur Attribute und Methoden, die von type definiert wurden (z. B. __repr__() und __mro__). Eine solche Variable kann mit beliebigen Argumenten aufgerufen werden, und der Rückgabetyp ist Any.

Type ist kovariant in seinem Parameter, da Type[Derived] eine Unterklasse von Type[Base] ist

def new_pro_user(pro_user_class: Type[ProUser]):
    user = new_user(pro_user_class)  # OK
    ...

Annotation von Instanz- und Klassenmethoden

In den meisten Fällen muss das erste Argument von Klassen- und Instanzmethoden nicht annotiert werden und wird für Instanzmethoden als Typ der enthaltenden Klasse und für Klassenmethoden als Typobjekttyp, der dem enthaltenden Klassenobjekt entspricht, angenommen. Darüber hinaus kann das erste Argument einer Instanzmethode mit einer Typvariable annotiert werden. In diesem Fall kann der Rückgabetyp dieselbe Typvariable verwenden, wodurch diese Methode zu einer generischen Funktion wird. Zum Beispiel

T = TypeVar('T', bound='Copyable')
class Copyable:
    def copy(self: T) -> T:
        # return a copy of self

class C(Copyable): ...
c = C()
c2 = c.copy()  # type here should be C

Das Gleiche gilt für Klassenmethoden, die Type[] in einer Annotation des ersten Arguments verwenden

T = TypeVar('T', bound='C')
class C:
    @classmethod
    def factory(cls: Type[T]) -> T:
        # make a new instance of cls

class D(C): ...
d = D.factory()  # type here should be D

Beachten Sie, dass einige Typ-Prüfer Einschränkungen für diese Verwendung anwenden können, z. B. die Anforderung einer geeigneten oberen Schranke für die verwendete Typvariable (siehe Beispiele).

Versions- und Plattformprüfung

Typ-Prüfer sollen einfache Versions- und Plattformprüfungen verstehen, z. B.

import sys

if sys.version_info[0] >= 3:
    # Python 3 specific definitions
else:
    # Python 2 specific definitions

if sys.platform == 'win32':
    # Windows specific definitions
else:
    # Posix specific definitions

Erwarten Sie nicht, dass ein Prüfer Verschleierungen wie "".join(reversed(sys.platform)) == "xunil" versteht.

Laufzeit- oder Typenprüfung?

Manchmal gibt es Code, der von einem Typ-Prüfer (oder anderen statischen Analysewerkzeugen) gesehen werden muss, aber nicht ausgeführt werden soll. Für solche Situationen definiert das Modul typing eine Konstante, TYPE_CHECKING, die während der Typ-Prüfung (oder anderer statischer Analyse) als True und zur Laufzeit als False betrachtet wird. Beispiel

import typing

if typing.TYPE_CHECKING:
    import expensive_mod

def a_func(arg: 'expensive_mod.SomeClass') -> None:
    a_var = arg  # type: expensive_mod.SomeClass
    ...

(Beachten Sie, dass die Typannotation in Anführungszeichen stehen muss, um sie zu einer „Vorwärtsreferenz“ zu machen, um die Referenz auf expensive_mod vor der Interpreterlaufzeit zu verbergen. Im Kommentar # type sind keine Anführungszeichen erforderlich.)

Dieser Ansatz kann auch nützlich sein, um Importzyklen zu behandeln.

Beliebige Argumentlisten und Standardargumentwerte

Beliebige Argumentlisten können ebenfalls typ-annotiert werden, so dass die Definition

def foo(*args: str, **kwds: int): ...

akzeptabel ist und bedeutet, dass z. B. alle folgenden Funktionen Aufrufe mit gültigen Argumenttypen darstellen

foo('a', 'b', 'c')
foo(x=1, y=2)
foo('', z=0)

Im Körper der Funktion foo wird der Typ der Variablen args als Tuple[str, ...] und der Typ der Variablen kwds als Dict[str, int] abgeleitet.

In Stubs kann es nützlich sein, ein Argument als Standardwert deklarieren zu lassen, ohne den tatsächlichen Standardwert anzugeben. Zum Beispiel

def foo(x: AnyStr, y: AnyStr = ...) -> AnyStr: ...

Wie sollte der Standardwert aussehen? Jede der Optionen "", b"" oder None erfüllt die Typbeschränkung nicht.

In solchen Fällen kann der Standardwert als literales Ellipse angegeben werden, d. h. das obige Beispiel ist wörtlich das, was Sie schreiben würden.

Nur positionsbezogene Argumente

Einige Funktionen sind so konzipiert, dass sie ihre Argumente nur positionell entgegennehmen und erwarten, dass ihre Aufrufer niemals den Namen des Arguments verwenden, um dieses Argument als Schlüsselwort zu übergeben. Alle Argumente mit Namen, die mit __ beginnen, werden als nur positionell angesehen, es sei denn, ihre Namen enden auch mit __

def quux(__x: int, __y__: int = 0) -> None: ...

quux(3, __y__=1)  # This call is fine.

quux(__x=3)  # This call is an error.

Annotation von Generatorfunktionen und Koroutinen

Der Rückgabetyp von Generatorfunktionen kann durch den generischen Typ Generator[yield_type, send_type, return_type] annotiert werden, der vom Modul typing.py bereitgestellt wird

def echo_round() -> Generator[int, float, str]:
    res = yield
    while res:
        res = yield round(res)
    return 'OK'

Koroutinen, die in PEP 492 eingeführt wurden, werden mit der gleichen Syntax wie normale Funktionen annotiert. Die Rückgabe-Typannotation bezieht sich jedoch auf den Typ des await-Ausdrucks, nicht auf den Koroutinen-Typ

async def spam(ignored: int) -> str:
    return 'spam'

async def foo() -> None:
    bar = await spam(42)  # type: str

Das Modul typing.py stellt eine generische Version des ABC collections.abc.Coroutine bereit, um Awaitables zu spezifizieren, die auch send() und throw() Methoden unterstützen. Die Varianz und Reihenfolge der Typvariablen entsprechen denen von Generator, nämlich Coroutine[T_co, T_contra, V_co], zum Beispiel

from typing import List, Coroutine
c = None  # type: Coroutine[List[str], str, int]
...
x = c.send('hi')  # type: List[str]
async def bar() -> None:
    x = await c  # type: int

Das Modul stellt auch generische ABCs Awaitable, AsyncIterable und AsyncIterator für Situationen bereit, in denen präzisere Typen nicht spezifiziert werden können

def op() -> typing.Awaitable[str]:
    if cond:
        return spam(42)
    else:
        return asyncio.Future(...)

Kompatibilität mit anderen Verwendungen von Funktionsannotationen

Eine Reihe bestehender oder potenzieller Anwendungsfälle für Funktionsannotationen existieren, die mit Typ-Hints inkompatibel sind. Diese können einen statischen Typ-Prüfer verwirren. Da Typ-Hint-Annotationen jedoch kein Laufzeitverhalten haben (außer der Auswertung des Annotationsausdrucks und der Speicherung von Annotationen im Attribut __annotations__ des Funktionsobjekts), macht dies das Programm nicht falsch – es kann nur dazu führen, dass ein Typ-Prüfer fälschlicherweise Warnungen oder Fehler ausgibt.

Um Programmteile zu markieren, die nicht von Typ-Hints abgedeckt werden sollen, können Sie eine oder mehrere der folgenden Optionen verwenden

  • einen Kommentar # type: ignore;
  • einen @no_type_check Dekorator auf einer Klasse oder Funktion;
  • einen benutzerdefinierten Klassen- oder Funktionsdekorator, der mit @no_type_check_decorator markiert ist.

Weitere Details finden Sie in späteren Abschnitten.

Um eine maximale Kompatibilität mit der Offline-Typ-Prüfung zu gewährleisten, könnte es schließlich eine gute Idee sein, Schnittstellen, die auf Annotationen angewiesen sind, auf einen anderen Mechanismus umzustellen, z. B. einen Dekorator. In Python 3.5 gibt es jedoch keinen Druck dazu. Siehe auch die ausführlichere Diskussion unter Abgelehnte Alternativen unten.

Typ-Kommentare

Es wird keine erstklassige Syntaxunterstützung zum expliziten Markieren von Variablen als einem bestimmten Typ hinzugefügt. Um bei komplexen Fällen bei der Typableitung zu helfen, kann ein Kommentar des folgenden Formats verwendet werden

x = []                # type: List[Employee]
x, y, z = [], [], []  # type: List[int], List[int], List[str]
x, y, z = [], [], []  # type: (List[int], List[int], List[str])
a, b, *c = range(5)   # type: float, float, List[float]
x = [1, 2]            # type: List[int]

Typ-Kommentare sollten auf der letzten Zeile der Anweisung platziert werden, die die Variablendefinition enthält. Sie können auch auf with-Anweisungen und for-Anweisungen direkt nach dem Doppelpunkt platziert werden.

Beispiele für Typ-Kommentare in with- und for-Anweisungen

with frobnicate() as foo:  # type: int
    # Here foo is an int
    ...

for x, y in points:  # type: float, float
    # Here x and y are floats
    ...

In Stubs kann es nützlich sein, die Existenz einer Variablen zu deklarieren, ohne ihr einen Anfangswert zu geben. Dies kann mit der PEP 526-Syntax für Variablentypannotationen erfolgen

from typing import IO

stream: IO[str]

Die obige Syntax ist für alle Python-Versionen in Stubs akzeptabel. In Nicht-Stub-Code für Python-Versionen 3.5 und früher gibt es jedoch einen Sonderfall

from typing import IO

stream = None  # type: IO[str]

Typ-Prüfer sollten sich darüber nicht beschweren (obwohl der Wert None nicht dem gegebenen Typ entspricht) und sie sollten den abgeleiteten Typ auch nicht zu Optional[...] ändern (trotz der Regel, die dies für annotierte Argumente mit einem Standardwert von None tut). Die Annahme hier ist, dass anderer Code sicherstellt, dass die Variable einen Wert vom richtigen Typ erhält, und alle Verwendungen davon ausgehen können, dass die Variable den gegebenen Typ hat.

Der Kommentar # type: ignore sollte auf der Zeile platziert werden, auf die sich der Fehler bezieht

import http.client
errors = {
    'not_found': http.client.NOT_FOUND  # type: ignore
}

Ein Kommentar # type: ignore auf einer eigenen Zeile am Anfang einer Datei, vor jeglichen Docstrings, Importen oder anderem ausführbaren Code, unterdrückt alle Fehler in der Datei. Leerzeilen und andere Kommentare, wie Shebang-Zeilen und Coding-Cookies, dürfen dem Kommentar # type: ignore vorausgehen.

In einigen Fällen sind möglicherweise Linting-Tools oder andere Kommentare in derselben Zeile wie ein Typ-Kommentar erforderlich. In diesen Fällen sollte der Typ-Kommentar vor anderen Kommentaren und Linter-Markierungen stehen

# type: ignore # <Kommentar oder andere Markierung>

Wenn sich Typ-Hints im Allgemeinen als nützlich erweisen, kann in einer zukünftigen Python-Version eine Syntax für die Typisierung von Variablen bereitgestellt werden. (UPDATE: Diese Syntax wurde in Python 3.6 über PEP 526 hinzugefügt.)

Casts

Gelegentlich benötigt der Typ-Prüfer eine andere Art von Hinweis: Der Programmierer weiß möglicherweise, dass ein Ausdruck einen eingeschränkteren Typ hat, als ein Typ-Prüfer ableiten kann. Zum Beispiel

from typing import List, cast

def find_first_str(a: List[object]) -> str:
    index = next(i for i, x in enumerate(a) if isinstance(x, str))
    # We only get here if there's at least one string in a
    return cast(str, a[index])

Einige Typ-Prüfer können möglicherweise nicht ableiten, dass der Typ von a[index] str ist, und leiten nur object oder Any ab, aber wir wissen, dass es (wenn der Code diesen Punkt erreicht) ein String sein muss. Der Aufruf cast(t, x) sagt dem Typ-Prüfer, dass wir zuversichtlich sind, dass der Typ von x t ist. Zur Laufzeit gibt ein Cast immer den Ausdruck unverändert zurück – er prüft nicht den Typ und konvertiert oder koerziert den Wert nicht.

Casts unterscheiden sich von Typ-Kommentaren (siehe vorheriger Abschnitt). Bei Verwendung eines Typ-Kommentars sollte der Typ-Prüfer immer noch überprüfen, ob der abgeleitete Typ mit dem angegebenen Typ übereinstimmt. Bei Verwendung eines Casts sollte der Typ-Prüfer dem Programmierer blind glauben. Außerdem können Casts in Ausdrücken verwendet werden, während Typ-Kommentare nur für Zuweisungen gelten.

NewType-Hilfsfunktion

Es gibt auch Situationen, in denen ein Programmierer logische Fehler vermeiden möchte, indem er einfache Klassen erstellt. Zum Beispiel

class UserId(int):
    pass

def get_by_user_id(user_id: UserId):
    ...

Dieser Ansatz führt jedoch zu einem Laufzeit-Overhead. Um dies zu vermeiden, bietet typing.py eine Hilfsfunktion NewType, die einfache eindeutige Typen mit fast keinem Laufzeit-Overhead erstellt. Für einen statischen Typ-Prüfer ist Derived = NewType('Derived', Base) ungefähr äquivalent zu einer Definition

class Derived(Base):
    def __init__(self, _x: Base) -> None:
        ...

Während zur Laufzeit gibt NewType('Derived', Base) eine Dummy-Funktion zurück, die einfach ihr Argument zurückgibt. Typ-Prüfer erfordern explizite Casts von int, wo UserId erwartet wird, während sie implizit von UserId casten, wo int erwartet wird. Beispiele

UserId = NewType('UserId', int)

def name_by_id(user_id: UserId) -> str:
    ...

UserId('user')          # Fails type check

name_by_id(42)          # Fails type check
name_by_id(UserId(42))  # OK

num = UserId(5) + 1     # type: int

NewType akzeptiert genau zwei Argumente: einen Namen für den neuen eindeutigen Typ und eine Basisklasse. Letztere sollte eine richtige Klasse sein (d. h. kein Typkonstrukt wie Union usw.) oder ein anderer eindeutiger Typ, der durch Aufruf von NewType erstellt wurde. Die von NewType zurückgegebene Funktion akzeptiert nur ein Argument; dies ist äquivalent zur Unterstützung nur eines Konstruktors, der eine Instanz der Basisklasse akzeptiert (siehe oben). Beispiel

class PacketId:
    def __init__(self, major: int, minor: int) -> None:
        self._major = major
        self._minor = minor

TcpPacketId = NewType('TcpPacketId', PacketId)

packet = PacketId(100, 100)
tcp_packet = TcpPacketId(packet)  # OK

tcp_packet = TcpPacketId(127, 0)  # Fails in type checker and at runtime

Sowohl isinstance als auch issubclass sowie Unterklassifizierung schlagen für NewType('Derived', Base) fehl, da Funktionsobjekte diese Operationen nicht unterstützen.

Stub-Dateien

Stub-Dateien sind Dateien, die Typ-Hints enthalten, die nur für den Typ-Prüfer, nicht zur Laufzeit verwendet werden. Es gibt mehrere Anwendungsfälle für Stub-Dateien

  • Erweiterungsmodule
  • Drittanbieter-Module, deren Autoren noch keine Typ-Hints hinzugefügt haben
  • Standardbibliotheksmodule, für die noch keine Typ-Hints geschrieben wurden
  • Module, die mit Python 2 und 3 kompatibel sein müssen
  • Module, die Annotationen für andere Zwecke verwenden

Stub-Dateien haben die gleiche Syntax wie reguläre Python-Module. Es gibt eine Funktion des Moduls typing, die in Stub-Dateien anders ist: der Dekorator @overload, der unten beschrieben wird.

Der Typ-Prüfer sollte nur Funktionssignaturen in Stub-Dateien überprüfen; Es wird empfohlen, dass Funktionskörper in Stub-Dateien nur ein einzelnes Ellipse (...) sind.

Der Typ-Prüfer sollte einen konfigurierbaren Suchpfad für Stub-Dateien haben. Wenn eine Stub-Datei gefunden wird, sollte der Typ-Prüfer nicht das entsprechende „echte“ Modul lesen.

Während Stub-Dateien syntaktisch gültige Python-Module sind, verwenden sie die Erweiterung .pyi, um die Wartung von Stub-Dateien im selben Verzeichnis wie das entsprechende echte Modul zu ermöglichen. Dies verstärkt auch die Vorstellung, dass von Stub-Dateien kein Laufzeitverhalten erwartet werden sollte.

Zusätzliche Hinweise zu Stub-Dateien

  • Module und Variablen, die in den Stub importiert werden, werden nicht als vom Stub exportiert betrachtet, es sei denn, der Import verwendet die Form import ... as ... oder die äquivalente Form from ... import ... as .... (UPDATE: Zur Klarstellung ist die Absicht hier, dass nur Namen, die mit der Form X as X importiert werden, exportiert werden, d. h. der Name vor und nach as muss gleich sein.)
  • Als Ausnahme zu dem vorherigen Aufzählungspunkt werden jedoch alle Objekte, die mit from ... import * in einen Stub importiert werden, als exportiert betrachtet. (Dies erleichtert den erneuten Export aller Objekte aus einem bestimmten Modul, die je nach Python-Version variieren können.)
  • Genau wie in normalen Python-Dateien werden Untermodule beim Importieren automatisch zu exportierten Attributen ihres übergeordneten Moduls. Wenn das Paket spam beispielsweise die folgende Verzeichnisstruktur hat
    spam/
        __init__.pyi
        ham.pyi
    

    wobei __init__.pyi eine Zeile wie from . import ham oder from .ham import Ham enthält, dann ist ham ein exportiertes Attribut von spam.

  • Stub-Dateien können unvollständig sein. Um Typüberprüfer darauf aufmerksam zu machen, kann die Datei den folgenden Code enthalten
    def __getattr__(name) -> Any: ...
    

    Jeder Bezeichner, der nicht im Stub definiert ist, wird daher als vom Typ Any angenommen.

Funktions-/Methodenüberladung

Der Dekorator @overload ermöglicht die Beschreibung von Funktionen und Methoden, die mehrere verschiedene Kombinationen von Argumenttypen unterstützen. Dieses Muster wird häufig in eingebauten Modulen und Typen verwendet. Zum Beispiel kann die Methode __getitem__() des Typs bytes wie folgt beschrieben werden

from typing import overload

class bytes:
    ...
    @overload
    def __getitem__(self, i: int) -> int: ...
    @overload
    def __getitem__(self, s: slice) -> bytes: ...

Diese Beschreibung ist präziser als das, was mit Vereinigungen (Unions) möglich wäre (die die Beziehung zwischen den Argument- und Rückgabetypen nicht ausdrücken können)

from typing import Union

class bytes:
    ...
    def __getitem__(self, a: Union[int, slice]) -> Union[int, bytes]: ...

Ein weiteres Beispiel, bei dem @overload nützlich ist, ist der Typ der eingebauten Funktion map(), die je nach Typ des Aufrufbaren eine unterschiedliche Anzahl von Argumenten annimmt

from typing import Callable, Iterable, Iterator, Tuple, TypeVar, overload

T1 = TypeVar('T1')
T2 = TypeVar('T2')
S = TypeVar('S')

@overload
def map(func: Callable[[T1], S], iter1: Iterable[T1]) -> Iterator[S]: ...
@overload
def map(func: Callable[[T1, T2], S],
        iter1: Iterable[T1], iter2: Iterable[T2]) -> Iterator[S]: ...
# ... and we could add more items to support more than two iterables

Beachten Sie, dass wir auch problemlos Elemente hinzufügen könnten, um map(None, ...) zu unterstützen

@overload
def map(func: None, iter1: Iterable[T1]) -> Iterable[T1]: ...
@overload
def map(func: None,
        iter1: Iterable[T1],
        iter2: Iterable[T2]) -> Iterable[Tuple[T1, T2]]: ...

Verwendungen des Dekorators @overload, wie oben gezeigt, sind für Stub-Dateien geeignet. In regulären Modulen muss auf eine Reihe von @overload-dekorierten Definitionen genau eine nicht @overload-dekorierte Definition (für dieselbe Funktion/Methode) folgen. Die @overload-dekorierten Definitionen sind nur für den Typüberprüfer bestimmt, da sie von der nicht @overload-dekorierten Definition überschrieben werden, während letztere zur Laufzeit verwendet, aber von einem Typüberprüfer ignoriert werden sollte. Zur Laufzeit löst der direkte Aufruf einer @overload-dekorierten Funktion NotImplementedError aus. Hier ist ein Beispiel für eine Nicht-Stub-Überladung, die nicht einfach mit einer Vereinigung (Union) oder einer Typvariable ausgedrückt werden kann

@overload
def utf8(value: None) -> None:
    pass
@overload
def utf8(value: bytes) -> bytes:
    pass
@overload
def utf8(value: unicode) -> bytes:
    pass
def utf8(value):
    <actual implementation>

HINWEIS: Obwohl es möglich wäre, eine Mehrfachdispatch-Implementierung mit dieser Syntax bereitzustellen, würde ihre Implementierung die Verwendung von sys._getframe() erfordern, was verpönt ist. Außerdem ist das Entwerfen und Implementieren eines effizienten Mehrfachdispatch-Mechanismus schwierig, weshalb frühere Versuche zugunsten von functools.singledispatch() aufgegeben wurden. (Siehe PEP 443, insbesondere den Abschnitt „Alternative Ansätze“.) In Zukunft werden wir möglicherweise ein zufriedenstellendes Mehrfachdispatch-Design entwickeln, aber wir möchten nicht, dass ein solches Design durch die für Typ-Hints in Stub-Dateien definierte Überladungs-Syntax eingeschränkt wird. Es ist auch möglich, dass sich beide Features unabhängig voneinander entwickeln (da Überladungen in Typüberprüfern andere Anwendungsfälle und Anforderungen haben als Mehrfachdispatch zur Laufzeit – z. B. wird letzteres wahrscheinlich keine generischen Typen unterstützen).

Eine eingeschränkte TypeVar kann oft anstelle der Verwendung des Dekorators @overload verwendet werden. Zum Beispiel sind die Definitionen von concat1 und concat2 in dieser Stub-Datei äquivalent

from typing import TypeVar, Text

AnyStr = TypeVar('AnyStr', Text, bytes)

def concat1(x: AnyStr, y: AnyStr) -> AnyStr: ...

@overload
def concat2(x: str, y: str) -> str: ...
@overload
def concat2(x: bytes, y: bytes) -> bytes: ...

Einige Funktionen, wie map oder bytes.__getitem__ oben, können nicht präzise mit Typvariablen dargestellt werden. Im Gegensatz zu @overload können Typvariablen jedoch auch außerhalb von Stub-Dateien verwendet werden. Wir empfehlen, @overload nur in Fällen zu verwenden, in denen eine Typvariable nicht ausreicht, aufgrund ihres speziellen Stub-only-Status.

Ein weiterer wichtiger Unterschied zwischen Typvariablen wie AnyStr und der Verwendung von @overload ist, dass erstere auch zur Definition von Einschränkungen für generische Klassen-Typ-Parameter verwendet werden können. Zum Beispiel ist der Typ-Parameter der generischen Klasse typing.IO eingeschränkt (nur IO[str], IO[bytes] und IO[Any] sind gültig)

class IO(Generic[AnyStr]): ...

Speichern und Verteilen von Stub-Dateien

Die einfachste Form der Speicherung und Verteilung von Stub-Dateien ist, sie zusammen mit Python-Modulen im selben Verzeichnis abzulegen. Dies erleichtert das Auffinden sowohl für Programmierer als auch für Werkzeuge. Da Paketbetreuer jedoch frei sind, Typ-Hinweise nicht zu ihren Paketen hinzuzufügen, werden auch von Drittanbietern installierbare Stubs von PyPI unterstützt, die mit pip installiert werden können. In diesem Fall müssen wir drei Probleme berücksichtigen: Benennung, Versionierung und Installationspfad.

Dieses PEP gibt keine Empfehlung für ein Benennungsschema für Stub-Pakete von Drittanbietern. Die Auffindbarkeit wird hoffentlich auf der Beliebtheit des Pakets beruhen, wie z. B. bei Django-Paketen.

Stub-Dateien von Drittanbietern müssen mit der niedrigsten Version des Quellpakets, mit dem sie kompatibel sind, versioniert werden. Beispiel: FooPackage hat die Versionen 1.0, 1.1, 1.2, 1.3, 2.0, 2.1, 2.2. Es gibt API-Änderungen in den Versionen 1.1, 2.0 und 2.2. Der Betreuer des Stub-Pakets ist frei, Stubs für alle Versionen zu veröffentlichen, aber mindestens 1.0, 1.1, 2.0 und 2.2 sind erforderlich, damit der Endbenutzer alle Versionen typüberprüfen kann. Dies liegt daran, dass der Benutzer weiß, dass die nächstgelegene geringere oder gleiche Version von Stubs kompatibel ist. Im obigen Beispiel würde der Benutzer für FooPackage 1.3 die Stub-Version 1.1 wählen.

Beachten Sie, dass die Verwendung der „neuesten“ Stub-Dateien bei Verwendung des „neuesten“ Quellpakets im Allgemeinen auch funktionieren sollte, wenn sie häufig aktualisiert werden.

Stub-Pakete von Drittanbietern können jeden Speicherort für die Stub-Speicherung verwenden. Typüberprüfer sollten mit PYTHONPATH danach suchen. Ein standardmäßiges Fallback-Verzeichnis, das immer überprüft wird, ist shared/typehints/pythonX.Y/ (für ein beliebiges PythonX.Y, wie vom Typüberprüfer ermittelt, nicht nur die installierte Version). Da pro Umgebung nur ein Paket für eine bestimmte Python-Version installiert werden kann, wird unter diesem Verzeichnis keine zusätzliche Versionierung durchgeführt (genau wie bei Bare-Directory-Installationen von pip in site-packages). Autoren von Stub-Paketen könnten den folgenden Ausschnitt in setup.py verwenden

...
data_files=[
    (
        'shared/typehints/python{}.{}'.format(*sys.version_info[:2]),
        pathlib.Path(SRC_PATH).glob('**/*.pyi'),
    ),
],
...

(UPDATE: Seit Juni 2018 hat sich die empfohlene Methode zur Verteilung von Typ-Hinweisen für Pakete von Drittanbietern geändert – zusätzlich zu typeshed (siehe nächster Abschnitt) gibt es nun einen Standard für die Verteilung von Typ-Hinweisen, PEP 561. Er unterstützt separat installierbare Pakete, die Stubs enthalten, Stub-Dateien, die in derselben Distribution wie der ausführbare Code eines Pakets enthalten sind, und Inline-Typ-Hinweise; die beiden letzteren Optionen werden durch die Aufnahme einer Datei namens py.typed in das Paket aktiviert.)

Das Typeshed-Repository

Es gibt ein gemeinsames Repository, in dem nützliche Stubs gesammelt werden. Richtlinien bezüglich der hier gesammelten Stubs werden separat festgelegt und in der Dokumentation des Repos beibehalten. Beachten Sie, dass Stubs für ein bestimmtes Paket hier nicht enthalten sein werden, wenn die Paketbesitzer ausdrücklich deren Auslassung verlangt haben.

Ausnahmen

Es wird keine Syntax zum Auflisten explizit ausgelöster Ausnahmen vorgeschlagen. Derzeit ist der einzige bekannte Anwendungsfall für diese Funktion dokumentarisch, in diesem Fall wird empfohlen, diese Informationen in einem Docstring zu platzieren.

Das Modul typing

Um die Nutzung statischer Typüberprüfung auch für Python 3.5 und ältere Versionen zu ermöglichen, ist ein einheitlicher Namensraum erforderlich. Zu diesem Zweck wird ein neues Modul in der Standardbibliothek eingeführt, das typing heißt.

Es definiert die grundlegenden Bausteine für den Aufbau von Typen (z. B. Any), Typen, die generische Varianten von integrierten Sammlungen darstellen (z. B. List), Typen, die generische Sammlungs-ABCs darstellen (z. B. Sequence) und eine kleine Sammlung von Hilfsdefinitionen.

Beachten Sie, dass spezielle Typkonstrukte wie Any, Union und mit TypeVar definierte Typvariablen nur im Kontext von Typannotationen unterstützt werden und Generic nur als Basisklasse verwendet werden darf. Alle diese (außer unparametrisierten Generics) lösen TypeError aus, wenn sie in isinstance oder issubclass vorkommen.

Grundlegende Bausteine

  • Any, verwendet als def get(key: str) -> Any: ...
  • Union, verwendet als Union[Type1, Type2, Type3]
  • Callable, verwendet als Callable[[Arg1Type, Arg2Type], ReturnType]
  • Tuple, verwendet durch Auflistung der Elementtypen, z. B. Tuple[int, int, str]. Das leere Tupel kann als Tuple[()] typisiert werden. Homogene Tupel beliebiger Länge können mit einem Typ und einer Ellipse ausgedrückt werden, z. B. Tuple[int, ...]. (Die ... hier sind Teil der Syntax, eine literale Ellipse.)
  • TypeVar, verwendet als X = TypeVar('X', Type1, Type2, Type3) oder einfach Y = TypeVar('Y') (siehe oben für weitere Details)
  • Generic, verwendet zur Erstellung benutzerdefinierter generischer Klassen
  • Type, verwendet zur Annotation von Klassenobjekten

Generische Varianten von integrierten Sammlungen

  • Dict, verwendet als Dict[key_type, value_type]
  • DefaultDict, verwendet als DefaultDict[key_type, value_type], eine generische Variante von collections.defaultdict
  • List, verwendet als List[element_type]
  • Set, verwendet als Set[element_type]. Siehe Anmerkung zu AbstractSet unten.
  • FrozenSet, verwendet als FrozenSet[element_type]

Hinweis: Dict, DefaultDict, List, Set und FrozenSet sind hauptsächlich für die Annotation von Rückgabewerten nützlich. Für Argumente werden die unten definierten abstrakten Sammlungs-Typen bevorzugt, z. B. Mapping, Sequence oder AbstractSet.

Generische Varianten von Container-ABCs (und einige Nicht-Container)

  • Awaitable
  • AsyncIterable
  • AsyncIterator
  • ByteString
  • Callable (siehe oben, hier zur Vollständigkeit aufgeführt)
  • Collection
  • Container
  • ContextManager
  • Coroutine
  • Generator, verwendet als Generator[yield_type, send_type, return_type]. Dies stellt den Rückgabewert von Generatorfunktionen dar. Er ist eine Unterklasse von Iterable und verfügt über zusätzliche Typvariablen für den Typ, der von der Methode send() akzeptiert wird (er ist kontravariant in dieser Variablen – ein Generator, der das Senden einer Employee-Instanz akzeptiert, ist in einem Kontext gültig, in dem ein Generator benötigt wird, der das Senden von Manager-Instanzen akzeptiert) und den Rückgabetyp des Generators.
  • Hashable (nicht generisch, aber zur Vollständigkeit vorhanden)
  • ItemsView
  • Iterable
  • Iterator
  • KeysView
  • Mapping
  • MappingView
  • MutableMapping
  • MutableSequence
  • MutableSet
  • Sequence
  • Set, umbenannt in AbstractSet. Diese Namensänderung war erforderlich, da Set im Modul typing set() mit Generics bedeutet.
  • Sized (nicht generisch, aber zur Vollständigkeit vorhanden)
  • ValuesView

Einige einmalige Typen werden definiert, die auf einzelne spezielle Methoden prüfen (ähnlich wie Hashable oder Sized)

  • Reversible, zum Testen von __reversed__
  • SupportsAbs, zum Testen von __abs__
  • SupportsComplex, zum Testen von __complex__
  • SupportsFloat, zum Testen von __float__
  • SupportsInt, zum Testen von __int__
  • SupportsRound, zum Testen von __round__
  • SupportsBytes, zum Testen von __bytes__

Hilfsdefinitionen

  • Optional, definiert durch Optional[t] == Union[t, None]
  • Text, ein einfacher Alias für str in Python 3, für unicode in Python 2
  • AnyStr, definiert als TypeVar('AnyStr', Text, bytes)
  • NamedTuple, verwendet als NamedTuple(type_name, [(field_name, field_type), ...]) und äquivalent zu collections.namedtuple(type_name, [field_name, ...]). Dies ist nützlich, um die Typen der Felder eines Named-Tuple-Typs zu deklarieren.
  • NewType, verwendet zur Erstellung eindeutiger Typen mit geringem Laufzeit-Overhead UserId = NewType('UserId', int)
  • cast(), zuvor beschrieben
  • @no_type_check, ein Dekorator zum Deaktivieren der Typüberprüfung pro Klasse oder Funktion (siehe unten)
  • @no_type_check_decorator, ein Dekorator zur Erstellung eigener Dekoratoren mit derselben Bedeutung wie @no_type_check (siehe unten)
  • @type_check_only, ein Dekorator, der nur während der Typüberprüfung für die Verwendung in Stub-Dateien verfügbar ist (siehe oben); markiert eine Klasse oder Funktion als zur Laufzeit nicht verfügbar
  • @overload, zuvor beschrieben
  • get_type_hints(), eine Hilfsfunktion zum Abrufen der Typ-Hinweise einer Funktion oder Methode. Bei einem Funktions- oder Methodenobjekt gibt sie ein Dictionary im selben Format wie __annotations__ zurück, evaluiert aber Rückwärtsreferenzen (die als Zeichenkettenliterale gegeben sind) als Ausdrücke im Kontext der ursprünglichen Funktions- oder Methodendefinition.
  • TYPE_CHECKING, False zur Laufzeit, aber True für Typüberprüfer

I/O-bezogene Typen

  • IO (generisch über AnyStr)
  • BinaryIO (eine einfache Unterklasse von IO[bytes])
  • TextIO (eine einfache Unterklasse von IO[str])

Typen im Zusammenhang mit regulären Ausdrücken und dem Modul re

  • Match und Pattern, Typen der Ergebnisse von re.match() und re.compile() (generisch über AnyStr)

Vorgeschlagene Syntax für Python 2.7 und überlappenden Code

Einige Werkzeuge möchten möglicherweise Typannotationen in Code unterstützen, der mit Python 2.7 kompatibel sein muss. Zu diesem Zweck enthält dieses PEP eine vorgeschlagene (aber nicht zwingende) Erweiterung, bei der Funktionsannotationen in einem Kommentar # type: platziert werden. Ein solcher Kommentar muss unmittelbar nach dem Funktionsheader (vor dem Docstring) platziert werden. Beispiel: Der folgende Python 3-Code

def embezzle(self, account: str, funds: int = 1000000, *fake_receipts: str) -> None:
    """Embezzle funds from account using fake receipts."""
    <code goes here>

ist äquivalent zu folgendem

def embezzle(self, account, funds=1000000, *fake_receipts):
    # type: (str, int, *str) -> None
    """Embezzle funds from account using fake receipts."""
    <code goes here>

Beachten Sie, dass bei Methoden kein Typ für self benötigt wird.

Für eine argumentlose Methode würde es so aussehen

def load_cache(self):
    # type: () -> bool
    <code>

Manchmal möchten Sie den Rückgabetyp für eine Funktion oder Methode angeben, ohne (noch) die Argumenttypen anzugeben. Um dies explizit zu unterstützen, kann die Argumentliste durch eine Ellipse ersetzt werden. Beispiel

def send_email(address, sender, cc, bcc, subject, body):
    # type: (...) -> bool
    """Send an email message.  Return True if successful."""
    <code>

Manchmal haben Sie eine lange Liste von Parametern und die Angabe ihrer Typen in einem einzigen Kommentar # type: wäre umständlich. Zu diesem Zweck können Sie die Argumente einzeln auflisten und nach dem zugehörigen Komma, falls vorhanden, einen Kommentar # type: pro Zeile hinzufügen. Um den Rückgabetyp anzugeben, verwenden Sie die Ellipsen-Syntax. Die Angabe des Rückgabetyps ist nicht zwingend erforderlich und nicht jedes Argument muss einen Typ erhalten. Eine Zeile mit einem Kommentar # type: sollte genau ein Argument enthalten. Der Typkommentar für das letzte Argument (falls vorhanden) sollte der schließenden Klammer vorausgehen. Beispiel

def send_email(address,     # type: Union[str, List[str]]
               sender,      # type: str
               cc,          # type: Optional[List[str]]
               bcc,         # type: Optional[List[str]]
               subject='',
               body=None    # type: List[str]
               ):
    # type: (...) -> bool
    """Send an email message.  Return True if successful."""
    <code>

Anmerkungen

  • Werkzeuge, die diese Syntax unterstützen, sollten sie unabhängig von der zu überprüfenden Python-Version unterstützen. Dies ist notwendig, um Code zu unterstützen, der sowohl Python 2 als auch Python 3 umfasst.
  • Es ist einem Argument oder Rückgabewert nicht gestattet, sowohl eine Typannotation als auch einen Typkommentar zu haben.
  • Bei Verwendung der Kurzform (z. B. # type: (str, int) -> None) müssen alle Argumente berücksichtigt werden, mit Ausnahme des ersten Arguments von Instanz- und Klassenmethoden (diese werden normalerweise weggelassen, es ist aber erlaubt, sie einzuschließen).
  • Der Rückgabetyp ist für die Kurzform zwingend erforderlich. Wenn in Python 3 einige Argumente oder der Rückgabetyp weggelassen würden, sollte die Python 2-Notation Any verwenden.
  • Bei Verwendung der Kurzform setzen Sie für *args und **kwds einen oder zwei Sterne vor die entsprechende Typannotation. (Wie bei Python 3-Annotationen bezeichnet die Annotation hier den Typ der einzelnen Argumentwerte, nicht des Tupels/Dictionaries, das Sie als speziellen Argumentwert args oder kwds erhalten.)
  • Wie andere Typkommentare müssen auch Namen, die in den Annotationen verwendet werden, von dem Modul, das die Annotation enthält, importiert oder definiert sein.
  • Bei Verwendung der Kurzform muss die gesamte Annotation einzeilig sein.
  • Die Kurzform kann auch in derselben Zeile wie die schließende Klammer erscheinen, z. B.
    def add(a, b):  # type: (int, int) -> int
        return a + b
    
  • Falsch platzierte Typkommentare werden von einem Typüberprüfer als Fehler gekennzeichnet. Bei Bedarf könnten solche Kommentare doppelt auskommentiert werden. Zum Beispiel
    def f():
        '''Docstring'''
        # type: () -> None  # Error!
    
    def g():
        '''Docstring'''
        # # type: () -> None  # This is OK
    

Bei der Überprüfung von Python 2.7-Code sollten Typüberprüfer die Typen int und long als äquivalent behandeln. Für Parameter mit dem Typ Text sollten Argumente vom Typ str sowie unicode akzeptabel sein.

Abgelehnte Alternativen

Während der Diskussion früherer Entwürfe dieses PEP wurden verschiedene Einwände erhoben und Alternativen vorgeschlagen. Wir besprechen einige davon hier und erklären, warum wir sie ablehnen.

Mehrere Haupteinwände wurden erhoben.

Welche Klammern für generische Typparameter?

Die meisten Menschen sind mit der Verwendung von spitzen Klammern (z. B. List<int>) in Sprachen wie C++, Java, C# und Swift zur Angabe der Parametrisierung generischer Typen vertraut. Das Problem dabei ist, dass sie besonders für einen einfachen Parser wie Python sehr schwer zu parsen sind. In den meisten Sprachen werden die Mehrdeutigkeiten normalerweise dadurch behoben, dass spitze Klammern nur in bestimmten syntaktischen Positionen erlaubt sind, wo allgemeine Ausdrücke nicht zulässig sind. (Und auch durch die Verwendung sehr leistungsfähiger Parsing-Techniken, die über einen beliebigen Code-Abschnitt zurückverfolgen können.)

Aber in Python möchten wir, dass Typausdrücke (syntaktisch) mit anderen Ausdrücken identisch sind, sodass wir z. B. Variablendeklarationen zur Erstellung von Typ-Aliassen verwenden können. Betrachten Sie diesen einfachen Typausdruck

List<int>

Aus Sicht des Python-Parsers beginnt der Ausdruck mit denselben vier Token (NAME, LESS, NAME, GREATER) wie ein verketteter Vergleich

a < b > c  # I.e., (a < b) and (b > c)

Wir können sogar ein Beispiel erstellen, das auf beide Arten geparst werden könnte

a < b > [ c ]

Unter der Annahme, dass wir spitze Klammern in der Sprache hätten, könnte dies wie folgt interpretiert werden

(a<b>)[c]      # I.e., (a<b>).__getitem__(c)
a < b > ([c])  # I.e., (a < b) and (b > [c])

Es wäre sicherlich möglich, eine Regel zu finden, um solche Fälle aufzulösen, aber für die meisten Benutzer würden sich die Regeln willkürlich und komplex anfühlen. Außerdem müssten wir den CPython-Parser (und jeden anderen Python-Parser) drastisch ändern. Es sollte beachtet werden, dass der aktuelle Parser von Python absichtlich „dumm“ ist – eine einfache Grammatik ist für Benutzer leichter nachvollziehbar.

Aus all diesen Gründen sind eckige Klammern (z. B. List[int]) die (seit langem) bevorzugte Syntax für generische Typ-Parameter. Sie können durch die Definition der Methode __getitem__() auf der Metaklasse implementiert werden, und es ist überhaupt keine neue Syntax erforderlich. Diese Option funktioniert in allen neueren Python-Versionen (ab Python 2.2). Python ist mit dieser syntaktischen Wahl nicht allein – generische Klassen in Scala verwenden ebenfalls eckige Klammern.

Was ist mit bestehenden Verwendungen von Annotationen?

Ein Argumentationsstrang weist darauf hin, dass PEP 3107 die Verwendung beliebiger Ausdrücke in Funktionsannotationen ausdrücklich unterstützt. Der neue Vorschlag wird dann als inkompatibel mit der Spezifikation von PEP 3107 betrachtet.

Unsere Antwort darauf ist, dass der aktuelle Vorschlag erstens keine direkten Inkompatibilitäten einführt, sodass Programme, die Annotationen in Python 3.4 verwenden, auch in Python 3.5 korrekt und ohne Benachteiligung funktionieren.

Wir hoffen, dass Typ-Hinweise schließlich der alleinige Verwendungszweck für Annotationen werden, aber dies erfordert weitere Diskussionen und eine Übergangsfrist nach der anfänglichen Einführung des `typing`-Moduls mit Python 3.5. Dieses PEP wird einen provisorischen Status haben (siehe PEP 411), bis Python 3.6 veröffentlicht wird. Das schnellstmöglich denkbare Schema würde die stille Deprekation von Nicht-Typ-Hinweis-Annotationen in 3.6, die vollständige Deprekation in 3.7 und die Erklärung von Typ-Hinweisen als alleinigen erlaubten Verwendungszweck von Annotationen in Python 3.8 einführen. Dies sollte Autoren von Paketen, die Annotationen verwenden, ausreichend Zeit geben, einen anderen Ansatz zu finden, selbst wenn Typ-Hinweise über Nacht erfolgreich werden.

(UPDATE: Seit Herbst 2017 hat sich der Zeitplan für das Ende des provisorischen Status dieses PEP und des Moduls typing.py geändert, und damit auch der Zeitplan für die Deprekation anderer Verwendungen von Annotationen. Für den aktualisierten Zeitplan siehe PEP 563.)

Ein weiteres mögliches Ergebnis wäre, dass Typ-Hinweise schließlich die Standardbedeutung für Annotationen werden, aber dass es immer eine Option geben wird, sie zu deaktivieren. Zu diesem Zweck definiert der aktuelle Vorschlag einen Dekorator @no_type_check, der die Standardinterpretation von Annotationen als Typ-Hinweise in einer bestimmten Klasse oder Funktion deaktiviert. Er definiert auch einen Meta-Dekorator @no_type_check_decorator, der verwendet werden kann, um einen Dekorator (!) zu dekorieren, wodurch Annotationen in jeder Funktion oder Klasse, die mit letzterem dekoriert sind, vom Typüberprüfer ignoriert werden.

Es gibt auch # type: ignore-Kommentare, und statische Prüfer sollten Konfigurationsoptionen unterstützen, um die Typüberprüfung in ausgewählten Paketen zu deaktivieren.

Trotz all dieser Optionen wurden Vorschläge zirkuliert, um Typ-Hinweise und andere Formen von Annotationen für einzelne Argumente nebeneinander zuzulassen. Ein Vorschlag besagt, dass, wenn eine Annotation für ein bestimmtes Argument eine Dictionary-Literal ist, jeder Schlüssel eine andere Form der Annotation darstellt und der Schlüssel 'type' für Typ-Hinweise verwendet würde. Das Problem mit dieser Idee und ihren Varianten ist, dass die Notation sehr „laut“ und schwer zu lesen wird. Außerdem wäre in den meisten Fällen, in denen vorhandene Bibliotheken Annotationen verwenden, die Kombination mit Typ-Hinweisen kaum erforderlich. Der einfachere Ansatz, Typ-Hinweise selektiv zu deaktivieren, erscheint daher ausreichend.

Das Problem von Vorwärtsdeklarationen

Der aktuelle Vorschlag ist zugegebenermaßen sub-optimal, wenn Typ-Hinweise Rückwärtsreferenzen enthalten müssen. Python verlangt, dass alle Namen definiert sind, wenn sie verwendet werden. Abgesehen von zirkulären Importen ist dies selten ein Problem: „Verwendung“ bedeutet hier „zur Laufzeit nachschlagen“, und bei den meisten „rückwärtsgerichteten“ Referenzen gibt es kein Problem, sicherzustellen, dass ein Name definiert ist, bevor die verwendende Funktion aufgerufen wird.

Das Problem bei Typ-Hinweisen ist, dass Annotationen (gemäß PEP 3107 und ähnlich wie Standardwerte) zum Zeitpunkt der Funktionsdefinition ausgewertet werden, und somit müssen alle in einer Annotation verwendeten Namen definiert sein, wenn die Funktion definiert wird. Ein häufiges Szenario ist die Klassendefinition, deren Methoden in ihren Annotationen auf die Klasse selbst verweisen müssen. (Allgemeiner kann dies auch bei wechselseitig rekursiven Klassen auftreten.) Dies ist für Containertypen zum Beispiel natürlich

class Node:
    """Binary tree node."""

    def __init__(self, left: Node, right: Node):
        self.left = left
        self.right = right

So wie es geschrieben ist, wird es wegen der Eigenart von Python, dass Klassennamen definiert werden, sobald der gesamte Körper der Klasse ausgeführt wurde, nicht funktionieren. Unsere Lösung, die nicht besonders elegant, aber zweckmäßig ist, besteht darin, die Verwendung von Zeichenkettenliteralen in Annotationen zu erlauben. Die meiste Zeit müssen Sie dies jedoch nicht verwenden – die meisten *Verwendungen* von Typ-Hinweisen werden voraussichtlich auf eingebaute Typen oder in anderen Modulen definierte Typen verweisen.

Ein Gegenentwurf würde die Semantik von Typ-Hinweisen ändern, sodass sie zur Laufzeit überhaupt nicht ausgewertet werden (schließlich findet die Typüberprüfung offline statt, warum sollten Typ-Hinweise zur Laufzeit ausgewertet werden müssen?). Dies würde natürlich die Abwärtskompatibilität verletzen, da der Python-Interpreter nicht weiß, ob eine bestimmte Annotation als Typ-Hinweis oder etwas anderes gedacht ist.

Ein Kompromiss ist möglich, bei dem ein `__future__`-Import *alle* Annotationen in einem bestimmten Modul in Zeichenkettenliterale umwandeln könnte, wie folgt

from __future__ import annotations

class ImSet:
    def add(self, a: ImSet) -> List[ImSet]: ...

assert ImSet.add.__annotations__ == {'a': 'ImSet', 'return': 'List[ImSet]'}

Eine solche __future__ Import-Anweisung kann in einem separaten PEP vorgeschlagen werden.

(UPDATE: Diese __future__ Import-Anweisung und ihre Konsequenzen werden in PEP 563 diskutiert.)

Der Doppelpunkt

Einige kreative Seelen haben versucht, Lösungen für dieses Problem zu erfinden. Zum Beispiel wurde vorgeschlagen, ein doppeltes Doppelpunktzeichen (::) für Typ-Hints zu verwenden, wodurch zwei Probleme auf einmal gelöst werden: die Unterscheidung zwischen Typ-Hints und anderen Annotationen sowie die Änderung der Semantik, um die Laufzeitauswertung auszuschließen. Es gibt jedoch mehrere Probleme mit dieser Idee.

  • Es ist hässlich. Der einfache Doppelpunkt in Python hat viele Verwendungszwecke, und alle sehen vertraut aus, da sie der Verwendung des Doppelpunkts in englischen Texten ähneln. Dies ist eine allgemeine Faustregel, an die sich Python für die meisten Formen der Interpunktion hält; Ausnahmen sind typischerweise aus anderen Programmiersprachen bekannt. Aber diese Verwendung von :: ist im Englischen unbekannt und wird in anderen Sprachen (z. B. C++) als Bereichsoperator verwendet, was etwas ganz anderes ist. Im Gegensatz dazu liest sich der einfache Doppelpunkt für Typ-Hints natürlich – und kein Wunder, da er sorgfältig für diesen Zweck entwickelt wurde (die Idee ging lange vor PEP 3107 voraus). Er wird auch in anderen Sprachen von Pascal bis Swift in gleicher Weise verwendet.
  • Was würden Sie für Rückgabetyp-Annotationen tun?
  • Es ist eigentlich eine Funktion, dass Typ-Hints zur Laufzeit ausgewertet werden.
    • Typ-Hints zur Laufzeit verfügbar zu machen, ermöglicht den Aufbau von Laufzeit-Typprüfern auf Basis von Typ-Hints.
    • Es fängt Fehler auf, auch wenn der Typ-Checker nicht ausgeführt wird. Da es sich um ein separates Programm handelt, können Benutzer wählen, es nicht auszuführen (oder es gar nicht zu installieren), möchten aber dennoch Typ-Hints als prägnante Form der Dokumentation verwenden. Fehlerhafte Typ-Hints sind auch für die Dokumentation nutzlos.
  • Da es sich um eine neue Syntax handelt, würde die Verwendung des doppelten Doppelpunkts für Typ-Hints diese auf Code beschränken, der nur mit Python 3.5 funktioniert. Durch die Verwendung vorhandener Syntax kann der aktuelle Vorschlag problemlos für ältere Versionen von Python 3 funktionieren. (Tatsächlich unterstützt mypy Python 3.2 und neuer.)
  • Wenn Typ-Hints erfolgreich werden, könnten wir uns in Zukunft entscheiden, neue Syntax hinzuzufügen, um den Typ von Variablen zu deklarieren, zum Beispiel var age: int = 42. Wenn wir für Argument-Typ-Hints einen doppelten Doppelpunkt verwenden würden, müssten wir aus Gründen der Konsistenz dieselbe Konvention für zukünftige Syntax verwenden und damit die Hässlichkeit fortsetzen.

Andere Formen neuer Syntax

Einige andere Formen alternativer Syntax wurden vorgeschlagen, z. B. die Einführung eines where Schlüsselworts und Cobra-inspirierte requires Klauseln. Aber diese teilen alle ein Problem mit dem doppelten Doppelpunkt: Sie funktionieren nicht für frühere Versionen von Python 3. Dasselbe würde für einen neuen __future__ Import gelten.

Andere abwärtskompatible Konventionen

Die vorgebrachten Ideen umfassen

  • Ein Dekorator, z. B. @typehints(name=str, returns=str). Das könnte funktionieren, ist aber ziemlich umständlich (eine zusätzliche Zeile und die Argumentnamen müssen wiederholt werden) und weit entfernt von der Eleganz der PEP 3107 Notation.
  • Stub-Dateien. Wir wollen Stub-Dateien, aber sie sind hauptsächlich nützlich, um Typ-Hints zu vorhandenem Code hinzuzufügen, der sich nicht für das Hinzufügen von Typ-Hints eignet, z. B. 3rd-Party-Pakete, Code, der sowohl Python 2 als auch Python 3 unterstützen muss, und insbesondere Erweiterungsmodule. In den meisten Situationen sind die Annotationen direkt bei den Funktionsdefinitionen viel nützlicher.
  • Docstrings. Es gibt eine bestehende Konvention für Docstrings, basierend auf der Sphinx-Notation (:type arg1: description). Dies ist ziemlich umständlich (eine zusätzliche Zeile pro Parameter) und nicht sehr elegant. Wir könnten auch etwas Neues erfinden, aber die Annotationssyntax ist schwer zu schlagen (da sie genau für diesen Zweck entwickelt wurde).

Es wurde auch vorgeschlagen, einfach eine weitere Version abzuwarten. Aber welches Problem würde das lösen? Es wäre nur Prokrastination.

PEP-Entwicklungsprozess

Ein Live-Entwurf für dieses PEP befindet sich auf GitHub. Es gibt auch ein Issue-Tracker, wo ein Großteil der technischen Diskussion stattfindet.

Der Entwurf auf GitHub wird regelmäßig in kleinen Schritten aktualisiert. Das offizielle PEPS-Repository wird (normalerweise) nur aktualisiert, wenn ein neuer Entwurf an python-dev gesendet wird.

Danksagungen

Dieses Dokument konnte nicht ohne wertvollen Input, Ermutigung und Ratschläge von Jim Baker, Jeremy Siek, Michael Matson Vitousek, Andrey Vlasovskikh, Radomir Dopieralski, Peter Ludemann und dem BDFL-Delegate, Mark Shannon, fertiggestellt werden.

Einflüsse umfassen bestehende Sprachen, Bibliotheken und Frameworks, die in PEP 482 erwähnt werden. Vielen Dank an ihre Schöpfer, in alphabetischer Reihenfolge: Stefan Behnel, William Edwards, Greg Ewing, Larry Hastings, Anders Hejlsberg, Alok Menghrajani, Travis E. Oliphant, Joe Pamer, Raoul-Gabriel Urma und Julien Verlaguet.


Quelle: https://github.com/python/peps/blob/main/peps/pep-0484.rst

Zuletzt geändert: 2025-02-01 08:59:27 GMT