在Python中,类型形参列表是指在函数定义中使用泛型类型的占位符。这些占位符可以用于指定函数接受的参数类型、返回值类型以及函数内部变量的类型等。类型参数是紧接在函数、类或类型别名的名称之后的方括号 ([]) 中声明的。
一、类型形参列表
type_params ::= "[" type_param ("," type_param)* "]" type_param ::= typevar | typevartuple | paramspec typevar ::= identifier (":" expression)? typevartuple ::= "*" identifier paramspec ::= "**" identifier
函数 (包括 协程), 类 和 类型别名 可能包含类型形参列表:
def max[T](args: list[T]) -> T: ... async def amax[T](args: list[T]) -> T: ... class Bag[T]: def __iter__(self) -> Iterator[T]: ... def add(self, arg: T) -> None: ... type ListOrSet[T] = list[T] | set[T]
从语义上讲,这表明函数、类或类型别名是类型变量的泛型,主要供静态类型检查器使用,并且在运行时,泛型对象的行为与其对应的非泛型对象非常相似。
类型参数是紧接在函数、类或类型别名的名称之后的方括号 ([]) 中声明的,类型参数可在泛型对象的作用域内访问,但不能在其他地方访问,因此,在声明 def func[T](): pass 之后,模块作用域中就不能再使用 T 这个名称。 类型形参的作用域是用一个特殊函数 (从技术上说,是一个 标注作用域) 来模拟的,它封装了泛型对象的创建操作。
泛型函数、类和类型别名都有一个 __type_params__ 属性用于列出它们的类型形参。类型形参可分为三种:
1、typing.TypeVar,由一个普通名称 (例如 T) 引入。 从语义上讲,这对类型检查器来说代表了一个单独类型。
2、typing.TypeVarTuple,通过在前面添加一个星号的名称来引入 (例如 *Ts)。 从语义上讲,它代表由任意多个类型组成的元组。
3、typing.ParamSpec,通过在前面添加两个星号的名称来引入 (例如 **P)。 从语义上讲,它代表一个可调用对象的形参。
typing.TypeVar 声明可以通过在冒号 (: ) 后跟一个表达式来定义范围和约束。 冒号后的单独表达式表示一个范围 (例如 T: int), typing.TypeVar 能表示的类型只能是该范围的子类型,冒号后在圆括号内的表达式元组指定了一组约束 (例如 T: (str, bytes))。 元组中的每个成员都应为一个类型 (同样,在运行时并不强制要求这一点)。 约束的类型变量只能使用约束列表内的类型中选择一种。
对于使用类型形参列表语法声明的 typing.TypeVar,范围和约束在创建泛型对象时并不会被求值,只有在通过属性 __bound__ 和 __constraints__ 显式地访问它时才会被求值。 要做到这一点,需要在单独的标注作用域中对范围和约束进行求值。
注意:typing.TypeVarTuple 和 typing.ParamSpec 不能拥有范围或约束。
下面的例子显示了所有被允许的类型形参声明:
def overly_generic[ SimpleTypeVar, TypeVarWithBound: int, TypeVarWithConstraints: (str, bytes), *SimpleTypeVarTuple, **SimpleParamSpec, ]( a: SimpleTypeVar, b: TypeVarWithBound, c: Callable[SimpleParamSpec, TypeVarWithConstraints], *d: SimpleTypeVarTuple, ): ...
二、泛型函数
泛型函数的声明方式如下:
def func[T](arg: T): ...
该语法等价于:
annotation-def TYPE_PARAMS_OF_func(): T = typing.TypeVar("T") def func(arg: T): ... func.__type_params__ = (T,) return func func = TYPE_PARAMS_OF_func()
这里 annotation-def 指定了一个 标注作用域,它在运行时并不会实际绑定到任何名称。 (另一项自由是在翻译中达成的:该语法没有通过 typing 模块的属性访问,而是直接创建了一个 typing.TypeVar 的实例)。
泛型函数的标注会在用于声明类型形参的标注作用域内进行求值,但函数的默认值和装饰器则不会。下面的例子演示了针对这些场景,以及类型形参的变化形式的作用域规则:
@decorator def func[T: int, *Ts, **P](*args: *Ts, arg: Callable[P, T] = some_default): ...
除了 TypeVar 绑定的惰性求值以外,这等同于:
DEFAULT_OF_arg = some_default annotation-def TYPE_PARAMS_OF_func(): annotation-def BOUND_OF_T(): return int # In reality, BOUND_OF_T() is evaluated only on demand. T = typing.TypeVar("T", bound=BOUND_OF_T()) Ts = typing.TypeVarTuple("Ts") P = typing.ParamSpec("P") def func(*args: *Ts, arg: Callable[P, T] = DEFAULT_OF_arg): ... func.__type_params__ = (T, Ts, P) return func func = decorator(TYPE_PARAMS_OF_func())
大写形式的名称如 DEFAULT_OF_arg 在运行时不会被实际绑定。
三、泛型类
泛型类的声明方式如下:
class Bag[T]: ...
该语法等价于:
annotation-def TYPE_PARAMS_OF_Bag(): T = typing.TypeVar("T") class Bag(typing.Generic[T]): __type_params__ = (T,) ... return Bag Bag = TYPE_PARAMS_OF_Bag()
这里还是用 annotation-def (不是真正的关键字) 指明 标注作用域,而名称TYPE_PARAMS_OF_Bag 在不会运行时实际被绑定。
泛型类隐式地继承自 typing.Generic。 泛型类的基类和关键字参数在类型形参的类型作用域内进行求值,而装饰器则在该作用域之外进行求值。 以下示例对此进行了说明:
@decorator class Bag(Base[T], arg=T): ...
这相当于:
annotation-def TYPE_PARAMS_OF_Bag(): T = typing.TypeVar("T") class Bag(Base[T], typing.Generic[T], arg=T): __type_params__ = (T,) ... return Bag Bag = decorator(TYPE_PARAMS_OF_Bag())
四、泛型类型别名
type 语句也可被用来创建泛型类型别名:
type ListOrSet[T] = list[T] | set[T]
除了会对值执行 惰性求值 以外,这等同于:
annotation-def TYPE_PARAMS_OF_ListOrSet(): T = typing.TypeVar("T") annotation-def VALUE_OF_ListOrSet(): return list[T] | set[T] # In reality, the value is lazily evaluated return typing.TypeAliasType("ListOrSet", VALUE_OF_ListOrSet(), type_params=(T,)) ListOrSet = TYPE_PARAMS_OF_ListOrSet()
- 这里annotation-def (不是一个真正的关键字) 指明标注作用域;
- 像TYPE_PARAMS_OF_ListOrSet 这样的大写名称不会在运行时实际被绑定。