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

Python Enhancement Proposals

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

Wichtig

Diese PEP ist ein historisches Dokument: siehe Standardwerte für Typparameter und Typparameterlisten für aktuelle Spezifikationen und Dokumentationen. Kanonische Tippspezifikationen werden auf der Website für Tippspezifikationen gepflegt; das Laufzeit-Tippverhalten wird in der CPython-Dokumentation beschrieben.

×

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

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’s dtype wäre float64. Derzeit ist es Unknown oder Any.
  • TensorFlow – dies könnte für Tensor ähnlich wie numpy.ndarray verwendet werden und wäre nützlich, um die Definition von Layer zu 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, ParamSpec und TypeVarTuple sollten den an default übergebenen Typ preisgeben. Dieser wäre als Attribut __default__ verfügbar, das None wäre, wenn kein Argument übergeben wird, und NoneType, wenn default=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


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

Zuletzt geändert: 2024-09-03 17:24:02 GMT