如何使用Literal?它有什么好处?

6,622 阅读3分钟

说句题外话,类型提示通常指定变量的类型。 但是当一个变量只能包含有限的字面值时,我们可以用 typing.Literal这允许类型检查器进行额外的推断,给我们的代码增加了安全级别。

在这篇文章中,我们将看看如何使用Literal ,以及它提供的好处。

举例说明

要使用Literal ,我们只需用允许的字面价值对其进行参数化。

from typing import Literal

game: Literal["checkers", "chess"]

我们已经声明,game 变量只有两个可能的值:字符串"checkers""chess"

允许的类型

typing.Literal 是在PEP 586中定义的,它定义了值的允许类型。

  • ints
  • strs
  • byteses
  • bools
  • enum.Enum价值
  • None - 为方便起见,有一个特例: ,相当于Literal[None] None

此外,我们可以嵌套Literals,将它们的值结合起来。例如,Literal[Literal[1, 2], Literal[3], 4] 相当于Literal[1, 2, 3, 4]

优点

当我们使用一个Literal 类型时,类型检查器可以确保。

  1. 赋值使用允许的值。
  2. 函数调用使用允许的值。
  3. 比较使用允许的值。
  4. 条件块通过专门的类型缩小,使用它们所比较的值的子集。
  5. 当我们使用穷举性检查时,链式if/elif 语句使用所有允许的值。

让我们依次来看看这些。

1.赋值

想象一下,我们打错了字,给我们的game 变量分配了一个错误的值。

from typing import Literal

game: Literal["checkers", "chess"]

game = "chuss"

Mypy将为我们发现这一点。

$ mypy example.py
example.py:5: error: Incompatible types in assignment (expression has type "Literal['chuss']", variable has type "Union[Literal['checkers'], Literal['chess']]")
Found 1 error in 1 file (checked 1 source file)

2.函数调用

同样,假设我们使用Literal ,向一个函数传递一个不支持的值。

from typing import Literal


def get_game_count(game: Literal["checkers", "chess"]) -> int:
    ...


get_game_count("chockers")

Mypy也会发现这一点。

$ mypy example.py
example.py:8: error: Argument 1 to "get_game_count" has incompatible type "Literal['chuss']"; expected "Union[Literal['checkers'], Literal['chess']]"
Found 1 error in 1 file (checked 1 source file)

3.比较

我们也可能在与一个Literal 值的比较中使用一个不支持的值。

from typing import Literal

game: Literal["checkers", "chess"] = "checkers"

if game == "owela":
    ...

在这种情况下,只要我们激活了Mypy的strict_equality 选项,它就会发现这个错误。

$ mypy --strict-equality example.py
example.py:5: error: Non-overlapping equality check (left operand type: "Union[Literal['checkers'], Literal['chess']]", right operand type: "Literal['owela']")
Found 1 error in 1 file (checked 1 source file)

请注意,在编写本文时,Mypy只对==!= 进行严格的平等检查。更复杂的比较,如game in ["owela"] ,将不会失败的严格平等检查。

4.条件块类型的缩小

当我们对一个变量的类型进行比较时,Mypy可以执行类型缩小,以推断出该变量在条件块中的限制类型。 我们之前介绍过如何用结构体如if isinstance(...)

当我们将一个Literal 与一个或多个值进行比较时,Mypy也会进行类型缩小。它可以在条件块中推断出该值具有更有限的Literal 类型。 例如,拿这段代码来说。

from typing import Literal

game: Literal["checkers", "chess"] = "checkers"

if game == "checkers":
    reveal_type(game)
else:
    reveal_type(game)

当我们对其运行Mypy时,reveal_type() 调试调用向我们显示了缩小的Literal 类型。

$ mypy --strict-equality example.py
example.py:6: note: Revealed type is "Literal['checkers']"
example.py:8: note: Revealed type is "Literal['chess']"

5.穷举性检查

穷举性检查是指类型检查器确保我们涵盖了一个变量的类型或值的所有可能选项。

Python 的类型提示中还没有关于穷举性检查的正式规范,但是我们可以用 NoReturn 类型来模拟它。如果我们使用一个接受NoReturn 作为值类型的函数,对它的任何调用都会失败,因为NoReturn 没有匹配的类型。

这种技术在Haki Benita的一篇博文中对Enum 类型进行了记录。我们也可以对Literal 类型使用这种技术。

想象一下,我们忘记在我们的get_game_count() 函数中处理"chess" 的情况。

from typing import Literal, NoReturn


def assert_never(value: NoReturn) -> NoReturn:
    """Exhaustiveness checking failure function"""
    assert False


GameType = Literal["checkers", "chess"]


def get_game_count(game: GameType) -> int:
    if game == "checkers":
        count = 123
    else:
        assert_never(game)
    return count

Mypy报告。

$ mypy example.py
example.py:16: error: Argument 1 to "assert_never" has incompatible type "Literal['chess']"; expected "NoReturn"
Found 1 error in 1 file (checked 1 source file)

错误信息不是清楚,因为我们只是在模拟穷举性检查。 但它确实报告了未处理的值是Literal['chess'] ,以及纠正它的大概行数。

当我们在系统中引入一个新的值时,穷举性检查特别有用。 我们可以把这个值添加到我们的第一个共享Literal ,然后用Mypy来找到需要更新的地方。