PEP 655 – Kennzeichnung einzelner TypedDict-Elemente als erforderlich oder potenziell fehlend
- Autor:
- David Foster <david at dafoster.net>
- Sponsor:
- Guido van Rossum <guido at python.org>
- Discussions-To:
- Typing-SIG-Thread
- Status:
- Final
- Typ:
- Standards Track
- Thema:
- Typisierung
- Erstellt:
- 30. Jan 2021
- Python-Version:
- 3.11
- Post-History:
- 31. Jan 2021, 11. Feb 2021, 20. Feb 2021, 26. Feb 2021, 17. Jan 2022, 28. Jan 2022
- Resolution:
- Python-Dev Nachricht
Inhaltsverzeichnis
- Zusammenfassung
- Motivation
- Begründung
- Spezifikation
- Abwärtskompatibilität
- Wie man das lehrt
- Referenzimplementierung
- Abgelehnte Ideen
- Spezielle Syntax um den Schlüssel eines TypedDict-Elements
- Kennzeichnung erforderlicher oder potenziell fehlender Schlüssel mit einem Operator
- Kennzeichnung des Fehlens eines Wertes mit einer speziellen Konstante
- Optional durch Nullable ersetzen. Optional umfunktionieren, um "optionales Element" zu bedeuten.
- Optional ändern, um in bestimmten Kontexten "optionales Element" anstelle von "nullable" zu bedeuten
- Verschiedene Synonyme für "potenziell fehlendes Element"
- Referenzen
- Urheberrecht
Zusammenfassung
PEP 589 definiert eine Notation zur Deklaration eines TypedDict mit allen erforderlichen Schlüsseln und eine Notation zur Definition eines TypedDicts mit allen potenziell fehlenden Schlüsseln. Es gibt jedoch keinen Mechanismus, um einige Schlüssel als erforderlich und andere als potenziell fehlend zu deklarieren. Dieses PEP führt zwei neue Notationen ein: Required[], die auf einzelnen Elementen eines TypedDict verwendet werden kann, um sie als erforderlich zu kennzeichnen, und NotRequired[], die auf einzelnen Elementen verwendet werden kann, um sie als potenziell fehlend zu kennzeichnen.
Dieses PEP macht keine Änderungen an der Python-Grammatik. Die korrekte Verwendung von erforderlichen und potenziell fehlenden Schlüsseln von TypedDicts soll nur von statischen Typ-Checkern erzwungen werden und muss nicht von Python selbst zur Laufzeit erzwungen werden.
Motivation
Es ist nicht ungewöhnlich, ein TypedDict mit einigen erforderlichen und anderen potenziell fehlenden Schlüsseln definieren zu wollen. Derzeit ist die einzige Möglichkeit, ein solches TypedDict zu definieren, ein TypedDict mit einem Wert für total zu deklarieren und es dann von einem anderen TypedDict mit einem anderen Wert für total zu erben.
class _MovieBase(TypedDict): # implicitly total=True
title: str
class Movie(_MovieBase, total=False):
year: int
Zwei verschiedene TypedDict-Typen für diesen Zweck deklarieren zu müssen, ist umständlich.
Dieses PEP führt zwei neue Typqualifizierer ein, typing.Required und typing.NotRequired, die die Definition eines einzigen TypedDicts mit einer Mischung aus erforderlichen und potenziell fehlenden Schlüsseln ermöglichen.
class Movie(TypedDict):
title: str
year: NotRequired[int]
Dieses PEP ermöglicht auch die Definition von TypedDicts in der alternativen funktionalen Syntax mit einer Mischung aus erforderlichen und potenziell fehlenden Schlüsseln, was derzeit überhaupt nicht möglich ist, da die alternative Syntax keine Vererbung unterstützt.
Actor = TypedDict('Actor', {
'name': str,
# "in" is a keyword, so the functional syntax is necessary
'in': NotRequired[List[str]],
})
Begründung
Man mag es ungewöhnlich finden, eine Notation vorzuschlagen, die die Kennzeichnung von erforderlichen Schlüsseln anstelle von potenziell fehlenden Schlüsseln priorisiert, wie es in anderen Sprachen wie TypeScript üblich ist.
interface Movie {
title: string;
year?: number; // ? marks potentially-missing keys
}
Die Schwierigkeit besteht darin, dass das beste Wort zur Kennzeichnung eines potenziell fehlenden Schlüssels, Optional[], bereits in Python für einen völlig anderen Zweck verwendet wird: zur Kennzeichnung von Werten, die entweder vom angegebenen Typ oder None sein können. Insbesondere funktioniert Folgendes nicht:
class Movie(TypedDict):
...
year: Optional[int] # means int|None, not potentially-missing!
Der Versuch, ein beliebiges Synonym von "optional" zur Kennzeichnung potenziell fehlender Schlüssel zu verwenden (wie Missing[]), wäre Optional[] zu ähnlich und leicht mit ihm zu verwechseln.
Daher wurde beschlossen, sich stattdessen auf die positive Formulierung für erforderliche Schlüssel zu konzentrieren, die einfach als Required[] geschrieben werden kann.
Dennoch ist es üblich, dass Personen, die ein reguläres (total=True) TypedDict erweitern möchten, nur eine kleine Anzahl von potenziell fehlenden Schlüsseln hinzufügen wollen, was eine Möglichkeit erfordert, Schlüssel zu kennzeichnen, die nicht erforderlich und potenziell fehlend sind. Daher erlauben wir für diesen Fall auch die Form NotRequired[].
Spezifikation
Der Typqualifizierer typing.Required wird verwendet, um anzugeben, dass eine in einer TypedDict-Definition deklarierte Variable ein erforderlicher Schlüssel ist.
class Movie(TypedDict, total=False):
title: Required[str]
year: int
Zusätzlich wird der Typqualifizierer typing.NotRequired verwendet, um anzugeben, dass eine in einer TypedDict-Definition deklarierte Variable ein potenziell fehlender Schlüssel ist.
class Movie(TypedDict): # implicitly total=True
title: str
year: NotRequired[int]
Es ist ein Fehler, Required[] oder NotRequired[] an einer anderen Stelle als bei einem Element eines TypedDict zu verwenden. Typ-Checker müssen diese Einschränkung erzwingen.
Es ist gültig, Required[] und NotRequired[] auch für Elemente zu verwenden, bei denen dies redundant ist, um bei Bedarf zusätzliche Klarheit zu schaffen.
class Movie(TypedDict):
title: Required[str] # redundant
year: NotRequired[int]
Es ist ein Fehler, sowohl Required[] als auch NotRequired[] gleichzeitig zu verwenden.
class Movie(TypedDict):
title: str
year: NotRequired[Required[int]] # ERROR
Typ-Checker müssen diese Einschränkung erzwingen. Die Laufzeitimplementierungen von Required[] und NotRequired[] können diese Einschränkung ebenfalls erzwingen.
Die alternative funktionale Syntax für TypedDict unterstützt ebenfalls Required[] und NotRequired[].
Movie = TypedDict('Movie', {'name': str, 'year': NotRequired[int]})
Interaktion mit total=False
Jedes PEP 589-konforme TypedDict, das mit total=False deklariert wurde, ist äquivalent zu einem TypedDict mit einer impliziten total=True-Definition, bei der alle seine Schlüssel als NotRequired[] markiert sind.
Daher
class _MovieBase(TypedDict): # implicitly total=True
title: str
class Movie(_MovieBase, total=False):
year: int
ist äquivalent zu
class _MovieBase(TypedDict):
title: str
class Movie(_MovieBase):
year: NotRequired[int]
Interaktion mit Annotated[]
Required[] und NotRequired[] können mit Annotated[] in jeder verschachtelten Reihenfolge verwendet werden.
class Movie(TypedDict):
title: str
year: NotRequired[Annotated[int, ValueRange(-9999, 9999)]] # ok
class Movie(TypedDict):
title: str
year: Annotated[NotRequired[int], ValueRange(-9999, 9999)] # ok
Insbesondere ermöglicht die Tatsache, dass Annotated[] die äußerste Annotation für ein Element sein kann, eine bessere Interoperabilität mit nicht-typing-bezogenen Verwendungen von Annotationen, die Annotated[] immer als äußerste Annotation haben möchten. [3]
Laufzeitverhalten
Interaktion mit get_type_hints()
typing.get_type_hints(...), das auf ein TypedDict angewendet wird, entfernt standardmäßig alle Typqualifizierer Required[] oder NotRequired[], da diese Qualifizierer für Code, der Typannotationen beiläufig introspektiert, als unpraktisch erachtet werden.
typing.get_type_hints(..., include_extras=True) behält jedoch die Typqualifizierer Required[] und NotRequired[] bei, für fortgeschrittenen Code, der Typannotationen introspektiert und alle Annotationen im ursprünglichen Quellcode beibehalten möchte.
class Movie(TypedDict):
title: str
year: NotRequired[int]
assert get_type_hints(Movie) == \
{'title': str, 'year': int}
assert get_type_hints(Movie, include_extras=True) == \
{'title': str, 'year': NotRequired[int]}
Interaktion mit get_origin() und get_args()
typing.get_origin() und typing.get_args() werden aktualisiert, um Required[] und NotRequired[] zu erkennen.
assert get_origin(Required[int]) is Required
assert get_args(Required[int]) == (int,)
assert get_origin(NotRequired[int]) is NotRequired
assert get_args(NotRequired[int]) == (int,)
Interaktion mit __required_keys__ und __optional_keys__
Ein mit Required[] markiertes Element erscheint immer in __required_keys__ für sein umschließendes TypedDict. Ebenso erscheint ein mit NotRequired[] markiertes Element immer in __optional_keys__.
assert Movie.__required_keys__ == frozenset({'title'})
assert Movie.__optional_keys__ == frozenset({'year'})
Abwärtskompatibilität
Dieses PEP führt keine abwärtsinkompatiblen Änderungen ein.
Wie man das lehrt
Um ein TypedDict zu definieren, bei dem die meisten Schlüssel erforderlich und einige potenziell fehlend sind, definieren Sie ein einzelnes TypedDict wie gewohnt (ohne das Schlüsselwort total) und markieren Sie die wenigen potenziell fehlenden Schlüssel mit NotRequired[].
Um ein TypedDict zu definieren, bei dem die meisten Schlüssel potenziell fehlend und einige erforderlich sind, definieren Sie ein total=False TypedDict und markieren Sie die wenigen erforderlichen Schlüssel mit Required[].
Wenn einige Elemente zusätzlich zu einem regulären Wert auch None akzeptieren, wird empfohlen, die TYP|None-Notation gegenüber Optional[TYP] zur Kennzeichnung solcher Elementwerte zu bevorzugen, um die Verwendung von Required[] oder NotRequired[] zusammen mit Optional[] innerhalb derselben TypedDict-Definition zu vermeiden.
Ja
from __future__ import annotations # for Python 3.7-3.9
class Dog(TypedDict):
name: str
owner: NotRequired[str|None]
Okay (erforderlich für Python 3.5.3-3.6)
class Dog(TypedDict):
name: str
owner: 'NotRequired[str|None]'
Nein
class Dog(TypedDict):
name: str
# ick; avoid using both Optional and NotRequired
owner: NotRequired[Optional[str]]
Verwendung in Python <3.11
Wenn Ihr Code Python <3.11 unterstützt und Required[] oder NotRequired[] verwenden möchte, sollte er typing_extensions.TypedDict anstelle von typing.TypedDict verwenden, da letzteres (Not)Required[] nicht verstehen wird. Insbesondere __required_keys__ und __optional_keys__ des resultierenden TypedDict-Typs sind dann nicht korrekt.
Ja (nur Python 3.11+)
from typing import NotRequired, TypedDict
class Dog(TypedDict):
name: str
owner: NotRequired[str|None]
Ja (Python <3.11 und 3.11+)
from __future__ import annotations # for Python 3.7-3.9
from typing_extensions import NotRequired, TypedDict # for Python <3.11 with (Not)Required
class Dog(TypedDict):
name: str
owner: NotRequired[str|None]
Nein (Python <3.11 und 3.11+)
from typing import TypedDict # oops: should import from typing_extensions instead
from typing_extensions import NotRequired
class Movie(TypedDict):
title: str
year: NotRequired[int]
assert Movie.__required_keys__ == frozenset({'title', 'year'}) # yikes
assert Movie.__optional_keys__ == frozenset() # yikes
Referenzimplementierung
Die Typ-Checker mypy 0.930, pyright 1.1.117 und pyanalyze 0.4.0 unterstützen Required und NotRequired.
Eine Referenzimplementierung der Laufzeitkomponente ist im Modul typing_extensions verfügbar.
Abgelehnte Ideen
Spezielle Syntax um den Schlüssel eines TypedDict-Elements
class MyThing(TypedDict):
opt1?: str # may not exist, but if exists, value is string
opt2: Optional[str] # always exists, but may have None value
Diese Notation würde Änderungen an der Python-Grammatik erfordern, und es wird nicht angenommen, dass die Kennzeichnung von TypedDict-Elementen als erforderlich oder potenziell fehlend die hohe Hürde für solche Grammatikänderungen erreichen würde.
class MyThing(TypedDict):
Optional[opt1]: str # may not exist, but if exists, value is string
opt2: Optional[str] # always exists, but may have None value
Diese Notation führt dazu, dass Optional[] je nach Position unterschiedliche Bedeutungen hat, was inkonsistent und verwirrend ist.
Außerdem: „Lassen wir einfach keine seltsame Syntax vor dem Doppelpunkt stehen.“ [1]
Kennzeichnung erforderlicher oder potenziell fehlender Schlüssel mit einem Operator
Wir könnten den unären + als Kurzform zur Kennzeichnung eines erforderlichen Schlüssels, den unären - zur Kennzeichnung eines potenziell fehlenden Schlüssels oder den unären ~ zur Kennzeichnung eines Schlüssels mit entgegengesetzter Totalität verwenden.
class MyThing(TypedDict, total=False):
req1: +int # + means a required key, or Required[]
opt1: str
req2: +float
class MyThing(TypedDict):
req1: int
opt1: -str # - means a potentially-missing key, or NotRequired[]
req2: float
class MyThing(TypedDict):
req1: int
opt1: ~str # ~ means a opposite-of-normal-totality key
req2: float
Solche Operatoren könnten auf type über die speziellen Methoden __pos__, __neg__ und __invert__ implementiert werden, ohne die Grammatik zu ändern.
Es wurde entschieden, dass es ratsam wäre, zuerst eine Langform-Notation (d. h. Required[] und NotRequired[]) einzuführen, bevor eine Kurzform-Notation eingeführt wird. Zukünftige PEPs können die Einführung dieser oder anderer Kurzform-Notationen erneut prüfen.
Beachten Sie bei der erneuten Prüfung der Einführung dieser Kurzform-Notation, dass +, - und ~ bereits bestehende Bedeutungen in der Python-Typisierungswelt haben: kovariant, kontravariant und invariant.
>>> from typing import TypeVar
>>> (TypeVar('T', covariant=True), TypeVar('U', contravariant=True), TypeVar('V'))
(+T, -U, ~V)
Kennzeichnung des Fehlens eines Wertes mit einer speziellen Konstante
Wir könnten eine neue Typ-Ebene-Konstante einführen, die das Fehlen eines Wertes signalisiert, wenn sie als Teil einer Union verwendet wird, ähnlich wie bei JavaScripts undefined-Typ, vielleicht Missing genannt.
class MyThing(TypedDict):
req1: int
opt1: str|Missing
req2: float
Eine solche Missing-Konstante könnte auch für andere Szenarien verwendet werden, wie z. B. den Typ einer Variablen, die nur bedingt definiert ist.
class MyClass:
attr: int|Missing
def __init__(self, set_attr: bool) -> None:
if set_attr:
self.attr = 10
def foo(set_attr: bool) -> None:
if set_attr:
attr = 10
reveal_type(attr) # int|Missing
Abweichung von der Art und Weise, wie Unions für Werte gelten
Diese Verwendung von ...|Missing, äquivalent zu Union[..., Missing], passt jedoch nicht gut zu dem, was eine Union normalerweise bedeutet: Union[...] beschreibt immer den Typ eines vorhandenen Wertes. Fehlen oder Nicht-Totalität ist dagegen eine Eigenschaft einer Variablen. Aktuelle Präzedenzfälle für die Kennzeichnung von Variablen-Eigenschaften umfassen Final[...] und ClassVar[...], mit denen der Vorschlag für Required[...] übereinstimmt.
Abweichung von der Art und Weise, wie Unions unterteilt werden
Darüber hinaus stimmt die Verwendung von Union[..., Missing] nicht mit den üblichen Methoden überein, mit denen Union-Werte aufgeteilt werden: Normalerweise können Sie Komponenten eines Union-Typs mit isinstance-Prüfungen eliminieren.
class Packet:
data: Union[str, bytes]
def send_data(packet: Packet) -> None:
if isinstance(packet.data, str):
reveal_type(packet.data) # str
packet_bytes = packet.data.encode('utf-8')
else:
reveal_type(packet.data) # bytes
packet_bytes = packet.data
socket.send(packet_bytes)
Wenn wir jedoch Union[..., Missing] zulassen würden, müssten Sie entweder den Missing-Fall mit hasattr für Objektattribute eliminieren.
class Packet:
data: Union[str, Missing]
def send_data(packet: Packet) -> None:
if hasattr(packet, 'data'):
reveal_type(packet.data) # str
packet_bytes = packet.data.encode('utf-8')
else:
reveal_type(packet.data) # Missing? error?
packet_bytes = b''
socket.send(packet_bytes)
oder eine Prüfung gegen locals() für lokale Variablen.
def send_data(packet_data: Optional[str]) -> None:
packet_bytes: Union[str, Missing]
if packet_data is not None:
packet_bytes = packet.data.encode('utf-8')
if 'packet_bytes' in locals():
reveal_type(packet_bytes) # bytes
socket.send(packet_bytes)
else:
reveal_type(packet_bytes) # Missing? error?
oder eine Prüfung über andere Mittel, wie z. B. gegen globals() für globale Variablen.
warning: Union[str, Missing]
import sys
if sys.version_info < (3, 6):
warning = 'Your version of Python is unsupported!'
if 'warning' in globals():
reveal_type(warning) # str
print(warning)
else:
reveal_type(warning) # Missing? error?
Seltsam und inkonsistent. Missing ist eigentlich kein Wert; es ist das Fehlen einer Definition, und ein solches Fehlen sollte speziell behandelt werden.
Schwierig zu implementieren
Eric Traut vom Pyright-Typ-Checker-Team hat erklärt, dass die Implementierung einer Notation im Stil von Union[..., Missing] schwierig wäre. [2]
Führt einen zweiten null-ähnlichen Wert in Python ein
Die Definition einer neuen Typ-Ebene-Konstante Missing wäre der Einführung einer neuen Wert-Ebene-Konstante Missing zur Laufzeit sehr ähnlich und würde einen zweiten null-ähnlichen Laufzeitwert neben None erzeugen. Zwei verschiedene null-ähnliche Konstanten in Python (None und Missing) zu haben, wäre verwirrend. Viele Neueinsteiger in JavaScript haben bereits Schwierigkeiten, zwischen seinen analogen Konstanten null und undefined zu unterscheiden.
Optional durch Nullable ersetzen. Optional umfunktionieren, um "optionales Element" zu bedeuten.
Optional[] ist zu allgegenwärtig, um es zu verwerfen, obwohl seine Verwendung im Laufe der Zeit zugunsten der durch PEP 604 spezifizierten Notation T|None abnehmen könnte.
Optional ändern, um in bestimmten Kontexten "optionales Element" anstelle von "nullable" zu bedeuten
Erwägen Sie die Verwendung eines speziellen Flags in einer TypedDict-Definition, um die Interpretation von Optional innerhalb des TypedDict von "nullable" auf "optionales Element" umzuändern.
class MyThing(TypedDict, optional_as_missing=True):
req1: int
opt1: Optional[str]
oder
class MyThing(TypedDict, optional_as_nullable=False):
req1: int
opt1: Optional[str]
Dies würde zu mehr Verwirrung bei den Benutzern führen, da dies bedeuten würde, dass in einigen Kontexten die Bedeutung von Optional[] anders ist als in anderen Kontexten, und es wäre leicht, das Flag zu übersehen.
Verschiedene Synonyme für "potenziell fehlendes Element"
- Omittable – zu leicht mit optional zu verwechseln
- OptionalItem, OptionalKey – zwei Wörter; zu leicht mit optional zu verwechseln
- MayExist, MissingOk – zwei Wörter
- Droppable – zu ähnlich zu Rusts
Drop, was etwas anderes bedeutet - Potential – zu vage
- Open – klingt, als würde es sich auf eine ganze Struktur anstatt auf ein Element beziehen
- Excludable
- Checked
Referenzen
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-0655.rst
Zuletzt geändert: 2024-06-16 22:42:44 GMT