Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 增强提案

PEP 647 - 用户定义类型守卫

作者:
Eric Traut <erictr at microsoft.com>
赞助商:
Guido van Rossum <guido at python.org>
讨论对象:
Typing-SIG 列表
状态:
最终
类型:
标准跟踪
主题:
类型
创建:
2020-10-07
Python 版本:
3.10
历史记录:
2020-12-28, 2021-04-09
决议:
Python-Dev 线程

目录

注意

此 PEP 是一份历史文档:请参阅 TypeGuardtyping.TypeGuard 获取最新规范和文档。规范的官方 typing 规范在 typing 规范站点 上维护;运行时类型行为在 CPython 文档中描述。

×

请参阅 typing 规范更新过程,了解如何建议更改 typing 规范。

摘要

此 PEP 指定了一种方法,允许程序根据运行时检查影响类型检查器使用的条件类型缩窄。

动机

静态类型检查器通常采用一种称为“类型缩窄”的技术来确定程序代码流中表达式更精确的类型。当基于条件代码流语句(例如 ifwhile 语句)在代码块中应用类型缩窄时,条件表达式有时被称为“类型守卫”。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 模块导出的符号 TypeGuardTypeGuard 是一种特殊形式,它接受单个类型参数。它用于注释用户定义类型守卫函数的返回类型。类型守卫函数中的 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 返回类型中指定的类型,除非和直到它在条件代码块内进一步缩窄。

一些内置类型守卫对正负测试(在 ifelse 子句中)都提供缩窄。例如,考虑形式为 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]
        ...

向后兼容性

不使用此新功能的现有代码将不受影响。

值得注意的是,以与 stdlib typing 库不兼容的方式使用注释的代码应该简单地不导入 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

但是,有许多方法可以破坏类型安全性——最常见的是使用 castAny。如果 Python 开发人员花时间了解并在代码中实现用户定义类型守卫,那么可以安全地假设他们对类型安全性感兴趣,并且不会以损害类型安全性或产生毫无意义的结果的方式编写类型守卫函数。

有条件地应用 TypeGuard 类型

有人建议,如果表达式的类型是 TypeGuard 返回类型中指定的类型的适当子类型,则传递给类型守卫函数的第一个参数的表达式应保留其现有类型。例如,如果类型守卫函数是 def f(value: object) -> TypeGuard[float],并且传递给此函数的表达式类型为 int,则它将保留 int 类型,而不是采用 TypeGuard 返回类型指示的 float 类型。该建议被拒绝,因为它增加了复杂性,不一致性,并引发了关于表达式类型为并集或具有多个约束的类型变量等复合类型时正确行为的更多问题。考虑到它将提供很少或根本没有额外价值,我们决定添加的复杂性和不一致性是不合理的。

任意参数的缩窄

TypeScript 中用户定义类型守卫的实现允许使用任何输入参数作为用于缩窄的值进行测试。TypeScript 语言作者无法回忆起在 TypeScript 中任何实际案例中,被测试的参数不是第一个参数。因此,为了避免对用户定义类型守卫的 Python 实现添加额外的复杂性来支持这种虚构的用例,决定无需这样做。如果将来发现此类用例,则可以通过扩展 TypeGuard 机制来解决。这可能涉及使用关键字索引,如 PEP 637 中提出的那样。

隐式“self”和“cls”参数的缩窄

该提案指出,假设第一个位置参数是被测试用于缩窄的值。如果类型守卫函数是作为实例或类方法实现的,则还会隐式地将 selfcls 参数传递给该函数。有人提出了一个问题,即可能存在需要将缩窄逻辑应用于 selfcls 的情况。这是一种不常见的用例,解决它将极大地增加用户定义类型守卫的实现复杂性。因此,决定不为此做出任何特殊规定。如果需要缩窄 selfcls,则可以将该值作为显式参数传递给类型守卫函数。


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

上次修改:2024-06-11 22:12:09 GMT