PEP 696 – Typ-Standardwerte für Typparameter
- Autor:
- James Hilton-Balfe <gobot1234yt at gmail.com>
- Sponsor:
- Jelle Zijlstra <jelle.zijlstra at gmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Final
- Typ:
- Standards Track
- Thema:
- Typisierung
- Erstellt:
- 14. Jul 2022
- Python-Version:
- 3.13
- Post-History:
- 22. Mär 2022, 08. Jan 2023
- Resolution:
- Discourse-Nachricht
Inhaltsverzeichnis
- Zusammenfassung
- Motivation
- Spezifikation
- Standard-Reihenfolge und Abonnementregeln
ParamSpec-StandardwerteTypeVarTuple-Standardwerte- Verwendung eines anderen Typparameters als
default GenericTypeAliases- Unterklasse
- Verwendung von
boundunddefault - Beschränkungen
- Funktions-Standardwerte
- Standardwerte nach
TypeVarTuple - Subtyping
TypeVarTuples als Standardwerte
- Bindungsregeln
- Implementierung
- Abgelehnte Alternativen
- Zulassen, dass Typ-Parameter-Standardwerte an
type.__new__’s**kwargsübergeben werden - Zulassen, dass Nicht-Standardwerte auf Standardwerte folgen
- Implizites Setzen von
defaultaufbound - Zulassen, dass Typ-Parameter mit Standardwerten in Funktionssignaturen verwendet werden
- Zulassen von Typ-Parametern aus äußeren Gültigkeitsbereichen in
default
- Zulassen, dass Typ-Parameter-Standardwerte an
- Danksagungen
- Urheberrecht
Zusammenfassung
Diese PEP führt das Konzept von Typ-Standardwerten für Typparameter ein, einschließlich TypeVar, ParamSpec und TypeVarTuple, die als Standardwerte für Typparameter dienen, für die kein Typ angegeben ist.
Die Unterstützung für Standard-Typargumente ist in einigen beliebten Sprachen wie C++, TypeScript und Rust verfügbar. Eine Umfrage zur Syntax von Typparametern in einigen gängigen Sprachen wurde vom Autor von PEP 695 durchgeführt und kann in dessen Anhang A gefunden werden.
Motivation
T = TypeVar("T", default=int) # This means that if no type is specified T = int
@dataclass
class Box(Generic[T]):
value: T | None = None
reveal_type(Box()) # type is Box[int]
reveal_type(Box(value="Hello World!")) # type is Box[str]
Ein Ort, an dem dies regelmäßig vorkommt, ist Generator. Ich schlage vor, die Stub-Definition wie folgt zu ändern:
YieldT = TypeVar("YieldT")
SendT = TypeVar("SendT", default=None)
ReturnT = TypeVar("ReturnT", default=None)
class Generator(Generic[YieldT, SendT, ReturnT]): ...
Generator[int] == Generator[int, None] == Generator[int, None, None]
Dies ist auch nützlich für ein Generic, das üblicherweise über einen Typ geht.
class Bot: ...
BotT = TypeVar("BotT", bound=Bot, default=Bot)
class Context(Generic[BotT]):
bot: BotT
class MyBot(Bot): ...
reveal_type(Context().bot) # type is Bot # notice this is not Any which is what it would be currently
reveal_type(Context[MyBot]().bot) # type is MyBot
Dies verbessert nicht nur das Tippen für diejenigen, die es explizit verwenden, sondern hilft auch Nicht-Tipp-Benutzern, die sich auf Autovervollständigung verlassen, um ihre Entwicklung zu beschleunigen.
Dieses Entwurfsmuster ist in Projekten wie
- discord.py – woher das obige Beispiel stammt.
- NumPy – der Standard für Typen wie
ndarray’sdtypewärefloat64. Derzeit ist esUnknownoderAny. - TensorFlow – dies könnte für Tensor ähnlich wie
numpy.ndarrayverwendet werden und wäre nützlich, um die Definition vonLayerzu vereinfachen.
Spezifikation
Standard-Reihenfolge und Abonnementregeln
Die Reihenfolge für Standardwerte sollte den Standardregeln für Funktionsparameter folgen, sodass ein Typparameter ohne default nicht auf einen mit einem default-Wert folgen kann. Dies sollte idealerweise einen TypeError in typing._GenericAlias/types.GenericAlias auslösen, und ein Typ-Checker sollte dies als Fehler kennzeichnen.
DefaultStrT = TypeVar("DefaultStrT", default=str)
DefaultIntT = TypeVar("DefaultIntT", default=int)
DefaultBoolT = TypeVar("DefaultBoolT", default=bool)
T = TypeVar("T")
T2 = TypeVar("T2")
class NonDefaultFollowsDefault(Generic[DefaultStrT, T]): ... # Invalid: non-default TypeVars cannot follow ones with defaults
class NoNonDefaults(Generic[DefaultStrT, DefaultIntT]): ...
(
NoNoneDefaults ==
NoNoneDefaults[str] ==
NoNoneDefaults[str, int]
) # All valid
class OneDefault(Generic[T, DefaultBoolT]): ...
OneDefault[float] == OneDefault[float, bool] # Valid
reveal_type(OneDefault) # type is type[OneDefault[T, DefaultBoolT = bool]]
reveal_type(OneDefault[float]()) # type is OneDefault[float, bool]
class AllTheDefaults(Generic[T1, T2, DefaultStrT, DefaultIntT, DefaultBoolT]): ...
reveal_type(AllTheDefaults) # type is type[AllTheDefaults[T1, T2, DefaultStrT = str, DefaultIntT = int, DefaultBoolT = bool]]
reveal_type(AllTheDefaults[int, complex]()) # type is AllTheDefaults[int, complex, str, int, bool]
AllTheDefaults[int] # Invalid: expected 2 arguments to AllTheDefaults
(
AllTheDefaults[int, complex] ==
AllTheDefaults[int, complex, str] ==
AllTheDefaults[int, complex, str, int] ==
AllTheDefaults[int, complex, str, int, bool]
) # All valid
Mit der neuen Python 3.12-Syntax für Generics (eingeführt durch PEP 695) kann dies zur Kompilierzeit erzwungen werden
type Alias[DefaultT = int, T] = tuple[DefaultT, T] # SyntaxError: non-default TypeVars cannot follow ones with defaults
def generic_func[DefaultT = int, T](x: DefaultT, y: T) -> None: ... # SyntaxError: non-default TypeVars cannot follow ones with defaults
class GenericClass[DefaultT = int, T]: ... # SyntaxError: non-default TypeVars cannot follow ones with defaults
ParamSpec-Standardwerte
ParamSpec-Standardwerte werden mit derselben Syntax wie TypeVars definiert, verwenden aber eine Liste von Typen oder ein Ellipsen-Literal „...“ oder ein anderes in-Scope befindliches ParamSpec (siehe Gültigkeitsbereichsregeln).
DefaultP = ParamSpec("DefaultP", default=[str, int])
class Foo(Generic[DefaultP]): ...
reveal_type(Foo) # type is type[Foo[DefaultP = [str, int]]]
reveal_type(Foo()) # type is Foo[[str, int]]
reveal_type(Foo[[bool, bool]]()) # type is Foo[[bool, bool]]
TypeVarTuple-Standardwerte
TypeVarTuple-Standardwerte werden mit derselben Syntax wie TypeVars definiert, verwenden aber ein entpacktes Tupel von Typen anstelle eines einzelnen Typs oder eines anderen in-Scope befindlichen TypeVarTuple (siehe Gültigkeitsbereichsregeln).
DefaultTs = TypeVarTuple("DefaultTs", default=Unpack[tuple[str, int]])
class Foo(Generic[*DefaultTs]): ...
reveal_type(Foo) # type is type[Foo[DefaultTs = *tuple[str, int]]]
reveal_type(Foo()) # type is Foo[str, int]
reveal_type(Foo[int, bool]()) # type is Foo[int, bool]
Verwendung eines anderen Typparameters als default
Dies ermöglicht die Wiederverwendung eines Wertes, wenn der Typparameter für ein Generic fehlt, aber ein anderer Typparameter angegeben ist.
Um einen anderen Typparameter als Standardwert zu verwenden, müssen der default und der Typparameter vom selben Typ sein (der Standardwert eines TypeVar muss ein TypeVar sein, usw.).
Dies könnte bei builtins.slice verwendet werden, wobei der start-Parameter auf int standardisieren sollte, stop auf den Typ von start und step auf int | None standardisieren sollte.
StartT = TypeVar("StartT", default=int)
StopT = TypeVar("StopT", default=StartT)
StepT = TypeVar("StepT", default=int | None)
class slice(Generic[StartT, StopT, StepT]): ...
reveal_type(slice) # type is type[slice[StartT = int, StopT = StartT, StepT = int | None]]
reveal_type(slice()) # type is slice[int, int, int | None]
reveal_type(slice[str]()) # type is slice[str, str, int | None]
reveal_type(slice[str, bool, timedelta]()) # type is slice[str, bool, timedelta]
T2 = TypeVar("T2", default=DefaultStrT)
class Foo(Generic[DefaultStrT, T2]):
def __init__(self, a: DefaultStrT, b: T2) -> None: ...
reveal_type(Foo(1, "")) # type is Foo[int, str]
Foo[int](1, "") # Invalid: Foo[int, str] cannot be assigned to self: Foo[int, int] in Foo.__init__
Foo[int]("", 1) # Invalid: Foo[str, int] cannot be assigned to self: Foo[int, int] in Foo.__init__
Wenn ein Typparameter als Standardwert für einen anderen Typparameter verwendet wird, gelten die folgenden Regeln, wobei T1 der Standardwert für T2 ist.
Gültigkeitsbereichsregeln
T1 muss vor T2 in der Parameterliste des Generics verwendet werden.
T2 = TypeVar("T2", default=T1)
class Foo(Generic[T1, T2]): ... # Valid
class Foo(Generic[T1]):
class Bar(Generic[T2]): ... # Valid
StartT = TypeVar("StartT", default="StopT") # Swapped defaults around from previous example
StopT = TypeVar("StopT", default=int)
class slice(Generic[StartT, StopT, StepT]): ...
# ^^^^^^ Invalid: ordering does not allow StopT to be bound
Die Verwendung eines Typparameters aus einem äußeren Gültigkeitsbereich als Standardwert wird nicht unterstützt.
Bindungsregeln
T1’s Bindung muss eine Unterart von T2’s Bindung sein.
T1 = TypeVar("T1", bound=int)
TypeVar("Ok", default=T1, bound=float) # Valid
TypeVar("AlsoOk", default=T1, bound=int) # Valid
TypeVar("Invalid", default=T1, bound=str) # Invalid: int is not a subtype of str
Einschränkungsregeln
Die Einschränkungen von T2 müssen eine Obermenge der Einschränkungen von T1 sein.
T1 = TypeVar("T1", bound=int)
TypeVar("Invalid", float, str, default=T1) # Invalid: upper bound int is incompatible with constraints float or str
T1 = TypeVar("T1", int, str)
TypeVar("AlsoOk", int, str, bool, default=T1) # Valid
TypeVar("AlsoInvalid", bool, complex, default=T1) # Invalid: {bool, complex} is not a superset of {int, str}
Typparameter als Parameter für Generics
Typparameter sind als Parameter für Generics innerhalb eines default gültig, wenn der erste Parameter gemäß dem vorherigen Abschnitt im Gültigkeitsbereich liegt.
T = TypeVar("T")
ListDefaultT = TypeVar("ListDefaultT", default=list[T])
class Bar(Generic[T, ListDefaultT]):
def __init__(self, x: T, y: ListDefaultT): ...
reveal_type(Bar) # type is type[Bar[T, ListDefaultT = list[T]]]
reveal_type(Bar[int]) # type is type[Bar[int, list[int]]]
reveal_type(Bar[int]()) # type is Bar[int, list[int]]
reveal_type(Bar[int, list[str]]()) # type is Bar[int, list[str]]
reveal_type(Bar[int, str]()) # type is Bar[int, str]
Spezialisierungsregeln
Typparameter können derzeit nicht weiter indiziert werden. Dies könnte sich ändern, wenn Higher Kinded TypeVars implementiert werden.
Generic TypeAliases
Generic TypeAliases sollten weiter indiziert werden können, gemäß den normalen Indizierungsregeln. Wenn ein Typparameter einen Standardwert hat, der nicht überschrieben wurde, sollte er so behandelt werden, als wäre er in das TypeAlias substituiert worden. Er kann jedoch später weiter spezialisiert werden.
class SomethingWithNoDefaults(Generic[T, T2]): ...
MyAlias: TypeAlias = SomethingWithNoDefaults[int, DefaultStrT] # Valid
reveal_type(MyAlias) # type is type[SomethingWithNoDefaults[int, DefaultStrT]]
reveal_type(MyAlias[bool]()) # type is SomethingWithNoDefaults[int, bool]
MyAlias[bool, int] # Invalid: too many arguments passed to MyAlias
Unterklasse
Unterklassen von Generics mit Typparametern, die Standardwerte haben, verhalten sich ähnlich wie Generic TypeAliases. Das heißt, Unterklassen können weiter indiziert werden, gemäß den normalen Indizierungsregeln, nicht überschriebene Standardwerte werden substituiert und Typparameter mit solchen Standardwerten können später weiter spezialisiert werden.
class SubclassMe(Generic[T, DefaultStrT]):
x: DefaultStrT
class Bar(SubclassMe[int, DefaultStrT]): ...
reveal_type(Bar) # type is type[Bar[DefaultStrT = str]]
reveal_type(Bar()) # type is Bar[str]
reveal_type(Bar[bool]()) # type is Bar[bool]
class Foo(SubclassMe[float]): ...
reveal_type(Foo().x) # type is str
Foo[str] # Invalid: Foo cannot be further subscripted
class Baz(Generic[DefaultIntT, DefaultStrT]): ...
class Spam(Baz): ...
reveal_type(Spam()) # type is <subclass of Baz[int, str]>
Verwendung von bound und default
Wenn sowohl bound als auch default übergeben werden, muss default eine Unterart von bound sein. Andernfalls sollte der Typ-Checker einen Fehler generieren.
TypeVar("Ok", bound=float, default=int) # Valid
TypeVar("Invalid", bound=str, default=int) # Invalid: the bound and default are incompatible
Beschränkungen
Für eingeschränkte TypeVars muss der Standardwert eine der Einschränkungen sein. Ein Typ-Checker sollte einen Fehler generieren, auch wenn er eine Unterart einer der Einschränkungen ist.
TypeVar("Ok", float, str, default=float) # Valid
TypeVar("Invalid", float, str, default=int) # Invalid: expected one of float or str got int
Funktions-Standardwerte
In generischen Funktionen können Typ-Checker den Standardwert eines Typparameters verwenden, wenn der Typparameter nicht zu etwas aufgelöst werden kann. Wir lassen die Semantik dieser Verwendung unbestimmt, da die Sicherstellung, dass der default in jedem Code-Pfad zurückgegeben wird, in dem der Typparameter ungelöst bleiben kann, möglicherweise zu schwer zu implementieren ist. Typ-Checker können entweder diesen Fall verbieten oder mit der Implementierung von Unterstützung experimentieren.
T = TypeVar('T', default=int)
def func(x: int | set[T]) -> T: ...
reveal_type(func(0)) # a type checker may reveal T's default of int here
Standardwerte nach TypeVarTuple
Ein TypeVar, der unmittelbar auf einen TypeVarTuple folgt, darf keinen Standardwert haben, da unklar wäre, ob ein Typ-Argument an das TypeVarTuple oder das standardisierte TypeVar gebunden werden soll.
Ts = TypeVarTuple("Ts")
T = TypeVar("T", default=bool)
class Foo(Generic[Ts, T]): ... # Type checker error
# Could be reasonably interpreted as either Ts = (int, str, float), T = bool
# or Ts = (int, str), T = float
Foo[int, str, float]
Mit der integrierten Generic-Syntax von Python 3.12 sollte dieser Fall einen SyntaxError auslösen.
Es ist jedoch zulässig, einen ParamSpec mit einem Standardwert nach einem TypeVarTuple mit einem Standardwert zu haben, da es keine Mehrdeutigkeit zwischen einem Typ-Argument für den ParamSpec und einem für den TypeVarTuple geben kann.
Ts = TypeVarTuple("Ts")
P = ParamSpec("P", default=[float, bool])
class Foo(Generic[Ts, P]): ... # Valid
Foo[int, str] # Ts = (int, str), P = [float, bool]
Foo[int, str, [bytes]] # Ts = (int, str), P = [bytes]
Subtyping
Typ-Parameter-Standardwerte beeinflussen nicht die Subtyping-Regeln für generische Klassen. Insbesondere können Standardwerte ignoriert werden, wenn geprüft wird, ob eine Klasse mit einem generischen Protokoll kompatibel ist.
TypeVarTuples als Standardwerte
Die Verwendung eines TypeVarTuple als Standardwert wird nicht unterstützt, weil
- Gültigkeitsbereichsregeln die Verwendung von Typ-Parametern aus äußeren Gültigkeitsbereichen nicht zulässt.
- Mehrere
TypeVarTuples können nicht in der Typparameterliste für ein einzelnes Objekt erscheinen, wie in PEP 646 angegeben.
Diese Gründe lassen keinen derzeit gültigen Ort übrig, an dem ein TypeVarTuple als Standardwert eines anderen TypeVarTuple verwendet werden könnte.
Bindungsregeln
Typ-Parameter-Standardwerte sollten durch Attributzugriff gebunden werden (einschließlich Aufruf und Indizierung).
class Foo[T = int]:
def meth(self) -> Self:
return self
reveal_type(Foo.meth) # type is (self: Foo[int]) -> Foo[int]
Implementierung
Zur Laufzeit würde dies folgende Änderungen am Modul typing beinhalten.
- Die Klassen
TypeVar,ParamSpecundTypeVarTuplesollten den andefaultübergebenen Typ preisgeben. Dieser wäre als Attribut__default__verfügbar, dasNonewäre, wenn kein Argument übergeben wird, undNoneType, wenndefault=None.
Folgende Änderungen wären an beiden GenericAliases erforderlich
- Logik zur Bestimmung der für eine Indizierung erforderlichen Standardwerte.
- Idealerweise Logik zur Bestimmung, ob eine Indizierung (wie
Generic[T, DefaultT]) gültig wäre.
Die Grammatik für Typparameterlisten müsste erweitert werden, um Standardwerte zuzulassen; siehe unten.
Eine Referenzimplementierung der Laufzeitänderungen finden Sie unter https://github.com/Gobot1234/cpython/tree/pep-696
Eine Referenzimplementierung des Typ-Checkers finden Sie unter https://github.com/Gobot1234/mypy/tree/TypeVar-defaults
Pyright unterstützt derzeit diese Funktionalität.
Grammatikänderungen
Die in PEP 695 eingeführte Syntax wird erweitert, um eine Möglichkeit zur Angabe von Standardwerten für Typparameter zu schaffen, indem der „=“-Operator innerhalb der eckigen Klammern wie folgt verwendet wird:
# TypeVars
class Foo[T = str]: ...
# ParamSpecs
class Baz[**P = [int, str]]: ...
# TypeVarTuples
class Qux[*Ts = *tuple[int, bool]]: ...
# TypeAliases
type Foo[T, U = str] = Bar[T, U]
type Baz[**P = [int, str]] = Spam[**P]
type Qux[*Ts = *tuple[str]] = Ham[*Ts]
type Rab[U, T = str] = Bar[T, U]
Ähnlich wie bei der Bindung für einen Typparameter sollten Standardwerte verzögert ausgewertet werden, mit denselben Gültigkeitsbereichsregeln, um die unnötige Verwendung von Anführungszeichen zu vermeiden.
Diese Funktionalität war im ersten Entwurf von PEP 695 enthalten, wurde jedoch aufgrund von Scope Creep entfernt.
Folgende Änderungen wären an der Grammatik vorzunehmen
type_param:
| a=NAME b=[type_param_bound] d=[type_param_default]
| a=NAME c=[type_param_constraint] d=[type_param_default]
| '*' a=NAME d=[type_param_default]
| '**' a=NAME d=[type_param_default]
type_param_default:
| '=' e=expression
| '=' e=starred_expression
Der Compiler würde erzwingen, dass Typparameter ohne Standardwerte nicht auf Typparameter mit Standardwerten folgen dürfen und dass TypeVars mit Standardwerten nicht unmittelbar auf TypeVarTuples folgen dürfen.
Abgelehnte Alternativen
Zulassen, dass Typ-Parameter-Standardwerte an type.__new__’s **kwargs übergeben werden
T = TypeVar("T")
@dataclass
class Box(Generic[T], T=int):
value: T | None = None
Dies ist zwar viel besser lesbar und folgt einer ähnlichen Begründung wie die TypeVar Unärische Syntax, wäre aber nicht abwärtskompatibel, da T möglicherweise bereits an eine Metaklasse/Superklasse übergeben wird oder Klassen unterstützt, die zur Laufzeit nicht von Generic erben.
Idealerweise wäre, wenn PEP 637 nicht abgelehnt worden wäre, Folgendes akzeptabel:
T = TypeVar("T")
@dataclass
class Box(Generic[T = int]):
value: T | None = None
Zulassen, dass Nicht-Standardwerte auf Standardwerte folgen
YieldT = TypeVar("YieldT", default=Any)
SendT = TypeVar("SendT", default=Any)
ReturnT = TypeVar("ReturnT")
class Coroutine(Generic[YieldT, SendT, ReturnT]): ...
Coroutine[int] == Coroutine[Any, Any, int]
Das Zulassen von Nicht-Standardwerten nach Standardwerten würde die Probleme beim Rückgeben von Typen wie Coroutine aus Funktionen lindern, bei denen das am häufigsten verwendete Typ-Argument das letzte ist (die Rückgabe). Das Zulassen von Nicht-Standardwerten nach Standardwerten ist zu verwirrend und potenziell mehrdeutig, selbst wenn nur die beiden obigen Formen gültig wären. Das Ändern der Argumentreihenfolge würde auch viele Codebasen brechen. Dies ist auch in den meisten Fällen mit einem TypeAlias lösbar.
Coro: TypeAlias = Coroutine[Any, Any, T]
Coro[int] == Coroutine[Any, Any, int]
Implizites Setzen von default auf bound
In einer früheren Version dieser PEP wurde default implizit auf bound gesetzt, wenn kein Wert für default übergeben wurde. Dies war zwar praktisch, konnte aber dazu führen, dass ein Typparameter ohne Standardwert auf einen Typparameter mit einem Standardwert folgte. Betrachten Sie
T = TypeVar("T", bound=int) # default is implicitly int
U = TypeVar("U")
class Foo(Generic[T, U]):
...
# would expand to
T = TypeVar("T", bound=int, default=int)
U = TypeVar("U")
class Foo(Generic[T, U]):
...
Dies wäre auch eine Breaking Change für eine kleine Anzahl von Fällen gewesen, in denen der Code sich auf Any als impliziten Standardwert verlassen hat.
Zulassen, dass Typ-Parameter mit Standardwerten in Funktionssignaturen verwendet werden
Eine frühere Version dieser PEP erlaubte die Verwendung von TypeVarLikes mit Standardwerten in Funktionssignaturen. Dies wurde aus den in Funktions-Standardwerte beschriebenen Gründen entfernt. Hoffentlich kann dies in Zukunft hinzugefügt werden, wenn eine Möglichkeit zur Abfrage des Laufzeitwerts eines Typparameters hinzugefügt wird.
Zulassen von Typ-Parametern aus äußeren Gültigkeitsbereichen in default
Dies wurde als zu Nischenfunktion erachtet, um die zusätzliche Komplexität wert zu sein. Wenn Fälle auftreten, in denen dies erforderlich ist, kann es in einer zukünftigen PEP hinzugefügt werden.
Danksagungen
Dank der folgenden Personen für ihr Feedback zur PEP.
Eric Traut, Jelle Zijlstra, Joshua Butt, Danny Yamamoto, Kaylynn Morgan und Jakub Kuczys
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-0696.rst
Zuletzt geändert: 2024-09-03 17:24:02 GMT