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

Python Enhancement Proposals

PEP 483 – Die Theorie der Typ-Annotationen

Autor:
Guido van Rossum <guido at python.org>, Ivan Levkivskyi <levkivskyi at gmail.com>
Discussions-To:
Python-Ideas Liste
Status:
Final
Typ:
Informational
Thema:
Typisierung
Erstellt:
19-Dez-2014
Post-History:


Inhaltsverzeichnis

Zusammenfassung

Dieses PEP legt die Theorie dar, auf die in PEP 484 verwiesen wird.

Einleitung

Dieses Dokument legt die Theorie des neuen Vorschlags für Typ-Annotationen für Python 3.5 dar. Es ist keine vollständige Beschreibung oder Spezifikation, da viele Details ausgearbeitet werden müssen, aber es legt die Theorie dar, ohne die es schwierig ist, detailliertere Spezifikationen zu diskutieren. Wir beginnen mit einer Wiederholung grundlegender Konzepte der Typentheorie; dann erklären wir graduelles Typisieren; dann legen wir einige allgemeine Regeln fest und definieren die neuen speziellen Typen (wie Union), die in Annotationen verwendet werden können; und schließlich definieren wir den Ansatz für generische Typen und pragmatische Aspekte von Typ-Annotationen.

Notationelle Konventionen

  • t1, t2 usw. und u1, u2 usw. sind Typen. Manchmal schreiben wir ti oder tj, um uns auf „irgendeinen von t1, t2 usw.“ zu beziehen.
  • T, U usw. sind Typvariablen (definiert mit TypeVar(), siehe unten).
  • Objekte, mit einer class-Anweisung definierte Klassen und Instanzen werden unter Verwendung der Standardkonventionen von PEP 8 bezeichnet.
  • Das Symbol ==, angewendet auf Typen im Kontext dieses PEP, bedeutet, dass zwei Ausdrücke denselben Typ darstellen.
  • Beachten Sie, dass PEP 484 eine Unterscheidung zwischen Typen und Klassen macht (ein Typ ist ein Konzept für den Typ-Checker, während eine Klasse ein Laufzeitkonzept ist). In diesem PEP klären wir diese Unterscheidung, vermeiden aber unnötige Strenge, um mehr Flexibilität bei der Implementierung von Typ-Checkern zu ermöglichen.

Hintergrund

Es gibt viele Definitionen des Konzepts des Typs in der Literatur. Hier nehmen wir an, dass ein Typ eine Menge von Werten und eine Menge von Funktionen ist, die man auf diese Werte anwenden kann.

Es gibt mehrere Möglichkeiten, einen bestimmten Typ zu definieren

  • Durch explizites Auflisten aller Werte. Z.B. bilden True und False den Typ bool.
  • Durch Angabe von Funktionen, die mit Variablen eines Typs verwendet werden können. Z.B. bilden alle Objekte, die eine __len__-Methode haben, den Typ Sized. Sowohl [1, 2, 3] als auch 'abc' gehören zu diesem Typ, da man len darauf anwenden kann
    len([1, 2, 3])  # OK
    len('abc')      # also OK
    len(42)         # not a member of Sized
    
  • Durch eine einfache Klassendefinition, z.B. wenn man eine Klasse definiert
    class UserID(int):
        pass
    

    dann bilden auch alle Instanzen dieser Klasse einen Typ.

  • Es gibt auch komplexere Typen. Z.B. kann man den Typ FancyList als alle Listen definieren, die nur Instanzen von int, str oder deren Unterklassen enthalten. Der Wert [1, 'abc', UserID(42)] hat diesen Typ.

Es ist wichtig für den Benutzer, Typen in einer Form definieren zu können, die von Typ-Checkern verstanden wird. Das Ziel dieses PEP ist es, eine solche systematische Methode zur Definition von Typen für Typ-Annotationen von Variablen und Funktionen unter Verwendung der PEP 3107-Syntax vorzuschlagen. Diese Annotationen können verwendet werden, um viele Arten von Fehlern zu vermeiden, zu Dokumentationszwecken oder vielleicht sogar zur Erhöhung der Geschwindigkeit der Programmausführung. Hier konzentrieren wir uns nur auf die Vermeidung von Fehlern mithilfe eines statischen Typ-Checkers.

Untertyp-Beziehungen

Ein entscheidendes Konzept für statische Typ-Checker ist die Untertyp-Beziehung. Sie ergibt sich aus der Frage: Wenn first_var den Typ first_type hat und second_var den Typ second_type hat, ist es sicher, first_var = second_var zuzuweisen?

Ein starkes Kriterium dafür, wann dies sicher *sein sollte*, ist:

  • Jeder Wert aus second_type ist auch in der Menge der Werte von first_type enthalten; und
  • Jede Funktion aus first_type ist auch in der Menge der Funktionen von second_type enthalten.

Die so definierte Beziehung wird als Untertyp-Beziehung bezeichnet.

Nach dieser Definition

  • Jeder Typ ist ein Untertyp von sich selbst.
  • Die Menge der Werte wird im Prozess des Subtypisierens kleiner, während die Menge der Funktionen größer wird.

Ein intuitives Beispiel: Jeder Dog ist ein Animal, außerdem hat Dog mehr Funktionen, zum Beispiel kann er bellen, daher ist Dog ein Untertyp von Animal. Umgekehrt ist Animal kein Untertyp von Dog.

Ein formelleres Beispiel: Ganzzahlen sind ein Untertyp von reellen Zahlen. Tatsächlich ist jede ganze Zahl natürlich auch eine reelle Zahl, und Ganzzahlen unterstützen mehr Operationen, wie z.B. Bitwise-Shifts << und >>

lucky_number = 3.14    # type: float
lucky_number = 42      # Safe
lucky_number * 2       # This works
lucky_number << 5      # Fails

unlucky_number = 13    # type: int
unlucky_number << 5    # This works
unlucky_number = 2.72  # Unsafe

Betrachten wir auch ein kniffliges Beispiel: Wenn List[int] den Typ bezeichnet, der aus allen Listen besteht, die nur Ganzzahlen enthalten, dann ist er *kein* Untertyp von List[float], der aus allen Listen besteht, die nur reelle Zahlen enthalten. Die erste Bedingung des Subtypisierens ist erfüllt, aber das Anhängen einer reellen Zahl funktioniert nur mit List[float], so dass die zweite Bedingung fehlschlägt

def append_pi(lst: List[float]) -> None:
    lst += [3.14]

my_list = [1, 3, 5]  # type: List[int]

append_pi(my_list)   # Naively, this should be safe...

my_list[-1] << 5     # ... but this fails

Es gibt zwei verbreitete Ansätze, Untertyp-Informationen für einen Typ-Checker zu *deklarieren*.

Beim nominalen Subtypisieren basiert der Typbaum auf dem Klassentyp, d.h. UserID wird als Untertyp von int betrachtet. Dieser Ansatz sollte unter Kontrolle des Typ-Checkers verwendet werden, da man in Python Attribute auf inkompatible Weise überschreiben kann

class Base:
    answer = '42' # type: str

class Derived(Base):
    answer = 5 # should be marked as error by type checker

Beim strukturellen Subtypisieren wird die Untertyp-Beziehung aus den deklarierten Methoden abgeleitet, d.h. UserID und int würden als derselbe Typ betrachtet. Obwohl dies gelegentlich zu Verwirrung führen kann, wird strukturelles Subtypisieren als flexibler angesehen. Wir bemühen uns, beide Ansätze zu unterstützen, so dass strukturelle Informationen zusätzlich zum nominalen Subtypisieren verwendet werden können.

Zusammenfassung des graduellen Typisierens

Graduelles Typisieren erlaubt es, nur einen Teil eines Programms zu annotieren und somit die wünschenswerten Aspekte sowohl des dynamischen als auch des statischen Typisierens zu nutzen.

Wir definieren eine neue Beziehung, ist-konsistent-mit, die ähnlich wie ist-ein-Untertyp-von ist, außer dass sie nicht transitiv ist, wenn der neue Typ Any beteiligt ist. (Keine der beiden Beziehungen ist symmetrisch.) Das Zuweisen von a_value zu a_variable ist in Ordnung, wenn der Typ von a_value mit dem Typ von a_variable konsistent ist. (Vergleichen Sie dies mit "... wenn der Typ von a_value ein Untertyp des Typs von a_variable ist", was eine der Grundlagen der OO-Programmierung besagt.) Die ist-konsistent-mit-Beziehung wird durch drei Regeln definiert

  • Ein Typ t1 ist konsistent mit einem Typ t2, wenn t1 ein Untertyp von t2 ist. (Aber nicht umgekehrt.)
  • Any ist konsistent mit jedem Typ. (Aber Any ist kein Untertyp von jedem Typ.)
  • Jeder Typ ist konsistent mit Any. (Aber jeder Typ ist kein Untertyp von Any.)

Das ist alles! Siehe Jeremy Sieks Blogbeitrag What is Gradual Typing für eine längere Erklärung und Motivation. Any kann als ein Typ betrachtet werden, der alle Werte und alle Methoden hat. Kombiniert mit der obigen Definition von Subtypisierung platziert dies Any teilweise an der Spitze (er hat alle Werte) und am Boden (er hat alle Methoden) der Typenhierarchie. Im Gegensatz dazu object – es ist nicht konsistent mit den meisten Typen (z.B. kann man keine object()-Instanz dort verwenden, wo ein int erwartet wird). Anders ausgedrückt, beide Any und object bedeuten "jeder Typ ist erlaubt", wenn sie zur Annotation eines Arguments verwendet werden, aber nur Any kann übergeben werden, unabhängig davon, welcher Typ erwartet wird (im Wesentlichen deklariert Any einen Fallback zur dynamischen Typisierung und unterdrückt Beschwerden des statischen Checkers).

Hier ist ein Beispiel, das zeigt, wie diese Regeln in der Praxis funktionieren

Nehmen wir an, wir haben eine Employee-Klasse und eine Unterklasse Manager

class Employee: ...
class Manager(Employee): ...

Nehmen wir an, die Variable worker ist mit dem Typ Employee deklariert

worker = Employee()  # type: Employee

Jetzt ist es in Ordnung, eine Manager-Instanz worker zuzuweisen (Regel 1)

worker = Manager()

Es ist nicht in Ordnung, eine Employee-Instanz einer Variablen zuzuweisen, die mit dem Typ Manager deklariert ist

boss = Manager()  # type: Manager
boss = Employee()  # Fails static check

Angenommen, wir haben jedoch eine Variable, deren Typ Any ist

something = some_func()  # type: Any

Jetzt ist es in Ordnung, something worker zuzuweisen (Regel 2)

worker = something  # OK

Natürlich ist es auch in Ordnung, worker something zuzuweisen (Regel 3), aber wir brauchten das Konzept der Konsistenz dafür nicht.

something = worker  # OK

Typen vs. Klassen

In Python sind Klassen Objekt-Fabriken, die durch die class-Anweisung definiert und von der integrierten Funktion type(obj) zurückgegeben werden. Eine Klasse ist ein dynamisches Laufzeitkonzept.

Das Typ-Konzept wurde oben beschrieben, Typen erscheinen in Variablen- und Funktions-Typ-Annotationen, können aus den unten beschriebenen Bausteinen konstruiert werden und werden von statischen Typ-Checkern verwendet.

Jede Klasse ist, wie oben diskutiert, ein Typ. Aber es ist schwierig und fehleranfällig, eine Klasse zu implementieren, die genau die Semantik eines gegebenen Typs darstellt, und das ist kein Ziel von PEP 484. *Die statischen Typen, die in* PEP 484 *beschrieben werden, sollten nicht mit den Laufzeitklassen verwechselt werden.* Beispiele

  • int ist eine Klasse und ein Typ.
  • UserID ist eine Klasse und ein Typ.
  • Union[str, int] ist ein Typ, aber keine richtige Klasse
    class MyUnion(Union[str, int]): ...  # raises TypeError
    
    Union[str, int]()  # raises TypeError
    

Die Typisierungs-Schnittstelle wird mit Klassen implementiert, d.h. zur Laufzeit ist es möglich, z.B. Generic[T].__bases__ zu evaluieren. Aber um die Unterscheidung zwischen Klassen und Typen zu betonen, gelten die folgenden allgemeinen Regeln

  • Keine der unten definierten Typen (d.h. Any, Union usw.) können instanziiert werden, ein Versuch dazu wird TypeError auslösen. (Aber nicht-abstrakte Unterklassen von Generic können es.)
  • Keine der unten definierten Typen können Unterklassen bilden, außer Generic und davon abgeleiteten Klassen.
  • All dies wird TypeError auslösen, wenn sie in isinstance oder issubclass vorkommen (außer für nicht-parametrisierte Generika).

Fundamentale Bausteine

  • Any. Jeder Typ ist konsistent mit Any; und er ist auch konsistent mit jedem Typ (siehe oben).
  • Union[t1, t2, …]. Typen, die Untertypen von mindestens einem der t1 usw. sind, sind Untertypen davon.
    • Vereinigungen, deren Komponenten Untertypen von t1 usw. sind, sind Untertypen davon. Beispiel: Union[int, str] ist ein Untertyp von Union[int, float, str].
    • Die Reihenfolge der Argumente spielt keine Rolle. Beispiel: Union[int, str] == Union[str, int].
    • Wenn ti selbst eine Union ist, wird das Ergebnis abgeflacht. Beispiel: Union[int, Union[float, str]] == Union[int, float, str].
    • Wenn ti und tj eine Untertyp-Beziehung haben, überlebt der weniger spezifische Typ. Beispiel: Union[Employee, Manager] == Union[Employee].
    • Union[t1] gibt nur t1 zurück. Union[] ist illegal, ebenso Union[()]
    • Korollar: Union[..., object, ...] gibt object zurück.
  • Optional[t1]. Alias für Union[t1, None], d.h. Union[t1, type(None)].
  • Tuple[t1, t2, …, tn]. Ein Tupel, dessen Elemente Instanzen von t1 usw. sind. Beispiel: Tuple[int, float] bedeutet ein Tupel aus zwei Elementen, das erste ist ein int, das zweite ist ein float; z.B. (42, 3.14).
    • Tuple[u1, u2, ..., um] ist ein Untertyp von Tuple[t1, t2, ..., tn], wenn sie die gleiche Länge n==m haben und jedes ui ein Untertyp von ti ist.
    • Um den Typ des leeren Tupels auszudrücken, verwenden Sie Tuple[()].
    • Ein variadischer homogener Tupeltyp kann als Tuple[t1, ...] geschrieben werden. (Das sind drei Punkte, ein literaler Ellipsen-Operator; und ja, das ist ein gültiges Token in der Syntax von Python.)
  • Callable[[t1, t2, …, tn], tr]. Eine Funktion mit Positionsargument-Typen t1 usw. und Rückgabetyp tr. Die Argumentliste kann leer sein n==0. Es gibt keine Möglichkeit, optionale oder Schlüsselwortargumente oder Varargs anzugeben, aber Sie können sagen, dass die Argumentliste vollständig unüberprüft ist, indem Sie Callable[..., tr] schreiben (wiederum ein literaler Ellipsen-Operator).

Wir könnten hinzufügen

  • Intersection[t1, t2, …]. Typen, die Untertypen von *jedem* der t1 usw. sind, sind Untertypen davon. (Vergleichen Sie mit Union, das *mindestens eins* anstelle von *jedem* in seiner Definition hat.)
    • Die Reihenfolge der Argumente spielt keine Rolle. Verschachtelte Schnitte werden abgeflacht, z.B. Intersection[int, Intersection[float, str]] == Intersection[int, float, str].
    • Ein Schnitt aus weniger Typen ist ein Obertyp eines Schnitts aus mehr Typen, z.B. ist Intersection[int, str] ein Obertyp von Intersection[int, float, str].
    • Ein Schnitt aus einem Argument ist nur dieses Argument, z.B. Intersection[int] ist int.
    • Wenn Argumente eine Untertyp-Beziehung haben, überlebt der spezifischere Typ, z.B. ist Intersection[str, Employee, Manager] Intersection[str, Manager].
    • Intersection[] ist illegal, ebenso Intersection[()].
    • Korollar: Any verschwindet aus der Argumentliste, z.B. Intersection[int, str, Any] == Intersection[int, str]. Intersection[Any, object] ist object.
    • Die Interaktion zwischen Intersection und Union ist komplex, sollte aber keine Überraschung sein, wenn man die Interaktion zwischen Schnitten und Vereinigungen regulärer Mengen versteht (beachten Sie, dass Mengen von Typen unendlich groß sein können, da es keine Begrenzung für die Anzahl neuer Unterklassen gibt).

Generische Typen

Die oben definierten fundamentalen Bausteine ermöglichen es, neue Typen generisch zu konstruieren. Zum Beispiel kann Tuple einen konkreten Typ float nehmen und einen konkreten Typ Vector = Tuple[float, ...] bilden, oder es kann einen anderen Typ UserID nehmen und einen anderen konkreten Typ Registry = Tuple[UserID, ...] bilden. Solche Semantik ist als generischer Typ-Konstruktor bekannt, sie ähnelt der Semantik von Funktionen, aber eine Funktion nimmt einen Wert und gibt einen Wert zurück, während ein generischer Typ-Konstruktor einen Typ nimmt und einen Typ „zurückgibt“.

Es ist üblich, dass eine bestimmte Klasse oder Funktion auf solche typen-generische Weise verhält. Betrachten Sie zwei Beispiele

  • Container-Klassen wie list oder dict enthalten typischerweise nur Werte eines bestimmten Typs. Daher möchte ein Benutzer sie möglicherweise wie folgt typisieren:
    users = [] # type: List[UserID]
    users.append(UserID(42)) # OK
    users.append('Some guy') # Should be rejected by the type checker
    
    examples = {} # type: Dict[str, Any]
    examples['first example'] = object() # OK
    examples[2] = None                   # rejected by the type checker
    
  • Die folgende Funktion kann zwei Argumente vom Typ int nehmen und ein int zurückgeben, oder zwei Argumente vom Typ float nehmen und ein float zurückgeben, etc.
    def add(x, y):
        return x + y
    
    add(1, 2) == 3
    add('1', '2') == '12'
    add(2.7, 3.5) == 6.2
    

Um Typ-Annotationen in Situationen des ersten Beispiels zu ermöglichen, werden eingebaute Container und abstrakte Basisklassen für Container um Typ-Parameter erweitert, sodass sie als generische Typ-Konstruktoren fungieren. Klassen, die als generische Typ-Konstruktoren fungieren, werden als *generische Typen* bezeichnet. Beispiel

from typing import Iterable

class Task:
    ...

def work(todo_list: Iterable[Task]) -> None:
    ...

Hier ist Iterable ein generischer Typ, der einen konkreten Typ Task nimmt und einen konkreten Typ Iterable[Task] zurückgibt.

Funktionen, die sich typen-generisch verhalten (wie im zweiten Beispiel), werden als *generische Funktionen* bezeichnet. Typ-Annotationen von generischen Funktionen werden durch *Typvariablen* ermöglicht. Ihre Semantik in Bezug auf generische Typen ist etwas ähnlich der Semantik von Parametern in Funktionen. Aber man weist Typvariablen keine konkreten Typen zu, es ist die Aufgabe eines statischen Typ-Checkers, ihre möglichen Werte zu finden und den Benutzer zu warnen, wenn er sie nicht finden kann. Beispiel

def take_first(seq: Sequence[T]) -> T: # a generic function
    return seq[0]

accumulator = 0 # type: int

accumulator += take_first([1, 2, 3])   # Safe, T deduced to be int
accumulator += take_first((2.7, 3.5))  # Unsafe

Typvariablen werden in Typ-Annotationen extensiv verwendet, auch die interne Maschinerie der Typ-Inferenz in Typ-Checkern basiert typischerweise auf Typvariablen. Daher betrachten wir sie im Detail.

Typvariablen

X = TypeVar('X') deklariert eine eindeutige Typvariable. Der Name muss mit dem Variablennamen übereinstimmen. Standardmäßig reicht eine Typvariable über alle möglichen Typen. Beispiel

def do_nothing(one_arg: T, other_arg: T) -> None:
    pass

do_nothing(1, 2)               # OK, T is int
do_nothing('abc', UserID(42))  # also OK, T is object

Y = TypeVar('Y', t1, t2, ...). Dito, eingeschränkt auf t1 usw. Verhält sich ähnlich wie Union[t1, t2, ...]. Eine eingeschränkte Typvariable reicht nur über die Einschränkungen t1 usw. *exakt*; Unterklassen der Einschränkungen werden durch die am weitesten abgeleitete Basisklasse unter t1 usw. ersetzt. Beispiele

  • Funktions-Typ-Annotation mit einer eingeschränkten Typvariable
    AnyStr = TypeVar('AnyStr', str, bytes)
    
    def longest(first: AnyStr, second: AnyStr) -> AnyStr:
        return first if len(first) >= len(second) else second
    
    result = longest('a', 'abc')  # The inferred type for result is str
    
    result = longest('a', b'abc')  # Fails static type check
    

    In diesem Beispiel müssen beide Argumente von longest() denselben Typ haben (str oder bytes), und darüber hinaus, selbst wenn die Argumente Instanzen einer gemeinsamen str-Unterklasse sind, ist der Rückgabetyp immer noch str, nicht diese Unterklasse (siehe nächstes Beispiel).

  • Zum Vergleich: Wenn die Typvariable uneingeschränkt wäre, würde die gemeinsame Unterklasse als Rückgabetyp gewählt, z.B.
    S = TypeVar('S')
    
    def longest(first: S, second: S) -> S:
        return first if len(first) >= len(second) else second
    
    class MyStr(str): ...
    
    result = longest(MyStr('a'), MyStr('abc'))
    

    Der abgeleitete Typ von result ist MyStr (während im AnyStr-Beispiel str wäre).

  • Zum Vergleich: Wenn eine Union verwendet wird, muss der Rückgabetyp ebenfalls eine Union sein
    U = Union[str, bytes]
    
    def longest(first: U, second: U) -> U:
        return first if len(first) >= len(second) else second
    
    result = longest('a', 'abc')
    

    Der abgeleitete Typ von result ist immer noch Union[str, bytes], auch wenn beide Argumente str sind.

    Beachten Sie, dass der Typ-Checker diese Funktion ablehnen wird

    def concat(first: U, second: U) -> U:
        return first + second  # Error: can't concatenate str and bytes
    

    Für solche Fälle, in denen Parameter ihre Typen nur gleichzeitig ändern könnten, sollte man eingeschränkte Typvariablen verwenden.

Definition und Verwendung generischer Typen

Benutzer können ihre Klassen als generische Typen mit dem speziellen Baustein Generic deklarieren. Die Definition class MyGeneric(Generic[X, Y, ...]): ... definiert einen generischen Typ MyGeneric über die Typvariablen X usw. MyGeneric selbst wird parametrisierbar, z.B. MyGeneric[int, str, ...] ist ein spezifischer Typ mit Substitutionen X -> int usw. Beispiel

class CustomQueue(Generic[T]):

    def put(self, task: T) -> None:
        ...
    def get(self) -> T:
        ...

def communicate(queue: CustomQueue[str]) -> Optional[str]:
    ...

Klassen, die von generischen Typen abgeleitet sind, werden generisch. Eine Klasse kann mehrere generische Typen unterklassifizieren. Klassen, die von spezifischen Typen abgeleitet sind, die von Generika zurückgegeben werden, sind jedoch nicht generisch. Beispiele

class TodoList(Iterable[T], Container[T]):
    def check(self, item: T) -> None:
        ...

def check_all(todo: TodoList[T]) -> None:  # TodoList is generic
    ...

class URLList(Iterable[bytes]):
    def scrape_all(self) -> None:
        ...

def search(urls: URLList) -> Optional[bytes]  # URLList is not generic
    ...

Die Unterklassifizierung eines generischen Typs erzwingt die Untertyp-Beziehung auf die entsprechenden spezifischen Typen, so dass TodoList[t1] im obigen Beispiel ein Untertyp von Iterable[t1] ist.

Generische Typen können in mehreren Schritten spezialisiert (indiziert) werden. Jede Typvariable kann durch einen konkreten Typ oder durch einen anderen generischen Typ ersetzt werden. Wenn Generic in der Basisklassenliste vorkommt, dann muss sie alle Typvariablen enthalten, und die Reihenfolge der Typ-Parameter wird durch die Reihenfolge bestimmt, in der sie in Generic vorkommen. Beispiele

Table = Dict[int, T]     # Table is generic
Messages = Table[bytes]  # Same as Dict[int, bytes]

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

class DerivedGeneric(BaseGeneric[int, T]): # DerivedGeneric has one parameter
    ...

SpecificType = DerivedGeneric[int]         # OK

class MyDictView(Generic[S, T, U], Iterable[Tuple[U, T]]):
    ...

Example = MyDictView[list, int, str]       # S -> list, T -> int, U -> str

Wenn ein generischer Typ in einer Typ-Annotation mit einer weggelassenen Typvariable vorkommt, wird angenommen, dass es sich um Any handelt. Diese Form kann als Fallback zur dynamischen Typisierung verwendet werden und ist für die Verwendung mit issubclass und isinstance erlaubt. Alle Typ-Informationen in Instanzen werden zur Laufzeit gelöscht. Beispiele

def count(seq: Sequence) -> int:      # Same as Sequence[Any]
    ...

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

class UserClass:
    ...

issubclass(UserClass, FrameworkBase)  # This is OK

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

IntNode = Node[int]
my_node = IntNode()  # at runtime my_node.__class__ is Node
                     # inferred static type of my_node is Node[int]

Kovarianz und Kontravarianz

Wenn t2 ein Untertyp von t1 ist, dann wird ein generischer Typ-Konstruktor GenType aufgerufen

  • Kovariant, wenn GenType[t2] ein Untertyp von GenType[t1] für alle solchen t1 und t2 ist.
  • Kontravariant, wenn GenType[t1] ein Untertyp von GenType[t2] für alle solchen t1 und t2 ist.
  • Invariant, wenn keine der obigen Bedingungen zutrifft.

Um diese Definition besser zu verstehen, machen wir eine Analogie zu gewöhnlichen Funktionen. Nehmen wir an, wir haben

def cov(x: float) -> float:
    return 2*x

def contra(x: float) -> float:
    return -x

def inv(x: float) -> float:
    return x*x

Wenn x1 < x2, dann gilt *immer* cov(x1) < cov(x2) und contra(x2) < contra(x1), während nichts über inv gesagt werden konnte. Ersetzen wir < durch ist-ein-Untertyp-von und Funktionen durch generische Typ-Konstruktoren, erhalten wir Beispiele für kovariantes, kontravariantes und invariantes Verhalten. Betrachten wir nun praktische Beispiele

  • Union verhält sich kovariant in all seinen Argumenten. Tatsächlich ist, wie oben diskutiert, Union[t1, t2, ...] ein Untertyp von Union[u1, u2, ...], wenn t1 ein Untertyp von u1 ist, usw.
  • FrozenSet[T] ist ebenfalls kovariant. Betrachten wir int und float anstelle von T. Erstens ist int ein Untertyp von float. Zweitens ist die Menge der Werte von FrozenSet[int] eindeutig eine Teilmenge der Werte von FrozenSet[float], während die Menge der Funktionen von FrozenSet[float] eine Teilmenge der Menge der Funktionen von FrozenSet[int] ist. Daher ist per Definition FrozenSet[int] ein Untertyp von FrozenSet[float].
  • List[T] ist invariant. Tatsächlich, obwohl die Menge der Werte von List[int] eine Teilmenge der Werte von List[float] ist, könnte nur int zu einer List[int] hinzugefügt werden, wie in Abschnitt „Hintergrund“ diskutiert. Daher ist List[int] keine Unterart von List[float]. Dies ist eine typische Situation mit veränderlichen Typen, sie sind typischerweise invariant.

Eines der besten Beispiele, um das (etwas kontraintuitive) kontravariante Verhalten zu illustrieren, ist der aufrufbare Typ. Er ist kovariant im Rückgabetyp, aber kontravariant in den Argumenten. Für zwei aufrufbare Typen, die sich nur im Rückgabetyp unterscheiden, folgt die Untertyp-Beziehung für die aufrufbaren Typen derjenigen der Rückgabetypen. Beispiele

  • Callable[[], int] ist eine Unterart von Callable[[], float].
  • Callable[[], Manager] ist eine Unterart von Callable[[], Employee].

Während für zwei aufrufbare Typen, die sich nur im Typ eines Arguments unterscheiden, die Untertyp-Beziehung für die aufrufbaren Typen *in die entgegengesetzte Richtung* geht wie für die Argumenttypen. Beispiele

  • Callable[[float], None] ist eine Unterart von Callable[[int], None].
  • Callable[[Employee], None] ist eine Unterart von Callable[[Manager], None].

Ja, Sie haben richtig gelesen. Tatsächlich, wenn eine Funktion erwartet wird, die das Gehalt eines Managers berechnen kann

def calculate_all(lst: List[Manager], salary: Callable[[Manager], Decimal]):
    ...

dann ist Callable[[Employee], Decimal], das ein Gehalt für jeden Mitarbeiter berechnen kann, ebenfalls akzeptabel.

Das Beispiel mit Callable zeigt, wie man präzisere Typannotationen für Funktionen erstellt: wählen Sie den allgemeinsten Typ für jedes Argument und den spezifischsten Typ für den Rückgabewert.

Es ist möglich, die Varianz für benutzerdefinierte generische Typen durch die Verwendung von speziellen Schlüsselwörtern covariant und contravariant in der Definition von Typvariablen, die als Parameter verwendet werden, zu *deklarieren*. Typen sind standardmäßig invariant. Beispiele

T = TypeVar('T')
T_co = TypeVar('T_co', covariant=True)
T_contra = TypeVar('T_contra', contravariant=True)

class LinkedList(Generic[T]):  # invariant by default
    ...
    def append(self, element: T) -> None:
        ...

class Box(Generic[T_co]):      #  this type is declared covariant
    def __init__(self, content: T_co) -> None:
        self._content = content
    def get_content(self) -> T_co:
        return self._content

class Sink(Generic[T_contra]): # this type is declared contravariant
    def send_to_nowhere(self, data: T_contra) -> None:
        with open(os.devnull, 'w') as devnull:
            print(data, file=devnull)

Beachten Sie, dass, obwohl die Varianz über Typvariablen definiert ist, sie keine Eigenschaft von Typvariablen ist, sondern eine Eigenschaft von generischen Typen. In komplexen Definitionen abgeleiteter Generika wird die Varianz *nur* aus den verwendeten Typvariablen bestimmt. Ein komplexes Beispiel

T_co = TypeVar('T_co', Employee, Manager, covariant=True)
T_contra = TypeVar('T_contra', Employee, Manager, contravariant=True)

class Base(Generic[T_contra]):
    ...

class Derived(Base[T_co]):
    ...

Ein Typ-Checker ermittelt aus der zweiten Deklaration, dass Derived[Manager] eine Unterart von Derived[Employee] ist und Derived[t1] eine Unterart von Base[t1] ist. Wenn wir die "ist-Untertyp-von"-Beziehung mit < bezeichnen, dann ist das vollständige Diagramm der Untertypen für diesen Fall

Base[Manager]    >  Base[Employee]
    v                   v
Derived[Manager] <  Derived[Employee]

so dass ein Typ-Checker auch findet, dass z.B. Derived[Manager] eine Unterart von Base[Employee] ist.

Weitere Informationen zu Typvariablen, generischen Typen und Varianz finden Sie in PEP 484, den mypy-Dokumenten zu Generics und Wikipedia.

Pragmatik

Einige Dinge sind für die Theorie irrelevant, machen aber die praktische Nutzung bequemer. (Dies ist keine vollständige Liste; ich habe wahrscheinlich ein paar Dinge übersehen und einige sind noch umstritten oder nicht vollständig spezifiziert.)

  • Wo ein Typ erwartet wird, kann None für type(None) substituiert werden; z.B. Union[t1, None] == Union[t1, type(None)].
  • Typ-Aliase, z.B.
    Point = Tuple[float, float]
    def distance(point: Point) -> float: ...
    
  • Vorwärtsreferenzen über Zeichenketten, z.B.
    class MyComparable:
        def compare(self, other: 'MyComparable') -> int: ...
    
  • Typvariablen können in unbeschränkter, beschränkter oder gebundener Form deklariert werden. Die Varianz eines generischen Typs kann auch mit einer mit speziellen Schlüsselwortargumenten deklarierten Typvariablen angegeben werden, wodurch spezielle Syntax vermieden wird, z.B.
    T = TypeVar('T', bound=complex)
    
    def add(x: T, y: T) -> T:
        return x + y
    
    T_co = TypeVar('T_co', covariant=True)
    
    class ImmutableList(Generic[T_co]): ...
    
  • Typdeklaration in Kommentaren, z.B.
    lst = []  # type: Sequence[int]
    
  • Casts mit cast(T, obj), z.B.
    zork = cast(Any, frobozz())
    
  • Weitere Dinge, z.B. Überladung und Stub-Module, siehe PEP 484.

Vordefinierte generische Typen und Protokolle in typing.py

(Siehe auch das typing.py-Modul.)

  • Alles aus collections.abc (aber Set umbenannt in AbstractSet).
  • Dict, List, Set, FrozenSet, ein paar mehr.
  • re.Pattern[AnyStr], re.Match[AnyStr].
  • io.IO[AnyStr], io.TextIO ~ io.IO[str], io.BinaryIO ~ io.IO[bytes].

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

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