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。我建议将存根定义更改为类似以下内容

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 相同的语法定义,但使用类型列表或省略号文字“...”或另一个作用域内的 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 参数默认为 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]

专门化规则

类型参数当前不能被进一步下标。如果实现了 高阶类型变量,这一点可能会改变。

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

使用 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,因为 ParamSpecTypeVarTuple 的类型参数之间不会有任何歧义。

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 作为默认值,因为

  • 作用域规则 不允许使用来自外部作用域的类型参数。
  • 根据PEP 646 的规定,单个对象的类型参数列表中不能出现多个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模块进行以下更改。

  • 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 和 Jakub Kuczys


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

上次修改:2024-09-03 17:24:02 GMT