流畅的 Python 第二版(GPT 重译)(四)(1)https://developer.aliyun.com/article/1484436
一个更有用的示例涉及到我们在“为可靠比较标准化 Unicode”中看到的unicode.normalize
函数。如果您使用来自许多语言的文本,您可能希望在比较或存储之前对任何字符串s
应用unicode.normalize('NFC', s)
。如果您经常这样做,最好有一个nfc
函数来执行,就像示例 7-17 中那样。
示例 7-17。使用partial
构建一个方便的 Unicode 标准化函数
>>> import unicodedata, functools >>> nfc = functools.partial(unicodedata.normalize, 'NFC') >>> s1 = 'café' >>> s2 = 'cafe\u0301' >>> s1, s2 ('café', 'café') >>> s1 == s2 False >>> nfc(s1) == nfc(s2) True
partial
以可调用对象作为第一个参数,后跟要绑定的任意数量的位置参数和关键字参数。
示例 7-18 展示了partial
与示例 7-9 中的tag
函数一起使用,冻结一个位置参数和一个关键字参数。
示例 7-18。演示partial
应用于示例 7-9 中的tag
函数
>>> from tagger import tag >>> tag <function tag at 0x10206d1e0> # ① >>> from functools import partial >>> picture = partial(tag, 'img', class_='pic-frame') # ② >>> picture(src='wumpus.jpeg') '<img class="pic-frame" src="wumpus.jpeg" />' # ③ >>> picture functools.partial(<function tag at 0x10206d1e0>, 'img', class_='pic-frame') # ④ >>> picture.func # ⑤ <function tag at 0x10206d1e0> >>> picture.args ('img',) >>> picture.keywords {'class_': 'pic-frame'}
①
从示例 7-9 导入tag
并显示其 ID。
②
通过使用tag
从tag
创建picture
函数,通过使用'img'
固定第一个位置参数和'pic-frame'
关键字参数。
③
picture
按预期工作。
④
partial()
返回一个functools.partial
对象。⁴
⑤
一个functools.partial
对象具有提供对原始函数和固定参数的访问的属性。
functools.partialmethod
函数与 partial
执行相同的工作,但设计用于与方法一起使用。
functools
模块还包括设计用作函数装饰器的高阶函数,例如 cache
和 singledispatch
等。这些函数在第九章中有介绍,该章还解释了如何实现自定义装饰器。
章节总结
本章的目标是探索 Python 中函数的头等性质。主要思想是你可以将函数分配给变量,将它们传递给其他函数,将它们存储在数据结构中,并访问函数属性,从而使框架和工具能够根据这些信息进行操作。
高阶函数,作为函数式编程的基本要素,在 Python 中很常见。sorted
、min
和 max
内置函数,以及 functools.partial
都是该语言中常用的高阶函数的例子。使用 map
、filter
和 reduce
不再像以前那样常见,这要归功于列表推导式(以及类似的生成器表达式)以及新增的归约内置函数如 sum
、all
和 any
。
自 Python 3.6 起,可调用对象有九种不同的类型,从使用 lambda
创建的简单函数到实现 __call__
的类实例。生成器和协程也是可调用的,尽管它们的行为与其他可调用对象非常不同。所有可调用对象都可以通过内置函数 callable()
进行检测。可调用对象提供了丰富的语法来声明形式参数,包括仅限关键字参数、仅限位置参数和注释。
最后,我们介绍了 operator
模块和 functools.partial
中的一些函数,通过最小化对功能受限的 lambda
语法的需求,促进了函数式编程。
进一步阅读
接下来的章节将继续探讨使用函数对象进行编程。第八章专注于函数参数和返回值中的类型提示。第九章深入探讨了函数装饰器——一种特殊的高阶函数,以及使其工作的闭包机制。第十章展示了头等函数如何简化一些经典的面向对象设计模式。
在Python 语言参考中,“3.2. 标准类型层次结构”介绍了九种可调用类型,以及所有其他内置类型。
Python Cookbook 第 3 版(O’Reilly)的第七章,由 David Beazley 和 Brian K. Jones 撰写,是对当前章节以及本书的第九章的极好补充,主要涵盖了相同概念但采用不同方法。
如果你对关键字参数的原理和用例感兴趣,请参阅PEP 3102—关键字参数。
了解 Python 中函数式编程的绝佳入门是 A. M. Kuchling 的“Python 函数式编程 HOWTO”。然而,该文本的主要焦点是迭代器和生成器的使用,这是第十七章的主题。
StackOverflow 上的问题“Python: 为什么 functools.partial 是必要的?”有一篇由经典著作Python in a Nutshell(O’Reilly)的合著者 Alex Martelli 所撰写的高度信息化(且有趣)的回答。
思考问题“Python 是一种函数式语言吗?”,我创作了我最喜欢的演讲之一,“超越范式”,我在 PyCaribbean、PyBay 和 PyConDE 上做过演讲。请查看我在柏林演讲中遇到本书两位技术审阅者 Miroslav Šedivý 和 Jürgen Gmach 的幻灯片和视频。
¹ 来自 Guido 的Python 的起源博客的“Python‘函数式’特性的起源”。
² 调用一个类通常会创建该类的一个实例,但通过重写__new__
可以实现其他行为。我们将在“使用 new 实现灵活的对象创建”中看到一个例子。
³ 既然我们已经有了random.choice
,为什么要构建一个BingoCage
?choice
函数可能多次返回相同的项,因为选定的项未从给定的集合中移除。调用BingoCage
永远不会返回重复的结果——只要实例填充了唯一的值。
⁴ functools.py的源代码显示,functools.partial
是用 C 实现的,并且默认情况下使用。 如果不可用,自 Python 3.4 起提供了partial
的纯 Python 实现。
⁵ 在将代码粘贴到网络论坛时,还存在缩进丢失的问题,但我岔开了话题。
第八章:函数中的类型提示
还应强调Python 将保持动态类型语言,并且作者从未希望通过约定使类型提示成为强制要求。
Guido van Rossum,Jukka Lehtosalo 和Łukasz Langa,PEP 484—类型提示¹
类型提示是自 2001 年发布的 Python 2.2 中的类型和类的统一以来 Python 历史上最大的变化。然而,并非所有 Python 用户都同等受益于类型提示。这就是为什么它们应该始终是可选的。
PEP 484—类型提示引入了函数参数、返回值和变量的显式类型声明的语法和语义。其目标是通过静态分析帮助开发人员工具在不实际运行代码测试的情况下发现 Python 代码库中的错误。
主要受益者是使用 IDE(集成开发环境)和 CI(持续集成)的专业软件工程师。使类型提示对该群体具有吸引力的成本效益分析并不适用于所有 Python 用户。
Python 的用户群比这个宽广得多。它包括科学家、交易员、记者、艺术家、制造商、分析师和许多领域的学生等。对于他们中的大多数人来说,学习类型提示的成本可能更高——除非他们已经了解具有静态类型、子类型和泛型的语言。对于许多这些用户来说,由于他们与 Python 的交互方式以及他们的代码库和团队的规模较小——通常是“一个人的团队”,因此收益会较低。Python 的默认动态类型在编写用于探索数据和想法的代码时更简单、更具表现力,比如数据科学、创意计算和学习,
本章重点介绍 Python 函数签名中的类型提示。第十五章探讨了类的上下文中的类型提示,以及其他typing
模块功能。
本章的主要主题包括:
- 一个关于使用 Mypy 逐渐类型化的实践介绍
- 鸭子类型和名义类型的互补视角
- 注解中可能出现的主要类型类别概述——这大约占了本章的 60%
- 类型提示可变参数(
*args
,**kwargs
) - 类型提示和静态类型化的限制和缺点
本章的新内容
本章是全新的。类型提示出现在我完成第一版流畅的 Python之后的 Python 3.5 中。
鉴于静态类型系统的局限性,PEP 484 的最佳想法是引入逐渐类型系统。让我们从定义这个概念开始。
关于逐渐类型化
PEP 484 向 Python 引入了逐渐类型系统。其他具有逐渐类型系统的语言包括微软的 TypeScript、Dart(由 Google 创建的 Flutter SDK 的语言)和 Hack(Facebook 的 HHVM 虚拟机支持的 PHP 方言)。Mypy 类型检查器本身起初是一种语言:一种逐渐类型化的 Python 方言,带有自己的解释器。Guido van Rossum 说服了 Mypy 的创造者 Jukka Lehtosalo,使其成为检查带注释的 Python 代码的工具。
逐渐类型系统:
是可选的
默认情况下,类型检查器不应对没有类型提示的代码发出警告。相反,当无法确定对象类型时,类型检查器会假定Any
类型。Any
类型被认为与所有其他类型兼容。
不会在运行时捕获类型错误
静态类型检查器、linter 和 IDE 使用类型提示来发出警告。它们不能阻止在运行时将不一致的值传递给函数或分配给变量。
不会增强性能
类型注释提供的数据理论上可以允许在生成的字节码中进行优化,但截至 2021 年 7 月,我所知道的任何 Python 运行时都没有实现这样的优化。²
逐步类型化最好的可用性特性是注释始终是可选的。
使用静态类型系统,大多数类型约束很容易表达,许多很繁琐,一些很困难,而一些则是不可能的。³ 你很可能会写出一段优秀的 Python 代码,具有良好的测试覆盖率和通过的测试,但仍然无法添加满足类型检查器的类型提示。没关系;只需省略有问题的类型提示并发布!
类型提示在所有级别都是可选的:你可以有完全没有类型提示的整个包,当你将其中一个这样的包导入到使用类型提示的模块时,你可以让类型检查器保持沉默,并且你可以添加特殊注释来让类型检查器忽略代码中特定的行。
提示
寻求 100% 的类型提示覆盖可能会刺激没有经过适当思考的类型提示,只是为了满足指标。这也会阻止团队充分利用 Python 的强大和灵活性。当注释会使 API 不够用户友好,或者不必要地复杂化其实现时,应该自然地接受没有类型提示的代码。
实践中的逐步类型化
让我们看看逐步类型化在实践中是如何工作的,从一个简单的函数开始,逐渐添加类型提示,由 Mypy 指导。
注意
有几个与 PEP 484 兼容的 Python 类型检查器,包括 Google 的 pytype、Microsoft 的 Pyright、Facebook 的 Pyre—以及嵌入在 IDE 中的类型检查器,如 PyCharm。我选择了 Mypy 作为示例,因为它是最知名的。然而,其他类型检查器可能更适合某些项目或团队。例如,Pytype 设计用于处理没有类型提示的代码库,并仍然提供有用的建议。它比 Mypy 更宽松,还可以为您的代码生成注释。
我们将为一个返回带有计数和单数或复数词的字符串的 show_count
函数添加注释:
>>> show_count(99, 'bird') '99 birds' >>> show_count(1, 'bird') '1 bird' >>> show_count(0, 'bird') 'no birds'
示例 8-1 展示了show_count
的源代码,没有注释。
示例 8-1. messages.py 中没有类型提示的 show_count
def show_count(count, word): if count == 1: return f'1 {word}' count_str = str(count) if count else 'no' return f'{count_str} {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-1 中没有发现任何问题。
警告
我正在使用 Mypy 0.910,在我审阅这篇文章时是最新版本(2021 年 7 月)。Mypy 的 “介绍” 警告说它“正式是测试版软件。偶尔会有破坏向后兼容性的更改。” Mypy 给我至少一个与我在 2020 年 4 月写这一章时不同的报告。当你阅读这篇文章时,你可能会得到与这里显示的不同的结果。
如果函数签名没有注释,Mypy 默认会忽略它—除非另有配置。
对于 示例 8-2,我还有 pytest
单元测试。这是 messages_test.py 中的代码。
示例 8-2. messages_test.py 中没有类型提示
from pytest import mark from 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 == expected def test_show_count_zero(): got = show_count(0, 'part') assert got == 'no parts'
现在让我们根据 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:
提示
与其像--disallow-incomplete-defs
这样输入命令行选项,你可以按照Mypy 配置文件文档中描述的方式保存你喜欢的选项。你可以有全局设置和每个模块的设置。以下是一个简单的mypy.ini示例:
[mypy] python_version = 3.9 warn_unused_configs = True disallow_incomplete_defs = True
默认参数值
示例 8-1 中的show_count
函数只适用于常规名词。如果复数不能通过添加's'
来拼写,我们应该让用户提供复数形式,就像这样:
>>> show_count(3, 'mouse', 'mice') '3 mice'
让我们进行一点“类型驱动的开发”。首先我们添加一个使用第三个参数的测试。不要忘记为测试函数添加返回类型提示,否则 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
,在示例 8-3 中添加了可选的plural
参数。
示例 8-3. hints_2/messages.py中带有可选参数的showcount
def show_count(count: int, singular: str, plural: str = '') -> str: if count == 1: return f'1 {singular}' count_str = str(count) if count else 'no' if not plural: plural = singular + 's' return f'{count_str} {plural}'
现在 Mypy 报告“成功”。
警告
这里有一个 Python 无法捕捉的类型错误。你能发现吗?
def hex2rgb(color=str) -> tuple[int, int, int]:
Mypy 的错误报告并不是很有帮助:
colors.py:24: error: Function is missing a type annotation for one or more arguments
color
参数的类型提示应为color: str
。我写成了color=str
,这不是一个注释:它将color
的默认值设置为str
。
根据我的经验,这是一个常见的错误,很容易忽视,特别是在复杂的类型提示中。
以下细节被认为是类型提示的良好风格:
- 参数名和
:
之间没有空格;:
后有一个空格 - 在默认参数值之前的
=
两侧留有空格
另一方面,PEP 8 表示如果对于特定参数没有类型提示,则=
周围不应有空格。
使用None
作为默认值
在示例 8-3 中,参数plural
被注释为str
,默认值为''
,因此没有类型冲突。
我喜欢那个解决方案,但在其他情况下,None
是更好的默认值。如果可选参数期望一个可变类型,那么None
是唯一明智的默认值——正如我们在“可变类型作为参数默认值:不好的主意”中看到的。
要将None
作为plural
参数的默认值,签名将如下所示:
from typing import Optional def 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
是一个好习惯,可以缩短函数签名的长度。
警告
Optional
不是一个很好的名称,因为该注释并不使参数变为可选的。使其可选的是为参数分配默认值。Optional[str]
只是表示:该参数的类型可以是str
或NoneType
。在 Haskell 和 Elm 语言中,类似的类型被命名为Maybe
。
现在我们已经初步了解了渐进类型,让我们考虑在实践中“类型”这个概念意味着什么。
类型由支持的操作定义
文献中对类型概念有许多定义。在这里,我们假设类型是一组值和一组可以应用于这些值的函数。
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 abc def double(x: abc.Sequence): return x * 2
类型检查器将拒绝该代码。如果告诉 Mypy x
的类型是 abc.Sequence
,它将标记 x * 2
为错误,因为 Sequence
ABC 没有实现或继承 __mul__
方法。在运行时,该代码将与具体序列(如 str
、tuple
、list
、array
等)以及数字一起工作,因为在运行时会忽略类型提示。但类型检查器只关心显式声明的内容,abc.Sequence
没有 __mul__
。
这就是为什么这一节的标题是“类型由支持的操作定义”。Python 运行时接受任何对象作为 x
参数传递给 double
函数的两个版本。计算 x * 2
可能有效,也可能会引发 TypeError
,如果 x
不支持该操作。相比之下,Mypy 在分析带注释的 double
源代码时会声明 x * 2
为错误,因为它对于声明的类型 x: abc.Sequence
是不支持的操作。
在渐进式类型系统中,我们有两种不同类型观点的相互作用:
鸭子类型
Smalltalk——开创性的面向对象语言——以及 Python、JavaScript 和 Ruby 采用的视角。对象具有类型,但变量(包括参数)是无类型的。实际上,对象的声明类型是什么并不重要,只有它实际支持的操作才重要。如果我可以调用 birdie.quack()
,那么在这个上下文中 birdie
就是一只鸭子。根据定义,鸭子类型只在运行时强制执行,当尝试对对象进行操作时。这比名义类型更灵活,但会在运行时允许更多的错误。⁵
名义类型
C++、Java 和 C# 采用的视角,由带注释的 Python 支持。对象和变量具有类型。但对象只在运行时存在,类型检查器只关心在变量(包括参数)被注释为类型提示的源代码中。如果 Duck
是 Bird
的一个子类,你可以将一个 Duck
实例分配给一个被注释为 birdie: Bird
的参数。但在函数体内,类型检查器认为调用 birdie.quack()
是非法的,因为 birdie
名义上是一个 Bird
,而该类不提供 .quack()
方法。在运行时实际参数是 Duck
也无关紧要,因为名义类型是静态强制的。类型检查器不运行程序的任何部分,它只读取源代码。这比鸭子类型更严格,优点是在构建流水线中更早地捕获一些错误,甚至在代码在 IDE 中输入时。
Example 8-4 是一个愚蠢的例子,对比了鸭子类型和名义类型,以及静态类型检查和运行时行为。⁶
示例 8-4. birds.py
class Bird: pass class Duck(Bird): # ① def quack(self): print('Quack!') def alert(birdie): # ② birdie.quack() def alert_duck(birdie: Duck) -> None: # ③ birdie.quack() def alert_bird(birdie: Bird) -> None: # ④ birdie.quack()
①
Duck
是 Bird
的一个子类。
②
alert
没有类型提示,因此类型检查器会忽略它。
③
alert_duck
接受一个 Duck
类型的参数。
④
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
模块,参见 Example 8-5。
示例 8-5. daffy.py
from birds import * daffy = Duck() alert(daffy) # ① alert_duck(daffy) # ② alert_bird(daffy) # ③
①
这是有效的调用,因为 alert
没有类型提示。
②
这是有效的调用,因为 alert_duck
接受一个 Duck
参数,而 daffy
是一个 Duck
。
③
有效的调用,因为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 爱好者感到惊讶:静态类型检查器有时会发现我们知道会执行的程序中的错误。
然而,如果几个月后你被要求扩展这个愚蠢的鸟类示例,你可能会感激 Mypy。考虑一下woody.py模块,它也使用了birds
,在示例 8-6 中。
示例 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
。每个Duck
都是一个Bird
,但并非每个Bird
都是一个Duck
。
在运行时,woody.py中的所有调用都失败了。这些失败的连续性在示例 8-7 中的控制台会话中最好地说明。
示例 8-7. 运行时错误以及 Mypy 如何帮助
>>> from birds import * >>> woody = Bird() >>> alert(woody) # ① Traceback (most recent call last): ... AttributeError: 'Bird' object has no attribute 'quack' >>> >>> alert_duck(woody) # ② Traceback (most recent call last): ... AttributeError: 'Bird' object has no attribute 'quack' >>> >>> alert_bird(woody) # ③ Traceback (most recent call last): ... AttributeError: 'Bird' object has no attribute 'quack'
①
Mypy 无法检测到这个错误,因为alert
中没有类型提示。
②
Mypy 报告了问题:“alert_duck"的第 1 个参数类型不兼容:“Bird”;预期是"Duck”。
③
自从示例 8-4 以来,Mypy 一直在告诉我们alert_bird
函数的主体是错误的:“Bird"没有属性"quack”。
这个小实验表明,鸭子类型更容易上手,更加灵活,但允许不支持的操作在运行时引发错误。名义类型在运行前检测错误,但有时可能会拒绝实际运行的代码,比如在示例 8-5 中的调用alert_bird(daffy)
。即使有时候能够运行,alert_bird
函数的命名是错误的:它的主体确实需要支持.quack()
方法的对象,而Bird
没有这个方法。
在这个愚蠢的例子中,函数只有一行。但在实际代码中,它们可能会更长;它们可能会将birdie
参数传递给更多函数,并且birdie
参数的来源可能相距多个函数调用,这使得很难准确定位运行时错误的原因。类型检查器可以防止许多这样的错误在运行时发生。
注意
类型提示在适合放在书中的小例子中的价值是有争议的。随着代码库规模的增长,其好处也会增加。这就是为什么拥有数百万行 Python 代码的公司——如 Dropbox、Google 和 Facebook——投资于团队和工具,支持公司范围内采用类型提示,并在 CI 管道中检查其 Python 代码库的重要部分。
在本节中,我们探讨了鸭子类型和名义类型中类型和操作的关系,从简单的double()
函数开始——我们没有为其添加适当的类型提示。当我们到达“静态协议”时,我们将看到如何为double()
添加类型提示。但在那之前,还有更基本的类型需要了解。
可用于注释的类型
几乎任何 Python 类型都可以用作类型提示,但存在限制和建议。此外,typing
模块引入了有时令人惊讶的语义的特殊构造。
本节涵盖了您可以在注释中使用的所有主要类型:
typing.Any
- 简单类型和类
typing.Optional
和typing.Union
- 泛型集合,包括元组和映射
- 抽象基类
- 通用可迭代对象
- 参数化泛型和
TypeVar
typing.Protocols
—静态鸭子类型的关键typing.Callable
typing.NoReturn
—一个结束这个列表的好方法
我们将依次介绍每一个,从一个奇怪的、显然无用但至关重要的类型开始。
任意类型
任何渐进式类型系统的基石是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
可以防止类型检查器实现其核心任务:在程序因运行时异常而崩溃之前检测潜在的非法操作。
流畅的 Python 第二版(GPT 重译)(四)(3)https://developer.aliyun.com/article/1484438