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:
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,t2usw. undu1,u2usw. sind Typen. Manchmal schreiben wirtiodertj, um uns auf „irgendeinen vont1,t2usw.“ zu beziehen.T,Uusw. sind Typvariablen (definiert mitTypeVar(), 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
TrueundFalseden Typbool. - Durch Angabe von Funktionen, die mit Variablen eines Typs verwendet werden können. Z.B. bilden alle Objekte, die eine
__len__-Methode haben, den TypSized. Sowohl[1, 2, 3]als auch'abc'gehören zu diesem Typ, da manlendarauf anwenden kannlen([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
FancyListals alle Listen definieren, die nur Instanzen vonint,stroder 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_typeist auch in der Menge der Werte vonfirst_typeenthalten; und - Jede Funktion aus
first_typeist auch in der Menge der Funktionen vonsecond_typeenthalten.
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
t1ist konsistent mit einem Typt2, wennt1ein Untertyp vont2ist. (Aber nicht umgekehrt.) Anyist konsistent mit jedem Typ. (AberAnyist kein Untertyp von jedem Typ.)- Jeder Typ ist konsistent mit
Any. (Aber jeder Typ ist kein Untertyp vonAny.)
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
intist eine Klasse und ein Typ.UserIDist eine Klasse und ein Typ.Union[str, int]ist ein Typ, aber keine richtige Klasseclass 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,Unionusw.) können instanziiert werden, ein Versuch dazu wirdTypeErrorauslösen. (Aber nicht-abstrakte Unterklassen vonGenerickönnen es.) - Keine der unten definierten Typen können Unterklassen bilden, außer
Genericund davon abgeleiteten Klassen. - All dies wird
TypeErrorauslösen, wenn sie inisinstanceoderissubclassvorkommen (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
t1usw. sind, sind Untertypen davon.- Vereinigungen, deren Komponenten Untertypen von
t1usw. sind, sind Untertypen davon. Beispiel:Union[int, str]ist ein Untertyp vonUnion[int, float, str]. - Die Reihenfolge der Argumente spielt keine Rolle. Beispiel:
Union[int, str] == Union[str, int]. - Wenn
tiselbst eineUnionist, wird das Ergebnis abgeflacht. Beispiel:Union[int, Union[float, str]] == Union[int, float, str]. - Wenn
tiundtjeine Untertyp-Beziehung haben, überlebt der weniger spezifische Typ. Beispiel:Union[Employee, Manager] == Union[Employee]. Union[t1]gibt nurt1zurück.Union[]ist illegal, ebensoUnion[()]- Korollar:
Union[..., object, ...]gibtobjectzurück.
- Vereinigungen, deren Komponenten Untertypen von
- Optional[t1]. Alias für
Union[t1, None], d.h.Union[t1, type(None)]. - Tuple[t1, t2, …, tn]. Ein Tupel, dessen Elemente Instanzen von
t1usw. sind. Beispiel:Tuple[int, float]bedeutet ein Tupel aus zwei Elementen, das erste ist einint, das zweite ist einfloat; z.B.(42, 3.14).Tuple[u1, u2, ..., um]ist ein Untertyp vonTuple[t1, t2, ..., tn], wenn sie die gleiche Längen==mhaben und jedesuiein Untertyp vontiist.- 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
t1usw. und Rückgabetyptr. Die Argumentliste kann leer seinn==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 SieCallable[..., tr]schreiben (wiederum ein literaler Ellipsen-Operator).
Wir könnten hinzufügen
- Intersection[t1, t2, …]. Typen, die Untertypen von *jedem* der
t1usw. sind, sind Untertypen davon. (Vergleichen Sie mitUnion, 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 vonIntersection[int, float, str]. - Ein Schnitt aus einem Argument ist nur dieses Argument, z.B.
Intersection[int]istint. - Wenn Argumente eine Untertyp-Beziehung haben, überlebt der spezifischere Typ, z.B. ist
Intersection[str, Employee, Manager]Intersection[str, Manager]. Intersection[]ist illegal, ebensoIntersection[()].- Korollar:
Anyverschwindet aus der Argumentliste, z.B.Intersection[int, str, Any] == Intersection[int, str].Intersection[Any, object]istobject. - Die Interaktion zwischen
IntersectionundUnionist 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).
- Die Reihenfolge der Argumente spielt keine Rolle. Verschachtelte Schnitte werden abgeflacht, z.B.
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
listoderdictenthalten 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
intnehmen und einintzurückgeben, oder zwei Argumente vom Typfloatnehmen und einfloatzurü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 (stroderbytes), und darüber hinaus, selbst wenn die Argumente Instanzen einer gemeinsamenstr-Unterklasse sind, ist der Rückgabetyp immer nochstr, 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
resultistMyStr(während imAnyStr-Beispielstrwäre). - Zum Vergleich: Wenn eine
Unionverwendet wird, muss der Rückgabetyp ebenfalls eineUnionseinU = 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
resultist immer nochUnion[str, bytes], auch wenn beide Argumentestrsind.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 vonGenType[t1]für alle solchent1undt2ist. - Kontravariant, wenn
GenType[t1]ein Untertyp vonGenType[t2]für alle solchent1undt2ist. - 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
Unionverhält sich kovariant in all seinen Argumenten. Tatsächlich ist, wie oben diskutiert,Union[t1, t2, ...]ein Untertyp vonUnion[u1, u2, ...], wennt1ein Untertyp vonu1ist, usw.FrozenSet[T]ist ebenfalls kovariant. Betrachten wirintundfloatanstelle vonT. Erstens istintein Untertyp vonfloat. Zweitens ist die Menge der Werte vonFrozenSet[int]eindeutig eine Teilmenge der Werte vonFrozenSet[float], während die Menge der Funktionen vonFrozenSet[float]eine Teilmenge der Menge der Funktionen vonFrozenSet[int]ist. Daher ist per DefinitionFrozenSet[int]ein Untertyp vonFrozenSet[float].List[T]ist invariant. Tatsächlich, obwohl die Menge der Werte vonList[int]eine Teilmenge der Werte vonList[float]ist, könnte nurintzu einerList[int]hinzugefügt werden, wie in Abschnitt „Hintergrund“ diskutiert. Daher istList[int]keine Unterart vonList[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 vonCallable[[], float].Callable[[], Manager]ist eine Unterart vonCallable[[], 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 vonCallable[[int], None].Callable[[Employee], None]ist eine Unterart vonCallable[[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
Nonefürtype(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(aberSetumbenannt inAbstractSet). 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].
Urheberrecht
Dieses Dokument ist unter der Open Publication License lizenziert.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0483.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT