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

Python 增强提案

PEP 724 – 更严格的类型守卫

作者:
Rich Chiodo <rchiodo at microsoft.com>, Eric Traut <erictr at microsoft.com>, Erik De Bonte <erikd at microsoft.com>
赞助商:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
讨论地址:
Discourse 主题
状态:
已撤回
类型:
标准跟踪
主题:
类型
创建日期:
2023-07-28
Python 版本:
3.13
历史记录:
2021-12-30, 2023-09-19

目录

状态

该 PEP 已撤回。类型委员会未能就该提案达成共识,作者决定撤回它。

摘要

PEP 647 引入了用户定义类型守卫函数的概念,该函数返回 True,如果传递给其第一个参数的表达式的类型与其返回的 TypeGuard 类型匹配。例如,返回类型为 TypeGuard[str] 的函数被假定为仅当传递给其第一个输入参数的表达式的类型为 str 时才返回 True。这允许类型检查器在用户定义的类型守卫函数返回 True 时缩小类型。

该 PEP 完善了 PEP 647 中引入的 TypeGuard 机制。它允许类型检查器在用户定义的类型守卫函数返回 False 时缩小类型。它还允许类型检查器在某些情况下(当类型守卫函数返回 True 时)应用额外的(更精确的)类型缩小。

动机

用户定义的类型守卫函数使类型检查器能够在将表达式作为参数传递给类型守卫函数时缩小表达式的类型。 PEP 647 中引入的 TypeGuard 机制很灵活,但这种灵活性也带来了一些限制,开发人员发现这些限制在某些情况下很不方便。

限制 1:类型检查器不允许在类型守卫函数返回 False 的情况下缩小类型。这意味着在负(“else”)子句中类型不会被缩小。

限制 2:类型检查器必须使用 TypeGuard 返回类型,如果类型守卫函数返回 True,无论是否可以根据对预缩小类型了解的信息应用额外的缩小。

以下代码示例演示了这两个限制。

def is_iterable(val: object) -> TypeGuard[Iterable[Any]]:
    return isinstance(val, Iterable)

def func(val: int | list[int]):
    if is_iterable(val):
        # The type is narrowed to 'Iterable[Any]' as dictated by
        # the TypeGuard return type
        reveal_type(val)  # Iterable[Any]
    else:
        # The type is not narrowed in the "False" case
        reveal_type(val)  # int | list[int]

    # If "isinstance" is used in place of the user-defined type guard
    # function, the results differ because type checkers apply additional
    # logic for "isinstance"

    if isinstance(val, Iterable):
        # Type is narrowed to "list[int]" because this is
        # a narrower (more precise) type than "Iterable[Any]"
        reveal_type(val)  # list[int]
    else:
        # Type is narrowed to "int" because the logic eliminates
        # "list[int]" from the original union
        reveal_type(val)  # int

PEP 647 强制实施了这些限制,以便它可以支持返回 TypeGuard 类型不是输入类型子类型的用例。有关示例,请参阅 PEP 647

原理

在许多情况下,更严格的 TypeGuard 本可以成为解决方案

规范

用户定义的类型守卫函数的使用涉及五种类型

  • I = TypeGuard 输入类型
  • R = TypeGuard 返回类型
  • A = 传递给类型守卫函数的参数的类型(预缩小)
  • NP = 缩小的类型(正)
  • NN = 缩小的类型(负)
def guard(x: I) -> TypeGuard[R]: ...

def func1(val: A):
    if guard(val):
        reveal_type(val)  # NP
    else:
        reveal_type(val)  # NN

该 PEP 提出对 PEP 647 进行一些修改,以解决上述限制。只有满足特定条件时,才能安全地消除这些限制。特别是,当用户定义的类型守卫函数的输出类型 R 与其第一个输入参数 (I) 的类型一致 [1] 时,类型检查器应该应用更严格的类型守卫语义。

# Stricter type guard semantics are used in this case because
# "Kangaroo | Koala" is consistent with "Animal"
def is_marsupial(val: Animal) -> TypeGuard[Kangaroo | Koala]:
    return isinstance(val, Kangaroo | Koala)

# Stricter type guard semantics are not used in this case because
# "list[T]"" is not consistent with "list[T | None]"
def has_no_nones(val: list[T | None]) -> TypeGuard[list[T]]:
    return None not in val

当应用更严格的类型守卫语义时,用户定义的类型守卫函数的应用会以两种方式改变。

  • 在负(“else”)情况下应用类型缩小。
def is_str(val: str | int) -> TypeGuard[str]:
    return isinstance(val, str)

def func(val: str | int):
    if not is_str(val):
        reveal_type(val)  # int
  • 如果适用,在正“if”情况下应用额外的类型缩小。
def is_cardinal_direction(val: str) -> TypeGuard[Literal["N", "S", "E", "W"]]:
    return val in ("N", "S", "E", "W")

def func(direction: Literal["NW", "E"]):
    if is_cardinal_direction(direction):
        reveal_type(direction)  # "Literal[E]"
    else:
        reveal_type(direction)  # "Literal[NW]"

下表指定了类型缩小的类型理论规则。

非严格类型守卫 严格类型守卫
适用条件 R 与 I 不一致 R 与 I 一致
NP 是 .. R AR
NN 是 .. A A∧¬R

在实践中,严格类型守卫的理论类型无法在 Python 类型系统中精确表达。类型检查器应该回退到这些类型的实际近似值。根据经验,类型检查器应该使用与处理“isinstance”相同的类型缩小逻辑,并获得与之一致的结果。此指南允许在将来扩展类型系统时进行更改和改进。

其他示例

Any 与任何其他类型一致 [1],这意味着可以应用更严格的语义。

 # Stricter type guard semantics are used in this case because
 # "str" is consistent with "Any"
def is_str(x: Any) -> TypeGuard[str]:
    return isinstance(x, str)

def test(x: float | str):
    if is_str(x):
        reveal_type(x)  # str
    else:
        reveal_type(x)  # float

向后兼容性

该 PEP 提出改变 TypeGuard 的现有行为。这对运行时没有影响,但它确实改变了类型检查器评估的类型。

def is_int(val: int | str) -> TypeGuard[int]:
    return isinstance(val, int)

def func(val: int | str):
    if is_int(val):
        reveal_type(val)  # "int"
    else:
        reveal_type(val)  # Previously "int | str", now "str"

这种行为更改会导致类型检查器评估不同的类型。因此,它可能会产生新的(或掩盖现有的)类型错误。

类型检查器通常会改进缩小逻辑或修复此类逻辑中的现有错误,因此静态类型的用户将习惯于这种行为更改。

我们还假设,现有的带类型的 Python 代码不太可能依赖于 TypeGuard 的当前行为。为了验证我们的假设,我们在 pyright 中实现了提议的更改,并将此修改后的版本应用于大约 25 个带类型的代码库,使用 mypy primer 查看输出是否有任何差异。正如预测的那样,行为更改的影响很小。唯一值得注意的更改是,一些 # type: ignore 注释不再必要,这表明这些代码库已经绕过了 TypeGuard 的现有限制。

破坏性更改

用户定义的类型守卫函数有可能依赖于旧的行为。此类类型守卫函数可能会因新行为而中断。

def is_positive_int(val: int | str) -> TypeGuard[int]:
    return isinstance(val, int) and val > 0

def func(val: int | str):
    if is_positive_int(val):
        reveal_type(val)  # "int"
    else:
        # With the older behavior, the type of "val" is evaluated as
        # "int | str"; with the new behavior, the type is narrowed to
        # "str", which is perhaps not what was intended.
        reveal_type(val)

我们认为,此类用户定义的类型守卫在现实世界代码中不太可能存在。mypy primer 的结果没有发现任何此类案例。

如何教授

不熟悉 TypeGuard 的用户可能会期望本 PEP 中概述的行为,因此使 TypeGuard 更易于教授和解释。

参考实现

该想法的参考 实现 存在于 pyright 中。

要启用修改后的行为,必须将配置标志 enableExperimentalFeatures 设置为 true。这可以通过在每个文件中添加以下注释来实现

# pyright: enableExperimentalFeatures=true

被拒绝的方案

StrictTypeGuard

提出了一种新的 StrictTypeGuard 结构。这种替代形式类似于 TypeGuard,只是它会应用更严格的类型守卫语义。它还会强制执行返回类型与输入类型一致 [1]。有关详细信息,请参阅此主题:StrictTypeGuard 提案

该方案被拒绝,因为它在大多数情况下没有必要,并且增加了不必要的复杂性。它将需要引入新的特殊形式,开发人员需要学习这两种形式之间的细微差别。

带有第二个输出类型的 TypeGuard

另一个方案建议 TypeGuard 可以支持第二个可选的类型参数,该参数指示在负(“else”)情况下应该用于缩小的类型。

def is_int(val: int | str) -> TypeGuard[int, str]:
    return isinstance(val, int)

该方案在 此处 提出。

该方案被拒绝,因为它被认为过于复杂,并且只解决了 TypeGuard 的两个主要限制之一。有关完整的讨论,请参阅此 主题

脚注


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

最后修改时间:2024-08-20 10:29:32 GMT