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

Python 增强提案

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 是一份历史文档:请参阅 类型参数的默认值类型参数列表 以获取最新规范和文档。规范的类型规范在 类型规范网站 上维护;运行时类型行为在 CPython 文档中描述。

×

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

摘要

本 PEP 引入了类型参数的类型默认值概念,包括 TypeVarParamSpecTypeVarTuple,它们作为未指定类型的类型参数的默认值。

一些流行语言,如 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。我建议将 stub definition 修改为类似这样

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 — 例如 ndarraydtype 的默认值将是 float64。目前它是 UnknownAny
  • 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 相同的语法定义,但使用类型 list 或省略号字面量 “...” 或另一个作用域内的 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,依此类推)。

这可以在 builtins.slice 上使用,其中 start 参数应默认为 intstop 默认为 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__

当使用一个类型参数作为另一个类型参数的默认值时,适用以下规则,其中 T1T2 的默认值。

作用域规则

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]

特殊化规则

类型参数目前无法进一步下标。如果 高阶 TypeVars 得到实现,这可能会改变。

Generic TypeAliases

Generic TypeAliases 应该能够按照正常的下标规则进一步下标。如果一个类型参数有一个未被覆盖的默认值,它应该被视为已替换到 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 TypeAliases。也就是说,子类可以按照正常的下标规则进一步下标,未被覆盖的默认值应该被替换,并且带有此类默认值的类型参数可以在后续进一步特殊化。

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

使用 bounddefault

如果同时传递 bounddefault,则 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,如 PEP 646 中所规定。

这些原因导致目前没有有效的场景可以将 TypeVarTuple 用作另一个 TypeVarTuple 的默认值。

绑定规则

类型参数默认值应通过属性访问(包括调用和下标)进行绑定。

class Foo[T = int]:
    def meth(self) -> Self:
        return self

reveal_type(Foo.meth)  # type is (self: Foo[int]) -> Foo[int]

实施

在运行时,这将涉及对 typing 模块进行以下更改。

  • TypeVarParamSpecTypeVarTuple 类应该公开传递给 default 的类型。这将作为 __default__ 属性提供,如果未传递参数,则为 None;如果 default=None,则为 NoneType

对两个 GenericAliases 都需要进行以下更改

  • 用于确定下标所需默认值的逻辑。
  • 理想情况下,用于确定下标(如 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 and Jakub Kuczys


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

最后修改: 2024-09-03 17:24:02 GMT