应该强调的是,Python 仍将是一种动态类型的语言,即使按照惯例,作者也不希望强制类型提示

--Guido van Rossum, Jukka Lehtosalo, and Łukasz Langa, PEP 484—Type Hints

类型提示是Python自 2001 年发布的 Python 2.2 中 unification of types and classes 以来历史上最大的变化。然而,类型提示并没有平等地使所有 Python 用户受益。这就是为什么它们应该始终是可选的。

主要受益者是使用 IDE(集成开发环境)和 CI(持续集成)的专业软件工程师。使类型提示对该组用户有吸引力的成本效益分析并不适用于所有 Python 用户。

Python 的用户群远不止于此。它包括许多领域的科学家、交易员、记者、艺术家、制造商、分析师和学生等。对于他们中的大多数人来说,学习类型提示的成本更高——除非他们已经知道一种具有静态类型、子类型和泛型的语言。考虑到这些用户与 Python 交互的方式,以及他们的代码库和团队(通常是一个人的“团队”)的规模较小,对于这些用户中的许多人来说,成本可能会更高,而收益会更低。在编写用于探索数据和想法的代码时,Python 的默认动态类型更简单、更具表现力,例如在数据科学、创造性计算和学习中.

本章重点介绍 Python 在函数签名中的类型提示。第 15 章探讨了类​​上下文中的类型提示以及typing模块的其他功能。

本章的主要主题是:

  1. 使用 Mypy 渐进式类型的实践介绍。
  2. 鸭子类型和名义类型的补充观点。
  3. 可以出现在注释中的主要类型类别的概述——这大约占用了本章 60%的篇幅。
  4. 输入类型可变参数 (*args, **kwargs)。
  5. 类型提示和静态类型的限制和缺点。

本章的新内容

本章是全新的。在我完成 Fluent Python 的第一版之后,Python 3.5 中出现了类型提示。现在让我们回顾一下渐进式类型的本质,然后通过一个例子在实践中来了解它。

鉴于静态类型系统的局限性,PEP 484 的最佳想法是引入渐进类型系统。让我们从渐进性类型系统的定义开始。

关于渐进式类型

PEP 484 向 Python 引入了渐进类型系统。其他具有渐进类型系统的语言包括 Microsoft 的 TypeScript、Dart(Flutter SDK 的语言,由 Google 创建)和 Hack(Facebook 的 HHVM 虚拟机支持的 ​​PHP 方言)。Mypy 类型检查器本身作为一种语言开始:一种逐渐类型化的 Python 方言,带有自己的解释器。 Guido van Rossum 说服 Mypy 的创建者 Jukka Lehtosalo 使其成为检查带注释的 Python 代码的工具。

一个渐进类型系统通常具有下面的特征:

  • 类型提示是可选的: 默认情况下,类型检查器不应为没有类型提示的代码发出警告。相反,如果无法推断类型,则类型检查器会假定 Any 类型,这与所有其他类型一致。
  • 不在运行时捕获类型错误: 静态类型检查器、linter 和 IDE 使用类型提示来引发警告;它们不能防止在运行时将不一致的值传递给函数或分配给变量。
  • 不会提高性能:类型注解提供的数据理论上可以允许在生成的字节码中进行优化,但在 2021.2 年 7 月我所知道的任何 Python 运行时中都没有实现此类优化

渐进式类型的最佳可用性特性是注解始终是可选的。

使用静态类型系统,大多数类型的约束很容易表达,但是其中很多很麻烦,有些很难,还有一些是不可能的。您可能会编写一段出色的 Python 代码,具有良好的测试覆盖率和通过测试,但仍然无法为这段代码添加满足类型检查器的类型提示。没关系,只需忽略有问​​题的类型提示并提交即可!

类型提示在所有级别都是可选的:您可以使一系列的整个包都没有类型提示,当您将这些包之一导入使用类型提示的模块时,您可以使类型检查器静音,并且您可以添加特殊注解以使类型检查器忽略代码中的特定行。

TIP:

寻求 100% 的类型提示覆盖率很可能会在没有适当考虑的情况下过度使用类型提示,只是为了满足度量标准。当注解会使 API 的用户友好性降低或使其实现过于复杂时,应该编写没有类型提示的代码。

渐进式类型实践

让我们看看渐进类型在实践中是如何工作的,从一个简单的函数开始,并在 Mypy 的指导下逐渐向它添加类型提示。

Note:

除了嵌入在 PyCharm 等 IDE 中的类型检查器之外,还有几种与 PEP 484 兼容的 Python 类型检查器,包括 Google 的 Pytype、Microsoft 的 Pyright、Facebook 的 Pyre。我之所以选择 Mypy 作为示例,因为迄今为止它是最有名的。但是,其他的类型检查器可能更适合某些项目或团队。例如,Pytype 旨在处理没有类型提示的代码库,并且仍然提供有用的建议。它比 Mypy 更宽松,还可以为您的代码生成注解。

我们将给 show_count 函数添加注解,该函数返回一个带有计数和单数或复数单词的字符串,具体取决于计数:

>>> show_count(99, 'bird')
'99 birds'
>>> show_count(1, 'bird')
'1 bird'
>>> show_count(0, 'bird')
'no bird'

示例 8-1 显示了 show_count 的源代码,没有注解。

def show_count(count, word):if count == 0:return f'no {word}'elif count == 1:return f'{count} {word}'return f'{count} {word}s'

开始使用Mypy

要开始类型检查,我在 messages.py 模块上运行 mypy 命令:

…/no_hints/ $ pip install mypy
[lots of messages omitted...]
…/no_hints/ $ mypy messages.py
Success: no issues found in 1 source file

Mypy 使用默认设置运行时,发现例 8-2 中的代码没有问题.

Warning:

我正在使用 Mypy 0.910,这是我在 2021 年 7 月回顾时的最新版本。Mypy 介绍警告说它“是正式的测试版软件。偶尔会有破坏向后兼容性的更改。” Mypy 至少给了我一份报告,与我在 2020 年 4 月撰写本章时得到的报告不同。当您阅读本文时,您可能会得到与此处显示的结果不同的结果。

如果函数签名没有注释,Mypy 默认会忽略它——除非另有配置。

对于这个例子,我也有 pytest 单元测试。这是messages_test.py 中的代码。

例 8-2。没有类型提示的 messages_test.py。

from pytest import markfrom messages import show_count@mark.parametrize('qty, expected', [(1, '1 part'),(2, '2 parts'),
])
def test_show_count(qty, expected):got = show_count(qty, 'part')assert got == expecteddef test_show_count_zero():got = show_count(0, 'part')assert got == 'no part'

现在让我们在 Mypy 的指导下添加类型提示

使 Mypy 进行更加严格的检查

命令行选项 --disallow-untyped-defs 使 Mypy对任何没有为其所有参数和返回值提供类型提示的函数定义进行标记。

在测试文件上使用 --disallow-untyped-defs 会产生三个错误和一个提示:

…/no_hints/ $ mypy --disallow-untyped-defs messages_test.py
messages.py:14: error: Function is missing a type annotation
messages_test.py:10: error: Function is missing a type annotation
messages_test.py:15: error: Function is missing a return type annotation
messages_test.py:15: note: Use "-> None" if function does not return a value
Found 3 errors in 2 files (checked 1 source file)

对于渐进式类型的第一步,我更喜欢使用另一个选项:--disallow-incomplete-defs。最初,它什么也没告诉我:

…/no_hints/ $ mypy --disallow-incomplete-defs messages_test.py
Success: no issues found in 1 source file

但是现在,我可以在messages.py 中只对show_count 添加返回类型:

def show_count(count, word) -> str:

这足以让Mypy注意到它。使用与之前相同的命令行检查messages_test.py,会导致Mypy再次查看messages.py:

…/no_hints/ $ mypy --disallow-incomplete-defs messages_test.py
messages.py:14: error: Function is missing a type annotation for one or more arguments
Found 1 error in 1 file (checked 1 source file)

现在我可以为每个函数逐步添加类型提示,而不会收到有关我未注解的函数的警告。这是一个满足 Mypy 的完全注释签名:

def show_count(count: int, word: str) -> str:

TIP:

与在命令行上提供 --disallow-incomplete-defs 之类的选项相比,按照 Mypy 配置文件文档中的描述创建一个配置文件会是更好的方法。您可以拥有全局设置和每个模块的设置。这是一个初始的简单的 mypy.ini :

[mypy]
python_version = 3.9
warn_unused_configs = True
disallow_incomplete_defs = True

默认参数值

show_count 函数——首先在例 8-2 中展示——有一个明显的限制:它只适用于常规名词。如果不能通过附加“s”来拼写复数,我们应该让用户提供复数形式,如下所示:

>>> show_count(3, 'mouse', 'mice')
'3 mice'

让我们做一点“类型驱动开发”。首先,我们添加一个使用第三个参数的测试。不要忘记测试函数的返回类型提示 -> None 否则 Mypy 不会检查它。

def test_irregular() -> None:got = show_count(2, 'child', 'children')assert got == '2 children'

Mypy 检测到错误:

…/hints_2/ $ mypy messages_test.py
messages_test.py:22: error: Too many arguments for "show_count"
Found 1 error in 1 file (checked 1 source file)

现在我编辑 show_count,添加plural可选参数:

例 8-3。来自hints_2/messages.py 的showcount 带有可选参数。

def show_count(count: int, singular: str, plural: str = '') -> str:if count == 0:return f'no {singular}'elif count == 1:return f'1 {singular}'else:if plural:return f'{count} {plural}'else:return f'{count} {singular}s'

现在 Mypy 报告“成功”。

Warning:

这是 Python 没有发现的一个类型错误。你能看出来吗?

def hex2rgb(color=str) -> tuple[int, int, int]:

Mypy 的错误报告不是很有帮助:

colors.py:24: error: Function is missing a typeannotation for one or more arguments

color 参数的类型提示应该是 color: str。我写的color=str,不是注解:它把color的默认值设置为str。

根据我的经验,这是一个常见的错误并且很容易被忽视,尤其是在复杂的类型提示中。

以下细节不是强制性的,但被认为是类型提示的好风格:

  • 参数名和 : 之间不应有空格,并且 : 后应有一个空格。
  • 在默认参数值之前的 = 的每一侧都应该有一个空格。

另一方面,PEP 8 说如果没有该特定参数的类型提示,则 = 周围不应有空格。

CODE STYLE: USE FLAKE8 AND BLUE

与其记住这些愚蠢的规则,不如使用 flake8 和 blue 之类的工具。 flake8 报告代码样式以及许多其他问题,blue 根据嵌入在黑色代码格式化工具中的(大多数)规则重写源代码。

鉴于强制执行“标准”编码风格的目标,蓝色优于黑色,因为它遵循 Python 自己的默认使用单引号的风格,双引号作为替代方案:

>>> "I prefer single quotes"
'I prefer single quotes'

对单引号的偏好嵌入在 repr() 中,以及 CPython 中的其他地方。默认情况下, doctest 模块依赖于使用单引号的 repr() 。

如果必须使用black,请使用black -S 选项。然后它将保留您的引号。

Note:

blue 的作者之一是 Barry Warsaw,PEP 8 的合著者,自 1994 年以来的 Python 核心开发人员,2019 年至今(2021 年 7 月)担任 Python 指导委员会成员。当我们默认选择单引号时,我们处于非常好的大佬的陪伴中。

使用 None 作为默认值

例8-3中参数plural被注解为str,默认值为'',所以不存在类型冲突。我喜欢那个解决方案,但在其他情况下 None 是更好的默认值。如果可选参数需要可变类型,那么 None 是唯一合理的默认值——正如我们在“Mutable Types as Parameter Defaults: Bad Idea”中所见。

要将 None 作为plural参数的默认值,签名如下所示:

from typing import Optionaldef show_count(count: int, singular: str, plural: Optional[str] = None) -> str:

让我们解读这段代码:

  • Optional[str] 表示plural可以是 str 或 None。
  • 您必须明确提供默认值 = None。

如果您没有为plural分配默认值,Python 运行时会将其视为必需参数。请记住:在运行时,类型提示将被忽略。

注意,我们需要从typing中导入 Optional。导入类型时,最好使用from typing import X 的语法,以减少函数签名的长度。

Warning:Optional 不是一个好名字,因为该注解不会使参数成为可选的。使它成为可选的是为参数分配一个默认值。Optional[str] 只是表示:这个参数的类型可能是 str 或 NoneType。在 Haskell 和 Elm 语言中,类似的类型被命名为 Maybe。

Python 2.7 和 3.x 的类型提示

拥有大型 Python 2 代码库的公司已经了解到,在迁移到 Python 3 时,类型提示非常有用。可以使用 PEP 484 中描述的特殊注释来注解将在 Python 2.7 和 Python 3.x 上运行的代码。

这是使用适用于 Python 2.7 和 3.x 的语法的 show_count 签名的最终版本的样子——Mypy 和其他类型检查器也支持:

from typing import Optionaldef show_count(count, singular, plural=None):# type: (int, str, Optional[str]) -> str

请注意,注解中仅显示参数类型。 如果参数列表太长,签名可能会像这样注释:

from typing import Optionaldef show_count(count,       # type: intsingular,    # type: strplural=None  # type: Optional[str]):# type: (...) -> str

最后一个类型注释将完全如图所示: ... 替换已经给出的参数类型,并且 -> str 定义返回类型。

更多细节可以参考 Suggested syntax for Python 2.7 and straddling code in PEP 484.

存根文件:

使类型提示在 Python 2.7 和 3.x 中兼容的另一种方法是使用存根文件,它只包含带注释的函数和类声明——很像 C 和 C++ 中的头文件。Mypy、PyCharm 和其他类型检查器知道如何读取存根文件,并且他们共享 typeshed 项目、Python 标准库的存根文件集合和流行的外部包,如 Flask、attr、requests 等。我不会介绍如何创建和管理存根文件。如果您有兴趣,请参阅 PEP 484 部分Stub Files 和 PEP 561—Distributing and Packaging Type Information

类型由支持的操作定义

字面上对类型的概念有很多定义。这里我们假设 type 是一组值和一组可以应用于这些值的函数。                                                                                         ---PEP 483:类型提示理论

在实践中,将支持的操作集视为类型的定义特征更有用。

例如,从应用操作的角度来看,以下函数中 x 的有效类型是什么?

def double(x):return x * 2

x 参数类型可以是数字(int、complex、Fraction、numpy.uint32 等),但它也可能是一个序列(str、tuple、list、array)、一个 N 维 numpy.array 或任何其他实现或继承接受 int 参数的 __mul__ 方法的类型。

但是,请考虑这个带注解的 double。现在请忽略缺少的返回类型,让我们关注参数类型:

from collections import abcdef double(x: abc.Sequence):return x * 2

类型检查器将拒绝这段代码。如果你告诉 Mypy x 是 abc.Sequence 类型,它会将 x * 2 标记为错误,因为 Sequence ABC 没有实现或继承 __mul__ 方法。在运行时,该代码能够处理具体的序列,例如 str、元组、列表、数组等——以及数字,因为在运行时类型提示被忽略。但是类型检查器只关心显式声明的内容,而 且abc.Sequence 没有 __mul__。

这就是为什么本节的标题是“类型由操作定义”。Python 运行时接受任何对象作为两个版本的 double 函数的 x 参数。计算 n * 2 可能有效,或者如果 x 不支持该操作,它可能会引发 TypeError。相比之下,Mypy 在分析带注解的double源代码时会声明 n * 2 为错误,因为它是声明类型不支持的操作:n: abc.Sequence。

在渐进的类型系统中,我们可以看到两种不同的类型视图的相互作用:

鸭子类型:

Smalltalk(开创性的面向对象语言)以及 Python 和 Ruby 所采用的观点。对象有类型,但变量(包括参数)是无类型的。在实践中,对象的声明类型是什么并不重要,重要的是它实际支持什么操作。如果我可以调用bridie.quack(),那么birdie在这种情况下就是一只鸭子。根据定义,当鸭子类型尝试对对象进行操作时,仅在运行时强制执行。这比名义类型更灵活,代价是在运行时需要处理更多错误。

名义类型:

C++、Java 和 C# 采用的视图,带注解的 Python 同样支持。对象和变量都有类型。但是对象只存在于运行时,类型检查器只关心变量(包括参数)用类型提示注解的源代码。如果 Duck 是 Bird 的子类,则可以将 Duck 实例分配给一个带有注解的参数birdie:Bird。但是在函数体中,类型检查器认为调用birdie.quack() 是非法的,因为birdie 名义上是Bird,而该类不提供.quack() 方法。运行时的实际参数是否为 Duck类型 并不重要,因为名义类型是静态强制的。类型检查器不运行程序的任何部分,它只阅读源代码。这比鸭子类型更严格,优点是可以在构建管道中更早地捕获一些错误,甚至在 IDE 中键入代码时。

这是一个愚蠢的例子,对比了鸭子类型和名义类型,以及静态类型检查和运行时行为

例 8-4。birds.py

class Bird:passclass Duck(Bird):  1def quack(self):print('Quack!')def alert(birdie):  2birdie.quack()def alert_duck(birdie: Duck) -> None:  3birdie.quack()def alert_bird(birdie: Bird) -> None:  4birdie.quack()
  1. Duck是Bird类型的子类
  2. alert 没有类型提示,因此类型检查器会忽略它。
  3. alert_duck 接受一个 Duck 类型的参数。
  4. alert_bird 接受一个 Bird 类型的参数

用Mypy检查birds.py,我们看到一个问题:

…/birds/ $ mypy birds.py
birds.py:16: error: "Bird" has no attribute "quack"
Found 1 error in 1 file (checked 1 source file)

仅通过分析源代码,Mypy 就发现 alert_bird 有问题:类型提示声明了 Birdie 类型为 Bird 的参数,但函数体调用了 Birdie.quack()——而 Bird 类没有这样的方法。

现在让我们尝试使用 daffy.py 中的birds模块:

示例 8-5. daffy.py

from birds import *daffy = Duck()
alert(daffy)       1
alert_duck(daffy)  2
alert_bird(daffy)  3
  1. 有效调用,因为警报没有类型提示。
  2. 有效调用,因为 alert_duck 接受 Duck 参数,而 daffy 就是 Duck。
  3. 有效调用,因为 alert_bird 接受一个 Bird 参数,而 daffy 也是一个 Bird——Duck 的超类。

在 daffy.py 上运行 Mypy 会引发与 Birds.py 中定义的 alert_bird 函数中的 quack 调用相同的错误:

…/birds/ $ mypy daffy.py
birds.py:16: error: "Bird" has no attribute "quack"
Found 1 error in 1 file (checked 1 source file)

但是Mypy认为daffy.py本身没有问题:三个函数都可以正常调用。

现在,如果你运行 daffy.py,这就是你得到的结果:

…/birds/ $ python3 daffy.py
Quack!
Quack!
Quack!

一切正常!鸭子类型棒极了!

在运行时,Python 不关心声明的类型。它只使用鸭子类型。Mypy 在 alert_bird 中标记了一个错误,但在运行时使用 daffy 调用它可以正常工作。起初,这可能会让许多 Python 专家感到惊讶:静态类型检查器有时会在我们知道可以执行的程序中发现错误。

但是,如果几个月后您的任务是扩展silly bird示例,您可能会感谢 Mypy。考虑这个也使用birds的 woody.py 模块:

示例 8-6. woody.py

from birds import *woody = Bird()
alert(woody)
alert_duck(woody)
alert_bird(woody)

Mypy在检查woody.py时发现两个错误:

…/birds/ $ mypy woody.py
birds.py:16: error: "Bird" has no attribute "quack"
woody.py:5: error: Argument 1 to "alert_duck" has incompatible type "Bird"; expected "Duck"
Found 2 errors in 2 files (checked 1 source file)

第一个错误是在birds.py 中:我们之前见过的alert_bird 中的birdie.quack() 调用。

第二个错误是在 woody.py 中:woody 是 Bird 的一个实例,因此调用 alert_duck(woody) 是无效的,因为该函数需要一个 Duck。每只鸭子都是一只鸟,但不是每只鸟都是鸭子。

在运行时,woody.py 中的所有调用都没有成功。在带有标注的控制台会话中最好地说明了连续的失败:

例 8-7。运行时错误以及 Mypy 如何提供帮助

>>> from birds import *
>>> woody = Bird()
>>> alert(woody)  1
Traceback (most recent call last):...
AttributeError: 'Bird' object has no attribute 'quack'
>>>
>>> alert_duck(woody) 2
Traceback (most recent call last):...
AttributeError: 'Bird' object has no attribute 'quack'
>>>
>>> alert_bird(woody)  3
Traceback (most recent call last):...
AttributeError: 'Bird' object has no attribute 'quack'
  1. Mypy 无法检测到此错误,因为alert中没有类型提示。
  2. Mypy 报告问题:Argument 1 to "alert_duck" has incompatible type "Bird"; expected "Duck".
  3. Mypy 从示例 8-4 开始就告诉我们 alert_bird 函数体是错误的:"Bird" has no attribute "quack".

这个小实验表明,鸭子类型更容易上手,也更灵活,但允许调用不支持的操作而导致在运行时报错。名义类型在运行之前检测到错误,但有时会对实际可以运行的代码报错——例如示例 8-5 中的调用 alert_bird(daffy)。即使它有时有效,alert_bird 函数也会被误用:它的主体确实需要一个支持 .quack() 方法的对象,而 Bird 没有。

在这个有问题的例子中,函数是只有一行的。但在实际代码中,它们可能更长,它们可以将bird参数传递给更多函数,并且birdie参数的起源可能相距很远,很难查明运行时错误的原因。类型检查器可防止许多此类错误在运行时发生。

Note:

当代码块行数较少时,类型提示的价值是值得怀疑的。然而随着代码库的大小增长,类型提示开始凸显价值。这就是为什么拥有数百万行 Python 代码的公司(例如 Dropbox、Google 和 Facebook)投资于团队和工具以支持在公司范围内采用类型提示,并在其 CI 流水线中检查了大量且不断增加的 Python 代码库类型。

在本节中,我们从简单的 double() 函数开始,探讨了鸭子类型和名义类型中类型和操作的关系——我们没有提供适当的类型提示。现在我们将学习用于注释函数的最重要的类型。当我们到达“静态协议”时,我们将看到一种向 double() 添加类型提示的好方法。但在我们开始之前,还有更多基本类型需要了解。

可用于注解的类型

几乎任何 Python 类型都可以在类型提示中使用,但有一些限制和建议。此外,typing 模块引入了具有有时令人惊讶的语义的特殊结构。

本节涵盖了可以与注解一起使用的所有主要类型:

  • typing.Any;

  • 简单的 types 和classes;

  • typing.Optional 和 typing.Union;

  • 泛型集合, 包括tuples 和 mappings;

  • 抽象基类(ABC)

  • 泛型可迭代对象;

  • 参数化泛型和 TypeVar; XXX

  • typing.Protocols—静态鸭子类型的关键;

  • typing.Callable;

  • typing.NoReturn—结束这个列表的好方法。

我们将依次介绍每一种类型,从一种奇怪的、看似无用但至关重要的类型开始。

Any 类型:

任何渐进类型系统的基石是 Any 类型,也称为动态类型。当类型检查器看到这样的无类型的函数时:

def double(x):return x * 2

它被假定为:

def double(x: Any) -> Any:return x * 2

这意味着 x 参数和返回值可以是任何类型,包括参数和返回值是不同的类型。我们假设 Any 支持所有可能的操作。

将 Any 与 object 进行对比。考虑这个签名:

def double(x: object) -> object:

该函数还接受各种类型的参数,因为每种类型都是object的子类型。

但是,类型检查器将拒绝下面的函数:

def double(x: object) -> object:return x * 2

问题是object不支持 __mul__ 操作。这是 Mypy 报告的内容:

…/birds/ $ mypy double_object.py
double_object.py:2: error: Unsupported operand types for * ("object" and "int")
Found 1 error in 1 file (checked 1 source file)

更通用的类型会具有更窄的接口,即它们支持更少的操作。Object类实现的操作比 abc.Sequence 少,abc.Sequence实现的操作比 abc.MutableSequence 少,abc.MutableSequence实现的操作比 list 少。

但是 Any 是一种神奇的类型,它位于类型层次结构的顶部和底部。它同时是最通用的类​​型——因此参数 n:Any 接受每种类型的值——也是最特定的类型,支持所有可能的操作。至少,这就是类型检查器理解 Any 的方式。

当然,没有任何类型可以支持所有可能的操作,因此使用 Any 会阻止类型检查器完成其核心使命:在您的程序因运行时异常而崩溃之前检测潜在的非法操作。

是...的子类型和与...一致

传统的面向对象的名义类型系统依赖于 subtype-of 关系。给定一个类 T1 和一个子类 T2,那么 T2 是 T1 的子类型。

考虑下面的代码:

class T1:...class T2(T1):...def f1(p: T1) -> None:...o2 = T2()f1(o2)  # OK

调用 f1(o2) 是 Liskov 替换原则 - LSP 的应用。Barbara Liskov 实际上定义了支持操作的 is-sub-type-of 术语:如果类型 T2 的对象替换了类型 T1 的对象并且程序仍然正确运行,则 T2 是 T1 的子类型。

继续前面的代码,这明显违反LSP原则 :

def f2(p: T2) -> None:...o1 = T1()f2(o1)  # type error

从支持的操作的角度来看,这样的结果是完全合理的:作为一个子类,T2 继承并且必须支持 T1 所做的所有操作。因此,T2 的实例可以用于任何需要 T1 实例的地方。但反过来不一定正确:T2 可能实现额外的方法,因此 T1 的实例可能不能在任何需要 T2 实例的地方使用。这种对支持操作的关注反映在名称behavioral subtyping,中,也用于指代 LSP。

在渐进式类型系统中,还有另一种关系:consistent-with,适用于任何 subtype-of 适用的地方,对 Any 类型有特殊规定。

consistent-with的规则是:

  1. 给定 T1 和其子类型 T2,则 T2 与 T1 一致(Liskov 替换)。
  2. 任何类型都与 Any 一致:您可以将每种类型的对象传递给声明为 Any 类型的参数。
  3. Any 与所有类型一致:您始终可以在需要另一种类型的参数的时候,传递 Any 类型的对象。

继续使用对象 o1 和 o2 的先前定义,以下是有效代码示例,说明规则 #2 和 #3:

def f3(p: Any) -> None:...o0 = object()
o1 = T1()
o2 = T2()f3(o0)  #
f3(o1)  #  all OK: rule #2
f3(o2)  #def f4():  # implicit return type: `Any`...o4 = f4()  # inferred type: `Any`f1(o4)  #
f2(o4)  #  all OK: rule #3
f3(o4)  #

每个渐进类型系统都需要像 Any 这样的通配符类型。

TIP:

动词“to infer”是“to guess”的同义词,用在类型分析的上下文中。Python 和其他语言中的现代类型检查器不需要无处不在的类型注解,因为它们可以推断许多表达式的类型。例如,如果我写 x = len(s) * 10,类型检查器不需要显式的局部声明来知道 x 是一个 int,只要它可以找到 len 内置的类型提示。

现在我们可以探索注解中使用的其余类型。

简单类型和类

像 int、float、str、bytes 这样的简单类型可以直接在类型提示中使用。来自标准库、外部包或用户定义的具体类(FrenchDeck、Vector2d 和 Duck)也可用于类型提示。

抽象基类在类型提示中也很有用。我们将在研究集合类型和“ABC”中研究它们。

在类中,consistent-with的定义就像子类型一样:子类与其所有超类一致。

然而,“实用胜过纯粹”,所以有一个重要的例外:

int 和 complex一致

内置类型 int、float 和 complex 之间没有名义上的子类型关系:它们是 object 的直接子类。但是 PEP 484 声明 int 与 float 一致,而 float 与 complex 一致。这在实践中是有意义的:int 实现了 float 所做的所有操作,并且 int 还实现了其他操作——如 &、|、<< 等按位操作。最终结果是:int 与complex一致。对于 i = 3,i.real 为 3,i.imag 为 0。

Optional 和 Union 类型

我们在“使用 None 作为默认值”中看到了 Optional 特殊类型。它解决了将 None 作为默认值的问题,如该部分中的示例所示:

from typing import Optionaldef show_count(count: int, singular: str, plural: Optional[str] = None) -> str:

结构体 Optional[str] 实际上是 Union[str, None] 的快捷方式,这意味着plural的类型可以是 str 或 None。

Python 3.10 中 Optional 和 UNION 的更好语法:

从 Python 3.10 开始,我们可以编写str |bytes代替Union[str, bytes]。更少的代码,无需从typing模块中导入 Optional 或 Union。对比 show_count 的plural参数类型提示的新旧语法:

plural: Optional[str] = None    # before
plural: str | None = None       # after

| 运算符还可以与insinstance 和issubclass 一起使用来构建第二个参数:isinstance(x, int | str)。有关更多信息,请参阅 PEP 604—Complementary syntax for Union[]。

ord 内置函数的签名是 Union 的一个简单示例——它接受 str 或bytes,并返回一个 int:

def ord(c: Union[str, bytes]) -> int: ...

下面是一个函数的例子,它接受一个 str,但可能返回一个 str 或一个浮点数:

from typing import Uniondef parse_token(token: str) -> Union[str, float]:try:return float(token)except ValueError:return token

如果可能,避免创建返回 Union 类型的函数,因为它们会给用户带来额外的负担——迫使用户在运行时检查返回值的类型以了解如何处理它。但是上面的 parse_token 在简单表达式求值器的上下文中是一个合理的用例。

TIP:

在“Dual-Mode str and bytes APIs” 中,我们看到函数接受 str 或 bytes 参数,但如果参数是 str 则返回 str 或如果参数是bytes则返回bytes。在这些情况下,返回类型由输入类型决定,因此 Union 不是一个准确的解决方案。为了正确注解这些函数,我们需要一个类型变量——在“Parameterized generics and TypeVar”中呈现——或者使用重载,我们将在 “Overloaded signatures”中看到。

Union[] 至少需要两种类型。嵌套Union类型与扁平的Union具有相同的效果。所以下述类型提示:

Union[A, B, Union[C, D, E]]

和下面的类型提示是相同的:

Union[A, B, C, D, E]

Union 对不一致的类型更有用。例如: Union[int, float] 作为类型提示就是冗余的,因为 int 与 float 一致。通常使用 float 注解参数即可,然后这个参数也会接受 int 类型。

泛型集合

大多数 Python 集合是异构的。例如,您可以将不同类型的任意混合放入列表中。然而,在实践中这并不是很有用:如果你把对象放在一个集合中,你很可能想稍后对它们进行操作,通常这意味着它们必须至少共享一个公共方法。

可以使用类型参数声明泛型类型,以指定它们可以处理的项的类型。

例如,可以参数化一个列表以限制其元素的类型:

例 8-8。使用 Python ≥ 3.9 的类型提示的tokenize函数

def tokenize(text: str) -> list[str]:return text.upper().split()

在 Python ≥ 3.9 中,这意味着 tokenize 返回一个列表,其中每个项都是 str 类型。

注解 stuff: list 和 stuff: list[Any] 意思相同:stuff 是任何类型的对象列表。

TIP:

如果您使用的是 Python 3.8 或更早版本,则其理念是相同的,但您需要更多代码才能使其工作 - 如可选部分“Legacy Support and Deprecated Collection Types”所述。

PEP 585—Type Hinting Generics In Standard Collections列出了标准库中接受泛型类型提示的集合。以下列表仅显示使用最简单的泛型类型提示形式的那些集合:container[item]。

list        collections.deque        abc.Sequence   abc.MutableSequence
set         abc.Container            abc.Set        abc.MutableSet
frozenset   abc.Collection

元组和映射类型支持更复杂的类型提示,我们将在它们各自的部分中看到。

从 Python 3.10 开始,考虑到 typecode 构造函数参数决定数组中存储的是整数还是浮点数,没有很好的方法来注解array.array。一个更难的问题是如何在在运行时向数组添加元素时对整数值的范围进行类型检查以防止溢出错误。例如,typecode='B' 的数组只能保存 0 到 255 之间的 int 值。目前,Python 的静态类型系统无法应对这一挑战。


LEGACY SUPPORT AND DEPRECATED COLLECTION TYPES

(如果您只使用 Python 3.9 或更高版本,则可以跳过这部分。)

对于 Python 3.7 和 3.8,您需要导入 __future__ 以使 [] 表示法与内置集合(例如列表)一起使用:

例 8-9。使用 Python ≥ 3.7 的类型提示的tokenize

from __future__ import annotationsdef tokenize(text: str) -> list[str]:return text.upper().split()

__future__ 导入不适用于 Python 3.6 或更早版本。这是如何以适用于 Python ≥ 3.5 的方式注解tokenize:

例 8-10。使用 Python ≥ 3.5 的类型提示的tokenize

from typing import Listdef tokenize(text: str) -> List[str]:return text.upper().split()

为了提供对泛型类型提示的初始支持,PEP 484 的作者在typing模块中创建了数十种泛型类型。表 8-1 显示了其中的一些。如需完整列表,请访问typing 文档。

Table 8-1. Collection types and their type hint equivalents (mapping types excluded)
collection type hint equivalent

list

typing.List

set

typing.Set

frozenset

typing.FrozenSet

collections.deque

typing.Deque

collections.abc.MutableSequence

typing.MutableSequence

collections.abc.Sequence

typing.Sequence

collections.abc.Collection

typing.Collection

collections.abc.Container

typing.Container

collections.abc.Set

typing.AbstractSet

collections.abc.MutableSet

typing.MutableSet

PEP 585—Type Hinting Generics In Standard Collections 启动了一个多年的过程,以提高泛型类型命中的可用性。我们可以将这个过程总结为 4 个步骤:

  1. 在 Python 3.7 中引入 from __future__ import annotations,以允许使用标准库类作为具有 list[str] 符号的泛型。
  2. 使该行为成为 Python 3.9 中的默认行为: list[str] 现在无需导入 __future__即可工作。
  3. 弃用typing模块中的所有冗余泛型类型。Python 解释器不会发出弃用警告,因为当检查的程序面向 Python 3.9 或更新版本时,类型检查器应该标记弃用类型。
  4. 在 Python 3.9 5 年后发布的第一个 Python 版本中删除那些冗余的泛型类型。按照目前的节奏,这可能是 Python 3.14,也就是 Python Pi。

现在让我们看看如何注解泛型元组。

元组

有三种方法可以注解元组类型:

  1. 元组作为记录
  2. 元组作为有命名字段的记录
  3. 元组作为不可变序列

1.Tuples as records

如果您使用元组作为记录,请使用内置的tuple并在 [] 中声明字段的类型。

例如,如果要接受带有城市名称、人口和国家的元组:('Shanghai', 24.28, 'China'),类型提示将是 tuple[str, float, str] 。

考虑一个函数,它接受一对地理坐标并返回一个 Geohash,函数是这样调用的:

>>> shanghai = 31.2304, 121.4737
>>> geohash(shanghai)
'wtw3sjq6q'

这就是 geohash 的定义方式,使用来自 PyPI 的 geolib 包:

例 8-11。带有 geohash 函数的coordinates.py。

from geolib import geohash as gh  # type: ignore  1PRECISION = 9def geohash(lat_lon: tuple[float, float]) -> str:  2return gh.encode(*lat_lon, PRECISION)
  1. 此注释会取消 Mypy 报告 geolib 包没有类型提示。
  2. lat_lon 参数注解为具有两个float字段的元组。

TIP:

对于 Python < 3.9,在类型提示中导入并使用 typing.Tuple。它已被弃用,但将保留在标准库中至少到 2024 年。

Tuples as records with named fields

要注解具有多个字段的元组,或您的代码在许多地方使用的特定类型的元组,我强烈建议使用 Typing.NamedTuple——如第 5 章所介绍的。这是示例 8-11 的带有 NamedTuple 的变体:

例 8-12。带有 NamedTuple Coordinates 和 geohash 函数的coordinates_named.py。

from typing import Tuple, NamedTuplefrom geolib import geohash as gh  # type: ignorePRECISION = 9class Coordinate(NamedTuple):lat: floatlon: floatdef geohash(lat_lon: Coordinate) -> str:return gh.encode(*lat_lon, PRECISION)

如“Overview of data class builders”中所述,typing.NamedTuple 是元组子类的工厂,因此 Coordinate 与 Tuple[float, float] 一致,但反之则不然---由于Coordinate 有 NamedTuple 添加的额外方法,比如 ._as_dict(),也可以有用户定义的方法。

实际上,这意味着将 Coordinate 实例传递给下面定义的display函数是类型安全的。

def display(lat_lon: tuple[float, float]) -> str:lat, lon = lat_lonns = 'N' if lat >= 0 else 'S'ew = 'E' if lon >= 0 else 'W'return f'{abs(lat):0.1f}°{ns}, {abs(lon):0.1f}°{ew}'

Tuples as immutable sequences

要注解用作不可变列表的未指定长度的元组,您必须指定单个类型,后面跟逗号和 ...(这是 Python 的省略号标记,由三个句点组成,而不是 Unicode U+2026—HORIZONTAL ELLIPSIS)。

例如, tuple[int, ...] 代表一个由int类型的值组成的元组。

省略号表示可以接受 >= 1 的任意数量的元素。无法为未指定长度的元组指定多个字段类型。

这是一个 columnize 函数,它将序列转换为具有未指定长度的元组列表形式的行和单元格表。这对于在列中显示项目很有用,如下所示:

>>> animals = 'drake fawn heron ibex koala lynx tahr xerus yak zapus'.split()
>>> table = columnize(animals)
>>> table
[('drake', 'koala', 'yak'), ('fawn', 'lynx', 'zapus'), ('heron', 'tahr'),('ibex', 'xerus')]
>>> for row in table:
...     print(''.join(f'{word:10}' for word in row))
...
drake     koala     yak
fawn      lynx      zapus
heron     tahr
ibex      xerus

示例 8-13 展示了 columnize 的实现。注意返回类型:

list[tuple(str, ...)]

例 8-13。 columnize.py 返回字符串元组列表。

from collections.abc import Sequencedef columnize(sequence: Sequence[str], num_columns: int = 0
) -> list[tuple[str, ...]]:if num_columns == 0:num_columns = round(len(sequence) ** 0.5)num_rows, reminder = divmod(len(sequence), num_columns)num_rows += bool(reminder)return [tuple(sequence[i::num_rows]) for i in range(num_rows)]

泛型映射

通用映射类型被注解为 MappingType[KeyType, ValueType]。内置 dict 和 collections 以及 collections.abc 中的映射类型在 Python ≥ 3.9 中接受这种表示方法。对于早期版本,您必须使用 Typing.Dict 和来自 Typing 模块的其他映射类型,如“Legacy Support and Deprecated Collection Types”中所述。

例 8-14 展示了返回倒排索引的函数的实际使用,以按名称搜索 Unicode 字符——例 4-21 的变体更适合我们将在第 21 章研究的服务器端代码。

给定开始和结束的 Unicode 字符代码,name_index 返回一个 Dict[str, Set[str]],它是一个倒排索引,将每个单词映射到名称中包含该单词的一组字符。例如,将 ASCII 字符从 32 索引到 64 后,这里是映射到单词 'SIGN' 和 'DIGIT' 的字符集,以及如何找到名为 'DIGIT EIGHT' 的字符:

>>> index = name_index(32, 65)
>>> index['SIGN']
{'$', '>', '=', '+', '<', '%', '#'}
>>> index['DIGIT']
{'8', '5', '6', '2', '3', '0', '1', '4', '7', '9'}
>>> index['DIGIT'] & index['EIGHT']
{'8'}

下面是带有 name_index 函数的 charindex.py 的源代码。除了 Dict[] 类型提示之外,这个例子还有三个首次出现在书中的功能。

示例 8-14. charindex.py

import sys
import re
import unicodedata
from collections.abc import IteratorRE_WORD = re.compile(r'\w+')
STOP_CODE = sys.maxunicode + 1def tokenize(text: str) -> Iterator[str]:  1"""return iterable of uppercased words"""for match in RE_WORD.finditer(text):yield match.group().upper()def name_index(start: int = 32, end: int = STOP_CODE) -> dict[str, set[str]]:index: dict[str, set[str]] = {}  2for char in (chr(i) for i in range(start, end)):if name := unicodedata.name(char, ''):  3for word in tokenize(name):index.setdefault(word, set()).add(char)return index
  1. tokenize 是一个生成器函数。第 17 章会介绍生成器。
  2. 局部变量index被注解。如果没有这个类型提示,Mypy 会报错:错误:Need type annotation for 'index' (hint: "index: dict[<type>, <type>] = ...").
  3. 我在 if 条件中使用了 海象运算符 := 。它将 unicodedata.name() 调用的结果分配给 name,整个表达式的计算结果为该结果。当结果为 '' 时,这是错误的,index不会更新。

Note:

当使用 dict 作为记录时,通常所有的键都是 str 类型的,不同类型的值取决于键。这在“TypedDict”,第 15 章中有介绍。

抽象基类

在你返回的内容上保持保守,在你接收的内容上保持自由。

----Postel 定律,又名鲁棒性原则

表 8-1 列出了 collections.abc 中的几个抽象类。理想情况下,函数应该接受这些抽象类型的参数——或者它们在 Python 3.9 之前的等价类型——而不是具体类型。这为调用者提供了更大的灵活性。

给定这个函数签名:

from collections.abc import Mappingdef name2hex(name: str, color_map: Mapping[str, int]) -> str:

使用 abc.Mapping 允许调用者提供 dict、defaultdict、ChainMap、UserDict 的子类或作为 Mapping 子类的任何其他类型的实例。

相反,请考虑以下签名:

def name2hex(name: str, color_map: Dict[str, int]) -> str:

现在 color_map 必须是 dict 或其子类型之一,例如 DefaultDict 或 OrderedDict。特别是,collections.UserDict 的子类不会通过 color_map 的类型检查,尽管它是创建用户定义映射的推荐方法,正如我们在“子类化 UserDict”中看到的那样。Mypy 会拒绝 UserDict 或从它派生的类的实例,因为 UserDict 不是 dict 的子类;他们是兄弟姐妹,都是 abc.MutableMapping 的子类。

因此,一般情况下最好使用 abc.Mapping 或abc.MutableMapping 而不是 dict (或在遗留代码中typing.Dict)作为参数类型。如果 name2hex 函数不需要更改给定的 color_map,则 color_map 最准确的类型提示是 abc.Mapping。这样,调用者不需要提供一个对象来实现像 setdefault、pop 和 update 这样的方法,这些方法是 MutableMapping 接口的一部分,但不是 Mapping 的一部分。这与 Postel 定律的第二部分有关:“对你所接受的事物保持自由”。

Postel 定律还告诉我们在发送的内容上要保守。函数的返回值永远是一个具体的对象,所以返回类型提示应该是一个具体的类型,如“泛型集合”中的示例——假设代码将在 Python 3.9 上运行,它使用 list[str]。

def tokenize(text: str) -> list[str]:return text.upper().split()

在typing.List条目下,Python文档说:

列表的泛型版本,用于注解返回类型。要注解参数,最好使用抽象集合类型,例如 Sequence 或 Iterable。

类似的注解出现在用于 Typing.Dict 和 typing.Set 的条目中。

请记住,来自 collections.abc 的大多数 ABC 和来自collections的其他具体类,以及内置集合,从 Python 3.9 开始支持泛型类型提示表示法,如 collections.deque[str]。仅需要相应的typing集合来支持用 Python 3.8 或更早版本编写的代码。成为泛型的类的完整列表出现在 Implementation of PEP 585—Type Hinting Generics In Standard Collections中。

为了结束我们在类型提示中对 ABC 的讨论,我们需要谈谈Numbers。

The Fall of the Numeric Tower

numbers 包定义了 PEP 3141—A Type Hierarchy for Numbers 中描述的所谓的数字塔。塔是 ABC 的线性层次结构,Number在塔的顶部:

  • Number

  • Complex

  • Real

  • Rational

  • Integral

这些 ABC 非常适合运行时类型检查,但它们不支持静态类型检查。PEP 484 的The Numeric Tower部分拒绝了numbers  ABC,并规定内置类型 complex、float 和 int 应被视为特殊情况,如“int 与 complex 一致”中所述。

我们将在第 13 章的“The numbers ABCs and numeric protocols”中讨论这个问题,该章专门介绍协议和 ABC。

在实践中,如果你想为静态类型检查去注解数字参数,你有几个选择:

  1. 使用 PEP 488 推荐的具体类型 int、float、complex 之一;
  2. 声明一个Union类型,如 Union[float, Decimal, Fraction];
  3. 如果您想避免硬编码具体类型,请使用数字协议,例如“运行时可检查静态协议”中介绍的 SupportsFloat。

即将到来的“静态协议”部分是理解数字协议的前置条件。

同时,让我们来看看最有用的类型提示 ABC 之一:Iterable。

Iterable

我刚刚引用的 Typing.List 文档推荐 Sequence 和 Iterable 用于函数参数类型提示。

Iterable 参数的一个示例出现在标准库中的 math.fsum 函数中:

def fsum(__seq: Iterable[float]) -> float:

STUB FILES AND THE TYPESHED PROJECT:

从 Python 3.10 开始,标准库没有注解,但 Mypy、PyCharm 等可以在 Typeshed 项目中以存根文件的形式找到必要的类型提示: 带有 .pyi 扩展名的特殊源文件,具有带注解的函数和方法签名,没有实现——很像 C 中的头文件。

math.fsum 的签名在 /stdlib/2and3/math.pyi 中。 __seq 中的前导下划线是 PEP 484 约定,在“Annotating positional-only and variadic parameters”中进行了解释。


示例 8-15 是另一个使用 Iterable 参数的示例,该参数生成 Tuple[str, str] 的项。以下是该函数的使用方法:

>>> l33t = [('a', '4'), ('e', '3'), ('i', '1'), ('o', '0')]
>>> text = 'mad skilled noob powned leet'
>>> from replacer import zip_replace
>>> zip_replace(text, l33t)
'm4d sk1ll3d n00b p0wn3d l33t'

下面是具体的实现:

示例 8-15. replacer.py

from collections.abc import IterableFromTo = tuple[str, str]  1def zip_replace(text: str, changes: Iterable[FromTo]) -> str:  2for from_, to in changes:text = text.replace(from_, to)return text
  1. FromTo 是一个类型别名:我将 tuple[str, str] 赋值给 FromTo,以使 zip_replace 的签名更具可读性。
  2. changes参数是一个 Iterable[FromTo];这与 Iterable[Tuple[str, str]] 相同,但更短且更易于阅读。

EXPLICIT TYPEALIAS IN PYTHON 3.10

类型别名:PEP 613—Explicit Type Aliases 介绍了一种特殊的类型,类型别名,使创建类型别名的赋值更加可见且更容易进行类型检查。从 Python 3.10 开始,这是创建类型别名的首选方式:

from typing import TypeAliasFromTo: TypeAlias = tuple[str, str]

Iterable 对比 Sequence

math.fsum 和 replacer.zip_replace 都必须遍历整个 Iterable 参数以返回结果。给定一个无限迭代(例如 itertools.cycle 生成器)作为输入,这些函数将消耗所有内存并使 Python 进程崩溃。尽管存在这种潜在危险,但在现代 Python 中提供接受 Iterable 输入的函数是相当普遍的,即使它们必须完全处理它才能返回结果。这使调用者可以选择将输入数据作为生成器而不是预先构建的序列提供,如果输入项的数量很大,则可能会节省大量内存。

另一方面,示例 8-13 中的 columnize 函数需要一个 Sequence 参数,而不是一个 Iterable,因为它必须获取输入的 len() 来决定行数。

与 Sequence 一样,Iterable 最好用作参数类型。作为返回类型太模糊了。函数应该更精确地了解它返回的具体类型。

与 Iterable 密切相关的是 Iterator 类型,在示例 8-14 中用作返回类型。我们将在第 17 章讨论生成器和经典迭代器。

参数化泛型和TypeVar

一个参数化泛型是泛型类型,写成 list[T] ,其中 T 是一个类型变量,每次使用都会绑定到特定类型。这允许参数类型与返回值类型一致。

示例 8-16 定义了 sample,一个带有两个参数的函数:一个元素类型为T的序列 和一个 int。它返回一个同类型 T 的元素列表,从第一个参数中随机选取。

以下是两个示例说明了sample的行为:

  1. 如果使用类型为 Tuple[int, ...] 的元组调用(与 Sequence[int] 一致),则类型参数为 int,因此返回类型为 List[int];
  2. 如果使用与 Sequence[str] 一致的 str 调用,则类型参数为 str,因此返回类型为 List[str]。

下面是实现:

示例 8-14. sample.py

from random import shuffle
from typing import TypeVar
from collections.abc import SequenceT = TypeVar('T')def sample(population: Sequence[T], size: int) -> list[T]:if size < 1:raise ValueError('size must be >= 1')result = list(population)shuffle(result)return result[:size]

以下是我在sample.py中使用类型变量的两个示例:

  1. 如果调用时传入类型为 tuple[int, ...] 的元组(与 Sequence[int] 一致),则类型参数为 int,因此返回类型为 list[int];
  2. 如果调用时传入与 Sequence[str] 一致的 str 类型实例,则类型参数为 str,因此返回类型为 list[str]。

为什么要使用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

在不使用 TypeVar 的情况下,mode可能具有以下签名:

例 8-13。 mode_float.py:mode对float和子类型进行操作

from collections import Counter
from collection.abc import Iterabledef 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 的许多用途涉及 int 或 float 值,但 Python 有其他数字类型,希望返回类型遵循给定 Iterable 的元素类型。我们可以改进那个 TypeVar。让我们从一个简单但错误的参数化签名开始:

from typing import Iterable, TypeVarT = TypeVar('T')def mode(data: Iterable[T]) -> T:

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

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

被限制的类型变量TypeVar

TypeVar 接受额外的位置参数来限制类型参数。所以签名可以这样改进,以接受更多的数字类型:

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

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

但是,statistics.mode 文档包含以下示例:

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

临时解决方案,我们可以将 str 添加到 NumberT 定义中:

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

这当然可以执行,但如果 NumberT 接受 str ,它的名字就是不恰当的。更重要的是,我们不能永远维护一个类型的列表,因为我们意识到mode可以处理它们。我们可以使用 TypeVar 的另一个特性来变的更加合理,接下来介绍。

有边界的TypeVar

查看示例 8-17中的mode主体,我们看到 Counter 类用于排名。 Counter 基于 dict,因此Iterable的data的元素类型必须是可散列的。

起初,这个签名似乎有效:

from typing import Iterable, Hashabledef 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 TypeVarHashableT = 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 声明中指定的类型之一。
  • 有界类型变量将被设置为表达式的推断类型——只要推断类型与 TypeVar 的 bound= 关键字参数中声明的边界一致.

Note:

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

Typing.TypeVar 构造函数还有其他可选参数——covariant and contravariant——我们将在第 15 章“covariant”中介绍。

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

AnyStr 预定义类型变量

typing模块包括一个名为 AnyStr 的预定义 TypeVar。它是这样定义的:

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

AnyStr 用于许多接受字节或 str 并返回给定类型值的函数中。

现在,进入typing.Protocol,这是Python 3.8 的一个新特性,可以支持更多Pythonic 的类型提示的用法。

静态协议

NOTE:在面向对象编程中,“协议”作为非正式接口的概念与 Smalltalk 一样古老,并且从一开始就是 Python 的重要组成部分。然而,在类型提示的上下文中,协议是一个 Typing.Protocol 子类,定义了一个类型检查器可以验证的接口。这两种协议都在第 13 章中介绍。这只是在函数注解上下文中的简要介绍。

PEP 544—Protocols: Structural subtyping (static duck typing) 中介绍​​的协议类型类似于 Go 中的接口:通过指定一个或多个方法来定义协议类型,类型检查器验证被检查的对象是否实现了协议类型中的方法。

在 Python 中,协议的定义需要编写为 Typing.Protocol 的子类。但是,实现协议的类不需要继承、注册或声明与定义协议的类的任何关系。由类型检查器来查找可用的协议类型并强制使用它们。

这是一个可以借助 Protocol 和 TypeVar 解决的问题。假设您要创建一个函数 top(it, n) 来返回 iterable 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。带有未定义 T 类型参数的 top 函数。

def top(series: Iterable[T], length: int) -> List[T]:ordered = sorted(series, reverse=True)return ordered[:length]

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

>>> 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列表进行排序,因为Spam实现了 __lt__——支持 < 运算符的特殊方法.

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

这是新的 SupportsLessThan 类型,一个协议:

例 8-20。 compare.py:SupportsLessThan Protocol类型的定义:

from typing import Protocol, Anyclass SupportsLessThan(Protocol):  1def __lt__(self, other: Any) -> bool: ...  2
  1. 协议是typing.Protocol 的子类。
  2. 协议的主体有一个或多个方法定义,在它们的主体中带有 ...。

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

给定 SupportsLessThan,我们现在可以定义 top 的这个工作版本:

例 8-21。 top.py:使用bounded=SupportsLessThan 的 TypeVar 定义 top 函数

from typing import TypeVar
from collections.abc import Iterable
from comparable import SupportsLessThanLT = TypeVar('LT', bound=SupportsLessThan)def top(series: Iterable[LT], length: int) -> list[LT]:return sorted(series, reverse=True)[:length]

让我们对top函数进行测试驱动。示例 8-18 显示了与 pytest 一起使用的测试套件的一部分。它尝试使用生成tuple [int, str] 类型的生成器表达式调用 top,然后使用对象列表调用。对于object的列表,我们期望得到一个 TypeError 异常。

例 8-22。 top_test.py:top 测试套件的部分代码

from collections.abc import Iterator
from typing import TYPE_CHECKING  1import pytestfrom top import top# several lines omitteddef test_top_tuples() -> None:fruit = 'mango pear apple kiwi banana'.split()series: Iterator[tuple[int, str]] = (  2(len(s), s) for s in fruit)length = 3expected = [(6, 'banana'), (5, 'mango'), (5, 'apple')]result = top(series, length)if TYPE_CHECKING:  3reveal_type(series)  4reveal_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)  5assert "'<' not supported" in str(excinfo.value)
  1. Typing.TYPE_CHECKING 常量在运行时始终为 False,但类型检查器在进行类型检查时假定它为 True。
  2. aeries变量的显式类型声明,使 Mypy 输出更易于阅读。正如我们将在“Generic Iterable Types”.中看到的那样
  3. 这个if会阻止接下来的三行在测试运行时执行。
  4. reveal_type() 不能在运行时调用,因为它不是一个常规函数而是一个 Mypy 调试工具——这就是为什么没有导入它的原因。Mypy 将为每个reveal_type() 伪函数调用输出一条调试消息,显示参数的推断类型。
  5. 此行将被 Mypy 标记为错误。

上面的测试通过了——不管在 top.py 中有没有类型提示,它们都会通过。更重要的是,如果我使用 Mypy 检查该测试文件,我会发现 TypeVar 正在按预期工作。请参见示例 8-23 中的 mypy 命令输出

Warning:从 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]]" 1
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]]" 2
top_test.py:41: note:Revealed type is "builtins.list[builtins.object*]" 3
top_test.py:43: error:Value of type variable "LT" of "top" cannot be "object"  4
Found 1 error in 1 file (checked 1 source file)
  1. 在 test_top_tuples 中,reveal_type(series) 显示它是一个 Iterator[tuple[int, str]]——这是我显示声明的。
  2. reveal_type(result) 确认 top 调用返回的类型是我想要的:给定series的类型,结果是 list[tuple[int, str]]。
  3. 在 test_top_objects_error 中,reveal_type(series) 显示它是 list[object*]。 Mypy 在推断出的任何类型后放置一个 *:我没有在此测试中注解series的类型。
  4. Mypy 标记了这个测试故意触发的错误:Iterable 系列的元素类型不能是 object(它必须是 SupportsLessThan 类型)

协议类型相对于 ABC 的一个关键优势是,类型不需要任何特殊声明来与协议类型保持一致。这允许利用已存在的类型或在我们无法控制的代码中实现的类型来创建协议。我不需要使用 SupportsLessThan 派生或注册 str、tuple、float、set 等,以便在需要 SupportsLessThan 参数的地方使用它们。他们只需要实现 __lt__方法。并且类型检查器仍然可以完成它的工作,因为 SupportsLessThan 被显示定义为协议——与鸭子类型常见的隐式协议形成对比,后者对于类型检查器来说是不可见的。

PEP 544—Protocols: Structural subtyping (static duck typing).中引入了特殊的协议类。示例 8-21 展示了为什么这个特性被称为静态鸭子类型:注解top的series参数的解决方案是说"series的声明类型无关紧要,只要它实现了 __lt__ 方法”。Python 的鸭子类型总是允许我们隐式地说这件事,从而让静态类型检查器对此毫不知晓。类型检查器无法读取 CPython 的 C 源代码,也无法执行控制台实验来找出 sorted 方法只需要所有的元素都支持支持 <操作符。

现在通过协议使鸭子类型对于静态类型检查器是显示可见的。这就解释了为什么 Typing.Protocol 为我们提供了静态鸭子类型。

关于typing.Protocol,后面还有更多内容。我们将在第四部分回到它,第 13 章对比了结构类型、鸭子类型和 ABC——另一种形式化协议的方法。此外,“重载签名”(第 15 章)解释了如何使用 @typing.overload 声明重载的函数签名,并包含一个使用 Typing.Protocol 和有界 TypeVar 的通用示例。

Note:

Typing.Protocol 可以在不丢失功能的情况下注解“Types are defined by supported operations” 中提供的double功能。关键是用 __mul__ 方法定义一个协议类。我邀请你这样做作为练习。解决方案出现在“The typed double function” (Chapter 13)中。

Callable

为了注解高阶函数返回的回调参数或函数对象,typing 模块提供了 Callable 类型,其参数化如下:

Callable[[ParamType1, ParamType2], ReturnType]

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

这是上述上下文中的示例:

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

repl 函数是简单交互式解释器的一部分。在正常使用期间,repl 函数使用 Python 内置的input来读取用户的输入。但是,对于自动化测试或与其他输入源的集成,repl 接受可选的 input_fn 参数:具有与input相同的参数和返回类型的 Callable。

内置 input() 在 typeshed 上有以下签名:

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

该函数与此 Callable 类型提示一致:

Callable[[Any], str]

再举一个例子,在第 10 章中,示例 10-3 中的 Order.__init__ 方法使用了这个签名:

class Order:def __init__(self,  1customer: Customer,cart: Sequence[LineItem],promotion: Optional[Callable[['Order'], float]] = None,  2) -> None:  3
  1. self 很少需要类型提示
  2. promotion可能是 None 或 Callable[[Order], float]:一个接受 Order 类型的参数并返回一个 float类型的值 的函数。
  3. __init__ 总是返回 None,但我还是建议为它添加返回类型提示

请注意,Order 类型在 Callable 类型提示中显示为字符串 'Order',否则 Python 会引发 NameError: name 'Order' is not defined--因为 Order 类是 Python 读取整个类实体之后定义的——我们将在第 25 章:类元编程中讨论这个问题。
TIP:

PEP 563—Postponed Evaluation of Annotations在 Python 3.7 中实现,以支持注释中的前向引用,避免在前面的例子中将 Order 写成字符串。但是,该功能仅在模块最上方使用 from __future__ import annotations时启用,以避免破坏依赖于在运行时读取注解的代码,例如 pydantic 和 FastAPI 包 - 仅举两个例子。PEP 563 行为计划成为 Python 3.10 中的默认行为,但这已被推迟 ---关心在运行时使用注解的人和不关心的人之间已经达成了妥协。有关更多信息,请参阅 Python 指导委员会的此消息:PEP 563 and Python 3.10.

Callable[] 中没有注解可选参数或关键字参数的语法。文档说“这种函数类型很少用作回调类型”。如果需要类型提示来匹配具有动态签名的函数,将整个参数列表替换为 ...,如下所示:Callable[..., ReturnType]。

NoReturn

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

例如: sys.exit() 抛出 SystemExit异常,以终止 Python 进程。

它在 typeshed 中的签名是

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

__status 参数是仅限位置参数,它有一个默认值。存根文件没有写全默认值:它们使用 ... 代替。__status 的类型是 object,这意味着它也可能是 None,因此将其标记为 Optional[object] 是多余的。

在第 24 章中,示例 24-6 在 __flag_unknown_attrs 中使用了 NoReturn,该方法旨在生成用户友好且全面的错误消息,然后抛出 AttributeError异常。

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

注解仅位置参数和可变参数

回忆一下示例 7-9 中的tag函数。我们最后一次看到它的签名是在“仅位置参数”部分:

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

这是注解好的tag函数,用多行编写——长签名的常见约定,换行符与blue 格式化程序一样:

from typing import Optionaldef 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 Optionaldef tag(__name: str, *content: str, class_: Optional[str] = None,**attrs: str) -> str:

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

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

有缺陷的Typing模块和强大的测试

大型公司代码库的维护者报告说,静态类型检查器发现了许多错误,并且比仅在代码在生产环境中运行后才发现错误的修复成本更低。

但是,必须注意的是,自动化测试是标准做法,并且在我所知道的公司中引入静态类型检查之前很久就被广泛采用。

即使在它们最有益的上下文中,也不能相信静态类型是正确性的最终仲裁者。不难发现静态类型检查有两个主要的问题:

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

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

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

常见的数据约束不能在类型系统中表达——即使是简单的。例如:类型提示无法确保“quantity必须是大于 0 的整数”或者是“标签必须是具有 6 到 12 个 ASCII 字母的字符串”。通常,类型提示无助于捕获业务逻辑中的错误。

鉴于这些警告,类型提示不能成为软件质量的支柱,并且强制的类型检查无疑会放大其缺点。

将静态类型检查器视为现代 CI 流水线中的工具之一,以及test runner、linter 等。CI 流水线的重点是减少软件故障,自动化测试会捕获许多类型提示无法触及的错误。无论是否带有类型提示,您都可以用 Python 编写任何代码,并且所有代码都可以在 Python 中进行测试。

NOTE:

本节的标题和结论受到 Bruce Eckel 的文章 Strong Typing vs. Strong Testing的启发,该文章也发表在 Joel Spolky 编辑的我编辑的选集  The Best Software Writing中。Bruce 是 Python 的粉丝,也是有关 C++、Java、Scala 和 Kotlin 书籍的作者。在那篇文章中,他讲述了在学习 Python 之前他是如何成为静态类型的倡导者并得出结论的:"如果一个 Python 程序有足够的单元测试,它就可以像带有足够单元测试的 C++、Java 或 C# 程序一样健壮(尽管 Python 中的测试编写起来会更快)。”

这结束了我们现在对 Python 类型提示的讲解。它们也是第 15 章的主要焦点,其中涵盖了泛型类、方差、签名重载、类型转换等。同时,类型提示将在整本书的几个例子中客串出现。

第八章 函数中的类型提示相关推荐

  1. 全面理解Python中的类型提示(Type Hints)

    众所周知,Python 是动态类型语言,运行时不需要指定变量类型.这一点是不会改变的,但是2015年9月创始人 Guido van Rossum 在 Python 3.5 引入了一个类型系统,允许开发 ...

  2. php 类型提示,PHP中的类型提示(type hinting)功能介绍

    PHP中的类型提示(type hinting)功能介绍 这篇文章主要介绍了PHP中的类型提示(type hinting)功能介绍,本文讲解了类型提示的作用和使用方法以及使用示例,需要的朋友可以参考下 ...

  3. python怎么显示提示_Python中的类型提示(中)

    Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发. 3.接口存根文件 这个选项允许你如下图一般保存你的代码: 并在原文件的旁边添加一个扩展名为pyi的文件: ...

  4. 《流畅的Python第二版》读书笔记——函数中的类型注解

    引言 这是<流畅的Python第二版>抢先版的读书笔记.Python版本暂时用的是python3.10.为了使开发更简单.快捷,本文使用了JupyterLab. 本章关注于Python在函 ...

  5. iview在render函数中添加Poptip提示

    前言: 在使用iview的table的时候删除一般为了防止误删,我们会用 poptip 气泡提示来进行操作二次限制,这里使用iview的table的render函数渲染直接加入 效果图: 实现代码: ...

  6. 在Python中检查类型的规范方法是什么?

    检查给定对象是否为给定类型的最佳方法是什么? 如何检查对象是否从给定类型继承? 假设我有一个对象o . 如何检查是否为str ? #1楼 前往雨果: 您可能是说list而不是array ,但这指向类型 ...

  7. php 单元测试 静态类,可选的PHP类型提示/检查单元测试或静态分析?

    PHP类型提示不支持标量变量[1],如int或string 但是,我们发现在连续集成期间注释函数中的类型(int或string)以发现错误仍然非常有用,例如: 目前我用的方法就像 function f ...

  8. (原创)c++中的类型擦除

    c++11 boost技术交流群:296561497,欢迎大家来交流技术. 关于类型擦除,可能很多人都不清楚,不知道类型擦除是干啥的,为什么需要类型擦除.有必要做个说明,类型擦除就是将原有类型消除或者 ...

  9. Python函数 — 类型提示和存根文件

    1.类型提示 类型提示(Type Hints)也叫函数标注,函数注解,元数据(元数据是用来描述数据的数据). 详见 PEP 3107 和 PEP 484 Python中,自定义函数的时候,可以为函数添 ...

最新文章

  1. 在winform程序里实现最小化隐藏到windows右下角
  2. 被Html的Button标签耍了一次
  3. Luogu P4707 重返现世 (拓展Min-Max容斥、DP)
  4. mysql null的作用_MySQL中对于NULL值的理解和使用教程
  5. Java开发Web Service的简介
  6. C#LeetCode刷题之#892-三维形体的表面积(Surface Area of 3D Shapes)
  7. SONY的CMOS 图像传感器技术发展路线
  8. sql stuff 函数_SQL STUFF函数概述
  9. ElasticSearch讲解
  10. 线程池版本的mysql_MySQL线程池内幕
  11. VS+VSS代码管理
  12. 探讨基于球谐函数的全局光照
  13. 章文嵩评价左耳朵耗子(2016年发布于内网)
  14. linux下载tar包和rpm包以及镜像的地址分享一下
  15. matlab TVdenoise,TV_Denoise TV全变分模型图像去噪 以及高斯模糊处理 2D Graphic 2D图形编程 272万源代码下载- www.pudn.com...
  16. 雅虎市值_也许不算雅虎! 刚出来
  17. 西部数码虚拟服务器,西部数码虚拟主机301转向功能介绍
  18. 从零开始学前端 - 16. JS对象Object介绍及常用方法
  19. Teams app 的 SSO 机制
  20. C++通过调用Python函数调用讯飞OCR识别接口

热门文章

  1. 【numpy】numpy中np.nonzero()的用法
  2. 中文能自动换行,针对字母、数字不会自动换行问题
  3. X-Mirage苹果屏幕录制工具7天试用期破解 imsoft.cnblogs
  4. 狼为什么被逼去吃草?
  5. vue+vuedraggable 实现文字/按钮/div拖拽组件详解
  6. arcgis 同名图层合并_【工具分享】ArcGIS中批量合并同名shp文件的代码实现
  7. python基于PHP+MySQL婚介交友网站的设计与开发
  8. 哔哩哔哩验证码的破解
  9. 获取html表格指定行的元素
  10. 抖音充值显示服务器繁忙是什么原因,抖音直播提现及充值常见问题解答