PEP 585 – Type Hinting Generics In Standard Collections
- Autor:
- Łukasz Langa <lukasz at python.org>
- Discussions-To:
- Typing-SIG list
- Status:
- Final
- Typ:
- Standards Track
- Thema:
- Typisierung
- Erstellt:
- 03-März-2019
- Python-Version:
- 3.9
- Resolution:
- Python-Dev thread
Zusammenfassung
Statisches Typisieren, wie es durch die PEPs 484, 526, 544, 560 und 563 definiert wurde, wurde inkrementell auf der bestehenden Python-Laufzeit aufgebaut und durch bestehende Syntax und Laufzeitverhalten eingeschränkt. Dies führte zur Existenz einer duplizierten Sammlungshierarchie im typing-Modul aufgrund von Generika (zum Beispiel typing.List und die eingebaute list).
Diese PEP schlägt vor, die Unterstützung für die Syntax von Generika in allen Standard-Sammlungen zu aktivieren, die derzeit im typing-Modul verfügbar sind.
Begründung und Ziele
Diese Änderung entfernt die Notwendigkeit einer parallelen Typenhierarchie im typing-Modul, was es für Benutzer einfacher macht, ihre Programme zu annotieren, und es für Lehrer einfacher macht, Python zu lehren.
Terminologie
Generisch (Adj.) – ein Typ, der parametrisiert werden kann, typischerweise ein Container. Auch bekannt als parametrisierter Typ oder generischer Typ. Beispiel: dict.
parametrisierte Generika – eine spezifische Instanz eines Generikums mit den erwarteten Typen für Elementen des Containers. Auch bekannt als parametrisierter Typ. Beispiel: dict[str, int].
Abwärtskompatibilität
Werkzeuge, einschließlich Typprüfern und Lintern, müssen angepasst werden, um Standard-Sammlungen als Generika zu erkennen.
Auf Quellcode-Ebene erfordert die neu beschriebene Funktionalität Python 3.9. Für Anwendungsfälle, die auf Typ-Annotationen beschränkt sind, können Python-Dateien mit dem "annotations"-Future-Import (verfügbar seit Python 3.7) Standard-Sammlungen, einschließlich Builtins, parametrisieren. Zur Wiederholung, dies hängt davon ab, dass externe Werkzeuge verstehen, dass dies gültig ist.
Implementierung
Ab Python 3.7 können mit from __future__ import annotations Funktions- und Variablenannotationen Standard-Sammlungen direkt parametrisieren. Beispiel
from __future__ import annotations
def find(haystack: dict[str, list[int]]) -> int:
...
Der Nutzen dieser Syntax vor PEP 585 ist begrenzt, da externe Werkzeuge wie Mypy Standard-Sammlungen nicht als generisch erkennen. Darüber hinaus erfordern bestimmte Funktionen von `typing`, wie Typ-Aliase oder Casting, das Platzieren von Typen außerhalb von Annotationen, im Laufzeitkontext. Obwohl diese relativ weniger verbreitet sind als Typ-Annotationen, ist es wichtig, die gleiche Typ-Syntax in allen Kontexten zu erlauben. Deshalb werden ab Python 3.9 die folgenden Sammlungen generisch, unter Verwendung von __class_getitem__() zur Parametrisierung von enthaltenen Typen
tuple# typing.Tuplelist# typing.Listdict# typing.Dictset# typing.Setfrozenset# typing.FrozenSettype# typing.Typecollections.dequecollections.defaultdictcollections.OrderedDictcollections.Countercollections.ChainMapcollections.abc.Awaitablecollections.abc.Coroutinecollections.abc.AsyncIterablecollections.abc.AsyncIteratorcollections.abc.AsyncGeneratorcollections.abc.Iterablecollections.abc.Iteratorcollections.abc.Generatorcollections.abc.Reversiblecollections.abc.Containercollections.abc.Collectioncollections.abc.Callablecollections.abc.Set# typing.AbstractSetcollections.abc.MutableSetcollections.abc.Mappingcollections.abc.MutableMappingcollections.abc.Sequencecollections.abc.MutableSequencecollections.abc.ByteStringcollections.abc.MappingViewcollections.abc.KeysViewcollections.abc.ItemsViewcollections.abc.ValuesViewcontextlib.AbstractContextManager# typing.ContextManagercontextlib.AbstractAsyncContextManager# typing.AsyncContextManagerre.Pattern# typing.Pattern, typing.re.Patternre.Match# typing.Match, typing.re.Match
Der Import dieser aus typing ist veraltet. Aufgrund von PEP 563 und der Absicht, die Laufzeitwirkung von Typisierungen zu minimieren, wird diese Veraltung keine DeprecationWarnings erzeugen. Stattdessen können Typprüfer vor einer solchen veralteten Verwendung warnen, wenn die Zielversion des geprüften Programms als Python 3.9 oder neuer signalisiert ist. Es wird empfohlen, diese Warnungen projektweit zu unterdrücken.
Die veraltete Funktionalität kann schließlich aus dem typing-Modul entfernt werden. Die Entfernung wird nicht vor dem Ende der Lebensdauer von Python 3.9, geplant für Oktober 2025, erfolgen.
Parameter für Generika sind zur Laufzeit verfügbar
Die Beibehaltung des generischen Typs zur Laufzeit ermöglicht die Introspektion des Typs, die für die API-Generierung oder zur Laufzeit-Typprüfung verwendet werden kann. Eine solche Verwendung ist bereits im Umlauf.
Genau wie beim typing-Modul heute behalten die parametrisierten generischen Typen, die in der vorherigen Sektion aufgeführt sind, ihre Typ-Parameter zur Laufzeit bei.
>>> list[str]
list[str]
>>> tuple[int, ...]
tuple[int, ...]
>>> ChainMap[str, list[str]]
collections.ChainMap[str, list[str]]
Dies wird mit einem dünnen Proxy-Typ implementiert, der alle Methodenaufrufe und Attributzugriffe an den reinen Ursprungstyp weiterleitet, mit folgenden Ausnahmen:
- die
__repr__zeigt den parametrisierten Typ an; - das Attribut
__origin__zeigt auf die nicht-parametrisierte generische Klasse; - das Attribut
__args__ist ein Tupel (möglicherweise der Länge 1) von generischen Typen, die an den ursprünglichen__class_getitem__übergeben wurden; - das Attribut
__parameters__ist ein verzögert berechnetes Tupel (möglicherweise leer) von eindeutigen Typvariablen, die in__args__gefunden wurden; - die
__getitem__löst eine Ausnahme aus, um Fehler wiedict[str][str]zu verhindern. Sie erlaubt jedoch z. B.dict[str, T][int]und gibt in diesem Falldict[str, int]zurück.
Dieses Design bedeutet, dass es möglich ist, Instanzen von parametrisierten Sammlungen zu erstellen, wie z. B.
>>> l = list[str]()
[]
>>> list is list[str]
False
>>> list == list[str]
False
>>> list[str] == list[str]
True
>>> list[str] == list[int]
False
>>> isinstance([1, 2, 3], list[str])
TypeError: isinstance() arg 2 cannot be a parameterized generic
>>> issubclass(list, list[str])
TypeError: issubclass() arg 2 cannot be a parameterized generic
>>> isinstance(list[str], types.GenericAlias)
True
Objekte, die mit reinen Typen und parametrisierten Typen erstellt wurden, sind exakt gleich. Die generischen Parameter werden nicht in Instanzen beibehalten, die mit parametrisierten Typen erstellt wurden. Mit anderen Worten, generische Typen löschen Typ-Parameter während der Objekterstellung.
Eine wichtige Konsequenz davon ist, dass der Interpreter keine Versuche unternimmt, Operationen auf der mit einem parametrisierten Typ erstellten Sammlung zu typisieren. Dies bietet Symmetrie zwischen
l: list[str] = []
und
l = list[str]()
Für den Zugriff auf den Proxy-Typ aus Python-Code wird dieser aus dem types-Modul als GenericAlias exportiert.
Das Pickling oder (flache oder tiefe) Kopieren einer GenericAlias-Instanz bewahrt den Typ, den Ursprung, die Attribute und die Parameter.
Vorwärtskompatibilität
Zukünftige Standard-Sammlungen müssen das gleiche Verhalten implementieren.
Referenzimplementierung
Ein Proof-of-Concept oder Prototyp Implementierung existiert.
Abgelehnte Alternativen
Nichts tun
Das Beibehalten des Status quo zwingt Python-Programmierer, Importe aus dem typing-Modul für Standard-Sammlungen zu verwalten, was alle bis auf die einfachsten Annotationen umständlich zu warten macht. Die Existenz paralleler Typen verwirrt Neulinge (warum gibt es sowohl list als auch List?).
Die oben genannten Probleme existieren auch nicht bei benutzerdefinierten generischen Klassen, die Laufzeitfunktionalität und die Möglichkeit, sie als generische Typ-Annotationen zu verwenden, teilen. Die erschwerte Nutzung von Standard-Sammlungen in der Typ-Annotation von Benutzerklassen behinderte die Akzeptanz und Benutzerfreundlichkeit von Typisierungen.
Generische Löschung
Es wäre einfacher, __class_getitem__ auf den aufgelisteten Standard-Sammlungen so zu implementieren, dass der generische Typ nicht beibehalten wird, anders ausgedrückt
>>> list[str]
<class 'list'>
>>> tuple[int, ...]
<class 'tuple'>
>>> collections.ChainMap[str, list[str]]
<class 'collections.ChainMap'>
Dies ist problematisch, da es die Abwärtskompatibilität bricht: aktuelle Äquivalente dieser Typen im typing-Modul behalten den generischen Typ bei.
>>> from typing import List, Tuple, ChainMap
>>> List[str]
typing.List[str]
>>> Tuple[int, ...]
typing.Tuple[int, ...]
>>> ChainMap[str, List[str]]
typing.ChainMap[str, typing.List[str]]
Wie im Abschnitt "Implementierung" erwähnt, ermöglicht die Beibehaltung des generischen Typs zur Laufzeit die Introspektion des Typs, die für die API-Generierung oder zur Laufzeit-Typprüfung verwendet werden kann. Eine solche Verwendung ist bereits im Umlauf.
Zusätzlich würde die Implementierung von Subskripten als Identitätsfunktionen Python für Anfänger weniger freundlich machen. Wenn ein Benutzer beispielsweise versehentlich einen Listentyp anstelle eines Listenobjekts an eine Funktion übergibt und diese Funktion das empfangene Objekt indiziert, würde der Code keinen Fehler mehr auslösen.
Heute
>>> l = list
>>> l[-1]
TypeError: 'type' object is not subscriptable
Mit __class_getitem__ als Identitätsfunktion
>>> l = list
>>> l[-1]
list
Das erfolgreiche Indizieren würde hier wahrscheinlich zu einem Fehler in einiger Entfernung führen, was den Benutzer verwirren würde.
Instanziierung von parametrisierten Typen verbieten
Da der Proxy-Typ, der __origin__ und __args__ beibehält, größtenteils für Laufzeit-Introspektionszwecke nützlich ist, hätten wir die Instanziierung von parametrisierten Typen verbieten können.
Tatsächlich verbietet das typing-Modul heute die Instanziierung von Typen, die eingebaute Sammlungen parallelisieren (die Instanziierung anderer parametrisierter Typen ist erlaubt).
Der ursprüngliche Grund für diese Entscheidung war, irreführende Parametrisierungen abzuschrecken, die die Objekterstellung um bis zu zwei Größenordnungen langsamer machten als die spezielle Syntax, die für diese eingebauten Sammlungen verfügbar ist.
Diese Begründung ist nicht stark genug, um die Ausnahmebehandlung von Builtins zu erlauben. Alle anderen parametrisierten Typen können instanziiert werden, einschließlich der Parallelen von Sammlungen in der Standardbibliothek. Darüber hinaus erlaubt Python die Instanziierung von Listen mit list() und einige eingebaute Sammlungen bieten keine spezielle Syntax für die Instanziierung.
Machen Sie isinstance(obj, list[str]) zu einer Prüfung, die Generika ignoriert
Eine frühere Version dieser PEP schlug vor, parametrisierte Generika wie list[str] für die Zwecke von isinstance() und issubclass() als äquivalent zu ihren nicht-parametrisierten Varianten wie list zu behandeln. Dies wäre symmetrisch dazu, wie list[str]() eine reguläre Liste erstellt.
Dieses Design wurde abgelehnt, da isinstance() und issubclass() Prüfungen mit parametrisierten Generika wie elementweise Laufzeit-Typprüfungen gelesen würden. Das Ergebnis dieser Prüfungen wäre überraschend, zum Beispiel
>>> isinstance([1, 2, 3], list[str])
True
Beachten Sie, dass das Objekt nicht dem angegebenen generischen Typ entspricht, aber isinstance() immer noch True zurückgibt, da es nur prüft, ob das Objekt eine Liste ist.
Wenn eine Bibliothek mit einem parametrisierten Generikum konfrontiert wird und eine isinstance()-Prüfung mit dem Basistyp durchführen möchte, kann dieser Typ über das Attribut __origin__ des parametrisierten Generikums abgerufen werden.
Machen Sie isinstance(obj, list[str]) zu einer Laufzeit-Typprüfung
Diese Funktionalität erfordert das Iterieren über die Sammlung, was bei einigen davon eine destruktive Operation ist. Diese Funktionalität wäre nützlich gewesen, aber die Implementierung des Typprüfers innerhalb von Python, der mit komplexen Typen, verschachtelter Typprüfung, Typvariablen, Zeichenketten-Vorwärtsreferenzen usw. umgehen würde, liegt außerhalb des Rahmens dieser PEP.
Nennen Sie den Typ GenericType statt GenericAlias
Wir haben einen anderen Namen für diesen Typ in Betracht gezogen, aber entschieden, dass GenericAlias besser ist – dies sind keine echten Typen, sie sind Aliase für den entsprechenden Containertyp mit einigen zusätzlichen Metadaten.
Hinweis zum ersten Entwurf
Eine frühe Version dieser PEP diskutierte Angelegenheiten über Generika in Standard-Sammlungen hinaus. Diese nicht zusammenhängenden Themen wurden zur Klarheit entfernt.
Danksagungen
Vielen Dank an Guido van Rossum für seine Arbeit an Python und insbesondere für die Implementierung dieser PEP.
Urheberrecht
Dieses Dokument wird in die Public Domain oder unter die CC0-1.0-Universal-Lizenz gestellt, je nachdem, welche Lizenz permissiver ist.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0585.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT