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

Python Enhancement Proposals

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

Inhaltsverzeichnis

Wichtig

Diese PEP ist ein historisches Dokument. Die aktuelle, kanonische Dokumentation finden Sie nun unter Generic Alias Type und die Dokumentation für __class_getitem__().

×

Siehe PEP 1, um Änderungen vorzuschlagen.

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.Tuple
  • list # typing.List
  • dict # typing.Dict
  • set # typing.Set
  • frozenset # typing.FrozenSet
  • type # typing.Type
  • collections.deque
  • collections.defaultdict
  • collections.OrderedDict
  • collections.Counter
  • collections.ChainMap
  • collections.abc.Awaitable
  • collections.abc.Coroutine
  • collections.abc.AsyncIterable
  • collections.abc.AsyncIterator
  • collections.abc.AsyncGenerator
  • collections.abc.Iterable
  • collections.abc.Iterator
  • collections.abc.Generator
  • collections.abc.Reversible
  • collections.abc.Container
  • collections.abc.Collection
  • collections.abc.Callable
  • collections.abc.Set # typing.AbstractSet
  • collections.abc.MutableSet
  • collections.abc.Mapping
  • collections.abc.MutableMapping
  • collections.abc.Sequence
  • collections.abc.MutableSequence
  • collections.abc.ByteString
  • collections.abc.MappingView
  • collections.abc.KeysView
  • collections.abc.ItemsView
  • collections.abc.ValuesView
  • contextlib.AbstractContextManager # typing.ContextManager
  • contextlib.AbstractAsyncContextManager # typing.AsyncContextManager
  • re.Pattern # typing.Pattern, typing.re.Pattern
  • re.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 wie dict[str][str] zu verhindern. Sie erlaubt jedoch z. B. dict[str, T][int] und gibt in diesem Fall dict[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.


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

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