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

Python Enhancement Proposals

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

Wichtig

Dieses PEP ist ein historisches Dokument: siehe required-notrequired, typing.Required und typing.NotRequired für aktuelle Spezifikationen und Dokumentation. Kanonische Typenspezifikationen werden auf der Typing Specs-Website gepflegt; das Laufzeit-Typing-Verhalten wird in der CPython-Dokumentation beschrieben.

×

Siehe den Prozess zur Aktualisierung der Typ-Spezifikation, um Änderungen an der Typ-Spezifikation vorzuschlagen.

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


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

Zuletzt geändert: 2024-06-16 22:42:44 GMT