PEP 647 – 用户定义的类型守卫
- 作者:
- Eric Traut <erictr at microsoft.com>
- 发起人:
- Guido van Rossum <guido at python.org>
- 讨论至:
- Typing-SIG 邮件列表
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 主题:
- 类型标注
- 创建日期:
- 2020年10月7日
- Python 版本:
- 3.10
- 发布历史:
- 2020年12月28日,2021年4月9日
- 决议:
- Python-Dev 帖子
摘要
本 PEP 规定了一种程序影响类型检查器根据运行时检查进行的条件类型收窄的方式。
动机
静态类型检查器通常采用一种称为“类型收窄”的技术来确定程序代码流中表达式的更精确类型。当基于条件代码流语句(例如 if 和 while 语句)在代码块内应用类型收窄时,条件表达式有时被称为“类型守卫”。Python 类型检查器通常支持各种形式的类型守卫表达式。
def func(val: Optional[str]):
# "is None" type guard
if val is not None:
# Type of val is narrowed to str
...
else:
# Type of val is narrowed to None
...
def func(val: Optional[str]):
# Truthy type guard
if val:
# Type of val is narrowed to str
...
else:
# Type of val remains Optional[str]
...
def func(val: Union[str, float]):
# "isinstance" type guard
if isinstance(val, str):
# Type of val is narrowed to str
...
else:
# Type of val is narrowed to float
...
def func(val: Literal[1, 2]):
# Comparison type guard
if val == 1:
# Type of val is narrowed to Literal[1]
...
else:
# Type of val is narrowed to Literal[2]
...
在某些情况下,仅凭静态信息无法应用类型收窄。考虑以下示例:
def is_str_list(val: List[object]) -> bool:
"""Determines whether all objects in the list are strings"""
return all(isinstance(x, str) for x in val)
def func1(val: List[object]):
if is_str_list(val):
print(" ".join(val)) # Error: invalid type
这段代码是正确的,但类型检查器会报告一个类型错误,因为传递给 join 方法的值 val 被理解为 List[object] 类型。类型检查器没有足够的信息来静态验证此时 val 的类型是 List[str]。
本 PEP 引入了一种将像 is_str_list 这样的函数定义为“用户定义的类型守卫”的方法。这允许代码扩展类型检查器支持的类型守卫。
使用这种新机制,上述示例中的 is_str_list 函数将被稍作修改。其返回类型将从 bool 更改为 TypeGuard[List[str]]。这不仅保证返回值是布尔值,而且 true 表示函数的输入是指定类型。
from typing import TypeGuard
def is_str_list(val: List[object]) -> TypeGuard[List[str]]:
"""Determines whether all objects in the list are strings"""
return all(isinstance(x, str) for x in val)
用户定义的类型守卫还可以用于确定字典是否符合 TypedDict 的类型要求。
class Person(TypedDict):
name: str
age: int
def is_person(val: dict) -> "TypeGuard[Person]":
try:
return isinstance(val["name"], str) and isinstance(val["age"], int)
except KeyError:
return False
def print_age(val: dict):
if is_person(val):
print(f"Age: {val['age']}")
else:
print("Not a person!")
规范
TypeGuard 类型
本 PEP 引入了从 typing 模块导出的符号 TypeGuard。TypeGuard 是一种特殊形式,接受一个单一的类型参数。它用于标注用户定义的类型守卫函数的返回类型。类型守卫函数中的 return 语句应返回布尔值,类型检查器应验证所有返回路径都返回布尔值。
在所有其他方面,TypeGuard 是一个与 bool 不同的类型。它不是 bool 的子类型。因此,Callable[..., TypeGuard[int]] 不能赋值给 Callable[..., bool]。
当 TypeGuard 用于标注接受至少一个参数的函数或方法的返回类型时,类型检查器将该函数或方法视为用户定义的类型守卫。TypeGuard 提供的类型参数表示已由函数验证的类型。
用户定义的类型守卫可以是泛型函数,如本例所示:
_T = TypeVar("_T")
def is_two_element_tuple(val: Tuple[_T, ...]) -> TypeGuard[Tuple[_T, _T]]:
return len(val) == 2
def func(names: Tuple[str, ...]):
if is_two_element_tuple(names):
reveal_type(names) # Tuple[str, str]
else:
reveal_type(names) # Tuple[str, ...]
类型检查器应假定类型收窄应用于作为第一个位置参数传递给用户定义的类型守卫的表达式。如果类型守卫函数接受多个参数,则不对这些额外参数表达式应用类型收窄。
如果类型守卫函数作为实例方法或类方法实现,则第一个位置参数映射到第二个参数(在“self”或“cls”之后)。
以下是一些接受多个参数的用户定义的类型守卫函数的示例:
def is_str_list(val: List[object], allow_empty: bool) -> TypeGuard[List[str]]:
if len(val) == 0:
return allow_empty
return all(isinstance(x, str) for x in val)
_T = TypeVar("_T")
def is_set_of(val: Set[Any], type: Type[_T]) -> TypeGuard[Set[_T]]:
return all(isinstance(x, type) for x in val)
用户定义的类型守卫函数的返回类型通常会引用一个比第一个参数类型“更窄”的类型(也就是说,它是一个更具体的类型,可以赋值给更一般的类型)。然而,不要求返回类型必须严格更窄。这允许像上面示例中 List[str] 不能赋值给 List[object] 的情况,因为不变性规则。
当条件语句包含对用户定义的类型守卫函数的调用,并且该函数返回 true 时,静态类型检查器应假定作为第一个位置参数传递给类型守卫函数的表达式采用 TypeGuard 返回类型中指定的类型,除非并在条件代码块中进一步收窄。
一些内置的类型守卫为正向和负向测试(在 if 和 else 子句中)提供收窄。例如,考虑形式为 x is None 的表达式的类型守卫。如果 x 的类型是 None 和其他一些类型的联合,它将在正向情况中收窄为 None,在负向情况中收窄为其他类型。用户定义的类型守卫仅在正向情况(if 子句)中应用收窄。在负向情况中不收窄类型。
OneOrTwoStrs = Union[Tuple[str], Tuple[str, str]]
def func(val: OneOrTwoStrs):
if is_two_element_tuple(val):
reveal_type(val) # Tuple[str, str]
...
else:
reveal_type(val) # OneOrTwoStrs
...
if not is_two_element_tuple(val):
reveal_type(val) # OneOrTwoStrs
...
else:
reveal_type(val) # Tuple[str, str]
...
向后兼容性
不使用此新功能的现有代码将不受影响。
值得注意的是,以与标准库类型库不兼容的方式使用注解的代码不应导入 TypeGuard。
参考实现
Pyright 类型检查器支持本 PEP 中描述的行为。
被拒绝的想法
装饰器语法
曾考虑使用装饰器来定义类型守卫。
@type_guard(List[str])
def is_str_list(val: List[object]) -> bool: ...
装饰器方法较差,因为它需要运行时评估类型,排除了前向引用。提议的方法也被认为更容易理解和实现。
强制严格收窄
曾考虑强制执行严格的类型收窄(要求 TypeGuard 类型参数中指定的类型是为第一个参数指定的类型的更窄形式),但这消除了此功能的有价值用例。例如,上面的 is_str_list 示例将被视为无效,因为由于不变性规则,List[str] 不是 List[object] 的子类型。
曾考虑的一种变体是默认要求严格收窄,但允许类型守卫函数指定某个标志以指示其不遵循此要求。这被拒绝了,因为它被认为繁琐且不必要。
另一个考虑是定义一些不那么严格的检查,以确保值类型和 TypeGuard 中指定的收窄类型之间存在一些重叠。这个提案的问题在于,考虑到联合、协议、类型变量、泛型等,类型兼容性的规则已经非常复杂。仅仅为了这个特性而定义这些规则的一个变体,放宽其中一些约束,将要求我们阐明这些规则在哪些微妙方面有所不同以及在何种特定情况下约束被放宽。因此,决定省略所有检查。
有人指出,如果不强制执行严格收窄,可能会破坏类型安全。一个编写不佳的类型守卫函数可能会产生不安全甚至荒谬的结果。例如:
def f(value: int) -> TypeGuard[str]:
return True
然而,有许多方法可以被坚决或不知情的开发人员用来颠覆类型安全——最常见的是通过使用 cast 或 Any。如果一个 Python 开发人员花时间学习并在其代码中实现用户定义的类型守卫,可以放心地假设他们对类型安全感兴趣,并且不会以破坏类型安全或产生荒谬结果的方式编写其类型守卫函数。
有条件地应用 TypeGuard 类型
曾建议,如果表达式的类型是 TypeGuard 返回类型中指定的类型的正确子类型,则作为第一个参数传递给类型守卫函数的表达式应保留其现有类型。例如,如果类型守卫函数是 def f(value: object) -> TypeGuard[float],并且传递给此函数的表达式是 int 类型,它将保留 int 类型,而不是采用 TypeGuard 返回类型指示的 float 类型。这个提案被拒绝了,因为它增加了复杂性、不一致性,并引发了关于如果表达式的类型是联合或具有多个约束的类型变量等复合类型时的正确行为的其他问题。鉴于它几乎不提供或不提供附加价值,因此决定不增加额外的复杂性和不一致性。
任意参数的收窄
TypeScript 中用户定义类型守卫的实现允许任何输入参数用作测试收窄的值。TypeScript 语言的作者无法回忆起 TypeScript 中任何实际示例,其中被测试的参数不是第一个参数。因此,决定不必为 Python 中用户定义类型守卫的实现增加额外的复杂性来支持一个人为的用例。如果将来发现此类用例,TypeGuard 机制可以通过多种方式进行扩展。这可能涉及使用关键字索引,如 PEP 637 中所提议的。
隐式“self”和“cls”参数的收窄
该提案指出,第一个位置参数被假定为用于收窄测试的值。如果类型守卫函数作为实例方法或类方法实现,一个隐式的 self 或 cls 参数也将传递给函数。有人担心在某些情况下,希望将收窄逻辑应用于 self 和 cls。这是一个不寻常的用例,而容纳它将显著复杂化用户定义类型守卫的实现。因此,决定不对其做特殊规定。如果需要对 self 或 cls 进行收窄,则该值可以作为显式参数传递给类型守卫函数。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0647.rst