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日
- 决议:
- 讨论消息
摘要
此 PEP 指定了一种改进的语法,用于在泛型类、函数或类型别名中指定类型参数。它还引入了用于声明类型别名的新的语句。
动机
PEP 484 将类型变量引入到语言中。 PEP 612 在此概念的基础上引入了参数规范,而 PEP 646 添加了可变参数类型变量。
虽然泛型类型和类型参数越来越受欢迎,但指定类型参数的语法在 Python 中仍然感觉像是“硬加”的。这是 Python 开发人员困惑的根源。
Python 静态类型社区达成共识,现在是时候提供一种正式的语法,类似于其他支持泛型类型的现代编程语言。
对 25 个流行的类型化 Python 库的分析表明,类型变量(特别是 typing.TypeVar
符号)在 14% 的模块中使用。
困惑点
虽然类型变量的使用已经很普遍,但它们在代码中指定的格式是许多 Python 开发人员困惑的根源。有几个因素导致了这种困惑。
类型变量的作用域规则难以理解。类型变量通常在全局作用域内分配,但它们的语义含义仅在泛型类、函数或类型别名的上下文中使用时才有效。类型变量的单个运行时实例可以在多个泛型上下文中重复使用,并且在每个上下文中具有不同的语义含义。此 PEP 建议通过在类、函数或类型别名声明语句中的自然位置声明类型参数来消除这种困惑的根源。
泛型类型别名经常被误用,因为开发人员不清楚在使用类型别名时必须提供类型参数。这会导致隐式类型参数为 Any
,而这很少是开发者的意图。此 PEP 建议添加新的语法,使泛型类型别名声明更清晰。
PEP 483 和 PEP 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
模块导入 TypeVar
和 Generic
符号。在过去的几个 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
泛型类、函数或类型别名中的类型参数名称在同一类、函数或类型别名中必须唯一。重复的名称会在编译时生成语法错误。这与函数签名中参数名称必须唯一的要求一致。
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
对象,这些属性会延迟求值,如下面的延迟求值部分所述。
泛型类型别名
我们建议引入一个新的语句来声明类型别名。类似于class
和def
语句,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__
是TypeVar
、TypeVarTuple
或ParamSpec
对象的元组,如果类型别名是泛型,则这些对象参数化类型别名__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
新类型参数语法引入的词法作用域不同于由def
或class
语句引入的传统作用域。类型参数作用域更像是一个包含作用域的临时“覆盖”。其符号表中仅包含使用新语法定义的类型参数。所有其他符号的引用都被视为在包含作用域中找到。这允许基类列表(在类定义中)和类型注解表达式(在函数定义中)引用包含作用域中定义的符号。
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
的界限和约束中使用。类似地,在这些上下文中不允许使用yield
、yield from
和await
表达式。
此限制是必要的,因为在新词法作用域内计算的表达式不应在此作用域内引入除定义的类型参数之外的其他符号,也不应影响封闭函数是否为生成器或协程。
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__
的新属性可用于泛型类、函数和类型别名。此属性是参数化类、函数或别名的类型参数的元组。该元组包含TypeVar
、ParamSpec
和TypeVarTuple
实例。
使用新语法声明的类型参数不会出现在globals()
或locals()
返回的字典中。
方差推断
本 PEP 消除了对类型参数指定方差的需要。相反,类型检查器将根据类型参数在类中的使用情况推断其方差。类型参数根据其使用方式被推断为不变、协变或逆变。
Python 类型检查器已经具备确定类型参数方差的能力,以便验证泛型协议类中的方差。此功能可用于所有类(无论它们是否为协议)来计算每个类型参数的方差。
计算类型参数方差的算法如下。
对于泛型类中的每个类型参数
1. 如果类型参数是可变的(TypeVarTuple
)或参数规范(ParamSpec
),则始终将其视为不变。无需进一步推断。
2. 如果类型参数来自传统的TypeVar
声明,并且未指定为infer_variance
(见下文),则其方差由TypeVar
构造函数调用指定。无需进一步推断。
3. 创建类的两个专门版本。我们将这些版本称为upper
和lower
专门化。在这两个专门化中,用虚拟类型实例(一个具体的匿名类,与自身类型兼容,并假设满足类型参数的界限或约束)替换除要推断的那个之外的所有类型参数。在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]
我们发现,使用PEP 484中定义的正常类型兼容性规则,upper
不能分配给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
类构造函数接受名为 covariant
和 contravariant
的关键字参数。如果这两个参数都为 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]): ...
与传统 TypeVar 的兼容性
分配 TypeVar
、TypeVarTuple
和 ParamSpec
的现有机制为了向后兼容性而保留。但是,这些“传统”类型变量不应该与使用新语法分配的类型参数组合使用。这种组合应该被类型检查器标记为错误。这是必要的,因为类型参数顺序不明确。
如果类、函数或类型别名未使用新语法,则可以将传统类型变量与新式类型参数组合使用。在这种情况下,新式类型参数必须来自外部作用域。
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
。它以以下方式修改了语法
- 在
class
和def
语句中添加可选的类型参数子句。
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] ...
- 添加用于定义类型别名的新的
type
语句。
type_alias: "type" n=NAME t=[type_params] '=' b=expression
AST 更改
本 PEP 引入了一种名为 TypeAlias
的新的 AST 节点类型。
TypeAlias(expr name, typeparam* typeparams, expr value)
它还添加了一个表示类型参数的 AST 节点类型。
typeparam = TypeVar(identifier name, expr? bound)
| ParamSpec(identifier name)
| TypeVarTuple(identifier name)
边界和约束在 AST 中以相同的方式表示。在实现中,任何作为 Tuple
AST 节点的表达式都被视为约束,任何其他表达式都被视为边界。
它还修改了现有的 AST 节点类型 FunctionDef
、AsyncFunctionDef
和 ClassDef
,以包含一个名为 typeparams
的附加可选属性,该属性包含与函数或类关联的类型参数列表。
延迟求值
本 PEP 引入了三个新的上下文,其中可能出现表示静态类型的表达式:TypeVar
边界、TypeVar
约束和类型别名的值。这些表达式可能包含对尚未定义的名称的引用。例如,类型别名可能是递归的,甚至可能是相互递归的,并且类型变量边界可能引用回当前类。如果这些表达式被急切地求值,用户需要将这些表达式括在引号中以防止运行时错误。PEP 563 和 PEP 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 的语义构建影响类型注释的类似示例。
延迟求值的简单实现将错误地处理类命名空间,因为类中的函数通常无法访问封闭类命名空间。实现将保留对类命名空间的引用,以便正确解析类范围内的名称。
作用域行为
新语法需要一种新的作用域类型,其行为与 Python 中现有的作用域不同。因此,新语法不能完全用现有的 Python 作用域行为来描述。本节通过参考现有的作用域行为进一步指定这些作用域:新作用域的行为类似于函数作用域,除了下面列出的一些细微差别。
所有示例都包含使用伪关键字 def695
引入的函数。此关键字在实际语言中不存在;它用于阐明新作用域在大多数情况下类似于函数作用域。
def695
作用域与常规函数作用域的不同之处在于
- 如果
def695
作用域直接位于类作用域内,或位于另一个直接位于类作用域内的def695
作用域内,则可以在def695
作用域内访问在该类作用域中定义的名称。(相比之下,常规函数无法访问在封闭类作用域中定义的名称。) - 以下结构不允许直接位于
def695
作用域内,尽管它们可以在嵌套在def695
作用域内的其他作用域内使用yield
yield from
await
:=
(海豹运算符)
- 在
def695
作用域内定义的对象(类和函数)的限定名称(__qualname__
)就像这些对象是在最接近的封闭作用域内定义的一样。 - 在
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()
在本例中,x
和 y
的注解在 def695
范围内进行评估,因为它们需要访问泛型方法的类型参数 T
。但是,它们还需要访问类命名空间中定义的 Nested
名称。如果 def695
范围的行为类似于常规函数范围,则 Nested
在函数范围内将不可见。因此,直接位于类范围内的 def695
范围可以访问该类范围,如上所述。
库更改
目前在 Python 中实现的 typing
模块中的几个类必须在 C 中部分实现。这包括 TypeVar
、TypeVarTuple
、ParamSpec
和 Generic
,以及新的类 TypeAliasType
(如上所述)。实现可能会委托给 typing.py
的 Python 版本,以处理与模块其余部分大量交互的行为。这些类的已记录行为不应更改。
参考实现
此提案已在 CPython PR #103764 中进行了原型设计。
Pyright 类型检查器支持本 PEP 中描述的行为。
被拒绝的想法
前缀子句
我们探索了各种用于指定在 def
和 class
语句之前出现的类型参数的语法选项。我们考虑过的一种变体使用了如下所示的 using
子句
using S, T
class ClassA: ...
此选项被拒绝,因为类型参数的作用域规则不够清晰。此外,此语法与 Python 中常用的类和函数装饰器交互不佳。只有另一种流行的编程语言 C++ 使用这种方法。
我们同样考虑了类似装饰器的前缀形式(例如,@using(S, T)
)。这个想法被拒绝,因为此类形式会与常规装饰器混淆,并且它们与现有装饰器的组合效果不佳。此外,装饰器在逻辑上是在其修饰的语句之后执行的,因此它们引入在“被修饰”语句(在逻辑上先于装饰器本身执行)中可见的符号(类型参数)会令人困惑。
尖括号
许多支持泛型的语言都使用尖括号。(有关总结,请参阅附录 A 末尾的表格。)我们探索了在 Python 中使用尖括号进行类型参数声明,但最终出于两个原因拒绝了它。首先,Python 扫描程序不认为尖括号是“配对”的,因此在 <
和 >
标记之间的换行符将被保留。这意味着类型参数列表中的任何换行符都需要使用难看且繁琐的 \
转义序列。其次,Python 已将方括号用于泛型类型的显式特化(例如,list[int]
)。我们得出结论,使用尖括号进行泛型声明但使用方括号进行显式特化是不一致且令人困惑的。我们调查的所有其他语言在这方面都是一致的。
边界语法
我们探索了各种用于指定类型变量的边界和约束的语法选项。我们考虑过但最终拒绝了使用类似 Scala 中的 <:
标记,使用类似各种其他语言中的 extends
或 with
关键字,以及使用类似于当今 typing.TypeVar
构造函数的函数调用语法。简单的冒号语法与许多其他编程语言一致(见附录 A),并且在接受调查的 Python 开发人员中非常受欢迎。
显式方差
我们考虑过添加语法以指定类型参数是意图不变、协变还是逆变。Python 中的 typing.TypeVar
机制需要此功能。包括 Scala 和 C# 在内的其他一些语言也要求开发人员指定方差。我们拒绝了这个想法,因为方差通常可以推断出来,并且大多数现代编程语言都根据用法推断方差。方差是一个高级主题,许多开发人员发现它令人困惑,因此我们希望消除大多数 Python 开发人员理解此概念的需要。
名称混淆
在考虑实现选项时,我们考虑了一种“名称混淆”方法,其中编译器为每个类型参数提供一个唯一的“混淆”名称。此混淆名称将基于与其关联的泛型类、函数或类型别名的限定名称。这种方法被拒绝,因为限定名称不一定是唯一的,这意味着混淆名称需要基于其他一些随机值。此外,此方法与用于评估引用的(前向引用)类型注解的技术不兼容。
附录 A:类型参数语法的调查
许多编程语言都支持泛型类型。在本节中,我们提供了对其他流行编程语言中使用的选项的调查。这与 Python 开发人员更容易理解此概念相关。我们在此处提供其他详细信息(例如,默认类型参数支持),这些信息在考虑 Python 类型系统的未来扩展时可能会有用。
C++
C++ 使用尖括号结合 template
和 typename
关键字来声明类型参数。它使用尖括号进行特化。
C++20 引入了广义约束的概念,它可以像 Python 中的协议一样工作。可以在名为 concept
的命名实体中定义一组约束。
方差没有明确指定,但约束可以强制执行方差。
可以使用 =
运算符指定默认类型参数。
// Generic class
template <typename>
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# 使用声明点方差,使用 in
和 out
关键字分别表示逆变和协变。默认情况下,类型参数是不变的。
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 引入了使用 in
和 out
关键字指定方差的能力。这是为了处理推断方差成本很高的极其复杂的类型而添加的。
可以使用 =
运算符指定默认类型参数。
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 使用尖括号声明类型参数和进行特化。类型参数的上界使用冒号指定。或者,where
子句可以指定各种约束。
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 支持声明点方差,其中类型参数的方差使用 in
和 out
关键字显式声明。它还支持使用点方差,这限制了可以使用哪些方法和成员。
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 支持声明点方差,其中类型参数的方差使用 in
、out
和 inout
关键字显式声明。它不支持使用点方差。
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++ | 模板 <> | n/a | n/a | = | n/a | n/a |
Java | <> | 扩展 | 使用 | 超级,扩展 | ||
C# | <> | 其中 | 声明 | 输入,输出 | ||
TypeScript | <> | 扩展 | = | 声明 | 推断,输入,输出 | |
Scala | [] | T <: X | T >: X | 使用,声明 | +, - | |
Swift | <> | T: X | n/a | n/a | ||
Rust | <> | T: X,其中 | = | n/a | n/a | |
Kotlin | <> | T: X,其中 | 使用,声明 | 输入,输出 | ||
Julia | {} | T <: X | X <: T | n/a | n/a | |
Dart | <> | 扩展 | 声明 | 输入,输出,输入输出 | ||
Go | [] | T X | n/a | n/a | ||
Python(拟议) | [] | T: X | 声明 | 推断 |
致谢
感谢 Sebastian Rittau 启动了导致此提案的讨论,感谢 Jukka Lehtosalo 提出类型别名语句的语法,并感谢 Jelle Zijlstra、Daniel Moisset 和 Guido van Rossum 对规范和实现提出的宝贵反馈和改进建议。
版权
本文档置于公共领域或根据 CC0-1.0-Universal 许可证,以两者中较宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0695.rst