Python类型提示 - 如何使用@overload

1,194 阅读1分钟

有时几个变量的类型是相关的,比如 "如果x是A型,y是B型,否则y是C型"。 基本的类型提示不能描述这种关系,使类型检查变得麻烦或不准确。 我们可以用 @typing.overload来正确表示类型关系。

以这个函数为例。

from __future__ import annotations


def double(input_: int | list[int]) -> int | list[int]:
    if isinstance(input_, list):
        return [i * 2 for i in input_]
    return input_ * 2

变量有这些类型关系。

  • 如果input_ 是一个int ,返回值是一个int
  • 如果input_ 是一个list[int] ,那么返回值也是一个list[int]

int 只有这些组合是可能的。不可能input_ ,而返回值是list[int] ,反之亦然。但目前的类型提示并没有捕捉到这种关系。

让我们reveal_type()进行调试。

x = double(12)
reveal_type(x)

Mypy输出。

$ mypy example.py
example.py:11: note: Revealed type is 'Union[builtins.int, builtins.list[builtins.int]]'

输入是一个int ,但是Mypy发现它把x 的类型看成是int | list[int] (在旧的长式拼写中)。任何试图使用int- only operations withx ,比如除法,都会导致类型检查失败。为了修复这种错误,我们将被迫使用类型缩小

我们可以重写double 的提示,用 @typing.overload来表示类型关系。

from __future__ import annotations

from typing import overload


@overload
def double(input_: int) -> int:
    ...


@overload
def double(input_: list[int]) -> list[int]:
    ...


def double(input_: int | list[int]) -> int | list[int]:
    if isinstance(input_, list):
        return [i * 2 for i in input_]
    return input_ * 2

这乍一看有点奇怪--我们对double 进行了三次定义!让我们把它拆开。

前两个@overload 定义只为它们的类型提示而存在。每个定义代表一个允许的类型组合。这些定义从不运行,所以它们的主体可以包含任何东西,但使用 Python 的... (省略号) 字样是很习惯的。

第三个定义是实际的实现。 在这种情况下,我们需要提供类型提示,联合每个变量的所有可能的类型。 如果没有这样的提示,Mypy 将跳过对函数体的类型检查。

当 Mypy 检查文件时,它收集了@overload 定义作为类型提示。然后它使用第一个非@overload 定义作为实现。所有@overload 定义必须在实现之前,不允许有多个实现。

当 Python 导入文件时,@overload 定义会创建临时的double 函数,但每个定义都会被下一个定义覆盖。在导入后,只有实现存在。作为防止意外丢失实现的保护措施,试图调用@overload 定义会引发NotImplementedError

有了我们的类型关系描述,让我们检查一下两种输入类型的返回类型。

x = double(12)
reveal_type(x)

y = double([1, 2])
reveal_type(y)

Mypy说。

$ mypy example.py
example.py:23: note: Revealed type is 'builtins.int'
example.py:26: note: Revealed type is 'builtins.list[builtins.int]'

很好!返回类型与输入类型相匹配,正如我们所希望的那样。现在可以对double() 的任何调用者进行准确的类型检查,不需要任何额外的缩小。

@overload 关于更多的例子,请看Mypy文档中的函数重载部分

Fin

可能类型提示永远不会让你重载。

-Adam