Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 695 – 类型参数语法

作者:
Eric Traut <erictr at microsoft.com>
发起人:
Guido van Rossum <guido at python.org>
讨论至:
Typing-SIG 讨论串
状态:
最终版
类型:
标准跟踪
主题:
类型标注
创建日期:
2022年6月15日
Python 版本:
3.12
发布历史:
2022年6月20日, 2022年12月4日
决议:
Discourse 消息

目录

重要

此 PEP 是一份历史文档:请参阅 协变/逆变推理类型别名类型参数列表type 语句注解作用域 以获取最新的规范和文档。规范的类型规范维护在 类型规范站点;运行时类型行为在 CPython 文档中描述。

×

有关如何提议更改类型规范的信息,请参阅类型规范更新过程

摘要

本 PEP 规定了在泛型类、函数或类型别名中指定类型参数的改进语法。它还引入了一个用于声明类型别名的新语句。

动机

PEP 484 将类型变量引入了语言。PEP 612 在此概念的基础上引入了参数规范,而 PEP 646 添加了可变参数类型变量。

虽然泛型类型和类型参数越来越受欢迎,但指定类型参数的语法仍然感觉像是“附加”到 Python 上的。这是 Python 开发人员之间混淆的根源。

Python 静态类型社区内部达成共识,现在是时候提供一种类似于其他支持泛型类型的现代编程语言的正式语法了。

对 25 个流行的类型化 Python 库的分析显示,类型变量(特别是 typing.TypeVar 符号)在 14% 的模块中使用。

混淆点

虽然类型变量的使用已变得广泛,但它们在代码中指定的方式是许多 Python 开发人员混淆的根源。有几个因素导致了这种混淆。

类型变量的作用域规则难以理解。类型变量通常在全局作用域中分配,但它们的语义意义仅在泛型类、函数或类型别名的上下文中使用时才有效。类型变量的单个运行时实例可以在多个泛型上下文中重用,并且在每个上下文中它具有不同的语义意义。本 PEP 建议通过在类、函数或类型别名声明语句中的自然位置声明类型参数来消除这种混淆。

泛型类型别名经常被误用,因为开发人员不清楚在使用类型别名时必须提供类型参数。这导致了一个隐含的 Any 类型参数,这很少是意图。本 PEP 建议添加新语法,使泛型类型别名声明清晰。

PEP 483PEP 484 引入了在泛型类中使用的类型变量的“协变/逆变”概念。类型变量可以是协变、逆变或不变。协变/逆变的概念是类型理论的一个高级细节,大多数 Python 开发人员并不完全理解,但他们今天在定义第一个泛型类时必须面对这个概念。本 PEP 在很大程度上消除了大多数开发人员在定义泛型类时理解协变/逆变概念的需要。

当泛型类或类型别名使用多个类型参数时,类型参数的顺序规则可能会令人困惑。它通常基于它们在类或类型别名声明语句中首次出现的顺序。但是,这可以在类定义中通过包含“Generic”或“Protocol”基类来覆盖。例如,在类声明 class ClassA(Mapping[K, V]) 中,类型参数的顺序是 K 然后是 V。然而,在类声明 class ClassB(Mapping[K, V], Generic[V, K]) 中,类型参数的顺序是 V 然后是 K。本 PEP 建议在所有情况下都使类型参数的顺序明确。

如今,跨多个泛型上下文共享类型变量的做法带来了其他问题。现代编辑器提供了“查找所有引用”和“重命名所有引用”等功能,这些功能在语义级别操作符号。当类型参数在多个泛型类、函数和类型别名之间共享时,所有引用在语义上都是等价的。

在全局作用域中定义的类型变量还需要以 _ 开头的名称,以表示该变量对模块是私有的。全局定义的类型变量也经常被赋予名称以指示它们的协变/逆变,导致诸如“_T_contra”和“_KT_co”之类的笨重名称。当前分配类型变量的机制还要求开发人员在引号中提供一个冗余名称(例如 T = TypeVar("T"))。本 PEP 消除了对冗余名称和笨重变量名称的需要。

定义类型参数今天需要从 typing 模块导入 TypeVarGeneric 符号。在 Python 过去的几个版本中,已经努力消除在常见用例中导入 typing 符号的需要,本 PEP 进一步实现了这一目标。

摘要示例

在此 PEP 之前定义一个泛型类看起来像这样。

from typing import Generic, TypeVar

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

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

使用新语法,它看起来像这样。

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

这是一个今天的泛型函数示例。

from typing import TypeVar

_T = TypeVar("_T")

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

以及新语法。

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

这是一个今天的泛型类型别名示例。

from typing import TypeAlias

_T = TypeVar("_T")

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

以及新语法。

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

规范

类型参数声明

这里是为泛型类、函数和类型别名声明类型参数的新语法。该语法支持在类、函数或类型别名名称后使用方括号中的逗号分隔类型参数列表。

简单(非可变参数)类型变量使用未修饰的名称声明。可变参数类型变量前面带有 *(详情请参阅 PEP 646)。参数规范前面带有 **(详情请参阅 PEP 612)。

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

不需要将 Generic 作为基类。它的包含作为基类由类型参数的存在隐含,它将自动包含在类的 __mro____orig_bases__ 属性中。显式使用 Generic 基类将导致运行时错误。

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

带有类型参数的 Protocol 基类可能会产生运行时错误。类型检查器在这种情况下应该生成错误,因为不需要使用类型参数,并且类的类型参数顺序不再由它们在 Protocol 基类中的顺序决定。

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

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

泛型类、函数或类型别名中的类型参数名称必须在该类、函数或类型别名中是唯一的。重复的名称会在编译时生成语法错误。这与函数签名中参数名称必须唯一的 C# 要求一致。

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

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

如果类类型参数名称以双下划线开头,则它们将被修饰,以避免使类中使用的名称的名称查找机制复杂化。然而,类型参数的 __name__ 属性将保存未修饰的名称。

上限规范

对于非可变参数类型参数,可以通过使用类型注解表达式来指定“上限”类型。如果未指定上限,则上限假定为 object

class ClassA[T: str]: ...

指定的上限类型必须使用类型注解中允许的表达式形式。更复杂的表达式形式应由类型检查器标记为错误。允许带引号的前向引用。

指定的上限类型必须是具体的。尝试使用泛型类型应由类型检查器标记为错误。这与类型检查器对 TypeVar 构造函数调用的现有规则一致。

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

受限类型规范

PEP 484 引入了“受限类型变量”的概念,该变量受限于两个或更多类型的集合。新语法通过使用包含两个或更多类型的字面量元组表达式来支持这种类型的约束。

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

如果指定的类型不是元组表达式,或者元组表达式包含类型注解中不允许的复杂表达式形式,类型检查器应生成错误。允许带引号的前向引用。

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

指定的受限类型必须是具体的。尝试使用泛型类型应由类型检查器标记为错误。这与类型检查器对 TypeVar 构造函数调用的现有规则一致。

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

上限和约束的运行时表示

TypeVar 对象的上限和约束在运行时通过 __bound____constraints__ 属性访问。对于通过新语法定义的 TypeVar 对象,这些属性将惰性求值,如下面的 惰性求值 所述。

泛型类型别名

我们建议引入一个新的语句来声明类型别名。与 classdef 语句类似,type 语句定义了类型参数的作用域。

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

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

类型别名可以引用自身而无需使用引号。

# 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]]

type 关键字是一个新的软关键字。它仅在该语法部分中被解释为关键字。在所有其他位置,它被假定为标识符名称。

作为泛型类型别名的一部分声明的类型参数仅在评估类型别名的右侧时才有效。

typing.TypeAlias 一样,类型检查器应将右侧表达式限制为类型注解中允许的表达式形式。使用更复杂的表达式形式(调用表达式、三元运算符、算术运算符、比较运算符等)应标记为错误。

类型别名表达式不允许使用传统类型变量(即那些通过显式 TypeVar 构造函数调用分配的类型变量)。类型检查器在这种情况下应生成错误。

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

我们建议弃用 PEP 613 中引入的现有 typing.TypeAlias。新语法完全消除了它的必要性。

运行时类型别名类

在运行时,type 语句将生成 typing.TypeAliasType 的实例。此类表示该类型。其属性包括

  • __name__ 是表示类型别名名称的字符串
  • __type_params__TypeVarTypeVarTupleParamSpec 对象的元组,如果它是泛型,则参数化类型别名
  • __value__ 是类型别名的求值结果

所有这些属性都是只读的。

类型别名的值是惰性求值的(参见下面的 惰性求值)。

类型参数作用域

当使用新语法时,会引入一个新的词法作用域,该作用域包含类型参数。类型参数可以在内部作用域中通过名称访问。与 Python 中的其他符号一样,内部作用域可以定义自己的符号,该符号会覆盖同名的外部作用域符号。本节提供了新作用域规则的口头描述。下面的 作用域行为 部分通过转换为近似等效的现有 Python 代码来指定行为。

类型参数对其列表中声明的其他类型参数可见。这允许类型参数在其定义中使用其他类型参数。虽然目前没有此功能的使用场景,但它保留了将来支持依赖于早期类型参数的上限表达式或类型参数默认值的可能性。

如果早期类型参数的定义引用了后期类型参数,即使该名称在外部作用域中定义,也会生成编译错误或运行时异常。

# 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]: ...

作为泛型类的一部分声明的类型参数在类体及其包含的内部作用域中有效。在评估构成类定义的参数列表(基类和任何关键字参数)时,类型参数也可访问。这允许基类通过这些类型参数进行参数化。类型参数在类体之外不可访问,包括类装饰器。

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]: ...

作为泛型函数的一部分声明的类型参数在函数体及其包含的任何作用域中有效。它在参数和返回类型注解中也有效。函数参数的默认参数值在此作用域之外进行评估,因此类型参数在默认值表达式中不可访问。同样,类型参数不在函数装饰器的作用域中。

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](): ...

作为泛型类型别名的一部分声明的类型参数在类型别名表达式中有效。

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

外部作用域中定义的类型参数符号不能通过内部作用域中的 nonlocal 语句进行绑定。

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

新类型参数语法引入的词法作用域与 defclass 语句引入的传统作用域不同。类型参数作用域更像是包含作用域的临时“覆盖”。其符号表中包含的唯一新符号是使用新语法定义的类型参数。对所有其他符号的引用都被视为在包含作用域中找到。这允许基类列表(在类定义中)和类型注解表达式(在函数定义中)引用在包含作用域中定义的符号。

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

编译器允许内部作用域定义一个本地符号,该符号会覆盖外部作用域的类型参数。

PEP 484 中定义的范围规则一致,如果内部范围的泛型类、函数或类型别名重用与外部范围相同的类型参数名称,则类型检查器应生成错误。

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)
        ...

内部作用域中引用的符号使用现有规则解析,但类型参数作用域在名称解析期间也会被考虑。

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

当泛型类使用新的类型参数语法时,类定义的参数列表中不允许使用赋值表达式。同样,对于使用新的类型参数语法的函数,参数或返回类型注解中不允许使用赋值表达式,在定义类型别名的表达式中也不允许使用,在 TypeVar 的边界和约束中也不允许使用。类似地,在这些上下文中不允许使用 yieldyield fromawait 表达式。

此限制是必要的,因为在新词法作用域内求值的表达式不应在该作用域内引入除已定义的类型参数之外的符号,并且不应影响 enclosing 函数是否是生成器或协程。

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

运行时访问类型参数

泛型类、函数和类型别名上提供了一个名为 __type_params__ 的新属性。此属性是参数化类、函数或别名的类型参数元组。该元组包含 TypeVarParamSpecTypeVarTuple 实例。

使用新语法声明的类型参数不会出现在 globals()locals() 返回的字典中。

协变/逆变推理

本 PEP 消除了为类型参数指定协变/逆变的需要。相反,类型检查器将根据类型参数在类中的使用情况推断其协变/逆变。类型参数根据其使用方式被推断为协变、逆变或不变。

Python 类型检查器已经具有确定类型参数协变/逆变的能力,目的是验证泛型协议类中的协变/逆变。此功能可用于所有类(无论它们是否是协议)来计算每个类型参数的协变/逆变。

计算类型参数协变/逆变的算法如下。

对于泛型类中的每个类型参数

1. 如果类型参数是可变参数(TypeVarTuple)或参数规范(ParamSpec),则始终视为不变。无需进一步推断。

2. 如果类型参数来自传统的 TypeVar 声明且未指定为 infer_variance(见下文),则其协变/逆变由 TypeVar 构造函数调用指定。无需进一步推断。

3. 创建类的两个专用版本。我们将这些称为 upperlower 专用版本。在这两种专用版本中,将除正在推断的类型参数之外的所有类型参数替换为虚拟类型实例(一个具体的匿名类,它与自身类型兼容并假定满足类型参数的上限或约束)。在 upper 专用类中,将目标类型参数专门化为 object 实例。此专门化忽略类型参数的上限或约束。在 lower 专用类中,将目标类型参数专门化为自身(即,相应的类型参数是类型参数本身)。

4. 使用正常的类型兼容性规则确定 lower 是否可以分配给 upper。如果是,则目标类型参数是协变的。如果不是,则确定 upper 是否可以分配给 lower。如果是,则目标类型参数是逆变的。如果这两种组合都不可分配,则目标类型参数是不变的。

这是一个示例。

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

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

为了确定 T1 的协变/逆变,我们专门化 ClassA 如下

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

我们发现 upper 不能根据 PEP 484 中定义的正常类型兼容性规则分配给 lower。同样,lower 也不能分配给 upper,因此我们得出结论 T1 是不变的。

为了确定 T2 的协变/逆变,我们专门化 ClassA 如下

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

由于 upper 可以分配给 lower,因此 T2 是逆变的。

为了确定 T3 的协变/逆变,我们专门化 ClassA 如下

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

由于 lower 可以分配给 upper,因此 T3 是协变的。

TypeVar 的自动协变/逆变

现有的 TypeVar 类构造函数接受名为 covariantcontravariant 的关键字参数。如果两者都为 False,则类型变量被假定为不变的。我们建议添加另一个名为 infer_variance 的关键字参数,表示类型检查器应使用推断来确定类型变量是不变、协变还是逆变。在运行时可以访问相应的实例变量 __infer_variance__ 以确定协变/逆变是否被推断。使用新语法隐式分配的类型变量将始终将 __infer_variance__ 设置为 True

使用传统语法的泛型类可能包含具有显式和推断协变/逆变的类型变量组合。

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]): ...

与传统 TypeVars 的兼容性

为了向后兼容,TypeVarTypeVarTupleParamSpec 的现有分配机制得以保留。但是,这些“传统”类型变量不应与使用新语法分配的类型参数结合使用。这种组合应由类型检查器标记为错误。这是必要的,因为类型参数顺序不明确。

如果类、函数或类型别名不使用新语法,则可以将传统类型变量与新样式类型参数结合使用。在这种情况下,新样式类型参数必须来自外部作用域。

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: ...

运行时实现

语法变更

本 PEP 引入了一个新的软关键字 type。它以以下方式修改语法

  1. classdef 语句中添加可选类型参数子句。
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. 添加新的 type 语句来定义类型别名。
type_alias: "type" n=NAME t=[type_params] '=' b=expression

AST 变更

本 PEP 引入了一种新的 AST 节点类型,称为 TypeAlias

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

它还添加了一个表示类型参数的 AST 节点类型。

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

在 AST 中,边界和约束的表示方式是相同的。在实现中,任何是 Tuple AST 节点的表达式都被视为约束,而任何其他表达式都被视为边界。

它还修改了现有的 AST 节点类型 FunctionDefAsyncFunctionDefClassDef,以包含一个名为 typeparams 的额外可选属性,该属性包含与函数或类关联的类型参数列表。

惰性求值

本 PEP 引入了三个新的上下文,其中表达式可能出现,这些表达式表示静态类型:TypeVar 边界、TypeVar 约束和类型别名的值。这些表达式可能包含对尚未定义的名称的引用。例如,类型别名可以是递归的,甚至是相互递归的,并且类型变量边界可能指回当前类。如果这些表达式是急切求值的,用户需要将此类表达式用引号括起来以防止运行时错误。PEP 563PEP 649 详细介绍了这种情况下类型注解的问题。

为了防止本 PEP 中提出的新语法出现类似情况,我们建议对这些表达式采用惰性求值,类似于 PEP 649 中的方法。具体来说,每个表达式都将保存在一个代码对象中,并且只有当相应的属性被访问时(TypeVar.__bound__TypeVar.__constraints__TypeAlias.__value__),该代码对象才会被求值。成功求值后,该值将被保存,后续调用将返回相同的值而无需重新求值代码对象。

如果 PEP 649 被实现,应该添加额外的求值机制以反映 PEP 为注解提供的选项。在当前版本的 PEP 中,这可能包括向 TypeVar 添加一个 __evaluate_bound__ 方法,该方法接受一个 format 参数,其含义与 PEP 649 的 __annotate__ 方法相同(以及类似的 __evaluate_constraints__ 方法,以及 TypeAliasType 上的 __evaluate_value__ 方法)。但是,在 PEP 649 被接受和实现之前,只支持默认求值格式(PEP 649 的“VALUE”格式)。

由于惰性求值,属性观察到的值可能取决于属性访问的时间。

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"

可以使用 PEP 563 或 PEP 649 的语义构建影响类型注解的类似示例。

惰性求值的简单实现会错误地处理类命名空间,因为类中的函数通常无法访问 enclosing 类命名空间。该实现将保留对类命名空间的引用,以便正确解析类作用域的名称。

作用域行为

新语法需要一种新的作用域,其行为与 Python 中现有的作用域不同。因此,新语法不能完全用现有的 Python 作用域行为来描述。本节通过引用现有作用域行为进一步指定这些作用域:新作用域的行为类似于函数作用域,除了下面列出的一些细微差异。

所有示例都包含使用伪关键字 def695 引入的函数。这个关键字在实际语言中将不存在;它用于阐明新作用域在大多数情况下类似于函数作用域。

def695 作用域与常规函数作用域在以下方面有所不同

  • 如果一个 def695 作用域直接在一个类作用域内,或者在一个立即在一个类作用域内的另一个 def695 作用域内,那么在该类作用域中定义的名称可以在 def695 作用域内访问。(相比之下,常规函数不能访问在 enclosing 类作用域中定义的名称。)
  • 以下构造不允许直接在 def695 作用域内使用,但可以在 def695 作用域内嵌套的其他作用域中使用
    • yield
    • yield from
    • await
    • := (海象运算符)
  • def695 作用域内定义的对象(类和函数)的合格名称(__qualname__)如同对象在最近的 enclosing 作用域中定义一样。
  • def695 作用域中绑定的名称不能通过嵌套作用域中的 nonlocal 语句重新绑定。

def695 作用域用于评估本 PEP 中提出的几种新语法构造。有些是急切求值的(当定义类型别名、函数或类时);有些是惰性求值的(仅当明确请求求值时)。在所有情况下,作用域语义都是相同的

  • 急切求值的值
    • 泛型类型别名的类型参数
    • 泛型函数的类型参数和注解
    • 泛型类的类型参数和基类表达式
  • 惰性求值的值
    • 泛型类型别名的值
    • 类型变量的边界
    • 类型变量的约束

在下面的翻译中,以两个下划线开头的名称是实现内部的,对实际的 Python 代码不可见。我们使用以下内在函数,它们在真实实现中直接在解释器中定义

  • __make_typealias(*, name, type_params=(), evaluate_value):创建一个新的 typing.TypeAlias 对象,具有给定的名称、类型参数和惰性求值的值。该值直到访问 __value__ 属性才会被求值。
  • __make_typevar_with_bound(*, name, evaluate_bound):创建一个新的 typing.TypeVar 对象,具有给定的名称和惰性求值的边界。该边界直到访问 __bound__ 属性才会被求值。
  • __make_typevar_with_constraints(*, name, evaluate_constraints):创建一个新的 typing.TypeVar 对象,具有给定的名称和惰性求值的约束。这些约束直到访问 __constraints__ 属性才会被求值。

非泛型类型别名翻译如下

type Alias = int

等同于

def695 __evaluate_Alias():
    return int

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

泛型类型别名

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

等同于

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()

泛型函数

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

等同于

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()

一个更完整的泛型函数示例,说明了默认值、装饰器和边界的作用域行为。请注意,此示例没有正确使用 ParamSpec,因此它应该被静态类型检查器拒绝。然而,它在运行时是有效的,并且这里用于说明运行时语义。

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

等同于

__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())

泛型类

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

等同于

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()

def695 作用域与现有行为最大的不同之处在于类作用域内的行为。这种不同是必要的,以便在类中定义的泛型以直观的方式行为

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

等同于

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()

在此示例中,xy 的注解在 def695 作用域内求值,因为它们需要访问泛型方法的类型参数 T。但是,它们还需要访问在类命名空间中定义的 Nested 名称。如果 def695 作用域的行为类似于常规函数作用域,则 Nested 在函数作用域内将不可见。因此,直接位于类作用域内的 def695 作用域可以访问该类作用域,如上所述。

库变更

typing 模块中目前以 Python 实现的几个类必须部分以 C 语言实现。这包括 TypeVarTypeVarTupleParamSpecGeneric,以及新的类 TypeAliasType(如上所述)。该实现可能会将一些与模块其余部分密切相关的行为委托给 Python 版本的 typing.py。这些类的已记录行为不应改变。

参考实现

此提案在 CPython PR #103764 中进行了原型设计。

Pyright 类型检查器支持此 PEP 中描述的行为。

被拒绝的想法

前缀子句

我们探讨了各种语法选项来指定位于 defclass 语句之前的类型参数。我们考虑的一种变体使用了 using 子句,如下所示

using S, T
class ClassA: ...

此选项被拒绝,因为类型参数的作用域规则不够清晰。此外,此语法与类和函数装饰器(在 Python 中很常见)交互不佳。只有另一种流行的编程语言 C++ 使用这种方法。

我们同样考虑了看起来像装饰器的前缀形式(例如,@using(S, T))。这个想法被拒绝了,因为这种形式会与常规装饰器混淆,并且它们不能很好地与现有装饰器组合。此外,装饰器在逻辑上是在它们装饰的语句之后执行的,所以如果它们引入在“被装饰”语句中可见的符号(类型参数),而“被装饰”语句在逻辑上是在装饰器本身之前执行的,那将是令人困惑的。

尖括号

许多支持泛型的语言都使用尖括号。(请参阅附录 A 末尾的表格以获取摘要。)我们探讨了在 Python 中使用尖括号声明类型参数,但最终因两个原因拒绝了它。首先,尖括号不被 Python 扫描器视为“配对”,因此 <> 标记之间的行尾字符被保留。这意味着类型参数列表中的任何换行符都需要使用难看且笨重的 \ 转义序列。其次,Python 已经建立了方括号用于泛型类型的显式专门化(例如,list[int])。我们得出结论,将尖括号用于泛型声明但将方括号用于显式专门化将是不一致和令人困惑的。我们调查的所有其他语言在这方面都是一致的。

边界语法

我们探讨了各种语法选项来指定类型变量的边界和约束。我们考虑过,但最终拒绝了,使用像 Scala 中的 <: 标记,使用像其他各种语言中的 extendswith 关键字,以及使用类似于当今 typing.TypeVar 构造函数的函数调用语法。简单的冒号语法与许多其他编程语言一致(参见附录 A),并且受到被调查的 Python 开发人员的大力偏爱。

显式协变/逆变

我们考虑添加语法来指定类型参数是打算不变、协变还是逆变。Python 中的 typing.TypeVar 机制需要这样做。其他一些语言,包括 Scala 和 C#,也要求开发人员指定协变/逆变。我们拒绝了这个想法,因为协变/逆变通常可以推断出来,并且大多数现代编程语言确实会根据使用情况推断协变/逆变。协变/逆变是一个许多开发人员觉得困惑的高级主题,因此我们希望消除大多数 Python 开发人员理解这个概念的需要。

名称修饰

在考虑实现选项时,我们考虑了一种“名称修饰”方法,其中编译器为每个类型参数赋予一个唯一的“修饰”名称。这个修饰名称将基于它所关联的泛型类、函数或类型别名的限定名称。这种方法被拒绝,因为限定名称不一定是唯一的,这意味着修饰名称需要基于其他随机值。此外,这种方法与用于评估带引号(前向引用)类型注解的技术不兼容。

附录 A:类型参数语法调查

许多编程语言都支持泛型类型。在本节中,我们对其他流行编程语言使用的选项进行了调查。这之所以相关,是因为熟悉其他语言将使 Python 开发人员更容易理解这个概念。我们在此提供了更多细节(例如,默认类型参数支持),这在考虑 Python 类型系统未来的扩展时可能有用。

C++

C++ 使用尖括号与关键字 templatetypename 结合来声明类型参数。它使用尖括号进行专门化。

C++20 引入了广义约束的概念,它们可以像 Python 中的协议一样工作。可以在名为 concept 的实体中定义一组约束。

协变/逆变未显式指定,但约束可以强制协变/逆变。

可以使用 = 运算符指定默认类型参数。

// 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 使用尖括号声明类型参数和专门化。默认情况下,类型参数是不变的。extends 关键字用于指定上限。super 关键字用于指定逆变边界。

Java 使用使用点协变/逆变。编译器根据泛型类型的使用对可以访问的方法和成员施加限制。协变/逆变未显式指定。

Java 没有提供指定默认类型参数的方法。

// 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# 使用尖括号声明类型参数和专门化。where 关键字和冒号用于指定类型参数的边界。

C# 使用声明点协变/逆变,分别使用关键字 inout 表示逆变和协变。默认情况下,类型参数是不变的。

C# 没有提供指定默认类型参数的方法。

// 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 使用尖括号声明类型参数和专门化。extends 关键字用于指定边界。它可以与其他类型运算符(如 keyof)结合使用。

TypeScript 使用声明点协变/逆变。协变/逆变是从使用中推断出来的,而不是显式指定的。TypeScript 4.7 引入了使用 inout 关键字指定协变/逆变的能力。这是为了处理协变/逆变推断成本极高的高度复杂类型。

可以使用 = 运算符指定默认类型参数。

TypeScript 支持 type 关键字来声明类型别名,此语法支持泛型。

// 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

在 Scala 中,方括号用于声明类型参数。方括号也用于专门化。<:>: 运算符分别用于指定上限和下限。

Scala 使用使用点协变/逆变,但也允许声明点协变/逆变规范。它分别使用 +- 前缀运算符表示协变和逆变。

Scala 没有提供指定默认类型参数的方法。

它确实支持高阶类型(接受类型类型参数的类型参数)。

// 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 使用尖括号声明类型参数和专门化。类型参数的上限使用冒号指定。

Swift 不支持泛型协变/逆变;所有类型参数都是不变的。

Swift 没有提供指定默认类型参数的方法。

// 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 使用尖括号声明类型参数和专门化。类型参数的上限在其名称之后指定,并且必须始终指定。关键字 any 用于无界类型参数。

Rust 没有传统的面向对象继承或协变/逆变。Rust 中的子类型化受到非常严格的限制,仅由于生命周期方面的协变/逆变而发生。

可以使用 = 运算符指定默认类型参数。

// 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 使用尖括号声明类型参数和专门化。默认情况下,类型参数是不变的。类型的上限使用冒号指定。或者,where 子句可以指定各种约束。

Kotlin 支持声明点协变/逆变,其中类型参数的协变/逆变使用 inout 关键字显式声明。它还支持使用点协变/逆变,这限制了可以使用的哪些方法和成员。

Kotlin 没有提供指定默认类型参数的方法。

// 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 使用花括号声明类型参数和专门化。<: 运算符可以在 where 子句中使用,以声明类型的上限和下限。

# 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 使用尖括号声明类型参数和专门化。类型的上限使用 extends 关键字指定。默认情况下,类型参数是协变的。

Dart 支持声明点协变/逆变,其中类型参数的协变/逆变使用 inoutinout 关键字显式声明。它不支持使用点协变/逆变。

Dart 没有提供指定默认类型参数的方法。

// 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 使用方括号声明类型参数和专门化。类型的上限在参数名称之后指定,并且必须始终指定。关键字 any 用于无界类型参数。

Go 不支持协变/逆变;所有类型参数都是不变的。

Go 没有提供指定默认类型参数的方法。

Go 不支持泛型类型别名。

// 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]() { }

总结

声明语法 上限 下限 默认值 协变/逆变位置 协变/逆变
C++ template <> 不适用 不适用 = 不适用 不适用
Java <> extends 使用 super, extends
C# <> where 声明 in, out
TypeScript <> extends = 声明 推断, in, out
Scala [] T <: X T >: X 使用,声明 +, -
Swift <> T: X 不适用 不适用
Rust <> T: X, where = 不适用 不适用
Kotlin <> T: X, where 使用,声明 in, out
Julia {} T <: X X <: T 不适用 不适用
Dart <> extends 声明 in, out, inout
Go [] T X 不适用 不适用
Python(提案) [] T: X 声明 推断

致谢

感谢 Sebastian Rittau 启动了促成此提案的讨论,感谢 Jukka Lehtosalo 提出了类型别名语句的语法,感谢 Jelle Zijlstra、Daniel Moisset 和 Guido van Rossum 对规范和实现的宝贵反馈和建议改进。


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

最后修改:2025-07-07 12:42:34 GMT