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

Python 增强提案

PEP 586 – 字面量类型

作者:
Michael Lee <michael.lee.0x2a at gmail.com>,Ivan Levkivskyi <levkivskyi at gmail.com>,Jukka Lehtosalo <jukka.lehtosalo at iki.fi>
BDFL 代表:
Guido van Rossum <guido at python.org>
讨论列表:
Typing-SIG 列表
状态:
最终
类型:
标准跟踪
主题:
类型提示
创建:
2019 年 3 月 14 日
Python 版本:
3.8
历史记录:
2019 年 3 月 14 日
决议:
Typing-SIG 消息

目录

注意

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

×

请参阅 类型规范更新流程,了解如何提出对类型规范的更改。

摘要

此 PEP 提出向 PEP 484 生态系统添加 *字面量类型*。字面量类型指示某个表达式确实具有特定值。例如,以下函数将仅接受确实值为“4”的表达式

from typing import Literal

def accepts_only_four(x: Literal[4]) -> None:
    pass

accepts_only_four(4)   # OK
accepts_only_four(19)  # Rejected

动机和基本原理

Python 拥有许多 API,它们根据提供的某些参数的值返回不同的类型。例如

  • open(filename, mode) 根据第二个参数是否类似于 rrb 返回 IO[bytes]IO[Text]
  • subprocess.check_output(...) 根据 universal_newlines 关键字参数是否设置为 True 返回字节或文本。

此模式在许多流行的第三方库中也相当常见。例如,以下是来自 pandas 和 numpy 的两个示例

  • pandas.concat(...) 将根据 axis 参数是否设置为 0 或 1 返回 SeriesDataFrame
  • numpy.unique 将根据三个布尔标志值返回单个数组或包含两个到四个数组的元组。

类型问题跟踪器包含一些 其他示例和讨论

目前无法表达这些函数的类型签名:PEP 484 不包含任何机制来编写返回值类型根据传入值而变化的签名。请注意,即使我们重新设计这些 API 以改为接受枚举,此问题仍然存在:MyEnum.FOOMyEnum.BAR 都被视为类型 MyEnum

目前,类型检查器通过为重要的内置函数和标准库函数添加临时扩展来解决此限制。例如,mypy 带有一个插件,该插件尝试为 open(...) 推断更精确的类型。虽然此方法适用于标准库函数,但它通常是不可持续的:不能期望第三方库作者为 N 个不同的类型检查器维护插件。

我们建议添加 *字面量类型* 来解决这些差距。

核心语义

本节概述字面量类型的基本行为。

核心行为

字面量类型指示变量具有特定且具体的值。例如,如果我们将某个变量 foo 定义为类型 Literal[3],我们声明 foo 必须完全等于 3,而不是其他值。

给定某个值为 v 且是类型 T 的成员,类型 Literal[v] 将被视为 T 的子类型。例如,Literal[3]int 的子类型。

父类型的全部方法都将被字面量类型直接继承。因此,如果我们有一些类型为 Literal[3] 的变量 foo,则执行诸如 foo + 5 之类的事情是安全的,因为 foo 继承了 int 的 __add__ 方法。 foo + 5 的结果类型为 int

此“继承”行为与我们 处理 NewTypes 的方式相同。

两个字面量的等价性

当以下两个条件都为真时,两种类型 Literal[v1]Literal[v2] 等价

  1. type(v1) == type(v2)
  2. v1 == v2

例如,Literal[20]Literal[0x14] 等价。但是,Literal[0]Literal[False] *不等价*,尽管 0 == False 在运行时计算结果为“真”:0 的类型为 int,而 False 的类型为 bool

简化字面量联合

字面量使用一个或多个值进行参数化。当字面量使用多个值进行参数化时,它被视为与这些类型的并集完全等价。也就是说,Literal[v1, v2, v3] 等价于 Union[Literal[v1], Literal[v2], Literal[v3]]

此快捷方式有助于使为接受许多不同字面量的函数编写签名更符合人体工程学——例如,像 open(...) 这样的函数

# Note: this is a simplification of the true type signature.
_PathType = Union[str, bytes, int]

@overload
def open(path: _PathType,
         mode: Literal["r", "w", "a", "x", "r+", "w+", "a+", "x+"],
         ) -> IO[Text]: ...
@overload
def open(path: _PathType,
         mode: Literal["rb", "wb", "ab", "xb", "r+b", "w+b", "a+b", "x+b"],
         ) -> IO[bytes]: ...

# Fallback overload for when the user isn't using literal types
@overload
def open(path: _PathType, mode: str) -> IO[Any]: ...

提供的这些值不必都是同一类型的成员。例如,Literal[42, "foo", True] 是合法类型。

但是,Literal **必须**使用至少一个类型进行参数化。像 Literal[]Literal 这样的类型是非法的。

类型推断

本节描述了关于类型推断和字面量的一些规则,以及一些示例。

向后兼容性

当类型检查器添加对 Literal 的支持时,务必以最大限度地提高向后兼容性的方式进行。类型检查器应确保在最佳努力的基础上,添加对 Literal 的支持后,以前可以进行类型检查的代码继续进行类型检查。

这在执行类型推断时尤其重要。例如,给定语句 x = "blue"x 的推断类型应该是 str 还是 Literal["blue"]

一种简单的策略是始终假设表达式旨在成为 Literal 类型。因此,在上面的示例中,x 将始终具有 Literal["blue"] 的推断类型。这种简单的策略几乎肯定过于激进——它会导致以下程序在以前没有失败的情况下开始失败

# If a type checker infers 'var' has type Literal[3]
# and my_list has type List[Literal[3]]...
var = 3
my_list = [var]

# ...this call would be a type-error.
my_list.append(4)

此策略失败的另一个示例是在设置对象中的字段时

class MyObject:
    def __init__(self) -> None:
        # If a type checker infers MyObject.field has type Literal[3]...
        self.field = 3

m = MyObject()

# ...this assignment would no longer type check
m.field = 4

另一种确实在每种情况下都保持兼容性的策略是始终假设表达式不是 Literal 类型,除非它们被显式注释。使用此策略的类型检查器将在上述第一个示例中始终推断 x 的类型为 str

这不是唯一可行的策略:类型检查器可以自由地尝试更复杂的推断技术。此 PEP 并不强制执行任何特定策略;它只强调向后兼容性的重要性。

在字面量上下文中使用非字面量

Literal 类型遵循关于子类型的现有规则,没有额外的特殊情况。例如,以下程序是类型安全的

def expects_str(x: str) -> None: ...
var: Literal["foo"] = "foo"

# Legal: Literal["foo"] is a subtype of str
expects_str(var)

这也意味着通常情况下,非 Literal 表达式不应自动转换为 Literal。例如

def expects_literal(x: Literal["foo"]) -> None: ...

def runner(my_str: str) -> None:
    # ILLEGAL: str is not a subclass of Literal["foo"]
    expects_literal(my_str)

注意:如果用户希望其 API 支持接受字面量原始类型(可能是出于遗留目的),则应实现一个回退重载。请参阅 与重载的交互

与其他类型和功能的交互

本节讨论 Literal 类型如何与其他现有类型交互。

结构化数据的智能索引

字面量可用于“智能索引”结构化类型,如元组、NamedTuple 和类。(注意:这不是详尽的列表)。

例如,当使用与有效索引对应的 int 键索引到元组中时,类型检查器应该推断出正确的类型值

a: Literal[0] = 0
b: Literal[5] = 5

some_tuple: Tuple[int, str, List[bool]] = (3, "abc", [True, False])
reveal_type(some_tuple[a])   # Revealed type is 'int'
some_tuple[b]                # Error: 5 is not a valid index into the tuple

我们期望在使用诸如 getattr 之类的函数时有类似的行为

class Test:
    def __init__(self, param: int) -> None:
        self.myfield = param

    def mymethod(self, val: int) -> str: ...

a: Literal["myfield"]  = "myfield"
b: Literal["mymethod"] = "mymethod"
c: Literal["blah"]     = "blah"

t = Test()
reveal_type(getattr(t, a))  # Revealed type is 'int'
reveal_type(getattr(t, b))  # Revealed type is 'Callable[[int], str]'
getattr(t, c)               # Error: No attribute named 'blah' in Test

注意:请参阅 与 Final 的交互,了解有关如何以更紧凑的方式表达上述变量声明的建议。

与重载的交互

Literal 类型和重载不需要以特殊方式交互:现有的规则可以正常工作。

但是,类型检查器必须注意支持的一个重要用例是当用户不使用字面量类型时能够使用回退。例如,考虑 open

_PathType = Union[str, bytes, int]

@overload
def open(path: _PathType,
         mode: Literal["r", "w", "a", "x", "r+", "w+", "a+", "x+"],
         ) -> IO[Text]: ...
@overload
def open(path: _PathType,
         mode: Literal["rb", "wb", "ab", "xb", "r+b", "w+b", "a+b", "x+b"],
         ) -> IO[bytes]: ...

# Fallback overload for when the user isn't using literal types
@overload
def open(path: _PathType, mode: str) -> IO[Any]: ...

如果我们将 open 的签名更改为仅使用前两个重载,我们将破坏任何不传入字面量字符串表达式的代码。例如,以下代码将被破坏

mode: str = pick_file_mode(...)
with open(path, mode) as f:
    # f should continue to be of type IO[Any] here

更广泛地说:我们建议向 typeshed 添加一项策略,该策略规定,每当我们将字面量类型添加到某个现有 API 时,我们还始终包含一个回退重载以保持向后兼容性。

与泛型的交互

Literal[3] 这样的类型旨在仅是 int 的普通旧子类。这意味着您可以在可以使用普通类型(例如泛型)的任何地方使用像 Literal[3] 这样的类型。

这意味着使用 Literal 类型参数化泛型函数或类是合法的

A = TypeVar('A', bound=int)
B = TypeVar('B', bound=int)
C = TypeVar('C', bound=int)

# A simplified definition for Matrix[row, column]
class Matrix(Generic[A, B]):
    def __add__(self, other: Matrix[A, B]) -> Matrix[A, B]: ...
    def __matmul__(self, other: Matrix[B, C]) -> Matrix[A, C]: ...
    def transpose(self) -> Matrix[B, A]: ...

foo: Matrix[Literal[2], Literal[3]] = Matrix(...)
bar: Matrix[Literal[3], Literal[7]] = Matrix(...)

baz = foo @ bar
reveal_type(baz)  # Revealed type is 'Matrix[Literal[2], Literal[7]]'

同样,使用涉及 Literal 类型的值限制或边界的构造 TypeVar 也是合法的

T = TypeVar('T', Literal["a"], Literal["b"], Literal["c"])
S = TypeVar('S', bound=Literal["foo"])

……尽管目前尚不清楚何时使用 Literal 上限构造 TypeVar 会有用。例如,上面示例中的 S TypeVar 本质上毫无意义:我们可以通过使用 S = Literal["foo"] 来获得等效的行为。

注意:Literal 类型和泛型有意仅以非常基本和有限的方式交互。特别是,希望类型检查包含大量数值或 numpy 风格操作的代码的库几乎肯定可能会发现此 PEP 中提出的 Literal 类型不足以满足其需求。

我们考虑了几个不同的修复方案,但最终决定将整数泛型的难题推迟到以后解决。有关更多详细信息,请参阅 被拒绝或超出范围的想法

与枚举和穷举检查的交互

当使用具有封闭数量变体的 Literal 类型(例如枚举)时,类型检查器应该能够执行穷尽性检查。例如,类型检查器应该能够推断出最终的 else 语句必须是 str 类型,因为 Status 枚举的所有三个值都已用尽

class Status(Enum):
    SUCCESS = 0
    INVALID_DATA = 1
    FATAL_ERROR = 2

def parse_status(s: Union[str, Status]) -> None:
    if s is Status.SUCCESS:
        print("Success!")
    elif s is Status.INVALID_DATA:
        print("The given data is invalid because...")
    elif s is Status.FATAL_ERROR:
        print("Unexpected fatal error...")
    else:
        # 's' must be of type 'str' since all other options are exhausted
        print("Got custom status: " + s)

上面描述的交互不是新的:它已经在 PEP 484 中进行了编码。但是,许多类型检查器(例如 mypy)尚未实现此功能,因为预期实现工作会很复杂。

一旦引入了 Literal 类型,其中一些复杂性将得到缓解:与其完全特殊处理枚举,我们可以将其视为大致等同于其值的联合,并利用类型检查器可能已经实现的关于联合、穷尽性、类型缩小、可达性等任何现有逻辑。

因此,Status 枚举可以被视为大致等价于 Literal[Status.SUCCESS, Status.INVALID_DATA, Status.FATAL_ERROR],并且 s 的类型相应地被缩小。

与收窄的交互

类型检查器可以选择对枚举和非枚举 Literal 类型执行超出上述部分描述的额外分析。

例如,基于包含或相等性检查执行缩小可能很有用。

def parse_status(status: str) -> None:
    if status in ("MALFORMED", "ABORTED"):
        # Type checker could narrow 'status' to type
        # Literal["MALFORMED", "ABORTED"] here.
        return expects_bad_status(status)

    # Similarly, type checker could narrow 'status' to Literal["PENDING"]
    if status == "PENDING":
        expects_pending_status(status)

考虑涉及 Literal 布尔值的表达式执行缩小也可能很有用。例如,我们可以组合 Literal[True]Literal[False] 和重载来构建“自定义类型守卫”。

@overload
def is_int_like(x: Union[int, List[int]]) -> Literal[True]: ...
@overload
def is_int_like(x: object) -> bool: ...
def is_int_like(x): ...

vector: List[int] = [1, 2, 3]
if is_int_like(vector):
    vector.append(3)
else:
    vector.append("bad")   # This branch is inferred to be unreachable

scalar: Union[int, str]
if is_int_like(scalar):
    scalar += 3      # Type checks: type of 'scalar' is narrowed to 'int'
else:
    scalar += "foo"  # Type checks: type of 'scalar' is narrowed to 'str'

与 Final 的交互

PEP 591 提案建议向类型系统添加“Final”限定符。此限定符可用于声明某些变量或属性不能重新赋值。

foo: Final = 3
foo = 4           # Error: 'foo' is declared to be Final

请注意,在上面的示例中,我们知道 foo 将始终完全等于 3。类型检查器可以使用此信息推断 foo 在任何期望 Literal[3] 的上下文中都是有效的。

def expects_three(x: Literal[3]) -> None: ...

expects_three(foo)  # Type checks, since 'foo' is Final and equal to 3

Final 限定符作为声明变量为有效 Literal 的简写。

如果此 PEP 和 PEP 591 均被接受,则类型检查器应支持此快捷方式。具体来说,给定以下形式的变量或属性赋值 var: Final = value,其中 valueLiteral[...] 的有效参数,类型检查器应该理解 var 可用于任何期望 Literal[value] 的上下文中。

类型检查器没有义务理解 Final 的任何其他用法。例如,以下程序是否进行类型检查未作规定。

# Note: The assignment does not exactly match the form 'var: Final = value'.
bar1: Final[int] = 3
expects_three(bar1)  # May or may not be accepted by type checkers

# Note: "Literal[1 + 2]" is not a legal type.
bar2: Final = 1 + 2
expects_three(bar2)  # May or may not be accepted by type checkers

被拒绝或超出范围的想法

本节概述了一些明确超出范围的潜在功能。

真正的依赖类型/整数泛型

本提案实质上是在描述将一个非常简化的依赖类型系统添加到 PEP 484 生态系统中。一个明显的扩展是实现一个完整的依赖类型系统,允许用户以任意方式根据其值来预测类型。这将允许我们编写如下签名

# A vector has length 'n', containing elements of type 'T'
class Vector(Generic[N, T]): ...

# The type checker will statically verify our function genuinely does
# construct a vector that is equal in length to "len(vec1) + len(vec2)"
# and will throw an error if it does not.
def concat(vec1: Vector[A, T], vec2: Vector[B, T]) -> Vector[A + B, T]:
    # ...snip...

至少,添加某种形式的整数泛型将很有用。

尽管这样的类型系统肯定有用,但它超出了此 PEP 的范围:与当前提案相比,它需要更多大量的实现工作、讨论和研究才能完成。

我们完全有可能将来会重新审视这个主题:我们很可能需要某种形式的依赖类型以及其他扩展(如可变参数泛型)来支持像 numpy 这样的流行库。

此 PEP 应被视为实现此目标的垫脚石,而不是试图提供全面的解决方案。

添加更简洁的语法

对本 PEP 的一个反对意见是,必须显式地编写 Literal[...] 感觉冗长。例如,而不是编写

def foobar(arg1: Literal[1], arg2: Literal[True]) -> None:
    pass

…最好改为编写

def foobar(arg1: 1, arg2: True) -> None:
    pass

不幸的是,这些缩写根本无法在运行时与现有的 typing 实现一起使用。例如,以下代码段在使用 Python 3.7 运行时会崩溃。

from typing import Tuple

# Supposed to accept tuple containing the literals 1 and 2
def foo(x: Tuple[1, 2]) -> None:
    pass

运行此代码会产生以下异常。

TypeError: Tuple[t0, t1, ...]: each t must be a type. Got 1.

我们不希望用户不得不记住何时可以省略 Literal,因此我们要求 Literal 始终存在。

更广泛地说,我们认为彻底修改 Python 中类型的语法超出了此 PEP 的范围:最好在单独的 PEP 中进行讨论,而不是将其附加到此 PEP 上。因此,此 PEP 故意没有尝试创新 Python 的类型语法。

回溯移植 Literal 类型

一旦此 PEP 被接受,则需要将 Literal 类型向后移植到捆绑了旧版本 typing 模块的 Python 版本。我们计划通过将 Literal 添加到 typing_extensions 第三方模块来实现此目的,该模块包含各种其他向后移植的类型。

实现

mypy 类型检查器目前已实现了本规范中描述的大部分行为,除了枚举 Literal 和上面描述的一些更复杂的缩小交互之外。

致谢

感谢 Mark Mendoza、Ran Benita、Rebecca Chen 和 typing-sig 的其他成员对本 PEP 的评论。

另外感谢 mypy 和 typing 问题跟踪器中的各种参与者,他们帮助提供了本 PEP 背后的许多动机和推理。


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

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