PEP 586 – Literal Types
- Autor:
- Michael Lee <michael.lee.0x2a at gmail.com>, Ivan Levkivskyi <levkivskyi at gmail.com>, Jukka Lehtosalo <jukka.lehtosalo at iki.fi>
- BDFL-Delegate:
- Guido van Rossum <guido at python.org>
- Discussions-To:
- Typing-SIG list
- Status:
- Final
- Typ:
- Standards Track
- Thema:
- Typisierung
- Erstellt:
- 14. März 2019
- Python-Version:
- 3.8
- Post-History:
- 14. März 2019
- Resolution:
- Typing-SIG Nachricht
Inhaltsverzeichnis
Zusammenfassung
Dieses PEP schlägt die Hinzufügung von Literal-Typen zum PEP 484-Ökosystem vor. Literal-Typen zeigen an, dass ein Ausdruck buchstäblich einen bestimmten Wert hat. Zum Beispiel akzeptiert die folgende Funktion nur Ausdrücke, die buchstäblich den Wert „4“ haben
from typing import Literal
def accepts_only_four(x: Literal[4]) -> None:
pass
accepts_only_four(4) # OK
accepts_only_four(19) # Rejected
Motivation und Begründung
Python hat viele APIs, die je nach Wert eines bereitgestellten Arguments unterschiedliche Typen zurückgeben. Zum Beispiel
open(filename, mode)gibt entwederIO[bytes]oderIO[Text]zurück, je nachdem, ob das zweite Argument so etwas wieroderrbist.subprocess.check_output(...)gibt entweder Bytes oder Text zurück, je nachdem, ob das Schlüsselwortargumentuniversal_newlinesaufTrueoder nicht gesetzt ist.
Dieses Muster ist auch in vielen beliebten Drittanbieterbibliotheken relativ verbreitet. Hier sind nur zwei Beispiele aus pandas und numpy
pandas.concat(...)gibt entwederSeriesoderDataFramezurück, je nachdem, ob das Argumentaxisauf 0 oder 1 gesetzt ist.numpy.uniquegibt entweder ein einzelnes Array oder ein Tupel zurück, das je nach drei booleschen Flag-Werten zwischen zwei und vier Arrays enthält.
Der Tracking-Service für Typen enthält zusätzliche Beispiele und Diskussionen.
Derzeit gibt es keine Möglichkeit, die Typsignaturen dieser Funktionen auszudrücken: PEP 484 enthält keinen Mechanismus, um Signaturen zu schreiben, bei denen der Rückgabetyp vom übergebenen Wert abhängt. Beachten Sie, dass dieses Problem auch dann besteht, wenn wir diese APIs neu gestalten, um stattdessen Enums zu akzeptieren: MyEnum.FOO und MyEnum.BAR gelten beide als vom Typ MyEnum.
Derzeit umgehen Typprüfer diese Einschränkung, indem sie Ad-hoc-Erweiterungen für wichtige integrierte Funktionen und Standardbibliotheksfunktionen hinzufügen. Mein mypy wird beispielsweise mit einem Plugin geliefert, das versucht, genauere Typen für open(...) abzuleiten. Obwohl dieser Ansatz für Standardbibliotheksfunktionen funktioniert, ist er im Allgemeinen nicht nachhaltig: Es ist nicht zumutbar, von Autoren von Drittanbieterbibliotheken zu erwarten, dass sie Plugins für N verschiedene Typprüfer pflegen.
Wir schlagen die Hinzufügung von Literal-Typen vor, um diese Lücken zu schließen.
Kernsemantik
Dieser Abschnitt beschreibt das grundlegende Verhalten von Literal-Typen.
Kernverhalten
Literal-Typen geben an, dass eine Variable einen bestimmten und konkreten Wert hat. Wenn wir beispielsweise eine Variable foo mit dem Typ Literal[3] definieren, deklarieren wir, dass foo exakt gleich 3 sein muss und kein anderer Wert.
Für einen gegebenen Wert v, der Mitglied des Typs T ist, wird der Typ Literal[v] als Untertyp von T behandelt. Zum Beispiel ist Literal[3] ein Untertyp von int.
Alle Methoden vom Elterntyp werden direkt vom Literal-Typ geerbt. Wenn wir also eine Variable foo vom Typ Literal[3] haben, ist es sicher, Dinge wie foo + 5 zu tun, da foo die __add__-Methode von int erbt. Der resultierende Typ von foo + 5 ist int.
Dieses „erbende“ Verhalten ist identisch damit, wie wir NewTypes behandeln.
Gleichheit zweier Literale
Zwei Typen Literal[v1] und Literal[v2] sind gleich, wenn beide der folgenden Bedingungen erfüllt sind
type(v1) == type(v2)v1 == v2
Zum Beispiel sind Literal[20] und Literal[0x14] gleich. Jedoch sind Literal[0] und Literal[False] nicht gleich, obwohl 0 == False zur Laufzeit „true“ ergibt: 0 hat den Typ int und False hat den Typ bool.
Verkürzung von Unions aus Literalen
Literale werden mit einem oder mehreren Werten parametrisiert. Wenn ein Literal mit mehr als einem Wert parametrisiert wird, wird es als exakt gleich der Union dieser Typen behandelt. Das heißt, Literal[v1, v2, v3] ist gleich Union[Literal[v1], Literal[v2], Literal[v3]].
Diese Abkürzung hilft, das Schreiben von Signaturen für Funktionen, die viele verschiedene Literale akzeptieren, ergonomischer zu gestalten – zum Beispiel Funktionen wie open(...)
# Note: this is a simplification of the true type signature.
_PathType = Union[str, bytes, int]
@overload
def open(path: _PathType,
mode: Literal["r", "w", "a", "x", "r+", "w+", "a+", "x+"],
) -> IO[Text]: ...
@overload
def open(path: _PathType,
mode: Literal["rb", "wb", "ab", "xb", "r+b", "w+b", "a+b", "x+b"],
) -> IO[bytes]: ...
# Fallback overload for when the user isn't using literal types
@overload
def open(path: _PathType, mode: str) -> IO[Any]: ...
Die bereitgestellten Werte müssen nicht alle Mitglieder desselben Typs sein. Zum Beispiel ist Literal[42, "foo", True] ein legaler Typ.
Jedoch muss Literal mit mindestens einem Typ parametrisiert sein. Typen wie Literal[] oder Literal sind illegal.
Legale und illegale Parametrisierungen
Dieser Abschnitt beschreibt, was genau einen legalen Literal[...]-Typ ausmacht: welche Werte als Parameter verwendet werden dürfen und welche nicht.
Kurz gesagt, ein Literal[...]-Typ kann mit einem oder mehreren Literal-Ausdrücken parametrisiert werden, und nichts anderem.
Legale Parameter für Literal zur Typprüfungszeit
Literal kann mit ganzzahligen Literalen, Byte- und Unicode-Strings, Bools, Enum-Werten und None parametrisiert werden. So wären zum Beispiel all diese legal:
Literal[26]
Literal[0x1A] # Exactly equivalent to Literal[26]
Literal[-4]
Literal["hello world"]
Literal[b"hello world"]
Literal[u"hello world"]
Literal[True]
Literal[Color.RED] # Assuming Color is some enum
Literal[None]
Hinweis: Da der Typ None nur durch einen einzigen Wert besiedelt ist, sind die Typen None und Literal[None] exakt gleich. Typprüfer können Literal[None] zu einfach None vereinfachen.
Literal kann auch durch andere Literal-Typen oder Typ-Aliase für andere Literal-Typen parametrisiert werden. Zum Beispiel ist Folgendes legal
ReadOnlyMode = Literal["r", "r+"]
WriteAndTruncateMode = Literal["w", "w+", "wt", "w+t"]
WriteNoTruncateMode = Literal["r+", "r+t"]
AppendMode = Literal["a", "a+", "at", "a+t"]
AllModes = Literal[ReadOnlyMode, WriteAndTruncateMode,
WriteNoTruncateMode, AppendMode]
Diese Funktion ist wieder dazu gedacht, die Verwendung und Wiederverwendung von Literal-Typen ergonomischer zu gestalten.
Hinweis: Als Konsequenz der obigen Regeln wird erwartet, dass Typprüfer auch Typen unterstützen, die wie die folgenden aussehen
Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]
Dies sollte exakt gleich dem folgenden Typ sein
Literal[1, 2, 3, "foo", 5, None]
... und auch dem folgenden Typ
Optional[Literal[1, 2, 3, "foo", 5]]
Hinweis: String-Literal-Typen wie Literal["foo"] sollten auf Byte- oder Unicode-Typen sottotypen, so wie reguläre String-Literale zur Laufzeit.
Zum Beispiel ist in Python 3 der Typ Literal["foo"] gleich Literal[u"foo"], da "foo" in Python 3 gleich u"foo" ist.
Ebenso ist in Python 2 der Typ Literal["foo"] gleich Literal[b"foo"] – es sei denn, die Datei enthält einen Import from __future__ import unicode_literals, in diesem Fall wäre er gleich Literal[u"foo"].
Illegale Parameter für Literal zur Typprüfungszeit
Die folgenden Parameter sind absichtlich per Design nicht zugelassen
- Beliebige Ausdrücke wie
Literal[3 + 4]oderLiteral["foo".replace("o", "b")].- Begründung: Literal-Typen sollen eine minimale Erweiterung des PEP 484-Typisierungssystems sein, und die Anforderung, dass Typprüfer potenziell Ausdrücke innerhalb von Typen interpretieren müssen, fügt zu viel Komplexität hinzu. Siehe auch Abgelehnte oder außer Reichweite liegende Ideen.
- Infolgedessen sind auch komplexe Zahlen wie
Literal[4 + 3j]undLiteral[-4 + 2j]verboten. Zur Konsistenz sind auch Literale wieLiteral[4j], die nur eine einzelne komplexe Zahl enthalten, verboten. - Die einzige Ausnahme von dieser Regel ist der unäre
-(Minus) für Integers: Typen wieLiteral[-5]werden akzeptiert.
- Tupel, die gültige Literal-Typen enthalten, wie
Literal[(1, "foo", "bar")]. Der Benutzer könnte diesen Typ immer alsTuple[Literal[1], Literal["foo"], Literal["bar"]]ausdrücken. Außerdem sind Tupel wahrscheinlich mit derLiteral[1, 2, 3]-Abkürzung zu verwechseln. - Mutable Literal-Datenstrukturen wie Dict-Literale, List-Literale oder Set-Literale: Literale sind immer implizit final und unveränderlich. Daher ist
Literal[{"a": "b", "c": "d"}]illegal. - Alle anderen Typen: zum Beispiel sind
Literal[Path]oderLiteral[some_object_instance]illegal. Dies schließt Typevars ein: WennTein Typevar ist, istLiteral[T]nicht erlaubt. Typevars können nur über Typen variieren, niemals über Werte.
Die folgenden sind der Einfachheit halber vorläufig nicht zugelassen. Wir können erwägen, sie in zukünftigen Erweiterungen dieses PEP zuzulassen.
- Floats: z. B.
Literal[3.14]. Die Darstellung von Literalen für Unendlichkeit oder NaN auf saubere Weise ist schwierig; reale APIs werden ihr Verhalten wahrscheinlich nicht aufgrund eines Float-Parameters ändern. - Any: z. B.
Literal[Any].Anyist ein Typ, undLiteral[...]soll nur Werte enthalten. Es ist auch unklar, wasLiteral[Any]semantisch bedeuten würde.
Parameter zur Laufzeit
Obwohl die Menge der Parameter, die Literal[...] zur Typprüfungszeit enthalten kann, sehr klein ist, wird die tatsächliche Implementierung von typing.Literal keine Prüfungen zur Laufzeit durchführen. Zum Beispiel
def my_function(x: Literal[1 + 2]) -> int:
return x * 3
x: Literal = 3
y: Literal[my_function] = my_function
Der Typprüfer sollte dieses Programm ablehnen: Alle drei Verwendungen von Literal sind gemäß dieser Spezifikation ungültig. Python selbst sollte dieses Programm jedoch ohne Fehler ausführen.
Dies liegt teilweise daran, dass wir uns Flexibilität bewahren können, falls wir den Umfang dessen, wofür Literal verwendet werden kann, in Zukunft erweitern möchten, und teilweise daran, dass es nicht möglich ist, von vornherein alle illegalen Parameter zur Laufzeit zu erkennen. Zum Beispiel ist es unmöglich, zwischen Literal[1 + 2] und Literal[3] zur Laufzeit zu unterscheiden.
Literale, Enums und Forward References
Eine potenzielle Mehrdeutigkeit besteht zwischen Literal-Strings und Forward References zu Literal-Enum-Mitgliedern. Angenommen, wir haben den Typ Literal["Color.RED"]. Enthält dieser Literal-Typ einen String-Literal oder eine Forward Reference zu einem Color.RED-Enum-Mitglied?
In solchen Fällen gehen wir immer davon aus, dass der Benutzer einen Literal-String meinte. Wenn der Benutzer eine Forward Reference möchte, muss er den gesamten Literal-Typ in einen String einschließen – z. B. "Literal[Color.RED]".
Typinferenz
Dieser Abschnitt beschreibt einige Regeln bezüglich Typinferenz und Literalen, zusammen mit einigen Beispielen.
Abwärtskompatibilität
Wenn Typprüfer die Unterstützung für Literal hinzufügen, ist es wichtig, dass sie dies auf eine Weise tun, die die Abwärtskompatibilität maximiert. Typprüfer sollten sicherstellen, dass Code, der früher typgeprüft wurde, nach der Hinzufügung von Unterstützung für Literal auf bester Grundlage weiterhin typgeprüft wird.
Dies ist besonders wichtig bei der Typinferenz. Sollte beispielsweise bei der Anweisung x = "blue" der abgeleitete Typ von x str oder Literal["blue"] sein?
Eine naive Strategie wäre, immer davon auszugehen, dass Ausdrücke als Literal-Typen gedacht sind. So hätte x im obigen Beispiel immer den abgeleiteten Typ Literal["blue"]. Diese naive Strategie ist fast sicher zu störend – sie würde Programme wie die folgenden zum Scheitern bringen, die zuvor nicht gescheitert sind
# If a type checker infers 'var' has type Literal[3]
# and my_list has type List[Literal[3]]...
var = 3
my_list = [var]
# ...this call would be a type-error.
my_list.append(4)
Ein weiteres Beispiel, wann diese Strategie fehlschlägt, ist das Setzen von Feldern in Objekten
class MyObject:
def __init__(self) -> None:
# If a type checker infers MyObject.field has type Literal[3]...
self.field = 3
m = MyObject()
# ...this assignment would no longer type check
m.field = 4
Eine alternative Strategie, die die Kompatibilität in jedem Fall aufrechterhält, besteht darin, immer davon auszugehen, dass Ausdrücke keine Literal-Typen sind, es sei denn, sie werden explizit anders annotiert. Ein Typprüfer, der diese Strategie verwendet, würde im ersten Beispiel oben immer ableiten, dass x vom Typ str ist.
Dies ist nicht die einzig mögliche Strategie: Typprüfer sollten sich frei fühlen, mit ausgefeilteren Inferenztechniken zu experimentieren. Dieses PEP schreibt keine bestimmte Strategie vor; es betont nur die Bedeutung der Abwärtskompatibilität.
Verwendung von Nicht-Literal-Typen in Literal-Kontexten
Literal-Typen folgen den bestehenden Regeln für Subtyping ohne zusätzliche Spezialbehandlung. Beispielsweise sind Programme wie die folgenden typsicher
def expects_str(x: str) -> None: ...
var: Literal["foo"] = "foo"
# Legal: Literal["foo"] is a subtype of str
expects_str(var)
Das bedeutet auch, dass allgemeine Nicht-Literal-Ausdrücke nicht automatisch in Literale umgewandelt werden sollten. Zum Beispiel
def expects_literal(x: Literal["foo"]) -> None: ...
def runner(my_str: str) -> None:
# ILLEGAL: str is not a subclass of Literal["foo"]
expects_literal(my_str)
Hinweis: Wenn der Benutzer möchte, dass seine API sowohl Literale als auch den ursprünglichen Typ unterstützt – vielleicht aus Gründen der Abwärtskompatibilität – sollte er einen Fallback-Overload implementieren. Siehe Interaktionen mit Overloads.
Interaktionen mit anderen Typen und Features
Dieser Abschnitt diskutiert, wie Literal-Typen mit anderen bestehenden Typen interagieren.
Intelligente Indizierung von strukturierten Daten
Literale können verwendet werden, um strukturierte Typen wie Tupel, NamedTuple und Klassen „intelligent zu indizieren“. (Hinweis: Dies ist keine erschöpfende Liste).
Zum Beispiel sollten Typprüfer den korrekten Werttyp ableiten, wenn sie ein Tupel mit einem Integer-Schlüssel indizieren, der einem gültigen Index entspricht
a: Literal[0] = 0
b: Literal[5] = 5
some_tuple: Tuple[int, str, List[bool]] = (3, "abc", [True, False])
reveal_type(some_tuple[a]) # Revealed type is 'int'
some_tuple[b] # Error: 5 is not a valid index into the tuple
Wir erwarten ein ähnliches Verhalten bei der Verwendung von Funktionen wie getattr
class Test:
def __init__(self, param: int) -> None:
self.myfield = param
def mymethod(self, val: int) -> str: ...
a: Literal["myfield"] = "myfield"
b: Literal["mymethod"] = "mymethod"
c: Literal["blah"] = "blah"
t = Test()
reveal_type(getattr(t, a)) # Revealed type is 'int'
reveal_type(getattr(t, b)) # Revealed type is 'Callable[[int], str]'
getattr(t, c) # Error: No attribute named 'blah' in Test
Hinweis: Siehe Interaktionen mit Final für einen Vorschlag, wie wir die obigen Variablendeklarationen kompakter ausdrücken können.
Interaktionen mit Overloads
Literal-Typen und Overloads müssen nicht auf spezielle Weise interagieren: Die bestehenden Regeln funktionieren gut.
Ein wichtiger Anwendungsfall, den Typprüfer jedoch sorgfältig unterstützen müssen, ist die Möglichkeit, einen Fallback zu verwenden, wenn der Benutzer keine Literal-Typen verwendet. Betrachten Sie zum Beispiel open
_PathType = Union[str, bytes, int]
@overload
def open(path: _PathType,
mode: Literal["r", "w", "a", "x", "r+", "w+", "a+", "x+"],
) -> IO[Text]: ...
@overload
def open(path: _PathType,
mode: Literal["rb", "wb", "ab", "xb", "r+b", "w+b", "a+b", "x+b"],
) -> IO[bytes]: ...
# Fallback overload for when the user isn't using literal types
@overload
def open(path: _PathType, mode: str) -> IO[Any]: ...
Wenn wir die Signatur von open ändern würden, um nur die beiden oberen Overloads zu verwenden, würden wir jeglichen Code brechen, der keine Literal-String-Ausdrücke übergibt. Zum Beispiel wäre Code wie dieser kaputt
mode: str = pick_file_mode(...)
with open(path, mode) as f:
# f should continue to be of type IO[Any] here
Etwas breiter gefasst: Wir schlagen vor, eine Richtlinie für typ-sicher hinzuzufügen, die vorschreibt, dass immer, wenn wir Literal-Typen zu einer bestehenden API hinzufügen, wir auch immer einen Fallback-Overload zur Aufrechterhaltung der Abwärtskompatibilität hinzufügen.
Interaktionen mit Generics
Typen wie Literal[3] sollen einfach normale Unterklassen von int sein. Das bedeutet, dass Sie Typen wie Literal[3] überall dort verwenden können, wo Sie normale Typen verwenden könnten, z. B. mit Generics.
Das bedeutet, dass es legal ist, generische Funktionen oder Klassen mit Literal-Typen zu parametrisieren
A = TypeVar('A', bound=int)
B = TypeVar('B', bound=int)
C = TypeVar('C', bound=int)
# A simplified definition for Matrix[row, column]
class Matrix(Generic[A, B]):
def __add__(self, other: Matrix[A, B]) -> Matrix[A, B]: ...
def __matmul__(self, other: Matrix[B, C]) -> Matrix[A, C]: ...
def transpose(self) -> Matrix[B, A]: ...
foo: Matrix[Literal[2], Literal[3]] = Matrix(...)
bar: Matrix[Literal[3], Literal[7]] = Matrix(...)
baz = foo @ bar
reveal_type(baz) # Revealed type is 'Matrix[Literal[2], Literal[7]]'
Ebenso ist es legal, TypeVars mit Wertbeschränkungen oder Grenzen, die Literal-Typen beinhalten, zu konstruieren
T = TypeVar('T', Literal["a"], Literal["b"], Literal["c"])
S = TypeVar('S', bound=Literal["foo"])
…obwohl es unklar ist, wann es nützlich wäre, ein TypeVar mit einer Literal-Obergrenze zu konstruieren. Zum Beispiel ist der S TypeVar im obigen Beispiel im Wesentlichen nutzlos: Wir können ein äquivalentes Verhalten erzielen, indem wir stattdessen S = Literal["foo"] verwenden.
Hinweis: Literal-Typen und Generics interagieren bewusst nur auf sehr grundlegende und begrenzte Weise. Insbesondere Bibliotheken, die Code mit vielen numerischen oder NumPy-ähnlichen Manipulationen typisieren möchten, werden die in diesem PEP vorgeschlagenen Literal-Typen wahrscheinlich für ihre Bedürfnisse unzureichend finden.
Wir haben mehrere verschiedene Vorschläge zur Behebung dieses Problems in Betracht gezogen, haben uns aber letztendlich entschieden, das Problem der Integer-Generics auf einen späteren Zeitpunkt zu verschieben. Siehe Abgelehnte oder außer Reichweite liegende Ideen für weitere Details.
Interaktionen mit Enums und Vollständigkeitsprüfungen
Typprüfer sollten in der Lage sein, Vollständigkeitsprüfungen durchzuführen, wenn sie Literal-Typen mit einer geschlossenen Anzahl von Varianten, wie z. B. Enums, bearbeiten. Zum Beispiel sollte der Typprüfer in der Lage sein abzuleiten, dass die letzte else-Anweisung vom Typ str sein muss, da alle drei Werte des Status-Enums bereits erschöpft sind
class Status(Enum):
SUCCESS = 0
INVALID_DATA = 1
FATAL_ERROR = 2
def parse_status(s: Union[str, Status]) -> None:
if s is Status.SUCCESS:
print("Success!")
elif s is Status.INVALID_DATA:
print("The given data is invalid because...")
elif s is Status.FATAL_ERROR:
print("Unexpected fatal error...")
else:
# 's' must be of type 'str' since all other options are exhausted
print("Got custom status: " + s)
Die beschriebene Interaktion ist nicht neu: Sie ist bereits in PEP 484 kodifiziert. Jedoch implementieren viele Typprüfer (wie mypy) dies noch nicht aufgrund der erwarteten Komplexität der Implementierungsarbeit.
Ein Teil dieser Komplexität wird durch die Einführung von Literal-Typen verringert: Anstatt Enums vollständig speziell zu behandeln, können wir sie als annähernd gleich der Union ihrer Werte behandeln und jede bestehende Logik für Unions, Vollständigkeit, Typ-Narrowing, Erreichbarkeit und so weiter nutzen, die der Typprüfer möglicherweise bereits implementiert hat.
Hier könnte das Status-Enum also annähernd als gleich Literal[Status.SUCCESS, Status.INVALID_DATA, Status.FATAL_ERROR] behandelt werden, und der Typ von s entsprechend verengt werden.
Interaktionen mit Narrowing
Typprüfer können optional zusätzliche Analysen sowohl für Enum- als auch für Nicht-Enum-Literal-Typen durchführen, die über das in dem obigen Abschnitt Beschriebene hinausgehen.
Zum Beispiel kann es nützlich sein, eine Verengung basierend auf Dingen wie Containment- oder Gleichheitsprüfungen durchzuführen
def parse_status(status: str) -> None:
if status in ("MALFORMED", "ABORTED"):
# Type checker could narrow 'status' to type
# Literal["MALFORMED", "ABORTED"] here.
return expects_bad_status(status)
# Similarly, type checker could narrow 'status' to Literal["PENDING"]
if status == "PENDING":
expects_pending_status(status)
Es kann auch nützlich sein, eine Verengung unter Berücksichtigung von Ausdrücken durchzuführen, die Literal-Booleans beinhalten. Zum Beispiel können wir Literal[True], Literal[False] und Overloads kombinieren, um „benutzerdefinierte Typ-Guards“ zu erstellen
@overload
def is_int_like(x: Union[int, List[int]]) -> Literal[True]: ...
@overload
def is_int_like(x: object) -> bool: ...
def is_int_like(x): ...
vector: List[int] = [1, 2, 3]
if is_int_like(vector):
vector.append(3)
else:
vector.append("bad") # This branch is inferred to be unreachable
scalar: Union[int, str]
if is_int_like(scalar):
scalar += 3 # Type checks: type of 'scalar' is narrowed to 'int'
else:
scalar += "foo" # Type checks: type of 'scalar' is narrowed to 'str'
Interaktionen mit Final
PEP 591 schlägt die Hinzufügung eines „Final“-Qualifikators zum Typisierungssystem vor. Dieser Qualifikator kann verwendet werden, um zu deklarieren, dass eine Variable oder ein Attribut nicht neu zugewiesen werden kann
foo: Final = 3
foo = 4 # Error: 'foo' is declared to be Final
Beachten Sie, dass wir im obigen Beispiel wissen, dass foo immer exakt gleich 3 sein wird. Ein Typprüfer kann diese Information nutzen, um zu schlussfolgern, dass foo in jedem Kontext gültig ist, der ein Literal[3] erwartet
def expects_three(x: Literal[3]) -> None: ...
expects_three(foo) # Type checks, since 'foo' is Final and equal to 3
Der Final-Qualifikator dient als Abkürzung, um zu deklarieren, dass eine Variable effektiv Literal ist.
Wenn sowohl dieses PEP als auch PEP 591 akzeptiert werden, wird erwartet, dass Typprüfer diese Abkürzung unterstützen. Insbesondere bei einer Variablen- oder Attributzuweisung der Form var: Final = value, wobei value ein gültiger Parameter für Literal[...] ist, sollten Typprüfer verstehen, dass var in jedem Kontext verwendet werden kann, der ein Literal[value] erwartet.
Typprüfer sind nicht verpflichtet, andere Verwendungen von Final zu verstehen. Zum Beispiel bleibt es unbestimmt, ob das folgende Programm typsicher ist
# Note: The assignment does not exactly match the form 'var: Final = value'.
bar1: Final[int] = 3
expects_three(bar1) # May or may not be accepted by type checkers
# Note: "Literal[1 + 2]" is not a legal type.
bar2: Final = 1 + 2
expects_three(bar2) # May or may not be accepted by type checkers
Abgelehnte oder außer Reichweite liegende Ideen
Dieser Abschnitt umreißt einige potenzielle Features, die explizit außer Reichweite liegen.
Echte abhängige Typen/Integer-Generics
Dieser Vorschlag beschreibt im Wesentlichen die Hinzufügung eines sehr vereinfachten Systems abhängiger Typen zum PEP 484-Ökosystem. Eine offensichtliche Erweiterung wäre die Implementierung eines vollwertigen Systems abhängiger Typen, das es Benutzern ermöglicht, Typen basierend auf ihren Werten auf beliebige Weise zu prädizieren. Das würde es uns ermöglichen, Signaturen wie die unten aufgeführten zu schreiben
# A vector has length 'n', containing elements of type 'T'
class Vector(Generic[N, T]): ...
# The type checker will statically verify our function genuinely does
# construct a vector that is equal in length to "len(vec1) + len(vec2)"
# and will throw an error if it does not.
def concat(vec1: Vector[A, T], vec2: Vector[B, T]) -> Vector[A + B, T]:
# ...snip...
Mindestens wäre es nützlich, eine Form von Integer-Generics hinzuzufügen.
Obwohl ein solches Typsystem zweifellos nützlich wäre, liegt es außerhalb des Rahmens dieses PEP: Es würde weitaus mehr Implementierungsarbeit, Diskussion und Forschung erfordern, um es im Vergleich zum aktuellen Vorschlag abzuschließen.
Es ist durchaus möglich, dass wir zu diesem Thema zurückkehren und es erneut prüfen: Wir werden sehr wahrscheinlich eine Form von abhängiger Typisierung zusammen mit anderen Erweiterungen wie variadischen Generics benötigen, um beliebte Bibliotheken wie numpy zu unterstützen.
Dieses PEP sollte als Sprungbrett für dieses Ziel betrachtet werden und nicht als Versuch, eine umfassende Lösung anzubieten.
Hinzufügen einer prägnanteren Syntax
Ein Einwand gegen dieses PEP ist, dass die explizite Angabe von Literal[...] wortreich erscheint. Anstatt zum Beispiel zu schreiben
def foobar(arg1: Literal[1], arg2: Literal[True]) -> None:
pass
…wäre es schön, stattdessen zu schreiben
def foobar(arg1: 1, arg2: True) -> None:
pass
Leider funktionieren diese Abkürzungen einfach nicht mit der bestehenden Implementierung von typing zur Laufzeit. Zum Beispiel stürzt das folgende Snippet ab, wenn es mit Python 3.7 ausgeführt wird
from typing import Tuple
# Supposed to accept tuple containing the literals 1 and 2
def foo(x: Tuple[1, 2]) -> None:
pass
Die Ausführung ergibt die folgende Ausnahme
TypeError: Tuple[t0, t1, ...]: each t must be a type. Got 1.
Wir möchten nicht, dass sich Benutzer genau merken müssen, wann es in Ordnung ist, Literal wegzulassen, daher verlangen wir, dass Literal immer vorhanden ist.
Allgemeiner gesagt, sind wir der Meinung, dass eine Überarbeitung der Syntax von Typen in Python nicht im Rahmen dieses PEP liegt: Es wäre am besten, diese Diskussion in einem separaten PEP zu führen, anstatt sie an dieses anzuhängen. Daher versucht dieses PEP bewusst nicht, die Typ-Syntax von Python zu innovieren.
Zurückportierung des Literal-Typs
Sobald dieses PEP akzeptiert ist, muss der Literal-Typ für Python-Versionen zurückportiert werden, die ältere Versionen des typing-Moduls gebündelt haben. Wir planen dies zu tun, indem wir Literal dem typing_extensions-Drittanbietermodul hinzufügen, das eine Vielzahl anderer zurückportierter Typen enthält.
Implementierung
Der mypy-Typprüfer hat derzeit einen großen Teil des in dieser Spezifikation beschriebenen Verhaltens implementiert, mit Ausnahme von Enum-Literalen und einiger der komplexeren Narrowing-Interaktionen, die oben beschrieben wurden.
Danksagungen
Vielen Dank an Mark Mendoza, Ran Benita, Rebecca Chen und die anderen Mitglieder von typing-sig für ihre Kommentare zu diesem PEP.
Zusätzlicher Dank geht an die verschiedenen Teilnehmer der mypy- und typing-Issue-Tracker, die viel Motivation und Begründung für dieses PEP geliefert haben.
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0586.rst
Zuletzt geändert: 2025-02-01 07:28:42 GMT