流畅的 Python 第二版(GPT 重译)(四)(4)

简介: 流畅的 Python 第二版(GPT 重译)(四)

流畅的 Python 第二版(GPT 重译)(四)(3)https://developer.aliyun.com/article/1484438

为什么需要 TypeVar?

PEP 484 的作者希望通过添加typing模块引入类型提示,而不改变语言的其他任何内容。通过巧妙的元编程,他们可以使[]运算符在类似Sequence[T]的类上起作用。但括号内的T变量名称必须在某处定义,否则 Python 解释器需要进行深层更改才能支持通用类型符号作为[]的特殊用途。这就是为什么需要typing.TypeVar构造函数:引入当前命名空间中的变量名称。像 Java、C#和 TypeScript 这样的语言不需要事先声明类型变量的名称,因此它们没有 Python 的TypeVar类的等价物。

另一个例子是标准库中的statistics.mode函数,它返回系列中最常见的数据点。

这里是来自文档的一个使用示例:

>>> mode([1, 1, 2, 3, 3, 3, 3, 4])
3

如果不使用TypeVarmode可能具有示例 8-17 中显示的签名。

示例 8-17。mode_float.py:对float和子类型进行操作的mode¹³
from collections import Counter
from collections.abc import Iterable
def mode(data: Iterable[float]) -> float:
    pairs = Counter(data).most_common(1)
    if len(pairs) == 0:
        raise ValueError('no mode for empty data')
    return pairs[0][0]

许多mode的用法涉及intfloat值,但 Python 还有其他数值类型,希望返回类型遵循给定Iterable的元素类型。我们可以使用TypeVar来改进该签名。让我们从一个简单但错误的参数化签名开始:

from collections.abc import Iterable
from typing import TypeVar
T = TypeVar('T')
def mode(data: Iterable[T]) -> T:

当类型参数T首次出现在签名中时,它可以是任何类型。第二次出现时,它将意味着与第一次相同的类型。

因此,每个可迭代对象都与Iterable[T]一致,包括collections.Counter无法处理的不可哈希类型的可迭代对象。我们需要限制分配给T的可能类型。我们将在接下来的两节中看到两种方法。

限制的 TypeVar

TypeVar接受额外的位置参数来限制类型参数。我们可以改进mode的签名,接受特定的数字类型,就像这样:

from collections.abc import Iterable
from decimal import Decimal
from fractions import Fraction
from typing import TypeVar
NumberT = TypeVar('NumberT', float, Decimal, Fraction)
def mode(data: Iterable[NumberT]) -> NumberT:

这比以前好,这是 2020 年 5 月 25 日typeshedstatistics.pyi存根文件中mode的签名。

然而,statistics.mode文档中包含了这个例子:

>>> mode(["red", "blue", "blue", "red", "green", "red", "red"])
'red'

匆忙之间,我们可以将str添加到NumberT的定义中:

NumberT = TypeVar('NumberT', float, Decimal, Fraction, str)

当然,这样做是有效的,但如果它接受str,那么NumberT的命名就非常不合适。更重要的是,我们不能永远列出类型,因为我们意识到mode可以处理它们。我们可以通过TypeVar的另一个特性做得更好,接下来介绍。

有界的 TypeVar

查看示例 8-17 中mode的主体,我们看到Counter类用于排名。Counter 基于dict,因此data可迭代对象的元素类型必须是可哈希的。

起初,这个签名似乎可以工作:

from collections.abc import Iterable, Hashable
def mode(data: Iterable[Hashable]) -> Hashable:

现在的问题是返回项的类型是Hashable:一个只实现__hash__方法的 ABC。因此,类型检查器不会让我们对返回值做任何事情,除了调用hash()。并不是很有用。

解决方案是TypeVar的另一个可选参数:bound关键字参数。它为可接受的类型设置了一个上限。在示例 8-18 中,我们有bound=Hashable,这意味着类型参数可以是Hashable或其任何子类型。¹⁴

示例 8-18。mode_hashable.py:与示例 8-17 相同,但具有更灵活的签名
from collections import Counter
from collections.abc import Iterable, Hashable
from typing import TypeVar
HashableT = TypeVar('HashableT', bound=Hashable)
def mode(data: Iterable[HashableT]) -> HashableT:
    pairs = Counter(data).most_common(1)
    if len(pairs) == 0:
        raise ValueError('no mode for empty data')
    return pairs[0][0]

总结一下:

  • 限制类型变量将被设置为TypeVar声明中命名的类型之一。
  • 有界类型变量将被设置为表达式的推断类型——只要推断类型与TypeVarbound=关键字参数中声明的边界一致即可。
注意

不幸的是,声明有界TypeVar的关键字参数被命名为bound=,因为动词“绑定”通常用于表示设置变量的值,在 Python 的引用语义中最好描述为将名称绑定到值。如果关键字参数被命名为boundary=会更少令人困惑。

typing.TypeVar构造函数还有其他可选参数——covariantcontravariant——我们将在第十五章中介绍,“Variance”中涵盖。

让我们用AnyStr结束对TypeVar的介绍。

预定义的 AnyStr 类型变量

typing模块包括一个预定义的TypeVar,名为AnyStr。它的定义如下:

AnyStr = TypeVar('AnyStr', bytes, str)

AnyStr在许多接受bytesstr的函数中使用,并返回给定类型的值。

现在,让我们来看看typing.Protocol,这是 Python 3.8 的一个新特性,可以支持更具 Python 风格的类型提示的使用。

静态协议

注意

在面向对象编程中,“协议”概念作为一种非正式接口的概念早在 Smalltalk 中就存在,并且从一开始就是 Python 的一个基本部分。然而,在类型提示的背景下,协议是一个typing.Protocol子类,定义了一个类型检查器可以验证的接口。这两种类型的协议在第十三章中都有涉及。这只是在函数注释的背景下的简要介绍。

PEP 544—Protocols: Structural subtyping (static duck typing)中所述,Protocol类型类似于 Go 中的接口:通过指定一个或多个方法来定义协议类型,并且类型检查器验证在需要该协议类型的地方这些方法是否被实现。

在 Python 中,协议定义被写作typing.Protocol子类。然而,实现协议的类不需要继承、注册或声明与定义协议的类的任何关系。这取决于类型检查器找到可用的协议类型并强制执行它们的使用。

这是一个可以借助ProtocolTypeVar解决的问题。假设您想创建一个函数top(it, n),返回可迭代对象it中最大的n个元素:

>>> top([4, 1, 5, 2, 6, 7, 3], 3)
[7, 6, 5]
>>> l = 'mango pear apple kiwi banana'.split()
>>> top(l, 3)
['pear', 'mango', 'kiwi']
>>>
>>> l2 = [(len(s), s) for s in l]
>>> l2
[(5, 'mango'), (4, 'pear'), (5, 'apple'), (4, 'kiwi'), (6, 'banana')]
>>> top(l2, 3)
[(6, 'banana'), (5, 'mango'), (5, 'apple')]

一个参数化的泛型top看起来像示例 8-19 中所示的样子。

示例 8-19。带有未定义T类型参数的top函数
def top(series: Iterable[T], length: int) -> list[T]:
    ordered = sorted(series, reverse=True)
    return ordered[:length]

问题是如何约束T?它不能是Anyobject,因为series必须与sorted一起工作。sorted内置实际上接受Iterable[Any],但这是因为可选参数key接受一个函数,该函数从每个元素计算任意排序键。如果您给sorted一个普通对象列表但不提供key参数会发生什么?让我们试试:

>>> l = [object() for _ in range(4)]
>>> l
[<object object at 0x10fc2fca0>, <object object at 0x10fc2fbb0>,
<object object at 0x10fc2fbc0>, <object object at 0x10fc2fbd0>]
>>> sorted(l)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'object' and 'object'

错误消息显示sorted在可迭代对象的元素上使用<运算符。这就是全部吗?让我们做另一个快速实验:¹⁵

>>> class Spam:
...     def __init__(self, n): self.n = n
...     def __lt__(self, other): return self.n < other.n
...     def __repr__(self): return f'Spam({self.n})'
...
>>> l = [Spam(n) for n in range(5, 0, -1)]
>>> l
[Spam(5), Spam(4), Spam(3), Spam(2), Spam(1)]
>>> sorted(l)
[Spam(1), Spam(2), Spam(3), Spam(4), Spam(5)]

那证实了:我可以对Spam列表进行sort,因为Spam实现了__lt__——支持<运算符的特殊方法。

因此,示例 8-19 中的 T 类型参数应该限制为实现 __lt__ 的类型。在 示例 8-18 中,我们需要一个实现 __hash__ 的类型参数,因此我们可以使用 typing.Hashable 作为类型参数的上界。但是现在在 typingabc 中没有适合的类型,因此我们需要创建它。

示例 8-20 展示了新的 SupportsLessThan 类型,一个 Protocol

示例 8-20. comparable.py: SupportsLessThan Protocol 类型的定义
from typing import Protocol, Any
class SupportsLessThan(Protocol):  # ①
    def __lt__(self, other: Any) -> bool: ...  # ②

协议是 typing.Protocol 的子类。

协议的主体有一个或多个方法定义,方法体中有 ...

如果类型 T 实现了 P 中定义的所有方法,并且类型签名匹配,则类型 T 与协议 P 一致

有了 SupportsLessThan,我们现在可以在 示例 8-21 中定义这个可工作的 top 版本。

示例 8-21. top.py: 使用 TypeVarbound=SupportsLessThan 定义 top 函数
from collections.abc import Iterable
from typing import TypeVar
from comparable import SupportsLessThan
LT = TypeVar('LT', bound=SupportsLessThan)
def top(series: Iterable[LT], length: int) -> list[LT]:
    ordered = sorted(series, reverse=True)
    return ordered[:length]

让我们来测试 top。示例 8-22 展示了一部分用于 pytest 的测试套件。首先尝试使用生成器表达式调用 top,该表达式生成 tuple[int, str],然后使用 object 列表。对于 object 列表,我们期望得到一个 TypeError 异常。

示例 8-22. top_test.py: top 测试套件的部分清单
from collections.abc import Iterator
from typing import TYPE_CHECKING  # ①
import pytest
from top import top
# several lines omitted
def test_top_tuples() -> None:
    fruit = 'mango pear apple kiwi banana'.split()
    series: Iterator[tuple[int, str]] = (  # ②
        (len(s), s) for s in fruit)
    length = 3
    expected = [(6, 'banana'), (5, 'mango'), (5, 'apple')]
    result = top(series, length)
    if TYPE_CHECKING:  # ③
        reveal_type(series)  # ④
        reveal_type(expected)
        reveal_type(result)
    assert result == expected
# intentional type error
def test_top_objects_error() -> None:
    series = [object() for _ in range(4)]
    if TYPE_CHECKING:
        reveal_type(series)
    with pytest.raises(TypeError) as excinfo:
        top(series, 3)  # ⑤
    assert "'<' not supported" in str(excinfo.value)

typing.TYPE_CHECKING 常量在运行时始终为 False,但类型检查器在进行类型检查时会假装它为 True

显式声明 series 变量的类型,以使 Mypy 输出更易读。¹⁶

这个 if 阻止了接下来的三行在测试运行时执行。

reveal_type() 不能在运行时调用,因为它不是常规函数,而是 Mypy 的调试工具—这就是为什么没有为它导入任何内容。对于每个 reveal_type() 伪函数调用,Mypy 将输出一条调试消息,显示参数的推断类型。

这一行将被 Mypy 标记为错误。

前面的测试通过了—但无论是否在 top.py 中有类型提示,它们都会通过。更重要的是,如果我用 Mypy 检查该测试文件,我会看到 TypeVar 正如预期的那样工作。查看 示例 8-23 中的 mypy 命令输出。

警告

截至 Mypy 0.910(2021 年 7 月),reveal_type 的输出在某些情况下并不精确显示我声明的类型,而是显示兼容的类型。例如,我没有使用 typing.Iterator,而是使用了 abc.Iterator。请忽略这个细节。Mypy 的输出仍然有用。在讨论输出时,我会假装 Mypy 的这个问题已经解决。

示例 8-23. mypy top_test.py 的输出(为了可读性而拆分的行)
…/comparable/ $ mypy top_test.py
top_test.py:32: note:
    Revealed type is "typing.Iterator[Tuple[builtins.int, builtins.str]]" # ①
top_test.py:33: note:
    Revealed type is "builtins.list[Tuple[builtins.int, builtins.str]]"
top_test.py:34: note:
    Revealed type is "builtins.list[Tuple[builtins.int, builtins.str]]" # ②
top_test.py:41: note:
    Revealed type is "builtins.list[builtins.object*]" # ③
top_test.py:43: error:
    Value of type variable "LT" of "top" cannot be "object"  # ④
Found 1 error in 1 file (checked 1 source file)

test_top_tuples 中,reveal_type(series) 显示它是一个 Iterator[tuple[int, str]]—这是我明确声明的。

reveal_type(result) 确认了 top 调用返回的类型是我想要的:给定 series 的类型,resultlist[tuple[int, str]]

test_top_objects_error 中,reveal_type(series) 显示为 list[object*]。Mypy 在任何推断的类型后面加上 *:我没有在这个测试中注释 series 的类型。

Mypy 标记了这个测试故意触发的错误:Iterable series的元素类型不能是object(必须是SupportsLessThan类型)。

协议类型相对于 ABCs 的一个关键优势是,一个类型不需要任何特殊声明来与协议类型一致。这允许创建一个协议利用预先存在的类型,或者在我们无法控制的代码中实现的类型。我不需要派生或注册strtuplefloatset等类型到SupportsLessThan以在期望SupportsLessThan参数的地方使用它们。它们只需要实现__lt__。而类型检查器仍然能够完成其工作,因为SupportsLessThan被明确定义为Protocol—与鸭子类型常见的隐式协议相反,这些协议对类型检查器是不可见的。

特殊的Protocol类在PEP 544—Protocols: Structural subtyping (static duck typing)中引入。示例 8-21 展示了为什么这个特性被称为静态鸭子类型:注释topseries参数的解决方案是说“series的名义类型并不重要,只要它实现了__lt__方法。”Python 的鸭子类型总是允许我们隐式地说这一点,让静态类型检查器一头雾水。类型检查器无法阅读 CPython 的 C 源代码,或者执行控制台实验来发现sorted只需要元素支持<

现在我们可以为静态类型检查器明确地定义鸭子类型。这就是为什么说typing.Protocol给我们静态鸭子类型是有意义的。¹⁷

还有更多关于typing.Protocol的内容。我们将在第四部分回来讨论它,在第十三章中对比结构化类型、鸭子类型和 ABCs——另一种形式化协议的方法。此外,“重载签名”(第十五章)解释了如何使用@typing.overload声明重载函数签名,并包括了一个使用typing.Protocol和有界TypeVar的广泛示例。

注意

typing.Protocol使得可以注释“类型由支持的操作定义”中提到的double函数而不会失去功能。关键是定义一个带有__mul__方法的协议类。我邀请你将其作为练习完成。解决方案出现在“类型化的 double 函数”中(第十三章)。

Callable

为了注释回调参数或由高阶函数返回的可调用对象,collections.abc模块提供了Callable类型,在尚未使用 Python 3.9 的情况下在typing模块中可用。Callable类型的参数化如下:

Callable[[ParamType1, ParamType2], ReturnType]

参数列表—[ParamType1, ParamType2]—可以有零个或多个类型。

这是在我们将在“lis.py 中的模式匹配:案例研究”中看到的一个repl函数的示例:¹⁸

def repl(input_fn: Callable[[Any], str] = input]) -> None:

在正常使用中,repl函数使用 Python 的input内置函数从用户那里读取表达式。然而,对于自动化测试或与其他输入源集成,repl接受一个可选的input_fn参数:一个与input具有相同参数和返回类型的Callable

内置的input在 typeshed 上有这个签名:

def input(__prompt: Any = ...) -> str: ...

input的签名与这个Callable类型提示一致

Callable[[Any], str]

没有语法来注释可选或关键字参数类型。typing.Callable文档说“这样的函数类型很少用作回调类型。”如果你需要一个类型提示来匹配具有灵活签名的函数,用...替换整个参数列表—就像这样:

Callable[..., ReturnType]

泛型类型参数与类型层次结构的交互引入了一个新的类型概念:variance。

Callable 类型中的 variance

想象一个简单的温度控制系统,其中有一个简单的update函数,如示例 8-24 所示。update函数调用probe函数获取当前温度,并调用display显示温度给用户。probedisplay都作为参数传递给update是为了教学目的。示例的目标是对比两个Callable注释:一个有返回类型,另一个有参数类型。

示例 8-24。说明 variance。
from collections.abc import Callable
def update(  # ①
        probe: Callable[[], float],  # ②
        display: Callable[[float], None]  # ③
    ) -> None:
    temperature = probe()
    # imagine lots of control code here
    display(temperature)
def probe_ok() -> int:  # ④
    return 42
def display_wrong(temperature: int) -> None:  # ⑤
    print(hex(temperature))
update(probe_ok, display_wrong)  # type error # ⑥
def display_ok(temperature: complex) -> None:  # ⑦
    print(temperature)
update(probe_ok, display_ok)  # OK # ⑧

update接受两个可调用对象作为参数。

probe必须是一个不带参数并返回float的可调用对象。

display接受一个float参数并返回None

probe_okCallable[[], float]一致,因为返回一个int不会破坏期望float的代码。

display_wrongCallable[[float], None]不一致,因为没有保证一个期望int的函数能处理一个float;例如,Python 的hex函数接受一个int但拒绝一个float

Mypy 标记这行是因为display_wrongupdatedisplay参数中的类型提示不兼容。

display_okCallable[[float], None]一致,因为一个接受complex的函数也可以处理一个float参数。

Mypy 对这行很满意。

总结一下,当代码期望返回float的回调时,提供返回int的回调是可以的,因为int值总是可以在需要float的地方使用。

正式地说,Callable[[], int]subtype-ofCallable[[], float]——因为intsubtype-offloat。这意味着Callable在返回类型上是协变的,因为类型intfloatsubtype-of关系与使用它们作为返回类型的Callable类型的关系方向相同。

另一方面,当需要处理float时,提供一个接受int参数的回调是类型错误的。

正式地说,Callable[[int], None]不是subtype-ofCallable[[float], None]。虽然intsubtype-offloat,但在参数化的Callable类型中,关系是相反的:Callable[[float], None]subtype-ofCallable[[int], None]。因此我们说Callable在声明的参数类型上是逆变的。

“Variance”在第十五章中详细解释了 variance,并提供了不变、协变和逆变类型的更多细节和示例。

提示

目前,可以放心地说,大多数参数化的泛型类型是invariant,因此更简单。例如,如果我声明scores: list[float],那告诉我可以分配给scores的对象。我不能分配声明为list[int]list[complex]的对象:

  • 一个list[int]对象是不可接受的,因为它不能容纳float值,而我的代码可能需要将其放入scores中。
  • 一个list[complex]对象是不可接受的,因为我的代码可能需要对scores进行排序以找到中位数,但complex没有提供__lt__,因此list[complex]是不可排序的。

现在我们来讨论本章中最后一个特殊类型。

NoReturn

这是一种特殊类型,仅用于注释永远不返回的函数的返回类型。通常,它们存在是为了引发异常。标准库中有数十个这样的函数。

例如,sys.exit()引发SystemExit来终止 Python 进程。

它在typeshed中的签名是:

def exit(__status: object = ...) -> NoReturn: ...

__status参数是仅位置参数,并且具有默认值。存根文件不详细说明默认值,而是使用...__status的类型是object,这意味着它也可能是None,因此标记为Optional[object]将是多多的。

在第二十四章中,示例 24-6 在__flag_unknown_attrs中使用NoReturn,这是一个旨在生成用户友好和全面错误消息的方法,然后引发AttributeError

这一史诗般章节的最后一节是关于位置和可变参数。

注释位置参数和可变参数

回想一下从示例 7-9 中的tag函数。我们上次看到它的签名是在“仅位置参数”中:

def tag(name, /, *content, class_=None, **attrs):

这里是tag,完全注释,写成几行——长签名的常见约定,使用换行符的方式,就像蓝色格式化程序会做的那样:

from typing import Optional
def tag(
    name: str,
    /,
    *content: str,
    class_: Optional[str] = None,
    **attrs: str,
) -> str:

注意对于任意位置参数的类型提示*content: str;这意味着所有这些参数必须是str类型。函数体中content的类型将是tuple[str, ...]

在这个例子中,任意关键字参数的类型提示是**attrs: str,因此函数内部的attrs类型将是dict[str, str]。对于像**attrs: float这样的类型提示,函数内部的attrs类型将是dict[str, float]

如果attrs参数必须接受不同类型的值,你需要使用Union[]Any**attrs: Any

仅位置参数的/符号仅适用于 Python ≥ 3.8。在 Python 3.7 或更早版本中,这将是语法错误。PEP 484 约定是在每个位置参数名称前加上两个下划线。这里是tag签名,再次以两行的形式,使用 PEP 484 约定:

from typing import Optional
def tag(__name: str, *content: str, class_: Optional[str] = None,
        **attrs: str) -> str:

Mypy 理解并强制执行声明位置参数的两种方式。

为了结束这一章,让我们简要地考虑一下类型提示的限制以及它们支持的静态类型系统。

不完美的类型和强大的测试

大型公司代码库的维护者报告说,许多错误是由静态类型检查器发现的,并且比在代码运行在生产环境后才发现这些错误更便宜修复。然而,值得注意的是,在我所知道的公司中,自动化测试在静态类型引入之前就是标准做法并被广泛采用。

即使在它们最有益处的情况下,静态类型也不能被信任为正确性的最终仲裁者。很容易找到:

假阳性

工具会报告代码中正确的类型错误。

假阴性

工具不会报告代码中不正确的类型错误。

此外,如果我们被迫对所有内容进行类型检查,我们将失去 Python 的一些表现力:

  • 一些方便的功能无法进行静态检查;例如,像config(**settings)这样的参数解包。
  • 属性、描述符、元类和一般元编程等高级功能对类型检查器的支持较差或超出理解范围。
  • 类型检查器落后于 Python 版本,拒绝甚至在分析具有新语言特性的代码时崩溃——在某些情况下超过一年。

通常的数据约束无法在类型系统中表达,甚至是简单的约束。例如,类型提示无法确保“数量必须是大于 0 的整数”或“标签必须是具有 6 到 12 个 ASCII 字母的字符串”。总的来说,类型提示对捕捉业务逻辑中的错误并不有帮助。

鉴于这些注意事项,类型提示不能成为软件质量的主要支柱,强制性地使其成为例外会放大缺点。

将静态类型检查器视为现代 CI 流水线中的工具之一,与测试运行器、代码检查器等一起。CI 流水线的目的是减少软件故障,自动化测试可以捕获许多超出类型提示范围的错误。你可以在 Python 中编写的任何代码,都可以在 Python 中进行测试,无论是否有类型提示。

本节的标题和结论受到 Bruce Eckel 的文章“强类型 vs. 强测试”的启发,该文章也发表在 Joel Spolsky(Apress)编辑的文集The Best Software Writing I中。Bruce 是 Python 的粉丝,也是关于 C++、Java、Scala 和 Kotlin 的书籍的作者。在那篇文章中,他讲述了他是如何成为静态类型支持者的,直到学习 Python 并得出结论:“如果一个 Python 程序有足够的单元测试,它可以和有足够单元测试的 C++、Java 或 C#程序一样健壮(尽管 Python 中的测试编写速度更快)。”

目前我们的 Python 类型提示覆盖到这里。它们也是第十五章的主要内容,该章涵盖了泛型类、变异、重载签名、类型转换等。与此同时,类型提示将在本书的几个示例中做客串出现。

章节总结

我们从对渐进式类型概念的简要介绍开始,然后转向实践方法。没有一个实际读取类型提示的工具,很难看出渐进式类型是如何工作的,因此我们开发了一个由 Mypy 错误报告引导的带注解函数。

回到渐进式类型的概念,我们探讨了它是 Python 传统鸭子类型和用户更熟悉的 Java、C++等静态类型语言的名义类型的混合体。

大部分章节都致力于介绍注解中使用的主要类型组。我们涵盖的许多类型与熟悉的 Python 对象类型相关,如集合、元组和可调用对象,扩展以支持类似Sequence[float]的泛型表示。许多这些类型是在 Python 3.9 之前在typing模块中实现的临时替代品,直到标准类型被更改以支持泛型。

一些类型是特殊实体。AnyOptionalUnionNoReturn与内存中的实际对象无关,而仅存在于类型系统的抽象领域中。

我们研究了参数化泛型和类型变量,这为类型提示带来了更多灵活性,而不会牺牲类型安全性。

使用Protocol使参数化泛型变得更加表达丰富。因为它仅出现在 Python 3.8 中,Protocol目前并不广泛使用,但它非常重要。Protocol实现了静态鸭子类型:Python 鸭子类型核心与名义类型之间的重要桥梁,使静态类型检查器能够捕捉错误。

在介绍一些类型的同时,我们通过 Mypy 进行实验,以查看类型检查错误,并借助 Mypy 的神奇reveal_type()函数推断类型。

最后一节介绍了如何注释位置参数和可变参数。

类型提示是一个复杂且不断发展的主题。幸运的是,它们是一个可选功能。让我们保持 Python 对最广泛用户群体的可访问性,并停止宣扬所有 Python 代码都应该有类型提示的说法,就像我在类型提示布道者的公开布道中看到的那样。

我们的退休 BDFL¹⁹领导了 Python 中类型提示的推动,因此这一章的开头和结尾都以他的话语开始:

我不希望有一个我在任何时候都有道义义务添加类型提示的 Python 版本。我真的认为类型提示有它们的位置,但也有很多时候不值得,而且很棒的是你可以选择使用它们。²⁰

Guido van Rossum

进一步阅读

Bernát Gábor 在他的优秀文章中写道,“Python 中类型提示的现状”

只要值得编写单元测试,就应该使用类型提示。

我是测试的忠实粉丝,但我也做很多探索性编码。当我在探索时,测试和类型提示并不有用。它们只是累赘。

Gábor 的文章是我发现的关于 Python 类型提示的最好介绍之一,还有 Geir Arne Hjelle 的“Python 类型检查(指南)”。Claudio Jolowicz 的“超现代 Python 第四章:类型”是一个更简短的介绍,也涵盖了运行时类型检查验证。

想要更深入的了解,Mypy 文档是最佳来源。它对于任何类型检查器都很有价值,因为它包含了关于 Python 类型提示的教程和参考页面,不仅仅是关于 Mypy 工具本身。在那里你还会找到一份方便的速查表和一个非常有用的页面,介绍了常见问题和解决方案

typing模块文档是一个很好的快速参考,但它并没有详细介绍。PEP 483—类型提示理论包括了关于协变性的深入解释,使用Callable来说明逆变性。最终的参考资料是与类型提示相关的 PEP 文档。已经有 20 多个了。PEP 的目标受众是 Python 核心开发人员和 Python 的指导委员会,因此它们假定读者具有大量先前知识,绝对不是轻松阅读。

如前所述,第十五章涵盖了更多类型相关主题,而“进一步阅读”提供了额外的参考资料,包括表 15-1,列出了截至 2021 年底已批准或正在讨论的类型 PEPs。

“了不起的 Python 类型提示”是一个有价值的链接集合,包含了工具和参考资料。

¹ PEP 484—类型提示,“基本原理和目标”;粗体强调保留自原文。

² PyPy 中的即时编译器比类型提示有更好的数据:它在 Python 程序运行时监视程序,检测使用的具体类型,并为这些具体类型生成优化的机器代码。

³ 例如,截至 2021 年 7 月,不支持递归类型—参见typing模块问题#182,定义 JSON 类型和 Mypy 问题#731,支持递归类型

⁴ Python 没有提供控制类型可能值集合的语法—除了在Enum类型中。例如,使用类型提示,你无法将Quantity定义为介于 1 和 1000 之间的整数,或将AirportCode定义为 3 个字母的组合。NumPy 提供了uint8int16和其他面向机器的数值类型,但在 Python 标准库中,我们只有具有非常小值集合(NoneTypebool)或极大值集合(floatintstr、所有可能的元组等)的类型。

⁵ 鸭子类型是一种隐式的结构类型形式,Python ≥ 3.8 也支持引入typing.Protocol。这将在本章后面—“静态协议”—进行介绍,更多细节请参见第十三章。

⁶ 继承经常被滥用,并且很难在现实但简单的示例中证明其合理性,因此请接受这个动物示例作为子类型的快速说明。

⁷ 麻省理工学院教授、编程语言设计师和图灵奖获得者。维基百科:芭芭拉·利斯科夫

⁸ 更准确地说,ord仅接受len(s) == 1strbytes。但目前的类型系统无法表达这个约束。

⁹ 在 ABC 语言——最初影响 Python 设计的语言中——每个列表都受限于接受单一类型的值:您放入其中的第一个项目的类型。

¹⁰ 我对typing模块文档的贡献之一是在 Guido van Rossum 的监督下将“模块内容”下的条目重新组织为子部分,并添加了数十个弃用警告。

¹¹ 在一些示例中,我使用:=是有意义的,但我在书中没有涵盖它。请参阅PEP 572—赋值表达式获取所有详细信息。

¹² 实际上,dictabc.MutableMapping的虚拟子类。虚拟子类的概念在第十三章中有解释。暂时知道issubclass(dict, abc.MutableMapping)True,尽管dict是用 C 实现的,不继承任何东西自abc.MutableMapping,而只继承自object

¹³ 这里的实现比 Python 标准库中的statistics模块更简单。

¹⁴ 我向typeshed贡献了这个解决方案,这就是为什么modestatistics.pyi中的注释截至 2020 年 5 月 26 日。

¹⁵ 多么美妙啊,打开一个交互式控制台并依靠鸭子类型来探索语言特性,就像我刚才做的那样。当我使用不支持它的语言时,我非常想念这种探索方式。

¹⁶ 没有这个类型提示,Mypy 会将series的类型推断为Generator[Tuple[builtins.int, builtins.str*], None, None],这是冗长的但与Iterator[tuple[int, str]]一致,正如我们将在“通用可迭代类型”中看到的。

¹⁷ 我不知道谁发明了术语静态鸭子类型,但它在 Go 语言中变得更加流行,该语言的接口语义更像 Python 的协议,而不是 Java 的名义接口。

¹⁸ REPL 代表 Read-Eval-Print-Loop,交互式解释器的基本行为。

¹⁹ “终身仁慈独裁者”。参见 Guido van Rossum 关于“BDFL 起源”

²⁰ 来自 YouTube 视频,“Guido van Rossum 关于类型提示(2015 年 3 月)”。引用开始于13’40”。我进行了一些轻微的编辑以提高清晰度。

²¹ 来源:“与艾伦·凯的对话”

相关文章
|
6天前
|
数据采集 存储 人工智能
【Python+微信】【企业微信开发入坑指北】4. 企业微信接入GPT,只需一个URL,自动获取文章总结
【Python+微信】【企业微信开发入坑指北】4. 企业微信接入GPT,只需一个URL,自动获取文章总结
44 0
|
6天前
|
Python
过年了,让GPT用Python给你写个放烟花的程序吧!
过年了,让GPT用Python给你写个放烟花的程序吧!
21 0
|
6天前
|
人工智能 JSON 机器人
【Chat GPT】用 ChatGPT 运行 Python
【Chat GPT】用 ChatGPT 运行 Python
|
6天前
|
机器学习/深度学习 人工智能 自然语言处理
总结几个GPT的超实用之处【附带Python案例】
总结几个GPT的超实用之处【附带Python案例】
|
6天前
|
前端开发 JavaScript 安全
JavaScript 权威指南第七版(GPT 重译)(七)(4)
JavaScript 权威指南第七版(GPT 重译)(七)
29 0
|
6天前
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(3)
JavaScript 权威指南第七版(GPT 重译)(七)
39 0
|
6天前
|
前端开发 JavaScript Unix
JavaScript 权威指南第七版(GPT 重译)(七)(2)
JavaScript 权威指南第七版(GPT 重译)(七)
43 0
|
6天前
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(1)
JavaScript 权威指南第七版(GPT 重译)(七)
69 0
|
6天前
|
存储 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(六)(4)
JavaScript 权威指南第七版(GPT 重译)(六)
127 3
JavaScript 权威指南第七版(GPT 重译)(六)(4)
|
6天前
|
前端开发 JavaScript API
JavaScript 权威指南第七版(GPT 重译)(六)(3)
JavaScript 权威指南第七版(GPT 重译)(六)
68 4