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

Python Enhancement Proposals

PEP 695 – Typparametersyntax

Autor:
Eric Traut <erictr at microsoft.com>
Sponsor:
Guido van Rossum <guido at python.org>
Discussions-To:
Typing-SIG-Thread
Status:
Final
Typ:
Standards Track
Thema:
Typisierung
Erstellt:
15. Juni 2022
Python-Version:
3.12
Post-History:
20. Juni 2022, 04. Dez. 2022
Resolution:
Discourse-Nachricht

Inhaltsverzeichnis

Wichtig

Dieses PEP ist ein historisches Dokument: siehe Varianzinferenz, Typaliase, Typparameterlisten und Die type-Anweisung und Anmerkungs-Geltungsbereiche. für aktuelle Spezifikationen und Dokumentation. Kanonische Tippspezifikationen werden auf der Website der Tippspezifikationen gepflegt; das Laufzeitverhalten von Tipps wird in der CPython-Dokumentation beschrieben.

×

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

Zusammenfassung

Dieses PEP spezifiziert eine verbesserte Syntax für die Angabe von Typparametern in einer generischen Klasse, Funktion oder einem Typalias. Es führt auch eine neue Anweisung zur Deklaration von Typaliase ein.

Motivation

PEP 484 führte Typvariablen in die Sprache ein. PEP 612 baute auf diesem Konzept auf, indem es Parameterspezifikationen einführte, und PEP 646 fügte variadische Typvariablen hinzu.

Obwohl generische Typen und Typparameter immer beliebter werden, fühlt sich die Syntax zur Angabe von Typparametern für Python immer noch "angeklebt" an. Dies ist eine Quelle der Verwirrung unter Python-Entwicklern.

Es besteht Konsens innerhalb der Python-Community für statische Typisierung, dass es an der Zeit ist, eine formale Syntax bereitzustellen, die anderen modernen Programmiersprachen ähnelt, die generische Typen unterstützen.

Eine Analyse von 25 beliebten typisierten Python-Bibliotheken ergab, dass Typvariablen (insbesondere das Symbol typing.TypeVar) in 14 % der Module verwendet wurden.

Unklarheiten

Obwohl die Verwendung von Typvariablen weit verbreitet ist, ist die Art und Weise, wie sie im Code angegeben werden, für viele Python-Entwickler eine Quelle der Verwirrung. Mehrere Faktoren tragen zu dieser Verwirrung bei.

Die Gültigkeitsbereichsregeln für Typvariablen sind schwer zu verstehen. Typvariablen werden typischerweise im globalen Gültigkeitsbereich zugewiesen, aber ihre semantische Bedeutung ist nur im Kontext einer generischen Klasse, Funktion oder eines Typalias gültig. Eine einzelne Laufzeitinstanz einer Typvariable kann in mehreren generischen Kontexten wiederverwendet werden und hat in jedem dieser Kontexte eine andere semantische Bedeutung. Dieses PEP schlägt vor, diese Quelle der Verwirrung zu beseitigen, indem Typparameter an einer natürlichen Stelle innerhalb einer Klassen-, Funktions- oder Typalias-Deklarationsanweisung deklariert werden.

Generische Typaliase werden oft missbraucht, da den Entwicklern nicht klar ist, dass ein Typargument bei der Verwendung des Typalias bereitgestellt werden muss. Dies führt zu einem impliziten Typargument von Any, was selten die Absicht ist. Dieses PEP schlägt die Einführung einer neuen Syntax vor, die generische Typaliasdeklarationen klar macht.

PEP 483 und PEP 484 führten das Konzept der "Varianz" für eine Typvariable ein, die innerhalb einer generischen Klasse verwendet wird. Typvariablen können invariant, kovariant oder kontravariant sein. Das Konzept der Varianz ist ein fortgeschrittenes Detail der Typentheorie, das von den meisten Python-Entwicklern nicht gut verstanden wird, dennoch müssen sie diesem Konzept heute begegnen, wenn sie ihre erste generische Klasse definieren. Dieses PEP eliminiert weitgehend die Notwendigkeit, dass die meisten Entwickler das Konzept der Varianz beim Definieren generischer Klassen verstehen.

Wenn mehr als ein Typparameter mit einer generischen Klasse oder einem Typalias verwendet wird, können die Regeln für die Reihenfolge der Typparameter verwirrend sein. Sie basiert normalerweise auf der Reihenfolge, in der sie zuerst in einer Klassen- oder Typaliasdeklarationsanweisung erscheinen. Dies kann jedoch in einer Klassendefinition überschrieben werden, indem eine Basisklasse "Generic" oder "Protocol" aufgenommen wird. Zum Beispiel in der Klassendeklaration class ClassA(Mapping[K, V]) sind die Typparameter als K und dann V geordnet. In der Klassendeklaration class ClassB(Mapping[K, V], Generic[V, K]) sind die Typparameter jedoch als V und dann K geordnet. Dieses PEP schlägt vor, die Reihenfolge der Typparameter in allen Fällen explizit zu machen.

Die Praxis, eine Typvariable über mehrere generische Kontexte hinweg zu teilen, schafft weitere Probleme. Moderne Editoren bieten Funktionen wie "Alle Referenzen finden" und "Alle Referenzen umbenennen", die auf Symbole auf semantischer Ebene angewendet werden. Wenn ein Typparameter zwischen mehreren generischen Klassen, Funktionen und Typaliase geteilt wird, sind alle Referenzen semantisch äquivalent.

Typvariablen, die im globalen Gültigkeitsbereich definiert sind, müssen auch einen Namen erhalten, der mit einem Unterstrich beginnt, um anzuzeigen, dass die Variable für das Modul privat ist. Global definierte Typvariablen erhalten auch oft Namen, die ihre Varianz angeben, was zu umständlichen Namen wie "_T_contra" und "_KT_co" führt. Die aktuellen Mechanismen zur Zuweisung von Typvariablen erfordern auch, dass der Entwickler einen redundanten Namen in Anführungszeichen angibt (z. B. T = TypeVar("T")). Dieses PEP eliminiert die Notwendigkeit des redundanten Namens und umständlicher Variablennamen.

Die Definition von Typparametern erfordert derzeit den Import der Symbole TypeVar und Generic aus dem Modul typing. In den letzten Python-Versionen gab es Anstrengungen, die Notwendigkeit des Imports von typing-Symbolen für häufige Anwendungsfälle zu eliminieren, und dieses PEP verfolgt dieses Ziel weiter.

Zusammenfassende Beispiele

Die Definition einer generischen Klasse sieht vor diesem PEP etwa so aus.

from typing import Generic, TypeVar

_T_co = TypeVar("_T_co", covariant=True, bound=str)

class ClassA(Generic[_T_co]):
    def method1(self) -> _T_co:
        ...

Mit der neuen Syntax sieht es so aus.

class ClassA[T: str]:
    def method1(self) -> T:
        ...

Hier ist ein Beispiel für eine generische Funktion heute.

from typing import TypeVar

_T = TypeVar("_T")

def func(a: _T, b: _T) -> _T:
    ...

Und die neue Syntax.

def func[T](a: T, b: T) -> T:
    ...

Hier ist ein Beispiel für einen generischen Typalias heute.

from typing import TypeAlias

_T = TypeVar("_T")

ListOrSet: TypeAlias = list[_T] | set[_T]

Und mit der neuen Syntax.

type ListOrSet[T] = list[T] | set[T]

Spezifikation

Typparameterdeklarationen

Hier ist eine neue Syntax zur Deklaration von Typparametern für generische Klassen, Funktionen und Typaliase. Die Syntax fügt Unterstützung für eine durch Kommas getrennte Liste von Typparametern in eckigen Klammern nach dem Namen der Klasse, Funktion oder des Typalias hinzu.

Einfache (nicht-variadische) Typvariablen werden mit einem unverzierten Namen deklariert. Variadische Typvariablen werden mit einem * vorangestellt (siehe PEP 646 für Details). Parameterspezifikationen werden mit einem ** vorangestellt (siehe PEP 612 für Details).

# This generic class is parameterized by a TypeVar T, a
# TypeVarTuple Ts, and a ParamSpec P.
class ChildClass[T, *Ts, **P]: ...

Es besteht keine Notwendigkeit, Generic als Basisklasse aufzunehmen. Seine Aufnahme als Basisklasse wird durch die Anwesenheit von Typparametern impliziert, und es wird automatisch in die Attribute __mro__ und __orig_bases__ für die Klasse aufgenommen. Die explizite Verwendung einer Generic-Basisklasse führt zu einem Laufzeitfehler.

class ClassA[T](Generic[T]): ...  # Runtime error

Eine Protocol-Basisklasse mit Typargumenten kann einen Laufzeitfehler verursachen. Typprüfer sollten in diesem Fall einen Fehler generieren, da die Verwendung von Typargumenten nicht erforderlich ist und die Reihenfolge der Typparameter für die Klasse nicht mehr durch ihre Reihenfolge in der Protocol-Basisklasse bestimmt wird.

class ClassA[S, T](Protocol): ... # OK

class ClassB[S, T](Protocol[S, T]): ... # Recommended type checker error

Typparameternamen innerhalb einer generischen Klasse, Funktion oder eines Typalias müssen innerhalb derselben Klasse, Funktion oder desselben Typalias eindeutig sein. Ein doppelter Name führt zu einem Syntaxfehler zur Kompilierzeit. Dies steht im Einklang mit der Anforderung, dass Parameternamen innerhalb einer Funktionssignatur eindeutig sein müssen.

class ClassA[T, *T]: ... # Syntax Error

def func1[T, **T](): ... # Syntax Error

Klassentypparameternamen werden gemangelt, wenn sie mit einem doppelten Unterstrich beginnen, um den Namensauflösungsmechanismus für innerhalb der Klasse verwendete Namen nicht zu verkomplizieren. Das Attribut __name__ des Typparameters enthält jedoch den nicht gemangelten Namen.

Spezifikation der Obergrenze

Für einen nicht-variadischen Typparameter kann eine "Obergrenze" vom Typ durch die Verwendung eines Typanmerkungsausdrucks angegeben werden. Wenn keine Obergrenze angegeben ist, wird die Obergrenze als object angenommen.

class ClassA[T: str]: ...

Der angegebene Obergrenzentyp muss einen Ausdruck verwenden, der in Typanmerkungen zulässig ist. Komplexere Ausdrucksformen sollten von einem Typprüfer als Fehler markiert werden. Geklammerte Vorwärtsreferenzen sind zulässig.

Der angegebene Obergrenzentyp muss konkret sein. Der Versuch, einen generischen Typ zu verwenden, sollte von einem Typprüfer als Fehler markiert werden. Dies steht im Einklang mit den bestehenden Regeln, die von Typprüfern für einen TypeVar-Konstruktoraufruf erzwungen werden.

class ClassA[T: dict[str, int]]: ...  # OK

class ClassB[T: "ForwardReference"]: ...  # OK

class ClassC[V]:
    class ClassD[T: dict[str, V]]: ...  # Type checker error: generic type

class ClassE[T: [str, int]]: ...  # Type checker error: illegal expression form

Spezifikation von eingeschränkten Typen

PEP 484 führte das Konzept einer "eingeschränkten Typvariable" ein, die auf eine Menge von zwei oder mehr Typen beschränkt ist. Die neue Syntax unterstützt diesen Arten von Einschränkungen durch die Verwendung eines literalen Tupelausdrucks, der zwei oder mehr Typen enthält.

class ClassA[AnyStr: (str, bytes)]: ...  # OK

class ClassB[T: ("ForwardReference", bytes)]: ...  # OK

class ClassC[T: ()]: ...  # Type checker error: two or more types required

class ClassD[T: (str, )]: ...  # Type checker error: two or more types required

t1 = (bytes, str)
class ClassE[T: t1]: ...  # Type checker error: literal tuple expression required

Wenn der angegebene Typ kein Tupelausdruck ist oder der Tupelausdruck komplexe Ausdrucksformen enthält, die in einer Typanmerkung nicht zulässig sind, sollte ein Typprüfer einen Fehler generieren. Geklammerte Vorwärtsreferenzen sind zulässig.

class ClassF[T: (3, bytes)]: ...  # Type checker error: invalid expression form

Die angegebenen eingeschränkten Typen müssen konkret sein. Der Versuch, einen generischen Typ zu verwenden, sollte von einem Typprüfer als Fehler markiert werden. Dies steht im Einklang mit den bestehenden Regeln, die von Typprüfern für einen TypeVar-Konstruktoraufruf erzwungen werden.

class ClassG[T: (list[S], str)]: ...  # Type checker error: generic type

Laufzeitdarstellung von Grenzen und Einschränkungen

Die Obergrenzen und Einschränkungen von TypeVar-Objekten sind zur Laufzeit über die Attribute __bound__ und __constraints__ zugänglich. Für TypeVar-Objekte, die durch die neue Syntax definiert wurden, werden diese Attribute als Lazy Evaluation ausgewertet, wie unter Lazy Evaluation unten erläutert.

Generischer Typalias

Wir schlagen die Einführung einer neuen Anweisung zur Deklaration von Typaliase vor. Ähnlich wie class und def-Anweisungen definiert eine type-Anweisung einen Gültigkeitsbereich für Typparameter.

# A non-generic type alias
type IntOrStr = int | str

# A generic type alias
type ListOrSet[T] = list[T] | set[T]

Typaliase können ohne die Verwendung von Anführungszeichen auf sich selbst verweisen.

# A type alias that includes a forward reference
type AnimalOrVegetable = Animal | "Vegetable"

# A generic self-referential type alias
type RecursiveList[T] = T | list[RecursiveList[T]]

Das Schlüsselwort type ist ein neues weiches Schlüsselwort. Es wird nur in diesem Teil der Grammatik als Schlüsselwort interpretiert. An allen anderen Stellen wird es als Bezeichnername angenommen.

Als Teil eines generischen Typalias deklarierte Typparameter sind nur bei der Auswertung der rechten Seite des Typalias gültig.

Wie bei typing.TypeAlias sollten Typprüfer den Ausdruck auf der rechten Seite auf Ausdrucksformen beschränken, die innerhalb von Typanmerkungen zulässig sind. Die Verwendung komplexerer Ausdrucksformen (Aufrufausdrücke, ternäre Operatoren, arithmetische Operatoren, Vergleichsoperatoren usw.) sollte als Fehler markiert werden.

Typalias-Ausdrücke dürfen keine traditionellen Typvariablen verwenden (d. h. solche, die mit einem expliziten TypeVar-Konstruktoraufruf zugewiesen wurden). Typprüfer sollten in diesem Fall einen Fehler generieren.

T = TypeVar("T")
type MyList = list[T]  # Type checker error: traditional type variable usage

Wir schlagen vor, die bestehende typing.TypeAlias, die in PEP 613 eingeführt wurde, als veraltet zu erklären. Die neue Syntax macht ihre Notwendigkeit vollständig überflüssig.

Laufzeitklasse für Typaliase

Zur Laufzeit erzeugt eine type-Anweisung eine Instanz von typing.TypeAliasType. Diese Klasse repräsentiert den Typ. Ihre Attribute umfassen

  • __name__ ist ein String, der den Namen des Typalias darstellt
  • __type_params__ ist ein Tupel von TypeVar, TypeVarTuple oder ParamSpec-Objekten, die den Typalias parametrisieren, wenn er generisch ist
  • __value__ ist der ausgewertete Wert des Typalias

Alle diese Attribute sind schreibgeschützt.

Der Wert des Typalias wird lazy ausgewertet (siehe Lazy Evaluation unten).

Gültigkeitsbereiche von Typparametern

Wenn die neue Syntax verwendet wird, wird ein neuer lexikalischer Gültigkeitsbereich eingeführt, und dieser Gültigkeitsbereich umfasst die Typparameter. Typparameter können innerhalb innerer Gültigkeitsbereiche unter ihrem Namen angesprochen werden. Wie bei anderen Symbolen in Python kann ein innerer Gültigkeitsbereich sein eigenes Symbol definieren, das ein Symbol des gleichen Namens aus einem äußeren Gültigkeitsbereich überschreibt. Dieser Abschnitt gibt eine verbale Beschreibung der neuen Geltungsbereichsregeln. Der Abschnitt Geltungsbereichsverhalten unten spezifiziert das Verhalten in Form einer Übersetzung in nahezu äquivalenten bestehenden Python-Code.

Typparameter sind für andere Typparameter sichtbar, die an anderer Stelle in der Liste deklariert sind. Dies ermöglicht es Typparametern, andere Typparameter in ihrer Definition zu verwenden. Obwohl es derzeit keinen Anwendungsfall für diese Funktionalität gibt, bewahrt sie die Möglichkeit, zukünftig Obergrenzausdrücke oder Standardwerte für Typargumente zu unterstützen, die von früheren Typparametern abhängen.

Ein Compilerfehler oder eine Laufzeitexception wird generiert, wenn die Definition eines früheren Typparameters einen späteren Typparameter referenziert, auch wenn der Name in einem äußeren Gültigkeitsbereich definiert ist.

# The following generates no compiler error, but a type checker
# should generate an error because an upper bound type must be concrete,
# and ``Sequence[S]`` is generic. Future extensions to the type system may
# eliminate this limitation.
class ClassA[S, T: Sequence[S]]: ...

# The following generates no compiler error, because the bound for ``S``
# is lazily evaluated. However, type checkers should generate an error.
class ClassB[S: Sequence[T], T]: ...

Ein Typparameter, der als Teil einer generischen Klasse deklariert ist, ist innerhalb des Klassenkörpers und der darin enthaltenen inneren Gültigkeitsbereiche gültig. Typparameter sind auch bei der Auswertung der Argumentenliste (Basisklassen und eventuelle Schlüsselwortargumente) zugänglich, die die Klassendefinition ausmachen. Dies ermöglicht es Basisklassen, durch diese Typparameter parametrisiert zu werden. Typparameter sind außerhalb des Klassenkörpers, einschließlich Klassen-Decorators, nicht zugänglich.

class ClassA[T](BaseClass[T], param = Foo[T]): ...  # OK

print(T)  # Runtime error: 'T' is not defined

@dec(Foo[T])  # Runtime error: 'T' is not defined
class ClassA[T]: ...

Ein Typparameter, der als Teil einer generischen Funktion deklariert ist, ist innerhalb des Funktionskörpers und aller darin enthaltenen Gültigkeitsbereiche gültig. Er ist auch innerhalb von Parameter- und Rückgabetypanmerkungen gültig. Standardargumentwerte für Funktionsparameter werden außerhalb dieses Gültigkeitsbereichs ausgewertet, sodass Typparameter in Ausdrücken für Standardwerte nicht zugänglich sind. Ebenso sind Typparameter nicht für Funktionsdekoder im Gültigkeitsbereich.

def func1[T](a: T) -> T: ...  # OK

print(T)  # Runtime error: 'T' is not defined

def func2[T](a = list[T]): ...  # Runtime error: 'T' is not defined

@dec(list[T])  # Runtime error: 'T' is not defined
def func3[T](): ...

Ein Typparameter, der als Teil eines generischen Typalias deklariert ist, ist innerhalb des Typalias-Ausdrucks gültig.

type Alias1[K, V] = Mapping[K, V] | Sequence[K]

Typparametersymbole, die in äußeren Gültigkeitsbereichen definiert sind, können nicht mit nonlocal-Anweisungen in inneren Gültigkeitsbereichen gebunden werden.

S = 0

def outer1[S]():
    S = 1
    T = 1

    def outer2[T]():

        def inner1():
            nonlocal S  # OK because it binds variable S from outer1
            nonlocal T  # Syntax error: nonlocal binding not allowed for type parameter

        def inner2():
            global S  # OK because it binds variable S from global scope

Der durch die neue Typparametersyntax eingeführte lexikalische Gültigkeitsbereich unterscheidet sich von traditionellen Gültigkeitsbereichen, die durch eine def- oder class-Anweisung eingeführt werden. Ein Typparameter-Gültigkeitsbereich fungiert eher wie eine temporäre "Überlagerung" des enthaltenden Gültigkeitsbereichs. Die einzigen neuen Symbole, die in seiner Symboltabelle enthalten sind, sind die mit der neuen Syntax definierten Typparameter. Referenzen auf alle anderen Symbole werden so behandelt, als ob sie im enthaltenden Gültigkeitsbereich gefunden worden wären. Dies ermöglicht es Basisklassenlisten (in Klassendefinitionen) und Typanmerkungsausdrücken (in Funktionsdefinitionen), Symbole zu referenzieren, die im enthaltenden Gültigkeitsbereich definiert sind.

class Outer:
    class Private:
        pass

    # If the type parameter scope was like a traditional scope,
    # the base class 'Private' would not be accessible here.
    class Inner[T](Private, Sequence[T]):
        pass

    # Likewise, 'Inner' would not be available in these type annotations.
    def method1[T](self, a: Inner[T]) -> Inner[T]:
        return a

Der Compiler erlaubt es inneren Gültigkeitsbereichen, ein lokales Symbol zu definieren, das einen äußeren Typparameter überschreibt.

Im Einklang mit den Geltungsbereichsregeln, die in PEP 484 definiert sind, sollten Typprüfer einen Fehler generieren, wenn inner-scope generische Klassen, Funktionen oder Typaliase denselben Typparameternamen wie ein äußerer Geltungsbereich wiederverwenden.

T = 0

@decorator(T)  # Argument expression `T` evaluates to 0
class ClassA[T](Sequence[T]):
    T = 1

    # All methods below should result in a type checker error
    # "type parameter 'T' already in use" because they are using the
    # type parameter 'T', which is already in use by the outer scope
    # 'ClassA'.
    def method1[T](self):
        ...

    def method2[T](self, x = T):  # Parameter 'x' gets default value of 1
        ...

    def method3[T](self, x: T):  # Parameter 'x' has type T (scoped to method3)
        ...

Symbole, auf die in inneren Gültigkeitsbereichen verwiesen wird, werden nach den bestehenden Regeln aufgelöst, mit der Ausnahme, dass auch Typparameter-Gültigkeitsbereiche bei der Namensauflösung berücksichtigt werden.

T = 0

# T refers to the global variable
print(T)  # Prints 0

class Outer[T]:
    T = 1

    # T refers to the local variable scoped to class 'Outer'
    print(T)  # Prints 1

    class Inner1:
        T = 2

        # T refers to the local type variable within 'Inner1'
        print(T)  # Prints 2

        def inner_method(self):
            # T refers to the type parameter scoped to class 'Outer';
            # If 'Outer' did not use the new type parameter syntax,
            # this would instead refer to the global variable 'T'
            print(T)  # Prints 'T'

    def outer_method(self):
        T = 3

        # T refers to the local variable within 'outer_method'
        print(T)  # Prints 3

        def inner_func():
            # T refers to the variable captured from 'outer_method'
            print(T)  # Prints 3

Wenn die neue Typparametersyntax für eine generische Klasse verwendet wird, sind Zuweisungsausdrücke in der Argumentenliste für die Klassendefinition nicht zulässig. Ebenso sind bei Funktionen, die die neue Typparametersyntax verwenden, Zuweisungsausdrücke in Parameter- oder Rückgabetypanmerkungen nicht zulässig, noch sind sie in dem Ausdruck zulässig, der einen Typalias definiert, oder in den Grenzen und Einschränkungen eines TypeVar. Ebenso sind yield, yield from und await-Ausdrücke in diesen Kontexten nicht zulässig.

Diese Einschränkung ist notwendig, da Ausdrücke, die innerhalb des neuen lexikalischen Gültigkeitsbereichs ausgewertet werden, keine Symbole innerhalb dieses Gültigkeitsbereichs einführen sollten, abgesehen von den definierten Typparametern, und nicht beeinflussen sollten, ob die umschließende Funktion ein Generator oder eine Koroutine ist.

class ClassA[T]((x := Sequence[T])): ...  # Syntax error: assignment expression not allowed

def func1[T](val: (x := int)): ...  # Syntax error: assignment expression not allowed

def func2[T]() -> (x := Sequence[T]): ...  # Syntax error: assignment expression not allowed

type Alias1[T] = (x := list[T])  # Syntax error: assignment expression not allowed

Zugriff auf Typparameter zur Laufzeit

Ein neues Attribut namens __type_params__ ist für generische Klassen, Funktionen und Typaliase verfügbar. Dieses Attribut ist ein Tupel der Typparameter, die die Klasse, Funktion oder den Alias parametrisieren. Das Tupel enthält Instanzen von TypeVar, ParamSpec und TypeVarTuple.

Typparameter, die mit der neuen Syntax deklariert wurden, erscheinen nicht im Wörterbuch, das von globals() oder locals() zurückgegeben wird.

Varianzinferenz

Dieses PEP eliminiert die Notwendigkeit, Varianz für Typparameter anzugeben. Stattdessen werden Typprüfer die Varianz von Typparametern basierend auf ihrer Verwendung innerhalb einer Klasse ableiten. Typparameter werden als invariant, kovariant oder kontravariant abgeleitet, je nachdem, wie sie verwendet werden.

Python-Typprüfer verfügen bereits über die Fähigkeit, die Varianz von Typparametern zu bestimmen, um die Varianz innerhalb einer generischen Protokollklasse zu validieren. Diese Funktionalität kann für alle Klassen (unabhängig davon, ob es sich um Protokolle handelt oder nicht) verwendet werden, um die Varianz jedes Typparameters zu berechnen.

Der Algorithmus zur Berechnung der Varianz eines Typparameters ist wie folgt.

Für jeden Typparameter in einer generischen Klasse

1. Wenn der Typparameter variadisch (TypeVarTuple) oder eine Parameterspezifikation (ParamSpec) ist, wird er immer als invariant betrachtet. Keine weitere Inferenz erforderlich.

2. Wenn der Typparameter aus einer traditionellen TypeVar-Deklaration stammt und nicht als infer_variance (siehe unten) angegeben ist, wird seine Varianz durch den TypeVar-Konstruktoraufruf bestimmt. Keine weitere Inferenz erforderlich.

3. Erstellen Sie zwei spezialisierte Versionen der Klasse. Wir werden diese als upper und lower Spezialisierungen bezeichnen. In beiden dieser Spezialisierungen ersetzen Sie alle Typparameter außer dem, der gerade abgeleitet wird, durch eine Dummy-Typinstanz (eine konkrete anonyme Klasse, die mit sich selbst typkompatibel ist und die Grenzen oder Einschränkungen des Typparameters erfüllt). In der upper spezialisierten Klasse spezialisieren Sie den Ziel-Typparameter mit einer object-Instanz. Diese Spezialisierung ignoriert die Obergrenze oder Einschränkungen des Typparameters. In der lower spezialisierten Klasse spezialisieren Sie den Ziel-Typparameter mit sich selbst (d. h. das entsprechende Typargument ist der Typparameter selbst).

4. Bestimmen Sie, ob lower auf upper mit normalen Typkompatibilitätsregeln zugewiesen werden kann. Wenn ja, ist der Ziel-Typparameter kovariant. Wenn nicht, bestimmen Sie, ob upper auf lower zugewiesen werden kann. Wenn ja, ist der Ziel-Typparameter kontravariant. Wenn keine dieser Kombinationen zuweisbar ist, ist der Ziel-Typparameter invariant.

Hier ist ein Beispiel.

class ClassA[T1, T2, T3](list[T1]):
    def method1(self, a: T2) -> None:
        ...

    def method2(self) -> T3:
        ...

Um die Varianz von T1 zu bestimmen, spezialisieren wir ClassA wie folgt

upper = ClassA[object, Dummy, Dummy]
lower = ClassA[T1, Dummy, Dummy]

Wir stellen fest, dass upper nicht auf lower mit den normalen Typkompatibilitätsregeln gemäß PEP 484 zugewiesen werden kann. Ebenso kann lower nicht auf upper zugewiesen werden, also schließen wir, dass T1 invariant ist.

Um die Varianz von T2 zu bestimmen, spezialisieren wir ClassA wie folgt

upper = ClassA[Dummy, object, Dummy]
lower = ClassA[Dummy, T2, Dummy]

Da upper auf lower zugewiesen werden kann, ist T2 kontravariant.

Um die Varianz von T3 zu bestimmen, spezialisieren wir ClassA wie folgt

upper = ClassA[Dummy, Dummy, object]
lower = ClassA[Dummy, Dummy, T3]

Da lower auf upper zugewiesen werden kann, ist T3 kovariant.

Automatische Varianz für TypeVar

Die bestehende TypeVar-Klassenkonstruktor akzeptiert Schlüsselwortparameter namens covariant und contravariant. Wenn beide False sind, wird die Typvariable als invariant angenommen. Wir schlagen die Hinzufügung eines weiteren Schlüsselwortparameters namens infer_variance vor, der angibt, dass ein Typprüfer die Inferenz verwenden soll, um zu bestimmen, ob die Typvariable invariant, kovariant oder kontravariant ist. Eine entsprechende Instanzvariable __infer_variance__ kann zur Laufzeit abgerufen werden, um zu bestimmen, ob die Varianz abgeleitet wird. Typvariablen, die implizit mit der neuen Syntax zugewiesen werden, haben immer __infer_variance__ auf True gesetzt.

Eine generische Klasse, die die traditionelle Syntax verwendet, kann Kombinationen von Typvariablen mit expliziter und abgeleiteter Varianz enthalten.

T1 = TypeVar("T1", infer_variance=True)  # Inferred variance
T2 = TypeVar("T2")  # Invariant
T3 = TypeVar("T3", covariant=True)  # Covariant

# A type checker should infer the variance for T1 but use the
# specified variance for T2 and T3.
class ClassA(Generic[T1, T2, T3]): ...

Kompatibilität mit traditionellen TypeVars

Der bestehende Mechanismus zur Zuweisung von TypeVar, TypeVarTuple und ParamSpec bleibt aus Kompatibilitätsgründen erhalten. Diese "traditionellen" Typvariablen sollten jedoch nicht mit Typparametern kombiniert werden, die mit der neuen Syntax zugewiesen wurden. Eine solche Kombination sollte von Typprüfern als Fehler markiert werden. Dies ist notwendig, da die Reihenfolge der Typparameter mehrdeutig ist.

Es ist in Ordnung, traditionelle Typvariablen mit neuen Typparametern zu kombinieren, wenn die Klasse, Funktion oder der Typalias nicht die neue Syntax verwendet. Die neuen Typparameter müssen in diesem Fall aus einem äußeren Gültigkeitsbereich stammen.

K = TypeVar("K")

class ClassA[V](dict[K, V]): ...  # Type checker error

class ClassB[K, V](dict[K, V]): ...  # OK

class ClassC[V]:
    # The use of K and V for "method1" is OK because it uses the
    # "traditional" generic function mechanism where type parameters
    # are implicit. In this case V comes from an outer scope (ClassC)
    # and K is introduced implicitly as a type parameter for "method1".
    def method1(self, a: V, b: K) -> V | K: ...

    # The use of M and K are not allowed for "method2". A type checker
    # should generate an error in this case because this method uses the
    # new syntax for type parameters, and all type parameters associated
    # with the method must be explicitly declared. In this case, ``K``
    # is not declared by "method2", nor is it supplied by a new-style
    # type parameter defined in an outer scope.
    def method2[M](self, a: M, b: K) -> M | K: ...

Laufzeitimplementierung

Grammatikänderungen

Dieses PEP führt ein neues weiches Schlüsselwort type ein. Es modifiziert die Grammatik auf folgende Weise

  1. Hinzufügen einer optionalen Typparameterklausel in class und def-Anweisungen.
type_params: '[' t=type_param_seq  ']'

type_param_seq: a[asdl_typeparam_seq*]=','.type_param+ [',']

type_param:
    | a=NAME b=[type_param_bound]
    | '*' a=NAME
    | '**' a=NAME

type_param_bound: ":" e=expression

# Grammar definitions for class_def_raw and function_def_raw are modified
# to reference type_params as an optional syntax element. The definitions
# of class_def_raw and function_def_raw are simplified here for brevity.

class_def_raw: 'class' n=NAME t=[type_params] ...

function_def_raw: a=[ASYNC] 'def' n=NAME t=[type_params] ...
  1. Hinzufügen einer neuen type-Anweisung zur Definition von Typaliase.
type_alias: "type" n=NAME t=[type_params] '=' b=expression

AST-Änderungen

Dieses PEP führt einen neuen AST-Knotentyp namens TypeAlias ein.

TypeAlias(expr name, typeparam* typeparams, expr value)

Es fügt auch einen AST-Knotentyp hinzu, der einen Typparameter repräsentiert.

typeparam = TypeVar(identifier name, expr? bound)
    | ParamSpec(identifier name)
    | TypeVarTuple(identifier name)

Grenzen und Einschränkungen werden im AST identisch dargestellt. In der Implementierung wird jeder Ausdruck, der ein Tuple AST-Knoten ist, als Einschränkung behandelt, und jeder andere Ausdruck wird als Grenze behandelt.

Es modifiziert auch bestehende AST-Knotentypen FunctionDef, AsyncFunctionDef und ClassDef, um ein zusätzliches optionales Attribut namens typeparams aufzunehmen, das eine Liste von Typparametern enthält, die der Funktion oder Klasse zugeordnet sind.

Lazy Evaluation

Dieses PEP führt drei neue Kontexte ein, in denen Ausdrücke auftreten können, die statische Typen darstellen: TypeVar-Grenzen, TypeVar-Einschränkungen und der Wert von Typaliase. Diese Ausdrücke können Referenzen auf noch nicht definierte Namen enthalten. Beispielsweise können Typaliase rekursiv oder sogar gegenseitig rekursiv sein, und Typvariablengrenzen können auf die aktuelle Klasse zurückverweisen. Wenn diese Ausdrücke eifrig ausgewertet würden, müssten Benutzer solche Ausdrücke in Anführungszeichen setzen, um Laufzeitfehler zu vermeiden. PEP 563 und PEP 649 beschreiben die Probleme dieser Situation für Typanmerkungen.

Um eine ähnliche Situation mit der neuen, in diesem PEP vorgeschlagenen Syntax zu vermeiden, schlagen wir die Verwendung von Lazy Evaluation für diese Ausdrücke vor, ähnlich dem Ansatz in PEP 649. Insbesondere wird jeder Ausdruck in einem Code-Objekt gespeichert, und das Code-Objekt wird nur dann ausgewertet, wenn auf das entsprechende Attribut zugegriffen wird (TypeVar.__bound__, TypeVar.__constraints__ oder TypeAlias.__value__). Nachdem der Wert erfolgreich ausgewertet wurde, wird der Wert gespeichert und spätere Aufrufe geben denselben Wert zurück, ohne das Code-Objekt erneut auszuwerten.

Wenn PEP 649 implementiert wird, sollten zusätzliche Auswertungsmechanismen hinzugefügt werden, um die Optionen zu spiegeln, die PEP für Anmerkungen bietet. In der aktuellen Version des PEP könnte dies die Hinzufügung einer Methode __evaluate_bound__ zu TypeVar umfassen, die einen Parameter format mit derselben Bedeutung wie in PEP 649's __annotate__-Methode (und einer ähnlichen Methode __evaluate_constraints__ sowie einer Methode __evaluate_value__ für TypeAliasType) aufweist. Bis PEP 649 jedoch akzeptiert und implementiert ist, wird nur das Standardauswertungsformat (PEP 649's "VALUE"-Format) unterstützt.

Als Konsequenz der Lazy Evaluation kann der Wert, der für ein Attribut beobachtet wird, davon abhängen, wann auf das Attribut zugegriffen wird.

X = int

class Foo[T: X, U: X]:
    t, u = T, U

print(Foo.t.__bound__)  # prints "int"
X = str
print(Foo.u.__bound__)  # prints "str"

Ähnliche Beispiele, die Typanmerkungen beeinflussen, können unter Verwendung der Semantik von PEP 563 oder PEP 649 konstruiert werden.

Eine naive Implementierung der Lazy Evaluation würde Klassen-Namensräume falsch behandeln, da Funktionen innerhalb einer Klasse normalerweise keinen Zugriff auf den umschließenden Klassen-Namensraum haben. Die Implementierung wird eine Referenz auf den Klassen-Namensraum beibehalten, damit klassenbezogene Namen korrekt aufgelöst werden.

Geltungsbereichsverhalten

Die neue Syntax erfordert eine neue Art von Gültigkeitsbereich, die sich anders verhält als bestehende Gültigkeitsbereiche in Python. Daher kann die neue Syntax nicht exakt in Bezug auf bestehende Python-Geltungsbereichsverhalten beschrieben werden. Dieser Abschnitt spezifiziert diese Gültigkeitsbereiche weiter durch Verweis auf bestehendes Geltungsbereichsverhalten: Die neuen Gültigkeitsbereiche verhalten sich wie Funktions-Gültigkeitsbereiche, mit Ausnahme einer Reihe von kleineren Unterschieden, die unten aufgeführt sind.

Alle Beispiele enthalten Funktionen, die mit dem Pseudo-Schlüsselwort def695 eingeführt wurden. Dieses Schlüsselwort wird in der tatsächlichen Sprache nicht existieren; es wird verwendet, um klarzustellen, dass die neuen Gültigkeitsbereiche größtenteils wie Funktions-Gültigkeitsbereiche sind.

def695-Gültigkeitsbereiche unterscheiden sich aus folgenden Gründen von regulären Funktions-Gültigkeitsbereichen

  • Wenn ein def695-Gültigkeitsbereich unmittelbar innerhalb eines Klassen-Gültigkeitsbereichs oder innerhalb eines anderen def695-Gültigkeitsbereichs liegt, der unmittelbar innerhalb eines Klassen-Gültigkeitsbereichs liegt, dann können in diesem Klassen-Gültigkeitsbereich definierte Namen innerhalb des def695-Gültigkeitsbereichs angesprochen werden. (Reguläre Funktionen können im Gegensatz dazu nicht auf Namen zugreifen, die in einem umschließenden Klassen-Gültigkeitsbereich definiert sind.)
  • Die folgenden Konstrukte sind direkt innerhalb eines def695-Gültigkeitsbereichs nicht zulässig, obwohl sie innerhalb anderer Gültigkeitsbereiche, die innerhalb eines def695-Gültigkeitsbereichs verschachtelt sind, verwendet werden können
    • yield
    • yield from
    • await
    • := (Walrus-Operator)
  • Der qualifizierte Name (__qualname__) von Objekten (Klassen und Funktionen), die innerhalb von def695-Scopes definiert sind, ist so, als ob die Objekte innerhalb des nächstgelegenen umschließenden Scopes definiert worden wären.
  • Namen, die innerhalb von def695-Scopes gebunden sind, können mit einer nonlocal-Anweisung in verschachtelten Scopes nicht neu gebunden werden.

def695-Scopes werden für die Auswertung mehrerer neuer syntaktischer Konstrukte verwendet, die in diesem PEP vorgeschlagen werden. Einige werden eifrig ausgewertet (wenn ein Typalias, eine Funktion oder eine Klasse definiert wird); andere werden träge ausgewertet (nur, wenn die Auswertung explizit angefordert wird). In allen Fällen sind die Scoping-Semantiken identisch

  • Eifrig ausgewertete Werte
    • Die Typparameter generischer Typaliase
    • Die Typparameter und Annotationen generischer Funktionen
    • Die Typparameter und Basisklassen-Ausdrücke generischer Klassen
  • Träge ausgewertete Werte
    • Der Wert generischer Typaliase
    • Die Grenzen von Typvariablen
    • Die Einschränkungen von Typvariablen

In den folgenden Übersetzungen sind Namen, die mit zwei Unterstrichen beginnen, implementierungsinterne Details und für tatsächlichen Python-Code nicht sichtbar. Wir verwenden die folgenden intrinsischen Funktionen, die in der tatsächlichen Implementierung direkt im Interpreter definiert sind

  • __make_typealias(*, name, type_params=(), evaluate_value): Erstellt ein neues typing.TypeAlias-Objekt mit dem angegebenen Namen, den Typparametern und einem träge ausgewerteten Wert. Der Wert wird erst ausgewertet, wenn auf das Attribut __value__ zugegriffen wird.
  • __make_typevar_with_bound(*, name, evaluate_bound): Erstellt ein neues typing.TypeVar-Objekt mit dem angegebenen Namen und einer träge ausgewerteten Grenze. Die Grenze wird erst ausgewertet, wenn auf das Attribut __bound__ zugegriffen wird.
  • __make_typevar_with_constraints(*, name, evaluate_constraints): Erstellt ein neues typing.TypeVar-Objekt mit dem angegebenen Namen und träge ausgewerteten Einschränkungen. Die Einschränkungen werden erst ausgewertet, wenn auf das Attribut __constraints__ zugegriffen wird.

Nicht-generische Typaliase werden wie folgt übersetzt

type Alias = int

Entspricht

def695 __evaluate_Alias():
    return int

Alias = __make_typealias(name='Alias', evaluate_value=__evaluate_Alias)

Generische Typaliase

type Alias[T: int] = list[T]

Entspricht

def695 __generic_parameters_of_Alias():
    def695 __evaluate_T_bound():
        return int
    T = __make_typevar_with_bound(name='T', evaluate_bound=__evaluate_T_bound)

    def695 __evaluate_Alias():
        return list[T]
    return __make_typealias(name='Alias', type_params=(T,), evaluate_value=__evaluate_Alias)

Alias = __generic_parameters_of_Alias()

Generische Funktionen

def f[T](x: T) -> T:
    return x

Entspricht

def695 __generic_parameters_of_f():
    T = typing.TypeVar(name='T')

    def f(x: T) -> T:
        return x
    f.__type_params__ = (T,)
    return f

f = __generic_parameters_of_f()

Ein umfassenderes Beispiel für generische Funktionen, das das Scoping-Verhalten von Standardwerten, Decorators und Grenzen veranschaulicht. Beachten Sie, dass dieses Beispiel ParamSpec nicht korrekt verwendet, und daher von einem statischen Typ-Checker abgelehnt werden sollte. Es ist jedoch zur Laufzeit gültig und wird hier verwendet, um die Laufzeit-Semantik zu veranschaulichen.

@decorator
def f[T: int, U: (int, str), *Ts, **P](
    x: T = SOME_CONSTANT,
    y: U,
    *args: *Ts,
    **kwargs: P.kwargs,
) -> T:
    return x

Entspricht

__default_of_x = SOME_CONSTANT  # evaluated outside the def695 scope
def695 __generic_parameters_of_f():
    def695 __evaluate_T_bound():
        return int
    T = __make_typevar_with_bound(name='T', evaluate_bound=__evaluate_T_bound)

    def695 __evaluate_U_constraints():
        return (int, str)
    U = __make_typevar_with_constraints(name='U', evaluate_constraints=__evaluate_U_constraints)

    Ts = typing.TypeVarTuple("Ts")
    P = typing.ParamSpec("P")

    def f(x: T = __default_of_x, y: U, *args: *Ts, **kwargs: P.kwargs) -> T:
        return x
    f.__type_params__ = (T, U, Ts, P)
    return f

f = decorator(__generic_parameters_of_f())

Generische Klassen

class C[T](Base):
    def __init__(self, x: T):
        self.x = x

Entspricht

def695 __generic_parameters_of_C():
    T = typing.TypeVar('T')
    class C(Base):
        __type_params__ = (T,)
        def __init__(self, x: T):
            self.x = x
   return C

C = __generic_parameters_of_C()

Die größte Abweichung vom bestehenden Verhalten für def695-Scopes ist das Verhalten innerhalb von Klassenscopes. Diese Abweichung ist notwendig, damit innerhalb von Klassen definierte Generics intuitiv funktionieren

class C:
    class Nested: ...
    def generic_method[T](self, x: T, y: Nested) -> T: ...

Entspricht

class C:
    class Nested: ...

    def695 __generic_parameters_of_generic_method():
        T = typing.TypeVar('T')

        def generic_method(self, x: T, y: Nested) -> T: ...
        return generic_method

    generic_method = __generic_parameters_of_generic_method()

In diesem Beispiel werden die Annotationen für x und y innerhalb eines def695-Scopes ausgewertet, da sie Zugriff auf den Typparameter T für die generische Methode benötigen. Sie benötigen jedoch auch Zugriff auf den Namen Nested, der innerhalb des Klassennamensraums definiert ist. Wenn def695-Scopes wie normale Funktions-Scopes agieren würden, wäre Nested nicht innerhalb des Funktions-Scopes sichtbar. Daher haben def695-Scopes, die sich unmittelbar innerhalb von Klassenscopes befinden, Zugriff auf diesen Klassenscope, wie oben beschrieben.

Bibliotheksänderungen

Mehrere Klassen im typing-Modul, die derzeit in Python implementiert sind, müssen teilweise in C implementiert werden. Dazu gehören TypeVar, TypeVarTuple, ParamSpec und Generic sowie die neue Klasse TypeAliasType (oben beschrieben). Die Implementierung kann auf die Python-Version von typing.py delegieren, für einige Verhaltensweisen, die stark mit dem Rest des Moduls interagieren. Die dokumentierten Verhaltensweisen dieser Klassen dürfen sich nicht ändern.

Referenzimplementierung

Dieser Vorschlag wird in CPython PR #103764 prototypisiert.

Der Pyright-Typ-Checker unterstützt das in diesem PEP beschriebene Verhalten.

Abgelehnte Ideen

Präfix-Klausel

Wir haben verschiedene syntaktische Optionen für die Angabe von Typparametern untersucht, die def und class Anweisungen vorangestellt sind. Eine solche Variante, die wir in Betracht gezogen haben, verwendete eine using-Klausel wie folgt

using S, T
class ClassA: ...

Diese Option wurde abgelehnt, da die Scoping-Regeln für die Typparameter weniger klar waren. Außerdem interagierte diese Syntax nicht gut mit Klassen- und Funktions-Decorators, die in Python üblich sind. Nur eine andere populäre Programmiersprache, C++, verwendet diesen Ansatz.

Wir haben ebenfalls Präfixformen in Betracht gezogen, die wie Decorators aussahen (z.B. @using(S, T)). Diese Idee wurde abgelehnt, da solche Formen mit regulären Decorators verwechselt würden und sie nicht gut mit bestehenden Decorators komponierbar wären. Darüber hinaus werden Decorators logisch nach der Anweisung ausgeführt, die sie dekorieren. Daher wäre es verwirrend, wenn sie Symbole (Typparameter) einführen würden, die innerhalb der „dekorierten“ Anweisung sichtbar sind, die logisch vor dem Decorator selbst ausgeführt wird.

Winkelklammern

Viele Sprachen, die Generics unterstützen, verwenden Winkelklammern. (Siehe Tabelle am Ende von Anhang A für eine Zusammenfassung.) Wir haben die Verwendung von Winkelklammern für Typparameterdeklarationen in Python untersucht, haben sie aber letztendlich aus zwei Gründen abgelehnt. Erstens werden Winkelklammern vom Python-Scanner nicht als „gepaart“ betrachtet, sodass Zeilenende-Zeichen zwischen einem < und > Token beibehalten werden. Das bedeutet, dass jede Zeilenunterbrechung innerhalb einer Liste von Typparametern die Verwendung von unschönen und umständlichen \-Escape-Sequenzen erfordern würde. Zweitens hat Python bereits die Verwendung von eckigen Klammern für die explizite Spezialisierung eines generischen Typs etabliert (z.B. list[int]). Wir kamen zu dem Schluss, dass es inkonsistent und verwirrend wäre, Winkelklammern für generische Deklarationen, aber eckige Klammern für explizite Spezialisierungen zu verwenden. Alle anderen Sprachen, die wir untersucht haben, waren diesbezüglich konsistent.

Syntax für Grenzen

Wir haben verschiedene syntaktische Optionen für die Angabe von Grenzen und Einschränkungen für eine Typvariable untersucht. Wir haben die Verwendung eines <:-Tokens wie in Scala, die Verwendung eines extends oder with-Schlüsselworts wie in verschiedenen anderen Sprachen und die Verwendung einer Funktionsaufrufsyntax ähnlich dem heutigen typing.TypeVar-Konstruktor in Betracht gezogen, aber letztendlich abgelehnt. Die einfache Doppelpunkt-Syntax ist konsistent mit vielen anderen Programmiersprachen (siehe Anhang A) und wurde von einem Querschnitt der befragten Python-Entwickler stark bevorzugt.

Explizite Varianz

Wir haben erwogen, eine Syntax für die Angabe hinzuzufügen, ob ein Typparameter für invariant, kovariant oder kontravariant bestimmt ist. Der typing.TypeVar-Mechanismus in Python erfordert dies. Einige andere Sprachen, darunter Scala und C#, verlangen ebenfalls, dass Entwickler die Varianz angeben. Wir haben diese Idee abgelehnt, da Varianz im Allgemeinen abgeleitet werden kann und die meisten modernen Programmiersprachen die Varianz basierend auf der Verwendung ableiten. Varianz ist ein fortgeschrittenes Thema, das viele Entwickler verwirrend finden, daher möchten wir die Notwendigkeit, dieses Konzept für die meisten Python-Entwickler zu verstehen, eliminieren.

Namens-Mangling

Bei der Betrachtung von Implementierungsoptionen haben wir einen „Name-Mangling“-Ansatz in Betracht gezogen, bei dem jedem Typparameter ein eindeutiger „mangelter“ Name vom Compiler zugewiesen wurde. Dieser gemangelte Name basierte auf dem qualifizierten Namen der generischen Klasse, Funktion oder des Typalias, mit dem er assoziiert war. Dieser Ansatz wurde abgelehnt, da qualifizierte Namen nicht unbedingt eindeutig sind, was bedeutet, dass der gemangelte Name auf einem anderen zufälligen Wert basieren müsste. Darüber hinaus ist dieser Ansatz nicht kompatibel mit Techniken, die zur Auswertung von zitierten (vorwärts referenzierten) Typannotationen verwendet werden.

Anhang A: Übersicht über die Typparametersyntax

Unterstützung für generische Typen findet sich in vielen Programmiersprachen. In diesem Abschnitt bieten wir eine Übersicht über die Optionen, die von anderen populären Programmiersprachen verwendet werden. Dies ist relevant, da die Vertrautheit mit anderen Sprachen es Python-Entwicklern erleichtern wird, dieses Konzept zu verstehen. Wir liefern hier zusätzliche Details (z.B. Unterstützung für Standard-Typargumente), die bei der Betrachtung zukünftiger Erweiterungen des Python-Typsystems nützlich sein können.

C++

C++ verwendet Winkelklammern in Kombination mit den Schlüsselwörtern template und typename, um Typparameter zu deklarieren. Es verwendet Winkelklammern für die Spezialisierung.

C++20 führte den Begriff der allgemeinen Einschränkungen ein, die wie Protokolle in Python wirken können. Eine Sammlung von Einschränkungen kann in einer benannten Entität namens concept definiert werden.

Varianz wird nicht explizit angegeben, aber Einschränkungen können Varianz erzwingen.

Ein Standard-Typargument kann mit dem Operator = angegeben werden.

// Generic class
template <typename T>
class ClassA
{
    // Constraints are supported through compile-time assertions.
    static_assert(std::is_base_of<BaseClass, T>::value);

public:
    Container<T> t;
};

// Generic function with default type argument
template <typename S = int>
S func1(ClassA<S> a, S b) {};

// C++20 introduced a more generalized notion of "constraints"
// and "concepts", which are named constraints.

// A sample concept
template<typename T>
concept Hashable = requires(T a)
{
    { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};

// Use of a concept in a template
template<Hashable T>
void func2(T value) {}

// Alternative use of concept
template<typename T> requires Hashable<T>
void func3(T value) {}

// Alternative use of concept
template<typename T>
void func3(T value) requires Hashable<T> {}

Java

Java verwendet Winkelklammern, um Typparameter und für die Spezialisierung zu deklarieren. Standardmäßig sind Typparameter invariant. Das Schlüsselwort extends wird verwendet, um eine Obergrenze anzugeben. Das Schlüsselwort super wird verwendet, um eine kontravariante Grenze anzugeben.

Java verwendet Use-Site-Varianz. Der Compiler legt Grenzen fest, welche Methoden und Member basierend auf der Verwendung eines generischen Typs zugegriffen werden können. Varianz wird nicht explizit angegeben.

Java bietet keine Möglichkeit, ein Standard-Typargument anzugeben.

// Generic class
public class ClassA<T> {
    public Container<T> t;

    // Generic method
    public <S extends Number> void method1(S value) { }

    // Use site variance
    public void method1(ClassA<? super Integer> value) { }
}

C#

C# verwendet Winkelklammern, um Typparameter und für die Spezialisierung zu deklarieren. Das Schlüsselwort where und ein Doppelpunkt werden verwendet, um die Grenze für einen Typparameter anzugeben.

C# verwendet Declaration-Site-Varianz mit den Schlüsselwörtern in und out für Kontravarianz bzw. Kovarianz. Standardmäßig sind Typparameter invariant.

C# bietet keine Möglichkeit, ein Standard-Typargument anzugeben.

// Generic class with bounds on type parameters
public class ClassA<S, T>
    where T : SomeClass1
    where S : SomeClass2
{
    // Generic method
    public void MyMethod<U>(U value) where U : SomeClass3 { }
}

// Contravariant and covariant type parameters
public class ClassB<in S, out T>
{
    public T MyMethod(S value) { }
}

TypeScript

TypeScript verwendet Winkelklammern, um Typparameter und für die Spezialisierung zu deklarieren. Das Schlüsselwort extends wird verwendet, um eine Grenze anzugeben. Es kann mit anderen Typoperatoren wie keyof kombiniert werden.

TypeScript verwendet Declaration-Site-Varianz. Varianz wird aus der Verwendung abgeleitet und nicht explizit angegeben. TypeScript 4.7 führte die Fähigkeit ein, Varianz mithilfe der Schlüsselwörter in und out anzugeben. Dies wurde hinzugefügt, um extrem komplexe Typen zu behandeln, bei denen die Ableitung der Varianz teuer war.

Ein Standard-Typargument kann mit dem Operator = angegeben werden.

TypeScript unterstützt das Schlüsselwort type, um einen Typalias zu deklarieren, und diese Syntax unterstützt Generics.

// Generic interface
interface InterfaceA<S, T extends SomeInterface1> {
    val1: S;
    val2: T;

    method1<U extends SomeInterface2>(val: U): S
}

// Generic function
function func1<T, K extends keyof T>(ojb: T, key: K) { }

// Contravariant and covariant type parameters (TypeScript 4.7)
interface InterfaceB<in S, out T> { }

// Type parameter with default
interface InterfaceC<T = SomeInterface3> { }

// Generic type alias
type MyType<T extends SomeInterface4> = Array<T>

Scala

In Scala werden eckige Klammern verwendet, um Typparameter zu deklarieren. Eckige Klammern werden auch für die Spezialisierung verwendet. Die Operatoren <: und >: werden verwendet, um obere und untere Grenzen anzugeben.

Scala verwendet Use-Site-Varianz, erlaubt aber auch die Angabe von Declaration-Site-Varianz. Es verwendet einen Präfixoperator + oder - für Kovarianz bzw. Kontravarianz.

Scala bietet keine Möglichkeit, ein Standard-Typargument anzugeben.

Es unterstützt höherstufige Typen (Typparameter, die Typparameter akzeptieren).

// Generic class; type parameter has upper bound
class ClassA[A <: SomeClass1]
{
    // Generic method; type parameter has lower bound
    def method1[B >: A](val: B) ...
}

// Use of an upper and lower bound with the same type parameter
class ClassB[A >: SomeClass1 <: SomeClass2] { }

// Contravariant and covariant type parameters
class ClassC[+A, -B] { }

// Higher-kinded type
trait Collection[T[_]]
{
    def method1[A](a: A): T[A]
    def method2[B](b: T[B]): B
}

// Generic type alias
type MyType[T <: Int] = Container[T]

Swift

Swift verwendet Winkelklammern, um Typparameter und für die Spezialisierung zu deklarieren. Die Obergrenze eines Typparameters wird mit einem Doppelpunkt angegeben.

Swift unterstützt keine generische Varianz; alle Typparameter sind invariant.

Swift bietet keine Möglichkeit, ein Standard-Typargument anzugeben.

// Generic class
class ClassA<T> {
    // Generic method
    func method1<X>(val: T) -> X { }
}

// Type parameter with upper bound constraint
class ClassB<T: SomeClass1> {}

// Generic type alias
typealias MyType<A> = Container<A>

Rust

Rust verwendet Winkelklammern, um Typparameter und für die Spezialisierung zu deklarieren. Die Obergrenze eines Typparameters wird mit einem Doppelpunkt angegeben. Alternativ kann eine where-Klausel verschiedene Einschränkungen angeben.

Rust hat keine traditionelle objektorientierte Vererbung oder Varianz. Subtypisierung in Rust ist sehr eingeschränkt und tritt nur aufgrund von Varianz in Bezug auf Lebenszeiten auf.

Ein Standard-Typargument kann mit dem Operator = angegeben werden.

// Generic class
struct StructA<T> { // T's lifetime is inferred as covariant
    x: T
}

fn f<'a>(
    mut short_lifetime: StructA<&'a i32>,
    mut long_lifetime: StructA<&'static i32>,
) {
    long_lifetime = short_lifetime;
    // error: StructA<&'a i32> is not a subtype of StructA<&'static i32>
    short_lifetime = long_lifetime;
    // valid: StructA<&'static i32> is a subtype of StructA<&'a i32>
}

// Type parameter with bound
struct StructB<T: SomeTrait> {}

// Type parameter with additional constraints
struct StructC<T>
where
    T: Iterator,
    T::Item: Copy
{}

// Generic function
fn func1<T>(val: &[T]) -> T { }

// Generic type alias
type MyType<T> = StructC<T>;

Kotlin

Kotlin verwendet Winkelklammern, um Typparameter und für die Spezialisierung zu deklarieren. Standardmäßig sind Typparameter invariant. Die Obergrenze eines Typs wird mit einem Doppelpunkt angegeben. Alternativ kann eine where-Klausel verschiedene Einschränkungen angeben.

Kotlin unterstützt Declaration-Site-Varianz, bei der die Varianz von Typparametern explizit mit den Schlüsselwörtern in und out deklariert wird. Es unterstützt auch Use-Site-Varianz, die einschränkt, welche Methoden und Member verwendet werden können.

Kotlin bietet keine Möglichkeit, ein Standard-Typargument anzugeben.

// Generic class
class ClassA<T>

// Type parameter with upper bound
class ClassB<T : SomeClass1>

// Contravariant and covariant type parameters
class ClassC<in S, out T>

// Generic function
fun <T> func1(): T {

    // Use site variance
    val covariantA: ClassA<out Number>
    val contravariantA: ClassA<in Number>
}

// Generic type alias
typealias TypeAliasFoo<T> = ClassA<T>

Julia

Julia verwendet geschweifte Klammern, um Typparameter und für die Spezialisierung zu deklarieren. Der Operator <: kann innerhalb einer where-Klausel verwendet werden, um obere und untere Grenzen für einen Typ anzugeben.

# Generic struct; type parameter with upper and lower bounds
# Valid for T in (Int64, Signed, Integer, Real, Number)
struct Container{Int <: T <: Number}
    x::T
end

# Generic function
function func1(v::Container{T}) where T <: Real end

# Alternate forms of generic function
function func2(v::Container{T} where T <: Real) end
function func3(v::Container{<: Real}) end

# Tuple types are covariant
# Valid for func4((2//3, 3.5))
function func4(t::Tuple{Real,Real}) end

Dart

Dart verwendet Winkelklammern, um Typparameter und für die Spezialisierung zu deklarieren. Die Obergrenze eines Typs wird mit dem Schlüsselwort extends angegeben. Standardmäßig sind Typparameter kovariant.

Dart unterstützt Declaration-Site-Varianz, bei der die Varianz von Typparametern explizit mit den Schlüsselwörtern in, out und inout deklariert wird. Es unterstützt keine Use-Site-Varianz.

Dart bietet keine Möglichkeit, ein Standard-Typargument anzugeben.

// Generic class
class ClassA<T> { }

// Type parameter with upper bound
class ClassB<T extends SomeClass1> { }

// Contravariant and covariant type parameters
class ClassC<in S, out T> { }

// Generic function
T func1<T>() { }

// Generic type alias
typedef TypeDefFoo<T> = ClassA<T>;

Go

Go verwendet eckige Klammern, um Typparameter und für die Spezialisierung zu deklarieren. Die Obergrenze eines Typs wird nach dem Namen des Parameters angegeben und muss immer angegeben werden. Das Schlüsselwort any wird für einen ungebundenen Typparameter verwendet.

Go unterstützt keine Varianz; alle Typparameter sind invariant.

Go bietet keine Möglichkeit, ein Standard-Typargument anzugeben.

Go unterstützt keine generischen Typaliase.

// Generic type without a bound
type TypeA[T any] struct {
    t T
}

// Type parameter with upper bound
type TypeB[T SomeType1] struct { }

// Generic function
func func1[T any]() { }

Zusammenfassung

Deklarationssyntax Obergrenze Untergrenze Standardwert Varianz-Site Varianz
C++ template <> n. z. z. n. z. z. = n. z. z. n. z. z.
Java <> extends use super, extends
C# <> where decl in, out
TypeScript <> extends = decl inferred, in, out
Scala [] T <: X T >: X use, decl +, -
Swift <> T: X n. z. z. n. z. z.
Rust <> T: X, where = n. z. z. n. z. z.
Kotlin <> T: X, where use, decl in, out
Julia {} T <: X X <: T n. z. z. n. z. z.
Dart <> extends decl in, out, inout
Go [] T X n. z. z. n. z. z.
Python (vorgeschlagen) [] T: X decl inferred

Danksagungen

Vielen Dank an Sebastian Rittau für den Anstoß der Diskussionen, die zu diesem Vorschlag führten, an Jukka Lehtosalo für den Vorschlag der Syntax für Typalias-Anweisungen und an Jelle Zijlstra, Daniel Moisset und Guido van Rossum für ihr wertvolles Feedback und ihre Verbesserungsvorschläge für die Spezifikation und Implementierung.


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

Zuletzt geändert: 2025-07-07 12:42:34 GMT