PEP 696 – 类型参数的默认值
- 作者:
- James Hilton-Balfe <gobot1234yt at gmail.com>
- 赞助者:
- Jelle Zijlstra <jelle.zijlstra at gmail.com>
- 讨论列表:
- Discourse 线程
- 状态:
- 最终
- 类型:
- 标准跟踪
- 主题:
- 类型提示
- 创建:
- 2022年7月14日
- Python 版本:
- 3.13
- 历史记录:
- 2022年3月22日, 2023年1月8日
- 决议:
- Discourse 消息
摘要
此 PEP 引入了类型参数默认值的概念,包括 TypeVar
、ParamSpec
和 TypeVarTuple
,它们充当未指定类型的类型参数的默认值。
一些流行的语言(如 C++、TypeScript 和 Rust)都支持默认类型参数。 PEP 695 的作者对一些常用语言中的类型参数语法进行了调查,并在其 附录 A 中进行了介绍。
动机
T = TypeVar("T", default=int) # This means that if no type is specified T = int
@dataclass
class Box(Generic[T]):
value: T | None = None
reveal_type(Box()) # type is Box[int]
reveal_type(Box(value="Hello World!")) # type is Box[str]
一个 经常出现 的地方是 Generator
。我建议将存根定义更改为类似以下内容
YieldT = TypeVar("YieldT")
SendT = TypeVar("SendT", default=None)
ReturnT = TypeVar("ReturnT", default=None)
class Generator(Generic[YieldT, SendT, ReturnT]): ...
Generator[int] == Generator[int, None] == Generator[int, None, None]
这对于通常只有一种类型的 Generic
也很有用。
class Bot: ...
BotT = TypeVar("BotT", bound=Bot, default=Bot)
class Context(Generic[BotT]):
bot: BotT
class MyBot(Bot): ...
reveal_type(Context().bot) # type is Bot # notice this is not Any which is what it would be currently
reveal_type(Context[MyBot]().bot) # type is MyBot
这不仅可以改善显式使用它的用户的类型提示,还可以帮助那些依赖于自动完成的非类型提示用户加快开发速度。
这种设计模式在以下项目中很常见:
- discord.py — 上述示例取自此处。
- NumPy — 对于
ndarray
的dtype
等类型,默认值为float64
。目前为Unknown
或Any
。 - TensorFlow — 这可以用于 Tensor,类似于
numpy.ndarray
,并且有助于简化Layer
的定义。
规范
默认顺序和订阅规则
默认值的顺序应遵循标准函数参数规则,因此没有 default
的类型参数不能位于具有 default
值的类型参数之后。这样做理想情况下应该在 typing._GenericAlias
/types.GenericAlias
中引发 TypeError
,并且类型检查器应将其标记为错误。
DefaultStrT = TypeVar("DefaultStrT", default=str)
DefaultIntT = TypeVar("DefaultIntT", default=int)
DefaultBoolT = TypeVar("DefaultBoolT", default=bool)
T = TypeVar("T")
T2 = TypeVar("T2")
class NonDefaultFollowsDefault(Generic[DefaultStrT, T]): ... # Invalid: non-default TypeVars cannot follow ones with defaults
class NoNonDefaults(Generic[DefaultStrT, DefaultIntT]): ...
(
NoNoneDefaults ==
NoNoneDefaults[str] ==
NoNoneDefaults[str, int]
) # All valid
class OneDefault(Generic[T, DefaultBoolT]): ...
OneDefault[float] == OneDefault[float, bool] # Valid
reveal_type(OneDefault) # type is type[OneDefault[T, DefaultBoolT = bool]]
reveal_type(OneDefault[float]()) # type is OneDefault[float, bool]
class AllTheDefaults(Generic[T1, T2, DefaultStrT, DefaultIntT, DefaultBoolT]): ...
reveal_type(AllTheDefaults) # type is type[AllTheDefaults[T1, T2, DefaultStrT = str, DefaultIntT = int, DefaultBoolT = bool]]
reveal_type(AllTheDefaults[int, complex]()) # type is AllTheDefaults[int, complex, str, int, bool]
AllTheDefaults[int] # Invalid: expected 2 arguments to AllTheDefaults
(
AllTheDefaults[int, complex] ==
AllTheDefaults[int, complex, str] ==
AllTheDefaults[int, complex, str, int] ==
AllTheDefaults[int, complex, str, int, bool]
) # All valid
使用 Python 3.12 中新的泛型语法(由 PEP 695 引入),可以在编译时执行此操作。
type Alias[DefaultT = int, T] = tuple[DefaultT, T] # SyntaxError: non-default TypeVars cannot follow ones with defaults
def generic_func[DefaultT = int, T](x: DefaultT, y: T) -> None: ... # SyntaxError: non-default TypeVars cannot follow ones with defaults
class GenericClass[DefaultT = int, T]: ... # SyntaxError: non-default TypeVars cannot follow ones with defaults
ParamSpec
默认值
ParamSpec
默认值使用与 TypeVar
相同的语法定义,但使用类型列表或省略号文字“...
”或另一个作用域内的 ParamSpec
(请参阅 作用域规则)。
DefaultP = ParamSpec("DefaultP", default=[str, int])
class Foo(Generic[DefaultP]): ...
reveal_type(Foo) # type is type[Foo[DefaultP = [str, int]]]
reveal_type(Foo()) # type is Foo[[str, int]]
reveal_type(Foo[[bool, bool]]()) # type is Foo[[bool, bool]]
TypeVarTuple
默认值
TypeVarTuple
默认值使用与 TypeVar
相同的语法定义,但使用解包的类型元组而不是单个类型或另一个作用域内的 TypeVarTuple
(请参阅 作用域规则)。
DefaultTs = TypeVarTuple("DefaultTs", default=Unpack[tuple[str, int]])
class Foo(Generic[*DefaultTs]): ...
reveal_type(Foo) # type is type[Foo[DefaultTs = *tuple[str, int]]]
reveal_type(Foo()) # type is Foo[str, int]
reveal_type(Foo[int, bool]()) # type is Foo[int, bool]
使用另一个类型参数作为 default
这允许在泛型的类型参数缺失但指定了另一个类型参数时再次使用某个值。
要使用另一个类型参数作为默认值,default
和类型参数必须是同一类型(TypeVar
的默认值必须是 TypeVar
等)。
这可以在内置的 slice 上使用,其中 start
参数默认为 int
,stop
默认为 start
的类型,step
默认为 int | None
。
StartT = TypeVar("StartT", default=int)
StopT = TypeVar("StopT", default=StartT)
StepT = TypeVar("StepT", default=int | None)
class slice(Generic[StartT, StopT, StepT]): ...
reveal_type(slice) # type is type[slice[StartT = int, StopT = StartT, StepT = int | None]]
reveal_type(slice()) # type is slice[int, int, int | None]
reveal_type(slice[str]()) # type is slice[str, str, int | None]
reveal_type(slice[str, bool, timedelta]()) # type is slice[str, bool, timedelta]
T2 = TypeVar("T2", default=DefaultStrT)
class Foo(Generic[DefaultStrT, T2]):
def __init__(self, a: DefaultStrT, b: T2) -> None: ...
reveal_type(Foo(1, "")) # type is Foo[int, str]
Foo[int](1, "") # Invalid: Foo[int, str] cannot be assigned to self: Foo[int, int] in Foo.__init__
Foo[int]("", 1) # Invalid: Foo[str, int] cannot be assigned to self: Foo[int, int] in Foo.__init__
当使用类型参数作为另一个类型参数的默认值时,以下规则适用,其中 T1
是 T2
的默认值。
作用域规则
T1
必须在泛型的参数列表中先于 T2
使用。
T2 = TypeVar("T2", default=T1)
class Foo(Generic[T1, T2]): ... # Valid
class Foo(Generic[T1]):
class Bar(Generic[T2]): ... # Valid
StartT = TypeVar("StartT", default="StopT") # Swapped defaults around from previous example
StopT = TypeVar("StopT", default=int)
class slice(Generic[StartT, StopT, StepT]): ...
# ^^^^^^ Invalid: ordering does not allow StopT to be bound
不支持使用来自外部作用域的类型参数作为默认值。
绑定规则
T1
的绑定必须是 T2
的绑定的子类型。
T1 = TypeVar("T1", bound=int)
TypeVar("Ok", default=T1, bound=float) # Valid
TypeVar("AlsoOk", default=T1, bound=int) # Valid
TypeVar("Invalid", default=T1, bound=str) # Invalid: int is not a subtype of str
约束规则
T2
的约束必须是 T1
的约束的超集。
T1 = TypeVar("T1", bound=int)
TypeVar("Invalid", float, str, default=T1) # Invalid: upper bound int is incompatible with constraints float or str
T1 = TypeVar("T1", int, str)
TypeVar("AlsoOk", int, str, bool, default=T1) # Valid
TypeVar("AlsoInvalid", bool, complex, default=T1) # Invalid: {bool, complex} is not a superset of {int, str}
类型参数作为泛型的参数
当第一个参数在作用域内(由 上一节 确定)时,类型参数作为泛型内部 default
的参数是有效的。
T = TypeVar("T")
ListDefaultT = TypeVar("ListDefaultT", default=list[T])
class Bar(Generic[T, ListDefaultT]):
def __init__(self, x: T, y: ListDefaultT): ...
reveal_type(Bar) # type is type[Bar[T, ListDefaultT = list[T]]]
reveal_type(Bar[int]) # type is type[Bar[int, list[int]]]
reveal_type(Bar[int]()) # type is Bar[int, list[int]]
reveal_type(Bar[int, list[str]]()) # type is Bar[int, list[str]]
reveal_type(Bar[int, str]()) # type is Bar[int, str]
专门化规则
类型参数当前不能被进一步下标。如果实现了 高阶类型变量,这一点可能会改变。
Generic
TypeAlias
Generic
TypeAlias
应该能够按照正常的订阅规则进一步进行下标。如果类型参数具有未被覆盖的默认值,则应将其视为已替换到 TypeAlias
中。但是,它可以在以后进一步专门化。
class SomethingWithNoDefaults(Generic[T, T2]): ...
MyAlias: TypeAlias = SomethingWithNoDefaults[int, DefaultStrT] # Valid
reveal_type(MyAlias) # type is type[SomethingWithNoDefaults[int, DefaultStrT]]
reveal_type(MyAlias[bool]()) # type is SomethingWithNoDefaults[int, bool]
MyAlias[bool, int] # Invalid: too many arguments passed to MyAlias
子类化
具有带有默认值的类型参数的 Generic
的子类行为类似于 Generic
TypeAlias
。也就是说,子类可以按照正常的订阅规则进一步进行下标,未被覆盖的默认值应被替换,并且具有此类默认值的类型参数可以在以后进一步专门化。
class SubclassMe(Generic[T, DefaultStrT]):
x: DefaultStrT
class Bar(SubclassMe[int, DefaultStrT]): ...
reveal_type(Bar) # type is type[Bar[DefaultStrT = str]]
reveal_type(Bar()) # type is Bar[str]
reveal_type(Bar[bool]()) # type is Bar[bool]
class Foo(SubclassMe[float]): ...
reveal_type(Foo().x) # type is str
Foo[str] # Invalid: Foo cannot be further subscripted
class Baz(Generic[DefaultIntT, DefaultStrT]): ...
class Spam(Baz): ...
reveal_type(Spam()) # type is <subclass of Baz[int, str]>
使用 bound
和 default
如果同时传递了 bound
和 default
,则 default
必须是 bound
的子类型。否则,类型检查器应生成错误。
TypeVar("Ok", bound=float, default=int) # Valid
TypeVar("Invalid", bound=str, default=int) # Invalid: the bound and default are incompatible
约束
对于受约束的 TypeVar
,默认值需要是约束之一。即使它是约束之一的子类型,类型检查器也应该生成错误。
TypeVar("Ok", float, str, default=float) # Valid
TypeVar("Invalid", float, str, default=int) # Invalid: expected one of float or str got int
函数默认值
在泛型函数中,当类型参数无法解析为任何内容时,类型检查器可以使用类型参数的默认值。我们没有指定此用法的语义,因为确保在类型参数无法解析的每个代码路径中都返回 default
可能难以实现。类型检查器可以自由地禁止这种情况或尝试实现支持。
T = TypeVar('T', default=int)
def func(x: int | set[T]) -> T: ...
reveal_type(func(0)) # a type checker may reveal T's default of int here
跟随 TypeVarTuple
的默认值
紧跟在 TypeVarTuple
之后的 TypeVar
不允许具有默认值,因为这将导致类型参数是绑定到 TypeVarTuple
还是绑定到具有默认值的 TypeVar
变得模棱两可。
Ts = TypeVarTuple("Ts")
T = TypeVar("T", default=bool)
class Foo(Generic[Ts, T]): ... # Type checker error
# Could be reasonably interpreted as either Ts = (int, str, float), T = bool
# or Ts = (int, str), T = float
Foo[int, str, float]
使用 Python 3.12 内置的泛型语法,这种情况应该会引发 SyntaxError。
但是,允许在具有默认值的 TypeVarTuple
之后具有带有默认值的 ParamSpec
,因为 ParamSpec
和 TypeVarTuple
的类型参数之间不会有任何歧义。
Ts = TypeVarTuple("Ts")
P = ParamSpec("P", default=[float, bool])
class Foo(Generic[Ts, P]): ... # Valid
Foo[int, str] # Ts = (int, str), P = [float, bool]
Foo[int, str, [bytes]] # Ts = (int, str), P = [bytes]
子类型化
类型参数的默认值不会影响泛型类的子类型化规则。特别是,在考虑类是否与泛型协议兼容时,可以忽略默认值。
TypeVarTuple
作为默认值
不支持使用 TypeVarTuple
作为默认值,因为
由于这些原因,目前没有有效的位置可以使用TypeVarTuple
作为另一个TypeVarTuple
的默认值。
绑定规则
类型参数的默认值应该通过属性访问(包括调用和下标)来绑定。
class Foo[T = int]:
def meth(self) -> Self:
return self
reveal_type(Foo.meth) # type is (self: Foo[int]) -> Foo[int]
实现
在运行时,这将涉及对typing
模块进行以下更改。
- 类
TypeVar
、ParamSpec
和TypeVarTuple
应该公开传递给default
的类型。这将作为__default__
属性可用,如果未传递参数,则为None
,如果default=None
,则为NoneType
。
以下更改需要同时应用于GenericAlias
es
- 用于确定订阅所需的默认值的逻辑。
- 理想情况下,用于确定订阅(如
Generic[T, DefaultT]
)是否有效的逻辑。
类型参数列表的语法需要更新以允许默认值;请参见下文。
运行时更改的参考实现可以在https://github.com/Gobot1234/cpython/tree/pep-696找到。
类型检查器的参考实现可以在https://github.com/Gobot1234/mypy/tree/TypeVar-defaults找到。
Pyright 目前支持此功能。
语法更改
在PEP 695中添加的语法将扩展为引入一种方法,使用方括号内的“=”运算符指定类型参数的默认值,如下所示
# TypeVars
class Foo[T = str]: ...
# ParamSpecs
class Baz[**P = [int, str]]: ...
# TypeVarTuples
class Qux[*Ts = *tuple[int, bool]]: ...
# TypeAliases
type Foo[T, U = str] = Bar[T, U]
type Baz[**P = [int, str]] = Spam[**P]
type Qux[*Ts = *tuple[str]] = Ham[*Ts]
type Rab[U, T = str] = Bar[T, U]
与类型参数的边界类似,默认值应该延迟计算,并遵循相同的范围规则,以避免对它们进行不必要的引用。
此功能包含在PEP 695的初始草案中,但由于范围蔓延而被删除。
将对语法进行以下更改
type_param:
| a=NAME b=[type_param_bound] d=[type_param_default]
| a=NAME c=[type_param_constraint] d=[type_param_default]
| '*' a=NAME d=[type_param_default]
| '**' a=NAME d=[type_param_default]
type_param_default:
| '=' e=expression
| '=' e=starred_expression
编译器将强制执行以下规则:没有默认值的类型参数不能跟随有默认值的类型参数,并且带有默认值的TypeVar
不能紧跟TypeVarTuple
。
被拒绝的替代方案
允许类型参数的默认值传递给 type.__new__
的 **kwargs
T = TypeVar("T")
@dataclass
class Box(Generic[T], T=int):
value: T | None = None
虽然这更容易阅读,并且遵循与TypeVar
一元语法类似的原理,但它不向后兼容,因为T
可能已经被传递给元类/超类或支持在运行时不子类化Generic
的类。
理想情况下,如果PEP 637没有被拒绝,以下将是可以接受的
T = TypeVar("T")
@dataclass
class Box(Generic[T = int]):
value: T | None = None
允许非默认值跟随默认值
YieldT = TypeVar("YieldT", default=Any)
SendT = TypeVar("SendT", default=Any)
ReturnT = TypeVar("ReturnT")
class Coroutine(Generic[YieldT, SendT, ReturnT]): ...
Coroutine[int] == Coroutine[Any, Any, int]
允许非默认值跟随默认值将缓解从函数返回诸如Coroutine
之类的类型的问题,其中最常用的类型参数是最后一个(返回值)。允许非默认值跟随默认值过于混乱且可能存在歧义,即使只有上述两种形式有效。现在更改参数顺序也会破坏许多代码库。这在大多数情况下也可以使用TypeAlias
解决。
Coro: TypeAlias = Coroutine[Any, Any, T]
Coro[int] == Coroutine[Any, Any, int]
使 default
隐式地成为 bound
在此 PEP 的早期版本中,如果未为default
传递值,则default
隐式设置为bound
。虽然方便,但这可能会导致没有默认值的类型参数跟随有默认值的类型参数。考虑
T = TypeVar("T", bound=int) # default is implicitly int
U = TypeVar("U")
class Foo(Generic[T, U]):
...
# would expand to
T = TypeVar("T", bound=int, default=int)
U = TypeVar("U")
class Foo(Generic[T, U]):
...
这也将对少数情况下代码依赖Any
作为隐式默认值的代码造成破坏性更改。
允许在函数签名中使用带有默认值的类型参数
此 PEP 的先前版本允许在函数签名中使用带有默认值的TypeVarLike
。由于函数默认值中描述的原因,此功能已被删除。如果将来添加了一种获取类型参数运行时值的方法,希望可以添加此功能。
允许在 default
中使用来自外部作用域的类型参数
这被认为是一个过于小众的功能,不值得增加复杂性。如果出现任何需要此功能的情况,可以在未来的 PEP 中添加。
鸣谢
感谢以下人员对 PEP 的反馈
Eric Traut、Jelle Zijlstra、Joshua Butt、Danny Yamamoto、Kaylynn Morgan 和 Jakub Kuczys
版权
本文档放置在公共领域或根据 CC0-1.0-Universal 许可证,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0696.rst