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年7月28日
- Python 版本:
- 3.13
- 发布历史:
- 2021年12月30日, 2023年9月19日
状态
此PEP已被撤回。打字委员会未能就该提案达成共识,作者决定撤回该提案。
摘要
PEP 647 引入了用户定义的类型守卫函数的概念,该函数返回 True
,如果传递给其第一个参数的表达式的类型与其返回的 TypeGuard
类型匹配。例如,返回类型为 TypeGuard[str]
的函数被假定为当且仅当传递给其第一个输入参数的表达式的类型是 str
时返回 True
。这允许类型检查器在用户定义的类型守卫函数返回 True
时缩小类型。
此PEP改进了PEP 647 中引入的 TypeGuard
机制。它允许类型检查器在用户定义的类型守卫函数返回 False
时缩小类型。当类型守卫函数返回 True
且在某些情况下可以应用其他(更精确的)类型缩小的情况下,它还允许类型检查器应用额外的(更精确的)类型缩小。
动机
用户定义的类型守卫函数使用户能够缩小表达式的类型,当它作为参数传递给类型守卫函数时。 PEP 647 中引入的 TypeGuard
机制非常灵活,但这种灵活性带来了一些限制,开发人员在某些用途中发现这些限制不方便。
限制1:类型检查器不允许在类型守卫函数返回 False
的情况下缩小类型。这意味着在否定(“else”)子句中不会缩小类型。
限制2:如果类型守卫函数返回 True
,类型检查器必须使用 TypeGuard
返回类型,而不管是否可以根据预缩小类型的知识应用其他缩小。
以下代码示例演示了这两个限制。
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 | A∧R |
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中实现了此提议的更改,并使用mypy primer在约25个类型化代码库上运行了此修改版本,以查看输出是否有任何差异。正如预期的那样,行为变化的影响很小。唯一值得注意的变化是,一些 # 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 proposal
该想法被拒绝,因为它在大多数情况下是不必要的,并且增加了不必要的复杂性。它需要引入一个新的特殊形式,并且需要对开发人员进行关于这两种形式之间细微差别的教育。
带有第二个输出类型的TypeGuard
还提出了一个想法,即 TypeGuard
可以支持第二个可选的类型参数,该参数指示在否定(“else”)情况下用于缩小的类型。
def is_int(val: int | str) -> TypeGuard[int, str]:
return isinstance(val, int)
这个想法是在这里提出的。
被拒绝是因为它被认为过于复杂,并且只解决了 TypeGuard
的两个主要限制之一。有关完整讨论,请参阅此线程。
脚注
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0724.rst