Python-技术手册第三版-全-

Python 技术手册第三版(全)

原文:annas-archive.org/md5/9e375b08cb0be52e8b7c2a9eba6f5313

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Python 编程语言调和了许多表面上的矛盾:优雅而实用,简单而强大,非常高级却在您需要处理位和字节时不会阻碍您,适合初学者也适合专家。

本书面向有一定 Python 经验的程序员,以及从其他语言首次接触 Python 的经验丰富的程序员。它提供了对 Python 本身、其庞大标准库中最常用部分的快速参考,以及一些最受欢迎和有用的第三方模块和包。Python 生态系统在丰富性、范围和复杂性方面已经发展得很多,一个单一的卷不能合理地希望成为百科全书。尽管如此,本书涵盖了广泛的应用领域,包括 Web 和网络编程、XML 处理、数据库交互和高速数值计算。它还探讨了 Python 的跨平台能力以及扩展 Python 和将其嵌入其他应用程序的基础知识。

如何使用本书

虽然您可以从头线性阅读这本书,但我们也希望它能成为工作程序员的有用参考资料。您可以使用索引查找感兴趣的项目,或者阅读特定章节以涵盖特定主题。无论您如何使用它,我们真诚地希望您享受阅读这本书,它代表了团队一年辛勤工作的成果的最佳部分。

本书共有五个部分,如下所示。

第一部分,Python 入门

第一章,“Python 简介”

涵盖了 Python 语言的一般特性、其实现、获取帮助和信息的途径、如何参与 Python 社区以及如何在计算机上获取和安装 Python 或在浏览器中运行 Python。

第二章,“Python 解释器”

讲解了 Python 解释器程序、其命令行选项以及如何使用它来运行 Python 程序和在交互会话中使用。本章提到了用于编辑 Python 程序的文本编辑器和用于检查 Python 源代码的辅助程序,还介绍了一些完整的集成开发环境,包括标准 Python 免费提供的 IDLE。本章还涵盖了从命令行运行 Python 程序的内容。

第二部分,核心 Python 语言和内置功能

第三章,“Python 语言”

涵盖了 Python 语法、内置数据类型、表达式、语句、控制流以及如何编写和调用函数。

第四章,“面向对象的 Python”

涵盖了 Python 中的面向对象编程。

第五章,“类型注解”

讲述了如何向 Python 代码添加类型信息,以获取现代代码编辑器中的类型提示和自动补全帮助,并支持类型检查器和代码分析工具的静态类型检查。

第六章,“异常”

讲述了如何使用异常处理错误和特殊情况,日志记录,以及如何编写在异常发生时自动清理的代码。

第七章,“模块和包”

讲述了 Python 如何将代码分组为模块和包,如何定义和导入模块,以及如何安装第三方 Python 包。本章还涵盖了使用虚拟环境来隔离项目依赖项。

第八章,“核心内置和标准库模块”

讲述了内置数据类型和函数,以及 Python 标准库中一些最基本的模块(大致上来说,提供了在其他一些语言中作为语言本身一部分的功能的模块集合)。

第九章,“字符串与相关”

讲述了 Python 处理字符串的功能,包括 Unicode 字符串、字节串和字符串字面值。

第十章,“正则表达式”

讲述了 Python 对正则表达式的支持。

第三部分,Python 库和扩展模块

第十一章,“文件和文本操作”

讲述了使用 Python 标准库中的许多模块以及丰富文本 I/O 的平台特定扩展处理文件和文本。本章还涵盖了国际化和本地化相关问题。

第十二章,“持久化和数据库”

讲述了 Python 的序列化和持久化机制以及其与 DBM 数据库和关系(基于 SQL)数据库的接口,特别是便捷的 Python 标准库中提供的 SQLite。

第十三章,“时间操作”

讲述了如何处理 Python 中的时间和日期,使用标准库和第三方扩展。

第十四章,“自定义执行”

讲述了如何在 Python 中实现高级执行控制,包括执行动态生成的代码和控制垃圾回收。本章还涵盖了一些 Python 内部类型,以及在程序终止时注册“清理”函数的具体问题。

第十五章,“并发:线程和进程”

讲述了 Python 的并发执行功能,通过多个线程在一个进程内运行以及通过多个进程在单台计算机上运行。¹ 本章还涵盖了如何访问进程环境以及如何通过内存映射机制访问文件。

第十六章,“数值处理”

讲解了 Python 在标准库模块和第三方扩展包中进行数值计算的功能,特别是如何使用十进制数或分数代替默认的二进制浮点数。本章还讲解了如何获取和使用伪随机和真随机数,以及如何快速处理整个数组(和矩阵)中的数字。

第十七章,“测试、调试和优化”

涵盖了帮助您确保程序正确(即它们执行预期功能)的 Python 工具和方法,找出并修复程序中的错误,以及检查和提升程序性能。本章还涵盖了警告的概念以及处理它们的 Python 库模块。

第四部分,“网络和 Web 编程”

第十八章,“网络基础”

讲解了 Python 网络编程的基础知识。

第十九章,“客户端网络协议模块”

涵盖了 Python 标准库中用于编写网络客户端程序的模块,特别是用于处理各种客户端网络协议、发送和接收电子邮件以及处理 URL 的模块。

第二十章,“提供 HTTP 服务”

讲解了如何在 Python 中为 Web 应用程序提供 HTTP 服务,使用流行的第三方轻量级 Python 框架,并利用 Python 的 WSGI 标准接口与 Web 服务器交互。

第二十一章,“电子邮件、MIME 和其他网络编码”

讲解了如何在 Python 中处理电子邮件消息和其他网络结构化和编码文档。

第二十二章,“结构化文本:HTML”

讲解了流行的第三方 Python 扩展模块如何处理、修改和生成 HTML 文档。

第二十三章,“结构化文本:XML”

讲解了 Python 库模块和流行扩展如何处理、修改和生成 XML 文档。

第五部分,“扩展、分发和版本升级与迁移”

第二十四章和第二十五章的内容以摘要形式包含在本书的印刷版中。您可以在支持的在线存储库中找到这些章节的完整内容,详情请参阅“如何联系我们”章节。

第二十四章,“打包程序和扩展”

讲解了打包和分享 Python 模块和应用程序的工具和模块。

第二十五章,“扩展和嵌入经典 Python”

讲解了如何使用 Python 的 C API、Cython 和其他工具编写 Python 扩展模块。

第二十六章,“v3.7 到 v3.n 迁移”

讨论了计划和部署 Python 版本升级的主题和最佳实践,适用于从个人到库维护者再到企业级部署和支持人员的 Python 用户。

附录,“Python 3.7 至 3.11 中的新功能和更改”

提供 Python 语言语法和标准库的功能和更改的详细列表。

本书中使用的约定

本书始终使用以下约定。

参考约定

在函数/方法参考条目中,如果可行,每个可选参数都会使用 Python 语法name=value显示其默认值。内置函数可能不接受命名参数,因此参数名称可能不重要。有些可选参数最好通过其是否存在来解释,而不是通过默认值。在这种情况下,我们用方括号([])表示参数是可选的。当有多个参数是可选时,方括号可以嵌套。

版本约定

本书涵盖 Python 版本 3.7 至 3.11 中的更改和功能。

Python 3.7 是所有表格和代码示例的基础版本,除非另有说明。² 您将看到这些注释以指示在覆盖的版本范围内添加和删除的更改或功能:

  • 3.x+标记的功能是在版本 3.x 中引入的,不适用于之前的版本。

  • -3.x 标记的功能是在版本 3.x 中删除的,仅在之前版本中可用。

排版约定

请注意,出于显示原因,我们的代码片段和示例有时可能与PEP 8不同。我们不建议在您的代码中采用这种自由风格。相反,请使用类似black的工具来采用规范的布局风格。

本书中使用以下排版约定:

斜体

用于文件和目录名称、程序名称、URL 以及引入新术语。

固定宽度

用于命令行输出和代码示例,以及出现在文本中的代码元素,包括方法、函数、类和模块。

常量宽度斜体

用于显示在代码示例和命令中应替换为用户提供的值的文本。

常量宽度粗体

用于在系统命令行中键入的命令,并在 Python 解释器会话示例中指示代码输出。也用于 Python 关键字。

提示

此元素表示提示或建议。

注意

此元素表示一般注释。

警告

此元素表示警告或注意事项。

使用代码示例

本书旨在帮助您完成工作。一般来说,如果本书提供了示例代码,您可以在您的程序和文档中使用它。除非您复制了代码的大部分,否则无需联系我们请求许可。例如,编写一个使用本书多个代码片段的程序并不需要许可。出售或分发 O’Reilly 书籍中的示例代码则需要许可。引用本书并引用示例代码来回答问题则不需要许可。将本书大量示例代码合并到您产品的文档中则需要许可。

我们感谢您的赞赏,但不需要署名。署名通常包括标题、作者、出版商和 ISBN 号。例如:“Python in a Nutshell,第四版,作者 Alex Martelli、Anna Martelli Ravenscroft、Steve Holden 和 Paul McGuire。版权所有 2023,978-1-098-11355-1。”

如果您觉得您使用的代码示例超出了合理使用范围或上述许可,请随时与我们联系:permissions@oreilly.com。

O’Reilly Online Learning

注意

超过 40 年来,O’Reilly Media 一直提供技术和商业培训、知识和见解,帮助公司取得成功。

我们独特的专家和创新者网络通过书籍、文章和我们的在线学习平台分享他们的知识和专业知识。O’Reilly 的在线学习平台为您提供按需访问的实时培训课程、深度学习路径、交互式编码环境以及来自 O’Reilly 和其他 200 多家出版商的大量文本和视频。更多信息,请访问 https://oreilly.com

如何联系我们

请将有关本书的评论和问题发送至出版商:

  • O’Reilly Media, Inc.

  • 1005 Gravenstein Highway North

  • Sebastopol, CA 95472

  • 800-998-9938(美国或加拿大)

  • 707-829-0515(国际或本地)

  • 707-829-0104(传真)

该书有其自己的 GitHub 仓库,我们在那里列出勘误、示例和任何额外信息。该仓库还包含第 24 和第二十五章的完整内容,因为印刷版空间不足。您可以在 https://github.com/pynutshell/pynut4 找到它。

我们尽力测试和验证了本书中的信息,但您可能会发现功能已更改(甚至我们犯了错误!)。请向出版商报告您发现的任何错误,以及对未来版本的建议。

O’Reilly 为本书设有网页,列出勘误、示例和任何额外信息。您可以访问此页面:https://oreil.ly/python-nutshell-4e

欲对本书发表评论或提出技术问题,请发送电子邮件至 pynut4@gmail.com。

欲了解关于我们书籍和课程的新闻和信息,请访问https://oreilly.com

在 LinkedIn 上找到我们:https://linkedin.com/company/oreilly-media

在 Twitter 上关注我们:https://twitter.com/oreillymedia

在 YouTube 上观看我们:https://www.youtube.com/oreillymedia

致谢

特别感谢 O’Reilly 的编辑和工作人员 Amanda Quinn、Brian Guerin、Zan McQuade 和 Kristen Brown。特别感谢我们的编辑 Angela Rufino,在确保我们按时完成这本书的过程中做了大量的工作!还要感谢我们优秀的文字编辑 Rachel Head,帮助我们看起来比我们实际更博学;以及我们的制作编辑 Christopher Faucher,帮助确保书籍在印刷和电子格式中都看起来最好。

特别感谢我们辛勤工作的技术审阅者 David Mertz、Mark Summerfield 和 Pankaj Gaijar,他们仔细阅读了书稿中的每一条解释和例子。没有他们,这本书就不会如此准确。³ 所有尚存的错误完全属于我们自己。

特别感谢 Luciano Ramalho、整个 PyPy 团队、Sebastián Ramírez、Fabio Pliger、Miguel Grinberg 和 Python 打包管理团队在本书的部分章节中提供的帮助,以及 Google 提供的实用的 Workspace 在线协作工具。没有这些工具,我们跨越不同洲际的密集沟通和协作将远没有现在这么方便和高效。

最后但同样重要的是,本书的作者和所有读者深深感谢 Python 语言核心开发者的巨大贡献,没有他们的英勇努力,就不会有这本书的需求。

¹ 本版不再包含第三版中有关异步编程的单独章节,而是推荐在第十五章中的参考资料中找到更详尽的覆盖。

² 例如,为了适应 Python 3.9 和 3.10 中广泛的类型注解变化,第五章的大部分内容使用 Python 3.10 作为特性和示例的基础版本。

³ 也不会有这么多脚注!

第一章:介绍 Python

Python 是一种经过良好建立的通用编程语言,由其创始人 Guido van Rossum 于 1991 年首次发布。这种稳定成熟的高级动态面向对象的跨平台语言具有非常吸引人的特性。Python 可以运行在 macOS、包括 Linux、Windows 在内的大多数当前 Unix 变体,以及经过一些调整后的移动平台。¹

Python 在软件生命周期的所有阶段(分析、设计、原型、编码、测试、调试、调优、文档编写和当然还有维护)都能提供高生产力。多年来,这种语言的流行度稳步增长,2021 年 10 月成为 TIOBE 指数 领导者。如今,对 Python 的熟悉对每位程序员来说都是一个优势:它已经渗透到大多数领域,在任何软件解决方案中都能发挥有用的作用。

Python 提供了优雅、简洁、实用和强大的独特结合。由于其一贯性和规律性、丰富的标准库以及众多第三方包和工具的可用性,你将会很快在 Python 上变得高效率。Python 容易学习,因此如果你是新手,它非常适合,但它也足够强大,可以满足最复杂专家的需求。

Python 语言

Python 语言虽然不是极简主义的,但出于良好的实用原因而简洁。一种语言一旦提供了表达设计的良好方式,增加其他方式的好处最多是有限的;语言复杂度的成本却不仅线性增长,尤其是特性数量增加时更为明显。复杂的语言比简单的语言更难学习和掌握(也更难高效实现和无 bug 地实施),在软件开发中尤其如此,特别是在许多开发者合作,并经常维护原本由他人编写的代码的大型项目中。

Python 相当简单,但不是简单化。它遵循一种理念:如果一种语言在某些情境下表现一定的方式,那么在所有情境下都应该理想地以相似的方式工作。Python 遵循的原则是语言不应该有“方便”的快捷方式、特例、临时例外、过于微妙的区别,或神秘和棘手的底层优化。一种好的语言,像任何其他设计良好的工件一样,必须在通用原则、品味、常识和大量实用性之间取得平衡。

Python 是一种通用编程语言:它的特性几乎在软件开发的任何领域都非常有用。没有任何领域是 Python 不能成为解决方案的一部分的。这里“一部分”很重要;尽管许多开发者发现 Python 能够满足他们的所有需求,但它并不必须单打独斗。Python 程序可以与多种其他软件组件合作,使其成为在其他语言中粘合组件的正确语言。语言的一个设计目标是长久以来一直是“与他人和谐相处”。

Python 是一种非常高级的语言(VHLL)。这意味着它使用更高级的抽象级别,概念上比传统的编译语言如 C、C++ 和 Rust 更远离底层机器。“高级”在这里指的是这种概念。Python 比经典的高级语言更简单,更快速(无论是对人还是对工具),并且更规则。这提高了程序员的生产力,使 Python 成为强大的开发工具。对于经典编译语言,优秀的编译器可以生成比 Python 更快运行的二进制代码。然而,在大多数情况下,Python 编写的应用程序的性能是足够的。当不够时,可以应用 “优化” 中涵盖的优化技术来提高程序的性能,同时保持高生产力的好处。

就语言水平而言,Python 可与 JavaScript、Ruby 和 Perl 等其他强大的 VHLLs 相媲美。然而,简单性和规则性的优势仍然在 Python 的一边。

Python 是一种面向对象的编程语言,但它允许你以面向对象和过程化的风格编程,并且还可以根据应用程序的要求混合和匹配功能式编程。Python 的面向对象特性在概念上与 C++ 相似,但使用起来更简单。

Python 标准库和扩展模块

Python 编程不仅仅是语言本身:标准库和其他扩展模块对于 Python 的使用几乎与语言本身一样重要。Python 标准库提供了许多设计良好、可靠的 Python 模块,方便重复使用。它包括用于表示数据、处理文本、与操作系统和文件系统交互、以及 web 编程等任务的模块,并且可以在 Python 支持的所有平台上运行。

扩展模块来自标准库或其他地方,让 Python 代码可以访问由底层操作系统或其他软件组件提供的功能,如图形用户界面(GUI)、数据库和网络。扩展模块还在计算密集型任务(如 XML 解析和数值数组计算)中提供了很高的速度。然而,不是用 Python 编写的扩展模块并不一定具有与纯 Python 代码相同的跨平台可移植性。

您可以使用低级语言编写扩展模块,以优化在 Python 中原型设计的小型计算密集部分的性能。您还可以使用诸如 Cython、ctypes 和 CFFI 之类的工具,将现有的 C/C++库包装为 Python 扩展模块,详细内容请参阅第二十五章中的“无需使用 Python 的 C API 扩展 Python”(在线获取:oreil.ly/python-nutshell-25)。您还可以将 Python 嵌入到使用其他语言编写的应用程序中,通过特定于应用程序的 Python 扩展模块向 Python 暴露应用程序功能。

本书记录了许多模块,包括来自标准库和其他来源的模块,用于客户端和服务器端网络编程、数据库、处理文本和二进制文件以及与操作系统交互。

Python 的实现

在撰写本文时,Python 有两个完整的生产质量实现(CPython 和 PyPy),以及几个较早开发阶段的高性能实现,如NuitkaRustPythonGraalVM PythonPyston,我们不会进一步介绍。在“其他发展、实现和分发”中,我们还提到了一些其他更早期的实现。

这本书主要介绍 CPython,这是最广泛使用的实现,我们通常简称为“Python”。然而,语言与其实现之间的区别非常重要!

CPython

经典 Python——也称为 CPython,通常简称为 Python——是最新、稳定和完整的 Python 生产质量实现。它是该语言的“参考实现”。CPython 是一个字节码编译器、解释器以及一组标准 C 编码的内置和可选模块。

CPython 可以在符合 ISO/IEC 9899:1990 标准(即所有现代、流行平台)的任何平台上使用 C 编译器。在“安装”中,我们解释了如何下载和安装 CPython。本书中的所有内容(除了明确标记为其他的几个部分)都适用于 CPython。截至撰写本文时,CPython 的当前版本刚刚发布,是 3.11 版本。

PyPy

PyPy是 Python 的快速灵活实现,使用 Python 本身的子集编码,能够针对多种低级语言和虚拟机进行优化,采用类型推断等先进技术。PyPy 的最大优势在于能够在运行 Python 程序时即时生成本机机器代码,具有显著的执行速度优势。目前,PyPy 实现了 3.8 版本(3.9 版本处于测试阶段)。

在 CPython、PyPy 和其他实现之间做出选择

如果你的平台像大多数情况一样能够运行 CPython、PyPy 以及我们提到的其他几种 Python 实现,你该如何在它们之间进行选择?首先,不要过早地做出选择:下载并安装它们全部。它们可以无冲突共存,而且它们都是免费的(其中一些还提供附加值如技术支持的商业版本,但对应的免费版本也是可以的)。在你的开发机器上同时拥有它们只需一些下载时间和少量磁盘空间,并且让你可以直接比较它们。话虽如此,以下是一些一般性建议。

如果你需要定制版的 Python,或者需要长时间运行程序的高性能,可以考虑 PyPy(或者,如果你对尚未完全成熟的版本可以接受的话,可以考虑我们提到的其他一些版本)。

如果主要在传统环境下工作,CPython 是一个很好的选择。如果没有更强烈的替代偏好,可以从标准的 CPython 参考实现开始,它得到了第三方附加组件和扩展的广泛支持,并提供最新的版本。

换句话说,要进行实验、学习和尝试,使用 CPython。要开发和部署,你最好的选择取决于你想要使用的扩展模块以及你想要分发程序的方式。CPython 从定义上支持所有 Python 扩展;然而,PyPy 支持大多数扩展,并且由于即时编译到机器代码,通常对长时间运行的程序来说速度更快——要确认这一点,请将你的 CPython 代码与 PyPy 进行基准测试(还有其他实现也要测试)。

CPython 是最成熟的:它存在的时间更长,而 PyPy(和其他的实现)则较为新颖,领域实践较少。CPython 版本的开发超前于其他实现。

我们提到的 PyPy、CPython 和其他实现都是优秀的、忠实的 Python 实现,在可用性和性能上都相当接近。熟悉每个实现的优势和劣势,然后为每个开发任务做出最优选择是明智的。

其他发展、实现和分发

Python 已经变得如此流行,以至于几个团体和个人对其开发表示了兴趣,并提供了核心开发团队关注外的功能和实现。

如今,大多数基于 Unix 的系统都包括 Python——通常是版本 3.x,其中x是某个值——作为“系统 Python”。要在 Windows 或 macOS 上获取 Python,通常需要下载并运行一个安装程序(参见“macOS”)。如果你对 Python 进行严肃的软件开发,第一件事就是不要动你系统安装的 Python!与其他问题无关,Python 正被操作系统的某些部分越来越多地使用,因此调整 Python 安装可能会导致麻烦。

因此,即使您的系统配备了“系统 Python”,也请考虑安装一个或多个 Python 实现,以便您可以自由地用于您的开发方便,安全地知道您所做的一切都不会影响操作系统。我们还强烈推荐使用虚拟环境(参见“Python 环境”)来隔离项目,让它们之间拥有可能会发生冲突的依赖关系(例如,如果您的两个项目需要同一个第三方模块的不同版本)。或者,也可以在本地并排安装多个 Python。

Python 的流行导致了许多活跃的社区的创建,语言的生态系统非常活跃。以下部分概述了一些更有趣的发展:请注意,我们未在此处包含某个项目,这反映了空间和时间的限制,而不是意味着不赞成!

Jython 和 IronPython

Jython,支持在JVM上运行 Python,并且 IronPython,支持在.NET上运行 Python,是两个开源项目,尽管它们支持的 Python 版本具有生产级别的质量,但在撰写本文时似乎“停滞不前”,因为它们支持的最新版本明显落后于 CPython。任何“停滞不前”的开源项目都有可能再次复活:只需一个或多个热情、承诺的开发人员致力于“复苏”它。作为 JVM 上的 Jython 的替代方案,您还可以考虑之前提到的 GraalVM Python。

Numba

Numba 是一个开源的即时(JIT)编译器,它将 Python 和 NumPy 的子集进行翻译。由于其对数字处理的强大关注,我们在第十六章中再次提到它。

Pyjion

Pyjion 是一个开源项目,最初由微软发起,其主要目标是向 CPython 添加一个 API 来管理 JIT 编译器。其次目标包括提供一个 JIT 编译器,用于微软的开源 CLR 环境(这是 .NET 的一部分)和一个用于开发 JIT 编译器的框架。Pyjion 不是取代 CPython;相反,它是一个你从 CPython 中导入的模块(目前需要 3.10),它允许你将 CPython 的字节码“即时”地翻译成多个不同环境的机器码。Pyjion 与 CPython 的集成由 PEP 523 启用;但是,由于构建 Pyjion 需要除了 C 编译器之外的几个工具(这是构建 CPython 所需的全部工具),Python 软件基金会(PSF)可能永远不会将 Pyjion 捆绑到它分发的 CPython 版本中。

IPython

IPython 增强了 CPython 的交互式解释器,使其更加强大和便捷。它允许使用缩写的函数调用语法,并通过百分号(%)字符引入称为magics的可扩展功能。它还提供了 shell 转义功能,允许 Python 变量接收 shell 命令的结果。您可以使用问号查询对象的文档(或两个问号以获取扩展文档);Python 交互式解释器的所有标准功能也可用。

IPython在科学和数据重点领域取得了特殊进展,并通过 IPython Notebook(现在重构并更名为 Jupyter Notebook,见“Jupyter”)的发展逐渐演变为一个交互式编程环境,除了代码片段外,还可以以literate programming风格嵌入评论(包括数学符号),并展示执行代码的输出,可选地使用诸如 matplotlib 和 bokeh 等子系统生成的高级图形。Jupyter Notebook 中嵌入 matplotlib 图形的示例如图 1-1 的下半部分所示。Jupyter/IPython 是 Python 的显著成功案例之一。

一个示例 Jupyter Notebook,嵌入了 matplotlib 图形

图 1-1. 一个示例 Jupyter Notebook,嵌入了 matplotlib 图形

MicroPython

小型化趋势持续推动着 Python 向业余爱好者的范围内发展。像Raspberry PiBeagle boards这样的单板计算机让您在完整的 Linux 环境中运行 Python。在此级别以下,有一类被称为微控制器的设备,它们是可编程的具有可配置硬件的芯片,通过简化模拟和数字传感器的使用,例如轻松实现光和温度测量等应用,使得业余和专业项目的范围扩大。

业余爱好者和专业工程师都在越来越多地使用这些设备,这些设备时常出现(有时又会消失)。多亏了MicroPython项目,许多这类设备micro:bitArduinopyboardLEGOⓇ MINDSTORMSⓇ EV3HiFive,等等)现在可以用(有限的方言)Python 进行编程。在撰写时值得注意的是Raspberry Pi Pico的推出。鉴于树莓派在教育领域的成功,以及 Pico 能够运行 MicroPython,似乎 Python 正在巩固其作为拥有最广泛应用范围的编程语言的地位。

MicroPython 是 Python 3.4 的实现(“包括后续版本的某些特性”,引用自其文档),它能产生字节码或可执行的机器码(许多用户可能并不了解后一种情况)。它完全实现了 Python 3.4 的语法,但缺少大部分标准库。特殊的硬件驱动模块可以让你控制内置硬件的各个部分;访问 Python 的 socket 库可以让设备与网络服务进行交互。外部设备和定时器事件可以触发代码执行。由于 MicroPython,Python 语言可以完全参与物联网。

一个设备通常可以通过 USB 串行端口或通过浏览器使用 WebREPL 协议访问解释器(尽管目前我们还不知道任何完全可用的 ssh 实现,因此,请确保适当地配置防火墙:不应该直接通过互联网访问这些设备,除非有适当的强化预防措施!)。你可以通过在设备内存中创建一个boot.py文件来用 Python 编程设备的上电启动序列,而且这个文件可以执行任意复杂的 MicroPython 代码。

Anaconda 和 Miniconda

近年来最成功的 Python 发行版之一⁴是Anaconda。这个开源软件包除了标准库外,还带有大量预配置和经过测试的扩展模块。在许多情况下,你可能会发现它包含了你工作所需的所有依赖项。如果你的依赖项不受支持,你也可以用 pip 安装模块。在基于 Unix 的系统上,它可以简单地安装在一个单独的目录中:只需将 Anaconda 的bin子目录添加到你的 shell PATH 的最前面即可激活它。

Anaconda 基于一种名为 conda 的包装技术。一个姐妹实现 Miniconda 提供了相同扩展的访问权限,但不会预加载它们;相反,它会在需要时下载它们,因此更适合创建定制环境。conda 不使用标准虚拟环境,但包含相应的功能以允许为多个项目分离依赖关系。

pyenv:支持多版本的简单实现

pyenv 的基本目的是让您轻松访问所需的多个不同版本的 Python。它通过为每个可执行文件安装所谓的 shim 脚本来实现这一点,这些脚本动态计算所需的版本,顺序如下查看各种信息源:

  1. 如果设置了 PYENV_VERSION 环境变量。

  2. 当前目录中的 .pyenv_version 文件(如果存在)— 您可以使用 pyenv local 命令设置此文件。

  3. 当在目录树中向上查找第一个 .pyenv_version 文件(如果找到的话)。

  4. 在 pyenv 安装根目录中的 version 文件— 您可以使用 pyenv global 命令设置此文件。

pyenv 将其 Python 解释器安装在其主目录下(通常是 ~/.pyenv),一旦可用,特定解释器可以安装为任何项目目录中的默认 Python。或者(例如,在多个版本下测试代码时),您可以使用脚本在脚本执行过程中动态更改解释器。

pyenv install –list 命令显示了一个令人印象深刻的列表,包括 PyPy、Miniconda、MicroPython 和其他几个官方 CPython 实现(截至撰写本文时为止,从 2.1.3 到 3.11.0rc1)。

Transcrypt:将您的 Python 转换为 JavaScript

有许多尝试将 Python 打造成为浏览器语言,但 JavaScript 的影响力十分顽强。Transcrypt 系统是一个可通过 pip 安装的 Python 包,用于将 Python 代码(目前支持版本为 3.9)转换为浏览器可执行的 JavaScript。您可以完全访问浏览器的 DOM,允许您的代码动态操作窗口内容并使用 JavaScript 库。

虽然它创建了缩小的代码,但 Transcrypt 提供了完整的 sourcemaps,允许您根据 Python 源代码而不是生成的 JavaScript 进行调试。您可以用 Python 编写浏览器事件处理程序,自由混合 HTML 和 JavaScript。Python 可能永远不会取代 JavaScript 作为嵌入式浏览器语言,但 Transcrypt 意味着您可能不再需要担心这个问题。

另一个非常活跃的项目让你可以用 Python 脚本编写网页(最多支持到 3.10)是 Brython,还有其他一些项目:Skulpt,还没有完全支持 Python 3,但正在朝这个方向发展;PyPy.js,同样;Pyodide,目前支持 Python 3.10 和许多科学扩展,中心是 Wasm;以及最近的 Anaconda 的 PyScript,建立在 Pyodide 上。我们在 “在浏览器中运行 Python”中更详细地描述了这些项目中的几个。

许可和价格问题

CPython 受到 Python 软件基金会许可证第 2 版的保护,它与 GNU 通用公共许可证(GPL)兼容,但允许你将 Python 用于任何专有、免费或其他开源软件开发,类似于 BSD/Apache/MIT 许可证。PyPy 和其他实现的许可证也类似宽松。你从主 Python 和 PyPy 网站下载的任何内容都不会花费你一分钱。此外,这些许可证不会限制你在使用这些工具、库和文档开发的软件时可以使用的许可和定价条件。

然而,并非所有与 Python 相关的内容都免于许可费用或麻烦。许多第三方 Python 来源、工具和扩展模块,你可以自由下载,拥有类似于 Python 本身的宽松许可证。其他的则受到 GPL 或较小 GPL(LGPL)的保护,限制了你对衍生作品的许可条件。一些商业开发的模块和工具可能要求你支付费用,无论是无条件的还是如果你用它们盈利的情况下都需要支付费用。⁶

对于许可条件和价格的细致检查没有替代品。在你投入时间和精力使用任何软件工具或组件之前,请确保你能接受它的许可证。通常,特别是在企业环境中,这些法律问题可能需要咨询律师。除非我们明确说明,否则本文中涉及的模块和工具在撰写本文时可以认为是自由下载的、开源的,并且采用类似于 Python 的自由许可证。然而,我们声明不具备法律专业知识,许可证可能会随时间变化,因此始终需要仔细核查。

Python 开发与版本

Python 由 Guido van Rossum(Python 的发明者、架构师,现在的“前”终身仁慈独裁者(BDFL))领导的核心开发团队开发、维护和发布。这个头衔意味着 Guido 对 Python 语言和标准库的组成有最终决定权。一旦 Guido 决定辞去 BDFL 职位,他的决策角色由由 PSF 成员选举产生的一个小型“指导委员会”接管,任期一年。

Python 的知识产权归 PSF 所有,这是一个致力于推广 Python 的非营利性组织,描述在“Python 软件基金会”中。许多 PSF 院士和成员在 Python 的参考源代码库中有提交权限,如“Python 开发者指南”所述,大多数 Python 提交者都是 PSF 的成员或院士。

提议对 Python 的更改详细说明在名为Python Enhancement Proposals (PEPs)的公共文档中。PEPs 由 Python 开发人员和更广泛的 Python 社区进行辩论,最终由指导委员会批准或拒绝。(指导委员会可能考虑辩论和初步投票,但并不受其约束。)数百人通过 PEPs、讨论、错误报告和对 Python 源代码、库和文档的补丁为 Python 开发做出贡献。

Python 核心团队以每年一次的速度发布 Python 的小版本(3.x,其中x为不断增长的值),也称为“特征发布”。

每个小版本发布(与 bug 修复的微版本相对)都添加了使 Python 更强大的功能,但也会注意保持向后兼容性。Python 3.0 首先于 2008 年 12 月发布,允许破坏向后兼容性以删除多余的“遗留”功能并简化语言。Python 3.11(出版时的最新稳定版本)于 2022 年 10 月首次发布。

每个小版本 3.x首先以 alpha 版本发布,标记为 3.xa0、3.xa1 等等。在 alpha 版本之后,至少有一个 beta 版本,3.xb1,以及 beta 版本之后,至少有一个发布候选版本,3.xrc1。当 3.x(3.x.0)的最终版本发布时,它是稳定的、可靠的,并在所有主要平台上经过测试。任何 Python 程序员都可以通过下载 alpha 版、beta 版和发布候选版、尝试它们,并为出现的任何问题提交错误报告来确保这一点。

一旦一个小版本发布,核心团队的注意力部分转移到下一个小版本上。然而,一个小版本通常会有连续的点发布(即 3.x.1、3.x.2 等),每两个月发布一次,不添加功能,但可以修复错误、解决安全问题、将 Python 移植到新平台、增强文档,并添加工具和(100%向后兼容的!)优化。

在主要版本内,Python 的向后兼容性相当好。你可以在线找到所有旧版本的 Python 的代码和文档,附录包含了本书涵盖的每个版本的变更摘要列表。

Python 资源

最丰富的 Python 资源在网络上:从 Python 的主页开始,这里有许多探索链接。

文档

CPython 和 PyPy 都配有良好的文档。你可以在 CPython 的在线手册 阅读(我们经常称之为“在线文档”),并且也有适合离线查看、搜索和打印的各种可下载格式。Python 的 文档页面 包含了指向大量其他文档的附加指针。PyPy 也有一个 文档页面,你可以在这里找到 Python 和 PyPy 的在线常见问题解答。

Python 非程序员文档

大多数 Python 文档(包括本书)都假设具有一定的软件开发知识。然而,Python 对于初学者来说非常适用,因此也有例外情况。非程序员的良好入门免费在线文本包括:

一个学习 Python 的绝佳资源(适用于非程序员和经验较少的程序员)是 “Python 初学者指南” wiki,其中包含大量链接和建议。这是由社区共同维护的,因此随着可用的书籍、课程、工具等的不断进步和改进,它会保持最新。

扩展模块和 Python 源码

探索 Python 扩展二进制和源码的良好起点是 Python Package Index(我们这些老前辈中的一些人仍然喜欢称之为“奶酪商店”,但现在通常称为 PyPI),在本文撰写时提供了超过 400,000 个包,每个包都有描述和指针。

标准的 Python 源码发布包含了在标准库和 Tools 目录中的优秀 Python 源代码,以及许多内置扩展模块的 C 源码。即使你对从源代码构建 Python 没有兴趣,我们建议你下载和解压 Python 源码发布包(例如,最新稳定版本的 Python 3.11),只是为了学习它;或者,如果你选择的话,浏览当前最新的 Python 标准库的 bleeding-edge 版本 在线文档

这本书涵盖的许多 Python 模块和工具也有专门的网站。我们在相应的章节中提供了这些网站的引用。

书籍

尽管网络是丰富的信息来源,但书籍仍然有其重要性(如果你对此意见不同,我们就不会写这本书,你也不会在读它)。关于 Python 的书籍很多。以下是我们推荐的一些(一些覆盖较旧的 Python 3 版本,而不是当前版本):

  • 如果您懂一些编程但刚开始学习 Python,并且喜欢图形化的教学方法,Head First Python,第二版,作者 Paul Barry(O’Reilly),可能非常适合您。像 Head First 系列中的所有书籍一样,它使用图形和幽默来教授其主题。

  • 深入 Python 3,作者 Mark Pilgrim(Apress),通过快节奏和彻底的示例方式进行教学,非常适合已经是其他语言专家程序员的人。

  • 从初学到专业:Python 起步,作者 Magnus Lie Hetland(Apress),通过详尽的解释和在各种应用领域中完整开发程序来进行教学。

  • 流畅的 Python,作者 Luciano Ramalho(O’Reilly),是一本适合有经验的开发人员,希望使用更多 Python 风格惯用语和功能的优秀书籍。

社区

Python 最大的优势之一是其强大、友好、包容的社区。Python 程序员和贡献者在会议、“黑客马拉松”(在 Python 社区中通常称为sprints)、本地用户组相聚;积极讨论共同的兴趣;并在邮件列表和社交媒体上互相帮助。要获取连接方式的完整列表,请访问https://www.python.org/community

Python 软件基金会

除了拥有 Python 编程语言的知识产权外,PSF 还促进 Python 社区。它赞助用户组、会议和“sprints”,并为开发、外展和教育等活动提供资助。PSF 有数十位Fellows(因其对 Python 的贡献而提名,包括 Python 核心团队的所有成员,以及本书的三位作者);数百名贡献了时间、工作和金钱的成员(包括许多获得Community Service Awards的人);以及数十家企业赞助商。任何使用和支持 Python 的人都可以成为 PSF 的成员。⁷查看会员页面获取有关各种会员级别的信息,以及如何成为 PSF 的成员。如果您有兴趣为 Python 本身做贡献,请参阅“Python 开发者指南”

工作组

工作组是由 PSF 成立的委员会,负责为 Python 进行特定而重要的项目。以下是写作时的一些活跃工作组的示例:

Python 会议

世界各地有许多 Python 会议。一般的 Python 会议包括国际和地区性的会议,如PyConEuroPython,以及其他更本地化的会议,如PyOhioPyCon Italia。专题会议包括SciPyPyData。会议通常会跟随编程冲刺,Python 贡献者们在几天内聚集在一起,专注于特定开源项目的编码,并享受友谊的互动。您可以在社区的会议和研讨会页面上找到会议列表。超过 450 个会议的 17,000 多个关于 Python 的讲座视频可以在PyVideo 网站上找到。

用户组织和组织

Python 社区在除南极洲外的每个大陆上都有本地用户组⁸,根据LocalUserGroups wiki上的列表,超过 1,600 个用户组。世界各地都有 Python 聚会PyLadies是一个国际性的导师组织,拥有本地分部,旨在促进 Python 中的女性;任何对 Python 感兴趣的人都欢迎加入。NumFOCUS是一个非营利性慈善机构,致力于促进研究、数据和科学计算领域的开放实践,赞助了 PyData 会议和其他项目。

邮件列表

社区邮件列表页面包含多个与 Python 相关的邮件列表(还有一些我们足够老的人还记得的 Usenet 组)。或者,您可以在Mailman中搜索,找到涵盖各种兴趣领域的活跃邮件列表。Python 相关的官方公告发布在python-announce 列表上。要获取特定问题的帮助,请写信至help@python.org。如需 Python 的学习或教学帮助,请写信至tutor@python.org,或者更好地加入列表。要获取有关 Python 新闻和文章的有用每周摘要,请订阅Python Weekly。您也可以在*@python_discussions@mastodon.social*上关注 Python Weekly。

社交媒体

若要获取与 Python 相关的博客的 RSS 订阅,请参阅 Planet Python。如果您关注语言发展,可以查看 discuss.python.org,它会发送有用的摘要,如果您不经常访问的话。在 Twitter 上,请关注 @ThePSF。IRC 上的 Libera.Chat 主持多个与 Python 相关的频道:主要频道是 #python。在 LinkedIn 上有许多 Python 群组,包括 Python Web Developers。在 Slack 上,加入 PySlackers 社区。在 Discord 上,请查看 Python Discord。关于 Python 编程的技术问题和答案也可以在 Stack Overflow 上找到和跟踪,包括各种标签,如 [python]。Python 目前是 Stack Overflow 上活跃度最高的编程语言,并且那里可以找到许多有用的答案和启发性讨论。如果您喜欢播客,可以听听 Python 相关的播客,例如 Python Bytes

安装

在大多数平台上,您可以安装 Python 的经典(CPython)和 PyPy 版本。具备适当的开发系统(对于 CPython 是 C;而 PyPy 则由 Python 自身编写,只需先安装 CPython),您可以从各自的源代码分发中安装 Python 版本。在流行的平台上,您也可以选择安装预构建的二进制分发版本。

如果 Python 已预装,如何安装

如果您的平台预装了 Python 的版本,建议您仍然安装一个独立的最新版本进行自己的代码开发。在这种情况下,请不要删除或覆盖平台原始版本:而是将新版本安装在原版本旁边。这样做可以避免干扰平台上的其他软件,因为这些软件可能依赖于平台自带的特定 Python 版本。

从二进制分发安装 CPython 更快,可以在某些平台上节省大量工作,并且如果没有适当的 C 编译器,这是唯一的可能性。从源代码安装可以提供更多的控制和灵活性,如果找不到适合您平台的预构建二进制分发版本,这是必须的。即使您从二进制分发安装,最好也下载源代码分发,因为它可能包含通常在预构建二进制分发中缺失的示例、演示和工具。接下来我们将看看如何进行这两种安装方式。

从二进制安装 Python

如果你的平台受欢迎且当前,你会很容易找到预先构建的、打包的 Python 二进制版本,可以立即安装。二进制包通常是自安装的,可以直接作为可执行程序安装,也可以通过适当的系统工具安装,如某些 Linux 版本上的 Red Hat Package Manager (RPM)和 Windows 上的 Microsoft Installer (MSI)。下载包后,通过运行程序并选择安装参数来安装它,例如 Python 的安装目录。在 Windows 中,选择标有“将 Python 3.10 添加到 PATH”选项,让安装程序将安装位置添加到 PATH 中,以便在命令提示符下轻松使用 Python(参见“python 程序”)。

你可以从Python 网站的下载页面获取“官方”二进制文件:点击标有“下载 Python 3.11.x”的按钮,下载适合你浏览器平台的最新二进制文件。

许多第三方为其他平台提供免费的二进制 Python 安装程序。针对 Linux 发行版,无论你的发行版是RPM-based(如红帽、Fedora、曼德里维亚、SUSE 等)还是Debian-based(包括 Ubuntu,在撰写本文时可能是最流行的 Linux 发行版)。Other Platforms 页面提供了链接到二进制发行版的链接,适用于现在有些略显陈旧的平台,如 AIX、OS/2、RISC OS、IBM AS/400、Solaris、HP-UX 等(通常不提供最新的 Python 版本,因为这些平台现在有些“过时”),以及非常流行的iOS 平台,即流行的iPhoneiPad设备的操作系统。

Anaconda,在本章前面提到过,是一个二进制发行版,包括 Python、conda软件包管理器,以及数百个第三方扩展,特别适用于科学、数学、工程和数据分析。它适用于 Linux、Windows 和 macOS。Miniconda,在本章前面也提到过,是相同的软件包,但没有所有这些扩展;你可以使用 conda 选择性地安装它们的子集。

macOS

流行的第三方 macOS 开源软件包管理器Homebrew提供了众多其他开源软件包之外,优秀的Python版本。在 macOS 中,如“Anaconda 和 Miniconda”中提到的,conda 也可以很好地工作。

从源代码安装 Python

要从源代码安装 CPython,你需要一个具有 ISO 兼容的 C 编译器和像 make 这样的工具的平台。在 Windows 上,构建 Python 的常规方式是使用 Visual Studio(最好是VS 2022,目前开发者可以免费获取)。

要下载 Python 源代码,请访问Python Source Releases页面(在 Python 网站上,悬停在菜单栏中的下载上,并选择“源代码”),然后选择您的版本。

标签为“Gzipped source tarball”的链接下的文件具有*.tgz文件扩展名;这相当于.tar.gz*(即通过流行的 gzip 压缩器压缩的文件tar归档)。或者,您可以使用标签为“XZ compressed source tarball”的链接,获取具有*.tar.xz*扩展名的版本,由更强大的 xz 压缩器压缩,如果您有处理 XZ 压缩所需的所有工具。

Microsoft Windows

在 Windows 上,从源代码安装 Python 可能会很麻烦,除非您熟悉 Visual Studio 并习惯于在称为命令提示符的文本导向窗口中工作——大多数 Windows 用户更倾向于直接从 Microsoft Store 下载预构建的Python

如果以下说明给您带来任何麻烦,请坚持按照前一节描述的方式从二进制文件安装 Python。即使您还从源代码安装,最好也单独进行二进制文件安装。如果您在使用您从源代码安装的版本时注意到任何奇怪的事情,请与从二进制文件安装双重检查。如果奇怪的事情消失了,那肯定是由于您在构建后者时选择的一些怪癖,因此您知道您必须双重检查您选择构建的详细信息。

在接下来的几节中,为了清晰起见,我们假设您已经创建了一个名为*%USERPROFILE%\py*(例如,c:\users\tim\py)的新文件夹,您可以通过在任何命令窗口中键入mkdir命令来实现这一点。将源*.tgz文件(例如Python-3.11.0.tgz*)下载到该文件夹中。当然,您可以根据需要命名和放置该文件夹:我们的命名选择仅供解释用途。

解压和展开 Python 源代码

您可以使用例如免费程序7-Zip来解压缩和展开*.tgz.tar.xz文件。从下载页面下载适当版本,安装并在您从 Python 网站下载的.tgz文件上运行它(例如,c:\users\alex\py\Python-3.11.0.tgz)。假设您将此文件下载到%USERPROFILE%\py文件夹中(或者如果需要,从%USERPROFILE%\downloads移动到那里),您现在将拥有一个名为%USERPROFILE%\py\Python-3.11.0*或类似的文件夹,具体取决于您下载的版本。这是一个包含整个标准 Python 分发的源代码树的根。

构建 Python 源代码

用任何文本编辑器打开位于此根文件夹的PCBuild子目录中的readme.txt文件,并按照其中找到的详细说明操作。

类 Unix 平台

在类 Unix 平台上,从源代码安装 Python 通常很简单。¹⁰ 在接下来的章节中,为了清晰起见,假设您已经创建了一个名为 ~/py 的新目录,并下载了源码 .tgz 文件,例如 Python-3.11.0.tgz ——当然,您可以根据需要命名和放置目录:我们的命名选择仅供说明目的。

解压和解包 Python 源代码

可以使用流行的 GNU 版本的 tar 解压和解包 .tgz.tar.xz 文件。只需在 shell 提示符下输入以下命令:

$ cd ~/py && tar xzf Python-3.11.0.tgz

现在您有一个名为 ~/py/Python-3.11.0 或类似的目录,具体取决于您下载的版本。这是一个树的根,其中包含整个标准 Python 发布版的源代码形式。

配置、构建和测试

您将在此目录下的 README 文件中找到详细的注释,位于“构建说明”标题下,我们建议您阅读这些注释。然而,在最简单的情况下,您可能只需在 shell 提示符下输入以下命令:

$ cd ~/py/Python-3.11/0
$ ./configure
    [configure writes much information, snipped here]
$ make
    [make takes quite a while and emits much information, snipped here]

如果在运行 make 之前未先运行 ./configuremake 隐式地运行 ./configure。当 make 完成时,请检查您刚刚构建的 Python 是否正常工作:

$ make test
    [takes quite a while, emits much information, snipped here]

通常,make test 确认您的构建工作正常,但还会通知您由于缺少可选模块而跳过了一些测试。

一些模块是特定于平台的(例如,某些仅在运行 SGI 古老 IRIX 操作系统的机器上工作的模块);您无需担心它们。但是,其他模块可能会被跳过,因为它们依赖于当前未安装在您的机器上的其他开源软件包。例如,在 Unix 上,模块 _tkinter —— 用于运行 Tkinter GUI 包和 IDLE 集成开发环境,它们与 Python 一起提供 —— 只有在 ./configure 可以在您的机器上找到 Tcl/Tk 8.0 或更高版本的安装时才能构建。有关更多细节和不同 Unix 及类 Unix 平台的特定注意事项,请参阅 README 文件。

从源代码构建使您可以以多种方式调整配置。例如,您可以以特殊方式构建 Python,以帮助您在开发 C 代码的 Python 扩展时调试内存泄漏,详细内容请参阅“构建和安装 C 编码的 Python 扩展”在 第二十五章 中。./configure --help 是了解可以使用的配置选项的良好信息来源。

构建后的安装

默认情况下,./configure 准备将 Python 安装在 /usr/local/bin/usr/local/lib 中。您可以在运行 make 之前通过在 ./configure 中使用 --prefix 选项来更改这些设置。例如,如果您希望在家目录的子目录 py311 中进行私有安装 Python,请运行:

$ cd ~/py/Python-3.11.0
$ ./configure --prefix=~/py311

并按照前一节中的方法继续执行make。一旦构建和测试 Python 完成,要执行所有文件的实际安装,请运行以下命令:¹¹

$ make install

运行make install的用户必须对目标目录具有写权限。根据您选择的目标目录及其权限,当您运行make install时,您可能需要surootbin或其他某个用户。用于此目的的常见习语是sudo make install:如果sudo提示输入密码,请输入当前用户的密码,而不是root的密码。另一种替代且推荐的方法是安装到虚拟环境中,如“Python 环境”中所述。

¹ 对于 Android,请参阅https://wiki.python.org/moin/Android,对于 iPhone 和 iPad,请参阅Python for iOS and iPadOS

² Python 3.11 版本开始使用“C11 无可选功能”并指定“公共 API 应与 C++兼容”。

³ 可以用许多编程语言编写,不仅仅是 Python。

⁴ 实际上,conda 的功能扩展到其他语言,并且 Python 只是另一个依赖项。

⁵ 通过 Anaconda 自动安装 250+,可以使用conda install显式安装 7500+。

⁶ 流行的商业模式是免费增值:同时发布免费版本和带有技术支持和可能的额外功能的商业“高级”版本。

⁷ Python 软件基金会运行重要基础设施,支持 Python 生态系统。欢迎捐赠给 PSF。

⁸ 我们需要鼓励更多的企鹅对我们的语言感兴趣!

⁹ 或者,在现代 Windows 版本中,极力推荐使用Windows Terminal

¹⁰ 源安装的大多数问题涉及各种支持库的缺失,这可能导致构建解释器时某些功能丢失。“Python 开发者指南”解释了如何处理各种平台上的依赖关系。build-python-from-source.com是一个有用的网站,展示了在多个 Linux 平台上下载、构建和安装特定版本的 Python 以及大多数所需支持库的所有命令。

¹¹ 或者make altinstall,如果您希望避免创建 Python 可执行文件和手动页的链接。

第二章:Python 解释器

在 Python 中开发软件系统时,通常会编写包含 Python 源代码的文本文件。您可以使用任何文本编辑器来完成这项工作,包括我们在“Python 开发环境”中列出的编辑器。然后,您可以使用 Python 编译器和解释器处理源文件。您可以直接执行此操作,也可以在集成开发环境 (IDE) 中执行此操作,或者通过嵌入 Python 的另一个程序执行此操作。Python 解释器还允许您交互地执行 Python 代码,就像 IDE 一样。

python 程序

Python 解释器程序的运行方式是 python(在 Windows 上命名为 python.exe)。该程序包括解释器本身和 Python 编译器,后者在需要时会隐式调用已导入的模块。根据您的系统,该程序可能必须位于 PATH 环境变量中列出的目录中。或者,与任何其他程序一样,您可以在命令 (shell) 提示符处提供其完整路径名,或在运行它的 shell 脚本 (或快捷方式目标等) 中提供其完整路径名。¹

在 Windows 上,按下 Windows 键然后开始键入 python。出现“Python 3.x”(命令行版本)以及其他选择,例如“IDLE”(Python GUI)。

环境变量

除了 PATH 外,其他环境变量也会影响 python 程序。其中一些与在命令行传递给 python 的选项具有相同的效果,就像我们在下一节中展示的那样,但是有几个环境变量提供了通过命令行选项不可用的设置。下面列出了一些经常使用的环境变量;有关完整详情,请参阅在线文档

PYTHONHOME

Python 安装目录。必须在此目录下包含一个 lib 子目录,其中包含 Python 标准库。在类 Unix 系统上,标准库模块应该位于 lib/python-3.x 中,其中 x 是次要 Python 版本。如果没有设置 PYTHONHOME,则 Python 会对安装目录进行一个明智的猜测。

PYTHONPATH

Python 可以导入模块的目录列表,在类 Unix 系统上用冒号分隔,在 Windows 上用分号分隔。这个列表扩展了 Python 的 sys.path 变量的初始值。我们在第七章中介绍了模块、导入和 sys.path。

PYTHONSTARTUP

每次启动交互式解释器会话时运行的 Python 源文件的名称。如果您没有设置此变量,或者设置为找不到的文件的路径,那么就不会运行这样的文件。当您运行 Python 脚本时,PYTHONSTARTUP 文件不会运行;它只在您启动交互式会话时运行。

如何设置和检查环境变量取决于您的操作系统。 在 Unix 中,使用 shell 命令,通常在启动 shell 脚本中。 在 Windows 上,按 Windows 键并开始键入 environment var,然后会出现一些快捷方式:一个用于用户环境变量,另一个用于系统环境变量。 在 Mac 上,您可以像在其他类 Unix 系统上一样工作,但您有更多的选择,包括专门针对 MacPython 的 IDE。 有关在 Mac 上使用 Python 的更多信息,请参阅 在线文档中的“在 Mac 上使用 Python”

命令行语法和选项

Python 解释器的命令行语法可以总结如下:

[*`path`*]`python` {*`options`*} [-`c` *`command`* | -`m` *`module`* | *`file`* | -] {*`args`*}

方括号([])表示可选内容,大括号()表示可能出现的项,竖线(|)表示多个选项之间的选择。Python 中使用斜杠(/)表示文件路径,就像在 Unix 中一样。

在命令行上运行 Python 脚本可以简单地如下所示:

$ python hello.py
Hello World

您还可以明确提供脚本的路径:

$ python ./hello/hello.py
 Hello World

脚本的文件名可以是绝对路径或相对路径,并且不一定需要特定的扩展名(虽然使用 .py 扩展名是常规做法)。

选项 是区分大小写的短字符串,以连字符开头,请求 python 以非默认行为运行。 python 只接受以连字符(-)开头的选项。 最常用的选项列在 表 2-1 中。 每个选项的描述都给出了环境变量(如果有的话),设置该变量会请求对应的行为。 许多选项都有更长的版本,以两个连字符开头,如 python -h 所示。 有关详细信息,请参阅 在线文档

表 2-1. 经常使用的 python 命令行选项

选项 意义(及对应的环境变量,如果有的话)
-B 不将字节码文件保存到磁盘上(PYTHONDONTWRITEBYTECODE)
-c 在命令行内给出 Python 语句
-E 忽略所有环境变量
-h 显示完整的选项列表,然后终止
-i 在文件或命令运行后运行交互会话(PYTHONINSPECT)
-m 指定要作为主脚本运行的 Python 模块
-O 优化字节码(PYTHONOPTIMIZE)—注意这是大写字母 O,而不是数字 0
-OO 类似于 -O,但同时从字节码中删除文档字符串
-S 在启动时省略隐式 import site(在 “站点定制” 中有介绍)
-t, -tt 警告不一致的制表符使用(-tt 对相同问题发出错误而不仅仅是警告)
-u 使用无缓冲的二进制文件进行标准输出和标准错误(PYTHONUNBUFFERED)
-v 详细跟踪模块导入和清理操作(PYTHONVERBOSE)
-V 打印 Python 版本号,然后终止
-W arg 向警告过滤器添加一个条目(参见“warnings 模块”)
-x 排除(跳过)脚本源代码的第一行

当您希望在运行某些脚本后立即获得交互式会话并且顶级变量仍然完整且可供检查时,请使用-i。对于正常的交互式会话,您不需要-i,尽管它也无害。

-O-OO 在您导入的模块生成的字节码中节省了时间和空间,将assert语句转换为无操作,正如我们在“assert 语句”中所述。-OO 还会丢弃文档字符串。²

在选项后,如果有的话,通过将文件路径添加到该脚本来告诉 Python 要运行哪个脚本。而不是文件路径,你可以使用-c command来执行 Python 代码字符串命令。command通常包含空格,因此你需要在其周围添加引号以满足操作系统的 shell 或命令行处理器的要求。某些 shell(例如,bash)允许您将多行输入作为单个参数,因此command可以是一系列 Python 语句。其他 shell(例如 Windows shell)限制您为单行;command可以是一个或多个用分号(;)分隔的简单语句,正如我们在“语句”中讨论的那样。

另一种指定要运行的 Python 脚本的方法是使用-m module。此选项告诉 Python 从 Python 的 sys.path 中的某个目录加载和运行名为module的模块(或名为module的包或 ZIP 文件的*main.py*成员);这对于使用 Python 标准库中的几个模块非常有用。例如,正如我们在“timeit 模块”中讨论的那样,-m timeit通常是执行 Python 语句的最佳方式。

连字符(-)或在此位置缺少任何令牌告诉解释器从标准输入读取程序源码,通常是交互式会话。只有在跟随进一步参数时才需要连字符。args是任意字符串;您运行的 Python 可以将这些字符串作为 sys.argv 列表的项访问。

例如,在命令提示符处输入以下内容即可使 Python 显示当前日期和时间:

$ python -c "import time; print(time.asctime())"

如果 Python 可执行文件的目录在您的 PATH 环境变量中,您可以仅以python开头(无需指定完整路径)。(如果您安装了多个版本的 Python,您可以使用例如python3python3.10指定版本;然后,如果您只说python,则使用的版本是您最近安装的版本。)

Windows py 启动器

在 Windows 上,Python 提供了 py 启动器,用于在计算机上安装和运行多个 Python 版本。安装程序的底部有一个选项,用于为所有用户安装启动器(默认已选中)。当存在多个版本时,您可以使用 py 后跟版本选项选择特定版本,而不是简单的 python 命令。常见的 py 命令选项列在 表 2-2 中(使用 py -h 查看所有选项)。

表 2-2. 经常使用的 py 命令行选项

选项 意义
-2 运行最新安装的 Python 2 版本。
-3 运行最新安装的 Python 3 版本。
-3.x-3.x-nn 运行特定的 Python 3 版本。当仅引用为 -3.10 时,使用 64 位版本,如果没有 64 位版本则使用 32 位版本。 -3.10-32-3.10-64 在两者都安装时选择特定的构建版本。
-0--list 列出所有已安装的 Python 版本,包括标识是否为 32 位或 64 位的构建,如 3.10-64
-h 列出所有 py 命令选项,后跟标准 Python 帮助。

如果未指定版本选项,py 将运行最新安装的 Python。

例如,要使用已安装的 Python 3.9 64 位版本显示本地时间,可以运行以下命令:

C:\> py -3.9 -c "import time; print(time.asctime())"

(通常不需要指定 py 的路径,因为安装 Python 会将 py 添加到系统 PATH 中。)

PyPy 解释器

PyPy,用 Python 编写,实现了自己的编译器以生成在 LLVM 后端运行的 LLVM 中间代码。PyPy 项目在性能和多线程方面比标准的 CPython 有一些改进。(截至本文写作时,PyPy 已更新至 Python 3.9。)

pypy 可以类似于 python 运行:

[*path*]pypy {*options*} [-c *command* | *file* | - ] {*args*}

请查看 PyPy 的 主页 获取安装说明和完整的最新信息。

交互式会话

当你运行 python 而没有脚本参数时,Python 启动交互会话,并提示你输入 Python 语句或表达式。交互式会话对于探索、检查和使用 Python 作为强大、可扩展的交互式计算器非常有用。(本章末尾简要讨论的 Jupyter Notebook 就像专门用于交互式会话的“强化版 Python”。)这种模式通常称为 REPL,即读取-求值-打印循环,因为解释器基本上就是这样做的。

当您输入完整语句时,Python 执行它。当您输入完整表达式时,Python 评估它。如果表达式有结果,Python 输出表示结果的字符串,并将结果分配给名为 _(单个下划线)的变量,以便您可以立即在另一个表达式中使用该结果。当 Python 预期语句或表达式时,提示字符串为 >>>,当已开始但未完成语句或表达式时为 ...。特别地,在您在前一行打开括号、方括号或大括号但尚未关闭它时,Python 使用 ... 提示。

在交互式 Python 环境中工作时,您可以使用内置的 help() 函数进入一个帮助实用程序,提供关于 Python 关键字和运算符、安装的模块以及一般主题的有用信息。在浏览长帮助描述时,按 q 返回到 help> 提示符。要退出实用程序并返回到 Python >>> 提示符,请输入 quit。您还可以通过在 Python 提示符下输入 help(obj) 来获取有关特定对象的帮助,其中 obj 是您想要更多帮助的程序对象。

有几种方式可以结束交互会话。最常见的是:

  • 输入您的操作系统的文件结尾按键(在 Windows 上为 Ctrl-Z,在类 Unix 系统上为 Ctrl-D)。

  • 执行内置函数 quit 或 exit,使用形式 quit() 或 exit()。(省略尾随 () 将显示消息,如“使用 quit() 或 Ctrl-D(即 EOF)退出”,但仍会保留您在解释器中。)

  • 执行语句 raise SystemExit,或调用 sys.exit()(我们在 第 6 章 中讨论 SystemExit 和 raise,以及在 第 8 章 中的 sys 模块)。

使用 Python 交互解释器进行实验。

在交互式解释器中尝试 Python 语句是快速实验 Python 并立即看到结果的一种方式。例如,这里是内置 enumerate 函数的简单使用:

>>> print(list(enumerate("abc")))
[(0, 'a'), (1, 'b'), (2, 'c')]

交互解释器是学习核心 Python 语法和特性的良好入门平台。(经验丰富的 Python 开发人员经常打开 Python 解释器来快速检查不经常使用的命令或函数。)

行编辑和历史记录功能部分依赖于 Python 的构建方式:如果包含了 readline 模块,则可使用 GNU readline 库的所有功能。Windows 对于像 python 这样的交互文本模式程序有一个简单但可用的历史记录功能。

除了内置的 Python 交互式环境和下一节介绍的更丰富的开发环境中提供的环境外,你可以自由下载其他强大的交互式环境。最流行的是IPython,在“IPython”中有详细介绍,提供了丰富的功能。一个更简单、更轻量级但同样非常方便的替代读取行解释器是bpython

Python 开发环境

Python 解释器的内置交互模式是 Python 最简单的开发环境。它比较原始,但是轻量级,占用空间小,启动速度快。配合一个好的文本编辑器(如“带有 Python 支持的免费文本编辑器”中讨论的),以及行编辑和历史记录功能,交互式解释器(或者更强大的 IPython/Jupyter 命令行解释器)是一个可用的开发环境。但是,你还可以使用其他几种开发环境。

IDLE

Python 的集成开发与学习环境(IDLE)随着大多数平台上的标准 Python 发行版一起提供。IDLE 是一个跨平台的、100% 纯 Python 应用程序,基于 Tkinter GUI。它提供一个类似交互式 Python 解释器的 Python shell,但功能更丰富。还包括一个专为编辑 Python 源代码优化的文本编辑器、集成的交互式调试器以及几个专用的浏览器/查看器。

若要在 IDLE 中获得更多功能,请安装IdleX,这是一个大量的免费第三方扩展集合。

要在 macOS 上安装并使用 IDLE,请按照 Python 网站上的具体说明进行操作。

其他 Python IDE

IDLE 是成熟、稳定、易用、功能相当丰富且可扩展的。然而,还有许多其他 IDE:跨平台或特定于平台、免费或商业化(包括带有免费提供的商业 IDE,特别是如果你开发开源软件)、独立或作为其他 IDE 的附加组件。

其中一些 IDE 具有静态分析、GUI 构建器、调试器等功能。Python 的 IDE wiki 页面列出了 30 多种,并指向许多其他 URL,包括评测和比较。如果你是 IDE 收集者,祝你好运!

即使是所有可用的 IDE 的一个小小子集,我们也无法完全公正地进行介绍。流行的跨平台、跨语言模块化 IDE Eclipse 的免费第三方插件 PyDev 具有出色的 Python 支持。史蒂夫长期以来一直使用由 Archaeopteryx 推出的 Wing,这是最古老的 Python 专用 IDE。保罗的首选 IDE,也可能是当今最流行的第三方 Python IDE,是由 JetBrains 推出的 PyCharmThonny 是一款流行的初学者 IDE,轻量但功能齐全,可以轻松安装在 Raspberry Pi(或几乎任何其他流行平台)上。还有不容忽视的是微软的 Visual Studio Code,这是一个非常出色且非常流行的跨平台 IDE,支持多种语言,包括 Python(通过插件)。如果您使用 Visual Studio,请查看 PTVS,这是一个开源插件,特别擅长在需要时允许 Python 和 C 语言混合调试。

具有 Python 支持的免费文本编辑器

您可以使用任何文本编辑器编辑 Python 源代码,甚至是简单的,比如在 Windows 上的记事本或在 Linux 上的 ed。许多强大的免费编辑器支持 Python,并带有额外功能,如基于语法的着色和自动缩进。跨平台编辑器使您能够在不同平台上以统一的方式工作。优秀的文本编辑器还允许您在编辑器内运行您选择的工具对正在编辑的源代码进行操作。Python 编辑器的最新列表可以在 PythonEditors wiki 上找到,其中列出了数十种编辑器。

就编辑能力而言,最出色的可能是经典的Emacs(请参阅 Python wiki 页面以获取特定于 Python 的附加组件)。Emacs 不易学习,也不是轻量级。³ Alex 的个人最爱⁴ 是另一个经典之作:Vim,Bram Moolenaar 改进的传统 Unix 编辑器 vi 的版本。可以说它几乎不如 Emacs 强大,但仍然值得考虑——它快速、轻量级、支持 Python 编程,并在文本模式和 GUI 版本中均可运行。对于优秀的 Vim 覆盖范围,请参阅Learning the vi and Vim Editors,Arnold Robbins 和 Elbert Hannah 编著的第 8 版(O’Reilly);参阅 Python wiki 页面以获取 Python 特定的技巧和附加组件。Steve 和 Anna 也使用 Vim,并且在可用时,Steve 还使用商业编辑器Sublime Text,具有良好的语法着色和足够的集成,可以从编辑器内部运行程序。对于快速编辑和执行短 Python 脚本(甚至对于多兆字节文本文件也是快速且轻量级的通用文本编辑器),Paul 选择SciTE

Python 程序检查工具

Python 编译器足以检查程序语法以便运行程序或报告语法错误。如果希望更彻底地检查 Python 代码,可以下载并安装一个或多个第三方工具。pyflakes 是一个非常快速、轻量级的检查器:它不是很彻底,但它不会导入它正在检查的模块,这使得使用它快速且安全。在另一端,pylint 非常强大且高度可配置;它不是轻量级的,但通过可编辑的配置文件可以高度自定义地检查许多样式细节。⁵ flake8 将 pyflakes 与其他格式化程序和自定义插件捆绑在一起,通过在多个进程之间分配工作可以处理大型代码库。black 及其变体blue 故意不太可配置;这使得它们在广泛分散的项目团队和开源项目中流行,以强制执行常见的 Python 风格。为了确保不会忘记运行它们,可以将一个或多个这些检查器/格式化程序整合到您的工作流程中,使用pre-commit package

对于更彻底地检查 Python 代码的正确类型使用,请使用mypy等工具;请参阅第五章了解更多相关内容。

运行 Python 程序

无论您使用什么工具来生成 Python 应用程序,您都可以将其视为一组 Python 源文件,这些文件是通常具有扩展名 .py 的普通文本文件。脚本 是可以直接运行的文件。模块 是可以导入的文件(详见第七章),为其他文件或交互式会话提供一些功能。Python 文件可以同时是模块(导入时提供功能)和脚本(可以直接运行)。一个有用且广泛使用的约定是,Python 文件如果主要用于导入为模块,在直接运行时应执行一些自测操作,详见“测试”。

Python 解释器会根据需要自动编译 Python 源文件。Python 会将编译后的字节码保存在模块源代码所在的子目录 pycache 中,并添加一个版本特定的扩展名来表示优化级别。

要避免将编译后的字节码保存到磁盘上,您可以使用选项 -B 运行 Python,当您从只读磁盘导入模块时可能会很方便。此外,当您直接运行脚本时,Python 不会保存脚本的编译后的字节码形式;相反,每次运行时都会重新编译脚本。Python 仅为您导入的模块保存字节码文件。每当必要时,例如编辑模块源代码时,它会自动重建每个模块的字节码文件。最终,您可以使用第二十四章中介绍的工具(在线版可参考这里)对 Python 模块进行打包部署。

您可以使用 Python 解释器或者一个 IDE 来运行 Python 代码。⁶ 通常,您通过运行顶层脚本开始执行。要运行一个脚本,请将其路径作为参数传递给 python,详见“python 程序”。根据您的操作系统,您可以直接从 shell 脚本或命令文件调用 python。在类 Unix 系统上,您可以通过设置文件的权限位 x 和 r,并以 shebang 行开头,例如以下行:

#!/usr/bin/env python

或者其他以 #! 开头,后跟 Python 解释器程序路径的行,此时您可以选择性地添加一个选项单词,例如:

#!/usr/bin/python -OB

在 Windows 上,你可以使用相同的 #! 行风格,符合PEP 397,指定特定版本的 Python,这样你的脚本可以在类 Unix 和 Windows 系统之间跨平台运行。你还可以通过双击图标等通常的 Windows 机制来运行 Python 脚本。当你通过双击脚本图标来运行 Python 脚本时,Windows 会在脚本终止后自动关闭与脚本关联的文本模式控制台。如果你希望控制台保持开放(以便用户可以在屏幕上看到脚本的输出),确保脚本不要过早终止。例如,在脚本的最后一个语句中使用:

input('Press Enter to terminate')

当你从命令提示符运行脚本时,这是不必要的。

在 Windows 上,你还可以使用扩展名 .pyw 和解释器程序 pythonw.exe 替代 .pypython.exew 变体运行 Python 时没有文本模式控制台,因此没有标准输入和输出。这对依赖 GUI 或在后台静默运行的脚本非常有用。只有在程序完全调试完成后才使用它们,以便在开发过程中保留标准输出和错误信息以供信息、警告和错误消息使用。

使用其他语言编码的应用程序可能会嵌入 Python,以控制 Python 的执行以实现其自身的目的。我们在“嵌入 Python”中简要讨论了这一点,详见第二十五章(在线版见此处)。

在浏览器中运行 Python

同样存在在浏览器会话中运行 Python 代码的选项,可以在浏览器进程中或某些独立的基于服务器的组件中执行。PyScript 是前者的典范,而 Jupyter 则是后者。

PyScript

最近 Python 在浏览器中的一个发展是由 Anaconda 发布的PyScript。PyScript 建立在 Pyodide 之上,⁷使用 WebAssembly 在浏览器中启动一个完整的 Python 引擎。PyScript 引入了自定义 HTML 标签,因此你可以在不需要了解或使用 JavaScript 的情况下编写 Python 代码。使用这些标签,你可以创建一个静态 HTML 文件,其中包含 Python 代码,在远程浏览器中运行,无需安装额外的软件。

简单的 PyScript “Hello, World!” HTML 文件可能看起来像这样:

<html>
<head>
    <link rel='stylesheet' 
 href='https://pyscript.net/releases/2022.06.1/pyscript.css' />
    <script defer 
 src='https://pyscript.net/releases/2022.06.1/pyscript.js'></script>
</head>
<body>
<py-script>
`import` time
print('Hello, World!')
print(f'The current local time is {time.asctime()}')
print(f'The current UTC time is {time.asctime(time.gmtime())}')
</py-script>
</body>
</html>

即使你的电脑上没有安装 Python,你也可以将这段代码保存为静态 HTML 文件并在客户端浏览器中成功运行。

PyScript 即将迎来变化

在出版时,PyScript 仍处于早期开发阶段,因此这里显示的特定标签和 API 可能会随着软件包的进一步开发而发生变化。

获取更全面和最新的信息,请参阅PyScript 网站

Jupyter

IPython 中交互式解释器的扩展(在“IPython”中涵盖)被 Jupyter 项目 进一步扩展,这个项目最著名的是 Jupyter Notebook,它为 Python 开发者提供了一种 “文学编程” 工具。一个笔记本服务器,通常通过网站访问,保存和加载每个笔记本,创建一个 Python 内核进程来交互地执行其 Python 命令。

笔记本是一个丰富的环境。每个笔记本都是一个单元格序列,其内容可以是代码或使用 Markdown 语言扩展的富文本格式,允许包含复杂的数学公式。代码单元格也可以产生丰富的输出,包括大多数流行的图像格式以及脚本化的 HTML。特殊的集成将 matplotlib 库适应到网络上,有越来越多的机制用于与笔记本代码进行交互。

更多的集成使得笔记本以其他方式出现成为可能。例如,通过适当的扩展,您可以轻松地将 Jupyter 笔记本格式化为 reveal.js 幻灯片,用于交互式执行代码单元格的演示。Jupyter Book 允许您将笔记本集合为章节并将其发布为书籍。GitHub 允许浏览(但不执行)上传的笔记本(一个特殊的渲染器提供正确的笔记本格式)。

互联网上有许多 Jupyter 笔记本的示例。要了解其功能的一个很好的演示,请查看 Executable Books 网站;笔记本支持其发布格式。

¹ 如果路径名包含空格,则可能需要使用引号—同样,这取决于您的操作系统。

² 这可能会影响解析文档字符串以进行有意义目的的代码;我们建议您避免编写此类代码。

³ 一个很好的入门地点是 Learning GNU Emacs, 3rd edition(O’Reilly)。

⁴ 不仅是“一个编辑器”,还是 Alex 最喜欢的“接近 IDE 的工具”!

⁵ pylint 还包括有用的 pyreverse 实用工具,可以直接从您的 Python 代码自动生成 UML 类和包图。

⁶ 或在线:例如,Paul 维护了一个在线 Python 解释器的 列表

⁷ 这是开源项目通过“站在巨人的肩膀上”获得的协同效应的一个很好的例子,这已经成为一种普遍的、日常的事情!

第三章:Python 语言

本章是 Python 语言的指南。如果您想从头开始学习 Python,我们建议您从在线文档中选择合适的链接,并参考“非程序员的 Python 文档”中提到的资源。如果您已经很熟悉至少一种其他编程语言,只是想了解 Python 的具体信息,那么本章适合您。但是,我们不打算教授 Python:我们在相当快的速度下涵盖了大量内容。我们专注于规则,只是次要地指出最佳实践和风格;作为您的 Python 风格指南,请使用PEP 8(可以加上额外的指导,如“Python 之旅”CKANGoogle的指导)。

词法结构

编程语言的词法结构是指规定如何在该语言中编写程序的基本规则集。它是语言的最低级语法,指定诸如变量名的外观和如何表示注释等事物。每个 Python 源文件,像任何其他文本文件一样,都是一系列字符。您还可以有用地将其视为行、标记或语句的序列。这些不同的词法视图相辅相成。Python 对程序布局非常讲究,特别是对行和缩进:如果您从其他语言来学习 Python,请注意这些信息。

行和缩进

Python 程序是一系列逻辑行,每个逻辑行由一个或多个物理行组成。每个物理行可能以注释结束。不在字符串字面值内的井号(#)开始注释。井号后的所有字符直到行尾(但不包括行尾)都是注释:Python 会忽略它们。只包含空白字符,可能带有注释的行是空白行:Python 会忽略它。在交互式解释器会话中,您必须输入一个空的物理行(没有任何空白或注释)来结束多行语句。

在 Python 中,物理行的结束标志着大多数语句的结束。与其他语言不同,您通常不需要使用分隔符(如分号;)来终止 Python 语句。当语句过长无法适应物理行时,可以通过确保第一物理行不包含注释并以反斜杠(\)结尾来将两个相邻的物理行连接成一个逻辑行。更加优雅的是,如果一个开放括号((),方括号(),或大括号({)尚未关闭,Python 也会自动将相邻的物理行合并为一个逻辑行:利用此机制可以生成比以反斜杠结尾更易读的代码。三引号字符串文字也可以跨越物理行。在逻辑行的第一个物理行之后的物理行被称为连续行。缩进规则适用于每个逻辑行的第一个物理行,而不是连续行。

Python 使用缩进来表示程序的块结构。Python 不使用大括号或其他开始/结束界定符来标识语句块;缩进是唯一指定块的方法。Python 程序中的每个逻辑行左侧的空白表示缩进。是一系列连续的逻辑行,所有这些行都具有相同的缩进量;具有较少缩进的逻辑行终止该块。块中的所有语句以及复合语句中的所有子句必须具有相同的缩进,源文件中的第一条语句不得缩进(即不得以任何空白字符开头)。在交互式解释器主提示符>>>(详见[“交互式会话”)处键入的语句也不得缩进。

Python 将每个制表符视为多达 8 个空格,因此制表符之后的下一个字符落入逻辑列 9、17、25 等。标准的 Python 风格是每个缩进级别使用四个空格(绝对不是制表符)。

如果必须使用制表符,Python 不允许混合使用制表符和空格进行缩进。

使用空格而非制表符

配置您喜欢的编辑器,将 Tab 键扩展为四个空格,以便您编写的所有 Python 源代码都只包含空格,而不是制表符。这样,包括 Python 在内的所有工具在处理 Python 源文件的缩进时都是一致的。最佳的 Python 风格是每个缩进块恰好缩进四个空格;不使用制表符。

字符集

Python 源文件可以使用任何 Unicode 字符,默认编码为 UTF-8(ASCII 字符是 0 到 127 之间的编码,UTF-8 将这些字符编码为相应的单个字节,因此 ASCII 文本文件也是良好的 Python 源文件。)

可以选择告诉 Python,某个源文件采用了不同的编码。在这种情况下,Python 将使用该编码读取文件。要让 Python 知道源文件使用了非标准编码,请在源文件开头加上以下形式的注释:

# coding: iso-8859-1

编码后,写入来自 codecs 模块的 ASCII 兼容编解码器的名称,例如 utf-8 或 iso-8859-1。请注意,这个coding directive注释(也称为encoding declaration)只有在它位于源文件的开头时才被视为这样(可能在“运行 Python 程序”中讨论的“shebang 行”之后)。最佳做法是对所有文本文件,包括 Python 源文件,都使用 utf-8。

令牌

Python 将每个逻辑行分解为一系列称为令牌的基本词法组件。每个令牌对应于逻辑行的子字符串。正常的令牌类型包括标识符关键字运算符分隔符字面值,我们将在以下部分介绍。您可以在令牌之间自由使用空格将它们分开。在逻辑上相邻的标识符或关键字之间需要一些空格分隔;否则,Python 会将它们解析为一个更长的标识符。例如,ifx 是一个单独的标识符;要写出关键字 if 后跟标识符 x,您需要插入一些空格(通常只有一个空格字符,即 if x)。

标识符

标识符是用于指定变量、函数、类、模块或其他对象的名称。标识符以字母开头(即任何 Unicode 分类为字母的字符)或下划线(_)开头,后跟零个或多个字母、下划线、数字或其他 Unicode 分类为字母、数字或组合标记的字符(如Unicode 标准附录#31定义)。

例如,在 Unicode Latin-1 字符范围内,标识符的有效前导字符是:

ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz
ªµºÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ

在领先字符之后,有效的标识符主体字符与之前相同,加上数字和·(Unicode 中间点)字符:

0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz
ªµ·ºÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ

大小写有区分:小写和大写字母是不同的。不允许在标识符中使用@、$ 和 ! 等标点符号字符。

小心使用同形字符的 Unicode 字符

一些 Unicode 字符与其他字符非常相似,甚至难以区分。这种字符对称为同形字符。例如,比较大写字母 A 和大写希腊字母 alpha (Α)。这实际上是两个不同的字母,在大多数字体中看起来非常相似(或相同)。在 Python 中,它们定义了两个不同的变量:

>>> A = 100
>>> *`# this variable is GREEK CAPITAL LETTER ALPHA:`*>>> Α = 200  
>>> print(A, Α)
100 200

如果要使您的 Python 代码广泛可用,我们建议制定一个所有标识符、注释和文档均使用英文书写的政策,特别是避免使用非英文同形字符。有关更多信息,请参阅 PEP 3131

Unicode 归一化策略增加了更多复杂性(Python 在解析包含 Unicode 字符的标识符时使用 NFKC 归一化)。有关更多信息,请参阅 Jukka K. Korpela 的《Unicode Explained》(O’Reilly)和 Unicode 网站上提供的其他技术信息及该网站引用的书籍。

避免在标识符中使用可归一化的 Unicode 字符

当名字包含特定 Unicode 字符时,Python 可能会在变量之间创建意外的别名,通过内部将名字转换为使用归一化字符的名字。例如,字母 ª 和 º 归一化为 ASCII 小写字母 a 和 o,因此使用这些字母的变量可能与其他变量冲突:

>>> a, o = 100, 101
>>> ª, º = 200, 201
>>> print(a, o, ª, º)
200 201 200 201  *# expected "100 101 200 201"*

最好避免在 Python 标识符中使用可归一化的 Unicode 字符。

Python 的正常风格是类名以大写字母开头,大多数¹其他标识符以小写字母开头。以单个前导下划线开头的标识符表明按照惯例该标识符应为私有。以两个前导下划线开头的标识符表示为强制私有标识符;然而,如果标识符同时以两个尾随下划线结尾,则意味着它是语言定义的特殊名称。由多个单词组成的标识符应全部小写,并在单词之间使用下划线,例如 login_password。有时这被称为蛇形命名法

交互式解释器中的单个下划线 (_)

标识符 _(一个单下划线)在交互式解释器会话中是特殊的:如果有的话,解释器将 _ 绑定到其最后评估的表达式语句的结果。

关键字

Python 有 35 个关键字,或者说保留给特殊语法用途的标识符。与标识符一样,关键字是区分大小写的。你不能将关键字用作普通标识符(因此有时也称为“保留字”)。一些关键字用于开始简单语句或复合语句的子句,而其他关键字是运算符。我们在本书中详细介绍所有 Python 的关键字,要么在本章节,要么在第 4、6 和 7 章节。Python 中的关键字包括:

and break elif from is pass with
as class else global lambda raise yield
assert continue except if nonlocal return False
async def finally import not try None
await del for in or while True

你可以通过导入 keyword 模块并打印 keyword.kwlist 来列出它们。

3.9+ 此外,Python 3.9 引入了软关键字的概念,这些关键字是上下文敏感的。也就是说,它们是某些特定语法结构的语言关键字,但在这些结构之外,它们可以用作变量或函数名,因此它们不是保留字。在 Python 3.9 中没有定义软关键字,但 Python 3.10 引入了以下软关键字:

_ case match

您可以通过打印 keyword.softkwlist 模块来列出它们。

运算符

Python 使用非字母数字字符和字符组合作为运算符。 Python 识别以下运算符,这些运算符在“表达式和运算符”中有详细介绍:

+  -  *  /  %   **  //  <<  >>  &   @

|  ^  ~  <  <=  >   >=  !=  ==  @=  :=

您可以使用 @ 作为运算符(在矩阵乘法中使用,详见第十六章),尽管(从学术角度讲!)该字符实际上是一个分隔符。

分隔符

Python 在各种语句、表达式和列表、字典和集合字面量以及推导中使用以下字符和组合作为分隔符,还有其他用途:

(    )    [    ]    {    }

,    :    .    =    ;    @

+=   -=   *=   /=   //=  %=

&=   |=   ^=   >>=  <<=  **=

句点 (.) 也可以出现在浮点文字中(例如,2.3)和虚数文字中(例如,2.3j)。最后两行是增强赋值运算符,它们是分隔符,但也执行操作。我们在介绍使用它们的对象或语句时讨论各种分隔符的语法。

以下字符在其他标记中具有特殊含义:

'  "  #  \

' 和 " 括字符串文字。 # 在字符串外部开始注释,该注释在当前行结束。 \ 在物理行末尾将其后的物理行连接到其后形成一个逻辑行; \ 在字符串中也是一个转义字符。字符 $ 和 ?,以及所有控制字符² 除了空白字符,永远不能成为 Python 程序文本的一部分,除非在注释或字符串文字中。

字面值

字面量是程序中数据值(数字、字符串或容器)的直接表示。以下是 Python 中的数字和字符串字面量:

42                     *`# Integer literal`*
3.14                   *`# Floating-point literal`*
1.0j                   *`# Imaginary literal`*
'hello'                *`# String literal`*
"world"                *`# Another string literal`*
*`"""Good`*
*`night"""`               `# Triple-quoted string literal, spanning 2 lines`*

结合适当的分隔符,可以直接使用数字和字符串字面值构建许多容器类型:

[42, 3.14, 'hello']     *`# List`*
[]                      *`# Empty list`*
100, 200, 300           *`# Tuple`*
(100, 200, 300)         *`# Tuple`*
()                      *`# Empty tuple`*
{'x':42, 'y':3.14}      *`# Dictionary`*
{}                      *`# Empty dictionary`*
{1, 2, 4, 8, 'string'}  *`# Set`*
*`# There is no literal form to denote an empty set; use set() instead`*

我们在“数据类型”详细讨论此类容器字面量³ 的语法,当我们讨论 Python 支持的各种数据类型时。在本书中,我们将这些表达式称为字面量,因为它们描述源代码中的字面(即不需要额外评估)值。

语句

您可以将 Python 源文件视为简单和复合语句的序列。

简单语句

简单语句是不包含其他语句的语句。简单语句完全位于一个逻辑行内。与许多其他语言一样,你可以在单个逻辑行上放置多个简单语句,分号(;)作为分隔符。然而,使用每行一个语句是通常和推荐的 Python 风格,它使程序更易读。

任何表达式都可以作为一个独立的简单语句存在(我们在“表达式和运算符”中讨论表达式)。当你在交互式环境中工作时,解释器会显示你在提示符(>>>)下输入的表达式语句的结果,并将结果绑定到名为 _(下划线)的全局变量中。除了交互式会话外,表达式语句仅用于调用具有副作用的函数(和其他可调用对象)(例如执行输出、更改参数或全局变量,或引发异常)。

赋值语句是将值分配给变量的简单语句,正如我们在“赋值语句”中讨论的那样。在 Python 中,使用 = 运算符进行赋值是一种语句,永远不能作为表达式的一部分。要作为表达式的一部分执行赋值,必须使用 :=(被称为“海象”)运算符。你将在“赋值表达式”中看到一些使用 := 的例子。

复合语句

复合语句包含一个或多个其他语句并控制它们的执行。复合语句有一个或多个子句,这些子句在相同的缩进级别上对齐。每个子句有一个以关键字开头并以冒号结尾的头部,后面跟着一个,即一个或多个语句的序列。通常,这些语句,也称为,在头部行之后的单独逻辑行上缩进四个空格。块在缩进返回到子句头部的缩进级别时(或者进一步向左到某个封闭复合语句的缩进级别时)在词法上结束。或者,体可以是跟在头部冒号后同一逻辑行上的单个简单语句。体也可以由几个简单语句组成,这些语句之间用分号隔开,但正如我们已经提到的,这不是良好的 Python 风格。

数据类型

Python 程序的运行依赖于它处理的数据。Python 中的数据值称为对象;每个对象,又称,都有一个类型。对象的类型决定了对象支持哪些操作(换句话说,你可以在值上执行哪些操作)。类型还决定了对象的属性(如果有的话),以及对象是否可以被修改。可以修改的对象称为可变对象,而不可修改的对象称为不可变对象。我们在“对象属性和项”中涵盖对象的属性和项。

内建函数 type(obj) 接受任何对象作为其参数,并返回对象 obj 的类型对象。 内建函数 isinstance(obj, type) 在对象 obj 的类型是 type(或其任何子类)时返回 True;否则返回 False。 isinstance 的 type 参数也可以是类型元组(3.10+ 或使用 | 操作符连接的多个类型),在这种情况下,如果 obj 的类型与给定类型的任何一个匹配,或其子类,则返回 True

Python 具有用于基本数据类型(如数字、字符串、元组、列表、字典和集合)的内建类型,如下一节所述。 您还可以创建用户定义的类型,称为 ,如 “类和实例” 中所述。

数字

Python 中的内建数值类型包括整数、浮点数和复数。 标准库还提供了十进制浮点数(在 “decimal 模块” 中介绍)和分数(在 “fractions 模块” 中介绍)。 Python 中的所有数字都是不可变对象;因此,当您对数字对象执行操作时,将产生新的数字对象。 我们在 “数值操作” 中涵盖了数字操作,也称为算术操作。

数值字面量不包括符号:如果存在,前导 + 或 - 是单独的运算符,如 “算术运算” 中所讨论的。

整数数字

整数字面量可以是十进制、二进制、八进制或十六进制。 十进制字面量是以非零开头的数字序列。 二进制字面量是 0b 后跟一系列二进制数字(0 或 1)。 八进制字面量是 0o 后跟一系列八进制数字(0 到 7)。 十六进制字面量是 0x 后跟一系列十六进制数字(0 到 9 和 A 到 F,大小写不限)。 例如:

1, 23, 3493                    *`# Decimal integer literals`*
0b010101, 0b110010, 0B01       *`# Binary integer literals`*
0o1, 0o27, 0o6645, 0O777       *`# Octal integer literals`*
0x1, 0x17, 0xDA5, 0xda5, 0Xff  *`# Hexadecimal integer literals`*

整数可以表示范围在 ±2**sys.maxsize,或大约 ±10^(2.8e18) 内的值。

表 3-1 列出了整数对象 i 支持的方法。

表 3-1. 整数方法

as_inte⁠g⁠e⁠r⁠_​r⁠a⁠t⁠i⁠o i.as_integer_ratio() 3.8+ 返回一个由两个整数构成的元组,其精确比例是原始整数值。(由于 i 总是 int,因此元组始终是 (i, 1);与 float.as_integer_ratio 进行比较。)
bit_count i.bit_count() 3.10+ 返回 abs(i) 二进制表示中 1 的个数。
bit_length i.bit_length() 返回表示 i 所需的最小位数。 相当于 abs(i) 的二进制表示的长度,在去除 'b' 和所有前导零后。(0).bit_length() 返回 0。
from_bytes int.from_bytes(bytes_value, byteorder, *, signed=False) 从 bytes_value 中的字节返回一个整数,参数使用与 to_bytes 相同。(注意,from_bytes 是 int 的类方法。)
to_bytes i.to_bytes(length, byteorder, , signed=False) 返回一个大小为length字节的 bytes 值,表示i的二进制值。byteorder必须是'str'值'big'或'little',表示返回值应该是大端(最高有效字节优先)或小端(最低有效字节优先)。例如,(258).to_bytes(2, 'big') 返回 b'\x01\x02',而(258).to_bytes(2, 'little') 返回 b'\x02\x01'。当i* < 0 且signedTrue时,to_bytes 返回i的二进制补码表示。当i < 0 且signedFalse时,to_bytes 引发 OverflowError。

浮点数

浮点数文字是一个包含十进制数字序列的字面量,包括小数点(.),指数后缀(e 或 E,可选地跟着+或-),或两者。浮点文字的前导字符不能是 e 或 E;它可以是任何数字或小数点(.)后跟一个数字。例如:

0., 0.0, .0, 1., 1.0, 1e0, 1.e0, 1.0E0  *`# Floating-point literals`*

Python 浮点数值对应于 C 双精度浮点数,并共享其范围和精度的限制:通常是 53 位——大约 15 位数字——在现代平台上的精度。 (关于代码运行平台上浮点数值的确切范围和精度以及许多其他细节,请参见sys.float_info的在线文档。)

表格 3-2 列出了浮点对象f支持的方法。

表格 3-2. 浮点方法

| as_inte⁠g⁠e⁠r⁠_​r⁠a⁠t⁠i⁠o | f.as_integer_ratio() 返回一个由两个整数组成的元组,分子和分母,其精确比值是原始浮点值f。例如:

>>> f=2.5
>>> f.as_integer_ratio()
(5, 2)

|

from_hex float.from_hex(s) 从十六进制字符串值s返回一个浮点值。s可以是 f.hex()返回的形式,或者只是一个十六进制数字字符串。当后者是这种情况时,from_hex 返回 float(int(s, 16))。
hex f.hex() 返回一个十六进制表示的f,包括前缀 0x 和尾随的 p 和指数。例如,(99.0).hex() 返回 '0x1.8c00000000000p+6'。
is_integer f.is_integer() 返回一个布尔值,指示f是否为整数值。等同于 int(f) == f

复数

复数由两个浮点值组成,分别用于实部和虚部。您可以访问复数对象z的部分作为只读属性z.real 和z.imag。您可以将虚数文字指定为任何浮点或整数十进制文字,后跟 j 或 J:

0j, 0.j, 0.0j, .0j, 1j, 1.j, 1.0j, 1e0j, 1.e0j, 1.0e0j

文字文字末尾的 j 表示负一的平方根,如在电气工程中通常使用的(其他学科使用 i,但 Python 使用 j)。没有其他复数文字。要表示任何常数复数,请加或减一个浮点数(或整数)文字和一个虚数。例如,要表示等于 1 的复数,请使用表达式如 1+0j 或 1.0+0.0j。Python 在编译时执行加法,因此不必担心开销。

复数对象 c 支持一个单一方法:

共轭 c.conjugate() 返回一个新的复数 complex(c.real, -c.imag)(即返回值具有 c 的 imag 属性,带有符号变化)。

参见 “数学和 cmath 模块” 以及使用浮点数和复数的其他函数。

数字文字中的下划线

为了帮助直观评估数字的大小,数字文字可以在数字之间或任何基数指定符号后包括单个下划线 (_)。然而,并不仅仅是十进制数字常量可以从这种符号自由中获益,正如以下示例所示:

>>> 100_000.000_0001, 0x_FF_FF, 0o7_777, 0b_1010_1010
(100000.0000001, 65535, 4095, 170)

下划线的位置规定并无强制要求(除了不能连续出现两个),因此 123_456 和 12_34_56 都表示相同的整数值为 123456。

序列

序列 是一种按整数索引的有序项容器。Python 具有称为字符串(bytes 或 str)、元组和列表的内置序列类型。库和扩展模块提供其他序列类型,您也可以自己编写(如在 “序列” 中讨论的)。您可以以多种方式操作序列,如在 “序列操作” 中讨论的。

可迭代对象

Python 中一个抽象概念是 可迭代对象,捕捉序列的迭代行为,详细讨论在 “for 语句” 中涵盖。所有序列都是可迭代的:每当我们说可以使用可迭代对象时,都可以使用序列(例如列表)。

此外,当我们说可以使用可迭代对象时,通常意味着一个 有界 的可迭代对象:一个最终停止产生项的可迭代对象。一般来说,序列是有界的。可迭代对象可以是无界的,但如果您尝试使用无界可迭代对象而不采取特殊预防措施,可能会导致程序永远不会终止,或者耗尽所有可用内存。

字符串

Python 有两种内置的字符串类型,str 和 bytes。⁴ str 对象是一系列字符,用于存储和表示基于文本的信息。bytes 对象存储和表示任意二进制字节序列。Python 中的这两种字符串都是 不可变的:当您对字符串执行操作时,总是产生同一类型的新字符串对象,而不是变异现有字符串。字符串对象提供许多方法,如在 “字符串对象的方法” 中详细讨论的。

字符串字面量可以用引号或三引号括起来。一个被引号括起来的字符串是一个在匹配的引号之间的零个或多个字符序列,单引号(')或双引号(")。例如:

'This is a literal string'
"This is another string"

两种不同的引号功能相同;拥有这两者可以让你在指定的字符串中包含另一种引号,而无需使用反斜杠字符()进行转义:

'I\'m a Python fanatic'     *`# You can escape a quote`*
"I'm a Python fanatic"      *`# This way may be more readable`*

许多(但远非所有)有关此主题的风格指南建议,当选择无关紧要时,使用单引号。流行的代码格式化工具 black 更喜欢使用双引号;这个选择足够有争议,足以成为“分支” blue 的主要灵感,其与 black 的主要区别在于更喜欢使用单引号,就像本书的大多数作者一样。

为了让字符串字面量跨越多个物理行,你可以在行的最后一个字符使用 \,以表示下一行是续行:

'A not very long string \ that spans two lines'       *`# Comment not allowed on previous line`*

你也可以在字符串中嵌入换行符,使其包含两行而不仅仅是一行:

'A not very long string\n\ that prints on two lines'   *`# Comment not allowed on previous line`*

然而,更好的方法是使用三引号字符串,由匹配的三重引号字符(''',或者更好的是根据PEP 8规定的""")包围。在三引号字符串字面量中,字面量中的换行符保持为结果字符串对象中的换行符:

*`"""`**`An even bigger`*
*`string that spans three lines"""`*              *`# Comments not allowed on previous lines`*

你可以用一个转义换行符开始一个三引号字面量,以避免字面量字符串内容的第一行与其余内容的缩进级别不同。例如:

the_text = """\ First line
Second line
"""      *`# The same as "First line\nSecond line\n" but more readable`*

唯一不能成为三引号字符串字面量一部分的字符是未转义的反斜杠,而单引号字符串字面量不能包含未转义的反斜杠,也不能包含封闭它的引号字符。反斜杠字符开始一个转义序列,它让你在任意类型的字符串字面量中引入任何字符。请参见表 3-3 以获取 Python 所有字符串转义序列的列表。

表 3-3. 字符串转义序列

序列 含义 ASCII / ISO 代码
** 忽略行尾
\ 反斜杠 0x5c
' 单引号 0x27
" 双引号 0x22
\a 响铃 0x07
\b 退格符 0x08
\f 换页符 0x0c
\n 换行符 0x0a
\r 回车符 0x0d
\t 制表符 0x09
\v 垂直制表符 0x0b
\ DDD 八进制值 DDD 如给定
\x XX 十六进制值 XX 如给定
\N{name} Unicode 字符 如给定
\ o 任何其他字符 o:一个由两字符组成的字符串 0x5c + 如给定

字符串文字的一种变体是原始字符串文字。语法与带引号或三重引号字符串文字相同,只是在开头引号之前立即加上 r 或 R。在原始字符串文字中,转义序列不会像在表 3-3 中那样被解释,而是直接复制到字符串中,包括反斜杠和换行符。原始字符串文字的语法对包含许多反斜杠的字符串特别方便,特别是正则表达式模式(参见“模式字符串语法”)和 Windows 绝对文件名(这些文件名使用反斜杠作为目录分隔符)。原始字符串文字不能以奇数个反斜杠结尾:最后一个反斜杠将被视为转义终止引号。

原始字符串和三重引号字符串文字并非不同类型

原始字符串和三重引号字符串文字并非与其他字符串类型不同;它们只是通常的两种字符串类型(bytes 和 str)的文字的替代语法。

在 str 文字中,您可以使用\u 后跟四个十六进制数字,或者\U 后跟八个十六进制数字来表示 Unicode 字符;您还可以使用在表 3-3 中列出的转义序列。str 文字还可以使用转义序列\N{name}来包含 Unicode 字符,其中name是标准的Unicode 名称。例如,\N表示一个 Unicode 版权符号字符(©)。

格式化字符串文字(通常称为f-strings)允许您将格式化表达式注入到您的字符串“文字”中,因此它们不再是常量,而是在执行时进行评估。格式化过程在“字符串格式化”中描述。从纯语法的角度来看,这些新的文字可以被视为另一种字符串文字。

任何类型的多个字符串文字(引号、三重引号、原始、bytes、格式化)可以相邻,中间可以有可选的空白(只要不混合包含文本和字节的字符串)。编译器将这些相邻的字符串文字连接成一个单一的字符串对象。以这种方式编写长字符串文字使您可以在多个物理行上可读地呈现它,并且可以在字符串的各个部分插入评论。例如:

marypop = ('supercali'       *`# '(' begins logical line,`*
           'fragilistic'     *`# indentation is ignored`*
           'expialidocious') *`# until closing ')'`*

分配给 marypop 的字符串是一个包含 34 个字符的单词。

bytes 对象

bytes 对象是从 0 到 255 的整数的有序序列。通常在从二进制源(例如文件、套接字或网络资源)读取数据或写入数据时遇到 bytes 对象。

可以从整数列表或字符字符串初始化 bytes 对象。bytes 文字具有与 str 文字相同的语法,前缀为 'b':

b'abc'
bytes([97, 98, 99])           *`# Same as the previous line`*
rb'\ = solidus'               *`# A raw bytes literal, containing a '\'`*

要将字节对象转换为字符串,使用 bytes.decode 方法。要将字符串对象转换为字节对象,使用 str.encode 方法,如第九章中详细描述的那样。

字节数组对象

字节数组是一个从 0 到 255 的可变有序整数序列;像字节对象一样,你可以用整数或字符的序列构造它。实际上,除了可变性外,它与字节对象完全相同。由于它们是可变的,字节数组对象支持修改数组中字节值的方法和运算符:

ba = bytearray([97, 98, 99])  *`# Like bytes, can take a sequence of ints`*
ba[1] = 97                    *`# Unlike bytes, contents can be modified`*
print(ba.decode())            *`# Prints 'aac'`*

第九章包含有关创建和使用字节数组对象的其他材料。

元组

一个元组是一个不可变的有序项序列。元组的项是任意对象,可以是不同类型的。你可以使用可变对象(如列表)作为元组的项,但通常最好避免这样做。

为了表示元组,使用一系列用逗号(,)分隔的表达式(元组的项);⁵如果每个项都是字面值,整个结构就是一个元组字面值。你可以选择在最后一项后面加一个多余的逗号。你可以用括号将元组项分组,但只有在逗号在其他情况下会有另一种含义(例如,在函数调用中)或者表示空或嵌套元组时才需要括号。恰好有两个项的元组也称为。要创建一个只有一个项的元组,在表达式末尾加一个逗号。要表示一个空元组,使用一个空的括号对。以下是一些元组字面值的例子,第二个例子使用了可选的括号:

100, 200, 300        *`# Tuple with three items`*
(3.14,)              *`# Tuple with one item, needs trailing comma`*
()                   *`# Empty tuple (parentheses NOT optional)`*

你也可以调用内置类型元组来创建一个元组。例如:

tuple('wow')

这将构建一个等同于元组字面值所示的元组:

('w', 'o', 'w')

tuple()没有参数时创建并返回一个空元组,就像()一样。当x可迭代时,tuple(x)返回一个与x中的项相同的元组。

列表

一个列表是一个可变的有序项序列。列表的项是任意对象,可以是不同类型的。要表示一个列表,使用方括号([])内的逗号(,)分隔的表达式(列表的项);⁶如果每个项都是字面值,整个结构就是一个列表字面值。你可以选择在最后一项后面加一个多余的逗号。要表示一个空列表,使用一个空的方括号对。以下是一些列表字面值的例子:

[42, 3.14, 'hello']  *`# List with three items`*
[100]                *`# List with one item`*
[]                   *`# Empty list`*

你也可以调用内置类型列表来创建一个列表。例如:

list('wow')

这将构建一个等同于列表字面值所示的列表:

['w', 'o', 'w']

list()没有参数时创建并返回一个空列表,就像[]一样。当x可迭代时,list(x)返回一个与x中的项相同的列表。

你还可以使用列表推导式构建列表,详见“列表推导式”。

集合

Python 有两种内置的集合类型,set 和 frozenset,用来表示任意顺序的唯一项目集合。集合中的项目可以是不同类型的,但它们都必须是可散列的(参见 表 8-2 中的 hash)。set 类型的实例是可变的,因此不可散列;frozenset 类型的实例是不可变的且可散列的。你不能有其项目为集合的集合,但可以有其项目为冻结集合的集合(或 frozenset)。集合和冻结集合是无序的。

要创建一个集合,可以调用没有参数的内置类型 set(这意味着一个空集)或一个可迭代的参数(这意味着集合的项目是可迭代对象的项目)。类似地,通过调用 frozenset 可以构建一个 frozenset。

或者,要表示一个(非冻结、非空)集合,使用一系列用逗号(,)分隔的表达式(集合的项目),这些表达式位于大括号()内;⁷ 如果每个项目都是字面值,则整个集合是一个集合字面量。可以在最后一个项目之后可选地放置冗余逗号。以下是一些示例集合(两个字面值,一个不是):

{42, 3.14, 'hello'}  *`# Literal for a set with three items`*
{100}                *`# Literal for a set with one item`*
set()                *`# Empty set - no literal for empty set`*
                     *`# {} is an empty dict!`*

你也可以使用集合推导式构建非冻结集合,如在 “Set comprehensions” 中讨论的那样。

注意,两个集合或冻结集合(或一个集合和一个冻结集合)可能比较相等,但由于它们是无序的,迭代它们可能以不同的顺序返回它们的内容。

字典

映射是一个由几乎任意值(称为)索引的任意对象集合。映射是可变的,并且像集合一样(但不像序列一样)(一定)有序。

Python 提供了一种内置的映射类型:字典类型 dict。库和扩展模块提供了其他映射类型,你也可以自己编写(如在 “Mappings” 中讨论的那样)。字典中的键可以是不同类型的,但它们必须是可散列的(参见 表 8-2 中的 hash)。字典中的值是任意对象,可以是任何类型。字典中的一个项是一个键/值对。你可以把字典看作是一个关联数组(在其他一些语言中称为“映射”、“哈希表”或“散列表”)。

要表示一个字典,可以使用一系列用冒号(:)分隔的表达式(字典的项)对,这些表达式位于大括号()内;⁹ 如果每个表达式都是字面值,则整个构造是一个字典字面量。可以在最后一个项目之后可选地放置冗余逗号。字典中的每个项都写成 key:value,其中 key 是给出项键的表达式,value 是给出项值的表达式。如果一个键的值在字典表达式中出现多次,则在生成的字典对象中只保留其中任意一个项目的值——字典不支持重复键。例如:

{1:2, 3:4, 1:5}  *`# The value of this dictionary is {1:5, 3:4}`*

要表示一个空字典,请使用一对空大括号。

这里是一些字典字面值:

{'x':42, 'y':3.14, 'z':7}    *`# Dictionary with three items, str keys`*
{1:2, 3:4}                   *`# Dictionary with two items, int keys`*
{1:'za', 'br':23}            *`# Dictionary with different key types`*
{}                           *`# Empty dictionary`*

你也可以调用内置类型 dict 以一种不那么简洁但有时更易读的方式创建字典。例如,前面片段中的字典也可以写成:

dict(x=42, y=3.14, z=7)      *`# Dictionary with three items, str keys`*
dict([(1, 2), (3, 4)])       *`# Dictionary with two items, int keys`*
dict([(1,'za'), ('br',23)])  *`# Dictionary with different key types`*
dict()                       *`# Empty dictionary`*

dict()没有参数时创建并返回一个空字典,就像一样。当 dict 的参数x是一个映射时,dict 返回一个新的字典对象,其键和值与x相同。当x是可迭代时,x中的项必须是成对的,dict(x)返回一个其项(键/值对)与x中项相同的字典。如果在x中一个键值出现多次,则结果字典中只保留最后一个x中具有该键值的项。

当你调用 dict 时,除了或者代替位置参数x,你可以传递命名参数,每个参数的语法为name=value,其中name是用作项键的标识符,value是给出项值的表达式。当你调用 dict 并同时传递一个位置参数和一个或多个命名参数时,如果一个键同时出现在位置参数和命名参数中,Python 将将该键关联到命名参数的值(即命名参数“胜出”)。

您可以使用**操作符将字典的内容解包到另一个字典中。

d1 = {'a':1, 'x': 0}
d2 = {'c': 2, 'x': 5}
d3 = {**d1, **d2}  # result is {'a':1, 'x': 5, 'c': 2}

3.9+ 自 Python 3.9 起,可以使用|运算符执行相同的操作。

d4 = d1 | d2  # same result as d3

你也可以通过调用 dict.fromkeys 来创建一个字典。第一个参数是一个可迭代对象,其项成为字典的键;第二个参数是对应于每个键的值(所有键最初映射到相同的值)。如果省略第二个参数,则默认为None。例如:

dict.fromkeys('hello', 2)    *`# Same as {'h':2, 'e':2, 'l':2, 'o':2}`*
dict.fromkeys([1, 2, 3])     *`# Same as {1:`**`None`**`, 2:`**`None`**`, 3:`**`None`**`}`*

您还可以使用字典推导式来构建字典,如在“字典推导式”中讨论的那样。

当比较两个字典是否相等时,如果它们具有相同的键和对应的值,则它们将被视为相等,即使键的顺序不同。

None

内置的None表示一个空对象。None没有方法或其他属性。当你需要一个引用但不关心引用的对象时,或者需要指示没有对象时,你可以使用None作为占位符。函数作为它们的结果返回None,除非它们具有返回其他值的特定return语句。None是可哈希的,可以用作字典的键。

省略号(...)

省略号(...)在 Python 中被写作三个不加间隔的句点,是在数值应用中使用的特殊对象¹⁰,或者在None是有效输入时作为None的替代。例如,要初始化一个可能接受None作为合法值的字典,可以用...作为指示“没有提供值,甚至不是None”。省略号是可哈希的,因此可以用作字典的键:

tally = dict.fromkeys(['A', 'B', `None`, ...], 0)

可调用对象

在 Python 中,可调用类型是那些支持函数调用操作的实例(见“调用函数”)。函数是可调用的。Python 提供了许多内置函数(见“内置函数”)并支持用户定义的函数(见“定义函数:def 语句”)。生成器也是可调用的(见“生成器”)。

类型本身也是可调用的,正如我们对内置类型 dict、list、set 和 tuple 所见。 (请参阅“内置类型”以获取完整的内置类型列表。)正如我们在“Python 类”中讨论的那样,对象(用户定义的类型)也是可调用的。通常调用一个类型会创建并返回该类型的新实例。

其他可调用对象包括方法,即绑定为类属性的函数,以及提供名为 call 的特殊方法的类的实例。

布尔值

在 Python 中,任何数据值都可以用作真值或假值。 任何非零数或非空容器(例如字符串,元组,列表,集合或字典)为真。 零(任何数值类型的 0),None和空容器为假。 您可能会看到“真值”和“假值”这两个术语用来表示评估为真或假的值。

谨慎使用浮点数作为真值

要小心使用浮点数作为真值:这就像将该数字与零进行精确比较,而浮点数几乎永远不应该进行精确比较。

内置类型 bool 是 int 的子类。 类型 bool 的唯二值为TrueFalse,它们的字符串表示分别为**'True''False'**,但数值上分别为 1 和 0。 几个内置函数返回 bool 结果,如比较运算符。

您可以使用任何x作为参数调用 bool(x)。 当x为真时结果为True,当x为假时结果为False。 良好的 Python 风格不是在这些调用多余时使用它们,因为它们通常是这样的:始终编写if x:,从不使用if bool(x):,if x is True:,if x == True:或if bool(x) == True:。 但是,您可以使用 bool(x)来计算序列中真实项目的数量。 例如:

`def` count_trues(*`seq`*):
    `return` sum(bool(x) `for` x `in` seq)

在这个例子中,bool 调用确保seq的每个项被计为 0(如果为假)或 1(如果为真),因此 count_trues 比 sum(seq)更通用。

当我们说“表达式是真时”,我们指的是 bool(表达式)会返回True。 如前所述,这也被称为“表达式真值”(另一种可能性是“表达式假值”)。

变量和其他引用

Python 程序通过引用访问数据值。引用是指向值(对象)的“名称”。引用以变量、属性和项的形式存在。在 Python 中,变量或其他引用没有固有类型。在程序执行过程中,给定时间点上引用绑定到的对象始终有一个类型,但给定的引用可能绑定到程序执行过程中各种类型的对象。

变量

在 Python 中,没有“声明”。变量的存在始于绑定变量的语句(换句话说,设置一个名称来保存对某个对象的引用)。你也可以解绑一个变量,重置名称,使其不再保存引用。赋值语句是绑定变量和其他引用的常见方式。del语句解除变量引用,尽管这种操作很少见。

绑定已经绑定的引用也称为重新绑定。每当提到绑定时,我们隐含地包括重新绑定(除非我们明确排除它)。重新绑定或解绑引用对引用的对象没有影响,除非没有任何引用指向它时对象会消失。清理没有引用的对象称为垃圾收集

你可以用任何标识符命名一个变量,除了 Python 关键字中的 30 多个(参见“关键字”)。变量可以是全局的或局部的。全局变量是模块对象的一个属性(参见第七章)。局部变量存在于函数的局部命名空间中(参见“命名空间”)。

对象属性和项

一个对象属性和项的主要区别在于访问它们的语法。要表示一个对象的属性,使用对对象的引用,后面跟一个句点(.),后面跟一个称为属性名的标识符。例如,x.y指的是绑定到名称x的对象的一个属性;具体来说,该属性的名称是'y'。

要表示对象的,使用对对象的引用,后跟方括号内的表达式[]。方括号内的表达式称为项的索引,对象称为项的容器。例如,x[y]指的是绑定到名称y的键或索引处的项,位于绑定到名称x的容器对象内。

可调用的属性也称为方法。Python 不像其他一些语言那样对可调用和不可调用的属性做出明确区分。所有关于属性的一般规则也适用于可调用的属性(方法)。

访问不存在的引用

常见的编程错误是访问不存在的引用。例如,变量可能未绑定,或者属性名或项索引可能对所应用的对象无效。Python 编译器在分析和编译源代码时仅诊断语法错误。编译不会诊断语义错误,例如尝试访问未绑定的属性、项或变量。Python 仅在错误代码执行时诊断语义错误,即 在运行时。尝试 Python 语义错误操作会引发异常(参见第六章)。访问不存在的变量、属性或项——与其他语义错误一样——会引发异常。

赋值语句

赋值语句可以是普通的或增强的。对变量进行普通赋值(例如 name = value)是创建新变量或重新绑定现有变量到新值的方式。对对象属性进行普通赋值(例如 x.attr = value)是请求对象 x 创建或重新绑定名为 'attr' 的属性。对容器中的项目进行普通赋值(例如 x[k] = value)是请求容器 x 创建或重新绑定索引或键 k 的项目。

增强赋值(例如 name += value)本身不能创建新引用。增强赋值可以重新绑定变量,请求对象重新绑定其现有属性或项,或请求目标对象修改自身。当您向对象发出任何请求时,取决于对象是否以及如何响应请求,以及是否引发异常。

普通赋值

最简单形式的普通赋值语句的语法是:

*target* = *expression*

目标称为左侧(LHS),表达式为右侧(RHS)。当执行赋值时,Python 评估 RHS 表达式,然后将表达式的值绑定到 LHS 目标。绑定永远不依赖于值的类型。特别是,Python 不像其他一些语言那样对可调用和不可调用对象有明显区分,因此您可以像绑定数字、字符串、列表等一样绑定函数、方法、类型和其他可调用对象。这是函数和其他可调用对象作为 一等公民对象 的一部分。

绑定的细节取决于目标的种类。赋值语句中的目标可以是标识符、属性引用、索引或切片,其中:

一个标识符

是一个变量名。将值分配给标识符会将变量绑定到此名称。

一个属性引用

具有形式 obj.nameobj 是任意表达式,name 是标识符,称为对象的 属性名。将值分配给属性引用会请求对象 obj 绑定其名为 'name' 的属性。

一个索引

具有obj[expr]的语法。objexpr都是任意表达式。对索引进行赋值会要求容器obj绑定其由expr的值指示的项,也称为容器中项的索引或键(索引是应用于容器的一个索引)。

切片操作

具有obj[start:stop]或obj[start:stop:stride]的语法。objstartstopstride都是任意表达式。startstopstride都是可选的(即obj[:stop:]和obj[:stop]也是语法上正确的切片,每个切片都等同于obj[None:stop:None])。对切片进行赋值会要求容器obj绑定或解绑其一些项。将这样的切片赋值给obj[start:stop:stride]等同于对索引obj[slice(start, stop, stride)]进行赋值。参见 Python 的内置类型 slice(表 8-1),其实例表示切片(切片是应用于容器的一个切片)。

当我们讨论列表操作时,我们将回到索引和切片目标的问题,详见“修改列表”,以及字典操作时,详见“字典索引”。

当赋值的目标是标识符时,赋值语句指定了变量的绑定。这是绝对不会被禁止的:当您请求时,它会发生。在所有其他情况下,赋值语句表示请求对象绑定一个或多个其属性或项。对象可能会拒绝创建或重新绑定一些(或全部)属性或项,在您尝试禁止创建或重新绑定时引发异常(另请参阅表 4-1 中的 setattr 和“容器方法”中的 setitem)。

简单赋值语句可以使用多个目标和等号(=)。例如:

a = b = c = 0

将变量 a、b 和 c 绑定到相同的值 0。每次执行该语句时,右侧表达式只计算一次,无论语句中有多少目标。每个目标从左到右绑定到由表达式返回的一个对象,就像依次执行多个简单赋值一样。

简单赋值语句中的目标可以是两个或多个引用,用逗号分隔,可选地用括号或方括号括起来。例如:

a, b, c = x

此语句要求 x 是一个具有三个项目的可迭代对象,并将 a 绑定到第一个项目,b 绑定到第二个项目,c 绑定到第三个项目。这种赋值称为解包赋值。右侧表达式必须是一个具有与目标引用数量相同数量的项目的可迭代对象;否则,Python 会引发异常。Python 将目标中的每个引用绑定到右侧表达式中相应的项目。例如,您可以使用解包赋值交换引用:

a, b = b, a

这个赋值语句重新将名字 a 绑定到名字 b 曾经绑定的内容上,反之亦然。拆包赋值的多个目标中只能有一个目标前面有*。如果有,那个星号目标将绑定到所有未分配给其他目标的项目(如果有的话)的列表上。例如,当 x 是一个列表时,这样:

first, *middle, last = x

和这个(但更简洁,更清晰,更一般,更快)是一样的:

first, middle, last = x[0], x[1:-1], x[-1]

这些形式中的每一个都要求 x 至少有两个项目。这个特性被称为扩展拆包

增强赋值

增强赋值(有时被称为就地赋值)与普通赋值的不同之处在于,目标和表达式之间不是等号(=),而是一个增强运算符,它是一个二元运算符后跟=。增强运算符包括+=、-=、*=、/=、//=、%=、**=、|=、>>=、<<=、&=、^= 和@=。增强赋值只能在 LHS 上有一个目标;增强赋值不支持多个目标。

在增强赋值中,就像在普通赋值中一样,Python 首先评估 RHS 表达式。然后,当 LHS 引用具有适当的就地版本运算符的对象时,Python 调用该方法并将 RHS 值作为其参数(由方法适当地修改 LHS 对象并返回修改后的对象;“特殊方法”介绍了特殊方法)。当 LHS 对象没有适用的就地特殊方法时,Python 在 LHS 和 RHS 对象上使用相应的二元运算符,然后重新绑定目标到结果上。例如,x += y 就像 x = x._iadd(y) 当x具有特殊方法 iadd 用于“就地加法”时;否则,x += y 就像 x = x + y

增强赋值从不创建其目标引用;在增强赋值执行时,目标必须已经被绑定。增强赋值可以将目标引用重新绑定到一个新对象,或者修改目标引用已经绑定的相同对象。相比之下,普通赋值可以创建或重新绑定 LHS 目标引用,但它永远不会修改目标引用先前绑定的对象(如果有的话)。这里对象和对象引用之间的区别至关重要。例如,x = x + y 从不修改x最初绑定的对象(如果有的话)。相反,它将x重新绑定到引用一个新对象的地方。x += y 相反,修改了名称x绑定的对象,当该对象具有特殊方法 iadd 时;否则,x += y 就像x = x + y 一样,重新绑定x到一个新对象。

del 语句

尽管它的名字是这样,一个del语句会解除引用——它不会直接删除对象。对象的删除可能会在没有对对象的更多引用存在时通过垃圾回收自动发生。

del 语句由关键字 del 开始,后跟一个或多个由逗号(,)分隔的目标引用。每个目标可以是变量、属性引用、索引或切片,就像赋值语句一样,在 del 执行时必须绑定。当 del 的目标是标识符时,del 语句意味着取消绑定该变量。如果标识符已绑定,则取消绑定是可以的;一旦请求,它就会发生。

在所有其他情况下,del 语句指定了向对象请求解绑一个或多个其属性或项。对象可能会拒绝解绑一些(或全部)属性或项,如果尝试不允许的解绑,则会引发异常(参见 “通用特殊方法” 中的 delattr 和 “容器方法” 中的 delitem)。解绑切片通常具有将空序列分配给该切片的相同效果,但由容器对象实现此等价性。

容器还允许 del 导致副作用。例如,假设 del C[2] 成功,当 C 是字典时,这将使得对 C[2] 的未来引用无效(引发 KeyError),直到你再次对 C[2] 进行赋值;但是当 C 是列表时,del C[2] 意味着 C 的每个后续项“向左移动一位”——因此,如果 C 足够长,对 C[2] 的未来引用仍然有效,但它们表示的是 del 之前所用 C[3] 的不同项(通常是你在 del 语句之前用 C[3] 来引用的项)。

表达式和运算符

表达式是代码的“短语”,Python 评估它以生成一个值。最简单的表达式是文字和标识符。你可以通过使用 表 3-4 中列出的运算符和/或分隔符连接子表达式来构建其他表达式。该表按降序列出优先级,优先级高于低。并排列出的运算符具有相同的优先级。第三列列出了运算符的结合性:L(从左到右)、R(从右到左)或 NA(非关联)。

表 3-4. 表达式中的运算符优先级

运算符 描述 结合性
{ key : expr, ... } 字典创建 NA
{ expr, ... } 集合创建 NA
[ expr, ... ] 列表创建 NA
( expr, ... ) 元组创建(推荐使用括号,但不一定要求;至少需要一个逗号),或者只是括号 NA
f ( expr, ... ) 函数调用 L
x [ index: index: step ] 切片 L
x [ index ] 索引 L
x . attr 属性引用 L
x ** y 指数运算(xy 次方) R
~ x, + x, - x 按位非、一元加和减 NA
x * y, x @ y, x / y, x // y, x % y 乘法,矩阵乘法,除法,地板除法,取余 L
x + y, x - y 加法,减法 L
x << y, x >> y 左移,右移 L
x & y 按位与 L
x ^ y 按位异或 L
x | y 按位或 L
x < y, xy, x > y, x >= y, x != y, x == y 比较运算符(小于,小于等于,大于,大于等于,不等于,等于) NA
x is y, x is not y 身份测试 NA
x in y, x not in y 成员测试 NA
not x 布尔非 NA
x and y 布尔与 L
x or y 布尔或 L
x if expr else y 条件表达式(或三元操作符) NA
lambda arg, ...: expr 匿名简单函数 NA
( ident := expr ) 赋值表达式(建议使用括号,但不总是必需) NA

在本表中,exprkeyfindexxy 表示任意表达式,而 attrargident 表示任何标识符。记号 , ... 表示逗号连接零个或多个重复项;在这种情况下,尾随逗号是可选且无害的。

比较链

你可以链式比较,隐含逻辑and。例如:

*`a`* < *`b`* <= *`c`* < *`d`*

其中 abcd 是任意表达式,在没有副作用的情况下,其值与以下表达式相同:

*`a`* < *`b`* `and` *`b`* <= *`c`* `and` *`c`* < *`d`*

这种链式形式更易读,并且每个子表达式最多评估一次。

短路运算符

andor 操作符会 短路 其操作数的评估:仅当需要其值以获取整个 andor 操作的真值时,才会评估右操作数。

换句话说,x and y 首先评估 x。当 x 为假时,结果为 x;否则,结果为 y。类似地,x or y 首先评估 x。当 x 为真时,结果为 x;否则,结果为 y

andor 不强制其结果为 TrueFalse,而是返回它们的操作数之一。这让你可以更广泛地使用这些操作符,不仅限于布尔上下文。由于其短路语义,andor 与其他操作符不同,其他操作符在执行操作之前会完全评估所有操作数。andor 让左操作数作为右操作数的 守卫

条件操作符

另一个短路运算符是条件操作符 if/else

*when_true* if *condition* else *when_false*

when_truewhen_falsecondition 都是任意表达式。首先评估 condition。当 condition 为真时,结果为 when_true;否则,结果为 when_false。只有 when_truewhen_false 中的一个会评估,取决于 condition 的真值。

在条件运算符中子表达式的顺序可能有点令人困惑。建议的风格是始终将整个表达式放在括号中。

赋值表达式

3.8+ 你可以使用 := 运算符结合表达式的评估和其结果的赋值。有几种常见情况下这是有用的。

在 if/elif 语句中的 :=

可以使用 := 折叠分配值然后检查它的代码:

re_match = re.match(r'Name: (\S)', input_string)
`if` re_match:
    print(re_match.groups(1))

*`# collapsed version using :=`*
`if` (re_match := re.match(r'Name: (\S)', input_string)):
    print(re_match.groups(1))

在编写一系列 if/elif 块时特别有帮助(你会在 Chapter 10 中找到一个更详细的例子)。

在 while 语句中的 :=

使用 := 简化以变量作为 while 条件的代码。考虑以下代码,它使用某个函数 get_next_value 返回的一系列值进行操作,当没有更多值需要处理时,返回 None

current_value = get_next_value()
`while` current_value `is` `not` `None`:
    `if` `not` filter_condition(current_value):
        `continue`   *`# BUG! Current_value is not advanced to next`*
 *`# ... do some work with current_value ...`*
    current_value = get_next_value()

这段代码有几个问题。首先,有重复调用 get_next_value,当 get_next_value 发生变化时,会带来额外的维护成本。但更严重的是,在添加了早期退出过滤器后会出现错误:continue 语句直接跳回 while 语句而不前进到下一个值,创建了一个无限循环。

当我们使用 := 将赋值合并到 while 语句本身时,我们解决了重复问题,并且调用 continue 不会导致无限循环:

`while` (current_value := get_next_value()) `is` `not` `None`:
    `if` `not` filter_condition(current_value):
        `continue`   *`# no bug, current_value advances in while statement`*
 *`# ... do some work with current_value ...`*

在列表推导式的过滤器中的 :=

列表推导式可以根据转换后的值过滤掉某些项目,但必须使用 := 仅进行一次转换。在这个例子中,一个将 strs 转换为 ints 的函数对无效值返回 None。没有 :=,列表推导式必须两次调用 safe_int,一次检查 None,然后再次将实际的 int 值添加到列表中:

`def` safe_int(s):
    `try`:
        `return` int(s)
    `except` Exception:
        `return` `None`

input_strings = ['1','2','a','11']

valid_int_strings = [safe_int(s) `for` s `in` input_strings 
                     `if` safe_int(s) `is` `not` `None`]

如果在列表推导式的条件部分使用赋值表达式,对于输入字符串中的每个值,safe_int 只会被调用一次:

valid_int_strings = [int_s `for` s `in` input_strings
                     `if` (int_s := safe_int(s)) `is` `not` `None`]

你可以在此功能的原始 PEP 中找到更多示例,PEP 572

数值操作

Python 提供了通常的数值操作,正如我们刚刚在 Table 3-4 中看到的那样。数字是不可变对象:当你在数字对象上执行操作时,总是产生新的对象,而不是修改现有的对象。你可以作为只读属性访问复杂对象 z 的部分,例如 *z.*real 和 *z.*imag。试图重新绑定这些属性会引发异常。

数字的可选 +- 符号,以及将浮点数文字与虚部连接以制作复数的 +- 不是文字语法的一部分。它们是普通的运算符,受正常运算符优先规则的约束(见表 3-4)。例如,-2 ** 2 计算结果为 -4:乘方比一元减号具有更高的优先级,因此整个表达式解析为 -(2 ** 2),而不是 (-2) ** 2。(再次强调,建议使用括号,以避免混淆代码读者。)

数值转换

您可以在 Python 内置类型(整数、浮点数和复数)之间执行算术操作和比较。如果操作数的类型不同,Python 将操作数转换为“更宽”的类型。¹⁴ 内置数值类型按从最窄到最宽的顺序是:int、float 和 complex。您可以通过将非复数数值参数传递给这些类型中的任何一个来请求显式转换。int 丢弃其参数的小数部分(如果有的话)(例如,int(9.8)9)。您还可以使用两个数值参数调用 complex,给出实部和虚部。您不能以这种方式将复数转换为另一种数值类型,因为没有单一明确的方法将复数转换为例如浮点数。

您还可以使用适当的数值文本语法调用每个内置数值类型,并带有字符串参数,具有小的扩展功能:参数字符串可以包含前导和/或尾随空格,可以以符号开头,并且对于复数数字,可以加和减去实部和虚部。int 还可以用两个参数调用:第一个是要转换的字符串,第二个是基数,一个介于 2 和 36 之间的整数,用作转换的基数(例如,int('101', 2) 返回 5,即在二进制中的'101'的值)。对于大于 10 的基数,ASCII 字母表中的适当子集(无论是小写还是大写)是所需的额外“数字”。¹⁵

算术运算

Python 中的算术操作通常表现得非常明显,除了除法和乘方可能有例外。

除法

当除法运算符 /, //, 或 % 的右操作数为 0 时,Python 在运行时会引发异常。否则,除法运算符 / 执行除法,返回两个操作数的浮点数结果(如果任一操作数是复数,则返回复数结果)。相比之下,除法运算符 // 执行地板除法,即返回整数结果(转换为更广泛操作数的相同类型),该结果是小于或等于真除法结果的最大整数(忽略余数,如果有的话);例如,5.0 // 2 = 2.0(而不是 2)。运算符 % 返回(地板)除法的余数,即整数 x // y * y + x % y == x 的整数。

–x // y 不同于 int(–x / y)

请注意,// 不是截断或整数除法的形式;这仅适用于操作数具有相同符号的情况。当操作数的符号不同时,最接近真实除法结果且小于等于它的整数实际上会比真实除法结果更负(例如,-5 / 2 返回 -2.5,因此 -5 // 2 返回 -3,而不是 -2)。

内置的 divmod 函数接受两个数值参数,并返回一个包含商和余数的对,因此你不必同时使用 // 得到商和 % 得到余数。¹⁶

指数运算

a 小于零且 b 是具有非零小数部分的浮点数时,“乘方”运算将返回一个复数。内置的 pow(a, b) 函数返回与 a ** b 相同的结果。带有三个参数的 pow(a, b, c) 返回与 (a ** b) % c 相同的结果,但有时可能更快。请注意,与其他算术运算不同,乘方运算从右向左求值:换句话说,a ** b ** c 的求值结果为 a ** (b ** c)。

比较

所有对象,包括数字,都可以进行相等性(==)和不等性(!=)比较。需要顺序比较(<、<=、>、>=)的比较可以用于任何两个数字,除非其中一个操作数是复数,在这种情况下会在运行时引发异常。所有这些操作符都返回布尔值(TrueFalse)。但是,在比较浮点数是否相等时要小心,如 第十六章 和 关于浮点数算术的在线教程 中所讨论的。

整数的位操作

整数可以被解释为比特字符串,并与 表格 3-4 中显示的位运算符一起使用。位运算符的优先级低于算术运算符。正整数在概念上通过左侧无限长度的比特字符串扩展,每个比特为 0。负整数则采用二进制补码表示,概念上通过左侧无限长度的比特字符串扩展,每个比特为 1。

序列操作

Python 支持多种操作,适用于所有序列,包括字符串、列表和元组。一些序列操作适用于所有容器(包括非序列的集合和字典);一些适用于所有可迭代对象(即“任何你可以循环遍历的对象”——所有容器,无论它们是否是序列,都是可迭代的,包括一些不是容器的对象,如文件,在 “The io Module” 中介绍,以及生成器,在 “Generators” 中介绍)。在接下来的内容中,我们非常精确地使用术语 sequencecontaineriterable,以准确指示每个类别适用的操作。

一般序列

序列是有序容器,其项可通过索引和切片访问。

内置的 len 函数接受任何容器作为参数,并返回容器中的项目数。

内置的 min 和 max 函数接受一个参数,该参数是可比较的项的可迭代对象,并分别返回最小和最大的项。您还可以使用多个参数调用 min 和 max 函数,此时它们将分别返回最小和最大的参数。

min 和 max 还接受两个仅限关键字的可选参数:key,一个应用于每个项目的可调用函数(然后比较将根据可调用函数的结果而不是项目本身进行);以及 default,当可迭代对象为空时返回的值(当可迭代对象为空且您未提供默认参数时,函数会引发 ValueError 异常)。例如,max('who', 'why', 'what', key=len) 返回 'what'。

内置的 sum 函数接受一个参数,该参数是数字的可迭代对象,并返回数字的总和。

序列转换

不同序列类型之间没有隐式转换。您可以使用内置的 tuple 和 list 函数并传入单个参数(任何可迭代对象),以获取与参数中相同顺序的相同项目的新实例。

连接和重复

您可以使用+运算符连接相同类型的序列。您可以使用整数n将序列S乘以运算符。S**nSn个副本的连接。当n ⇐ 0 时,S * n 是与S相同类型的空序列。

成员测试

x in S 运算符用于检查对象x是否等于序列(或其他类型的容器或可迭代对象)S中的任何项。当匹配时返回True,否则返回Falsex not in S 运算符等效于not (x in S)。对于字典,x in S 用于测试x是否作为键存在。在字符串的特定情况下,x in S 可能匹配多个预期外的项;在这种情况下,x in S 测试的是x是否等于S的任何子字符串,而不仅仅是单个字符。

索引序列

要表示序列S的第n个项,请使用索引:S[n]。索引是从零开始的:S的第一个项是S[0]。如果SL个项,则索引n可以是 0、1……直到L-1,但不能更大。n也可以是-1、-2……直到-L,但不能更小。负n(例如,-1)表示S中与L+n(例如,L-1)相同的项。换句话说,S[-1],如S[L-1],是S的最后一个元素,S[-2]是倒数第二个元素,依此类推。例如:

x = [10, 20, 30, 40]
x[1]                  *`# 20`*
x[-1]                 *`# 40`*

使用大于等于L或小于等于-L的索引会引发异常。对具有无效索引的项进行赋值也会引发异常。您可以向列表添加元素,但要这样做,您需要对切片进行赋值,而不是对项进行赋值,我们将很快讨论这一点。

对序列进行切片

要指示 S 的子序列,可以使用切片,其语法为 S[i:j],其中 ij 是整数。S[i:j] 是从第 i 个项(包括)到第 j 个项(不包括)的 S 的子序列(在 Python 中,范围始终包括下限且不包括上限)。当 j 小于或等于 ii 大于或等于 LS 的长度)时,切片是空子序列。当 i 等于 0 时,可以省略 i,使切片从 S 的开头开始。当 j 大于或等于 L 时,可以省略 j,使切片延伸到 S 的末尾。可以同时省略两个索引,以表示整个序列的浅拷贝:S[:]。任何一个或两个索引都可能为负数。以下是一些示例:

x = [10, 20, 30, 40]
x[1:3]                   *`# [20, 30]`*
x[1:]                    *`# [20, 30, 40]`*
x[:2]                    *`# [10, 20]`*

切片中的负索引 n 表示与 S 中的 L+n 相同的位置,就像在索引中一样。大于或等于 L 的索引表示 S 的末尾,而小于或等于 -L 的负索引表示 S 的开头。

切片可以使用扩展语法 S[i:j:k]。k 是切片的步长,表示连续索引之间的距离。S[i:j] 相当于 S[i:j:1],S[2] 是包含 S 中所有偶数索引项的子序列,S[-1] 是切片,也以“火星笑脸”闻名,包含与 S 相同的项,但顺序相反。使用负步长时,为了得到非空切片,第二个(“停止”)索引必须小于第一个(“开始”)索引——这与步长为正时必须满足的条件相反。步长为 0 会引发异常。以下是一些示例:

>>> y = list(range(10))  *`# values from 0-9`*
>>> y[-5:]               *`# last five items`*
[5, 6, 7, 8, 9]
>>> y[::2]               *`# every other item`*
[0, 2, 4, 6, 8]
>>> y[10:0:-2]           *`# every other item, in reverse order`*
[9, 7, 5, 3, 1]
>>> y[:0:-2]             *`# every other item, in reverse order (simpler)`*
[9, 7, 5, 3, 1]
>>> y[::-2]              *`# every other item, in reverse order (best)`*
[9, 7, 5, 3, 1]

字符串

字符串对象(包括 str 和 bytes)是不可变的:试图重新绑定或删除字符串的项或切片会引发异常。(Python 还有一种内置类型是可变的但在其他方面等同于 bytes 的:bytearray(参见“bytearray objects”)。文本字符串的项(字符串中的每个字符)本身也是文本字符串,每个长度为 1 — Python 没有“单字符”的特殊数据类型(bytes 或 bytearray 对象的项是整数)。字符串的所有切片都是相同类型的字符串。字符串对象有许多方法,在“String Objects 的方法”中介绍。

元组

元组对象是不可变的:因此,试图重新绑定或删除元组的项或切片会引发异常。元组的项可以是任意对象,并且可以是不同类型;元组的项可以是可变的,但我们建议不要变更它们,因为这样做会令人困惑。元组的切片也是元组。元组没有普通(非特殊)方法,除了 count 和 index,其含义与列表相同;它们有许多特殊方法,在“特殊方法”中介绍。

列表

列表对象是可变的:可以重新绑定或删除列表的项和切片。列表的项是任意对象,可以是不同类型的。列表的切片是列表。

修改列表

通过对索引进行赋值可以修改(重新绑定)列表中的单个项。例如:

x = [1, 2, 3, 4]
x[1] = 42                *`# x is now [1, 42, 3, 4]`*

修改列表对象 L 的另一种方法是将 L 的切片用作赋值语句的目标(LHS)。赋值语句的 RHS 必须是可迭代对象。当 LHS 切片是扩展形式(即切片指定除 1 以外的步长)时,RHS 必须有与 LHS 切片中项数相同的项数。当 LHS 切片未指定步长,或显式指定步长为 1 时,LHS 切片和 RHS 可以是任何长度;向列表的这种切片赋值可以使列表变长或变短。例如:

x = [10, 20, 30, 40, 50]
*`# replace items 1 and 2`*
x[1:3] = [22, 33, 44]    *`# x is now [10, 22, 33, 44, 40, 50]`*
*`# replace items 1-3`*
x[1:4] = [88, 99]        *`# x is now [10, 88, 99, 40, 50]`*

有一些对切片赋值的重要特例:

  • 使用空列表 [] 作为 RHS 表达式可以从 L 中移除目标切片。换句话说,L[i:j] = [] 的效果与 del L[i:j](或特殊语句 L[i:j] *= 0)相同。

  • 使用空切片 L 作为 LHS 目标将 RHS 的项插入到 L 中适当的位置。例如,L[i:i] = ['a', 'b'] 将 'a' 和 'b' 插入到在赋值之前索引 i 处的 L 中。

  • 使用覆盖整个列表对象的切片 L[:] 作为左手边的目标会完全替换 L 的内容。

可以使用 del 从列表中删除一个项或一个切片。例如:

x = [1, 2, 3, 4, 5]
`del` x[1]                 *`# x is now [1, 3, 4, 5]`*
`del` x[::2]               *`# x is now [3, 5]`*

列表的原位操作

列表对象定义了 + 和 * 操作符的原位版本,可以通过增强赋值语句使用。增强赋值语句 L += L1 的效果是将可迭代对象 L1 的项添加到 L 的末尾,就像 L.extend(L1) 一样。L *= n 的效果是将 L 添加 n-1 个副本到 L 的末尾;如果 n ⇐ 0,则 L *= n 会使 L 变为空,类似于 L[:] = [] 或 del L[:]。

列表方法

列表对象提供了多种方法,如 表格 3-5 所示。非变异方法返回结果而不改变它们应用的对象,而变异方法可能会改变它们应用的对象。许多列表的变异方法的行为类似于对列表适当切片的赋值。在本表中,L 表示任何列表对象,i 表示 L 中任何有效的索引,s 表示任何可迭代对象,x 表示任何对象。

表格 3-5. 列表对象方法

非变异的
count L.count(x) 返回 L 中等于 x 的项的数量。
index L.index(x) 返回 L 中第一次出现的等于 x 的项的索引,如果 L 中没有这样的项则引发异常。
变异的
append *L.*append(x) 将项 x 追加到 L 的末尾;类似于 L[len(L):] = [x]。
clear *L.*clear() 移除 L 中的所有项,使 L 变为空列表。
extend L.extend(s) 将可迭代对象s的所有项目附加到L的末尾;类似于L[len(L):] = sL += s
insert L.insert(i, x) 在索引i之前将项目x插入L中,将L中的后续项目(如果有)向“右移”,以腾出空间(增加 len(L)一次,不替换任何项目,不引发异常;表现就像L[i:i]=[x]一样)。
pop L.pop(i=-1) 返回索引i处项目的值并将其从L中移除;当省略i时,移除并返回最后一个项目;当L为空或iL中的无效索引时,引发异常。
remove L.remove(x) 从L中移除第一个等于x的项目,如果L中没有这样的项目,则引发异常。
reverse L.reverse() 就地反转L的项目。
sort L.sort(key=None, reverse=False) 就地对L的项目进行排序(默认按升序排序;如果参数 reverse 为True,则按降序排序)。当参数 key 不是None时,每个项目x的比较对象是 key(x),而不是x本身。有关更多详细信息,请参阅以下章节。

列表对象的所有突变方法,除了 pop,都返回None

对列表进行排序

列表的 sort 方法会就地排序列表(重新排列项目以按升序放置),并保证排序是稳定的(相等的元素不会交换位置)。在实践中,sort 方法非常快速——通常是异常快,因为它可以利用任何子列表中可能存在的顺序或逆序(高级算法 sort 使用的是timsort,以其发明者、伟大的 Pythonista Tim Peters来命名,这是一种“非递归自适应稳定的自然合并排序/二进制插入排序混合体”——这真是一个难以启齿的名字!)。

sort 方法接受两个可选参数,可以使用位置参数或命名参数语法传递。如果参数 key 不是None,则必须是一个可调用的函数,该函数以任何列表项作为其唯一参数进行调用。在这种情况下,为了比较任意两个项目xy,Python 比较 key(x)和 key(y)而不是xy本身(在内部,Python 实现了与“搜索和排序”章节中介绍的 decorate–sort–undecorate 惯用法相同的方式,但速度要快得多)。如果参数 reverse 为True,则导致每次比较的结果被颠倒;这与在排序后反转L不完全相同,因为排序是稳定的(相等的元素永远不会交换位置),无论参数 reverse 是True还是False。换句话说,默认情况下,Python 按升序对列表进行排序,如果 reverse 为True,则按降序排序:

mylist = ['alpha', 'Beta', 'GAMMA']
mylist.sort()                  *`# ['Beta', 'GAMMA', 'alpha']`*
mylist.sort(key=str.lower)     *`# ['alpha', 'Beta', 'GAMMA']`*

Python 还提供了内置函数 sorted(见 表 8-2)来生成任意输入可迭代对象的排序列表。sorted 函数除了第一个参数(即提供项目的可迭代对象)外,还接受与列表的 sort 方法相同的两个可选参数。

标准库模块 operator(见 “operator 模块”)提供了高阶函数 attrgetter、itemgetter 和 methodcaller,它们生成适用于列表 sort 方法和内置函数 sorted 的可选 key 参数的函数。这个可选参数也以完全相同的含义存在于内置函数 min 和 max,以及标准库模块 heapq 中的函数 nsmallest、nlargest 和 merge(见 “heapq 模块”)以及标准库模块 itertools 中的类 groupby(见 “itertools 模块”)。

集合操作

Python 提供了适用于集合(普通集合和冻结集合)的多种操作。由于集合是容器,内置函数 len 可以接受一个集合作为其唯一参数并返回集合中的项数。集合是可迭代的,因此可以将其传递给任何接受可迭代参数的函数或方法。在这种情况下,迭代以任意顺序生成集合的项。例如,对于任何集合 S,min(S) 返回 S 中的最小项,因为具有单个参数的 min 会迭代该参数(顺序无关紧要,因为隐含的比较是传递的)。

集合成员关系

k in S 操作符检查对象 k 是否等于集合 S 中的任一项。当集合包含 k 时返回 True,不包含时返回 Falsek not in S 类似于 not (k in S)。

集合方法

集合对象提供多个方法,如 表 3-6 所示。非变异方法返回结果而不更改其应用对象,并且也可以用于 frozenset 的实例;变异方法可能会更改其应用对象,并且只能用于 set 的实例。在本表中,s 表示任何集合对象,s1 表示任何可哈希项的可迭代对象(通常但不一定是集合或冻结集合),x 表示任何可哈希对象。

表 3-6. 集合对象方法

非变异
复制 s.copy() 返回 s 的浅拷贝(其项与 s 的相同对象,而不是副本);类似于 set(s)
差集 s.difference(s1) 返回集合 s 中不在 s1 中的所有项;可写作 s - s1
交集 s.intersection(s1) 返回集合 s 中同时也在 s1 中的所有项;可写作 s & s1
isdisjoint s.isdisjoint(s1) 如果 ss1 的交集为空集(它们没有共同项),则返回 True,否则返回 False
issubset s.issubset(s1) 当 s 的所有项也都在 s1 中时返回 True,否则返回 False;可以写成 ss1
issuperset s.issuperset(s1) 当 s 包含 s1 的所有项时返回 True,否则返回 False(类似于 s1.issubset(s));可以写成 s >= s1
symmet⁠r⁠i⁠c⁠_​d⁠i⁠f⁠ference s.symmetric_difference(s1) 返回所有在 ss1 中的项,但不同时在 ss1 中;可以写成 s ^ s1
union s.union(s1) 返回所有在 ss1 或两者中的项的集合;可以写成 s | s1
Mutating
add s.add(x) 将 x 作为一个项添加到 s;如果 x 已经是 s 的项,则无效果
clear s.clear() 清空 s 中的所有项,使 s 变为空集
discard s.discard(x) 从 s 中移除 x 作为一个项;如果 x 不是 s 的项,则无效果
pop s.pop() 移除并返回 s 中的一个任意项
remove s.remove(x) 从 s 中移除 x 作为一个项;如果 x 不是 s 的项,则引发 KeyError 异常

所有集合对象的变异方法,除了 pop,都返回 None

pop 方法可以用于集合的破坏性迭代,消耗少量额外内存。这种节约内存的方式使得 pop 方法在处理大型集合时非常有用,特别是当你希望在循环过程中“消耗”集合时。除了节省内存之外,这种破坏性循环的潜在优势还包括:

`while` S:
    item = S.pop()
 *`# ...handle item...`*

相对于这样的非破坏性循环:

`for` item `in` S:
 *`# ...handle item...`*

在破坏性循环的主体中,你被允许修改 S(添加和/或移除项),而在非破坏性循环中则不允许。

集合还具有名为 difference_update、intersection_update、symmetric_difference_update 和 update 的变异方法(对应于非变异方法 union)。每个这样的变异方法执行与对应的非变异方法相同的操作,但它会直接在调用它的集合上执行操作,改变集合,并返回 None

四个对应的非变异方法也可以通过运算符语法访问(其中 S2 是一个集合或不可变集合,分别为 S - S2S & S2S ^ S2S | S2),而变异方法可以通过增强赋值语法访问(分别为 S -= S2S &= S2S ^= S2S |= S2)。此外,集合和不可变集合还支持比较运算符:==(集合具有相同的项;即它们是“相等”的集合)、!=(与 == 相反)、>=(issuperset)、<=(issubset)、<(issubset 且不相等)、>(issuperset 且不相等)。

当您使用运算符或增强赋值语法时,两个操作数必须是集合或不可变集合;然而,当您调用命名方法时,参数 S1 可以是任何具有可哈希项的可迭代对象,并且它的工作方式就像您传递的参数是 set(S1) 一样。

字典操作

Python 提供了许多适用于字典的操作。由于字典是容器,内置的 len 函数可以接受字典作为参数,并返回字典中条目(键/值对)的数量。字典是可迭代的,因此您可以将其传递给任何接受可迭代参数的函数。在这种情况下,迭代按插入顺序仅生成字典的键。例如,对于任何字典 D,min(D) 返回字典中最小的键(迭代中的键顺序在此处无关紧要)。

字典成员资格

k in D 运算符检查对象 k 是否是字典 D 的键。如果键存在,则返回 True,否则返回 Falsek not in D 类似于 not (k in D)。

字典索引

要表示字典 D 中当前与键 k 关联的值,请使用索引:D[k]。使用不在字典中的键进行索引会引发异常。例如:

d = {'x':42, 'h':3.14, 'z':7}
d['x']                         *`# 42`*
d['z']                         *`# 7`*
d['a']                         *`# raises KeyError exception`*

用尚未存在于字典中的键进行简单赋值(例如,D[newkey]=value)是一个有效的操作,并将键和值作为字典中的新条目添加进去。例如:

d = {'x':42, 'h':3.14}
d['a'] = 16                    *`# d is now {'x':42, 'h':3.14, 'a':16}`*

del 语句,以形式 del D[k],从字典中移除键为 k 的条目。当 k 不是字典 D 的键时,del D[k] 引发 KeyError 异常。

字典方法

字典对象提供了几种方法,如表 3-7 所示。非变动方法返回结果而不更改其应用对象,而变动方法可能会更改其应用对象。在此表中,dd1 表示任意字典对象,k 表示任何可哈希对象,x 表示任何对象。

表 3-7. 字典对象方法

非变动
copy *d.*copy() 返回字典的浅拷贝(一个与 D 相同对象的拷贝,而不是它们的副本,就像 dict(d) 一样)
get *d.*get(k[, x]) 当 kd 的键时,返回 d[k];否则返回 x(或者当您不传递 x 时,返回 None
items d.items() 返回一个可迭代的视图对象,其条目是 d 中所有当前的条目(键/值对)
keys d.keys() 返回一个可迭代的视图对象,其条目是 d 中所有当前的键
values d.values() 返回一个可迭代的视图对象,其条目是 d 中所有当前的值
变动
clear d.clear() 移除 d 中的所有条目,使 d 变为空字典
pop *d.*pop(k[, x]) 当 kd 的键时,移除并返回 d[k];否则返回 x(或者当您不传递 x 时,引发 KeyError 异常)
popitem d.popitem() 按后进先出顺序从 d 中移除并返回项目。
setdefault d.setdefault(k, x) 当 kd 的一个键时返回 d[k];否则,设置 d[k] 等于 x(或 None,当未传递 x 时),然后返回 d[k]。
update d.update(d1) 对于映射 d1 中的每个 k,设置 d[k] 等于 d1[k]。

items、keys 和 values 方法返回称为 视图对象 的值。如果底层字典发生更改,则检索的视图也会更改;Python 不允许在使用 for 循环遍历其任何视图对象时修改底层字典的键集。

对任何视图对象进行迭代时,按插入顺序产生值。特别是当在没有任何中间更改字典的情况下调用多个这些方法时,结果的顺序对所有方法都是相同的。

字典还支持类方法 fromkeys(seq, value),它返回一个包含给定可迭代序列 seq 中所有键的字典,每个键都以 value 初始化。

在迭代字典时永远不要修改其键。

在迭代字典或其方法返回的任何可迭代视图时,不要修改字典的键集(即添加或删除键)。如果需要避免在迭代过程中对字典进行变更,建议明确地在从字典或视图构建的列表上进行迭代(即在 list(D) 上)。直接在字典 D 上进行迭代与在 D.keys() 上进行迭代完全相同。

items 和 keys 方法的返回值还实现了集合非变异方法,并且行为类似于 frozensets;而 values 方法的返回值不是,因为与其他方法(以及集合)不同,它可能包含重复项。

popitem 方法可用于对字典进行破坏性迭代。items 和 popitem 都返回字典项作为键/值对。在处理大型字典时,popitem 可用于在循环过程中“消耗”字典。

D.setdefault(k, x) 返回与 D.get(k, x) 相同的结果;但当 k 不是 D 的一个键时,setdefault 还具有将 D[k] 绑定到值 x 的副作用(在现代 Python 中,setdefault 不经常使用,因为类型 collections.defaultdict,详见 “defaultdict”,通常提供类似、更快、更清晰的功能)。

pop 方法与 get 方法返回相同的结果,但当 kD 中的一个键时,pop 方法还具有移除 D[k] 的副作用(当未指定 xk 不是 D 的键时,get 返回 None,但 pop 会引发异常)。d.pop(key, None) 是从字典中删除键的一个有用快捷方式,无需先检查键是否存在,类似于 s.discard(x)(与 s.remove(x) 相对应,s 是一个集合)。

3.9+ 更新方法可以通过增强赋值语法进行访问:其中 D2 是一个字典,D |= D2D.update(D2) 相同。操作符语法,D | D2,不会改变任何字典:相反,它返回一个新的字典结果,使得 D3 = D | D2 等同于 D3 = D.copy(); D3.update(D2)。

更新方法(但不包括 | 和 |= 操作符)还可以接受键/值对的可迭代对象作为替代参数,而不是映射,并且可以接受命名参数而不是或者除了它的位置参数;当调用内置的 dict 类型时,传递这些参数的语义与“字典”中涵盖的相同。

控制流程语句

程序的控制流调节了程序代码执行的顺序。Python 程序的控制流主要取决于条件语句、循环和函数调用。(本节涵盖了 ifmatch 条件语句,以及 forwhile 循环;我们在接下来的章节中介绍了异常的引发和处理方式(通过 trywith 语句);异常在 第六章中介绍。

if 语句

经常情况下,您需要仅当某些条件成立时执行某些语句,或者根据互斥条件选择要执行的语句。由 ifelifelse 子句组成的复合语句可让您有条件地执行语句块。 if 语句的语法如下:

`if` *`expression``:`*
    *`statement``(``s``)`*
`elif` *`expression``:`*
    *`statement``(``s``)`*
`elif` *`expression``:`*
    *`statement``(``s``)`*
...
`else`:
    *`statement``(``s``)`*

elifelse 子句是可选的。在引入我们接下来将要查看的 match 构造之前,使用 ifelifelse 是所有条件处理的最常见方法(尽管有时使用具有可调用值的字典可能会提供一个很好的替代方法)。

这是一个典型的 if 语句,包含了所有三种子句:

`if` x < 0:
    print('x is negative')
`elif` x % 2:
    print('x is positive and odd')
`else`:
    print('x is even and nonnegative')

每个子句控制一个或多个语句(称为一个块):在包含子句关键字的行(称为子句的头行)之后,将块的语句放在单独的逻辑行上,缩进四个空格。当缩进返回到子句头的级别时,或者从那里进一步向左移动时,块终止(这是由 PEP 8 规定的风格)。

您可以在 ifelif 子句中使用任何 Python 表达式¹⁸ 作为条件。以这种方式使用表达式称为在布尔上下文中使用它。在此上下文中,任何值都被视为真或假。如前所述,任何非零数字或非空容器(字符串、元组、列表、字典、集合等)都会评估为 true,而零(0,任何数字类型)、None 和空容器则会评估为 false。要在布尔上下文中测试值 x,请使用以下编码样式:

`if` *`x`*:

这是最清晰和最符合 Python 风格的形式。

不要使用以下任何内容:

if *`x`* is True:
`if` *`x`* == `True`:
`if` bool(*`x`*):

说一个表达式返回 True(表示该表达式以 bool 类型返回值 1),与说一个表达式评估为 true(表示该表达式返回在布尔上下文中为真的任何结果)之间存在重要区别。例如,在if子句中测试表达式时,你只关心它评估为什么,而不是它返回什么。正如我们先前提到的,“评估为真”通常非正式表达为“是真的”,而“评估为假”则为“是假的”。

if子句的条件评估为真时,执行if子句内的语句,然后整个if语句结束。否则,Python 按顺序评估每个elif子句的条件。如果任何elif子句的条件评估为真,则执行该子句内的语句,并且整个if语句结束。否则,当存在else子句时执行其内的语句。在任何情况下,执行在整个if结构后面,同一级别的语句会接着执行。

match语句

3.10+ match语句将结构化模式匹配引入 Python 语言。你可以将其视为对 Python 类型执行与 re 模块(参见“正则表达式和 re 模块”)类似的操作:它允许轻松测试 Python 对象的结构和内容。¹⁹ 除非需要分析对象的结构,否则应抵制使用match的诱惑。

该语句的整体语法结构是新的(软)关键字match,后跟一个表达式,其值成为匹配主题。然后是一个或多个缩进的case子句,每个控制它包含的缩进代码块的执行:

`match` *`expression`*:
    `case` *`pattern`* [`if` *`guard`*]:
        *`statement``(``s``)`*
    *`# ...`*

在执行时,Python 首先评估表达式,然后按顺序测试每个case中的模式与生成的主题值是否匹配,直到找到一个匹配的为止:然后,执行与匹配的case子句缩进的块。模式可以执行两个操作:

  • 验证主语是否是具有特定结构的对象。

  • 将匹配的组件绑定到名称以供进一步使用(通常在相关的case子句内)。

当模式与主题匹配时,守卫允许在选择执行用例之前进行最终检查。所有模式的名称绑定已经发生,你可以在守卫中使用它们。当没有守卫,或者守卫评估为真时,执行案例的缩进代码块,之后match语句的执行完成,不再检查进一步的用例。

match语句本身不提供默认操作。如果需要一个,默认情况下case子句必须指定一个通配符模式——确保它匹配任何主题值的语法。在具有这种通配符模式的case子句之后跟随任何进一步case子句是 SyntaxError。

模式元素不能预先创建,绑定到变量,并且(例如)在多个位置重复使用。模式语法仅在(软)关键字case之后立即有效,因此没有办法执行这样的赋值。对于每个match语句的执行,解释器可以自由地缓存重复出现在案例中的模式表达式,但缓存对于每次新执行都是空的。

我们将首先描述各种类型的模式表达式,然后讨论守卫并提供一些更复杂的示例。

模式表达式具有自己的语义

模式表达式的语法可能看起来很熟悉,但它们的解释有时与非模式表达式完全不同,这可能会误导不了解这些差异的读者。特定的语法形式在case子句中用于指示特定结构的匹配。在本书中,我们使用的简化表示法不足以完整总结此语法的概述;²⁰ 因此,我们更喜欢用简单的语言和示例来解释这一新特性。有关更详细的示例,请参阅 Python 文档

构建模式

模式是表达式,尽管具有与case子句特定的语法,因此即使某些特性被不同解释,也适用熟悉的语法规则。它们可以用括号括起来,以便将模式的元素视为单个表达式单元。与其他表达式一样,模式具有递归语法,并可组合形成更复杂的模式。让我们从最简单的模式开始。

字面模式

大多数字面值都是有效的模式。整数、浮点数、复数和字符串字面值(但不是格式化字符串字面值)都是允许的,²¹ 并且在匹配相同类型和值的主题时都成功:

>>> `for` subject `in` (42, 42.0, 42.1, 1+1j, b'abc', 'abc'):
... print(subject, end=': ')
... `match` subject:
...         `case` 42: print('integer')  *`# note this matches 42.0, too!`*
...         `case` 42.1: print('float')
...         `case` 1+1j: print('complex')
...         `case` b'abc': print('bytestring')
...         `case` 'abc': print('string')
42: integer
42.0: integer
42.1: float
(1+1j): complex
b'abc': bytestring
abc: string

对于大多数匹配项,解释器在没有类型检查的情况下检查相等性,这就是为什么 42.0 与整数 42 匹配。如果区分很重要,请考虑使用类匹配(参见“类模式”)而不是字面匹配。TrueFalseNone作为单例对象,每个都与自身匹配。

通配符模式

在模式语法中,下划线(_)扮演通配符表达式的角色。作为最简单的通配符模式,**_**可以匹配任何值:

>>> `for` subject `in` 42, 'string', ('tu', 'ple'), ['list'], object:
...     `match` subject:
...         `case` `_`: print('matched', subject)
...
matched 42
matched string
matched ('tu', 'ple')
matched ['list']
matched <class 'object'>

捕获模式

在模式中使用未限定名称(没有点的名称)与非模式中的使用方式非常不同,因此我们觉得有必要在本节开始时发出警告。

简单名称绑定到模式内部匹配的元素

未限定名称—简单标识符(例如color)而不是属性引用(例如name.attr)—在模式表达式中不一定具有它们通常的含义。某些名称不是值的引用,而是在模式匹配期间绑定到主体值的元素。

除了 _ 之外,未限定名称是捕获模式。它们是通配符,匹配任何内容,但有一个副作用:在当前局部命名空间中,名称绑定到模式匹配的对象。匹配创建的绑定在语句执行后保留,允许case子句和随后的代码处理主体值的提取部分。

下面的示例与前面的示例类似,但名称 x 而不是下划线在主体中进行匹配。没有异常显示名称在每种情况下捕获整个主体:

>>> `for` subject `in` 42, 'string', ('tu', 'ple'), ['list'], object:
...     `match` subject:
...         `case` x: `assert` x == subject
...

值模式

这一节也从一个提醒开始,指出简单名称不能用于将它们的绑定注入到要匹配的模式值中。

使用限定名称在模式中表示变量值

因为简单名称在模式匹配期间捕获值,您必须使用属性引用(如name.attr这样的限定名称)来表示在同一match语句的不同执行之间可能变化的值。

尽管这个特性很有用,但意味着您不能直接使用简单名称引用值。因此,在模式中,值必须由限定名称表示,这些被称为值模式—它们表示值,而不像简单名称那样捕获它们。虽然有些许不便,但使用限定名称,您只需在一个否则为空的类上设置属性值。²² 例如:

>>> `class` m: v1 = "one"; v2 = 2; v3 = 2.56
`.``.``.`
>>> `match` ('one', 2, 2.56):
`.``.``.`     `case` (m.v1, m.v2, m.v3):  print('matched')
`.``.``.`
matched

要访问当前模块的“全局”命名空间非常容易,像这样:

>>> import sys
>>> g = sys.modules[__name__]
>>> v1 = "one"; v2 = 2; v3 = 2.56
>>> match ('one', 2, 2.56):
...     case (g.v1, g.v2, g.v3):  print('matched')
...
matched

OR 模式

P1P2是模式时,表达式P1 | P2是一个OR 模式,匹配与P1P2中任一模式匹配的任何内容,如下例所示。可以使用任意数量的替代模式,匹配从左到右尝试:

>>> `for` subject `in` range(5):
...     `match` subject:
...         `case` 1 | 3: print('odd')
...         `case` 0 | 2 | 4: print('even')
...
even
odd
even
odd
even

在通配符模式后面跟随更多的替代是语法错误,因为它们永远不会被激活。尽管我们的初始示例很简单,但请记住语法是递归的,因此任何复杂度的模式都可以替换这些示例中的任何子模式。

分组模式

如果P1是一个模式,则(P1)也是一个匹配相同值的模式。当模式变得复杂时,添加“分组”括号非常有用,就像标准表达式一样。像其他表达式一样,务必区分(P1),一个简单的分组模式匹配P1,和(P1,),一个序列模式(稍后描述)匹配具有匹配P1的单个元素的序列。

序列模式

一个模式的列表或元组,可选地带有一个星号通配符 (*_) 或星号捕获模式 (*name),是 序列模式。当星号模式不存在时,模式匹配长度固定的值序列,与模式长度相同。序列的元素逐个匹配,直到所有元素都匹配成功(匹配成功)或某个元素无法匹配(匹配失败)。

当序列模式包括一个星号模式时,该子模式匹配足够长的元素序列,以允许其余的未星号化模式匹配序列的最后元素。当星号模式为 *name 形式时,name 绑定到中间元素列表(可能为空),这些元素不对应开始或结束的各个模式。

您可以使用看起来像元组或列表的模式来匹配序列——这对匹配过程没有影响。下一个示例展示了从序列中提取第一个、中间和最后一个元素的不必要复杂方式:

>>> `for` sequence `in` (["one", "two", "three"], range(2), range(6)):
...     `match` sequence:
...         `case`  (first, *vars, last): print(first, vars, last)
...
one ['two'] three
0 [] 1
0 [1, 2, 3, 4] 5

as 模式

您可以使用所谓的 as patterns 捕获由更复杂的模式匹配或模式的组成部分匹配的值,这是简单捕获模式(请参阅 “捕获模式”)所不能做到的。

P1 是一个模式时,P1 as name 也是一个模式;当 P1 成功时,Python 将匹配的值绑定到本地命名空间中的名称 name。解释器尝试确保,即使模式复杂,当匹配发生时也始终进行相同的绑定。因此,接下来的两个例子每个都会引发 SyntaxError,因为无法保证约束:

>>> `match` subject:
...     `case` ((0 | 1) `as` x) | 2: print(x)
...
SyntaxError: alternative patterns bind different names
>>> `match` subject:
...     `case` (2 | x): print(x)
...
SyntaxError: alternative patterns bind different names

但这个可以工作:

>>> `match` 42:
...     `case` (1 | 2 | 42) `as` x: print(x)
...
42

映射模式

映射模式 匹配映射对象,通常是字典,它们将键与值关联起来。映射模式的语法使用 key: pattern 对。键必须是文字或值模式。

解释器迭代处理映射模式中的键,每个键的处理方式如下:

  • Python 在主题映射中查找键;查找失败会立即导致匹配失败。

  • 然后 Python 将提取的值与键关联的模式进行匹配;如果值不能匹配模式,则整个匹配失败。

当映射模式中的所有键(及其关联的值)匹配时,整个匹配成功:

>>> `match` {1: "two", "two": 1}:
...     `case` {1: v1, "two": v2}: print(v1, v2)
...
two 1

您还可以将映射模式与 as 子句一起使用:

>>> `match` {1: "two", "two": 1}:
...     `case` {1: v1} `as` v2: print(v1, v2)
...
two {1: 'two', 'two': 1}

第二个示例中的 as 模式将 v2 绑定到整个主题字典,而不仅仅是匹配的键。

模式的最后一个元素可以选择是一个双星捕获模式,例如 **name。在这种情况下,Python 绑定 name 到一个可能为空的字典,其条目是主题映射中的 (key, value) 对,这些键不在模式中出现:

>>> `match` {1: 'one', 2: 'two', 3: 'three'}:
...     `case` {2: middle, **others}: print(middle, others)
...
two {1: 'one', 3: 'three'}

类模式

最后也可能是最多用途的一种模式是类模式,提供了匹配特定类及其属性实例的能力。

类模式的一般形式如下:

*name_or_attr*(*patterns*)

其中name_or_attr是绑定到类的简单或合格名称,具体来说,是内置类型 type 的实例(或其子类,但不需要超级花哨的元类!),而patterns是一个(可能为空的)逗号分隔的模式规范列表。当类模式中没有模式规范时,只要主题是给定类的实例,匹配就成功,因此例如模式 int()匹配任何整数。

类似于函数参数和参数,模式规范可以是位置的(如pattern)或命名的(如name=pattern)。如果类模式具有位置模式规范,则它们必须全部位于第一个命名模式规范之前。未设置类的*match_args*属性的用户定义类不能使用位置模式(请参见“配置类以进行位置匹配”)。

内置类型 bool、bytearray、bytes、dict、float、frozenset、int、list、set、str 和 tuple,以及任何命名元组和任何数据类都配置为接受单个位置模式,该模式与实例值匹配。例如,模式 str(x)匹配任何字符串,并通过将字符串的值与捕获模式进行匹配来绑定其值到x,就像 str() as x一样。

你可能还记得我们之前介绍的文字模式示例,显示文字匹配不能区分整数 42 和浮点数 42.0,因为 42 == 42.0。你可以使用类匹配来解决这个问题:

>>> `for` subject `in` 42, 42.0:
...     `match` subject:
...         `case` int(x): print('integer', x)
...         `case` float(x): print('float', x)
...
integer 42
float 42.0

一旦主题值的类型匹配成功,对于每个命名模式name=pattern,Python 从实例中检索属性name并将其值与pattern进行匹配。如果所有命名模式匹配成功,则整体匹配成功。Python 通过将位置模式转换为命名模式来处理位置模式,你很快就会看到。

Guards

case子句的模式成功时,通常方便根据从匹配中提取的值来确定是否应执行此case。如果存在守卫,则在成功匹配后执行它。如果守卫表达式评估为 false,则 Python 放弃当前case,尽管匹配成功,并继续考虑下一个 case。本示例使用守卫通过检查匹配中绑定的值来排除奇数整数:

>>> `for` subject `in` range(5):
...     `match` subject:
...         `case` int(i) `if` i % 2 == 0: print(i, "`is` even")
...
0 is even
2 is even
4 is even

配置类以进行位置匹配

当您希望自己的类处理匹配中的位置模式时,必须告诉解释器实例的哪个属性(而不是*init的哪个参数*)对应于每个位置模式。您可以通过将类的 match_args 属性设置为名称序列来实现这一点。如果尝试使用超过您定义的位置模式数量,则解释器会引发 TypeError 异常:

>>> `class` Color:
...     __match_args__ = ('red', 'green', 'blue')
...     `def` __init__(self, r, g, b, name='anonymous'):
...         self.name = name
...         self.red, self.green, self.blue = r, g, b
...
>>> color_red = Color(255, 0, 0, 'red')
>>> color_blue = Color(0, 0, 255)
>>> `for` subject `in` (42.0, color_red, color_blue):
...     `match` subject:
...         `case` float(x):
...             print('float', x)
...         `case` Color(red, green, blue, name='red'):
...             print(type(subject).__name__, subject.name,
...                   red, green, blue)
...         `case` Color(red, green, 255) `as` color:
...             print(type(subject).__name__, color.name,
...                   red, green, color.blue)
...         `case` _: print(type(subject), subject)
...
float 42.0
Color red 255 0 0
Color anonymous 0 0 255
>>> `match` color_red:
...     `case` Color(red, green, blue, name):
...         print("matched")
...
Traceback (most recent call last):
 File "<stdin>", line 2, in <module>
TypeError: Color() accepts 3 positional sub-patterns (4 given)

while 语句

while 语句重复执行由条件表达式评估为 true 的语句或语句块。以下是while语句的语法:

while *expression*:
    *statement(s)*

while 语句也可以包含else子句以及breakcontinue语句,我们在讨论for语句之后会详细讨论这些内容。

这是一个典型的while语句:

count = 0
`while` x > 0:
    x //= 2            *`# floor division`*
    count += 1
print('The approximate log2 is', count)

首先,Python 会在布尔上下文中评估expression,这被称为循环条件。当条件评估为 false 时,while语句结束。当循环条件评估为 true 时,执行组成循环体的语句或语句块。一旦循环体执行完毕,Python 会再次评估循环条件,以检查是否应执行另一次迭代。这个过程会持续进行,直到循环条件评估为 false,此时while语句结束。

循环体应包含最终使循环条件为 false 的代码,否则循环永远不会结束(除非循环体引发异常或执行break语句)。如果循环体执行return语句,函数体内的循环也会结束,因为此时整个函数结束。

for 语句

for 语句重复执行由可迭代表达式控制的语句或语句块。以下是语法:

for *target* in *iterable*:
    *statement*(*s*)

in关键字是for语句语法的一部分;在此处的目的与测试成员身份的in运算符不同。

这是一个相当典型的for语句:

`for` letter `in` 'ciao':
    print(f'give me a {letter}...')

for 语句也可以包含else子句以及breakcontinue语句;我们将很快讨论所有这些内容,从“循环语句上的 else 子句”开始。如前所述,iterable可以是任何可迭代的 Python 表达式。特别地,任何序列都是可迭代的。解释器隐式调用可迭代对象的内置 iter 函数,生成一个iterator(在下一小节中讨论),然后迭代它。

target通常是一个标识符,用于命名循环的控制变量for语句按顺序将该变量逐个重新绑定为迭代器的每个项。组成循环体的语句或语句为iterable中的每个项执行一次(除非由于异常、breakreturn语句而结束循环)。由于循环体可能在迭代器耗尽之前终止,因此这是可以使用无界可迭代对象的一种情况——即本身永远不会停止产生项的对象。

你也可以使用带有多个标识符的目标,例如解包赋值。在这种情况下,迭代器的 items 本身必须是可迭代对象,并且每个对象中的项数量必须与目标中的标识符数量完全相同。例如,当d是一个字典时,这是循环遍历d中的项(键/值对)的典型方式:

`for` key, value `in` d.items():
    `if` key `and` value:         *`# print only truthy keys and values`*
        print(key, value)

items 方法返回另一种类型的可迭代对象(视图),其项是键/值对;因此,我们使用带有两个标识符的for循环来解包每个项为键和值。

目标中可以有精确的一个标识符前带有星号,此时星号标识符将绑定到未分配给其他目标的所有项的列表。虽然目标的组成通常是标识符,但是值可以绑定到任何符合左侧表达式的可接受的 LHS 表达式中,如在“赋值语句”中所述——因此,以下写法是正确的,尽管不是最可读的样式:

prototype = [1, 'placeholder', 3]
`for` prototype[1] `in` 'xyz':
    print(prototype)
*`# prints`* *`[1, 'x', 3]`**`,`* *`then`* *`[1, 'y', 3]`*, *`then`* *`[1, 'z', 3]`*

在循环时不要改变可变对象

当迭代器具有可变的基础可迭代对象时,在对可迭代对象进行for循环执行期间不要更改该基础对象。例如,上述的键/值打印示例不能改变d。items 方法返回一个“视图”可迭代对象,其基础对象是d,因此循环体不能改变d中的键集(例如通过执行del d[key])。为确保d不是可迭代对象的基础对象,可以例如迭代 list(d.items())来允许循环体改变d。具体来说:

  • 在列表上循环时,不要插入、追加或删除项(重新绑定现有索引处的项是可以的)。

  • 在循环字典时,不要添加或删除项(重新绑定现有键的值是可以的)。

  • 在循环集合时,不要添加或删除项(不允许进行任何更改)。

循环体可能会重新绑定控制目标变量,但是循环的下一次迭代总会再次重新绑定它们。如果迭代器不产生任何项,则根本不会执行循环体。在这种情况下,for语句不以任何方式绑定或重新绑定其控制变量。然而,如果迭代器至少产生一项,则当循环语句结束时,控制变量仍绑定到循环语句绑定它的最后一个值。因此,以下代码仅在 someseq 不为空时才正确:

`for` x `in` someseq:
    process(x)
*`# potential NameError if someseq is empty`*
print(f'Last item processed was {x}')

迭代器

迭代器是一个对象i,使得您可以调用 next(i),它返回迭代器i的下一个项目,或者在耗尽时引发 StopIteration 异常。或者,您可以调用 next(i, default),在这种情况下,当迭代器i没有更多项目时,调用将返回default

当您编写一个类(参见“类和实例”)时,您可以通过定义一个特殊方法 next 来让该类的实例成为迭代器,该方法不接受除 self 之外的参数,并返回下一个项目或引发 StopIteration。大多数迭代器是通过隐式或显式调用内置函数 iter 构建的,见表 8-2。调用生成器也会返回一个迭代器,我们将在“生成器”中讨论。

如前所述,for 语句在其可迭代对象上隐式调用 iter 以获取一个迭代器。该语句:

`for` x `in` c:
    *`statement``(``s``)`*

是完全等价于:

*`_temporary_iterator`* = iter(c)
`while` `True`:
    `try`:
        x = next(*`_`**`temporary_iterator`*)
 `except` StopIteration`:`
 `break`
    *`statement``(``s``)`*

其中*_temporary_iterator*是当前范围内未使用的任意名称。

因此,当 iter(c)返回一个迭代器i,使得 next(i)永远不会引发 StopIteration(一个无界迭代器)时,循环 for x in c 将无限继续,除非循环体包含适当的 breakreturn 语句,或引发或传播异常。iter(c),依次调用特殊方法c.iter() 来获取并返回c上的迭代器。我们将在下一小节和“容器方法”中更多地讨论 iter

许多构建和操作迭代器的最佳方法都可以在标准库模块 itertools 中找到,见“itertools 模块”。

可迭代对象与迭代器

Python 的内置序列,像所有可迭代对象一样,实现了一个 iter 方法,解释器调用该方法来在可迭代对象上生成一个迭代器。因为每次调用内置的 iter 方法都会产生一个新的迭代器,所以可以在同一可迭代对象上嵌套多个迭代:

>>> iterable = [1, 2]
>>> `for` i `in` iterable:
... `for` j `in` iterable:
... print(i, j)
...
1 1
1 2
2 1
2 2

迭代器还实现了一个 iter 方法,但它总是返回 self,因此对迭代器进行嵌套迭代不起作用,就像你可能期望的那样:

>>> iterator = iter([1, 2])
>>> `for` i `in` iterator:
... `for` j `in` iterator:
... print(i, j)
...
1 2

在这里,内部循环和外部循环都在同一个迭代器上进行迭代。当内部循环第一次获得控制权时,迭代器的第一个值已经被消耗掉。内部循环的第一次迭代然后耗尽了迭代器,使得两个循环在尝试下一次迭代时结束。

range

循环遍历一系列整数是一种常见的任务,因此 Python 提供了内置函数 range 来生成一个整数的可迭代对象。在 Python 中循环 n 次的最简单方法是:

`for` i `in` range(*`n`*):
    *`statement``(``s``)`*

range(x) 生成从 0(包括)到 x(不包括)的连续整数。range(x, y) 生成一个列表,其项是从 x(包括)到 y(不包括)的连续整数。range(x, y, stride) 生成一个整数列表,其项从 x(包括)到 y(不包括),使得每两个相邻项之间的差异为 stride。如果 stride < 0,则 range 从 xy 递减。

x >= ystride >0,或者当 xystride <0 时,range 生成一个空的迭代器。当 stride ==0 时,range 抛出一个异常。

range 返回一个专用对象,仅用于像前面所示的 for 语句中的迭代。注意,range 返回一个可迭代对象,而不是迭代器;如果需要,您可以通过调用 iter(range(...)) 轻松获取这样的迭代器。range 返回的专用对象消耗的内存比相应的列表对象少(对于宽范围,内存少得多)。如果您真的需要一个是整数等差级数的列表,请调用 list(range(...))。您会发现,事实上,您并不需要完全构建在内存中的这样一个完整列表。

列表解析

for 循环的一个常见用法是检查可迭代对象中的每个项目,并通过附加在某些或所有项目上计算的表达式的结果来构建新列表。称为 列表解析listcomp 的表达式形式让您可以简洁直接地编码这种常见习语。由于列表解析是一个表达式(而不是一组语句块),您可以在需要表达式的任何地方使用它(例如,在函数调用中作为参数,在 return 语句中,或者作为某些其他表达式的子表达式)。

列表解析具有以下语法:

[ *`expression`* `for` *`target`* `in` *`iterable` `lc``-``clauses`* ]

列表解析中的每个 for 子句中的 targetiterable 与常规 for 语句中的相同语法和含义,并且列表解析中的每个 if 子句中的 expression 具有与常规 if 语句中的 expression 相同的语法和含义。当 expression 表示元组时,您必须将其括在括号中。

lc-clauses 是一系列零个或多个子句,每个子句都有这种形式之一:

`for` *`target`* `in` *`iterable`*

或者这种形式:

`if` *`expression`*

列表解析等效于通过重复调用结果列表的 append 方法构建相同列表的 for 循环²⁴。例如(将列表解析结果赋给一个变量以便清晰),这样:

result = [x+1 `for` x `in` some_sequence]

就是和 for 循环一样:

result = []
`for` x `in` some_sequence:
    result.append(x+1)

不要构建列表,除非你需要

如果你只是要对项进行一次循环,你不需要一个真正的可索引的列表:而是使用生成器表达式(在“生成器表达式”中讨论)。这样可以避免列表创建并且使用更少的内存。特别地,抵制使用列表解析作为不是特别可读的“单行循环”的诱惑,像这样:

[fn(x) `for` x `in` seq]

然后忽略生成的列表——而是使用普通的for循环!

这是一个使用if子句的列表推导式:

result = [x+1 `for` x `in` some_sequence `if` x>23]

这个列表推导式与包含if语句的for循环相同:

result = []
`for` x `in` some_sequence:
    `if` x>23:
        result.append(x+1)

这是一个使用嵌套for子句的列表推导式,用于将“列表的列表”扁平化为单个项目列表:

result = [x `for` sublist `in` listoflists `for` x `in` sublist]

这与嵌套另一个for循环的for循环相同:

result = []
`for` sublist `in` listoflists:
    `for` x `in` sublist:
        result.append(x)I

正如这些示例所示,列表推导式中forif的顺序与等价循环中的顺序相同,但是,在列表推导式中,嵌套保持隐式。如果您记住“按照嵌套循环中的顺序for子句”,那么可以帮助您正确设置列表推导式子句的顺序。

列表推导式和变量作用域

列表推导式表达式在自己的作用域内评估(与后面描述的集合和字典推导式以及本章末尾讨论的生成器表达式相同)。当for语句中的目标组件是一个名称时,该名称仅在表达式作用域内定义,并且在其外部不可用。

集合推导式

集合推导式与列表推导式具有完全相同的语法和语义,只是将其包含在大括号()中而不是方括号([])中。结果是一个集合;例如:

s = {n//2 `for` n `in` range(10)}
print(sorted(s))  *`# prints: [0, 1, 2, 3, 4]`*

类似的列表推导式将使每个项目重复两次,但是构建集合会删除重复项。

字典推导式

字典推导式与集合推导式具有相同的语法,只是在for子句之前使用两个表达式,并在两者之间使用冒号(:):key:value。结果是一个 dict,它保留插入顺序。例如:

d = {s: i `for` (i, s) `in` enumerate(['zero', 'one', 'two'])}
print(d)          *`# prints: {'zero': 0, 'one': 1, 'two': 2}`*

中断语句

您只能在循环体内使用break语句。当break执行时,循环终止,而不执行循环上的任何 else 子句。当循环嵌套时,break仅终止最内部的嵌套循环。在实践中,break通常位于循环体中的if(或偶尔是match)语句的子句中,因此break有条件地执行。

break的一个常见用途是实现一个决定是否应该保持循环的循环,仅在每个循环迭代的中间(这是唐纳德·克努特在他 1974 年的伟大论文“带有 go to 语句的结构化编程”^(25)中所说的“半循环”结构)。例如:

`while` `True`:          *`# this loop can never terminate "naturally"`*
    x = get_next()
    y = preprocess(x)
    `if` `not` keep_looping(x, y):
        `break`
    process(x, y)

继续语句

break一样,continue语句只能存在于循环体内。它导致循环体的当前迭代终止,并继续下一次循环的迭代。在实践中,continue通常位于循环体中的if(或偶尔是match)语句的子句中,因此continue有条件地执行。

有时,continue语句可以取代循环内嵌的if语句。例如,每个 x 在被完全处理之前必须通过多个测试:

`for` x `in` some_container:
    `if` seems_ok(x):
        lowbound, highbound = bounds_to_test()
        `if` lowbound <= x < highbound:
            pre_process(x)
            `if` final_check(x):
                do_processing(x)

条件数量增加时嵌套也会增加。使用continue的等效代码“扁平化”逻辑:

`for` x `in` some_container:
    `if` `not` seems_ok(x):
        `continue`
    lowbound, highbound = bounds_to_test()
    `if` x < lowbound `or` x >= highbound:
        `continue`
    pre_process(x)
    `if` final_check(x):
        do_processing(x)

Flat Is Better Than Nested

两个版本的工作方式相同,因此使用哪个是个人偏好和风格问题。Python 之禅 的原则之一(可以通过在交互式 Python 解释器提示符下键入import this随时查看)是“Flat is better than nested.” continue语句只是 Python 在您选择遵循此提示时帮助您在循环中向较少嵌套的结构迈进的一种方式。

循环语句的else子句

whilefor 语句可以选择带有尾部的else子句。该子句下的语句或块在循环自然终止时执行(在for迭代器结束时或while循环条件变为 false 时),但不会在循环过早终止时执行(通过breakreturn或异常)。当循环包含一个或多个break语句时,通常希望检查它是否自然或过早终止。您可以在循环上使用一个else子句来达到此目的:

`for` x `in` some_container:
    `if` is_ok(x):
        `break`  *`# item x is satisfactory, terminate loop`*
`else`:
    print('Beware: no satisfactory item was found in container')
    x = `None`

pass语句

Python 复合语句的主体不能是空的;它必须始终包含至少一条语句。当语法上需要语句但你无事可做时,可以使用一个pass语句,它不执行任何操作,作为一个显式的占位符。下面是一个在条件语句中使用pass作为一种相当复杂逻辑的一部分来测试互斥条件的示例:

`if` condition1(x):
    process1(x)
`elif` x>23 `or` (x<5 `and` condition2(x)):
    `pass`       *`# nothing to be done in this case`*
`elif` condition3(x):
    process3(x)
`else`:
    process_default(x)

空的defclass语句:使用文档字符串而非pass

您还可以使用文档字符串,在其他情况下空的defclass语句的主体。这样做时,您不需要额外添加pass语句(如果愿意可以添加,但这不是最佳 Python 风格)。

tryraise 语句

Python 通过try语句支持异常处理,其中包括tryexceptfinallyelse子句。您的代码还可以使用raise语句显式引发异常。当代码引发异常时,程序的正常控制流将停止,并且 Python 会寻找适当的异常处理程序。我们在 “Exception Propagation” 中详细讨论所有这些内容。

with 语句

with语句通常可以作为try/finally语句的更可读、更有用的替代方案。我们在 “The with Statement and Context Managers” 中详细讨论它。对上下文管理器的良好掌握通常可以帮助您更清晰地组织代码而不会影响效率。

函数

大多数典型的 Python 程序语句都是某些函数的一部分。代码放在函数体中可能比放在模块顶层要快,如在“避免使用 exec 和 from ... import *”中讨论的那样,因此在大多数情况下将大部分代码放入函数中是有很好的实际理由的,并且没有任何缺点:当你避免大量模块级代码时,清晰度、可读性和代码重用性都会提高。

函数是按请求执行的一组语句。Python 提供了许多内置函数,并允许程序员定义自己的函数。调用函数的请求称为函数调用。当你调用一个函数时,可以传递参数来指定函数执行计算的数据。在 Python 中,函数总是返回一个结果:要么是None,要么是计算的结果值。在class语句中定义的函数也称为方法。我们在“绑定和非绑定方法”中介绍了特定于方法的问题;然而,在本节中一般函数的覆盖同样适用于方法。

Python 在定义和调用函数方面为程序员提供了相当灵活的机会,这种灵活性意味着某些约束条件不能仅通过语法充分表达。在 Python 中,函数是对象(值),可以像处理其他对象一样处理。因此,你可以将一个函数作为参数传递给另一个函数的调用,并且一个函数可以作为调用的结果返回另一个函数。一个函数就像任何其他对象一样,可以绑定到一个变量上,可以是容器中的一个项,也可以是对象的属性。函数还可以作为字典中的键。在 Python 中,函数是普通对象的事实常常被称为函数是一等对象

例如,给定一个以函数为键的字典,并且其值是每个函数的逆,你可以通过将逆值作为键添加到字典中,并将其对应的键作为值来使字典双向化。以下是这个想法的一个小例子,使用了来自 math 模块中的一些函数(在“math 和 cmath 模块”中有介绍),该例子将一个单向映射的逆对添加到完整映射中:

`def` add_inverses(i_dict):
    `for` f `in` list(i_dict):  *`# iterates over keys while mutating i_dict`* 
        i_dict[i_dict[f]] = f
math_map = {sin:asin, cos:acos, tan:atan, log:exp}
add_inverses(math_map)

注意,在这种情况下,函数改变了它的参数(因此需要使用列表调用进行循环)。在 Python 中,通常约定这种函数不返回值(参见“return 语句”)。

定义函数:def 语句

def 语句是创建函数的常见方式。def 是一个单子句复合语句,具有以下语法:

`def` *`function_name`*(*`parameters`*):
   *`statement``(``s``)`*

function_name是一个标识符,缩进的 statement(s)function body。当解释器遇到 def 语句时,它会编译函数体,创建函数对象,并将(或重新绑定,如果存在绑定)function_name绑定到包含命名空间中的编译函数对象(通常是模块命名空间,或者定义方法时的类命名空间)。

parameters是一个可选列表,指定每个函数调用提供的将绑定到的标识符。我们通过将前者称为 parameters 后者称为 arguments,通常在计算机科学中区分它们。

在最简单的情况下,函数不定义任何参数,这意味着在调用时函数不接受任何参数。在这种情况下,function_name后的 def 语句括号为空,所有的调用也必须如此。否则,parameters将是一个规范的列表(参见“参数”)。当 def 语句执行时,函数体不会执行;相反,Python 将其编译为字节码,保存为函数对象的 code 属性,并在每次调用函数时稍后执行。函数体可以包含零个或多个 return 语句,我们稍后会讨论。

对函数的每次调用都提供了与函数定义中的参数对应的参数表达式。解释器从左到右评估参数表达式,并创建一个新的命名空间,在其中将参数值绑定到参数名作为函数调用的局部变量(我们在“调用函数”中讨论)。然后 Python 执行函数体,将函数调用命名空间作为局部命名空间。

这是一个简单的函数,每次调用时都返回传递给它的值的两倍的值:

`def` twice(x):
    `return` x*2

参数可以是任何可以乘以二的东西,因此您可以使用数字、字符串、列表或元组作为参数调用函数。每次调用都会返回与参数相同类型的新值:例如,twice('ciao')返回'ciaociao'。

函数的参数数量以及参数的名称、必须参数的数量以及是否以及如何收集未匹配的参数的信息构成了函数的 signature。签名定义了如何调用函数。

参数

参数(严谨地说,形式参数)指的是传递给函数调用的值,并且可以为它们指定默认值。每次调用函数时,调用会将每个参数名绑定到新的局部命名空间中相应的参数值,Python 稍后会在函数退出时销毁该命名空间。

除了允许您命名单个参数外,Python 还允许您收集未与单个参数匹配的参数值,并允许您特别要求某些参数是位置的或命名的。

位置参数

位置参数是一个标识符,name,用于命名参数。在函数体内使用这些名称访问调用的参数值。调用者通常可以使用位置或命名参数为这些参数提供值(请参阅“将参数匹配到参数”)。

命名参数

命名参数通常采用name=expression的形式(3.8+或在位置参数收集器之后,通常只是*,如稍后讨论的那样)。它们通常被称为默认可选甚至(令人困惑的是,因为它们不涉及任何 Python 关键字)关键字参数。当解释器执行def语句时,会评估每个这样的expression并保存结果值,称为参数的默认值,保存在函数对象的属性中。因此,在传统形式上写的命名参数不需要提供参数值:调用使用给定的expression作为默认值。对于某些命名参数,您可以提供位置参数作为值(3.8+,除了那些通过出现在位置参数收集器之后标识为命名参数的参数;另请参见“将参数匹配到参数”)。

Python 在def语句执行时仅一次计算每个默认值一次,而不是每次调用生成的函数。特别是这意味着当调用者不提供相应参数时,Python 将相同对象即默认值绑定到命名参数。

避免可变默认值

函数可以更改可变默认值,例如列表,在每次调用该函数时没有对应参数时。这通常不是您想要的行为;详情请参阅“可变默认参数值”。

仅位置标记符

3.8+ 函数的签名可以包含单个仅位置标记符(/)作为虚拟参数。标记符之前的参数称为仅位置参数,在调用函数时必须作为位置参数提供,不能作为命名参数提供;对这些参数使用命名参数会引发 TypeError 异常。

例如,内置的 int 类型具有以下签名:

int(x, /, base=10)

调用 int 时,必须为 x 传递一个值,并且必须按位置传递。base(当 x 是要转换为 int 的字符串时使用)是可选的,可以按位置或作为命名参数传递(如果将 x 作为数字传递并且也传递 base,这是一个错误,但是表示法无法捕获该怪癖)。

位置参数收集器

位置参数收集器可以采用两种形式之一,即name或 3.8+的*。在前一种情况下,调用时name绑定到一个未匹配位置参数的元组(当所有位置参数匹配时,元组为空)。在后一种情况下(*是一个虚拟参数),使用未匹配的位置参数调用会引发 TypeError 异常。

当函数签名具有任一种类的位置参数收集器时,没有调用可以为收集器后来的命名参数提供位置参数:收集器禁止(在形式中)或为不对应前面参数的位置参数提供目标(在**name形式中)。

例如,考虑 random 模块中的这个函数:

`def` sample(population, k, *, counts=`None`):

在调用 sample 时,需要为 population 和 k 传递值,可以按位置或按名称传递。counts 是可选的;如果传递它,则必须将其作为命名参数传递。

命名参数收集器

这种最终的可选参数规范的形式为**name。当调用函数时,name被绑定到一个字典,其项为任何未匹配命名参数的(key, value)对,如果没有这样的参数,则为空字典。

参数序列

一般来说,位置参数后跟命名参数,如果有的话,位置和命名参数收集器最后。但是,位置仅限标记可以出现在参数列表的任何位置。

可变默认参数值

当命名参数的默认值是可变对象,并且函数体改变了该参数时,情况变得复杂。例如:

`def` f(x, y=[]):
    y.append(x)
    `return` id(y), y
print(f(23))             *`# prints:`* *`(4302354376, [23])`*
print(f(42))             *`# prints:`* *`(4302354376, [23, 42])`*

第二个打印输出[23, 42],因为对 f 的第一次调用通过将 23 附加到它改变了 y 的默认值,最初是一个空列表[]。id 值(始终相等,尽管在其他方面是任意的)确认两次调用返回相同的对象。如果希望 y 在每次使用单个参数(这种情况更频繁!)调用 f 时是一个新的空列表对象,请改用以下习惯用法:

`def` f(x, y=`None`):
    `if` y `is` `None`:
        y = []
    y.append(x)
    `return` id(y), y
print(f(23))             *`# prints:`* *`(4302354376, [23])`*
print(f(42))             *`# prints:`* *`(4302180040, [42])`*

可能有情况需要显式使用可变的默认参数值,以便在多次函数调用之间保留,通常是为了缓存目的,如下例所示:

`def` cached_compute(x, _cache={}):
    `if` x `not` `in` _cache:
        _cache[x] = costly_computation(x)
    `return` _cache[x]

这种缓存行为(也称为记忆化)通常通过使用 functools.lru_cache 装饰底层的 costly_computation 函数来实现,详见表 8-7,并在第十七章中详细讨论。

参数收集器参数

在函数签名中存在参数收集器(特殊形式 *、name 或 **name)允许函数禁止()或收集位置参数(*name)或命名参数(**name)的参数,这些参数与任何参数都不匹配(参见“匹配参数到参数”)。不需要使用特定的名称 - 您可以在每个特殊形式中使用任何标识符。*args 和 **kwds 或 **kwargs,以及 *a 和 **k,是流行的选择。

特殊形式 * 的存在会导致具有未匹配位置参数的调用引发 TypeError 异常。

*args 指定任何额外的位置参数(即,在函数签名中未匹配到位置参数的位置参数)将被收集到一个(可能为空的)元组中,并绑定到名为 args 的调用本地命名空间中。没有位置参数收集器,未匹配的位置参数将引发 TypeError 异常。

例如,这里是一个接受任意数量位置参数并返回它们总和的函数(并演示了除 *args 之外的标识符的使用):

`def` sum_sequence(*numbers):
    `return` sum(numbers)
print(sum_sequence(23, 42))        *`# prints: 65`*

同样地,***kwds  * 指定任何额外命名的参数(即,那些在签名中未明确指定的命名参数)将被收集到一个(可能为空的)字典中,其条目为这些参数的名称和值,并绑定到函数调用命名空间中的名称 kwds 中。

例如,以下函数接受一个字典作为参数,其键为字符串,值为数字,并对某些值添加指定的数量:

`def` inc_dict(d, **values):
    `for` key, value `in` values.items():
 `if` key `in` d:
 d[key] += value
 `else`:
            d[key] = value

my_dict = {'one': 1, 'two': 2}

inc_dict(my_dict, one=3, new=42)
print(my_dict)                *`# prints:`* *`{'one': 4, 'two': 2, 'new':42}`*

没有命名参数收集器,未匹配的命名参数将引发 T⁠y⁠p⁠e​E⁠r⁠r⁠o⁠r 异常。

函数对象的属性

def 语句设置函数对象 f 的一些属性。字符串属性 f.name 是标识符,def 用作函数的名称。您可以将 name 重新绑定为任何字符串值,但尝试取消绑定会引发 TypeError 异常。f.defaults 是命名参数的默认值元组(如果函数没有命名参数,则为空)。

文档字符串

另一个函数属性是文档字符串docstring。您可以使用或重新绑定函数 f 的 docstring 属性作为 f.doc。当函数体中的第一条语句是字符串文字时,编译器将该字符串绑定为函数的 docstring 属性。类和模块也适用类似的规则(参见“类文档字符串” 和 “模块文档字符串”)。文档字符串可以跨多个物理行,因此最好以三引号字符串文字的形式指定它们。例如:

`def` sum_sequence(*numbers):
    *`"""Return the sum of multiple numerical arguments.`*

       *`The arguments are zero or more numbers.`*
       *`The result is their sum.`*
    *`"""`*
    `return` sum(numbers)

文档字符串应该是你编写的任何 Python 代码的一部分。它们起到与注释类似的作用,但它们更加有用,因为它们在运行时仍然可用(除非你使用 python -OO 运行程序,详见“命令行语法和选项”)。Python 的帮助函数(参见“交互会话”)、开发环境和其他工具可以使用函数、类和模块对象的文档字符串来提醒程序员如何使用这些对象。doctest 模块(详见“doctest 模块”)使得检查文档字符串中存在的示例代码是否准确和正确,并且随着代码和文档的编辑和维护而保持如此变得容易。

为了使你的文档字符串尽可能有用,请遵守一些简单的约定,详见 PEP 257。文档字符串的第一行应该是函数目的的简明摘要,以大写字母开头,并以句点结尾。它不应该提到函数的名称,除非名称恰好是自然语言中与函数操作的一个好、简明摘要相关的词语。使用命令式而不是描述性的形式:例如,说“Return xyz…” 而不是 “Returns xyz…” 如果文档字符串是多行的,则第二行应为空白行,接下来的行应形成一个或多个段落,段落之间用空行分隔,描述函数的参数、前提条件、返回值和副作用(如果有)。进一步的解释、文献引用和使用示例(你应该始终使用 doctest 进行检查)可以选择性地(通常非常有用地!)放在文档字符串的末尾。

函数对象的其他属性

除了其预定义的属性外,函数对象可能还有其他任意属性。要创建函数对象的属性,请在 def 语句执行后的赋值语句中将一个值绑定到相应的属性引用。例如,一个函数可以统计自己被调用的次数:

`def` counter():
    counter.count += 1
    `return` counter.count
counter.count = 0

注意,这种用法并不常见。更常见的情况是,当你想要将一些状态(数据)和一些行为(代码)组合在一起时,你应该使用第四章 中介绍的面向对象机制。然而,有时候将任意属性与函数关联起来的能力确实会派上用场。

函数注释

def 子句中的每个参数都可以用任意表达式进行注释,也就是说,在 def 的参数列表中任何可以使用标识符的地方,你都可以使用 标识符:表达式 的形式,表达式的值将成为该参数的注释

你还可以使用形式->表达式def子句的)和结束def子句的:之间注释函数的返回值;表达式的值成为名称'return'的注释。例如:

>>> `def` f(a:'foo', b)->'bar': `pass`
...
>>> f.__annotations__
{'a': 'foo', 'return': 'bar'}

如本示例所示,函数对象的 annotations 属性是一个字典,将每个带注释的标识符映射到相应的注释。

目前,理论上你可以为任何目的使用注释:Python 本身除了构造 annotations 属性外不做任何处理。关于用于类型提示的注释的详细信息,通常认为它们是关键用途,请参阅第五章。

return语句

在 Python 中,你只能在函数体内使用return关键字,并且可选择跟随一个表达式。当return执行时,函数终止,并且表达式的值是函数的结果。当函数通过到达其体的末尾或执行没有表达式的return语句(或明确执行return None)终止时,函数返回None

返回语句的良好风格

作为良好风格的一部分,当函数中的某些return语句有表达式时,函数中的所有return语句都应该有表达式。只有为了满足这种风格要求才明确写出return None永远不要在函数体末尾写出没有表达式的return语句。Python 不强制执行这些风格约定,但是当你遵循它们时,你的代码更清晰、更可读。

调用函数

函数调用是带有以下语法的表达式:

*`function_object`*(*`arguments`*)

function_object可以是对函数(或其他可调用)对象的任何引用;最常见的是函数的名称。括号表示函数调用操作本身。arguments在最简单的情况下是一系列由逗号(,)分隔的零个或多个表达式,给出了函数对应参数的值。当函数调用执行时,参数绑定到新的命名空间中的参数值,函数体执行,并且函数调用表达式的值是函数返回的任何内容。在函数内创建并返回的对象可能会被垃圾回收,除非调用者保留对它们的引用。

不要忘记在调用函数时加上尾随的()。

仅仅提及一个函数(或其他可调用对象)本身不会调用它。要调用一个函数(或其他对象)而不带参数,必须在函数名(或对可调用对象的其他引用)后面加上()。

位置参数和命名参数

参数可以有两种类型。位置参数是简单的表达式;命名参数(也被遗憾地称为关键字²⁶)参数采用以下形式:

*`identifier`*=*`expression`*

在函数调用中,命名参数位于位置参数之前是语法错误。可以在零个或多个位置参数后跟零个或多个命名参数。每个位置参数通过函数定义中的位置(顺序)对应于相应的参数提供值。没有要求位置参数必须匹配位置参数,反之亦然——如果位置参数多于位置参数,则附加参数将按位置绑定到签名中的所有参数之前的命名参数(如果有)。例如:

`def` f(a, b, c=23, d=42, *x):
    print(a, b, c, d, x)
f(1,2,3,4,5,6)  *`# prints:`* *`1 2 3 4 (5, 6)`*

注意,参数收集器出现在函数签名中的位置很重要(有关“将参数与参数匹配”的所有细节,请参见 “Matching arguments to parameters”):

`def` f(a, b, *x, c=23, d=42):
    print(a, b, x, c, d)
f(1,2,3,4,5,6)  *`# prints:`* *`1 2 (3, 4, 5, 6) 23 42`*

在没有任何命名参数收集器(**name)参数的情况下,每个参数的名称必须是函数签名中使用的参数名称之一。²⁷ expression 提供该名称参数的值。许多内置函数不接受命名参数:您必须仅使用位置参数调用这些函数。但是,使用 Python 编写的函数通常接受命名参数和位置参数,因此可以以不同的方式调用它们。在没有匹配位置参数的情况下,可以通过命名参数匹配位置参数。

函数调用必须通过位置参数或命名参数提供每个必需参数的一个值,并为每个可选参数提供零个或一个值。²⁸ 例如:

`def` divide(divisor, dividend=94):
    `return` dividend // divisor
print(divide(12))                          *`# prints:`* *`7`*
print(divide(12, 94))                      *`# prints:`* *`7`*
print(divide(dividend=94, divisor=12))     *`# prints:`* *`7`*
print(divide(divisor=12))                  *`# prints:`* *`7`*

正如您所见,在此处对 divide 的四次调用是等效的。在代码更易读的情况下,您可以传递命名参数,以便于识别每个参数的角色并控制参数顺序,增强代码的清晰度。

命名参数的常见用法是将某些可选参数绑定到特定值,同时让其他可选参数采用默认值:

`def` f(middle, begin='init', end='finis'):
    `return` begin+middle+end
print(f('tini', end=''))                   *`# prints:`* *`inittini`*

使用命名参数 end='' 时,调用者为 f 的第三个参数 end 指定一个值(空字符串 ''),同时让 f 的第二个参数 begin 使用其默认值 'init'。即使参数是命名的,也可以按位置传递参数;例如,对于上述函数:

print(f('a','c','t'))                      *`# prints:`* *`cat`*

在函数调用的参数末尾,您可以选择使用特殊形式 *seq 和 **dct 中的一个或两个。如果两种形式同时出现,则必须将带有两个星号的形式放在最后。 *seq 将 iterable seq 的项作为位置参数传递给函数(在调用使用常规语法给出的任何正常位置参数之后)。 seq 可以是任何可迭代对象。 **dctdct 的项作为命名参数传递给函数,其中 dct 必须是其所有键都为字符串的映射。每个项的键是参数名,项的值是参数的值。

当参数使用类似形式时,您可能希望传递一个seqdct的参数,如“参数”中所讨论的。例如,在该部分定义的 sum_sequence 函数(并在此处再次显示)中,您可能希望打印字典d中所有值的总和。这在使用**seq时很容易:

`def` sum_sequence(*numbers):
    `return` sum(numbers)

d = {'a': 1, 'b': 100, 'c': 1000}
print(sum_sequence(*d.values()))

(当然,print(sum(d.values()))会更简单和更直接。)

根据PEP 448的规定,函数调用可能包含零个或多个seq和/或dct*。即使在调用一个签名中没有使用对应形式的函数时,也可以传递seqdct*。在这种情况下,您必须确保可迭代对象seq具有正确数量的项,或者字典dct使用正确的标识符字符串作为键;否则,调用将引发异常。如下一节所述,位置参数不能匹配“仅关键字”参数;只有显式的或通过**kwargs传递的命名参数才能做到这一点。

“仅关键字”参数

在函数签名中的位置参数收集器(**name*或 3.8+ *)后面的参数被称为“仅关键字”参数:如果有的话,必须作为命名参数传递。在没有任何名称匹配的情况下,这样的参数将绑定到其在函数定义时设置的默认值。

“仅关键字”参数可以是位置的也可以是命名的。然而,您必须将它们作为命名参数传递,而不是作为位置参数。通常和可读性更强的做法是,在“仅关键字”参数规范的开始处(如果有的话)有简单的标识符,而在其后的identifier=default形式,尽管这不是 Python 语言的要求。

需要没有收集“多余”位置参数规范的函数在关键字参数规范的开始处使用一个仅包含星号(*)的虚拟参数,这个参数没有对应的实参。例如:

`def` f(a, *, b, c=56):  *`# b and c are keyword only`*
    `return` a, b, c
f(12, b=34)  *`# Returns (12, 34, 56) c is optional, it has a default`*
f(12)        *`# Raises a TypeError exception, since you didn't pass b`*
*`# Error message is: missing 1 required keyword-only argument: 'b'`*

如果您还指定了特殊形式**kwds,它必须位于参数列表的末尾(在有的话之后的仅关键字参数规范)。例如:

`def` g(x, *a, b=23, **k):  *`# b is keyword only`*
    `return` x, a, b, k
g(1, 2, 3, c=99)          *`# Returns (1, (2, 3), 23, {'c': 99})`*

将参数与参数匹配

函数调用必须为所有位置参数提供一个参数,并且可以为非仅关键字参数提供参数。

匹配过程如下所示:

  1. 形式为**expression的参数在内部被替换为一系列通过expression*迭代获得的位置参数。

  2. 形式为**expression的参数在内部被替换为一系列关键字参数,其名称和值通过迭代expression的 items()获得。

  3. 假设函数有N个位置参数,并且调用有M个位置参数:

    • MN时,将所有位置参数绑定到前M个位置参数名称;如果有剩余的位置参数,必须通过命名参数匹配。

    • M>N时,将剩余的位置参数按照它们在签名中出现的顺序绑定到命名参数。此过程以以下三种方式之一终止:

      1. 所有位置参数都已绑定。

      2. 签名中的下一个项是一个*参数收集器:解释器会引发 TypeError 异常。

      3. 签名中的下一个项是一个**name参数收集器:剩余的位置参数将被收集到一个元组中,然后绑定到函数调用命名空间中的name*。

  4. 则按照参数在调用中出现的顺序,按名称与参数(包括位置和命名参数)匹配。尝试重新绑定已绑定的参数名称会引发 TypeError 异常。

  5. 如果在此阶段有未匹配的命名参数:

    • 当函数签名包含一个**name收集器时,解释器会创建一个字典,其中包含键/值对(argument's_name, argument's_value),并将其绑定到函数调用命名空间中的name

    • 在没有这种参数收集器的情况下,Python 会引发 TypeError 异常。

  6. 任何剩余的未匹配命名参数都将绑定到其默认值。

此时,函数调用命名空间已经完全填充,解释器使用该“调用命名空间”作为函数的本地命名空间执行函数体。

参数传递的语义

传统上,Python 中的所有参数传递都是按值进行的(尽管在现代术语中,说参数传递是按对象引用更加精确和准确;请查看同义词按共享调用)。当您将一个变量作为参数传递给函数时,Python 会将变量当前引用的对象(值)传递给函数,并将该对象绑定到函数调用命名空间中的参数名称。因此,函数不能重新绑定调用者的变量。但是,将可变对象作为参数传递允许函数对该对象进行更改,因为 Python 传递的是对象本身的引用,而不是副本。重新绑定变量和变异对象是完全不同的概念。例如:

`def` f(x, y):
    x = 23
    y.append(42)
a = 77
b = [99]
f(a, b)
print(a, b)             *`# prints: 77`* *`[99, 42]`*

print 显示,a 仍绑定到 77。函数 f 对其参数 x 的重新绑定为 23 不会影响 f 的调用者,特别是不会影响用于传递 77 作为参数值的调用者变量的绑定。然而,print 还显示,b 现在绑定到 [99, 42]。b 仍绑定到调用之前的同一个列表对象,但是 f 已向该列表对象附加了 42,从而使其发生了变异。在这两种情况下,f 都没有改变调用者的绑定,也不能改变数字 77,因为数字是不可变的。不过,f 可以改变列表对象,因为列表对象是可变的。

命名空间

函数的参数加上在函数体中绑定(通过赋值或其他绑定语句,如def)的任何名称构成函数的局部命名空间,也称为其局部作用域。其中的每个变量被称为函数的局部变量。

非局部的变量称为全局变量(在没有嵌套函数定义的情况下,我们将很快讨论)。全局变量是模块对象的属性,如“模块对象的属性”所述。每当函数的局部变量与全局变量同名时,在函数体内,该名称指代的是局部变量,而不是全局变量。我们通过说局部变量隐藏了整个函数体中同名的全局变量来表示这一点。

global 语句

默认情况下,函数体中绑定的任何变量都是局部变量。如果函数需要绑定或重新绑定一些全局变量(是最佳实践!),函数体的第一个语句必须是:

`global` *`identifiers`*

标识符是一个或多个用逗号(,)分隔的标识符。全局语句中列出的标识符指的是函数需要绑定或重新绑定的全局变量(即模块对象的属性)。例如,我们在“函数对象的其他属性”中看到的函数计数器可以使用global和一个全局变量来实现,而不是函数对象的属性:

_count = 0
`def` counter():
    `global` _count
    _count += 1
    `return` _count

没有global语句,计数器函数在调用时会引发 UnboundLocalError 异常,因为 _count 将是一个未初始化(未绑定)的局部变量。虽然global语句使这种编程成为可能,但这种风格不够优雅,也不可取。正如我们之前提到的,当你想要将一些状态和行为组合在一起时,通常最好使用第四章中涵盖的面向对象机制。

避免使用 global

如果函数体只是使用全局变量(包括对该变量绑定的对象进行变异,当对象是可变的时),则不要使用global。只有当函数体重新绑定全局变量(通常通过给变量名赋值)时才使用global语句。就风格而言,除非绝对必要,否则不要使用global,因为它的存在会导致你的程序的读者认为该语句有某种有用的目的。除非作为函数体的第一个语句,否则永远不要使用global

嵌套函数和嵌套作用域

函数体内的def语句定义了一个嵌套函数,包含def的函数被称为嵌套函数的外部函数。嵌套函数体中的代码可以访问(但重新绑定)外部函数的局部变量,也称为嵌套函数的自由变量

让嵌套函数访问一个值的最简单方法通常不是依赖于嵌套作用域,而是将该值显式地作为函数的一个参数传递。如果需要,可以在嵌套函数的def时间绑定参数值:只需将该值用作可选参数的默认值。例如:

`def` percent1(a, b, c):
    `def` pc(x, total=a+b+c):
        `return` (x*100.0) / total
    print('Percentages are:', pc(a), pc(b), pc(c))

这里是使用嵌套作用域的相同功能:

`def` percent2(a, b, c):
    `def` pc(x):
        `return` (x*100.0) / (a+b+c)
    print('Percentages are:', pc(a), pc(b), pc(c))

在这个特定案例中,percent1 有微小的优势:计算 a+b+c 只发生一次,而 percent2 的内部函数 pc 会重复计算三次。然而,当外部函数在调用嵌套函数之间重新绑定局部变量时,重复计算可能是必要的:要注意这两种方法,并根据具体情况选择适当的方法。

一个访问“外部”(包含的)函数的局部变量值的嵌套函数也被称为闭包。下面的例子展示了如何构建一个闭包:

`def` make_adder(augend):
    `def` add(addend):
        `return` addend+augend
    `return` add
add5 = make_adder(5)
add9 = make_adder(9)

print(add5(100))   *`# prints: 105`*
print(add9(100))   *`# prints: 109`*

闭包有时是一个例外,与下一章介绍的面向对象机制作为捆绑数据和代码的最佳方式的一般规则不同。当您需要特别构造可调用对象,并在对象构造时固定一些参数时,闭包可能比类更简单和更直接。例如,make_adder(7) 的结果是一个函数,接受一个参数并返回 7 加上该参数的值。一个返回闭包的外部函数是“工厂”,用于生成一组由某些参数(例如前面示例中的 augend 参数的值)区分的函数成员,这通常有助于避免代码重复。

nonlocal 关键字类似于 global,但它引用的是某个词法上包围函数的命名空间中的名称。当它出现在几层深的函数定义中(这是一个很少需要的结构!),编译器会在最深层的包含函数的命名空间中搜索,然后是包含该函数的函数,依此类推,直到找到名称或没有更进一步的包含函数为止,此时编译器会引发错误(如果有全局名称则不匹配)。

这里是我们在之前章节中使用函数属性、然后是全局变量实现的“计数器”功能的嵌套函数方法:

`def` make_counter():
    count = 0
    `def` counter():
        `nonlocal` count
        count += 1
        `return` count
    `return` counter

c1 = make_counter()
c2 = make_counter()
print(c1(), c1(), c1())     *`# prints:`* *`1 2 3`*
print(c2(), c2())           *`# prints:`* *`1 2`*
print(c1(), c2(), c1())     *`# prints:`* *`4 3 5`*

这种方法与之前的方法相比的一个关键优势是,这两个嵌套函数,就像面向对象的方法一样,允许您创建独立的计数器 —— 这里是 c1 和 c2。每个闭包都保留其自己的状态,不会干扰另一个。

lambda 表达式

如果一个函数体只是一个单一的 return 表达式 语句,您可以(非常可选地!)选择使用特殊的 lambda 表达式形式替换该函数:

`lambda` *`parameters`*: *`expression`*

lambda表达式是普通函数的匿名等价物,其体是一个单一的return语句。lambda语法不使用return关键字。你可以在任何需要函数引用的地方使用lambda表达式。当你想要将一个极其简单的函数作为参数或返回值时,lambda有时会很方便。

下面是一个使用lambda表达式作为内置 sorted 函数参数的示例(详见表 8-2):

a_list = [-2, -1, 0, 1, 2]
sorted(a_list, key=`lambda` x: x * x)  *`# returns: [0, -1, 1, -2, 2]`*

或者,你可以总是使用本地def语句给函数对象一个名称,然后将这个名称作为参数或返回值。下面是使用本地def语句的相同 sorted 示例:

a_list = [-2, -1, 0, 1, 2]
`def` square(value):
    `return` value * value
sorted(a_list, key=square)           *`# returns: [0, -1, 1, -2, 2]`*

虽然lambda有时候很方便,但def通常更好:它更通用,帮助你使代码更易读,因为你可以为函数选择一个清晰的名称。

生成器

当函数体中包含一个或多个关键字yield的时候,该函数被称为生成器,或者更精确地称为生成器函数。当你调用一个生成器时,函数体不会执行。相反,生成器函数返回一个特殊的迭代器对象,称为生成器对象(有时候,令人困惑地,也称为“生成器”),包装函数体、其局部变量(包括参数)和当前执行点(最初在函数的起始点)。

当你(隐式或显式地)在一个生成器对象上调用 next 时,函数体从当前点开始执行,直到下一个yield,它的形式如下:

`yield` *`expression`*

一个裸的yield没有表达式也是合法的,并且等价于yield None。当yield执行时,函数的执行被“冻结”,保留当前执行点和局部变量,并且yield后面的表达式成为 next 的结果。当再次调用 next 时,函数体会从上次停止的地方继续执行,直到下一个yield。当函数体结束或执行return语句时,迭代器会抛出 StopIteration 异常以指示迭代结束。如果有的话,return后面的表达式是 StopIteration 异常的参数。

yield是一个表达式,而不是一个语句。当你在生成器对象g上调用g.send(value)时,yield的值是value;当你调用 next(g)时,yield的值是None。我们稍后会详细讨论这一点:它是在 Python 中实现协程的基本构建块。

生成器函数通常是构建迭代器的一种便捷方式。由于最常见的使用迭代器的方式是使用for语句进行循环,你通常会像这样调用一个生成器(在for语句中隐式地调用 next):

`for` *`avariable`* `in` *`somegenerator`*(*`arguments`*):

例如,假设您想要一个从 1 到N递增再递减到 1 的数字序列。一个生成器可以帮助实现:

`def` updown(N):
    `for` x `in` range(1, N):
        `yield` x
    `for` x `in` range(N, 0, -1):
        `yield` x
`for` i `in` updown(3):
    print(i)              *`# prints:`* *`1 2 3 2 1`*

这是一个生成器,与内置的 range 函数有些相似,但返回浮点值的迭代器而不是整数:

`def` frange(start, stop, stride=1.0):
    start = float(start)  *`# force all yielded values to be floats`*
    `while` start < stop:
        `yield` start
        start += stride

此示例仅部分类似于 range,因为为简单起见,假定了开始和停止参数是必需的,并且假定步长为正。

生成器函数比返回列表的函数更灵活。生成器函数可以返回一个无限迭代器,即一个产生无限流结果的迭代器(仅在通过其他方式终止循环,例如通过有条件执行的break语句时使用)。此外,生成器对象迭代器执行惰性评估:迭代器只能在需要时计算每个后续项,“及时而不是提前”,而等效的函数则提前进行所有计算,并可能需要大量内存来保存结果列表。因此,如果您只需要能够迭代计算的序列,通常最好在生成器对象中计算序列,而不是在返回列表的函数中计算。如果调用者需要一个由g(arguments)构建的有界生成器对象产生的所有项目的列表,则调用者可以简单地使用以下代码显式请求 Python 构建列表:

*`resulting_list`* = list(*`g`*(*`arguments`*))

yield from

当多级迭代需要产生值时,为了提高执行效率和清晰度,可以使用形式yield from expression,其中expression是可迭代的。这将expression中的值逐个地传递到调用环境中,避免了重复使用yield的需要。因此,我们可以简化先前定义的 updown 生成器:

`def` updown(N):
    `yield from` range(1, N)
    `yield from` range(N, 0, -1)
`for` i `in` updown(3):
    print(i)              *`# prints: 1 2 3 2 1`*

此外,使用yield from允许您将生成器用作协程,接下来将讨论。

几乎是协程的生成器

生成器还进一步增强,可以在每次yield执行时从调用者那里接收一个值(或异常)。这使得生成器可以实现协程,详见PEP 342。当生成器对象恢复时(即在其上调用 next),相应的yield的值为None。要将值x传递给某个生成器对象g(使g接收x作为其暂停的yield的值),而不是调用 next(g),可以调用g.send(x)(g.send(None)等同于 next(g))。

其他增强生成器的方面涉及异常处理:我们在“生成器和异常”中进行了介绍。

生成器表达式

Python 提供了一个更简单的方式来编写特别简单的生成器:生成器表达式,通常称为genexps。genexp 的语法与列表推导(如“列表推导”中所述)完全相同,只是 genexp 在括号(())内而不是方括号([])内。genexp 的语义与对应的列表推导相同,只是 genexp 生成一个一次产生一个项目的迭代器,而列表推导则在内存中生成所有结果的列表(因此,在适当时使用 genexp 可以节省内存)。例如,要计算所有单个数字整数的平方和,你可以编写 sum([xx for x in range(10)]),但你可以更好地表达为 sum(xx for x in range(10))(完全相同,但省略了方括号):你可以获得相同的结果但消耗更少的内存。指示函数调用的括号也“双重服务”并包含 genexp。但是,当 genexp 不是唯一参数时,括号是必需的。额外的括号不会有害,但通常最好省略,以提高清晰度。

递归

Python 支持递归(即 Python 函数可以直接或间接地调用自身),但递归的深度有限制。默认情况下,Python 在检测到递归深度超过 1,000 时中断递归并引发 RecursionLimitExceeded 异常(在“标准异常类”中介绍)。你可以通过在 sys 模块中调用 setrecursionlimit 函数来更改这个默认递归限制,如表 8-3 所述。

注意更改递归限制并不会让你无限递归。绝对最大限制取决于你的程序运行平台,特别是底层操作系统和 C 运行库,但通常是几千层。如果递归调用太深,你的程序会崩溃。当调用 setrecursionlimit 超出平台能力后,这种失控的递归是少数几个可以导致 Python 程序崩溃——真正的崩溃,没有 Python 异常机制的安全网之一。因此,小心通过提高递归限制来“修复”出现 RecursionLimitExceeded 异常的程序。虽然这一种有效的技术,但通常建议你寻找方法消除递归,除非你确信已经能够限制程序所需的递归深度。

熟悉 Lisp、Scheme 或函数式编程语言的读者特别需要注意,Python 实现尾递归消除的优化,这在这些语言中非常关键。在 Python 中,无论是递归调用还是非递归调用,都具有相同的时间和内存空间“成本”,仅依赖于参数的数量:调用是否是“尾调用”(即它是调用者执行的最后操作)并不会改变成本。这使得递归消除变得更加重要。

例如,考虑递归的经典用法:遍历二叉树。假设你将二叉树结构表示为节点,其中每个节点都是一个三项元组(有效载荷,左侧,右侧),左侧和右侧是类似的元组或None,表示左侧和右侧的后代。一个简单的示例可能是 (23, (42, (5, None, None), (55, None, None)), (94, None, None)),用于表示图 3-1 中显示的树。

一个二叉树的示例

图 3-1. 一个二叉树的示例

要编写一个生成器函数,给定这样一个树的根,逐步“遍历”树,按照自顶向下的顺序产生每个有效载荷,最简单的方法是递归:

`def` rec(t):
    `yield` t[0]
    `for` i `in` (1, 2):
        `if` t[i] `is` `not` `None``:`
 `yield from` rec(t[i])

但如果树非常深,递归可能会成为一个问题。为了消除递归,我们可以处理自己的堆栈——一个按照后进先出(LIFO)方式使用的列表,感谢其附加和弹出方法。例如:

`def` norec(t):
    stack = [t]
    `while` stack:
        t = stack.pop()
        `yield` t[0]
        `for` i `in` (2, 1):
            `if` t[i] `is` `not` `None`:
                stack.append(t[i])

要注意的唯一小问题是,为了保持与 rec 相同的yield顺序,需要调整 (1, 2) 索引顺序以检查后代,改为 (2, 1),以适应堆栈的“反向”(后进先出)行为。

¹ 常规上,指向常量的标识符都是大写。

² 控制字符包括非打印字符,如 \t(制表符)和 \n(换行符),两者都算作空白字符,以及诸如 \a(警告,也称“哔哔声”)和 \b(退格键),它们不是空白字符。

³ 根据在线文档中的描述,“容器显示”(例如 list_display),但具体指包含字面项的显示。

⁴ 还有一个不久会涉及的 bytearray 对象,这是一个类似于字节的“字符串”,可变的。

⁵ 有时被称为“元组显示”的语法。

⁶ 有时被称为“列表显示”的语法。

⁷ 有时被称为“集合显示”的语法。

⁸ 每种特定的映射类型可能对其接受的键的类型有一些限制:特别是,字典仅接受可散列的键。

⁹ 有时被称为“字典显示”的语法。

¹⁰ 参见“形状、索引和切片”。

¹¹ 严格来说,几乎任何:NumPy 数组,在第十六章中有例外。

¹² 与 NumPy 数组完全相同的例外。

¹³ 有时被称为三元操作符,在 C 语言(Python 的原始实现语言)中也是如此称呼。

¹⁴ 严格来说,这不是您在其他语言中观察到的“强制转换”;然而,在内置的数字类型中,它产生几乎相同的效果。

¹⁵ 因此,36 是基数的上限:10 个数字加上 26 个字母字符。

¹⁶ divmod 结果的第二项,就像%的结果一样,是余数,而不是,尽管函数的命名可能会误导。这种差异在除数为负数时很重要。在一些其他语言中,如 C#和 JavaScript,%运算符的结果实际上是模;在其他语言中,如 C 和 C++,当任一操作数为负时,结果是机器相关的。在 Python 中,是余数。

¹⁷ Timsort 有一个独特之处,它是唯一被美国最高法院提及的排序算法,具体来说是在Oracle v. Google案件中。

¹⁸ 除了,正如已经指出的,具有多于一个元素的 NumPy 数组。

¹⁹ 值得注意的是,match语句明确排除了与序列模式匹配的类型为 str、bytes 和 bytearray 的值。

²⁰ 实际上,Python 在线文档中使用的语法符号需要并且已经更新,以简洁地描述 Python 的一些较新的语法添加。

²¹ 尽管对于精确相等比较浮点数或复数通常是可疑的实践。

²² 对于这种独特的使用情况,打破了关于以大写字母开头的类名和避免在一行中使用分号存储多个赋值的正常风格约定是常见的;然而,作者们尚未找到支持这种奇特且相对新的用法的风格指南。

²³ 以及它的子类,例如 collections.defaultdict。

²⁴ 除了循环变量的作用域仅限于推导式内部,这与在for语句中作用域工作方式不同。

²⁵ 在那篇论文中,Knuth 还首次提出使用“像缩进这样的设备,而不是分隔符”来表达程序结构——正如 Python 所做的!

²⁶ “哎哟”,因为它们与 Python 关键字无关,所以术语很混乱;如果您使用实际的 Python 关键字来命名命名参数,会引发 SyntaxError。

²⁷ 当 Python 开发人员意识到许多内置函数的参数在解释器看来实际上没有有效的名称时,他们引入了位置参数。

²⁸ “可选参数”是指函数签名提供默认值的参数之一。

第四章:面向对象的 Python

Python 是一种面向对象(OO)编程语言。然而,与一些其他面向对象语言不同,Python 不强制您专门使用面向对象范式:它还支持过程式编程,具有模块和函数,因此您可以为程序的每个部分选择最佳范式。面向对象范式帮助您将状态(数据)和行为(代码)组合在方便的功能包中。此外,它提供了一些有用的专门机制,如继承特殊方法。更简单的过程式方法,基于模块和函数,当您不需要面向对象编程的优点时可能更合适。使用 Python,您可以混合和匹配范式。

除了核心面向对象概念外,本章还涵盖了抽象基类装饰器元类

类和实例

如果你熟悉其他面向对象编程语言(如 C++或 Java)中的面向对象编程,你可能对类和实例有很好的理解:是一种用户定义的类型,你可以实例化它以构建实例,即该类型的对象。Python 通过其类和实例对象支持此功能。

Python 类

一个是具有以下特征的 Python 对象:

  • 你可以像调用函数一样调用类对象。这种调用称为实例化,返回一个称为类的实例的对象;类也称为实例的类型

  • 一个类具有任意命名的属性,您可以绑定和引用。

  • 类属性的值可以是描述符(包括函数),在“描述符”中有介绍,也可以是普通数据对象。

  • 绑定到函数的类属性也称为类的方法

  • 一个方法可以有许多 Python 定义的名称之一,其名称前后有两个下划线(称为双下划线名称,简称“双下划线名称”——例如,名称 init,读作“dunder init”)。当类提供这些特殊方法时,Python 隐式调用它们,当发生类或其实例的各种操作时。

  • 一个类可以继承自一个或多个类,这意味着它将一些属性的查找委托给其他类对象(包括常规和双下划线方法),这些属性不在类本身中。

类的一个实例是一个 Python 对象,具有任意命名的属性,您可以绑定和引用。对于实例本身没有找到的任何属性,每个实例对象都将属性查找委托给其类。类反过来可能会将查找委托给它继承的类(如果有的话)。

在 Python 中,类是对象(值),与其他对象一样处理。你可以将一个类作为参数传递给函数调用,并且函数可以将一个类作为调用的结果返回。你可以将一个类绑定到一个变量,一个容器中的项,或者一个对象的属性。类也可以作为字典的键。由于在 Python 中类是完全普通的对象,我们经常说类是一等对象。

类语句

class 语句是创建类对象的最常见方式。class 是一个单子句复合语句,具有以下语法:

`class` *`Classname`*(*`base``-``classes`*, *, ***`kw`*):
    statement(s)

Classname 是一个标识符:当类语句完成时,它绑定(或重新绑定)到刚刚创建的类对象。Python 命名约定 建议对类名使用大写字母开头,例如 Item、PrivilegedUser、MultiUseFacility 等。

base-classes 是一个以逗号分隔的表达式序列,其值是类对象。各种编程语言对这些类对象使用不同的名称:你可以称它们为类的 bases, superclasses,或者 parents。你可以说创建的类从其基类 继承派生扩展,或者 子类化;在本书中,我们通常使用 extend。这个类是其基类的 直接子类 或者 后代。**kw 可以包括一个命名参数 metaclass= 来建立类的 元类,如 “Python 如何确定类的元类” 中所述。

从语法上讲,包括 base-classes 是可选的:要指示你正在创建一个没有基类的类,只需省略 base-classes(并且可选地也省略括号,将冒号直接放在类名后面)。每个类都继承自 object,无论你是否指定了显式基类。

类之间的子类关系是传递的:如果 C1 扩展了 C2,而 C2 扩展了 C3,那么 C1 扩展了 C3。内置函数 issubclass(*C1*, *C2*) 接受两个类对象:当 C1 扩展了 C2 时返回 True,否则返回 False。任何类都是其自身的子类;因此,对于任何类 Cissubclass(*C*, *C*) 返回 True。我们在 “继承” 中讨论了基类如何影响类的功能。

跟随class语句的缩进语句的非空序列是class body。类主体作为class语句的一部分立即执行。直到主体执行完毕,新的类对象才存在,并且Classname标识符尚未绑定(或重新绑定)。“元类如何创建类”提供了关于class语句执行时发生的详细信息。请注意,class语句不会立即创建任何新类的实例,而是定义了稍后通过调用该类创建实例时所有实例共享的属性集合。

类主体

类的主体通常是您指定类属性的地方;这些属性可以是描述符对象(包括函数)或任何类型的普通数据对象。类的一个属性可以是另一个类,所以,例如,您可以在另一个class语句内“嵌套”一个class语句。

类对象的属性

您通常通过在类主体内将值绑定到标识符来指定类对象的属性。例如:

`class` C1:
    x = 23
print(C1.x)                      *`# prints:`* *`23`*

在这里,类对象 C1 有一个名为 x 的属性,绑定到值 23,并且 C1.x 引用该属性。这样的属性也可以通过实例访问:c = C1(); print(c.x)。然而,在实践中,这并不总是可靠的。例如,当类实例 c 有一个 x 属性时,c.x 访问的是该属性,而不是类级别的属性。因此,要从实例中访问类级别的属性,例如,使用 print(c.class.x)可能是最好的选择。

您还可以在类主体外绑定或解绑类属性。例如:

`class` C2:
    `pass`
C2.x = 23
print(C2.x)                      *`# prints:`* *`23`*

如果您只在类主体内部的语句中绑定类属性,您的程序通常会更易读。但是,如果您希望在类级别而不是实例级别传递状态信息,则可能需要在其他地方重新绑定它们;如果您愿意,Python 允许您这样做。通过将属性赋值给属性,在类主体内部绑定的类属性与在类主体外部绑定或重新绑定的属性之间没有区别。

正如我们将很快讨论的那样,所有类实例共享类的所有属性。

class语句隐含地设置了一些类属性。属性 name 是在class语句中使用的Classname标识符字符串。属性 bases 是作为class语句中基类给定(或隐含)的类对象元组。例如,使用我们刚刚创建的 C1 类:

print(C1.__name__, C1.__bases__) *`# prints:`* *`C1 (<class 'object'>,)`*

类还有一个名为 dict 的属性,它是类使用的只读映射,用于保存其他属性(也可以非正式地称为类的命名空间)。

直接在类主体中的语句中,对类属性的引用必须使用简单名称,而不是完全限定名称。例如:

`class` C3:
    x = 23
    y = x + 22                   *`# must use just x`*, *`not`* *`C3.x`*

但是,在类主体中定义的方法中的语句中,对类属性的引用必须使用完全限定名称,而不是简单名称。例如:

`class` C4:
    x = 23
    `def` amethod(self):
        print(C4.x)              *`# must use C4.x or self.x`*, *`not`* *`just x!`*

属性引用(即,表达式如C.x)的语义比属性绑定更丰富。我们在“属性参考基础”中详细介绍这样的引用。

类主体中的函数定义

大多数类主体包括一些def语句,因为函数(在此上下文中称为方法)对于大多数类实例都是重要的属性。类主体中的def语句遵循“函数”中涵盖的规则。此外,类主体中定义的方法有一个强制的第一个参数,通常总是命名为 self,它引用调用方法的实例。self 参数在方法调用中起着特殊的作用,如“绑定和非绑定方法”中所述。

下面是一个包含方法定义的类的示例:

`class` C5:
    `def` hello(self):
        print('Hello')

一个类可以定义各种与其实例上的特定操作相关的特殊双下划线方法。我们在“特殊方法”中详细讨论这些方法。

类私有变量

当类主体中的语句(或主体中的方法)使用以两个下划线开头(但不以两个下划线结尾)的标识符时,例如*__ident*,Python 隐式地将标识符更改为*_Classname__ident*,其中Classname是类的名称。这种隐式更改允许类使用“私有”名称来命名属性、方法、全局变量和其他用途,从而减少意外重复使用其他地方使用的名称(特别是在子类中)的风险。

根据约定,以单个下划线开头的标识符是私有的,无论绑定它们的作用域是或不是一个类。Python 编译器不强制执行此隐私约定:由程序员来尊重它。

类文档字符串

如果类主体中的第一个语句是一个字符串文字,则编译器将该字符串绑定为类的文档字符串(或docstring);如果类主体中的第一个语句不是字符串文字,则其值为None。有关文档字符串的更多信息,请参见“文档字符串”。

描述符

描述符是一个对象,其类提供一个或多个名为 getsetdelete 的特殊方法。作为类属性的描述符控制访问该类实例上的属性的语义。粗略地说,当您访问一个实例属性时,Python 通过调用相应的描述符上的 get 来获取属性的值,如果有的话。例如:

`class` Const:  *`# class with an overriding descriptor, see later`*
    `def` __init__(self, value):
        self.__dict__['value'] = value
    `def` __set__(self, *_):  
        # silently ignore any attempt at setting
        # (a better design choice might be to raise AttributeError)
        `pass`
    `def` __get__(self, *_):
        *`# always return the constant value`*
        `return` self.__dict__['value']
    `def` __delete__(self, *_): 
        *`# silently ignore any attempt at deleting`* 
        # (a better design choice might be to raise AttributeError)
        `pass`

`class` X:
    c = Const(23)

x = X()
print(x.c)  *`# prints:`* *`23`*
x.c = 42    *`# silently ignored (unless you raise AttributeError)`*
print(x.c)  *`# prints:`* *`23`*
`del` x.c *`# silently ignored again (ditto)`*
print(x.c)  *`# prints:`* *`23`*

欲了解更多详细信息,请参阅“属性参考基础”。

覆盖和非覆盖描述符

当描述符的类提供了名为 set 的特殊方法时,该描述符称为 覆盖描述符(或者,使用较旧且令人困惑的术语,数据描述符);当描述符的类提供了 get 而没有提供 set 时,该描述符称为 非覆盖描述符

例如,函数对象的类提供了 get,但没有提供 set;因此,函数对象是非覆盖描述符。粗略地说,当您使用具有对应覆盖描述符的值分配实例属性时,Python 通过调用描述符的 set 方法设置属性值。有关详细信息,请参见“实例对象的属性”。

描述符协议的第三个双下划线方法是 delete,当使用 del 语句删除描述符实例时调用。如果不支持 del,实现 delete 并引发适当的 AttributeError 异常是一个好主意;否则,调用者将得到一个神秘的 AttributeError: delete 异常。

在线文档包含许多描述符及其相关方法的示例。

实例

要创建类的实例,请将类对象视为函数进行调用。每次调用返回一个类型为该类的新实例:

an_instance = C5()

内置函数 isinstance(i, C),其参数 C 是一个类,当 i 是类 C 或其任何子类的实例时返回 True。否则返回 False。如果 C 是类型元组(3.10+ 或使用 | 运算符连接的多个类型),isinstance 在 i 是任何给定类型的实例或子类实例时返回 True,否则返回 False

init

当一个类定义或继承了名为 init 的方法时,在调用类对象时会执行 init 方法来对新实例进行每个实例的初始化。调用时传递的参数必须对应于 init 的参数,除了参数 self。例如,考虑以下类定义:

`class` C6:
    `def` __init__(self, n):
        self.x = n

下面是创建 C6 类实例的方法:

another_instance = C6(42)

如 C6 类定义所示,init 方法通常包含绑定实例属性的语句。init 方法不能返回除 None 以外的值;如果返回其他值,Python 将引发 TypeError 异常。

init 的主要目的是绑定并创建新创建实例的属性。您也可以在 init 方法之外绑定、重新绑定或解绑实例属性。然而,当您最初在 init 方法中绑定所有类实例属性时,您的代码更易读。

init 方法不存在(且未从任何基类继承)时,必须以无参数调用类,并且新实例没有实例特定的属性。

实例对象的属性

创建实例后,可以使用点(.)运算符访问其属性(数据和方法)。例如:

an_instance.hello()                      *`# prints:`* *`Hello`*
print(another_instance.x)                *`# prints:`* *`42`*

Python 中这样的属性引用具有相当丰富的语义;我们会在“属性引用基础”中详细介绍它们。

你可以通过将值绑定到属性引用来为实例对象添加属性。例如:

`class` C7:
    `pass`
z = C7()
z.x = 23
print(z.x)                               *`# prints:`* *`23`*

实例对象z现在有一个名为 x 的属性,绑定到值 23,z.x 引用该属性。如果存在 setattr 特殊方法,则会拦截每次绑定属性的尝试。(我们在表 4-1 中介绍了 setattr。)

当尝试绑定到一个实例属性时,如果该属性名称对应于类中的重写描述符,描述符的 set 方法会拦截该尝试:如果 C7.x 是一个重写描述符,z.x=23 会执行 type(z).x.set(z, 23)。

创建实例会设置两个实例属性。对于任何实例zz.classz所属的类对象,z.dictz用来保存其其他属性的映射。例如,对于我们刚创建的实例z

print(z.__class__.__name__, z.__dict__)  *`# prints:`* *`C7 {'x':23}`*

你可以重新绑定(但不能解绑)这两个属性中的任何一个或两个,但这很少是必要的。

对于任何实例z、任何对象x和任何标识符S(除了 classdict),z.S=x 等同于 z.dict['S']=x(除非 setattr 特殊方法或重写描述符的 set 特殊方法拦截绑定尝试)。例如,再次引用我们刚创建的z

z.y = 45
z.__dict__['z'] = 67
print(z.x, z.y, z.z)                     *`# prints:`* *`23 45 67`*

在 Python 中,通过给属性赋值或显式绑定z.dict 中的条目创建的实例属性之间没有区别。

工厂函数习语

经常需要根据某些条件创建不同类的实例,或者如果可重用的实例已存在则避免创建新实例。一个常见的误解是通过让 init 方法返回特定对象来满足这些需求。然而,这种方法行不通:如果 init 返回除了None之外的任何值,Python 会引发异常。实现灵活对象创建的最佳方式是使用函数而不是直接调用类对象。以这种方式使用的函数称为工厂函数

调用工厂函数是一种灵活的方法:一个函数可以返回一个现有可重用的实例或通过调用适当的类创建一个新实例。假设你有两个几乎可以互换的类,SpecialCase 和 NormalCase,并且想要根据参数灵活地生成其中任何一个类的实例。下面这个适当的工厂函数示例允许你做到这一点(我们将在“绑定和未绑定方法”中更多地讨论 self 参数):

`class` SpecialCase:
    `def` amethod(self):
        print('special')
`class` NormalCase:
    `def` amethod(self):
        print('normal')
`def` appropriate_case(isnormal=`True`):
    `if` isnormal:
        `return` NormalCase()
    `else``:`
 `return` SpecialCase()
aninstance = appropriate_case(isnormal=`False`)
aninstance.amethod()                  *`# prints:`* *`special`*

new

每个类都有(或继承)一个名为 new 的类方法(我们在“类方法”中讨论)。当您调用C(args, **kwds)来创建类C的新实例时,Python 首先调用C.new(C, args, **kwds),并使用 new 的返回值x作为新创建的实例。然后 Python 调用C.init(x, args, **kwds),但仅当x确实是C或其任何子类的实例时(否则,x的状态将保持 new 留下的状态)。因此,例如,语句x*=C(23)等同于:

*`x`* = *`C`*.__new__(*`C`*, 23)
`if` isinstance(*`x`*, *`C`*):
    type(*`x`*).__init__(*`x`*, 23)

object.new 创建一个新的未初始化实例,该实例作为其第一个参数接收的类的实例。当该类具有 init 方法时,它会忽略其他参数,但当它接收到第一个参数之外的其他参数,并且第一个参数的类没有 init 方法时,它会引发异常。当您在类体内部重写 new 时,您无需添加 new=classmethod(new),也不需要使用@classmethod 装饰器,因为 Python 在此上下文中识别名称 new 并将其视为特殊名称。在那些偶发情况下,您稍后在类C的体外重新绑定C.new 时,您确实需要使用C.new=classmethod(whatever)。

new 具有工厂函数的大部分灵活性,如前一节所述。new 可以选择返回现有实例或根据需要创建新实例。当 new 确实创建新实例时,它通常将创建委托给 object.newC的另一个超类的 new 方法。

以下示例显示如何重写类方法 new 以实现 Singleton 设计模式的版本:

`class` Singleton:
    _singletons = {}
    `def` __new__(cls, *args, **kwds):
        `if` cls `not` `in` cls._singletons:
            cls._singletons[cls] = obj = super().__new__(cls)
            obj._initialized = False
        `return` cls._singletons[cls]

(我们在“协作超类方法调用”中介绍了内置的 super。)

任何 Singleton 的子类(不进一步重写 new 的子类)都有且仅有一个实例。当子类定义 init 时,它必须确保 init 可以安全地重复调用(在每次子类调用时)子类的唯一实例。³ 在此示例中,我们插入 _initialized 属性,设置为False,当 new 实际上创建新实例时。子类的 init 方法可以测试 self._initialized 是否为False,如果是,则将其设置为True并继续执行 init 方法的其余部分。当后续创建单例实例再次调用 init 时,self._initialized 将为True,表示实例已初始化,并且 init 通常可以直接返回,避免某些重复工作。

属性引用基础知识

属性引用是形式为x.name的表达式,其中x是任何表达式,name是称为属性名称的标识符。许多 Python 对象都有属性,但是当x引用类或实例时,属性引用具有特殊而丰富的语义。方法也是属性,因此我们对一般属性的所有说法也适用于可调用属性(即方法)。

假设x是类C的实例,该类继承自基类B。这些类和实例都有几个属性(数据和方法),如下所示:

`class` B:
    a = 23
    b = 45
    `def` f(self):
        print('method f in class B')
    `def` g(self):
        print('method g in class B')
`class` C(B):
    b = 67
    c = 89
    d = 123
    `def` g(self):
        print('method g in class C')
    `def` h(self):
        print('method h in class C')
x = C()
x.d = 77
x.e = 88

几个属性 dunder 名称是特殊的。C.name 是字符串'C',类的名称。C.bases 是元组(B,),C的基类的元组。x.classx所属的类C。当您使用这些特殊名称之一引用属性时,属性引用直接查找类或实例对象中的专用槽,并获取找到的值。您不能解绑这些属性。您可以即时重新绑定它们,更改类或实例的名称或基类,但这种高级技术很少必要。

C和实例x各自还有一个特殊属性:名为 dict 的映射(对x通常是可变的,但对C不是)。类或实例的所有其他属性,⁴除了少数特殊属性外,都保存为类或实例的 dict 属性中的项。

从类获取属性

当您使用语法C.name引用类对象C的属性时,查找进行两个步骤:

  1. 当'name'是C.dict 中的键时,C.nameC.dict['name']中获取值v。然后,当v是描述符时(即,type(v)提供名为 get 的方法),C.name的值是调用 type(v).get(v, None, C)的结果。当v不是描述符时,C.name的值是v

  2. 当'name'不是C.dict 中的键时,C.name将查找委托给C的基类,这意味着它在C的祖先类上循环,并在每个类上尝试name查找(按照"继承"中详述的方法解析顺序)。

从实例获取属性

当您使用语法x.name引用类C的实例x的属性时,查找进行三个步骤:

  1. 当'name'出现在C(或C的祖先类之一)中作为覆盖描述符v的名称时(即,type(v)提供方法 getset),x.name的值是 type(v).get(v, x, C)的结果。

  2. 否则,当'name'是x.dict 中的键时,x.name获取并返回x.dict['name']中的值。

  3. 否则,x.name将查找委托给x的类(按照与C.name相同的两步查找过程):

    • 当这找到描述符v时,属性查找的整体结果再次是 type(v).get(v, x, C)。

    • 当此查找到一个非描述符值v时,属性查找的整体结果就是v

当这些查找步骤未找到属性时,Python 会引发 AttributeError 异常。然而,对于x.name的查找,当C定义或继承特殊方法 getattr 时,Python 会调用C.getattr(*x, 'name')而不是引发异常。然后由 getattr 决定返回适当的值或引发适当的异常,通常是 AttributeError。

考虑前面定义的以下属性引用:

print(x.e, x.d, x.c, x.b, x.a)             *`# prints:`* *`88 77 89 67 23`*

当在步骤 2 的实例查找过程中,x.e 和 x.d 成功时,因为没有涉及描述符,并且'e'和'd'都是 x.dict 中的键。因此,查找不会继续,而是返回 88 和 77。另外三个引用必须继续到步骤 3 的实例查找过程,并查找 x.class(即 C)。x.c 和 x.b 在类查找过程的步骤 1 中成功,因为'c'和'b'都是 C.dict 中的键。因此,查找不会继续,而是返回 89 和 67。x.a 一直到类查找过程的步骤 2,查找 C.bases[0](即 B)。'a'是 B.dict 中的键;因此,x.a 最终成功并返回 23。

设置一个属性

注意,属性查找步骤只有在引用属性时才会像刚才描述的那样发生,而在绑定属性时不会。当绑定到一个名称不是特殊的类或实例属性时(除非 setattr 方法或覆盖描述符的 set 方法拦截实例属性的绑定),你只影响该属性的 dict 条目(在类或实例中分别)。换句话说,对于属性绑定,除了检查覆盖描述符外,不涉及查找过程。

绑定方法和非绑定方法

函数对象的 get 方法可以返回函数对象本身,或者包装该函数的绑定方法对象;绑定方法与从特定实例获取它时关联。

在前一节的代码中,属性 f、g 和 h 是函数;因此,对它们中的任何一个进行属性引用都会返回一个包装相应函数的方法对象。考虑以下内容:

print(x.h, x.g, x.f, C.h, C.g, C.f)

此语句输出三个绑定方法,用如下字符串表示:

<bound method C.h of <__main__.C object at 0x8156d5c>>

然后是三个函数对象,用字符串表示如下:

<function C.h at 0x102cabae8>

绑定方法与函数对象的比较

当属性引用在实例x上时,我们得到绑定方法,而当属性引用在类C上时,我们得到函数对象。

因为绑定方法已经与特定实例关联,所以可以按以下方式调用方法:

x.h()                   *`# prints:`* *`method h in class C`*

这里需要注意的关键点是,你不会通过通常的参数传递语法传递方法的第一个参数 self。相反,实例 x 的绑定方法会将 self 参数隐式绑定到对象 x。因此,方法体可以访问实例的属性,就像它们是 self 的属性一样,即使我们没有显式地向方法传递参数。

让我们仔细看看绑定方法。当实例上的属性引用在查找过程中找到一个在实例类中作为属性的函数对象时,查找会调用函数的 get 方法来获取属性的值。在这种情况下,调用会创建并返回一个绑定方法,它包装了该函数。

注意,当属性引用的查找直接在 x.dict 中找到一个函数对象时,属性引用操作不会创建绑定方法。在这种情况下,Python 不会将函数视为描述符,也不会调用函数的 get 方法;相反,函数对象本身就是属性的值。同样地,对于不是普通函数的可调用对象,如内置函数(而不是 Python 编写的函数),Python 不会创建绑定方法。

除了包装的函数对象的属性外,绑定方法还有三个只读属性:im_class 是提供方法的类对象,im_func 是被包装的函数,im_self 指的是来自你获取方法的实例 x

你可以像使用其 im_func 函数一样使用绑定方法,但是对绑定方法的调用不会显式提供一个对应于第一个参数(通常命名为 self)的参数。当你调用绑定方法时,在给定调用点的其他参数(如果有)之前,绑定方法会将 im_self 作为第一个参数传递给 im_func。

让我们详细地跟随一下使用常规语法 x.name(arg) 进行方法调用所涉及的概念步骤。

`def` f(a, b): ...              *`# a function f with two arguments`*

`class` C:
    name = f
x = C()

x 是类 C 的实例对象,name 是 x 的方法名称(C 的属性,其值是一个函数,在本例中是函数 f 的属性),arg是任何表达式。Python 首先检查'name'是否是 C 中覆盖描述符的属性名称,但它不是——函数是描述符,因为它们的类型定义了方法 get,但不是覆盖的描述符,因为它们的类型没有定义方法 set。Python 接下来检查'name'是否是 x.dict_ 中的一个键,但它不是。所以,Python 在 C 中找到了 name(如果 name 通过继承在 C 的一个 bases 中找到,则一切都将同样工作)。Python 注意到属性的值,函数对象 f,是一个描述符。因此,Python 调用 f.get(x, C),返回一个绑定方法对象,其 im_func 设置为 f,im_class 设置为 C,im_self 设置为 x。然后 Python 调用这个绑定方法对象,arg作为唯一的参数。绑定方法将 im_self(即 x)插入为调用绑定方法的 im_func(即函数 f)的第一个参数,arg成为第二个参数。整体效果就像调用:

x.__class__.__dict__'name'

当绑定方法的函数体执行时,它与其 self 对象或任何类之间没有特殊的命名空间关系。引用的变量是局部或全局的,就像任何其他函数一样,详见“命名空间”。变量不会隐式地指示 self 中的属性,也不会指示任何类对象中的属性。当方法需要引用、绑定或解绑其 self 对象的属性时,它通过标准的属性引用语法来完成(例如,self.name)。⁵ 缺乏隐式作用域可能需要一些时间来适应(因为在这一点上,Python 与许多面向对象的语言不同),但它确保了清晰性、简单性并消除了潜在的歧义。

绑定方法对象是一类一等公民对象:你可以在任何可调用对象的地方使用它们。由于绑定方法同时持有对其包装的函数和执行它的 self 对象的引用,它是闭包的一个强大而灵活的替代方案(详见“嵌套函数和嵌套作用域”)。如果一个实例对象的类提供了特殊方法 call(详见表 4-1),那么这是另一种可行的替代方案。这些构造允许你将一些行为(代码)和一些状态(数据)打包到一个可调用对象中。闭包最简单,但在适用性上有些限制。以下是来自嵌套函数和嵌套作用域部分的闭包示例:

`def` make_adder_as_closure(augend):
    `def` add(addend, _augend=augend):
        `return` addend + _augend
    `return` add

绑定方法和可调用实例比闭包更丰富和灵活。以下是如何使用绑定方法实现相同功能的方式:

`def` make_adder_as_bound_method(augend):
    `class` Adder:
        `def` __init__(self, augend):
            self.augend = augend
        `def` add(self, addend):
            `return` addend+self.augend
    `return` Adder(augend).add

这是如何使用可调用实例(一个其类提供特殊方法 call 的实例)来实现它的方式:

`def` make_adder_as_callable_instance(augend):
    `class` Adder:
        `def` __init__(self, augend):
            self.augend = augend
        `def` __call__(self, addend):
            `return` addend+self.augend
    `return` Adder(augend)

从调用函数的代码视角来看,所有这些工厂函数都是可互换的,因为它们都返回多态的可调用对象。在实现方面,闭包是最简单的;面向对象的方法,即绑定方法和可调用实例,使用更灵活、通用和强大的机制,但在这个简单的例子中并不需要这种额外的功能(因为除了加数之外,不需要其他状态,闭包和面向对象方法都可以轻松处理)。

继承

当您在类对象C上使用属性引用C.name,并且'name'不是C.dict 中的键时,查找将隐式地在C.bases 中的每个类对象上进行,按特定顺序(由于历史原因称为方法解析顺序或 MRO,但实际上适用于所有属性,而不仅仅是方法)。C的基类可能会有它们自己的基类。查找将逐个在 MRO 中的直接和间接祖先中进行,停止在找到'name'时。

方法解析顺序

在类中查找属性名的查找基本上是通过按左到右、深度优先顺序访问祖先类来进行的。然而,在多重继承的情况下(使得继承图成为一般有向无环图(DAG),而不仅仅是特定的树),这种简单方法可能导致某些祖先类被访问两次。在这种情况下,解析顺序在查找序列中只保留任何给定类的最右出现。

每个类和内置类型都有一个特殊的只读类属性称为 mro,它是用于方法解析的类型元组,按顺序排列。只能在类上引用 mro,而不能在实例上引用,并且由于 mro 是只读属性,因此无法重新绑定或解绑。有关 Python MRO 的所有方面的详细且高度技术性的解释,请参阅 Michele Simionato 的文章“Python 2.3 方法解析顺序”⁶和 Guido van Rossum 关于“Python 历史”的文章。特别要注意,Python 可能无法确定某个类的任何明确的 MRO:在这种情况下,当 Python 执行该语句时会引发 TypeError 异常。

覆盖属性

正如我们刚刚看到的,对属性的搜索沿着 MRO(通常是沿着继承树向上)进行,并且一旦找到属性就会停止。子类始终在其祖先之前进行检查,因此当子类定义与超类中同名的属性时,搜索将找到子类中的定义并在此处停止。这被称为子类覆盖超类中的定义。考虑以下代码:

`class` B:
    a = 23
    b = 45
    `def` f(self):
        print('method f in class B')
    `def` g(self):
        print('method g in class B')
`class` C(B):
    b = 67
    c = 89
    d = 123
    `def` g(self):
        print('method g in class C')
    `def` h(self):
        print('method h in class C')

在这里,类 C 覆盖了其超类 B 的属性 b 和 g。请注意,与某些其他语言不同,Python 中你可以像轻松覆盖可调用属性(方法)一样覆盖数据属性。

委托给超类方法

当子类 C 覆盖其超类 B 的方法 f 时,Cf 方法体通常希望将其操作的某部分委托给超类方法的实现。这有时可以使用函数对象来完成,如下所示:

`class` Base:
    `def` greet(self, name):
        print('Welcome', name)
`class` Sub(Base):
    `def` greet(self, name):
        print('Well Met and', end=' ')
        Base.greet(self, name)
x = Sub()
x.greet('Alex')

在 Sub 类的 greet 方法体中,委托到超类的方法使用了通过属性引用 Base.greet 获得的函数对象,因此通常会传递所有参数,包括 self。(如果显式使用基类看起来有点丑陋,请耐心等待;在本节中很快你会看到更好的方法)。委托到超类实现是这种函数对象的常见用法。

委托(Delegation)的一种常见用法出现在特殊方法 init 中。当 Python 创建一个实例时,不像一些其他面向对象的语言那样自动调用任何基类的 init 方法。这由子类来初始化其超类,必要时使用委托。例如:

`class` Base:
    `def` __init__(self):
        self.anattribute = 23
`class` Derived(Base):
    `def` __init__(self):
        Base.__init__(self)
        self.anotherattribute = 45

如果 Derived 类的 init 方法没有显式调用 Base 类的 init 方法,那么 Derived 的实例将缺少其初始化的部分。因此,这些实例将违反 里氏替换原则(LSP),因为它们将缺少属性 anattribute。如果子类不定义 init,则不会出现此问题,因为在这种情况下,它会从超类继承 init。因此,绝对不有理由编写以下代码:

`class` Derived(Base):
    `def` __init__(self):
        Base.__init__(self)

绝对不要编写仅委托给超类的方法。

永远不要定义一个语义上空的 init(即仅委托给超类的方法)。相反,应该从超类继承 init。这条建议适用于所有方法,特殊的或不是,但出于某种原因,编码这种语义上空的方法似乎最常见于 init

上述代码说明了将委托概念应用于对象的超类,但在今天的 Python 中,通过名称显式编码这些超类实际上是一种不良实践。如果基类重命名,所有对它的调用点都必须更新。或者更糟的是,如果重构类层次结构在 Derived 和 Base 类之间引入新层,则新插入类的方法将被静默跳过。

推荐的方法是使用内置的 super 类型调用定义在超类中的方法。要调用继承链中的方法,只需调用 super(),不带参数:

`class` Derived(Base):
    `def` __init__(self):
        super().__init__()
        self.anotherattribute = 45

合作超类方法调用

在多重继承和所谓的“菱形图”情况下,使用超类名称显式调用超类版本的方法也会带来很多问题。考虑以下代码:

`class` A:
    `def` met(self):
        print('A.met')
`class` B(A):
    `def` met(self):
        print('B.met')
        A.met(self)
`class` C(A):
    `def` met(self):
        print('C.met')
        A.met(self)
`class` D(B,C):
    `def` met(self):
        print('D.met')
        B.met(self)
        C.met(self)

当我们调用 D().met() 时,A.met 实际上被调用了两次。如何确保每个祖先方法的实现仅被调用一次?解决方案是使用 super:

`class` A:
    `def` met(self):
        print('A.met')
`class` B(A):
    `def` met(self):
        print('B.met')
        super().met()
`class` C(A):
    `def` met(self):
        print('C.met')
        super().met()
`class` D(B,C):
    `def` met(self):
        print('D.met')
        super().met()

现在,D().met() 将确保每个类的 met 方法仅被调用一次。如果你养成了使用 super 来编码超类调用的好习惯,你的类将在复杂的继承结构中表现得很顺畅——即使继承结构实际上很简单也不会有任何负面影响。

唯一的情况可能更喜欢通过显式语法调用超类方法的粗糙方法是,当不同类具有相同方法的不同和不兼容签名时。在许多方面,这种情况令人不快;如果你确实必须处理它,显式语法有时可能是最不受欢迎的方法。正确使用多重继承受到严重阻碍;但是,即使在面向对象编程的最基本属性中,如基类和子类实例之间的多态性中,在超类和其子类中为相同名称的方法指定不同签名时,也会受到影响。

使用内置函数 type 进行动态类定义

除了使用 type(obj) 的方式外,你还可以使用三个参数调用 type 来定义一个新的类:

NewClass = type(name, bases, class_attributes, **kwargs)

其中 name 是新类的名称(应与目标变量匹配),bases 是直接超类的元组,class_attributes 是要在新类中定义的类级方法和属性的字典,**kwargs 是要传递给其中一个基类的元类的可选命名参数。

例如,使用简单的 Vehicle 类层次结构(如 LandVehicle、WaterVehicle、AirVehicle、SpaceVehicle 等),你可以在运行时动态创建混合类,如:

AmphibiousVehicle = type('AmphibiousVehicle', 
                         (LandVehicle, WaterVehicle), {})

这相当于定义一个多重继承的类:

`class` AmphibiousVehicle(LandVehicle, WaterVehicle): `pass`

当你调用 type 在运行时创建类时,你无需手动定义所有 Vehicle 子类的组合扩展,并且添加新的子类也不需要大量扩展已定义的混合类。⁷ 欲了解更多注解和示例,请参阅在线文档

“删除”类属性

继承和重写提供了一种简单有效的方式来非侵入性地添加或修改(重写)类属性(如方法)——即在子类中添加或重写属性而无需修改定义属性的基类。然而,继承并未提供一种非侵入性地删除(隐藏)基类属性的方法。如果子类简单地未定义(重写)某个属性,则 Python 会找到基类的定义。如果需要执行此类删除操作,则可能的选择包括以下几种:

  • 重写方法并在方法体中引发异常。

  • 避免继承,将属性保存在子类的 dict 之外,并为选择性委派定义 getattr 方法。

  • 覆盖 getattribute 以类似的效果。

这些技术的最后一个在 “getattribute” 中演示。

考虑使用聚合而不是继承

继承的替代方法是使用 聚合:而不是从基类继承,而是将基类的实例作为私有属性。通过在包含类中提供公共方法(即调用属性上的等效方法)委托给包含的属性,您可以完全控制属性的生命周期和公共接口。这样,包含类对于属性的创建和删除有更多的控制权;此外,对于属性类提供的任何不需要的方法,您只需不在包含类中编写委派方法即可。

内置的 object 类型

内置的 object 类型是所有内置类型和类的祖先。object 类型定义了一些特殊方法(在 “特殊方法” 中记录),实现了对象的默认语义:

new, init

您可以通过调用 object() 而不传递任何参数来创建对象的直接实例。该调用使用 object.new 和 object.init 来创建并返回一个没有属性(甚至没有用于保存属性的 dict)的实例对象。这样的实例对象可能作为“哨兵”非常有用,确保与任何其他不同对象比较时不相等。

delattr, getattr, getattribute, setattr

默认情况下,任何对象都使用对象的这些方法处理属性引用(如 “属性引用基础知识” 中所述)。

hash, repr, str

将对象传递给 hash、repr 或 str 调用对象的相应 dunder 方法。

对象的子类(即任何类)可以——而且通常会!——覆盖这些方法中的任何一个,和/或添加其他方法。

类级方法

Python 提供了两种内置的非覆盖描述符类型,这使得类具有两种不同类型的“类级方法”:静态方法类方法

静态方法

静态方法 是可以在类上调用,或者在类的任何实例上调用的方法,而不受普通方法关于第一个参数的特殊行为和约束的影响。静态方法可以具有任何签名;它可以没有参数,并且如果有的话,第一个参数也不起任何特殊作用。您可以将静态方法视为一种普通函数,您可以正常调用它,尽管它恰好绑定到类属性上。

虽然定义静态方法从未 必需(您可以选择定义一个普通函数,而不是在类外部定义),但某些程序员认为它们是一种优雅的语法替代品,当函数的目的与某个特定类紧密绑定时。

要创建一个静态方法,调用内置的 type staticmethod,并将其结果绑定到一个类属性。与所有绑定类属性的方式一样,通常应在类的主体中完成,但您也可以选择在其他地方执行。staticmethod 的唯一参数是 Python 调用静态方法时要调用的函数。以下示例展示了定义和调用静态方法的一种方式:

`class` AClass:
    `def` astatic():
        print('a static method')
    astatic = staticmethod(astatic)

an_instance = AClass()
print(AClass.astatic())             *`# prints:`* *`a static method`*
print(an_instance.astatic())        *`# prints:`* *`a static method`*

此示例将同一名称用于传递给 staticmethod 的函数和绑定到 staticmethod 结果的属性。这种命名惯例并非强制性,但是是个好主意,我们建议您始终使用它。Python 提供了一种特殊的简化语法来支持这种风格,详见“装饰器”。

类方法

类方法是您可以在类上或在类的任何实例上调用的方法。Python 将方法的第一个参数绑定到调用该方法的类或调用该方法的实例的类;它不将其绑定到实例,如普通绑定方法。类方法的第一个参数通常被命名为 cls。

与静态方法一样,虽然定义类方法从不是必需的(您始终可以选择在类外定义一个普通函数,并将类对象作为其第一个参数),但类方法是这种函数的一种优雅替代方式(特别是在需要在子类中重写它们时)。

要创建一个类方法,调用内置的 type classmethod,并将其结果绑定到一个类属性。与所有绑定类属性的方式一样,通常应在类的主体中完成,但您也可以选择在其他地方执行。classmethod 的唯一参数是 Python 调用类方法时要调用的函数。以下是定义和调用类方法的一种方式:

`class` ABase:
    `def` aclassmet(cls):
        print('a class method for', cls.__name__)
    aclassmet = classmethod(aclassmet)
`class` `ADeriv`(ABase):
    `pass`

b_instance = ABase()
d_instance = ADeriv()
print(ABase.aclassmet())        *`# prints:`* *`a class method for ABase`*
print(b_instance.aclassmet())   *`# prints:`* *`a class method for ABase`*
print(ADeriv.aclassmet())       *`# prints:`* *`a class method for ADeriv`*
print(d_instance.aclassmet())   *`# prints:`* *`a class method for ADeriv`*

此示例将同一名称用于传递给 classmethod 的函数和绑定到 classmethod 结果的属性。同样,这种命名约定并非强制性,但是是个好主意,我们建议您始终使用它。Python 提供了一种特殊的简化语法来支持这种风格,详见“装饰器”。

属性

Python 提供了一种内置的重写描述符类型,可用于给类的实例提供属性。属性是具有特殊功能的实例属性。您可以使用普通语法(例如,print(x.prop),x.prop=23,del x.prop)引用、绑定或解绑属性。但是,与通常的属性引用、绑定和解绑语义不同,这些访问会在实例x上调用您作为内置 type property 的参数指定的方法。以下是定义只读属性的一种方式:

`class` Rectangle:
    `def` __init__(self, width, height):
        self.width = width
        self.height = height
    `def` area(self):
        `return` self.width * self.height
    area = property(area, doc='area of the rectangle')

类 Rectangle 的每个实例r都有一个合成的只读属性r.area,方法r.area()通过动态计算乘以边的方法来生成。Rectangle.area.doc 的文档字符串是'rectangle 的面积'。r.area 属性是只读的(尝试重新绑定或解绑它会失败),因为我们在 property 调用中仅指定了一个 get 方法,而没有 set 或 del 方法。

属性执行与特殊方法 getattrsetattrdelattr(在“通用特殊方法”中介绍)类似的任务,但属性更快更简单。要构建一个属性,请调用内置类型 property 并将其结果绑定到一个类属性。与类属性的所有绑定一样,通常在类的主体中完成,但您可以选择在其他地方完成。在类C的主体内部,您可以使用以下语法:

*`attrib`* = property(fget=`None`, fset=`None`, fdel=`None`, doc=`None`)

x是类C的一个实例,并且您引用x.attrib时,Python 会在x上调用作为 fget 参数传递给属性构造函数的方法,不带参数。当您赋值x.attrib = value时,Python 会调用作为 fset 参数传递的方法,并将value作为唯一的参数传递给它。当您执行del x.attrib时,Python 会调用作为 fdel 参数传递的方法,不带参数。Python 使用作为 doc 参数传递的参数作为属性的文档字符串。属性的所有参数都是可选的。当缺少某个参数时,当某些代码尝试进行该操作时,Python 会引发异常。例如,在矩形示例中,我们使属性 area 为只读,因为我们仅为参数 fget 传递了一个参数,而没有为参数 fset 和 fdel 传递参数。

在类中创建属性的一种优雅语法是使用 property 作为装饰器(参见“装饰器”):

`class` Rectangle:
    `def` __init__(self, width, height):
        self.width = width
        self.height = height
    @property
    `def` area(self):
        *`"""area of the rectangle"""`*
        `return` self.width * self.height

要使用这种语法,您必须将 getter 方法命名为您希望属性具有的相同名称;该方法的文档字符串将成为属性的文档字符串。如果您还想添加设置器和/或删除器,请使用名为(在此示例中)area.setter 和 area.deleter 的装饰器,并将如此装饰的方法命名为属性的相同名称。例如:

`import` math
`class` Rectangle:
    `def` __init__(self, width, height):
        self.width = width
        self.height = height
    @property
    `def` area(self):
        *`"""area of the rectangle"""`*
        `return` self.width * self.height
    @area.setter
    `def` area(self, value):
        scale = math.sqrt(value/self.area)
        self.width *= scale
        self.height *= scale

为什么属性很重要

属性的关键重要性在于它们的存在使得将公共数据属性作为类公共接口的一部分完全安全(事实上也是建议性的)。如果在将来的类版本或者需要与之多态的其他类中,需要在引用、重新绑定或解绑属性时执行一些代码,您可以将普通属性更改为属性,并获得所需效果,而不会对使用您的类的任何代码(即“客户端代码”)产生任何影响。这让您可以避免面向对象语言中缺乏属性而需要使用的笨拙惯用法,如访问器修改器方法。例如,客户端代码可以使用如下自然的惯用法:

some_instance.widget_count += 1

而不是被迫进入这样的复杂嵌套访问器和修改器:

some_instance.set_widget_count(some_instance.get_widget_count() + 1)

如果你有时候想要编写方法的自然名称像 get_this 或 set_that,最好将这些方法包装成属性,以增加代码的清晰度。

属性和继承

属性的继承与任何其他属性一样工作。然而,对于不留心的人来说,有一个小陷阱:用于访问属性的方法是在定义属性的类中定义的方法,而不是使用后续在子类中发生的进一步覆盖。考虑这个例子:

`class` B:
    `def` f(self):
        `return` 23
    g = property(f)
`class` C(B):
    `def` f(self):
        `return` 42

c = C()
print(c.g)                *`# prints:`* *`23`*, *`not`* *`42`*

访问属性 c.g 会调用 B.f,而不是你可能期望的 C.f。原因非常简单:属性构造函数(直接或通过装饰器语法)接收的是 函数对象 f(这发生在执行 B 的 class 语句时,因此问题中的函数对象也称为 B.f)。因此,稍后在子类 C 中重新定义名称 f 是无关紧要的,因为属性在创建时并不会查找该名称,而是使用它在创建时收到的函数对象。如果需要解决这个问题,可以通过手动添加额外的查找间接性来实现:

`class` B:
    `def` f(self):
        `return` 23
    `def` _f_getter(self):
        `return` self.f()
    g = property(_f_getter)
`class` C(B):
    `def` f(self):
        `return` 42

c = C()
print(c.g)                *`# prints:`* *`42`**`,`* *`as expected`*

在这里,属性所持有的函数对象是 B._f_getter,它反过来确实会查找名称 f(因为它调用 self.f());因此,对 f 的覆盖具有预期的效果。正如 David Wheeler 所说,“计算机科学中的所有问题都可以通过另一级间接性来解决。”⁸

slots

通常,任何类 C 的实例对象 x 都有一个字典 x.dict,Python 使用它让你在 x 上绑定任意属性。为了节省一点内存(以只允许 x 有预定义的一组属性名称为代价),可以在类 C 中定义一个类属性名为 slots,一个序列(通常是元组)的字符串(通常是标识符)。当类 Cslots 时,类 C 的实例 x 就没有 dict:试图在 x 上绑定一个不在 C.slots 中的属性名将会引发异常。

使用 slots 可以减少小实例对象的内存消耗,这些对象可以没有强大和便利的能力拥有任意命名的属性。只有在类可能同时有成百上千个实例的情况下,才值得为类添加 slots,以便每个实例节省几十个字节的内存。然而,与大多数其他类属性不同,只有在类体中的赋值将其绑定为类属性时,slots 才能像我们刚才描述的那样工作。以后对 slots 的任何更改、重绑定或取消绑定,以及从基类继承 slots 都没有效果。以下是如何在前面定义的 Rectangle 类中添加 slots 以获得更小(虽然不太灵活)的实例:

`class` OptimizedRectangle(Rectangle):
    __slots__ = 'width', 'height'

不需要为 area 属性定义槽:slots 不限制属性,只限制普通实例属性,如果没有定义 slots,则这些属性将存储在实例的 dict 中。

3.8+ slots 属性也可以使用以属性名称为键和以文档字符串为值的字典来定义。OptimizedRectangle 可以更详细地声明为:

`class` OptimizedRectangle(Rectangle):
    __slots__ = {'width': 'rectangle width in pixels',
                 'height': 'rectangle height in pixels'}

getattribute

所有对实例属性的引用都通过特殊方法 getattribute。这个方法来自 object,在那里它实现了属性引用的语义(如“属性引用基础”中所述)。你可以覆盖 getattribute 来隐藏子类实例的继承类属性等目的。例如,以下示例展示了一种实现无需 append 方法的列表的方法:

`class` listNoAppend(list):
    `def` __getattribute__(self, name):
        `if` name == 'append':
            `raise` AttributeError(name)
        `return` list.__getattribute__(self, name)

类 listNoAppend 的实例 x 几乎与内置的列表对象无法区分,唯一的区别是其运行时性能显著较差,并且任何对 x.append 的引用都会引发异常。

实现 getattribute 可能会比较棘手;使用内置函数 getattr 和 setattr 以及实例的 dict(如果有的话)或重新实现 getattrsetattr 通常更容易。当然,在某些情况下(如前面的例子),没有其他选择。

每个实例方法

实例可以具有所有属性的实例特定绑定,包括可调用属性(方法)。对于方法,与任何其他属性(除了绑定到覆盖描述符的属性之外),实例特定绑定会隐藏类级绑定:属性查找在找到直接在实例中绑定时不考虑类。对于可调用属性的实例特定绑定不执行“绑定和非绑定方法”中详细描述的任何转换:属性引用返回完全相同的可调用对象,该对象之前直接绑定到实例属性。

然而,这对于 Python 隐式调用的各种操作的每个实例绑定的特殊方法可能不像你期望的那样工作,如“特殊方法”中所述。这些特殊方法的隐式使用总是依赖于特殊方法的类级绑定(如果有的话)。例如:

`def` fake_get_item(idx):
    `return` idx
`class` MyClass:
    `pass`
n = MyClass()
n.__getitem__ = fake_get_item
print(n[23])                      *`# results in:`*
*`# Traceback (most recent call last):`*
*`#   File "<stdin>", line 1, in ?`*
*`# TypeError: unindexable object`*

从内置类型继承

类可以从内置类型继承。但是,类只能直接或间接扩展多个内置类型,前提是这些类型专门设计为允许这种互操作兼容性。Python 不支持从多个任意内置类型无约束地继承。通常,新式类最多只扩展一个实质性的内置类型。例如,这样:

`class` noway(dict, list):
    `pass`

引发 TypeError 异常,并详细说明“多个基类具有实例布局冲突。” 当你看到这样的错误消息时,意味着你试图直接或间接地从多个不特别设计以在如此深层次上合作的内置类型继承。

特殊方法

一个类可以定义或继承特殊方法,通常被称为“dunder”方法,因为如前所述,它们的名称前后都有双下划线。每个特殊方法与特定操作相关联。Python 在你对实例对象执行相关操作时会隐式调用特殊方法。在大多数情况下,方法的返回值是操作的结果,当操作所关联的方法不存在时,尝试该操作会引发异常。

在本节中,我们指出一般规则不适用的情况。在以下讨论中,x 是执行操作的类 C 的实例,y 是另一个操作数(如果有的话)。每个方法的参数 self 也指代实例对象 x。每当我们提到对 x.whatever(...) 的调用时,请记住,严格来说,正在发生的确切调用实际上是 x.class.whatever(x, ...)。

通用特殊方法

一些 dunder 方法与通用操作相关。定义或继承这些方法的类允许其实例控制这些操作。这些操作可以分为几类:

初始化和结束处理

类可以通过特殊方法 newinit 控制其实例的初始化(这是一个非常常见的需求),并且/或者通过 del 控制其终结处理(这是一个罕见的需求)。

字符串表示

类可以通过特殊方法 repr, str, formatbytes 控制 Python 如何将其实例呈现为字符串。

比较、哈希和在布尔上下文中的使用

一个类可以控制其实例如何与其他对象比较(通过特殊方法 lt, le, gt, ge, eqne),字典如何将其用作键以及集合如何将其用作成员(通过 hash),以及它们在布尔上下文中是否评估为真值或假值(通过 bool)。

属性引用、绑定和解绑

一个类可以通过特殊方法 getattribute, getattr, setattrdelattr 控制对其实例属性(引用、绑定、解绑)的访问。

可调用实例

类可以通过特殊方法 call 使其实例可调用,就像函数对象一样。

表 4-1 记录了通用的特殊方法。

表 4-1. 通用特殊方法

bool bool(self) 当对x进行真假判断(参见“布尔值”)时,例如在调用 bool(x)时,Python 会调用x.bool(),该方法应返回TrueFalse。当不存在 bool 方法时,Python 会调用 len 方法,并且当x.len()返回 0 时将x视为假(为了检查容器是否非空,避免编写if len(container)>0:;而是使用if container:)。当既不存在 bool 方法也不存在 len 方法时,Python 将x视为真。
bytes bytes(self) 调用 bytes(x)时会调用x.bytes(),如果存在的话。如果一个类同时提供了 bytesstr 特殊方法,它们分别应返回“等效”的 bytes 类型和 str 类型字符串。
call call(self[, args...]) 当调用x([args...])时,Python 会将此操作转换为对x.call([args...])的调用。调用操作的参数对应于 call 方法的参数,去除第一个参数。第一个参数,通常称为 self,引用x:Python 会隐式提供它,就像对绑定方法的任何其他调用一样。

| del | del(self) 当x通过垃圾收集消失之前,Python 会调用x.del()让x完成最后操作。如果没有 del 方法,Python 在回收x时不会进行特殊处理(这是最常见的情况:很少有类需要定义 del)。Python 忽略 del 的返回值,并且不会隐式调用C类超类的 del 方法。C.del 必须显式执行任何需要的最终操作,包括必要时通过委托来完成。当类C有需要终结的基类时,C.del 必须调用 super().del()。

del 方法与“del 语句”中涵盖的del语句没有特定联系。

一般情况下,当您需要及时和确保的最终操作时,del 并不是最佳选择。对于这种需求,应使用“try/finally”中涵盖的try/finally语句(或者更好的是“The with Statement”中涵盖的with语句)。定义有 del 方法的类的实例不参与循环垃圾收集,详见“垃圾收集”。注意避免涉及这些实例的引用循环:只有在没有可行的替代方案时才定义 del

delattr delattr(self, name) 每次请求解绑属性x.y(通常是del x.y),Python 会调用x.delattr('y')。所有后续讨论的 setattr 都适用于 delattr。Python 忽略 delattr 的返回值。如果不存在 delattr,Python 会将del x.y转换为del x.dict['y']。
dir dir(self) 当调用 dir(x)时,Python 将操作转换为调用x.dir(),它必须返回x的属性的排序列表。当x的类没有 dir 时,dir(x)执行内省以返回x的属性的排序列表,努力产生相关而非完整的信息。

| eq, ge, gt, le,

lt, ne | eq(self, other), ge(self, other), gt(self, other), le(self, other),

lt(self, other), ne(self, other)

比较x == y, x >= y, x > y, xy, x < y, 和x != y,分别调用列出的特殊方法,应返回FalseTrue。每个方法可以返回 NotImplemented 告知 Python 以替代方式处理比较(例如,Python 可能尝试y > x来代替x < y)。

最佳实践是仅定义一个不等比较方法(通常是 lt)加上 eq,并用 functools.total_ordering 修饰类(在表 8-7 中有介绍),以避免模板和比较中的逻辑矛盾风险。 |

format format(self, format_string='') 调用 format(x)会调用x.format(''),调用 format(x, format_string)会调用x.format(format_string)。类负责解释格式字符串(每个类可以定义自己的小型格式规范语言,受内置类型实现的启发,如在“字符串格式化”中介绍)。当从 object 继承 format 时,它委托给 str 并且不接受非空格式字符串。
getattr getattr(self, name) 当找不到x.y的常规步骤时(即通常会引发 AttributeError 时),Python 会调用x.getattr('y')。Python 不会对通过常规方式找到的属性调用 getattr(如作为x.dict 中的键或通过x.class 访问)。如果希望 Python 对每个属性都调用 getattr,可以将属性存放在其他位置(例如通过私有名称引用的另一个字典中),或者改写 getattribute。如果 getattr 找不到y,应该引发 AttributeError。

| getattribute | _getattribute(self, name) 每次访问属性x.y时,Python 调用x.getattribute('y'),它必须获取并返回属性值,否则引发 AttributeError。属性访问的通常语义(x.dictC.slotsC的类属性、x.getattr)都归因于 object.getattribute

当类C重写 getattribute 时,必须实现它想要提供的所有属性语义。实现属性访问的典型方式是委托(例如,在重写 getattribute 的操作中调用 object.getattribute(self, ...))。

重写 getattribute 会减慢属性访问速度

当一个类覆盖了 getattribute,则该类实例上的所有属性访问变得缓慢,因为覆盖的代码会在每次属性访问时执行。

|

| hash | hash(self) 调用 hash(x) 会调用 x.hash()(以及其他需要知道 x 哈希值的上下文,如将 x 作为字典键使用,如 D[x] 其中 D 是一个字典,或将 x 作为集合成员使用)。hash 必须返回一个 int,以便当 x==y 时意味着 hash(x)==hash(y),并且对于给定的对象必须始终返回相同的值。

当缺少 hash 时,调用 hash(x) 会调用 id(x),只要同时缺少 eq。其他需要知道 x 哈希值的上下文行为相同。

任何 x,使得 hash(x) 返回一个结果而不是引发异常,被称为可哈希对象。当缺少 hash,但存在 eq 时,调用 hash(x) 会引发异常(以及其他需要知道 x 的哈希值的上下文)。在这种情况下,x 不可哈希,因此不能作为字典键或集合成员。

通常只为不可变对象定义 hash,而且还定义了 eq。请注意,如果存在任何 y 使得 x==y,即使 y 是不同类型的对象,并且 xy 都是可哈希的,必须确保 hash(x)==hash(y)。(在 Python 内置类型中,存在一些情况,其中对象的不同类型之间可以相等。最重要的是不同数字类型之间的相等性:int 可以等于 bool、float、fractions.Fraction 实例或 decimal.Decimal 实例。)

| init | init(self[, args...]) 当调用 C([args...]) 创建类 C 的实例 x 时,Python 调用 x.init([args...]) 让 x 初始化自己。如果缺少 init(即从 object 继承),必须无参数调用 C,即 C(),并且在创建时 x 没有实例特定的属性。Python 不会对 C 类及其超类的 init 方法进行隐式调用。C.init 必须显式执行任何初始化操作,包括必要时委托。例如,当类 C 有一个需要无参数初始化的基类 B 时,C.init 中的代码必须显式调用 super().init()。init 的继承与任何其他方法或属性相同:如果 C 本身没有覆盖 init,它会从其 mro 中的第一个超类继承它,就像其他属性一样。

__init__ 必须返回 None;否则,调用该类会引发 TypeError。

| __new__ | __new__(cls[, *args*...]) 当你调用 *C*([*args*...]) 时,Python 通过调用 *C*.__new__(*C*[, *args*...]) 来获取你正在创建的新实例 x。每个类都有类方法 __new__(通常只是从 object 继承而来),它可以返回任何值 x。换句话说,__new__ 不需要返回 C 的新实例,尽管期望如此。如果 __new__ 返回的值 xC 的实例或 C 的任何子类的实例(无论是新的还是之前存在的实例),Python 就会在 x 上调用 __init__(使用最初传递给 __new__ 的相同 [args...])。

__new__ 中初始化不可变对象,其他对象在 __init__ 中初始化。

你可以在 __init____new__ 中执行大多数类型的新实例初始化,所以你可能想知道最好将它们放在哪里。最佳实践是只将初始化放在 __init__ 中,除非你有一个特定的理由将其放在 __new__ 中。(当类型是不可变的时,__init__ 不能改变其实例:在这种情况下,__new__ 必须执行所有的初始化。)

| |

__repr__ __repr__(self) 调用 repr(*x*)(当 x 是表达式语句的结果时,在交互解释器中会隐式发生)会调用 *x*.__repr__() 来获取并返回 x 的完整字符串表示。如果没有 __repr__,Python 就会使用默认的字符串表示。 __repr__ 应返回一个包含关于 x 的无歧义信息的字符串。在可行的情况下,尝试使 eval(repr(*x*))==*x*(但不要为了达到这个目标而过度努力!)。
__setattr__ __setattr__(self, *name*, *value*) 对于绑定属性 *x.y* 的任何请求(通常是赋值语句 *x.y=value*,但也可以是 setattr(*x*, '*y*', *value*)),Python 调用 *x*.__setattr__('*y*', *value*)。Python 总是对 x任何 属性绑定调用 __setattr__ —— 这与 __getattr__ 的主要区别(在这一点上,__setattr__ 更接近 __getattribute__)。为了避免递归,当 *x*.__setattr__ 绑定 x 的属性时,它必须直接修改 *x*.__dict__(例如,通过 *x*.__dict__[*name*]=*value*);或者更好的是,__setattr__ 可以委托给超类(调用 super().__setattr__('*y*', *value*))。Python 忽略 __setattr__ 的返回值。如果 __setattr__ 不存在(即从 object 继承),并且 C.y 不是覆盖描述符,Python 通常会将 *x.y=z* 翻译为 *x*.__dict__['*y*']=*z*(但 __setattr__ 也可以与 __slots__ 一起很好地工作)。
__str__ __str__(self) 类似于 print(*x*)str(*x*) 调用 *x*.__str__() 来获取 x 的非正式、简洁的字符串表示。如果没有 __str__,Python 就会调用 *x*.__repr____str__ 应返回一个方便阅读的字符串,即使这可能需要一些近似。

容器的特殊方法

实例可以是一个 container(序列、映射或集合——相互排斥的概念⁹)。为了最大限度地提高实用性,容器应提供特殊方法 getitemcontainsiter(如果可变,则还应提供 setitemdelitem),以及后续章节讨论的常规方法。在许多情况下,您可以通过扩展 collections.abc 模块中的适当抽象基类(例如 Sequence、MutableSequence 等)来获得合适的常规方法实现,如 “Abstract Base Classes” 中所述。

序列

在每个项目访问的特殊方法中,一个包含 L 个项目的序列应接受任何整数 key,使得 -L<=key<L。¹⁰ 为了与内置序列兼容,负索引 key,0>key>=-L,应等同于 key+L。当 key 具有无效类型时,索引应引发 TypeError 异常。当 key 是有效类型的值但超出范围时,索引应引发 IndexError 异常。对于不定义 iter 的序列类,for 语句依赖于这些要求,以及接受可迭代参数的内置函数也依赖于这些要求。每个序列的项目访问特殊方法也应(如果有可能)接受作为其索引参数的内置类型切片的实例,其 start、step 和 stop 属性为 int 或 None;slicing 语法依赖于此要求,如 “Container slicing” 中所述。

序列还应允许通过 + 进行连接(与同类型的另一个序列),并通过 *(乘以整数)进行重复。因此,序列应具有特殊方法 addmulraddrmul,如 “Special Methods for Numeric Objects” 中所述;此外,可变序列应具有等效的就地方法 iaddimul。序列应与同类型的另一个序列有意义地进行比较,实现 字典序比较,就像列表和元组一样。(继承自 Sequence 或 MutableSequence 抽象基类不能满足所有这些要求;最多只能从 MutableSequence 继承,只提供 iadd。)

每个序列都应包括 “List methods” 中介绍的常规方法:不区分大小写的 count 和 index 方法,如果可变,则还应包括 append、insert、extend、pop、remove、reverse 和 sort 方法,其签名和语义与列表的相应方法相同。(继承自 Sequence 或 MutableSequence 抽象基类足以满足这些要求,除了 sort 方法。)

如果一个不可变序列的所有项都是可散列的,那么它本身也应该是可散列的。序列类型可能以某些方式限制其项(例如,仅接受字符串项),但这不是强制性的。

映射

映射的元素访问特殊方法在接收到无效的 key 参数值(有效类型)时应引发 KeyError 异常,而不是 IndexError。任何映射都应定义在“字典方法”中介绍的非特殊方法:copy、get、items、keys 和 values。可变映射还应定义方法 clear、pop、popitem、setdefault 和 update。(从 Mapping 或 MutableMapping 抽象基类继承可以满足这些要求,但不包括 copy。)

如果其所有项都是可哈希的,则不可变映射类似类型应该是可哈希的。映射类似类型可以在某些方面对其键进行约束,例如仅接受可哈希键,或者(更具体地)例如仅接受字符串键,但这不是强制性的。任何映射应该与相同类型的另一个映射有意义地可比较(至少在等式和不等式方面,尽管不一定是有序比较)。

集合

集合是一种特殊的容器:它们既不是序列也不是映射,不能被索引,但是有长度(元素个数)并且可迭代。集合还支持许多运算符(&、|、^ 和 -),以及成员测试和比较,还有等效的非特殊方法(交集、并集等)。如果你实现了类似集合的容器,它应该对 Python 内置的集合具有多态性,详见“集合”。(从 Set 或 MutableSet 抽象基类继承可以满足这些要求。)

如果其所有元素都是可哈希的,则不可变集合类似类型应该是可哈希的。集合类似类型可以在某些方面对其元素进行约束,例如仅接受可哈希元素,或(更具体地)例如仅接受整数元素,但这不是强制性的。

容器切片

当你引用、绑定或取消绑定容器 x 上的切片,例如 x[i:j] 或 x[i:j:k](在实践中,这仅用于序列时),Python 调用 x 的适用的元素访问特殊方法,将一个名为 slice object 的内置类型的对象作为 key。切片对象具有属性 start、stop 和 step。如果在切片语法中省略了相应的值,则每个属性都是 None。例如,del x[:3] 调用 x.delitem(y),其中 y 是一个切片对象,使得 y.stop 为 3,y.start 为 Noney.step 为 None。容器对象 x 应适当地解释传递给 x 的特殊方法的切片对象参数。切片对象的方法 indices 可以帮助:以你的容器长度作为其唯一参数调用它,它返回一个包含三个非负索引的元组,适合作为循环索引切片中每个项目的开始、停止和步长。例如,在序列类的 getitem 特殊方法中,完全支持切片的常见习惯用法是:

`def` __getitem__(self, index):
    *`# Recursively special-case slicing`*
    `if` isinstance(index, slice):
        `return` self.__class__(self[x]
                              `for` x `in` range(*index.indices(len(self))))
    *`# Check index, and deal with a negative and/or out-of-bounds index`*
    index = operator.index(index)
    `if` index < 0:
        index += len(self)
    `if` `not` (0 <= index < len(self)):
        `raise` `IndexError`
    *`# Index is now a correct int, within range(len(self))`*
 *`# ...rest of __getitem__, dealing with single-item access...`*

这种习惯用法使用生成器表达式(genexp)语法,并假定你的类的 init 方法可以使用可迭代参数调用,以创建适当的新实例。

容器方法

特殊方法 getitemsetitemdelitemiterlencontains 公开容器功能(参见表 4-2)。

表格 4-2. 容器方法

| contains | 布尔测试y in x 调用x.contains(y)。当x是一个序列或类似集合时,contains 应该在y等于x中的一个项的值时返回True。当x是一个映射时,contains 应该在y等于x中的一个键的值时返回True。否则,contains 应该返回False。当 contains 不存在且x是可迭代的时候,Python 执行y in x如下,时间与 len(x)成正比:

`for` *`z`* `in` *`x`*:
    `if` *`y`*==*`z`*:
        `return` `True`
`return` `False`

|

delitem 当一个请求要解除x的一个项或片段的绑定(通常是 del x[key]),Python 调用x.delitem(key)。如果x是可变的且可以删除项(及可能的片段),则容器x应该有 delitem
getitem 当你访问x[key](即当你索引或切片容器x时),Python 调用x.getitem(key)。所有(非类似集合的)容器都应该有 getitem
iter 当一个请求要循环遍历x的所有项(通常是 for item in x),Python 调用x.iter()来获取x上的迭代器。内置函数 iter(x)也调用x.iter()。当 iter 不存在时,iter(x)会合成并返回一个迭代器对象,该对象包装x并产生x[0]、x[1]等,直到这些索引中的一个引发 IndexError 异常以指示容器的末尾。但是,最好确保你编写的所有容器类都有 iter
len 调用 len(x)会调用x.len()(其他需要知道容器x中有多少项的内置函数也会这样)。len 应该返回一个整数,即x中的项数。当 bool 不存在时,Python 还会调用x.len_()来评估x在布尔上下文中的值;在这种情况下,当且仅当容器为空时(即容器的长度为 0 时),容器是虚假的。所有容器都应该有 len,除非容器确定包含的项数太昂贵。
setitem 当一个请求要绑定x的一个项或片段(通常是一个赋值x[key]=value),Python 调用x.setitem(key, value)。如果x是可变的,则容器x应该有 setitem,因此可以添加或重新绑定项,也许还有片段。

抽象基类

抽象基类(ABCs)是面向对象设计中的重要模式:它们是不能直接实例化的类,而是存在于被具体类扩展的目的(通常的类,可以被实例化的那种)。

一个推荐的面向对象设计方法(归功于 Arthur J. Riel)是永远不要扩展一个具体类。¹¹ 如果两个具体类有足够的共同点,使你想让其中一个继承另一个,那么可以通过创建一个 抽象 基类来替代,该抽象基类涵盖它们所有的共同点,并让每个具体类扩展该 ABC。这种方法避免了继承中许多微妙的陷阱和问题。

Python 对 ABCs 提供了丰富的支持,足以使它们成为 Python 对象模型的一部分。¹²

abc 模块

标准库模块 abc 提供了元类 ABCMeta 和类 ABC(继承 abc.ABC 使得 abc.ABCMeta 成为元类,且没有其他效果)。

当你将 abc.ABCMeta 作为任何类 C 的元类时,这使得 C 成为一个 ABC,并提供了类方法 C.register,可用一个参数调用:该参数可以是任何现有类(或内置类型) X

调用 C.register(X) 使 X 成为 C 的一个 虚拟 子类,这意味着 issubclass(X, C) 返回 True,但 C 不出现在 X.mro 中,X 也不继承 C 的任何方法或其他属性。

当然,也可以像通常的子类化方式一样,让一个新类 Y 继承自 C,在这种情况下 C 会出现在 Y.mro 中,并且 Y 继承 C 的所有方法,就像通常的子类化一样。

一个 ABC C 还可以选择重写类方法 subclasshook,当 issubclass(X, C) 调用时会传入单一参数 XX 是任何类或类型)。当 C.subclasshook(X) 返回 True 时,issubclass(X, C) 也返回 True;当 C.subclasshook(X) 返回 False 时,issubclass(X, C) 也返回 False。当 C.subclasshook(X) 返回 NotImplemented 时,issubclass(X, C) 会按照通常的方式进行。

abc 模块还提供了 decorator abstractmethod 来指定必须在继承类中实现的方法。你可以通过先使用 property 然后是 abstractmethod decorators 的顺序来将属性定义为抽象。¹³ 抽象方法和属性可以有实现(通过 super 内建函数对子类可见),但将方法和属性设为抽象的目的是,只有当 X 覆盖了 ABC C 的每个抽象属性和方法时,才能实例化 ABC C 的非虚拟子类 X

collections 模块中的 ABCs

collections 提供了许多 ABCs,在 collections.abc.¹⁴ 中列出了一些这样的 ABCs,这些 ABCs 接受作为虚拟子类任何定义或继承特定抽象方法的类,如 表 4-3 中所列。

表 4-3. 单方法 ABCs

ABC 抽象方法
Callable call
Container contains
Hashable hash
Iterable iter
Sized len

collections.abc 中的其他 ABCs 扩展了其中一个或多个,添加了更多基于抽象方法的抽象方法和/或 mixin 方法。(当你在具体类中扩展任何 ABC 时,你 必须 覆盖抽象方法;你也可以覆盖一些或所有的 mixin 方法,以帮助提高性能,但这不是必须的——当这样做能够获得足够的性能以满足你的目的时,你可以直接继承它们。)

表 4-4 详细说明了 collections.abc 中直接扩展了前述 ABCs 的 ABCs。

表 4-4. 具有附加方法的 ABCs

ABC 扩展 抽象方法 Mixin 方法
Iterator Iterable next iter

| Mapping | Container Iterable

Sized | getitem iter

len | contains eq

ne

getitems

keys

values |

MappingView Sized len

| Sequence | Container Iterable

Sized | getitem len | contains iter

reversed

count

index |

| Set | Container Iterable

Sized | contains __iter

len | and^(a) eq

ge^(b)

gt

le

lt

ne

or

sub

xor

isdisjoint |

(a) 对于集合和可变集合,许多 dunder 方法等效于具体类 set 中的非特殊方法;例如,add 就像交集,而 iadd 就像 intersection_update。(b) 对于集合,排序方法反映了“子集”的概念:s1s2 意味着“s1s2 的子集或等于 s2。”

表 4-5 详细说明了本模块中进一步扩展的 ABCs。

表 4-5. collections.abc 中剩余的 ABCs

ABC 扩展 抽象方法 Mixin 方法
ItemsView MappingView Set contains iter
KeysView MappingView Set contains iter

| MutableMapping | Mapping | delitem getitem

iter

_len

setitem | Mapping’s methods, plus: clear

pop

popitem

setdefault

update |

| MutableSequence | Sequence | delitem getitem

len

setitem

insert | Sequence’s methods, plus: iadd

append

extend

pop

remove

reverse |

| MutableSet | Set | contains __iter

len

add

discard | Set’s methods, plus: iand

ior

isub

ixor

clear

pop

remove |

ValuesView MappingView contains iter

查看 在线文档 获取更多详细信息和使用示例。

numbers 模块中的 ABCs

numbers 提供了一个层次结构(也称为 tower)的 ABCs,表示各种类型的数字。 表 4-6 列出了 numbers 模块中的 ABCs。

表 4-6. numbers 模块提供的 ABCs

ABC 描述
Number 层次结构的根。包括任意类型的数值;不需要支持任何给定的操作。
Complex 扩展自 Number。必须支持(通过特殊方法)转换为 complex 和 bool,以及 +,-,*,/,==,!=,和 abs,以及直接的方法 conjugate 和属性 real 和 imag。
Real 扩展自 Complex。此外,必须支持(通过特殊方法)转换为 float,math.trunc,round,math.floor,math.ceil,divmod,//,%,<,<=,>,和 >=。
Rational 扩展自 Real。此外,必须支持 numerator 和 denominator 属性。
Integral 扩展自 Rational。此外,必须支持(通过特殊方法)转换为 int,**,和位运算 <<,>>,&,^, ,和 ~。
(a) 因此,每个整数或浮点数都有一个 real 属性等于其值,以及一个 imag 属性等于 0.(b) 因此,每个整数都有一个 numerator 属性等于其值,以及一个 denominator 属性等于 1.

查看 在线文档 获取关于实现自定义数值类型的说明。

数值对象的特殊方法

一个实例可以通过多个特殊方法支持数值操作。一些不是数字的类也支持 Table 4-7 中的一些特殊方法,以重载如 + 和 * 的运算符。特别是,序列应该有特殊方法 add, mul, radd, 和 rmul,如 “Sequences” 所述。当二进制方法之一(如 add, sub 等)被调用时,如果操作数的类型不支持该方法,则该方法应返回内置的 NotImplemented 单例。

表 4-7. 数值对象的特殊方法

| abs, invert,

neg,

pos | _abs(self), invert(self), neg(self), pos(self) 一元运算符 abs(x), ~x, -x, 和 +x,分别调用这些方法。

| add, mod,

mul,

sub | add (self, other), mod(self, other),

mul(self, other),

sub(self, other)

运算符 x + y, x % y, *x ** y, 和 x - y 分别调用这些方法,通常用于算术计算。

| and, lshift,

or,

rshift,

xor | and(self, other), lshift(self, other), or(self, other), _rshift(self, other),

xor(self, other)

运算符 x & y, x << y, x | y, x >> y, 和 x ^ y 分别调用这些方法,通常用于位运算。

| complex, float,

int | complex(self), float(self), int(self) 内置类型 complex(x), float(x), 和 int(x),分别调用这些方法。

divmod divmod(self, other) 内置函数 divmod(x, y) 调用 x.divmod(y)。divmod 应返回一对 (quotient, remainder) 等于 (x // y, x % y)。

| floordiv, truediv | floordiv(self, other),truediv(self, other),

运算符 x // yx / y,通常用于算术除法。 |

| iadd, ifloordiv,

imod

imul

isub

itruediv

imatmul | iadd(self, other),ifloordiv(self, other),

imod(self, other),

imul(self, other),

isub(self, other),

itruediv(self, other),

imatmul(self, other),

增强赋值 x += yx //= yx %= yx *= yx -= yx /= y,和 x @= y,分别调用这些方法。每个方法应该就地修改 x 并返回 self。当 x 是可变的时候定义这些方法(即,当 x 可以 就地更改时)。 |

| iand, ilshift,

ior

irshift

ixor | _iand(self, other),_ilshift(self, other),

ior(self, other),

irshift(self, other),

ixor(self, other),

增强赋值 x &= yx <<= yx = yx >>= y,和 x ^= y,分别调用这些方法。每个方法应该就地修改 x 并返回 self。当 x 是可变的时候定义这些方法(即,当 x 可以 就地更改时)。 |

index index(self) 像 int 一样,但只应由整数的替代实现类型提供(换句话说,该类型的所有实例都可以精确映射到整数)。例如,所有内置类型中,只有 int 提供 index;float 和 str 不提供,尽管它们提供 int。序列的索引和切片内部使用 index 来获取所需的整数索引。
ipow ipow(self,other) 增强赋值 x **= y 调用 x.ipow(y)。ipow 应该就地修改 x 并返回 self。
matmul matmul(self, other) 运算符 x @ y 调用这个方法,通常用于矩阵乘法。
pow pow(self,other[, modulo]) x ** y 和 pow(x, y) 都调用 x.pow(y),而 pow(x, y, z) 调用 x.pow(y, z)。 x.pow(y, z) 应该返回等于表达式 x.pow(y) % z 的值。

| radd, rmod,

rmul

rsub

rmatmul | radd(self, other),rmod(self, other),

rmul(self, other),

rsub(self, other),

rmatmul(self, other),

运算符 y + xy / xy % xy * xy - x,和 y @ x,分别在 y 没有所需方法 addtruediv 等,或者当该方法返回 NotImplemented 时,在 x 上调用这些方法。 |

| rand, rlshift,

ror

rrshift

rxor | rand(self, other),rlshift(self, other),

ror(self, other),

rrshift(self, other),

rxor(self, other),

运算符 y & xy << xy | xy >> x,以及 x ^ y 分别在 y 没有所需方法 andlshift,等等,或者当该方法返回 NotImplemented 时,在 x 上调用这些方法。|

rdivmod _rdivmod(self, other) 内置函数 divmod(y, x) 调用 x.rdivmod(y) 当 y 没有 divmod,或者当该方法返回 NotImplemented 时。rdivmod 应返回一个对 (remainder, quotient)。
rpow rpow(self,other) y ** x 和 pow(y, x) 调用 x.rpow(y) 当 y 没有 pow,或者当该方法返回 NotImplemented 时。在这种情况下,没有三个参数的形式。

装饰器

在 Python 中,经常使用 高阶函数:接受函数作为参数并返回函数作为结果的可调用对象。例如,描述符类型,如 staticmethod 和 classmethod,在类体内可以使用,如 “类级方法” 所述:

`def` f(cls, ...):
 *`# ...definition of f snipped...`*
f = classmethod(f)

然而,将 classmethod 的调用在 def 语句之后的文本上,对代码的可读性有所影响:当阅读 f 的定义时,代码的读者尚不知道 f 将成为类方法而不是实例方法。如果在 def 前面提到 classmethod,则代码更易读。为此,使用称为 装饰 的语法形式:

@classmethod
`def` f(cls, ...):
 *`# ...definition of f snipped...`*

装饰器,这里的 @classmethod,必须紧随其后的 def 语句,并意味着 f = classmethod(f) 在 def 语句之后立即执行(无论 fdef 定义的任何名称)。更一般地,@expression 评估表达式(必须是一个名称,可能是限定的,或者是一个调用),并将结果绑定到一个内部临时名称(比如,__aux);任何装饰器必须紧跟在 def(或 class)语句之后,并意味着 f = __aux(f) 在 defclass 语句之后立即执行(无论 fdefclass 定义的任何名称)。绑定到 __aux 的对象称为 装饰器,它被称为 装饰 函数或类 f

装饰器是一种便捷的高阶函数缩写。你可以将装饰器应用于任何 defclass 语句,不仅限于类体内。你可以编写自定义装饰器,它们只是接受函数或类对象作为参数,并返回函数或类对象作为结果的高阶函数。例如,这是一个简单的装饰器示例,它不修改其装饰的函数,而是在函数定义时将函数的文档字符串打印到标准输出:

`def` showdoc(f):
    `if` f.__doc__:
        print(f'{f.__name__}: {f.__doc__}')
    `else`:
        print(f'{f.__name__}: No docstring!')
    `return` f

@showdoc
`def` f1():
    """a docstring"""  *`# prints:`* *`f1: a docstring`*

@showdoc
`def` f2():
    `pass`               *`# prints:`* *`f2: No docstring!`*

标准库模块 functools 提供了一个方便的装饰器 wraps,用于增强常见的“包装”习惯建立的装饰器:

import functools

`def` announce(f):
    @functools.wraps(f)
    `def` wrap(*a, **k):
        print(f'Calling {f.__name__}')
        `return` f(*a, **k)
    `return` wrap

使用 @announce 装饰函数 f 导致在每次调用 f 之前打印一行公告。由于 functools.wraps(f) 装饰器,包装器采用被包装函数的名称和文档字符串:例如,在调用这样一个装饰过的函数时调用内置帮助是有用的。

元类

任何对象,甚至是类对象,都有一种类型。在 Python 中,类型和类也是一等对象。类对象的类型也称为类的 元类。¹⁵ 对象的行为主要由对象的类型确定。对于类也是如此:类的行为主要由类的元类确定。元类是一个高级主题,您可能想跳过本节的其余部分。但是,完全掌握元类可以带您深入了解 Python;偶尔定义自己的自定义元类可能会有用。

简单类定制的替代方法元类。

虽然自定义元类允许您以几乎任何想要的方式调整类的行为,但通常可以通过编写自定义元类来更简单地实现一些自定义。

当类 C 具有或继承类方法 init_subclass 时,Python 在每次对 C 进行子类化时调用该方法,将新构建的子类作为唯一的位置参数传递。init_subclass 也可以有命名参数,在这种情况下,Python 会传递在执行子类化的类语句中找到的相应命名参数。作为一个纯粹的说明性例子:

>>> `class` C:
...     `def` __init_subclass__(cls, foo=None, **kw):
...         print(cls, kw)
...         cls.say_foo = staticmethod(lambda: f'*{foo}*')
...         super().__init_subclass__(**kw)
... 
>>> `class` D(C, foo='bar'):
...     `pass`
...
<class '__main__.D'> {}
>>> D.say_foo()
'*bar*'

init_subclass 中的代码可以以适用的方式修改 cls,在类创建后工作方式上本质上像一个 Python 自动应用于 C 的任何子类的类装饰器。

另一个用于定制的特殊方法是 set_name,它允许您确保将描述符的实例添加为类属性时,它们知道您正在向其添加的类和名称。在将 ca 添加到名为 C 的类并命名为 n 的类语句的末尾,当 ca 的类型具有方法 set_name 时,Python 调用 ca.set_name(C, n)。例如:

>>> `class` Attrib:
...     `def` __set_name__(self, cls, name):
...         print(f'Attribute {name!r} added to {cls}')
... 
>>> `class` AClass:
...     some_name = Attrib()
...
Attribute 'some_name' added to <class '__main__.AClass'>
>>>

如何确定 Python 类的元类。

class 语句接受可选的命名参数(在基类之后,如果有)。最重要的命名参数是 metaclass,如果存在,则标识新类的元类。如果存在非类型元类,则还允许其他命名参数,此时这些参数传递给元类的可选 prepare 方法(完全由 prepare 方法决定如何使用此类命名参数)。¹⁶ 当命名参数 metaclass 不存在时,Python 通过继承来确定元类;对于没有明确指定基类的类,默认元类为 type。

Python 调用 prepare 方法(如果存在)来确定元类后立即调用元类,如下所示:

`class` M:
    `def` __prepare__(classname, *classbases, **kwargs):
        `return` {}
 *`# ...rest of M snipped...`*
`class` X(onebase, another, metaclass=M, foo='bar'):
 *`# ...body of X snipped...`*

在这里,调用等同于 M.prepare('X', onebase, another, foo='bar')。如果存在 prepare,则必须返回映射(通常只是字典),Python 将其用作执行类体的d映射。如果不存在 prepare,Python 将使用一个新的、最初为空的字典作为d

元类如何创建类

确定了元类M后,Python 使用三个参数调用M:类名(一个字符串)、基类元组t和字典(或其他由 prepare 生成的映射)d,其中类体刚刚执行完毕。¹⁷ 这个调用返回类对象C,Python 随后将其绑定到类名上,完成class语句的执行。注意,这实际上是类型M的实例化,因此对M的调用执行M.init(C, namestring, t, d),其中CM.new(M, namestring, t, d)的返回值,就像在任何其他实例化中一样。

Python 创建类对象C之后,类C与其类型(通常为M的类型)之间的关系与任何对象与其类型之间的关系相同。例如,当你调用类对象C(创建C的实例)时,M.call 执行,类对象C作为第一个参数。

注意,在这种情况下,描述的方法(“按实例方法”)的方法,仅在类上查找特殊方法,而不是在实例上。调用C实例化它必须执行元类的M.call,无论C是否具有每实例属性(方法)call(即,独立于C的实例是否可调用)。这种方式,Python 对象模型避免了必须将类及其元类的关系作为专门情况的问题。避免专门情况是 Python 强大的关键:Python 有少量、简单、通用的规则,并且一贯地应用这些规则。

自定义元类的定义和使用

定义自定义元类很容易:继承自 type 并重写其部分方法。你还可以使用 newinitgetattribute 等方法执行大多数你可能考虑创建元类的任务,而不涉及元类。然而,自定义元类可能会更快,因为特殊处理仅在类创建时执行,这是一种罕见的操作。自定义元类允许你在框架中定义一整类具有你编码的任何有趣行为的类,这与类本身可能选择定义的特殊方法完全独立。

要以明确的方式修改特定的类,一个很好的替代方法通常是使用类装饰器,如 “装饰器” 中所述。然而,装饰器不会被继承,因此必须显式地将装饰器应用于每个感兴趣的类。¹⁸ 另一方面,元类是可以继承的;事实上,当你定义一个自定义元类 M 时,通常也会定义一个否则为空的类 C,其元类为 M,这样需要 M 的其他类可以直接继承自 C

类对象的某些行为只能在元类中定制。下面的示例展示了如何使用元类来更改类对象的字符串格式:

`class` MyMeta(type):
    `def` __str__(cls):
        `return` f'Beautiful class {cls.__name__!r}'
`class` MyClass(metaclass=MyMeta):
    `pass`
x = MyClass()
print(type(x))      *`# prints:`* *`Beautiful class 'MyClass'`*

一个实质性的自定义元类示例

假设在 Python 编程中,我们想念 C 语言的结构体类型:一个按顺序排列、具有固定名称的数据属性对象(数据类,在下一节中详细讨论,完全满足此需求,这使得此示例纯粹是说明性的)。Python 允许我们轻松定义一个通用的 Bunch 类,它与固定顺序和名称除外是类似的:

`class` Bunch:
    `def` __init__(self, **fields):
        self.__dict__ = fields
p = Bunch(x=2.3, y=4.5)
print(p)       *`# prints:`* *`<_main__.Bunch object at 0x00AE8B10>`*

自定义元类可以利用属性名称在类创建时固定的事实。在 示例 4-1 中显示的代码定义了一个元类 MetaBunch 和一个类 Bunch,使我们能够编写如下代码:

`class` Point(Bunch):
    *`"""A Point has x and y coordinates, defaulting to 0.0,`*
       *`and a color, defaulting to 'gray'-and nothing more,`*
       *`except what Python and the metaclass conspire to add,`*
       *`such as __init__ and __repr__.`*
    *`"""`*
    x = 0.0
    y = 0.0
    color = 'gray'
*`# example uses of class Point`*
q = Point()
print(q)                    *`# prints:`* *`Point()`*
p = Point(x=1.2, y=3.4)
print(p)                    *`# prints:`* *`Point(x=1.2, y=3.4)`*

在这段代码中,print 调用会生成我们的 Point 实例的可读字符串表示。Point 实例非常节省内存,并且它们的性能基本上与前面示例中简单类 Bunch 的实例相同(由于对特殊方法的隐式调用没有额外开销)。示例 4-1 非常实质性,要理解其所有细节需要掌握本书后面讨论的 Python 方面,比如字符串(在 第九章 中讨论)和模块警告(在 “warnings 模块” 中讨论)。在 示例 4-1 中使用的标识符 mcl 表示“元类”,在这种特殊的高级情况下比 cls 表示“类”更清晰。

示例 4-1. MetaBunch 元类
`import` warnings
`class` MetaBunch(type):
    *`"""`*
    *`Metaclass for new and improved "Bunch": implicitly defines`*
    *`__slots__, __init__, and __repr__ from variables bound in`*
    *`class scope.`*
    *`A class statement for an instance of MetaBunch (i.e., for a`*
    *`class whose metaclass is MetaBunch) must define only`*
    *`class-scope data attributes (and possibly special methods, but`*
    *`NOT __init__ and __repr__). MetaBunch removes the data`*
    *`attributes from class scope, snuggles them instead as items in`*
    *`a class-scope dict named __dflts__, and puts in the class a`*
    *`__slots__ with those attributes' names, an __init__ that takes`*
    *`as optional named arguments each of them (using the values in`*
    *`__dflts__ as defaults for missing ones), and a __repr__ that`*
    *`shows the repr of each attribute that differs from its default`*
    *`value (the output of __repr__ can be passed to __eval__ to make`*
    *`an equal instance, as per usual convention in the matter, if`*
    *`each non-default-valued attribute respects that convention too).`*
    *`The order of data attributes remains the same as in the`* *`class body.`*
    *`"""`*
    `def` __new__(mcl, classname, bases, classdict):
        *`"""Everything needs to be done in __new__, since`*
           *`type.__new__ is where __slots__ are taken into account.`*
        *`"""`*
        *`# Define as local functions the __init__ and __repr__ that`*
        *`# we'll use in the new class`*
        `def` __init__(self, **kw):
            *`"""__init__ is simple: first, set attributes without`*
 *`explicit values to their defaults; then, set`* *`those`*
 *`explicitly`* *`passed in kw.`*
            *`"""`*
            `for` k `in` self.__dflts__:
                `if` `not` k `in` kw:
                    setattr(self, k, self.__dflts__[k])
            `for` k `in` kw:
                setattr(self, k, kw[k])
        `def` __repr__(self):
            *`"""__repr__ is minimal: shows only attributes that`*
               *`differ`* *`from default values, for compactness.`*
            *`"""`*
            rep = [f'{k}={getattr(self, k)!r}'
                    `for` k `in` self.__dflts__
                    `if` getattr(self, k) != self.__dflts__[k]
                  ]
            `return` f'{classname}({', '.join(rep)})'
        *`# Build the newdict that we'll use as class dict for the`*
        *`# new class`*
        newdict = {'__slots__': [], '__dflts__': {},
                   '__init__': __init__, '__repr__' :__repr__,}
        `for` k `in` classdict:
            `if` k.startswith('__') `and` k.endswith('__'):
                *`# Dunder methods: copy to newdict, or warn`*
                *`# about conflicts`*
                `if` k `in` newdict:
                    warnings.warn(f'Cannot set attr {k!r}'
                                  f' in bunch-class {classname!r}')
 `else``:`
                    newdict[k] = classdict[k]
 `else``:`
                *`# Class variables: store name in __slots__, and`*
                *`# name and value as an item in __dflts__`*
                newdict['__slots__'].append(k)
                newdict['__dflts__'][k] = classdict[k]
        *`# Finally, delegate the rest of the work to type.__new__`*
        `return` super().__new__(mcl, classname, bases, newdict)

`class` Bunch(metaclass=MetaBunch):
    *`"""For convenience: inheriting from Bunch can be used to get`*
       *`the new metaclass (same as defining metaclass= yourself).`*
    *`"""`*
 `pass`

数据类

正如前面的 Bunch 类所示,一个其实例仅仅是一组命名数据项的类是非常方便的。Python 的标准库通过 dataclasses 模块涵盖了这一点。

你将使用 dataclass 函数,它是 dataclasses 模块的主要特性:一种装饰器,你可以将其应用于希望成为一组命名数据项的任何类的实例。作为一个典型的例子,考虑以下代码:

`import` dataclasses
`@`dataclasses.dataclass
`class` Point:
    x: float
    y: float

现在,您可以调用例如 pt = Point(0.5, 0.5),并获得一个具有 pt.x 和 pt.y 属性的变量,每个属性都等于 0.5。默认情况下,dataclass 装饰器已经为 Point 类赋予了一个接受属性 x 和 y 的初始浮点值的 init 方法,并准备好适当显示类的任何实例的 repr 方法:

>>> pt
Point(x=0.5, y=0.5)

dataclass 函数接受许多可选的命名参数,以便调整装饰的类的详细信息。您可能经常明确使用的参数列在 Table 4-8 中。

Table 4-8. dataclass 函数常用参数

Parameter name Default value and resulting behavior
eq True 当为 True 时,生成一个 eq 方法(除非类已定义了一个)
frozen False 当为 True 时,使得类的每个实例为只读(不允许重新绑定或删除属性)
init True 当为 True 时,生成一个 init 方法(除非类已定义了一个)
kw_only False 3.10+ 当为 True 时,强制要求将参数传递给 init 方法时使用命名方式,而非位置方式
order False 当为 True 时,生成顺序比较的特殊方法(如 lelt 等),除非类已定义这些方法
repr True 当为 True 时,生成一个 repr 方法(除非类已定义了一个)
slots False 3.10+ 当为 True 时,向类添加适当的 slots 属性(为每个实例节省一些内存,但不允许向类实例添加其他任意属性)

当设置 frozen 为 True 时,装饰器还会为类添加一个 hash 方法(允许实例作为字典的键和集合的成员),当这是安全的时候(通常是这样的情况)。即使在不安全的情况下,您也可以强制添加 hash 方法,但我们强烈建议您不要这样做;如果您坚持要这样做,请查阅 online docs 了解详细信息。

如果需要在自动生成的 init 方法完成为每个实例属性分配核心工作后调整数据类的每个实例,请定义一个名为 post_init 的方法,装饰器将确保在 init 完成后立即调用它。

假设您希望向 Point 添加一个属性,以捕获创建点的时间。可以将其添加为在 post_init 中分配的属性,为 Point 的定义成员添加名为 create_time 的属性,类型为 float,默认值为 0,并添加一个 post_init 的实现:

`def` __post_init__(self):
    self.create_time = time.time()

现在,如果您创建变量 pt = Point(0.5, 0.5),打印它将显示创建时间戳,类似于以下内容:

>>> pt
Point(x=0.5, y=0.5, create_time=1645122864.3553088)

与常规类似,dataclass 还支持额外的方法和属性,例如计算两个点之间距离的方法以及返回到原点的点的距离的属性:

`def` distance_from(self, other):
    dx, dy = self.x - other.x, self.y - other.y
    `return` math.hypot(dx, dy)

@property
`def` distance_from_origin(self):
    `return` self.distance_from(Point(0, 0))

例如:

>>> pt.distance_from(Point(-1, -1))
2.1213203435596424
>>> pt.distance_from_origin
0.7071067811865476

dataclasses 模块还提供了 asdict 和 astuple 函数,每个函数的第一个参数都是 dataclass 实例,分别返回一个字典和一个元组,这些字典和元组包含类的字段。此外,该模块还提供了一个 field 函数,用于自定义数据类字段(即实例属性)的处理方式,以及几个其他专门用于非常高级、神秘目的的函数和类;要了解有关它们的全部信息,请查阅在线文档

枚举类型(Enums)

在编程时,通常希望创建一组相关的值,用于列举特定属性或程序设置的可能值,¹⁹ 无论它们是什么:终端颜色、日志级别、进程状态、扑克牌花色、服装尺寸,或者你能想到的任何其他东西。枚举类型(enum)是定义这种值组的一种类型,具有可作为类型化全局常量使用的符号名称。Python 提供了 enum 模块中的 Enum 类及其相关子类用于定义枚举。

定义一个枚举为你的代码提供了一组代表枚举中的值的符号常量。在没有枚举的情况下,常量可能会被定义为整数,如下所示:

*`# colors`*
RED = 1
GREEN = 2
BLUE = 3

*`# sizes`*
XS = 1
S = 2
M = 3
L = 4
XL = 5

然而,在这种设计中,没有机制可以警告类似 RED > XL 或 L * BLUE 这样的无意义表达式,因为它们都只是整数。也没有颜色或尺码的逻辑分组。

相反,你可以使用 Enum 子类来定义这些值:

`from` enum `import` Enum, auto

`class` Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

`class` Size(Enum):
    XS = auto()
    S = auto()
    M = auto()
    L = auto()
    XL = auto()

现在,像 Color.RED > Size.S 这样的代码在视觉上显得不正确,并且在运行时会引发 Python TypeError。使用 auto() 自动分配从 1 开始递增的整数值(在大多数情况下,分配给枚举成员的实际值是无意义的)。

调用 Enum 创建一个类,而不是一个实例

令人惊讶的是,当你调用 enum.Enum() 时,它不会返回一个新建的实例,而是一个新建的子类。因此,前面的片段等效于:

`from` enum `import` Enum
Color = Enum('Color', ('RED', 'GREEN', 'BLUE'))
Size = Enum('Size', 'XS S M L XL')

当你调用 Enum(而不是在类语句中显式地对其进行子类化)时,第一个参数是你正在构建的子类的名称;第二个参数给出了该子类成员的所有名称,可以是字符串序列或单个以空格分隔(或逗号分隔)的字符串。

我们建议您使用类继承语法定义 Enum 子类,而不是这种简写形式。形式更加视觉明确,因此更容易看出是否缺少、拼写错误或以后添加的成员。

枚举内部的值称为其成员。习惯上,使用全大写字符来命名枚举成员,将它们视为显式常量。枚举成员的典型用法包括赋值和身份检查:

`while` process_state `is` ProcessState.RUNNING:
 *`# running process code goes here`*
    `if` processing_completed():
        process_state = ProcessState.IDLE

通过迭代枚举类本身或从类的 members 属性获取,你可以获得枚举的所有成员。枚举成员都是全局单例,因此推荐使用 isis not 进行比较,而不是 == 或 !=。

枚举模块包含几个类²⁰,支持不同形式的枚举,列在 表 4-9 中。

表 4-9. 枚举类

描述
枚举 基本枚举类;成员值可以是任何 Python 对象,通常是整数或字符串,但不支持整数或字符串方法。适用于定义成员为无序组的枚举类型。
Flag 用于定义可以使用操作符 |, &, ^ 和 ~ 进行组合的枚举;成员值必须定义为整数以支持这些位操作(Python 但是不假定它们之间的顺序)。值为 0 的 Flag 成员为假;其他成员为真。在创建或检查使用位操作的值时非常有用(例如文件权限)。为了支持位操作,通常使用 2 的幂次方(1、2、4、8 等)作为成员值。
IntEnum 相当于 class IntEnum(int, Enum);成员值为整数并支持所有整数操作,包括排序。在需要对值进行排序时非常有用,比如定义日志级别。
IntFlag 相当于 class IntFlag(int, Flag);成员值为整数(通常是 2 的幂次方),支持所有整数操作,包括比较。
StrEnum 3.11+ 相当于 class StrEnum(str, Enum);成员值为字符串并支持所有字符串操作。

枚举模块还定义了一些支持函数,列在 表 4-10 中。

表 4-10. 枚举支持函数

支持函数 描述
自动 在定义成员时自动递增成员值。通常值从 1 开始,每次增加 1;对于 Flag,增量为 2 的幂次方。
唯一 类装饰器,确保成员值彼此不同。

下面的示例展示了如何定义一个 Flag 子类,以处理从调用 os.stat 或 Path.stat 返回的 st_mode 属性中的文件权限(有关 stat 函数的描述,请参见 第十一章):

`import` enum
`import` stat

`class` Permission(enum.Flag):
    EXEC_OTH = stat.S_IXOTH
    WRITE_OTH = stat.S_IWOTH
    READ_OTH = stat.S_IROTH
    EXEC_GRP = stat.S_IXGRP
    WRITE_GRP = stat.S_IWGRP
    READ_GRP = stat.S_IRGRP
    EXEC_USR = stat.S_IXUSR
    WRITE_USR = stat.S_IWUSR
    READ_USR = stat.S_IRUSR

    @classmethod
    `def` from_stat(cls, stat_result):
        `return` cls(stat_result.st_mode & 0o777)

`from` pathlib `import` Path

cur_dir = Path.cwd()
dir_perm = Permission.from_stat(cur_dir.stat())
`if` dir_perm & Permission.READ_OTH:
    print(f'{cur_dir} is readable by users outside the owner group')

*`# the following raises TypeError: Flag enums do not support order`* 
*`# comparisons`*
print(Permission.READ_USR > Permission.READ_OTH)

在代码中使用枚举替代任意的整数或字符串可以提升可读性和类型完整性。你可以在 Python 文档 中找到枚举模块的更多类和方法的详细信息。

¹ 或者,根据一位评论者的观点,也可以说是“缺点”。有人的福祸相依。

² 当情况如此时,在 metaclass= 后也可以有其他命名参数。这些参数(如果有)将传递给元类。

³ 这种需求是因为在 Singleton 的任何子类上定义了这个特殊方法的情况下,init 会在每次你实例化子类时重复执行,在每个 Singleton 子类的唯一实例上执行。

⁴ 除了定义了 slots 的类的实例外,涵盖在 “slots” 中。

⁵ 其他一些面向对象语言,如Modula-3,同样需要显式地使用 self。

⁶ 多个 Python 版本之后,Michele 的论文仍然适用!

⁷ 其中一位作者使用这种技术动态组合小的混合测试类,创建复杂的测试用例类来测试多个独立的产品特性。

⁸ 为了完整引用常被截断的名言:“当然除了太多的间接问题。”

⁹ 第三方扩展还可以定义不是序列、映射或集合的容器类型。

¹⁰ 包含下限,排除上限——这一点对于 Python 来说一直是规范。

¹¹ 参见例如 “避免扩展类”,作者是 Bill Harlan。

¹² 关于类型检查的相关概念,请参阅 typing.Protocols,涵盖在 “协议” 中。

¹³ abc 模块确实包含 abstractproperty 装饰器,它结合了这两者,但 abstractproperty 已经弃用,新代码应该按描述使用这两个装饰器。

¹⁴ 为了向后兼容性,这些 ABCs 在 Python 3.9 之前也可以在 collections 模块中访问,但在 Python 3.10 中移除了兼容性导入。新代码应该从 collections.abc 导入这些 ABCs。

¹⁵ 严格来说,类 C 的类型可以说是 C 的实例的元类,而不是 C 本身,但这种微妙的语义区别在实践中很少被注意到。

¹⁶ 或者当基类有 init_subclass 的情况下,命名参数将传递给该方法,如 “简单类定制的替代方案” 中描述的那样。

¹⁷ 这类似于调用 type 函数的三个参数版本,如 “使用 type 内置函数动态定义类” 中描述的那样。

¹⁸ init_subclass,在 “简单类定制的替代方案” 中讨论过,工作方式类似于“继承装饰器”,因此通常是自定义元类的替代选择。

¹⁹ 不要将这个概念与无关的内置函数 enumerate 混淆,该函数在 第八章 中介绍,它从可迭代对象生成 (序号, ) 对。

²⁰ enum 的专用元类与通常的类型元类行为差异如此之大,以至于值得指出 enum.Enum 和普通类之间的所有差异。你可以在 Python 在线文档的 “枚举有何不同?”章节 中阅读有关内容。

第五章:类型注释

使用类型信息对 Python 代码进行注释是一个可选步骤,在开发和维护大型项目或库时可能非常有用。静态类型检查器和 lint 工具可帮助识别和定位函数参数和返回值的数据类型不匹配。IDE 可以使用这些类型注释(也称为类型提示)来改进自动完成,并提供弹出式文档。第三方软件包和框架可以使用类型注释来定制运行时行为,或者根据方法和变量的类型注释自动生成代码。

Python 中的类型注释和检查仍在不断发展,并涉及许多复杂的问题。本章涵盖了类型注释的一些最常见用例;您可以在本章末尾列出的资源中找到更全面的资料。

Python 版本的类型注释支持因版本而异

支持类型注释的 Python 功能已经从一个版本发展到另一个版本,其中包括一些重大的增加和删除。本章的其余部分将描述 Python 最新版本(3.10 及更高版本)中的类型注释支持,其中包含一些可能在其他版本中存在或不存在的功能的注释。

历史

Python 本质上是一种动态类型语言。这使您能够通过命名和使用变量来快速开发代码,而无需声明它们。动态类型允许使用灵活的编码习惯、通用容器和多态数据处理,而无需显式定义接口类型或类层次结构。缺点是,在开发过程中,语言无法在传递给函数或从函数返回的不兼容类型的变量上提供帮助。Python 不像一些语言那样利用开发时编译步骤来检测和报告数据类型问题,而是依靠开发人员通过一系列测试用例在运行时环境中重建来发现数据类型错误。

类型注释不是强制性的

类型注释在运行时被强制执行。Python 不执行任何基于类型注释的类型验证或数据转换;可执行的 Python 代码仍然负责正确使用变量和函数参数。但是,类型注释必须在语法上是正确的。包含无效类型注释的延迟导入或动态导入模块会在运行中的 Python 程序中引发 SyntaxError 异常,就像任何无效的 Python 语句一样。

历史上,Python 缺乏任何类型检查被认为是其短板之一,一些程序员因此选择其他编程语言。然而,社区希望 Python 保持其运行时类型自由,因此逻辑上的做法是增加对由类似 lint 工具(在下一节进一步描述)和 IDE 执行的静态类型检查的支持。一些尝试是基于解析函数签名或文档字符串进行类型检查。Guido van Rossum 在Python 开发者邮件列表上引用了几个案例,显示类型注解可以帮助,例如在维护大型遗留代码库时。使用注解语法,开发工具可以执行静态类型检查,以突出显示与预期类型冲突的变量和函数使用。

类型注解的第一个官方版本使用特殊格式的注释来指示变量类型和返回代码,如PEP 484所定义的,这是 Python 3.5 的一项临时 PEP。² 使用注释可以快速实现和尝试新的类型语法,而无需修改 Python 编译器本身。³ 第三方包mypy通过使用这些注释进行静态类型检查得到了广泛接受。随着 Python 3.6 采纳了PEP 526,类型注解已完全整合到 Python 语言本身,并在标准库中添加了一个支持的 typing 模块。

类型检查工具

随着类型注解成为 Python 的一个已确立部分,类型检查工具和 IDE 插件也成为 Python 生态系统的一部分。

mypy

独立的mypy实用程序继续作为静态类型检查的主要工具,始终与 Python 类型注解形式的演变保持最新状态(只要考虑 Python 版本!)。mypy 还作为插件提供给编辑器,包括 Vim、Emacs 和 SublimeText,以及 Atom、PyCharm 和 VS Code IDE。 (PyCharm、VS Code 和 Wing IDE 还单独包含了自己的类型检查功能,与 mypy 分开)。运行 mypy 的最常见命令只是mypy my_python_script.py

您可以在mypy 在线文档中找到更详细的用法示例和命令行选项,以及一个作为便捷参考的速查表。本节后面的代码示例将包含 mypy 错误消息示例,以说明可以通过类型检查捕获的 Python 错误类型。

其他类型检查器

其他考虑使用的类型检查器包括:

MonkeyType

Instagram 的 MonkeyType 使用 sys.setprofile 钩子在运行时动态检测类型;像 pytype 一样(见下文),它也可以生成 .pyi(存根)文件,而不是或者除了在 Python 代码文件中插入类型注解。

pydantic

pydantic 也可以在运行时工作,但不生成存根或插入类型注解;它的主要目标是解析输入并确保 Python 代码获得干净的数据。正如在线文档中所述,它还允许您扩展其验证功能以适应自己的环境。参见“FastAPI”的简单示例。

Pylance

Pylance 是一个类型检查模块,主要用于将 Pyright(见下文)嵌入到 VS Code 中。

Pyre

Facebook 的 Pyre 也可以生成 .pyi 文件。目前在 Windows 上无法运行,除非安装了 Windows Subsystem for Linux (WSL)

Pyright

Pyright 是微软的静态类型检查工具,作为命令行实用程序和 VS Code 扩展提供。

pytype

pytype 是谷歌的静态类型检查器,专注于类型推断(即使在没有类型提示的情况下也能提供建议),除了类型注解。类型推断提供了强大的能力,即使在没有注解的代码中也能检测类型错误。pytype 还可以生成 .pyi 文件,并将存根文件合并回 .py 源代码中(最新版本的 mypy 也在效仿)。目前,pytype 在 Windows 上无法运行,除非你首先安装 WSL

多个主要软件组织开发的类型检查应用的出现,证明了 Python 开发者社区在使用类型注解方面的广泛兴趣。

类型注解语法

类型注解 在 Python 中使用以下形式指定:

*`identifier`*: *`type_specification`*

type_specification 可以是任何 Python 表达式,但通常涉及一个或多个内置类型(例如,仅提到 Python 类型就是一个完全有效的表达式)和/或从 typing 模块导入的属性(在下一节中讨论)。典型的形式是:

*`type_specifier`*[*`type_parameter`*, ...]

这里是一些作为变量类型注解使用的类型表达式示例:

`import` typing

*`# an int`*
count: int

*`# a list of ints, with a default value`*
counts: list[int] = []

*`# a dict with str keys, values are tuples containing 2 ints and a str`*
employee_data: dict[str, tuple[int, int, str]]

*`# a callable taking a single str or bytes argument and returning a bool`*
str_predicate_function: typing.Callable[[str | bytes], bool]

*`# a dict with str keys, whose values are functions that take and return`* 
*`# an int`*
str_function_map: dict[str, typing.Callable[[int], int]] = {
    'square': `lambda` x: x * x,
    'cube': `lambda` x: x * x * x,
}

请注意,lambda 不接受类型注解。

要为函数添加返回类型注解,请使用以下形式:

`def` identifier(argument, ...) -> type_specification :

每个 参数 的形式如下:

*`identifier`*[: *`type_specification`*[ = *`default_value`*]]

这是一个带有注解函数的示例:

`def` pad(a: list[str], min_len: int = 1, padstr: str = ' ') -> list[str]:
    *`"""Given a list of strings and a minimum length, return a copy of`*
 *`the list extended with "padding" strings to be at least the`*
 *`minimum length.`*
 *`"""`*
    `return` a + ([padstr] * (min_len - len(a)))

注意,当带有默认值的注解参数时,PEP 8 建议在等号周围使用空格。

还未完全定义的前向引用类型

有时,函数或变量定义需要引用尚未定义的类型。这在类方法或必须定义当前类类型的参数或返回值的方法中非常常见。这些函数签名在编译时解析,此时类型尚未定义。例如,此类方法无法编译通过:

`class` A:
    @classmethod
    `def` factory_method(cls) -> A:
        *`# ... method body goes here ...`*

由于 Python 编译 factory_method 时,类 A 尚未定义,因此代码会引发 NameError 错误。

问题可以通过在类型 A 的返回类型中添加引号来解决:

`class` A:
    @classmethod
    `def` factory_method(cls) -> 'A':
        *`# ... method body goes here ...`*

未来版本的 Python 可能会推迟对类型注解的评估,直到运行时,从而使封闭引号变得不必要(Python 的指导委员会正在评估各种可能性)。您可以使用 from future import annotations 预览此行为。

typing 模块

typing 模块支持类型提示。它包含在创建类型注释时有用的定义,包括:

  • 用于定义类型的类和函数

  • 用于修改类型表达式的类和函数

  • 抽象基类(ABCs)

  • 协议

  • 实用程序和装饰器

  • 用于定义自定义类型的类

类型

typing 模块最初的实现包括对应于 Python 内置容器和其他类型的类型定义,以及标准库模块中的类型。许多这些类型已被弃用(见下文),但某些仍然有用,因为它们不直接对应任何 Python 内置类型。 Table 5-1 列出了在 Python 3.9 及更高版本中仍然有用的 typing 类型。

表 5-1. typing 模块中有用的定义

Type 描述
Any 匹配任何类型。
AnyStr 等效于 str | bytes。AnyStr 用于注释函数参数和返回类型,其中任一字符串类型都可以接受,但不应在多个参数之间混合使用,或者在参数和返回类型之间混合使用。
BinaryIO 匹配具有二进制(bytes)内容的流,例如使用 mode='b' 打开的流或 io.BytesIO。
Callable Callable[[argument_type, ...], return_type] 定义可调用对象的类型签名。接受与可调用对象的参数对应的类型列表,以及函数返回值的类型。如果可调用对象不接受任何参数,请使用空列表 []。如果可调用对象没有返回值,请使用 None 作为 return_type
IO 等效于 BinaryIO | TextIO。
Lit⁠e⁠r⁠a⁠l​[⁠e⁠x⁠p⁠ression,...] 3.8+ 指定变量可能采用的有效值列表。
LiteralString 3.11+ 指定必须实现为文字引号值的 str。用于防止代码易受到注入攻击。
NoReturn 用作“永久运行”函数的返回类型,比如调用 http.serve_forever 或 event_loop.run_forever 而没有返回值的情况。这不适用于简单返回无明确值的函数;对于这种情况,请使用 → None。更多有关返回类型的讨论详见“为现有代码添加类型注解(逐步类型化)”。
Self 3.11+ 用作实例函数返回类型,返回 self(以及其他少数情况,详见PEP 673)。
TextIO 匹配文本流(str 类型内容),比如使用 mode='t' 打开的文件或者 io.StringIO 返回的对象。

-3.9 在 Python 3.9 之前,typing 模块的定义用于创建表示内置类型的类型,例如 List[int] 表示整数列表。从 Python 3.9 开始,这些名称已弃用,因为其对应的内置或标准库类型现在支持 [] 语法:整数列表现在简单地使用 list[int] 类型声明。Table 5-2 列出了在 Python 3.9 之前使用 typing 模块进行类型注解时必要的定义。

Table 5-2. Python 内置类型及其在 typing 模块中 3.9 之前的定义

内置类型 Python 3.9 前的 typing 模块等效类型
dict Dict
frozenset FrozenSet
list List
set Set
str Text
tuple Tuple
type Type
collections.ChainMap ChainMap
collections.Counter Counter
collections.defaultdict DefaultDict
collections.deque Deque
collections.OrderedDict OrderedDict
re.Match Match
re.Pattern Pattern

Type Expression Parameters

在 typing 模块中定义的某些类型修改其他类型表达式。Table 5-3 列出的类型提供了关于 type_expression 修改类型的额外类型信息或约束。

Table 5-3. 类型表达式参数

Parameter 用法和描述
Annotated Annotated[type_expression, expression, ...] 3.9+ 用额外的元数据扩展 type_expression。函数 fn 的额外元数据可以在运行时使用 get_type_hints(fn, include_extras=True) 获取。
ClassVar ClassVar[type_expression] 表示变量是类变量,不应该作为实例变量赋值。
Final Final[type_expression] 3.8+ 表示变量不应该在子类中写入或重写。
Optional Optional[type_expression] Equivalent to type_expression | None. Often used for named arguments with a default value of None. (Optional does not automatically define None as the default value, so you must still follow it with =None in a function signature.) 3.10+ With the availability of the | operator for specifying alternative type attributes, there is a growing consensus to prefer type_expression | None over using Optional[type_expression].

抽象基类

与内置类型类似,typing 模块的初始实现包括了与 collections.abc 模块中的抽象基类对应的类型定义。许多这些类型后来已被弃用(见下文),但两个定义已保留为 collections.abc 中 ABCs 的别名(见 表格 5-4)。

表格 5-4. 抽象基类别名

Type Method subclasses must implement
Hashable hash
Sized len

-3.9 在 Python 3.9 之前,typing 模块中的以下定义表示在 collections.abc 模块中定义的抽象基类,例如 Sequence[int] 用于整数序列。从 3.9 开始,typing 模块中这些名称已被弃用,因为它们在 collections.abc 中对应的类型现在支持 [] 语法:

AbstractSet Container Mapping
AsyncContextManager ContextManager MappingView
AsyncGenerator Coroutine MutableMapping
AsyncIterable Generator MutableSequence
AsyncIterator ItemsView MutableSet
Awaitable Iterable Reversible
ByteString Iterator Sequence
Collection KeysView ValuesView

协议

typing 模块定义了几个协议,类似于其他一些语言称为“接口”的概念。协议是抽象基类,旨在简洁表达对类型的约束,确保其包含某些方法。typing 模块中当前定义的每个协议都与单个特殊方法相关,其名称以 Supports 开头,后跟方法名(然而,如 typeshed 中定义的其他库可能不遵循相同的约束)。协议可用作确定类对该协议功能支持的最小抽象类:要遵守协议,类所需做的就是实现协议的特殊方法。

表格 5-5 列出了 typing 模块中定义的协议。

表格 5-5. typing 模块中的协议及其必需方法

Protocol Has method
SupportsAbs abs
SupportsBytes bytes
SupportsComplex complex
SupportsFloat float
SupportsIndex 3.8+ index
SupportsInt int
SupportsRound round

类不必显式从协议继承以满足 issubclass(clsprotocol_type),或使其实例满足 isinstance(objprotocol_type)。类只需实现协议中定义的方法即可。例如,想象一个实现罗马数字的类:

`class` RomanNumeral:
    *`"""Class representing some Roman numerals and their int`* 
 *`values.`*
 *`"""`*
    int_values = {'I': 1, 'II': 2, 'III': 3, 'IV': 4, 'V': 5}

    `def` __init__(self, label: str):
        self.label = label

    `def` __int__(self) -> int:
        `return` RomanNumeral.int_values[self.label]

要创建此类的实例(例如,表示电影标题中的续集)并获取其值,您可以使用以下代码:

>>> movie_sequel = RomanNumeral('II')
>>> print(int(movie_sequel))
2

RomanNumeral 满足 issubclass,并且由于实现了 int,与 SupportsInt 进行 isinstance 检查,即使它不是显式从协议类 SupportsInt 继承:⁴

>>> issubclass(RomanNumeral, typing.SupportsInt)
True
>>> isinstance(movie_sequel, typing.SupportsInt)
True

实用程序和装饰器

Table 5-6 列出了在 typing 模块中定义的常用函数和装饰器;接下来是一些示例。

表 5-6.定义在 typing 模块中的常用函数和装饰器

Function/decorator 用法和描述
cast cast(typevar)向静态类型检查器发出信号,var应被视为type类型。返回var;在运行时,var没有更改、转换或验证。请参见表后的示例。
final @final 3.8+ 用于装饰类定义中的方法,如果该方法在子类中被重写则发出警告。也可以用作类装饰器,用于警告是否正在对类本身进行子类化。
get_args get_args(custom_type)返回用于构造自定义类型的参数。
get_origin get_origin(custom_type)3.8+ 返回用于构造自定义类型的基础类型。
get_type_hints get_type_hints(obj)返回结果,就像访问obj.annotations 一样。可以使用可选的 globalns 和 localns 命名空间参数调用,以解析作为字符串给出的前向类型引用,和/或使用包含 Annotations 的任何非类型注释的可选 Boolean include_extras 参数。
NewType NewType(type_nametype)定义了从type派生的自定义类型。type_name是一个字符串,应与分配 NewType 的局部变量匹配。用于区分常见类型的不同用途,例如用于员工姓名的 str 与用于部门名称的 str。有关此函数的更多信息,请参见“NewType”。
no_type_check @no_type_check 用于指示注释不打算用作类型信息。可应用于类或函数。
no_type_che⁠c⁠k⁠_​d⁠e⁠c⁠orator @no_type_check_decorator 用于向另一个装饰器添加 no_type_check 行为。
overload @overload 用于允许定义多个方法,名称相同但签名类型不同。请参见表后的示例。
r⁠u⁠n⁠t⁠i⁠m⁠e⁠_​c⁠h⁠e⁠c⁠k⁠a⁠b⁠l⁠e @runtime_checkable 3.8+ 用于为自定义协议类添加 isinstance 和 issubclass 支持。有关此装饰器的更多信息,请参见 “Using Type Annotations at Runtime” 。
TypeAlias name: TypeAlias = type_expression 3.10+ 用于区分类型别名的定义和简单赋值。在 type_expression 是简单类名或字符串值引用尚未定义的类的情况下最有用,这可能看起来像是一个赋值。TypeAlias 只能在模块范围内使用。一个常见用法是使得一致重用冗长的类型表达式变得更容易,例如:Number: TypeAlias = int | float | Fraction。更多关于此注解的信息,请参见 “TypeAlias” 。
type_check_only @type_check_only 用于指示类或函数仅在类型检查时使用,而在运行时不可用。
TYPE_CHECKING 一个特殊的常量,静态类型检查器将其评估为 True,但在运行时设置为 False。使用它可以跳过导入用于支持类型检查的大型、导入缓慢的模块(以便在运行时不需要该导入)。
TypeVar TypeVar(type_name, *types) 定义用于复杂泛型类型中的类型表达式元素,使用 Generic。type_name 是一个字符串,应与分配给 TypeVar 的局部变量匹配。如果未提供 types,则相关的 Generic 将接受任何类型的实例。如果提供了 types,则 Generic 将仅接受提供的类型或其子类的实例。还接受名为协变和逆变(默认为 False)的布尔参数,以及 bound 参数。关于这些参数的详细信息,请参见 “Generics and TypeVars” 和 typing 模块文档

在类型检查时使用 overload 来标记必须以特定组合使用的命名参数。在这种情况下,fn 必须以 str 键和 int 值对或单个布尔值调用:

@typing.overload
`def` fn(*, key: str, value: int):
    `.``.``.`

@typing.overload
`def` fn(*, strict: bool):
    `.``.``.`

`def` fn(**kwargs):
    *`# implementation goes here, including handling of differing`* 
    *`# named arguments`*
    `pass`

*`# valid calls`*
fn(key='abc', value=100)
fn(strict=True)

*`# invalid calls`*
fn(1)
fn('abc')
fn('abc', 100)
fn(key='abc')
fn(`True`)
fn(strict=True, value=100)

请注意,overload 装饰器仅用于静态类型检查。要根据参数类型在运行时实际分派到不同方法,请使用 functools.singledispatch。

使用 cast 函数可以强制类型检查器在 cast 的作用域内将变量视为特定类型:

`def` func(x: list[int] | list[str]):
    `try`:
        `return` sum(x)
    `except` TypeError:
        x = cast(list[str], x)
        `return` ','.join(x)

谨慎使用 cast

cast 是一种覆盖代码中特定位置可能存在的所有推断或先前注释的方法。它可能隐藏代码中的实际类型错误,导致类型检查通行不完整或不准确。在前面的示例中,func 本身不会引发任何 mypy 警告,但如果传递了混合整数和字符串的列表,则在运行时会失败。

定义自定义类型

正如 Python 的class语法允许创建新的运行时类型和行为一样,本节讨论的 typing 模块构造使得能够创建用于高级类型检查的专门类型表达式。

typing 模块包括三个类,你可以继承这些类来获取类型定义和其他默认特性,详见表 5-7。

表 5-7. 定义自定义类型的基类

Generic Generic[type_var, ...] 定义了一个类型检查抽象基类,用于类的方法引用一个或多个 TypeVar 定义的类型。泛型将在以下小节详述。
NamedTuple NamedTuple 是 collections.namedtuple 的有类型实现。详见“NamedTuple”获取更多详情和示例。
TypedDict TypedDict 3.8+ 定义了一个类型检查字典,其具有每个键的特定键和值类型。详见“TypedDict”了解详情。

泛型和 TypeVar

泛型 是定义类模板的类型,这些类可以根据一个或多个类型参数调整其方法签名的类型注释。例如,dict 是一个泛型,接受两个类型参数:字典键的类型和字典值的类型。以下是如何使用 dict 来定义一个将颜色名称映射到 RGB 三元组的字典:

color_lookup: dict[str, tuple[int, int, int]] = {}

变量 color_lookup 将支持如下语句:

color_lookup['red'] = (255, 0, 0)
color_lookup['red'][2]

然而,以下语句由于键或值类型不匹配而生成了 mypy 错误:

color_lookup[0]
error: Invalid index type "int" for "dict[str, tuple[int, int, int]]";
expected type "str"
color_lookup['red'] = (255, 0, 0, 0)
error: Incompatible types in assignment (expression has type
"tuple[int, int, int, int]", target has type "tuple[int, int, int]")

泛型类型允许在一个类中定义与该类所处理对象的具体类型无关的行为。泛型通常用于定义容器类型,如 dict、list、set 等。通过定义泛型类型,我们避免了对 DictOfStrInt、DictOfIntEmployee 等详细定义类型的必要性。相反,泛型 dict 被定义为 dict[KT, VT],其中KTVT是字典的键类型和值类型的占位符,并且可以在实例化字典时定义任何特定类型。

举个例子,让我们定义一个假想的泛型类:一个累加器,可以更新值,但也支持撤销方法。由于累加器是一个泛型容器,我们声明一个 TypeVar 来表示所包含对象的类型:

`import` typing
T = typing.TypeVar('T')

累加器类被定义为泛型的子类,其中 T 作为类型参数。以下是类声明及其 init 方法,它创建了一个包含对象类型 T 的初始为空的列表:

`class` Accumulator(typing.Generic[T]):
    `def` __init__(self):
        self._contents: list[T] = []

要添加 update 和 undo 方法,我们定义引用类型 T 的参数,表示所包含的对象类型:

    `def` update(self, *args: T) -> `None`:
        self._contents.extend(args)

    `def` undo(self) -> `None`:
        *`# remove last value added`*
        `if` self._contents:
            self._contents.pop()

最后,我们添加 leniter 方法,以便可以对累加器实例进行迭代:

    `def` __len__(self) -> int:
        `return` len(self._contents)

    `def` __iter__(self) -> typing.Iterator[T]:
        `return` iter(self._contents)

现在,可以使用 Accumulator[int]编写代码来收集多个整数值:

acc: Accumulator[int] = Accumulator()
acc.update(1, 2, 3)
print(sum(acc))  # prints 6
acc.undo()
print(sum(acc))  # prints 3

因为 acc 是包含 ints 的 Accumulator,所以下面的语句会生成 mypy 错误消息:

acc.update('A')
error: Argument 1 to "update" of "Accumulator" has incompatible type
"str"; expected "int"
print(''.join(acc))
error: Argument 1 to "join" of "str" has incompatible type
"Accumulator[int]"; expected "Iterable[str]"

限制 TypeVar 为特定类型

在我们的 Accumulator 类中,我们从未直接调用所包含的 T 对象的方法。对于这个示例,T TypeVar 是纯粹无类型的,因此像 mypy 这样的类型检查器无法推断出 T 对象的任何属性或方法的存在。如果泛型需要访问其包含的 T 对象的属性,则应使用 TypeVar 的修改形式来定义 T。

下面是一些 TypeVar 定义的示例:

*`# T must be one of the types listed (int, float, complex, or str)`*
T = typing.TypeVar('T', int, float, complex, str)
*`# T must be the class MyClass or a subclass of the class MyClass`*
T = typing.TypeVar('T', bound=MyClass)
*`# T must implement __len__ to be a valid subclass of the Sized protocol`*
T = typing.TypeVar('T', bound=collections.abc.Sized)

这些形式的 T 允许在 T 的 TypeVar 定义中使用这些类型的方法。

NamedTuple

collections.namedtuple 函数简化了支持对元组元素进行命名访问的类似类的元组类型的定义。NamedTuple 提供了此功能的类型化版本,使用类似于数据类(在“数据类”中介绍)的属性样式语法的类。下面是一个具有四个元素的 NamedTuple,带有名称、类型和可选默认值:

`class` HouseListingTuple(typing.NamedTuple):
    address: str
    list_price: int
    square_footage: int = 0
    condition: str = 'Good'

NamedTuple 类生成一个默认的构造函数,接受每个命名字段的位置参数或命名参数:

listing1 = HouseListingTuple(
    address='123 Main',
    list_price=100_000,
    square_footage=2400,
    condition='Good',
)

print(listing1.address)  *`# prints: 123 Main`*
print(type(listing1))    *`# prints: <class 'HouseListingTuple'>`*

尝试创建元组时如果元素数量过少会引发运行时错误:

listing2 = HouseListingTuple(
    '123 Main',
)
*`# raises a runtime error: TypeError: HouseListingTuple.__new__()` 
`# missing 1 required positional argument: 'list_price'`*

TypedDict

3.8+ Python 字典变量在旧代码库中经常难以理解,因为字典有两种用法:作为键/值对的集合(例如,从用户 ID 到用户名的映射),以及将已知字段名映射到值的记录。通常很容易看出函数参数将作为字典传递,但实际的键和值类型取决于可能调用该函数的代码。除了简单地定义字典可以是一个 str 到 int 值的映射,例如 dict[str, int],TypedDict 还定义了预期的键和每个相应值的类型。以下示例定义了之前房屋列表类型的 TypedDict 版本(注意,TypedDict 定义不接受默认值定义):

`class` HouseListingDict(typing.TypedDict):
    address: str
    list_price: int
    square_footage: int
    condition: str

TypedDict 类生成一个默认的构造函数,为每个定义的键接受命名参数:

listing1 = HouseListingDict(
    address='123 Main',
    list_price=100_000,
    square_footage=2400,
    condition='Good',
)

print(listing1['address'])  # prints *`123 Main`*
print(type(listing1))  # prints *`<class 'dict'>`*

listing2 = HouseListingDict(
    address='124 Main',
    list_price=110_000,
)

与 NamedTuple 示例不同,listing2 不会引发运行时错误,只是创建一个具有给定键的字典。但是,mypy 将使用消息标记 listing2 为类型错误:

error: Missing keys ("square_footage", "condition") for TypedDict
"HouseListing"

要向类型检查器指示某些键可能被省略(但仍然验证给定的键),请将 total=False 添加到类声明中:

`class` HouseListing(typing.TypedDict, total=False):
    *`# ...`*

3.11+ 个别字段还可以使用 Required 或 NotRequired 类型注释显式地标记它们为必需或可选:

`class` HouseListing(typing.TypedDict):
    address: typing.Required[str]
    list_price: int
    square_footage: typing.NotRequired[int]
    condition: str

TypedDict 也可以用来定义泛型类型:

T = typing.TypeVar('T')

`class` Node(typing.TypedDict, typing.Generic[T]):
    label: T
    neighbors: list[T]

n = Node(label='Acme', neighbors=['anvil', 'magnet', 'bird seed'])

不要使用传统的 TypedDict(name, **fields) 格式

为了支持向较旧版本的 Python 进行回溯,TypedDict 的初始版本也允许您使用类似于 namedtuple 的语法,例如:

HouseListing = TypedDict('HouseListing',
                         address=str, 
                         list_price=int, 
                         square_footage=int, 
                         condition=str)

或:

HouseListing = TypedDict('HouseListing',
                         {'address': str, 
                          'list_price': int, 
                          'square_footage': int,
                          'condition': str})

这些形式在 Python 3.11 中已被弃用,并计划在 Python 3.13 中移除。

请注意,TypedDict 实际上不定义新类型。通过从 TypedDict 继承创建的类实际上充当字典工厂,从而创建的实例 字典。通过重新使用定义 Node 类的先前代码片段,我们可以看到这一点,使用 type 内置函数:

n = Node(label='Acme', neighbors=['anvil', 'magnet', 'bird seed'])
print(type(n))           *`# prints: <class 'dict'>`*
print(type(n) is dict)   *`# prints: True`*

使用 TypedDict 时没有特殊的运行时转换或初始化;TypedDict 的好处来自静态类型检查和自我文档化,这些自然地通过使用类型注解积累。

TypeAlias

3.10+ 定义简单类型别名可能会被误解为将类分配给变量。例如,在这里我们为数据库中的记录标识符定义了一个类型:

Identifier = int

为了澄清这个声明是为了定义用于类型检查的自定义类型名称,请使用 TypeAlias:

Identifier: TypeAlias = int

TypeAlias 在定义尚未定义的类型并以字符串值引用时非常有用:

*`# Python will treat this like a standard str assignment`*
TBDType = 'ClassNotDefinedYet'

*`# indicates that this is actually a forward reference to a class`*
TBDType: TypeAlias = 'ClassNotDefinedYet'

TypeAlias 类型只能在模块范围内定义。使用 TypeAlias 定义的自定义类型与目标类型可互换。与后续章节中涵盖的 NewType 相对比(NewType 不创建新类型,仅为现有类型提供新名称),TypeAlias 仅为现有类型提供新名称。

NewType

NewType 允许您定义特定于应用程序的子类型,以避免使用相同类型为不同变量可能导致的混淆。例如,如果您的程序使用 str 值来表示不同类型的数据,很容易意外地交换值。假设您有一个模拟员工和部门的程序。以下类型声明不够描述清楚——哪一个是关键,哪一个是值?

employee_department_map: dict[str, str] = {}

为员工和部门 ID 定义类型使得声明更清晰:

EmpId = typing.NewType('EmpId', str)
DeptId = typing.NewType('DeptId', str)
employee_department_map: dict[EmpId, DeptId] = {}

这些类型定义也将允许类型检查器标记此不正确的使用:

`def` transfer_employee(empid: EmpId, to_dept: DeptId):
 *`# update department for employee`
*     employee_department_map[to_dept] = empid

运行 mypy 时会报告这些错误,如下所示:employee_department_map[to_dept] = empid。

error: Invalid index type "DeptId" for "Dict[EmpId, DeptId]"; expected
type "EmpId"
error: Incompatible types in assignment (expression has type "EmpId",
target has type "DeptId")

使用 NewType 通常需要您也使用 typing.cast;例如,要创建一个 EmpId,您需要将一个 str 强制转换为 EmpId 类型。

您还可以使用 NewType 指示应用程序特定类型的所需实现类型。例如,基本的美国邮政编码是五位数字。通常会看到这种实现使用 int,这在具有前导 0 的邮政编码时会出现问题。为了指示邮政编码应使用 str 实现,您的代码可以定义此类型检查类型:

ZipCode = typing.NewType("ZipCode", str)

使用 ZipCode 注释变量和函数参数将有助于标记错误的 int 用于邮政编码值的使用。

在运行时使用类型注解

函数和类变量的注释可以通过访问函数或类的 annotations 属性进行内省(尽管更好的做法是调用 inspect.get_annotations()):

>>> `def` f(a:list[str], b) -> int:
...     `pass`
...
>>> f.__annotations__
{'a': list[str], 'return': <class 'int'>}
>>> `class` Customer:
...     name: str
...     reward_points: int = 0
...
>>> Customer.__annotations__
{'name': <class 'str'>, 'reward_points': <class 'int'>}

此功能被 pydantic 和 FastAPI 等第三方包使用,以提供额外的代码生成和验证功能。

3.8+ 要定义自己的自定义协议类,以支持运行时检查的子类和 isinstance,请将该类定义为 typing.Protocol 的子类,并对所需的协议方法进行空方法定义,并使用@runtime_checkable(在表 5-6 中介绍)。如果使用@runtime_checkable 装饰它,您仍然定义了一个非常适用于静态类型检查的协议,但它不会使用 issubclass 和 isinstance 进行运行时检查。

例如,我们可以定义一个协议,指示一个类实现了更新和撤销方法,如下所示(Python 中的省略号...是指示空方法定义的便捷语法):

T = typing.TypeVar('T')

@typing.runtime_checkable
`class` SupportsUpdateUndo(typing.Protocol):
    `def` update(self, *args: T) -> `None`:
        ...
    `def` undo(self) -> `None`:
        ...

在不对 Accumulator 的继承路径进行任何更改(在“泛型和 TypeVars”中定义)的情况下,它现在满足了对 SupportsUpdateUndo 的运行时类型检查:

>>> issubclass(Accumulator, SupportsUpdateUndo)
True
>>> isinstance(acc, SupportsUpdateUndo)
True

另外,现在任何其他实现了更新和撤销方法的类都将被视为SupportsUpdateUndo的“子类”。

如何为您的代码添加类型注解

看到了使用类型注解提供的一些特性和功能,您可能想知道最佳的入门方式。本节描述了添加类型注解的几种情景和方法。

向新代码添加类型注解

当您开始编写一个简短的 Python 脚本时,添加类型注解可能会显得多余。作为“两个披萨规则”的一个衍生,我们建议使用“两个函数规则”:一旦您的脚本包含两个函数或方法,就回头添加方法签名的类型注解,以及必要时添加任何共享变量或类型。使用 TypedDict 来注释任何在类的位置使用的 dict 结构,以便在一开始就清晰地定义 dict 键或在进行过程中进行文档化;使用 NamedTuples(或数据类:本书的一些作者强烈倾向于后者)来定义所需的特定属性,以用于这些数据“捆”。

如果您开始一个具有许多模块和类的重大项目,那么您一定应该从一开始就使用类型注解。它们可以让您更加高效,因为它们有助于避免常见的命名和类型错误,并确保您在 IDE 中获得更全面的支持自动完成。在具有多个开发人员的项目中,这一点尤为重要:在代码中记录类型有助于告诉团队中的每个人对类型和值的期望。将这些类型捕获在代码中使它们在开发过程中立即可访问和可见,比单独的文档或规范要更加方便。

如果你正在开发一个要在多个项目中共享的库,那么最好从一开始就使用类型注解,很可能与你 API 设计中的函数签名并行。在库中添加类型注解将会为客户开发者简化生活,因为所有现代 IDE 都包含类型注解插件来支持静态类型检查、函数自动完成和文档编写。它们在编写单元测试时也会帮助你,因为你将受益于相同的丰富 IDE 支持。

对于任何这些项目,将类型检查实用程序添加到你的预提交挂钩中,这样你可以及时解决任何可能潜入你新代码库中的类型违规。这样一来,你可以在出现问题时修复它们,而不是等到做大的提交后才发现在多个地方都出现了基本的类型错误。

给现有代码添加类型注解(渐进式类型)

有几家公司已经运行了将类型注解应用于大型现有代码库的项目,推荐采用渐进式的方法,称为渐进式类型。通过渐进式类型,你可以逐步地按步骤处理你的代码库,逐步添加和验证类型注解到几个类或模块。

有些工具,比如 mypy,会让你逐个函数地添加类型注解。默认情况下,mypy 会跳过没有类型签名的函数,因此你可以逐步地逐个函数地处理你的代码库。这种增量的过程允许你将精力集中在代码的各个部分,而不是一次性地在所有地方添加类型注解,然后试图解决一堆类型检查器错误。

推荐的一些方法包括:

  • 确定你使用最频繁的模块,并逐步添加类型,逐个方法地进行。(这些可能是核心应用程序类模块,或广泛共享的实用程序模块。)

  • 逐个方法地添加注解,以便逐步引发并解决类型检查问题。

  • 使用 pytype 或 pyre 推断生成初始的 .pyi 桩文件(在下一节中讨论)。然后,逐步从 .pyi 文件中迁移类型,可以手动进行,也可以使用像 pytype 的 merge_pyi 工具这样的自动化工具。

  • 开始使用类型检查器的宽松默认模式,这样大部分代码会被跳过,你可以将注意力集中在特定的文件上。随着工作的进行,逐渐转向更严格的模式,以突出剩余的项目,并且已经注释的文件不会因为接受新的非注释代码而退步。

使用 .pyi 桩文件

有时候你可能无法访问 Python 的类型注解。例如,你可能正在使用一个没有类型注解的库,或者使用一个其函数是用 C 实现的模块。

在这些情况下,可以使用单独的*.pyi*存根文件,其中只包含相关的类型注解。本章开头提到的多个类型检查器可以生成这些存根文件。您可以从typeshed 存储库下载流行的 Python 库以及 Python 标准库本身的存根文件。您可以从 Python 源文件中维护存根文件,或者使用某些类型检查器中可用的合并工具将其集成回原始 Python 源代码中。

摘要

Python 作为一个强大的语言和编程生态系统已经稳步崛起,支持重要的企业应用程序。曾经作为脚本和任务自动化的实用语言,现在已经成为影响数百万用户的重要和复杂应用程序平台,用于关键任务甚至是地外系统。⁵ 添加类型注解是开发和维护这些系统的重要一步。

Python 的类型注解在线文档提供了最新的描述、示例和最佳实践,因为类型注解的语法和实践不断演变。作者还特别推荐了流畅的 Python第二版,作者是 Luciano Ramalho(O'Reilly),尤其是第八章和第十五章,这些章节专门讲解了 Python 类型注解。

¹ 强大而广泛的单元测试也将防范许多商业逻辑问题,这是任何类型检查都无法捕捉的—所以,类型提示不应该代替单元测试,而应该单元测试一起使用。

² 类型注解的语法在 Python 3.0 中引入,但其语义则是后来才明确指定的。

³ 这种方法也兼容 Python 2.7 代码,当时广泛使用。

⁴ 并且 SupportsInt 使用了 runtime_checkable 装饰器。

⁵ NASA 的喷气推进实验室使用 Python 开发了坚韧号火星车和毅力号火星直升机;负责发现引力波的团队既用 Python 协调仪器,也用 Python 分析了得到的大量数据。

第六章:异常

Python 使用 异常 来指示错误和异常情况。当 Python 检测到错误时,它 引发 一个异常——也就是说,Python 通过将异常对象传递给异常传播机制来表示异常条件的发生。您的代码可以通过执行 raise 语句显式地引发异常。

处理 异常意味着从传播机制捕获异常对象,并根据需要采取行动来处理异常情况。如果程序未处理异常,则程序将以错误消息和回溯消息终止。然而,程序可以通过使用带有 except 子句的 try 语句来处理异常并继续运行,尽管存在错误或其他异常情况。

Python 还使用异常来指示一些不是错误,甚至不是异常的情况。例如,如在 “Iterators” 中所述,对迭代器调用内置的 next 函数在迭代器没有更多项时引发 StopIteration。这不是错误;它甚至不是异常,因为大多数迭代器最终会耗尽项目。因此,在 Python 中检查和处理错误及其他特殊情况的最佳策略与其他语言不同;我们在 “Error-Checking Strategies” 中介绍它们。

本章介绍如何使用异常处理错误和特殊情况。还涵盖了标准库中的日志记录模块,在 “Logging Errors” 中,以及 assert 语句,在 “The assert Statement” 中。

The try Statement

try 语句是 Python 的核心异常处理机制。它是一个复合语句,具有三种可选的子句:

  1. 它可以有零个或多个 except 子句,定义如何处理特定类别的异常。

  2. 如果它有 except 子句,那么紧接着可能还有一个 else 子句,仅当 try 语句块未引发异常时执行。

  3. 无论它是否有 except 子句,它都可能有一个单独的 finally 子句,无条件执行,其行为在 “try/except/finally” 中介绍。

Python 的语法要求至少有一个 except 子句或一个 finally 子句,两者都可以在同一语句中存在;else 只能在一个或多个 except 之后才有效。

try/except

下面是 try 语句的 try/except 形式的语法:

`try`:
    *`statement``(``s``)`*
`except` [*`expression`* [`as` *`target`*]]:
    *`statement``(``s``)`*
[`else`:
    *`statement``(``s``)`*]
[`finally`:
    *`statement``(``s``)`*]

这种形式的 try 语句具有一个或多个 except 子句,以及一个可选的 else 子句(和一个可选的 finally 子句,其含义不取决于是否存在 exceptelse 子句:我们在下一节中详细介绍这一点)。

每个 except 子句的主体称为 异常处理程序。当 except 子句中的 表达式 与从 try 子句传播出的异常对象匹配时,代码执行。表达式 是一个类或类元组,用括号括起来,匹配任何一个这些类或它们的子类的实例。可选的 目标 是一个标识符,它命名一个变量,Python 在异常处理程序执行之前将异常对象绑定到该变量上。处理程序还可以通过调用模块 sys 的 exc_info 函数(3.11+ 或异常函数)来获取当前的异常对象(在 Table 9-3 中介绍)。

这里是 try 语句的 try/except 形式的示例:

`try`:
    1/0
    print('not executed')
`except` ZeroDivisionError:
    print('caught divide-by-0 attempt')

当引发异常时,try 套件的执行立即停止。如果一个 try 语句有多个 except 子句,异常传播机制按顺序检查 except 子句;第一个表达式匹配异常对象的 except 子句作为处理程序执行,异常传播机制在此之后不再检查任何其他 except 子句。

先具体后一般

将特定情况的处理程序放在一般情况的处理程序之前:当你首先放置一个一般情况时,随后的更特定的 except 子句将不会执行。

最后的 except 子句不需要指定表达式。一个没有任何表达式的 except 子句处理在传播期间达到它的任何异常。这样的无条件处理很少见,但确实会发生,通常在必须在重新引发异常之前执行某些额外任务的“包装器”函数中(参见 “The raise Statement”)。

避免“裸的 except” 没有重新引发

小心使用“裸”的 except(一个没有表达式的 except 子句),除非你在其中重新引发异常:这种粗糙的风格会使得错误非常难以找到,因为裸的 except 太宽泛,可以轻易掩盖编码错误和其他类型的错误,允许执行在未预期的异常后继续。

“只是想让事情运行起来”的新程序员甚至可能编写如下的代码:

`try``:`
    *`# ...code that has a problem...`*
`except``:`
    `pass`

这是一种危险的做法,因为它捕捉到重要的进程退出异常,如 KeyboardInterrupt 或 SystemExit ——带有这种异常处理程序的循环无法通过 Ctrl-C 退出,并且可能甚至无法通过系统 kill 命令终止。至少,这样的代码应该使用 except Exception:,这仍然太宽泛,但至少不会捕获导致进程退出的异常。

当它找到一个表达式与异常对象匹配的处理程序时,异常传播终止。当try语句嵌套(在源代码的词法上,或在函数调用中动态地)在另一个try语句的try子句中时,内部try建立的处理程序首先在传播时达到,因此当匹配时它处理异常。这可能不是您想要的。考虑这个例子:

`try`:
    `try`:
        1/0
    `except`:
        print('caught an exception')
`except` `ZeroDivisionError`:
    print('caught divide-by-0 attempt')
*`# prints:`* *`caught an exception`*

在这种情况下,由外部try子句中的except ZeroDivisionError:建立的处理程序比内部try子句中的通用except:更为具体,并不重要。外部try不参与其中:异常不会从内部try传播出来。有关异常传播的更多信息,请参阅“异常传播”。

try/except的可选else子句仅在try子句正常终止时执行。换句话说,当异常从try子句传播出来时,或者try子句以breakcontinuereturn语句退出时,else子句不会执行。由try/except建立的处理程序仅覆盖try子句,而不包括else子句。else子句对于避免意外处理未预期的异常很有用。例如:

print(repr(value), 'is ', end=' ')
`try`:
    value + 0
`except` TypeError:
    *`# not a number, maybe a string...?`*
    `try`:
        value + ''
    `except` TypeError:
        print('neither a number nor a string')
    `else`:
        print('some kind of string')
`else`:
    print('some kind of number')

try/finally

下面是try语句的try/finally形式的语法:

`try`:
    *`statement``(``s``)`*
`finally`:
    *`statement``(``s``)`*

此形式有一个finally子句,没有else子句(除非它还有一个或多个except子句,如下一节所述)。

finally子句建立了所谓的清理处理程序。这段代码在try子句以任何方式终止后始终执行。当异常从try子句传播时,try子句终止,清理处理程序执行,异常继续传播。当没有异常发生时,无论try子句是否达到其末尾或通过执行breakcontinuereturn语句退出,清理处理程序都会执行。

使用try/finally建立的清理处理程序提供了一种健壮且明确的方式来指定必须始终执行的最终代码,无论如何,以确保程序状态和/或外部实体(例如文件、数据库、网络连接)的一致性。这种确保的最终化现在通常通过在with语句中使用上下文管理器来表达最佳(请参阅“with 语句和上下文管理器”)。这里是try语句的try/finally形式的示例:

f = open(some_file, 'w')
`try`:
    do_something_with_file(f)
`finally`:
    f.close()

这里是相应的更简洁和可读性更好的示例,使用with来达到完全相同的目的:

`with` open(some_file, 'w') `as` f:
    do_something_with_file(f)

避免在finally子句中使用breakreturn语句。

finally 子句可以包含一个或多个语句 continue,3.8+ breakreturn。 然而,这种用法可能使你的程序变得不太清晰:当这样的语句执行时,异常传播会停止,并且大多数程序员不希望在 finally 子句内停止传播。 这种用法可能会使阅读你代码的人感到困惑,因此我们建议你避免使用它。

try/except/finally

一个 try/except/finally 语句,例如:

`try`:
    ...*`guarded` `clause`*...
`except` ...*`expression`*...:
    ...*`exception` `handler` `code`*...
`finally`:
    ...*`cleanup` `code`*...

等价于嵌套语句:

`try`:
    `try`:
        ...*`guarded` `clause`*...
    `except` ...*`expression`*...:
        ...*`exception` `handler` `code`*...
`finally`:
    ...*`cleanup` `code`*...

try 语句可以有多个 except 子句,并且可选地有一个 else 子句,在终止的 finally 子句之前。 在所有变体中,效果总是像刚才展示的那样 - 即,它就像将一个 try/except 语句的所有 except 子句和 else 子句(如果有的话)嵌套到包含的 try/finally 语句中。

raise 语句

你可以使用 raise 语句显式地引发异常。 raise 是一个简单语句,其语法如下:

`raise` [*`expression`* [`from` *`exception`*]]

只有异常处理程序(或处理程序直接或间接调用的函数)可以使用没有任何表达式的 raise。 一个普通的 raise 语句会重新引发处理程序收到的相同异常对象。 处理程序终止,异常传播机制继续沿调用堆栈向上搜索其他适用的处理程序。 当处理程序发现无法处理接收到的异常或只能部分处理异常时,使用没有任何表达式的 raise 是有用的,因此异常应继续传播以允许调用堆栈上的处理程序执行其自己的处理和清理。

当存在 expression 时,它必须是从内置类 BaseException 继承的类的实例,Python 将引发该实例。

当包括 from exception(只能出现在接收 exceptionexcept 块中)时,Python 会将接收到的表达式“嵌套”在新引发的异常表达式中。 "异常“包裹”其他异常或回溯" 更详细地描述了这一点。

下面是 raise 语句的一个典型用例示例:

`def` cross_product(seq1, seq2):
    `if` `not` seq1 `or` `not` seq2:
        `raise` ValueError('Sequence arguments must be non-empty') ![1](https://gitee.com/OpenDocCN/ibooker-python-zh/raw/master/docs/py-ntsh-3e/img/1.png)
    `return` [(x1, x2) `for` x1 `in` seq1 `for` x2 `in` seq2]

1

有些人认为在此处引发标准异常是不合适的,他们更倾向于引发自定义异常的实例,这在本章后面有所涵盖;本书的作者对此持不同意见。

此 cross_product 示例函数返回由其序列参数中的每个项目组成的所有配对的列表,但首先它测试了两个参数。 如果任一参数为空,则该函数引发 ValueError,而不仅仅像列表推导通常所做的那样返回一个空列表。

只检查你需要的内容

无需 cross_product 检查 seq1 和 seq2 是否可迭代:如果其中任一者不可迭代,则列表推导本身会引发适当的异常,通常是 TypeError。

一旦由 Python 本身引发异常,或者在代码中使用显式 raise 语句引发异常,就由调用者来处理它(使用合适的 try/except 语句)或让它继续向调用堆栈上传播。

不要为冗余错误检查使用 raise 语句

仅在你的规范定义为错误的情况下,才使用 raise 语句来引发额外的异常。不要使用 raise 来复制 Python 已(隐式地)代表你执行的相同错误检查。

with 语句和上下文管理器

with 语句是一个复合语句,具有以下语法:

with *expression* [as *varname*] [, ...]:
    *statement(s)*

*# 3.10+ multiple context managers for a with statement* 
*# can be enclosed in parentheses*
with (*expression* [as *varname*], ...):
    *statement(s)*

with 的语义等效于

_normal_exit = `True`
_manager = *`expression`*
*`varname`* = _manager.__enter__()
`try`:
    *`statement``(``s``)`*
`except`:
    _normal_exit = `False`
    `if` `not` _manager.__exit_(*sys.exc_info()):
        `raise`
    *`# note that exception does not propagate if __exit__ returns`* 
    *`# a true value`*
`finally`:
    `if` _normal_exit:
        _manager.__exit__(`None``,` `None``,` `None`)

其中 _manager 和 _normal_exit 是当前范围中不使用的任意内部名称。如果在 with 子句的可选 as varname 部分中省略 varname,Python 仍会调用 _manager.enter,但不会将结果绑定到任何名称,并且仍会在块终止时调用 _manager.exit。通过 expression 返回的对象,具有方法 enterexit,被称为 上下文管理器

with 语句是 Python 中著名的 C++ 惯用语 “资源获取即初始化” (RAII) 的体现:你只需编写上下文管理器类,即包含两个特殊方法的类,enterexitenter 方法必须可被无参数调用。exit 方法必须接受三个参数:当主体完成且未传播异常时均为 None,否则为异常的类型、值和回溯信息。这提供了与 C++ 中自动变量的典型构造函数/析构函数对和 Python 或 Java 中 try/finally 语句相同的确保最终化行为。此外,它们可以根据传播的异常(如果有的话)以不同方式进行最终化,并且通过从 exit 返回 true 值来选择性地阻止传播的异常。

例如,这里是一个简单的纯示例方式,确保在一些其他输出周围打印 标签(请注意,上下文管理器类通常具有小写名称,而不是遵循类名的正常大写约定):

`class` enclosing_tag:
    `def` __init__(self, tagname):
        self.tagname = tagname
    `def` __enter__(self):
        print(f'<{self.tagname}>', end='')
    `def` __exit__(self, etyp, einst, etb):
        print(f'</{self.tagname}>')

*`# to be used as:`*
`with` enclosing_tag('sometag'):
    *`# ...statements printing output to be enclosed in`*
    *``# a matched open/close `sometag` pair...``*

构建上下文管理器的一种更简单方法是使用 Python 标准库中 contextlib 模块中的 contextmanager 装饰器。此装饰器将生成器函数转换为上下文管理器对象的工厂。

在 contextlib 模块中导入之后,实现 enclosing_tag 上下文管理器的方式是:

@contextlib.contextmanager
`def` enclosing_tag(tagname):
    print(f'<{tagname}>', end='')
    `try`:
        `yield`
    `finally`:
        print(f'</{tagname}>')
*`# to be used the same way as before`*

contextlib 提供了 Table 6-1 中列出的类和函数,以及其他一些类和函数。

表 6-1. 上下文管理模块中常用的类和函数

AbstractContextManager AbstractContextManager 是一个具有两个可重写方法的抽象基类:enter 默认为 return self,exit 默认为 return None。
chdir chdir(dir_path) 3.11+ 一个上下文管理器,其 enter 方法保存当前工作目录路径并执行 os.chdir(dir_path),其 exit 方法执行 os.chdir(saved_path)。
closing closing(something) 一个上下文管理器,其 enter 方法返回something,而其 exit 方法调用something.close()。
contextmanager contextmanager 将生成器应用为上下文管理器的装饰器。
nullcontext nullcontext(something) 一个上下文管理器,其 enter 方法返回something,而其 exit 方法什么也不做。
redirect_stderr redirect_stderr(destination) 一个上下文管理器,可以临时将 sys.stderr 在with语句体内重定向到文件或类文件对象destination
redirect_stdout redirect_stdout(destination) 一个上下文管理器,可以临时将 sys.stdout 在with语句体内重定向到文件或类文件对象destination

| suppress | suppress(exception_classes) 一个上下文管理器,可以在列出的exception_classes中的任何一种出现在with语句体中时,静默地抑制异常。例如,这个删除文件的函数忽略了 FileNotFoundError:

`def` delete_file(filename):
    `with` contextlib.suppress(FileNotFoundError):
        os.remove(filename)

使用时要节制,因为静默地抑制异常通常是不好的做法。 |

更多细节、示例、“配方”甚至更多(有些深奥)的类,请参阅 Python 的在线文档

Generators and Exceptions

为了帮助生成器与异常协作,try/finally语句中允许使用yield语句。此外,生成器对象还有另外两个相关方法,throw 和 close。给定通过调用生成器函数构建的生成器对象g,throw 方法的签名为:

g.throw(*`exc_value`*)

当生成器的调用者调用g.throw 时,其效果就好像在生成器g暂停的yield处执行具有相同参数的raise语句一样。

生成器方法 close 没有参数;当生成器的调用者调用g.close()时,其效果就像调用g.throw(GeneratorExit())一样。¹ GeneratorExit 是一个直接继承自 BaseException 的内置异常类。生成器还有一个终结器(特殊方法 del),当生成器对象被垃圾回收时会隐式调用 close 方法。

如果生成器引发或传播 StopIteration 异常,Python 会将异常类型转换为 RuntimeError。

异常传播

当引发异常时,异常传播机制接管控制。程序的正常控制流停止,Python 寻找合适的异常处理程序。Python 的 try 语句通过其 except 子句设立异常处理程序。处理程序处理 try 子句中引发的异常,以及直接或间接调用该代码的函数中传播的异常。如果在具有适用 except 处理程序的 try 子句中引发异常,则 try 子句终止并执行处理程序。处理程序完成后,继续执行 try 语句之后的语句(在没有显式更改控制流程的情况下,例如 raisereturn 语句)。

如果引发异常的语句不在具有适用处理程序的 try 子句内,则包含该语句的函数终止,并且异常沿着函数调用堆栈向上“传播”到调用该函数的语句。如果终止的函数调用位于具有适用处理程序的 try 子句内,则该 try 子句终止,并执行处理程序。否则,包含调用的函数终止,并且传播过程重复,展开 函数调用堆栈,直到找到适用的处理程序。

如果 Python 找不到任何适用的处理程序,默认情况下程序会将错误消息打印到标准错误流(sys.stderr)。错误消息包括有关在传播过程中终止的函数的详细跟踪信息。您可以通过设置 sys.excepthook(在 表 8-3 中讨论)来更改 Python 的默认错误报告行为。在错误报告之后,Python 返回交互会话(如果有),或者如果执行不是交互的,则终止。当异常类型为 SystemExit 时,终止是静默的,并结束交互会话(如果有)。

这里有一些函数来展示异常传播的工作原理:

`def` f():
    print('in f, before 1/0')
    1/0    *`# raises a ZeroDivisionError exception`*
    print('in f, after 1/0')
`def` g():
    print('in g, before f()')
    f()
    print('in g, after f()')
`def` h():
    print('in h, before g()')
    `try`:
        g()
        print('in h, after g()')
    `except` ZeroDivisionError:
        print('ZD exception caught')
    print('function h ends')

调用 h 函数会打印以下内容:

in h, before g()
in g, before f()
in f, before 1/0
ZD exception caught
function h ends

也就是说,由于异常传播的流程切断了它们,没有一个“after”打印语句被执行。

函数 h 设立了一个 try 语句并在 try 子句中调用函数 g。g 反过来调用 f,而 f 进行了除以 0 的操作,引发了 ZeroDivisionError 类型的异常。异常传播直到 h 的 except 子句。函数 f 和 g 在异常传播阶段终止,这就是为什么它们的“after”消息都没有打印出来。h 的 try 子句的执行也在异常传播阶段终止,因此它的“after”消息也没有打印出来。在处理程序之后,h 的 try/except 块结束时,继续执行。

异常对象

异常是 BaseException 的实例(更具体地说,是其子类之一的实例)。Table 6-2 列出了 BaseException 的属性和方法。

Table 6-2. BaseException 类的属性和方法

cause exc.cause 返回使用raise from引发的异常的父异常。
notes exc.notes 3.11+ 返回一个包含使用 add_note 添加到异常中的字符串列表。只有在至少调用一次 add_note 之后才存在此属性,因此安全的访问此列表的方法是使用 getattr(exc, 'notes', [])。
add_note exc.add_note(note) 3.11+ 将字符串 note 添加到此异常的注释中。在显示异常时,这些注释会显示在回溯信息之后。
args *exc.*args 返回用于构造异常的参数的元组。这些特定于错误的信息对诊断或恢复目的非常有用。某些异常类解释 args 并在类的实例上设置便捷的命名属性。
wi⁠t⁠h⁠_​t⁠r⁠a⁠c⁠e⁠b⁠a⁠c⁠k exc.with_traceback(tb) 返回一个新的异常,用新的回溯 tb 替换原始异常的回溯,如果 tbNone 则不包含回溯。可用于修剪原始回溯以删除内部库函数调用帧。

标准异常的层次结构

如前所述,异常是 BaseException 的子类的实例。异常类的继承结构很重要,因为它决定了哪些except子句处理哪些异常。大多数异常类扩展自 Exception 类;然而,KeyboardInterrupt、GeneratorExit 和 SystemExit 直接继承自 BaseException,并不是 Exception 的子类。因此,一个处理器子句except Exception as e 无法捕获 KeyboardInterrupt、GeneratorExit 或 SystemExit(我们在“try/except”和“Generators and Exceptions”中介绍了异常处理程序)。SystemExit 的实例通常是通过 sys 模块中的 exit 函数引发的(在 Table 8-3 中有所涵盖)。当用户按 Ctrl-C、Ctrl-Break 或其他中断键时,会引发 KeyboardInterrupt。

内置异常类的层次结构大致如下:

BaseException
  Exception
    AssertionError, AttributeError, BufferError, EOFError,
    MemoryError, ReferenceError, OsError, StopAsyncIteration,
    StopIteration, SystemError, TypeError
    ArithmeticError (abstract)
      OverflowError, ZeroDivisionError
    ImportError
      ModuleNotFoundError, ZipImportError
    LookupError (abstract)
      IndexError, KeyError
    NameError
      UnboundLocalError
    OSError
      ...
    RuntimeError
      RecursionError
      NotImplementedError
    SyntaxError
      IndentationError
        TabError
    ValueError
      UnsupportedOperation
      UnicodeError
        UnicodeDecodeError, UnicodeEncodeError,
        UnicodeTranslateError
    Warning
      ...
  GeneratorExit
  KeyboardInterrupt
  SystemExit

其他异常子类(特别是警告和 OSError 有很多,这里用省略号表示),但这就是要点。完整列表可在 Python 的在线文档中找到。

标记为“(abstract)”的类永远不会直接实例化;它们的目的是使您能够指定处理一系列相关错误的except子句。

标准异常类

Table 6-3 列出了由常见运行时错误引发的异常类。

Table 6-3. 标准异常类

异常类 抛出时机
AssertionError 一个assert语句失败。
AttributeError 属性引用或赋值失败。
ImportError 一个importfrom...import语句(详见“import 语句”)找不到要导入的模块(在这种情况下,Python 实际引发的是 ImportError 的子类 ModuleNotFoundError),或找不到要从模块导入的名称。
IndentationError 解析器由于不正确的缩进而遇到语法错误。子类 SyntaxError。
IndexError 用于索引序列的整数超出范围(使用非整数作为序列索引会引发 TypeError)。子类 LookupError。
KeyboardInterrupt 用户按下中断键组合(Ctrl-C、Ctrl-Break、Delete 或其他,取决于平台对键盘的处理)。
KeyError 用于索引映射的键不在映射中。子类 LookupError。
MemoryError 操作耗尽了内存。
NameError 引用了一个名称,但它没有绑定到当前作用域中的任何变量。
N⁠o⁠t⁠I⁠m⁠p⁠l⁠e⁠m⁠e⁠n⁠t⁠e⁠d​E⁠r⁠r⁠o⁠r 抽象基类引发以指示必须重写方法的具体子类。
OSError 由 os 模块中的函数引发(详见“os 模块”和“使用 os 模块运行其他程序”),以指示平台相关的错误。OSError 有许多子类,详见下一小节。
RecursionError Python 检测到递归深度已超出。子类 RuntimeError。
RuntimeError 为未归类的任何错误或异常引发。
SyntaxError Python 解析器遇到语法错误。
SystemError Python 检测到自己代码或扩展模块中的错误。请向您的 Python 版本维护者或相关扩展的维护者报告此问题,包括错误消息、确切的 Python 版本(sys.version),如果可能的话,请附上程序源代码。
TypeError 应用于不适当类型的对象的操作或函数。
UnboundLocalError 引用了一个本地变量,但当前未绑定任何值到该本地变量。子类 NameError。
UnicodeError 在转换 Unicode(即 str)到字节字符串或反之过程中发生错误。子类 ValueError。
ValueError 应用于具有正确类型但不合适值的对象的操作或函数,且没有更具体的异常适用(例如 KeyError)。
ZeroDivisionError 除数(/、//或%运算符的右操作数,或内置函数 divmod 的第二个参数)为 0。子类 ArithmeticError。

OSError 的子类

OSError 代表操作系统检测到的错误。为了更优雅地处理这些错误,OSError 有许多子类,其实例是实际抛出的内容;完整列表请参阅 Python 的在线文档

例如,考虑这个任务:尝试读取并返回某个文件的内容,如果文件不存在则返回默认字符串,并传播使文件不可读的任何其他异常(除了文件不存在)。使用现有的 OSError 子类,您可以很简单地完成这个任务:

`def` read_or_default(filepath, default):
    `try`:
        `with` open(filepath) `as` f:
            `return` f.read()
    `except` FileNotFoundError:
        `return` default

FileNotFoundError 的 OSError 子类使得这种常见任务在代码中表达起来简单直接。

异常“包装”其他异常或 traceback

有时候,在尝试处理另一个异常时会引发异常。为了让您清楚地诊断此问题,每个异常实例都持有其自己的 traceback 对象;您可以使用 with_traceback 方法创建另一个具有不同 traceback 的异常实例。

此外,Python 自动存储它正在处理的异常作为任何后续异常的 context 属性(除非您使用 raise...from 语句并将异常的 suppress_context 属性设置为 True,这部分我们稍后会讲到)。如果新异常传播,Python 的错误消息将使用该异常的 context 属性显示问题的详细信息。例如,看看这个(故意!)有问题的代码:

`try`:
    1/0
`except` ZeroDivisionError:
    1+'x'

显示的错误是:

Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
 File "<stdin>", line 3, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'

因此,Python 明确显示原始异常和干预异常。

如果您希望更多地控制错误显示,可以使用 raise...from 语句。当您执行 raise e from ex 时,eex 都是异常对象:e 是传播的异常,ex 是其“原因”。Python 记录 ex 作为 e.cause 的值,并将 e.suppress_context 设置为 true。(或者,ex 可以为 None:然后,Python 将 e.cause 设置为 None,但仍将 e.suppress_context 设置为 true,并因此保持 e.context 不变)。

作为另一个示例,这里是一个使用 Python 字典实现模拟文件系统目录的类,其中文件名作为键,文件内容作为值:

`class` FileSystemDirectory:
    `def` __init__(self):
        self._files = {}

    `def` write_file(self, filename, contents):
        self._files[filename] = contents

    `def` read_file(self, filename):
        `try`:
            return self._files[filename]
        `except` KeyError:
            `raise` FileNotFoundError(filename)

当使用不存在的文件名调用 read_file 时,对 self._files 字典的访问会引发 KeyError。由于此代码旨在模拟文件系统目录,read_file 捕获 KeyError 并抛出 FileNotFoundError。

就像现在,访问名为 'data.txt' 的不存在文件将输出类似以下的异常消息:

Traceback (most recent call last):
 File "C:\dev\python\faux_fs.py", line 11, in read_file
 return self._files[filename]
KeyError: 'data.txt'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
 File "C:\dev\python\faux_fs.py", line 20, in <module>
 print(fs.read_file("data.txt"))
 File "C:\dev\python\faux_fs.py", line 13, in read_file
 raise FileNotFoundError(filename)
FileNotFoundError: data.txt

此异常报告显示了 KeyError 和 FileNotFoundError。为了抑制内部 KeyError 异常(隐藏 FileSystemDirectory 的实现细节),我们在 read_file 中的 raise 语句改为:

 `raise` FileNotFoundError(filename) `from` `None`

现在异常只显示 FileNotFoundError 的信息:

Traceback (most recent call last):
 File "C:\dev\python\faux_fs.py", line 20, in <module>
 print(fs.read_file("data.txt"))
 File "C:\dev\python\faux_fs.py", line 13, in read_file
 raise FileNotFoundError(filename) from None
FileNotFoundError: data.txt

关于异常链和嵌入的详细信息和动机,请参阅 PEP 3134

自定义异常类

你可以扩展任何标准异常类来定义自己的异常类。通常,这样的子类除了一个文档字符串外没有其他内容:

`class` InvalidAttributeError(AttributeError):
    *`"""Used to indicate attributes that could never be valid."""`*

空类或函数应该有一个文档字符串。

如“pass 语句”所述,你不需要一个 pass 语句来构成一个类的主体。只要有文档字符串(如果什么都不写的话,也应该写一个来记录类的目的!),Python 就足够了。对于所有“空”类(无论是否为异常类),最佳实践通常是只有一个文档字符串,而没有pass语句。

鉴于try/except的语义,引发自定义异常类的实例(如 InvalidAttributeError)几乎与引发其标准异常超类 AttributeError 的实例相同,但具有一些优势。任何可以处理 AttributeError 的except子句也可以很好地处理 InvalidAttributeError。此外,了解你的 InvalidAttributeError 自定义异常类的客户端代码可以专门处理它,而不必在不准备处理其他情况的情况下处理所有其他 AttributeError 的情况。例如,假设你编写如下代码:

`class` SomeFunkyClass:
    *`"""much hypothetical functionality snipped"""`*
    `def` __getattr__(self, name):
        *`"""only clarifies the kind of attribute error"""`*
        `if` name.startswith('_'):
            `raise` InvalidAttributeError(
                f'Unknown private attribute {name!r}'
            )
        `else`:
            `raise` AttributeError(f'Unknown attribute {name!r}')

现在,客户端代码可以选择性地更加精确地处理异常。例如:

s = SomeFunkyClass()
`try`:
    value = getattr(s, thename)
`except` InvalidAttributeError as err:
    warnings.warn(str(err), stacklevel=2)
    value = `None`
*`# other cases of AttributeError just propagate, as they're unexpected`*

使用自定义异常类

在你的模块中定义和引发自定义异常类的实例,而不是普通的标准异常,是一个绝佳的主意。通过使用扩展标准异常的自定义异常类,你使调用者更容易单独处理来自你模块的异常,如果他们选择这样做的话。

自定义异常和多重继承

使用自定义异常类的有效方法是从你模块的特定自定义异常类和标准异常类中多重继承异常类,如下片段所示:

`class` CustomAttributeError(CustomException, AttributeError):
    *`"""An AttributeError which is ALSO a CustomException."""`*

现在,CustomAttributeError 的实例只能被显式和有意地引发,显示了与你的代码特别相关的错误,恰好是一个 AttributeError。当你的代码引发 CustomAttributeError 的实例时,该异常可以被设计为捕获所有 AttributeError 情况的调用代码以及被设计为仅处理你模块引发的所有异常情况的代码捕获。

使用多重继承定义自定义异常

每当您必须决定是否引发特定标准异常的实例,例如 AttributeError,或者您在模块中定义的自定义异常类的实例时,请考虑这种多重继承的方法,在本书作者的观点中,这种方法在这些情况下是最佳选择。确保您清楚地记录模块的这一方面,因为这种技术虽然方便,但使用并不广泛。除非您明确和明确地记录您正在做什么,否则模块的用户可能不会预期到这一点。

标准库中使用的其他异常

Python 标准库中的许多模块定义了它们自己的异常类,这些异常类相当于您自己模块可以定义的自定义异常类。通常,这类标准库模块中的所有函数可能会引发这些类的异常,除了“标准异常类”中覆盖的标准层次结构的异常之外。我们将在本书的其余部分中涵盖此类异常类的主要情况,这些情况涵盖了提供和可能引发它们的标准库模块的章节。

ExceptionGroup 和 except*

3.11+ 在某些情况下,例如对某些输入数据执行多个条件验证时,能够一次引发多个异常是有用的。Python 3.11 引入了一种使用 ExceptionGroup 实例一次引发多个异常并使用 except* 形式处理多个异常的机制。

要引发 ExceptionGroup,验证代码会将多个异常捕获到一个列表中,然后使用该列表构造一个 ExceptionGroup 并引发它。以下是一些搜索拼写错误和无效单词的代码,并引发包含所有找到错误的 ExceptionGroup 的示例:

`class` GrammarError(Exception):
 *`"""Base exception for grammar checking"""`*
 `def` __init__(self, found, suggestion):
        self.found = found
        self.suggestion = suggestion

`class` InvalidWordError(GrammarError):
  *`"""Misused or nonexistent word"""`*

`class` MisspelledWordError(GrammarError):
 *`"""Spelling error"""`*

invalid_words = {
    'irregardless': 'regardless',
    "ain't": "isn't",
} 
misspelled_words = {
    'tacco': 'taco',
}

`def` check_grammar(s):
    exceptions = []
 `for` word `in` s.lower().split():
 `if` (suggestion := invalid_words.get(word)) `is` `not` `None`:
 exceptions.append(InvalidWordError(word, suggestion))
 `elif` (suggestion := misspelled_words.get(word)) `is` `not` `None``:`
 exceptions.append(MisspelledWordError(word, suggestion))
 `if` exceptions:
 `raise` ExceptionGroup('Found grammar errors', exceptions)

下面的代码验证了一个示例文本字符串,并列出了所有找到的错误:

text = "Irregardless a hot dog ain't a tacco"
`try``:`
 check_grammar(text)
except* InvalidWordError `as` iwe`:`
 print('\n'.join(f'{e.found!r} is not a word, use {e.suggestion!r}'
 `for` e `in` iwe`.`exceptions))
except* MisspelledWordError `as` mwe:
  print('\n'.join(f'Found {e.found!r}, perhaps you meant'
                    f' {e.suggestion!r}?'
 `for` e `in` mwe`.`exceptions))
`else``:`
  print('No errors!')

给出以下输出:

'irregardless' is not a word, use 'regardless'
"ain't" is not a word, use "isn't"
Found 'tacco', perhaps you meant 'taco'?

except 不同,在找到初始匹配后,except* 会继续寻找匹配引发的 ExceptionGroup 中异常类型的其他异常处理程序。

错误检查策略

大多数支持异常的编程语言只在极少数情况下引发异常。Python 的重点不同。Python 认为在使程序更简单和更健壮时适当的地方引发异常,即使这样做使异常相当频繁。

LBYL 与 EAFP

在其他语言中的一种常见习惯用法,有时称为“先入为主”(LBYL),是在尝试操作之前提前检查可能使操作无效的任何内容。这种方法并不理想,原因如下:

  • 这些检查可能会降低在一切正常的常见主流情况下的可读性和清晰度。

  • 为了进行检查而需要的工作可能会重复操作本身的大部分工作。

  • 程序员可能很容易通过省略所需的检查而出错。

  • 在你执行检查的时刻和稍后(即使只有一小部分时间!)尝试操作的时刻之间,情况可能会发生变化。

Python 中首选的习惯用法是在 try 子句中尝试操作,并在一个或多个 except 子句中处理可能引发的异常。这种习惯用法称为“宁愿请求原谅,也不要事先征求许可”(EAFP),这是一个经常引用的格言,被广泛认为是 COBOL 的共同发明者之一的 Rear Admiral Grace Murray Hopper 所创。EAFP 不具有 LBYL 的任何缺陷。以下是一个使用 LBYL 惯用法的函数:

`def` safe_divide_1(x, y):
    `if` y==0:
        print('Divide-by-0 attempt detected')
 `return` `None`
    `else`:
        `return` x/y

使用 LBYL,检查首先进行,主流情况在函数末尾有点隐藏。

这是使用 EAFP 惯用法的等效函数:

`def` safe_divide_2(x, y):
    `try`:
        `return` x/y
    `except` ZeroDivisionError:
        print('Divide-by-0 attempt detected')
 `return` `None`

使用 EAFP,主流情况在 try 子句中前置,异常情况在接下来的 except 子句中处理,使整个函数更易于阅读和理解。

EAFP 是一种很好的错误处理策略,但它并非包治百病。特别是,您必须小心不要铺得太宽,捕捉到您没有预期到的错误,因此也没有打算捕捉到的错误。以下是这种风险的典型案例(我们在表 8-2 中介绍了内置函数 getattr):

`def` trycalling(obj, attrib, default, *args, **kwds):
    `try`:
        `return` getattr(obj, attrib)(*args, **kwds)
    `except` AttributeError:
        `return` default

函数 trycalling 的意图是尝试在对象 obj 上调用名为 attrib 的方法,但如果 obj 没有这样的方法,则返回 default。然而,所编写的函数并不仅仅如此:它还意外隐藏了在所寻找的方法内部引发 AttributeError 的任何错误情况,默默地在这些情况下返回 default。这可能会轻易隐藏其他代码中的 bug。要完全达到预期效果,函数必须多加小心:

`def` trycalling(obj, attrib, default, *args, **kwds):
    `try`:
        method = getattr(obj, attrib)
    `except` AttributeError:
        `return` default
    `else`:
        `return` method(*args, **kwds)

这个 trycalling 的实现将 getattr 调用与方法调用分开,getattr 调用放置在 try 子句中,因此受到 except 子句的保护,而方法调用放置在 else 子句中,因此可以自由传播任何异常。正确的 EAFP 方法涉及在 try/except 语句中频繁使用 else 子句(这比在整个 try/except 语句后放置非受保护代码更加明确,因此更符合 Python 风格)。

处理大型程序中的错误

在大型程序中,特别容易犯错的地方是将你的try/except语句设计得过于宽泛,特别是一旦你深信 EAFP 作为一种通用的错误检查策略的强大之后。当一个try/except组合捕获了太多不同的错误,或者在太多不同的位置可能发生错误时,这种组合就显得过于宽泛了。当你需要准确区分出错原因及位置,并且回溯信息不足以精确定位这些细节(或者你丢弃了回溯信息中的一些或全部信息)时,后者会成为问题。为了有效地处理错误,你必须清楚地区分你预期的(因此知道如何处理)和意外的错误和异常,后者可能表明你程序中存在漏洞。

有些错误和异常并非真正错误,也许甚至不算太反常:它们只是特殊的“边缘”情况,也许比较罕见,但仍然完全可以预料到,你选择通过 EAFP 处理而不是通过 LBYL 来避免 LBYL 的许多内在缺陷。在这种情况下,你应该只是处理这种异常,通常甚至不需要记录或报告它。

保持你的 try/except 结构尽可能狭窄

非常小心地保持try/except结构尽可能狭窄。使用一个小的try子句,其中包含少量不调用太多其他函数的代码,并在except子句中使用非常具体的异常类元组。如果需要,进一步分析异常的详细信息在你的处理代码中,当你知道这个处理程序无法处理的情况时,尽快raise出来。

依赖用户输入或其他不受你控制的外部条件导致的错误和异常总是可以预期的,这正是因为你无法控制它们的根本原因。在这种情况下,你应该集中精力优雅地处理这些异常,记录和记录其确切的性质和细节,并保持你程序的内部状态和持久状态不受损害。尽管当你使用 EAFP 来处理并非真正错误的特殊/边缘情况时,这并不像那么关键,但你的try/except子句仍应该相对狭窄。

最后,完全意想不到的错误和异常表明你程序的设计或编码存在漏洞。在大多数情况下,处理这类错误的最佳策略是避免使用try/except,直接让程序带着错误和回溯信息终止运行。(你可能想要在 sys.excepthook 的应用特定钩子中记录这些信息和/或更适当地显示,我们稍后会讨论这个。)在极少数情况下,即使在极端情况下你的程序必须继续运行,也可能适合使用相当宽泛的try/except语句,其中try子句保护调用涵盖大片程序功能的函数,并且广泛的except子句。

在长时间运行的程序中,务必记录异常或错误的所有细节到某个持久化存储位置,以便日后研究(同时也向自己报告问题的某些指示,这样您就知道需要进行这样的后续研究)。关键在于确保您能将程序的持久状态还原到某个未受损、内部一致的状态点。使长时间运行的程序能够克服其自身的某些缺陷以及环境逆境的技术被称为检查点技术(基本上是周期性保存程序状态,并编写程序以便重新加载保存的状态并从那里继续)和事务处理;在本书中我们不进一步介绍它们。

记录错误

当 Python 将异常传播到堆栈的顶部而没有找到适用的处理程序时,解释器通常会在终止程序之前将错误回溯打印到进程的标准错误流(sys.stderr)。您可以重新绑定 sys.stderr 到任何可用于输出的类似文件的对象,以便将此信息重定向到更适合您目的的位置。

当您希望在这些情况下更改输出的信息数量和类型时,重新绑定 sys.stderr 是不够的。在这种情况下,您可以将自己的函数分配给 sys.excepthook:当由于未处理的异常而终止程序时,Python 会调用它。在您的异常报告函数中,输出任何有助于诊断和调试问题的信息,并将该信息定向到任何您希望的位置。例如,您可以使用 traceback 模块(在“traceback 模块”中介绍)格式化堆栈跟踪。当您的异常报告函数终止时,程序也会终止。

日志模块

Python 标准库提供了丰富而强大的日志记录模块,让您以系统化、灵活的方式组织应用程序的日志记录消息。在极限情况下,您可以编写一整套 Logger 类和其子类;您可以将记录器与 Handler 类(及其子类)的实例或者插入 Filter 类的实例结合起来,以微调决定哪些消息以何种方式记录的标准。

消息由 Formatter 类的实例格式化——消息本身是 LogRecord 类的实例。日志模块甚至包括动态配置功能,通过该功能,您可以通过从磁盘文件读取或者通过专用线程中的专用套接字接收它们,动态设置日志配置文件。

虽然日志模块拥有一个复杂且强大的架构,适用于实现可能在庞大和复杂的软件系统中需要的高度复杂的日志策略和策略,但在大多数应用程序中,您可能只需使用该包的微小子集。首先,导入 logging。然后,通过将其作为字符串传递给模块的任何函数 debug、info、warning、error 或 critical,按严重性递增地发出您的消息。如果您传递的字符串包含诸如 %s(如 “使用 % 进行遗留字符串格式化” 中所述)的格式说明符,则在字符串之后,传递所有要在该字符串中格式化的值作为进一步的参数。例如,不要调用:

logging.debug('foo is %r' % foo)

这会执行格式化操作,无论是否需要;相反,调用:

logging.debug('foo is %r', foo)

这会执行格式化,仅在需要时执行(即仅当调用 debug 会导致日志输出时,取决于当前的阈值日志级别)。如果 foo 仅用于日志记录,并且创建 foo 特别耗费计算或 I/O,您可以使用 isEnabledFor 来有条件地执行创建 foo 的昂贵代码:

`if` logging.getLogger().isEnabledFor(logging.DEBUG):
    foo = cpu_intensive_function()
    logging.debug('foo is %r', foo)

配置日志记录

不幸的是,日志模块不支持在 “字符串格式化” 中涵盖的更易读的格式化方法,而仅支持前面子节中提到的遗留方法。幸运的是,很少需要超出 %s(调用 str)和 %r(调用 repr)之外的任何格式化说明符。

默认情况下,阈值级别为 WARNING:任何 warning、error 或 critical 函数都会产生日志输出,但 debug 和 info 函数不会。要随时更改阈值级别,请调用 logging.getLogger().setLevel,并将 logging 模块提供的相应常量之一作为唯一参数传递:DEBUG、INFO、WARNING、ERROR 或 CRITICAL。例如,一旦调用:

logging.getLogger().setLevel(logging.DEBUG)

所有从 debug 到 critical 的所有日志函数都会产生日志输出,直到再次更改级别。如果稍后调用:

logging.getLogger().setLevel(logging.ERROR)

那么只有 error 和 critical 函数会产生日志输出(debug、info 和 warning 不会产生日志输出);这个条件也会持续,直到再次更改级别,依此类推。

默认情况下,日志输出到进程的标准错误流(sys.stderr,如在表 8-3 中所述),并使用相对简单的格式(例如,每行输出不包括时间戳)。您可以通过实例化适当的处理程序实例、合适的格式化程序实例,并创建和设置一个新的记录器实例来控制这些设置。在简单且常见的情况下,您只需设置这些日志参数一次,然后它们在程序运行期间将保持不变,最简单的方法是通过命名参数调用 logging.basicConfig 函数。只有对 logging.basicConfig 的第一次调用才会产生任何效果,并且只有在调用任何日志函数(例如 debug、info 等)之前调用它才会产生效果。因此,最常见的用法是在程序的最开始调用 logging.basicConfig。例如,在程序的开始处常见的习惯用法如下:

`import` logging
logging.basicConfig(
    format='%(asctime)s %(levelname)8s %(message)s',
    filename='/tmp/logfile.txt', filemode='w')

此设置将日志消息写入文件,并以精确的人类可读时间戳进行格式化,后跟右对齐的八字符字段,然后是适当的消息内容。

要获取有关日志模块及其所有功能的详细信息,请务必查阅 Python 的丰富在线文档

assert 语句

assert 语句允许您在程序中引入“健全性检查”。assert 是一个简单语句,其语法如下:

`assert` *`condition`*[, *`expression`*]

运行 Python 时,如果使用优化标志(-O,如在“命令行语法和选项”中介绍的),assert 是一个空操作:编译器不会为其生成任何代码。否则,assert 会评估 condition。当 condition 满足时,assert 什么也不做。当 condition 不满足时,assert 会实例化 AssertionError,并将 expression 作为参数(如果没有 expression,则不带参数),然后引发该实例³。

assert 语句可以是记录程序的有效方法。当您想要声明在程序执行的某一点上已知存在一个重要且不明显的条件 C(称为程序的 不变量)时,assert C 通常比仅仅声明 C 成立的注释更好。

不要过度使用 assert。

除了用于健全性检查程序不变性之外,永远不要将 assert 用于其他目的。一个严重但非常常见的错误是在输入或参数的值上使用 assert。检查错误的参数或输入最好更加明确,特别是不能使用 assert,因为它可以通过 Python 命令行标志变成空操作。

assert 的优势在于,当 C 实际上 成立时,assert 会立即通过引发 AssertionError 警示问题,如果程序在没有 -O 标志的情况下运行。一旦代码彻底调试完成,使用 -O 运行它,将 assert 转换为一个空操作且不会产生任何开销(assert 保留在源代码中以记录不变量)。

debug 内置变量

在没有 -O 选项运行 Python 时,debug 内置变量为 True。在使用 -O 选项运行 Python 时,debugFalse。此外,在后一种情况下,编译器对唯一保护条件为 debug 的任何 if 语句不生成代码。

为了利用这一优化,用 if debug: 包围仅在 assert 语句中调用的函数的定义。这种技术使得在使用 -O 运行 Python 时,编译代码更小更快,并通过显示这些函数仅用于执行健全性检查来增强程序的清晰度。

¹ 除了允许多次调用 close 并且无害外:除第一个外的所有调用均不执行操作。

² 这在某种程度上是有争议的:虽然本书的作者认为这是“最佳实践”,但一些人坚决主张应始终避免多重继承,包括在这个特定情况下。

³ 一些第三方框架,如 pytest,实质上提高了 assert 语句的实用性。

第七章:模块和包

典型的 Python 程序由多个源文件组成。每个源文件都是一个模块,用于重复使用代码和数据。模块通常彼此独立,以便其他程序可以重复使用它们需要的特定模块。有时,为了管理复杂性,开发人员将相关模块组合成一个——这是一个相关模块和子包的层次化树状结构。

一个模块通过使用importfrom语句显式地建立对其他模块的依赖关系。在某些编程语言中,全局变量提供了模块之间的隐藏通道。在 Python 中,全局变量不是所有模块的全局变量,而是单个模块对象的属性。因此,Python 模块始终以显式和可维护的方式通信,通过显式地澄清它们之间的耦合关系。

Python 还支持扩展模块——用其他语言如 C、C++、Java、C#或 Rust 编写的模块。对于导入模块的 Python 代码来说,无论模块是纯 Python 还是扩展模块,都无关紧要。你始终可以先用 Python 编写一个模块。如果以后需要更快的速度,可以重构和重写模块的某些部分为更低级别的语言,而不需要改变使用该模块的客户端代码。第二十五章(在线版本链接)展示了如何用 C 和 Cython 编写扩展。

本章讨论了模块的创建和加载。还涵盖了如何将模块分组成包、使用setuptools来安装包以及如何准备包用于分发;后者更详细地在第二十四章(在线版本链接)中介绍。我们在本章结尾讨论了如何最好地管理你的 Python 环境。

模块对象

在 Python 中,模块是一个具有任意命名属性的对象,你可以绑定和引用这些属性。Python 中的模块与其他对象一样处理。因此,你可以将一个模块作为参数传递给函数调用。类似地,函数可以返回一个模块作为调用的结果。一个模块,就像任何其他对象一样,可以绑定到一个变量、容器中的一个项目或对象的属性。模块可以是字典中的键或值,并且可以是集合的成员。例如,sys.modules 字典讨论在“模块加载”中,保存模块对象作为其值。Python 中模块能像其他值一样对待的事实,经常被表达为模块是一等对象。

导入语句

名为aname的模块的 Python 代码通常存储在名为aname.py的文件中,如“在文件系统中搜索模块”中所述。您可以通过在另一个 Python 源文件中执行import语句来使用任何 Python 源文件¹作为模块。import具有以下语法:

`import` *`modname`* [`as` *`varname`*][,...]

import关键字后面跟着一个或多个由逗号分隔的模块说明符。在最简单、最常见的情况下,模块说明符只是modname,一个标识符—Python 在import语句完成时将其绑定到模块对象的变量。在这种情况下,Python 会寻找相同名称的模块以满足import请求。例如,这个语句:

`import` mymodule

寻找名为 mymodule 的模块,并将当前作用域中的变量名 mymodule 绑定到模块对象。 modname也可以是由点(.)分隔的标识符序列,用于命名包中包含的模块,如“包”中所述。

as varname是模块说明符的一部分时,Python 会寻找名为modname的模块,并将模块对象绑定到变量varname。例如,这样:

`import` mymodule `as` alias

寻找名为 mymodule 的模块,并将模块对象绑定到当前作用域中的变量aliasvarname必须始终是一个简单的标识符。

模块体

模块的body是模块源文件中的语句序列。没有特殊的语法用于指示源文件是一个模块;如前所述,您可以使用任何有效的 Python 源文件作为模块。模块的 body 在程序的给定运行第一次导入它时立即执行。当 body 开始执行时,模块对象已经被创建,已经在 sys.modules 中绑定到模块对象。模块的(全局)命名空间随着模块的 body 执行而逐渐填充。

模块对象的属性

import语句创建一个新的命名空间,其中包含模块的所有属性。要访问此命名空间中的属性,请使用模块的名称或别名作为前缀:

`import` mymodule
a = mymodule.f()

或者:

`import` mymodule `as` alias
a = alias.f()

这样可以减少导入模块所需的时间,并确保只有使用该模块的应用程序才会承担创建模块的开销。

通常,是模块体中的语句绑定模块对象的属性。当模块体中的语句绑定(全局)变量时,绑定的是模块对象的属性。

模块体存在以绑定模块的属性

模块体的正常目的是创建模块的属性:def语句创建和绑定函数,class语句创建和绑定类,赋值语句可以绑定任何类型的属性。为了使代码清晰易懂,请注意在模块体的顶层逻辑级别之外不要做任何其他事情,除了绑定模块的属性。

在模块范围定义的 getattr 函数可以动态创建新的模块属性。这样做的一个可能原因是延迟定义创建时间较长的属性;在模块级别的 getattr 函数中定义它们将推迟属性的创建,直到它们实际被引用为止。例如,可以将以下代码添加到mymodule.py中,以推迟创建包含前一百万个质数的列表,这需要一些时间来计算:

`def` __getattr__(name):
    `if` name == 'first_million_primes':
        `def` generate_n_primes(n):
            *`# ... code to generate 'n' prime numbers ...`*

        `import` sys
        *`# Look up __name__ in sys.modules to get current module`*
        this_module = sys.modules[__name__]
        this_module.first_million_primes = generate_n_primes(1_000_000)
        `return` this_module.first_million_primes

    `raise` AttributeError(f'module {__name__!r}
	                  f' has no attribute {name!r}')

使用模块级别的 getattr 函数对导入mymodule.py的时间影响很小,只有那些实际使用 mymodule.first_million_primes 的应用程序会产生创建它的开销。

你也可以在代码主体外绑定模块属性(即在其他模块中);只需将值分配给属性引用语法M.name(其中M是任何表达式,其值为模块,name是属性名称)。然而,为了清晰起见,最好只在模块自身的主体中绑定模块属性。

import语句在创建模块对象后立即绑定一些模块属性,而不是在模块主体执行之前。dict 属性是模块用作其属性命名空间的字典对象。与模块的其他属性不同,dict 在模块中不作为全局变量提供给代码使用。模块中的所有其他属性都是 dict 中的项,并且可以作为全局变量供模块中的代码使用。name 属性是模块的名称,file 是加载模块的文件名;其他 dunder 名称属性包含其他模块元数据。(另请参阅“Package Objects 的特殊属性”了解属性 path 的详细信息,仅适用于包)。

对于任何模块对象M,任何对象x,以及任何标识符字符串S(除了 dict),绑定M.S = x等同于绑定M.dict['S'] = x。像M.S这样的属性引用也基本等同于M.dict['S']。唯一的区别在于,当S不是M.dict 的键时,访问M.dict['S']会引发 KeyError,而访问M.S会引发 AttributeError。模块属性也可在模块主体中的所有代码中作为全局变量使用。换句话说,在模块主体中,作为全局变量使用的S等同于M.S(即M.dict['S']),无论是绑定还是引用(然而,当S不是M.dict 的键时,将S作为全局变量引用会引发 NameError)。

Python 内置函数

Python 提供了许多内置对象(在 第八章 中介绍)。所有内置对象都是预加载模块 builtins 的属性。当 Python 加载一个模块时,该模块会自动获得一个额外的属性 builtins,它指向模块 builtins 或其字典。Python 可能选择其中之一,因此不要依赖于 builtins。如果需要直接访问模块 builtins(这种情况很少见),请使用 import builtins 语句。当您访问当前模块的本地命名空间和全局命名空间中都找不到的变量时,Python 会在当前模块的 builtins 中查找该标识符,然后引发 NameError。

查找是 Python 唯一用来让您的代码访问内置的机制。您自己的代码可以直接使用访问机制(但请适度使用,否则会影响程序的清晰度和简洁性)。内置的名称不是保留的,也不是硬编码在 Python 本身中的——您可以添加自己的内置或替换正常的内置函数,在这种情况下,所有模块都会看到添加或替换的内置函数。由于 Python 仅在无法解析本地或模块命名空间中的名称时才访问内置,因此通常在其中一个命名空间中定义替代项就足够了。以下简单示例展示了如何使用您自己的函数包装内置函数,使 abs 函数接受一个字符串参数(并返回一个相当任意的字符串变形):

*`# abs takes a numeric argument; let's make it accept a string as well`*
`import` builtins
_abs = builtins.abs                       *`# save original built-in`*
`def` abs(str_or_num):
    `if` isinstance(str_or_num, str):       *`# if arg is a string`*
        `return` ''.join(sorted(set(str_or_num)))  *`# get this instead`*
    `return` _abs(str_or_num)               *`# call real built-in`*
builtins.abs = abs                        *`# override built-in w/wrapper`*

模块文档字符串

如果模块体中的第一条语句是一个字符串文字,Python 将该字符串绑定为模块的文档字符串属性,命名为 doc。有关文档字符串的更多信息,请参见 “文档字符串”。

模块私有变量

模块中没有真正私有的变量。然而,按照约定,每个以单个下划线(_)开头的标识符,例如 _secret,意味着是私有的。换句话说,前导下划线告诉客户端代码的程序员不应直接访问该标识符。

开发环境和其他工具依赖于以下划线开头的命名惯例来区分模块中哪些属性是公共的(即模块的接口的一部分),哪些是私有的(即仅在模块内部使用)。

遵守“前导下划线表示私有”惯例

在编写使用他人编写的模块的客户端代码时,尊重前导下划线表示私有的约定非常重要。避免在这些模块中使用任何以 _ 开头的属性。未来版本的模块将致力于保持其公共接口,但很可能会更改私有实现细节:私有属性正是为此类细节而设的。

from 语句

Python 的 from 语句允许您将模块中的特定属性导入到当前命名空间中。 from 有两种语法变体:

`from` *`modname`* `import` *`attrname`* [`as` *`varname`*][,...]
`from` *`modname`* `import` *

一个 from 语句指定一个模块名,后跟一个或多个用逗号分隔的属性说明符。在最简单和最常见的情况下,属性说明符只是一个标识符 attrname,它是 Python 绑定到模块 modname 中同名属性的变量。例如:

`from` mymodule `import` f

modname 也可以是由点(.)分隔的标识符序列,用于指定包内的模块,如“包”中所述。

as varname 是属性说明符的一部分时,Python 从模块获取属性 attrname 的值,并将其绑定到变量 varname。例如:

`from` mymodule `import` f `as` foo

attrnamevarname 始终是简单标识符。

您可以选择在 from 语句中关键字 import 后面的所有属性说明符周围加上括号。当您有许多属性说明符时,这样做可以更优雅地将 from 语句的单个逻辑行分成多个逻辑行,而不是使用反斜杠 ():

`from` some_module_with_a_long_name `import` (
    another_name, and_another `as` x, one_more, and_yet_another `as` y)

from...import *

直接位于模块体(而不是函数或类体)中的代码可以在 from 语句中使用星号 (*):

`from` mymodule `import` *
  • 请求将模块 modname 的“所有”属性作为全局变量绑定到导入模块中。当模块 modname 具有名为 all 的属性时,该属性的值是此类 from 语句绑定的属性名称列表。否则,此类 from 语句会绑定 modname 的所有属性,但排除以下划线开头的属性。

谨慎使用“from M import *”在您的代码中

由于 from M import * 可以绑定任意一组全局变量,它可能会产生意想不到的、不希望的副作用,比如隐藏内建变量并重新绑定你仍然需要的变量。几乎不要或者根本不要使用 from 的 * 形式,并且仅在明确文档支持这种用法的模块中使用。你的代码最好完全避免使用这种形式,这种形式主要是为了便于在交互式 Python 会话中偶尔使用。

from 与 import

import 语句通常比 from 语句更好。当你总是使用 import M 来访问模块 M,并且总是使用显式语法 M.A 来访问 M 的属性时,你的代码可能会略显冗长,但更加清晰和可读。from 的一个好用法是从包中导入特定的模块,正如我们在“包”中所讨论的。在大多数其他情况下,importfrom 更好。

处理导入失败

如果您正在导入不属于标准 Python 的模块,并希望处理导入失败,可以通过捕获 ImportError 异常来实现。例如,如果您的代码使用第三方的 rich 模块进行可选的输出格式化,但如果该模块未安装,则回退到常规输出,您可以这样导入模块:

try:
 `import` rich
`except` ImportError`:`
 rich = `None`

然后,在程序的输出部分,您可以写:

`if` rich `is` `not` `None`:
	... output using rich module features ...
`else`:
	... output using normal print() statements ...

模块加载

模块加载操作依赖于内置 sys 模块的属性(在 “The sys Module” 中有介绍),并且是通过内置函数 import 实现的。您的代码可以直接调用 import,但这在现代 Python 中被强烈不推荐;相反,最好使用 import importlib 并调用 importlib.import_module,其参数为模块名称字符串。import_module 返回模块对象或者,如果导入失败,则引发 ImportError。然而,最好对 import 的语义有清楚的理解,因为 import_module 和 import 语句都依赖于它。

要导入名为 M 的模块,import 首先检查字典 sys.modules,使用字符串 M 作为键。当键 M 在字典中时,import 返回相应值作为请求的模块对象。否则,import 将 sys.modules[M] 绑定到一个具有名称 M 的新空模块对象,然后查找正确的初始化(加载)模块的方法,如下一节中关于搜索文件系统的部分所述。

由于这种机制,相对较慢的加载操作仅在程序的给定运行中第一次导入模块时发生。当再次导入模块时,由于 import 快速找到并返回模块在 sys.modules 中的条目,因此模块不会重新加载。因此,在第一次导入后的所有给定模块的导入都非常快速:它们只是字典查找。(要 强制 重新加载,请参见 “Reloading Modules”。)

内置模块

当加载一个模块时,import 首先检查该模块是否为内置模块。元组 sys.builtin_module_names 列出所有内置模块的名称,但重新绑定该元组不会影响模块加载。当加载内置模块时,就像加载任何其他扩展一样,Python 调用模块的初始化函数。搜索内置模块还会在特定于平台的位置(例如 Windows 的注册表)中查找模块。

搜索文件系统中的模块

如果模块M不是内置模块,import 将其代码作为文件在文件系统上查找。import 按顺序查看 sys.path 列表的项,这些项是字符串。每个项是目录的路径,或者是流行的ZIP 格式中的存档文件的路径。sys.path 在程序启动时使用环境变量 PYTHONPATH 进行初始化(在“Environment Variables”中有介绍),如果存在。sys.path 中的第一个项始终是加载主程序的目录。sys.path 中的空字符串表示当前目录。

您的代码可以更改或重新绑定 sys.path,这些更改会影响 import 搜索以加载模块的目录和 ZIP 存档。更改 sys.path 不会影响已加载的模块(因此已在 sys.modules 中记录的模块)。

如果在启动时 PYTHONHOME 目录中存在带有*.pth扩展名的文本文件,Python 将文件内容逐行添加到 sys.path 中。.pth文件可以包含空行和以字符#开头的注释行;Python 会忽略这些行。.pth*文件还可以包含import语句(Python 在程序开始执行之前执行),但不能包含其他类型的语句。

在每个目录和 sys.path 沿途的 ZIP 存档中查找模块M的文件时,Python 按以下顺序考虑这些扩展名:

  1. .pyd和*.dll*(Windows)或*.so*(大多数类 Unix 平台),指示 Python 扩展模块。(某些 Unix 方言使用不同的扩展名;例如 HP-UX 上的*.sl*。)在大多数平台上,无法从 ZIP 存档中加载扩展—只能从源或字节码编译的 Python 模块中加载。

  2. .py,指示 Python 源模块。

  3. .pyc,指示字节码编译的 Python 模块。

  4. 当发现*.py文件时,Python 还会查找名为pycache的目录。如果找到这样的目录,Python 会在该目录中寻找扩展为..pyc的文件,其中*是查找模块的 Python 版本特定的字符串。

Python 寻找模块M文件的最后一个路径是M**/**init.py:一个名为M的目录中名为*init.py*的文件,如“Packages”中所述。

找到源文件M.py后,Python 会将其编译成M..pyc,除非字节码文件已经存在且比M.py新,并且是由相同版本的 Python 编译的。如果M.py是从可写目录编译的,Python 会在需要时创建一个*pycache*子目录,并将字节码文件保存到该子目录中,以便将来的运行不会不必要地重新编译它。当字节码文件新于源文件(基于字节码文件内部的时间戳,而不是文件系统中记录的日期)时,Python 不会重新编译模块。

一旦 Python 获得了字节码,无论是通过重新编译还是从文件系统中读取,Python 都会执行模块主体来初始化模块对象。如果模块是一个扩展,Python 将调用模块的初始化函数。

谨慎命名项目的 .py 文件

对初学者来说,一个常见的问题是,在编写他们的前几个项目时,他们会意外地将一个 .py 文件命名为已导入包或标准库(stdlib)中的同名模块。例如,学习 turtle 模块时很容易犯的一个错误是将程序命名为 turtle.py。然后当 Python 尝试从 stdlib 导入 turtle 模块时,它将加载本地模块而不是 stdlib 模块,并且通常会在此后不久引发一些意外的 AttributeErrors(因为本地模块不包含 stdlib 模块中定义的所有类、函数和变量)。不要将项目的 .py 文件命名为已导入或 stdlib 模块的同名文件!

你可以使用形如 python -m testname 的命令来检查模块名是否已存在。如果显示消息 'no module testname',那么你可以安全地将模块命名为 testname.py

一般来说,当你熟悉 stdlib 中的模块和常见包名时,你会知道要避免使用哪些名称。

主程序

Python 应用程序的执行始于顶级脚本(称为主程序),如 “python 程序”中所解释的。主程序像加载任何其他模块一样执行,只是 Python 将字节码保存在内存中,而不是保存到磁盘上。主程序的模块名称是 'main',既是 name 变量(模块属性),也是 sys.modules 中的键。

不要将正在使用的 .py 文件作为主程序导入

不要导入与主程序相同的 .py 文件。如果这样做,Python 将重新加载模块,并在一个不同的模块对象中执行主体,其 name 也不同。

Python 模块中的代码可以通过检查全局变量 name 是否具有值 'main' 来测试模块是否被用作主程序的方式。惯用法是:

`if` __name__ == '__main__':

经常用来保护某些代码,以便仅在模块作为主程序运行时执行。如果一个模块仅用于导入,通常在作为主程序运行时执行单元测试,详见 “单元测试和系统测试”。

重新加载模块

Python 在程序运行期间仅加载模块一次。在交互式开发中,编辑模块后需要 重新加载 模块(一些开发环境提供自动重新加载功能)。

要重新加载模块,请将模块对象(而不是模块名称)作为 importlib 模块的 reload 函数的唯一参数传递。importlib.reload(M) 确保客户端代码使用重新加载的 M 版本,该代码依赖于 import M 并使用 M.A 语法访问属性。但是,importlib.reload(M) 对绑定到 M 属性先前值的其他现有引用(例如,使用 from 语句)没有影响。换句话说,已绑定的变量保持原样,不受重新加载的影响。reload 无法重新绑定这些变量是使用 import 而不是 from 的进一步动机。

reload 不是递归的:当重新加载模块 M 时,这并不意味着通过 M 导入的其他模块也会被重新加载。您必须通过显式调用 reload 来重新加载您修改过的每个模块。一定要考虑任何模块引用依赖关系,以便按正确的顺序进行重新加载。

循环导入

Python 允许您指定循环导入。例如,可以编写一个包含 import b 的模块 a.py,而模块 b.py 包含 import a。

如果因某种原因决定使用循环导入,则需要了解循环导入的工作原理,以避免代码中的错误。

避免循环导入

实际上,几乎总是更好地避免循环导入,因为循环依赖是脆弱且难以管理的。

假设主脚本执行 import a。如前所述,此 import 语句在 sys.modules['a'] 中创建一个新的空模块对象,然后模块 a 的体开始执行。当 a 执行 import b 时,这将在 sys.modules['b'] 中创建一个新的空模块对象,然后模块 b 的体开始执行。直到 b 的模块体完成,a 的模块体才能继续执行。

现在,当 b 执行 import a 时,import 语句发现 sys.modules['a'] 已经绑定,因此将模块 b 中的全局变量 a 绑定到模块 a 的模块对象。由于当前阻塞了 a 的模块体执行,模块 a 此时通常只部分填充。如果 b 的模块体中的代码尝试访问尚未绑定的模块 a 的某个属性,将导致错误。

如果保留循环导入,必须仔细管理每个模块绑定其自己全局变量的顺序,导入其他模块并访问其他模块的全局变量。通过将语句分组到函数中,并按照受控顺序调用这些函数,而不是仅仅依赖模块体顶层语句的顺序执行,可以更好地控制发生的顺序。通过移动导入远离模块范围并进入引用函数,而不是确保用于处理循环依赖的防爆顺序,更容易去除循环依赖。

sys.modules 条目

import 从不绑定除模块对象以外的任何值到 sys.modules 中。但是,如果 import 在 sys.modules 中找到一个已有的条目,则返回该值,无论它是什么类型。importfrom语句依赖于 import,因此它们也可以使用非模块对象。

自定义导入器

Python 提供的另一种高级且不经常需要的功能是能够更改某些或所有importfrom语句的语义。

重新绑定 import

您可以重新绑定内置模块的 import 属性到您自己的自定义导入器函数,例如使用在“Python 内置”中显示的通用内置包装技术。这样的重新绑定会影响所有在重新绑定之后执行的importfrom语句,因此可能会产生不希望的全局影响。通过重新绑定 import 构建的自定义导入器必须实现与内置 import 相同的接口和语义,特别是负责支持 sys.modules 的正确使用。

避免重新绑定内置的 import

虽然重新绑定 import 可能最初看起来是一种有吸引力的方法,但在大多数需要自定义导入器的情况下,通过import hooks(接下来讨论)实现它们会更好。

导入钩子

Python 提供了丰富的支持,可以选择性地更改导入行为的细节。自定义导入器是一种高级且不经常调用的技术,但某些应用可能需要它们,例如从 ZIP 文件以外的存档、数据库、网络服务器等导入代码。

对于这种高度高级的需求,最合适的方法是将importer factory可调用项记录为模块 sys 的 meta_path 和/或 path_hooks 属性中的项目,详细信息请参见PEP 451。这是 Python 如何连接标准库模块 zipimport 以允许无缝导入来自 ZIP 文件的模块的方式,如前所述。要实现对 sys.path_hooks 和相关属性的实质性使用,必须全面研究 PEP 451 的详细内容,但以下是一个玩具级别的示例,可帮助理解可能的用途,如果您曾经需要的话。

假设我们在开发某个程序的首个大纲时,希望能够使用尚未编写的模块的import语句,只会得到消息(以及空模块)作为结果。我们可以通过编写一个自定义导入器模块来实现这样的功能(暂且不考虑与包相关的复杂性,仅处理简单模块):

`import` sys, types
`class` ImporterAndLoader:
     *`"""importer and loader can be a single class"""`*
     fake_path = '!dummy!'
     `def` __init__(self, path):
         *`# only handle our own fake-path marker`*
         `if` path != self.fake_path:
             `raise` ImportError
     `def` find_module(self, fullname):
         *`# don't even try to handle any qualified module name`*
         `if` '.' `in` fullname:
             `return` `None`
         `return` self
     `def` create_module(self, spec):
         *`# returning None will have Python fall back and`*
 *`# create the module "the default way"`*
 `return` `None`
     `def` exec_module(self, mod):
         *`# populate the already initialized module`*
         *`# just print a message in this toy example`*
         print(f'NOTE: module {mod!r} not yet written')
sys.path_hooks.append(ImporterAndLoader)
sys.path.append(ImporterAndLoader.fake_path)
`if` __name__ == '__main__':      *`# self-test when run as main script`*
    `import` missing_module       *`# importing a simple *missing* module`*
    print(missing_module)       *`# ...should succeed`*
    print(sys.modules.get('missing_module'))  *`# ...should also succeed`*

我们刚刚编写了 create_module 的简单版本(在本例中仅返回 None,请求系统以“默认方式”创建模块对象)和 exec_module 的简单版本(接收已初始化带有 dunder 属性的模块对象,其任务通常是适当地填充它)。

作为替代方案,我们还可以使用强大的新模块规范概念,详见 PEP 451。然而,这需要标准库模块 importlib;对于这个玩具示例,我们不需要额外的功能。因此,我们选择实现 find_module 方法,虽然现在已经被弃用,但仍然可以用于向后兼容。

正如本章开头所提到的, 是包含其他模块的模块。包中的一些或所有模块可以是子包,形成一个分层的树形结构。包 P 通常位于 sys.path 中某个目录的名为 P 的子目录中。包也可以存在于 ZIP 文件中;在本节中,我们解释了包存在于文件系统中的情况,但包存在于 ZIP 文件中的情况类似,依赖于 ZIP 文件内部的分层文件系统结构。

P 的模块体在文件 P/init.py 中。此文件必须存在(除了在PEP 420中描述的命名空间包的情况下),即使它是空的(代表空的模块体),以便告诉 Python 目录 P 确实是一个包。当你首次导入包(或包的任何模块)时,Python 会加载包的模块体,就像加载任何其他 Python 模块一样。目录 P 中的其他 .py 文件是包 P 的模块。包含 init.py 文件的 P 的子目录是 P 的子包。嵌套可以无限进行。

在包 P 中,你可以将名为 M 的模块导入为 P.M*.*。更多的点号可以让你在层次化的包结构中导航。(一个包的模块体总是在任何包中的模块之前加载。)如果你使用 import P.M 的语法,变量 P 将绑定到包 P 的模块对象,并且对象 P 的属性 M 绑定到模块 P.M。如果你使用 import P.M as V 的语法,变量 V 直接绑定到模块 P.M

使用 from P import M 来从包 P 导入特定模块 M 是一种完全可以接受的、而且确实是非常推荐的做法:在这种情况下,from 语句是完全 OK 的。from P import M as V 也是可以的,与 import P.M as V 完全等效。你也可以使用相对路径:也就是说,包 P 中的模块 M 可以使用 from . import X 导入它的“兄弟”模块 X(也在包 P 中)。

在包中的模块之间共享对象

在包 P 中,最简单、最清晰的共享对象(例如函数或常量)的方法是将共享对象分组到一个传统上命名为 common.py 的模块中。这样,你可以在包中的每个模块中使用 from . import common 来访问一些共享对象,然后引用这些对象为 common.f、common.K 等。

包对象的特殊属性

Pfile 属性是一个字符串,表示 P 的模块主体的路径,即文件 P/init.py 的路径。Ppackage 属性是 P 的包名。

P 的模块主体 - 即文件 P/init.py 中的 Python 源代码 - 可以选择性地设置一个名为 all 的全局变量(就像任何其他模块一样),以控制如果其他 Python 代码执行语句 from P import * 会发生什么。特别是,如果未设置 allfrom P import * 不会导入 P 的模块,而只会导入在 P 的模块主体中设置的没有前导 _ 的名称。无论如何,这都推荐使用。

Ppath 属性是一个字符串列表,其中包含加载 P 的模块和子包的目录路径。最初,Python 将 path 设置为一个列表,其中仅有一个元素:包含文件 init.py 的目录的路径,该文件是包的模块主体。您的代码可以修改此列表,以影响对此包的模块和子包的未来搜索。这种高级技术很少必要,但在您想要将包的模块放置在多个目录中时可能会很有用(然而,命名空间包是实现此目标的通常方法)。

绝对导入与相对导入

如前所述,import 语句通常期望在 sys.path 的某处找到其目标,这种行为称为绝对导入。或者,您可以显式使用相对导入,意味着从当前包中导入对象。使用相对导入可以使您更轻松地重构或重新组织包中的子包。相对导入使用以一个或多个点开头的模块或包名称,并且仅在 from 语句中可用。from . import X 在当前包中查找名为 X 的模块或对象;from .X import y 在当前包的模块或子包 X 中查找名为 y 的模块或对象。如果您的包有子包,它们的代码可以通过在 fromimport 之间放置的模块或子包名称的起始处使用多个点来访问包中的更高级对象。每一个额外的点都会提升目录层次结构一级。对这个特性的过度使用可能会损害您代码的清晰度,因此请谨慎使用,只在必要时使用。

分发工具(distutils)和 setuptools

Python 模块、扩展和应用可以以多种形式打包和分发:

压缩归档文件

通常的 .zip.tar.gz(也称为 .tgz)、.tar.bz2.tar.xz 文件 - 所有这些形式都是可移植的,还有许多其他形式的文件和目录树的压缩归档存在

自解包或自安装的可执行文件

通常用于 Windows 的 .exe 文件

自包含、即时运行的可执行文件,无需安装

例如,对于 Windows 是*.exe*,在 Unix 上是带有短脚本前缀的 ZIP 存档文件,对于 Mac 是*.app*,等等

平台特定的安装程序

例如,在许多 Linux 发行版上是*.rpm.srpm*,在 Debian GNU/Linux 和 Ubuntu 上是*.deb*,在 macOS 上是*.pkg*。

Python Wheels

流行的第三方扩展,详见下文

Python Wheels

一个 Python wheel 是一个包含结构化元数据和 Python 代码的存档文件。Wheels 提供了一个出色的方式来打包和分发你的 Python 包,而且 setuptools(通过pip install wheel 轻松安装 wheel 扩展)与它们无缝配合。在PythonWheels.com和第二十四章(在线版本在此)了解更多信息。

当你将一个包作为一个自安装可执行文件或平台特定的安装程序进行分发时,用户只需运行安装程序。如何运行这样的程序取决于平台,但程序是用哪种语言编写的并不重要。我们在第二十四章中介绍了为各种平台构建自包含可运行可执行文件的方法。

当你将一个包作为一个存档文件或一个解压但不安装自身的可执行文件进行分发时,重要的是这个包是用 Python 编写的。在这种情况下,用户必须首先将存档文件解压到某个适当的目录中,比如在 Windows 机器上是 C:\Temp\MyPack,在类 Unix 机器上是 ~/MyPack。在提取的文件中应该有一个脚本,按照惯例命名为 setup.py,它使用 Python 设施称为 distribution utilities(现在已经被弃用,但仍然有效的标准库包 distutils^(2))或者更好的是,更流行、现代和强大的第三方包 setuptools。然后,分发的包几乎和自安装可执行文件一样容易安装;用户只需打开一个命令提示窗口,切换到解压存档文件的目录,然后运行,例如:

C:\Temp\MyPack> `python` `setup``.``py` `install`

(另一种常用的选择是使用 pip;我们将立即描述它。)运行此 install 命令的 setup.py 脚本会根据包的作者在设置脚本中指定的选项将包安装为用户的 Python 安装的一部分。当然,用户需要适当的权限才能写入 Python 安装目录的目录,因此可能也需要像 sudo 这样提高权限的命令;或者更好的做法是,您可以安装到虚拟环境中,如下一节所述。distutils 和 setuptools 在用户运行 setup.py 时,默认会打印一些信息。在 install 命令之前包含 --quiet 选项可以隐藏大部分详细信息(用户仍然可以看到错误消息,如果有的话)。以下命令可以详细了解 distutils 或 setuptools,具体取决于包作者在其 setup.py 中使用的工具集:

C:\Temp\MyPack> `python` `setup``.``py` `-``-``help`

这个过程的另一个选择,也是现在安装包的首选方式,是使用与 Python 随附的优秀安装程序 pip。pip——“pip installs packages”的递归缩写——在大多数情况下使用起来非常简单,但在线有大量文档支持。pip install package 会查找package的在线版本(通常在巨大的 PyPI 仓库中,在撰写本文时托管了超过 400,000 个包),下载并为您安装它(如果已激活虚拟环境,则在其中安装;有关详细信息,请参阅下一节)。这本书的作者已经使用这种简单而强大的方法安装了超过 90% 的安装已经相当长时间了。

即使您已经将包本地下载(比如到 /tmp/mypack),出于任何原因(也许它不在 PyPI 上,或者您正在尝试一个尚未发布的实验版本),pip 仍然可以为您安装它:只需运行 pip install --no-index --find-links=/tmp/mypack,pip 将完成其余操作。

Python 环境

典型的 Python 程序员同时在多个项目上工作,每个项目都有自己的依赖列表(通常是第三方库和数据文件)。当所有项目的依赖项都安装到同一个 Python 解释器时,很难确定哪些项目使用了哪些依赖项,并且无法处理使用某些依赖项冲突版本的项目。

早期的 Python 解释器是基于这样的假设构建的:每台计算机系统都安装有“一个 Python 解释器”,用于在该系统上运行任何 Python 程序。操作系统发行版很快开始将 Python 包含在它们的基础安装中,但由于 Python 一直在积极开发中,用户经常抱怨他们希望使用比其操作系统提供的更更新的语言版本。

出现了让系统安装多个版本的语言的技术,但第三方软件的安装仍然是非标准和具有侵入性的。通过引入site-packages目录作为添加到 Python 安装的模块的存储库来缓解这个问题,但仍然不可能使用相同的解释器维护具有冲突要求的多个项目。

习惯于命令行操作的程序员熟悉shell 环境的概念。在进程中运行的 shell 程序有一个当前目录,您可以使用 shell 命令设置变量(与 Python 命名空间非常相似),以及各种其他进程特定状态数据。Python 程序可以通过 os.environ 访问 shell 环境。

如“环境变量”中所述,shell 环境的各个方面都会影响 Python 的运行。例如,PATH 环境变量决定了哪个程序会对python和其他命令做出响应。你可以把影响 Python 运行的 shell 环境的各个方面称为你的Python 环境。通过修改它,你可以确定哪个 Python 解释器会对python命令做出响应,哪些包和模块在特定名称下可用,等等。

不要将系统的 Python 用于系统

我们建议控制你的 Python 环境。特别是,不要在系统分发的 Python 上构建应用程序。相反,独立安装另一个 Python 发行版,并调整你的 shell 环境,以便python命令运行你本地安装的 Python,而不是系统的 Python。

进入虚拟环境

pip 实用程序的引入为在 Python 环境中安装(并且首次卸载)包和模块提供了一种简单的方法。修改系统 Python 的site-packages仍然需要管理员权限,因此 pip 也需要(虽然它可以选择安装到site-packages之外的地方)。安装在中央site-packages中的模块对所有程序都可见。

缺失的部分是能够对 Python 环境进行受控更改,以指导使用特定的解释器和一组特定的 Python 库。这正是虚拟环境virtualenvs)所提供的功能。基于特定 Python 解释器创建的虚拟环境会复制或链接到该解释器安装的组件。关键是,每个虚拟环境都有自己的site-packages目录,你可以将你选择的 Python 资源安装到其中。

创建虚拟环境比安装 Python 简单得多,并且需要的系统资源远远少于后者(典型的新创建的虚拟环境占用不到 20 MB)。您可以随时轻松创建和激活虚拟环境,并且同样轻松地取消激活和销毁它们。在其生命周期内,您可以任意次数地激活和取消激活虚拟环境,并且必要时使用 pip 来更新已安装的资源。当您完成时,删除其目录树会回收虚拟环境占用的所有存储空间。虚拟环境的生命周期可以是几分钟或几个月。

什么是虚拟环境?

虚拟环境本质上是您的 Python 环境的一个自包含子集,您可以根据需要随时切换。对于 Python 3.x 解释器,它包括,除其他外,一个包含 Python 3.x 解释器的 bin 目录,以及一个包含预安装版本的 easy-install、pip、pkg_resources 和 setuptools 的 lib/python3.x/site-packages 目录。维护这些重要的分发相关资源的独立副本使您可以根据需要更新它们,而不是强迫您依赖基本 Python 发行版。

虚拟环境在(在 Windows 上)或符号链接到(在其他平台上)Python 发行文件上有其自己的副本。它调整了 sys.prefix 和 sys.exec_prefix 的值,从而解释器和各种安装工具确定了一些库的位置。这意味着 pip 可以将依赖项安装在与其他环境隔离的情况下,在虚拟环境的 site-packages 目录中。实际上,虚拟环境重新定义了运行 python 命令时的解释器以及可用于解释器的大多数库,但保留了 Python 环境的大多数方面(如 PYTHONPATH 和 PYTHONHOME 变量)。由于其更改会影响您的 shell 环境,因此它们也会影响您运行命令的任何子 shell。

使用单独的虚拟环境,您可以例如,测试项目中相同库的两个不同版本,或者使用多个 Python 版本测试您的项目。您还可以为您的 Python 项目添加依赖项,而无需任何特殊权限,因为您通常在您具有写权限的地方创建您的虚拟环境。

处理虚拟环境的现代方法是使用标准库的 venv 模块:只需运行 python -m venv envpath

创建和删除虚拟环境

命令 python -m venv envpath 创建一个基于运行该命令的 Python 解释器的虚拟环境(在必要时还会创建 envpath 目录)。您可以提供多个目录参数来创建多个虚拟环境(使用相同的 Python 解释器),然后可以在每个虚拟环境中安装不同的依赖项集。venv 可以接受多个选项,如表 7-1 所示。

表 7-1. venv 选项

选项 目的
--clear 在安装虚拟环境之前移除任何现有目录内容
--copies 在类 Unix 平台上使用复制方式安装文件,这是默认使用符号链接的系统
--h--help 打印出命令行摘要和可用选项列表
--symlinks 在平台上使用符号链接安装文件,这是默认复制的系统
--system-site-packages 将标准系统 site-packages 目录添加到环境的搜索路径中,使基础 Python 中已安装的模块在环境内可用
--upgrade 在虚拟环境中安装当前正在运行的 Python,替换最初创建环境的版本
--without-pip 阻止调用 ensurepip 来将 pip 安装器引导到环境中的常规行为

知道您正在使用哪个 Python

当您在命令行输入 python 命令时,您的 shell 有一些规则(在 Windows、Linux 和 macOS 中有所不同),决定您运行的程序。如果您清楚这些规则,您就始终知道您正在使用哪个解释器。

使用 python -m venv directory_path 命令创建虚拟环境可以保证它基于与创建时所用解释器相同的 Python 版本。类似地,使用 python -m pip package_name 将为与 python 命令关联的解释器安装包。激活虚拟环境会改变与 python 命令的关联:这是确保包安装到虚拟环境中的最简单方法。

下面的 Unix 终端会话显示了虚拟环境的创建及创建的目录结构。bin 子目录的列表显示了这个特定用户默认使用的解释器安装在 /usr/local/bin 中。³

$ python3 -m venv /tmp/tempenv
$ tree -dL 4 /tmp/tempenv
/tmp/tempenv
|--- bin
|--- include
|___ lib
     |___ python3.5
          |___ site-packages
               |--- __pycache__
               |--- pip
               |--- pip-8.1.1.dist-info
               |--- pkg_resources
               |--- setuptools
               |___ setuptools-20.10.1.dist-info

11 directories
$ ls -l /tmp/tempenv/bin/
total 80
-rw-r--r-- 1 sh wheel 2134 Oct 24 15:26 activate
-rw-r--r-- 1 sh wheel 1250 Oct 24 15:26 activate.csh
-rw-r--r-- 1 sh wheel 2388 Oct 24 15:26 activate.fish
-rwxr-xr-x 1 sh wheel  249 Oct 24 15:26 easy_install
-rwxr-xr-x 1 sh wheel  249 Oct 24 15:26 easy_install-3.5
-rwxr-xr-x 1 sh wheel  221 Oct 24 15:26 pip
-rwxr-xr-x 1 sh wheel  221 Oct 24 15:26 pip3
-rwxr-xr-x 1 sh wheel  221 Oct 24 15:26 pip3.5
lrwxr-xr-x 1 sh wheel    7 Oct 24 15:26 python->python3
lrwxr-xr-x 1 sh wheel   22 Oct 24 15:26 python3->/usr/local/bin/python3

删除虚拟环境就像删除它所在的目录一样简单(以及树中的所有子目录和文件:在类 Unix 系统中使用 rm -rf envpath)。易于删除是使用虚拟环境的一个有用方面。

venv 模块包括一些功能,帮助编程创建定制环境(例如,在环境中预安装某些模块或执行其他创建后步骤)。在线文档提供了详细说明 online;我们在本书中不再详细介绍 API。

使用虚拟环境

要使用虚拟环境,需要在正常的 shell 环境中 activate 它。一次只能激活一个虚拟环境,激活不像函数调用那样可以“堆叠”。激活告诉你的 Python 环境使用虚拟环境的 Python 解释器和 site-packages(以及解释器的完整标准库)。当你想要停止使用这些依赖时,取消激活虚拟环境,你的标准 Python 环境将再次可用。虚拟环境目录树继续存在,直到被删除,因此可以随意激活和取消激活。

在 Unix-based 环境中激活虚拟环境需要使用 source shell 命令,以便激活脚本中的命令可以修改当前的 shell 环境。简单运行脚本会导致其命令在子 shell 中执行,当子 shell 终止时,更改将会丢失。对于 bash、zsh 和类似的 shell,你可以使用以下命令激活位于路径 envpath 的环境:

$ source *envpath*/bin/activate

或者:

$ *. envpath*/bin/activate

其他 shell 的用户可以使用同一目录中的 activate.cshactivate.fish 脚本来获得支持。在 Windows 系统上,使用 activate.bat(或者如果使用 Powershell,则使用 Activate.ps1):

C:\> *envpath*/Scripts/activate.bat

激活会执行许多操作。最重要的是:

  • 将虚拟环境的 bin 目录添加到 shell 的 PATH 环境变量的开头,这样它的命令优先于已存在于 PATH 中同名的任何内容。

  • 定义了一个 deactivate 命令,用于取消激活的所有效果,并将 Python 环境恢复到其原始状态。

  • 修改 shell 提示符,在开头包含虚拟环境的名称。

  • 定义了一个 VIRTUAL_ENV 环境变量,指向虚拟环境的根目录(脚本可以使用此变量来检查虚拟环境)。

由于这些操作的结果,一旦激活了虚拟环境,python 命令将运行与该虚拟环境关联的解释器。解释器可以看到安装在该环境中的库(模块和包),而 pip——现在是虚拟环境中的 pip,因为安装模块也安装了命令到虚拟环境的 bin 目录——默认将新的包和模块安装到环境的 site-packages 目录中。

对于刚接触虚拟环境的人来说,需要理解虚拟环境与任何项目目录都无关。完全可以在同一个虚拟环境中处理多个项目,每个项目都有自己的源代码树。激活虚拟环境后,你可以根据需要在文件系统中移动,完成编程任务,使用相同的库(因为虚拟环境确定了 Python 环境)。

当你想要禁用虚拟环境并停止使用那组资源时,只需执行命令 deactivate。这将撤销激活时所做的更改,将虚拟环境的 bin 目录从你的 PATH 中移除,因此 python 命令将再次运行你通常的解释器。只要不删除它,虚拟环境将保持可用状态供将来使用:只需重复执行命令来激活它。

不要在 Windows 的虚拟环境中使用 py –3.x

Windows py 启动器对虚拟环境的支持提供了混合支持。它使得使用特定 Python 版本定义虚拟环境变得非常简单,例如使用以下命令:

C:\> `py` `-``3.7` `-``m` `venv` `C``:``\``path``\``to``\``new_virtualenv`

这将创建一个新的虚拟环境,运行已安装的 Python 3.7 版本。

一旦激活,你可以使用 python 命令或不指定版本的裸露 py 命令在虚拟环境中运行 Python 解释器。然而,如果你使用版本选项指定 py 命令,即使它是用来构建虚拟环境的相同版本,你也将不会运行 virtualenv Python。相反,你将运行相应的系统安装版本的 Python。

管理依赖要求

由于虚拟环境设计为与 pip 安装相辅相成,因此不足为奇,pip 是在虚拟环境中维护依赖项的首选方式。由于 pip 已经有了广泛的文档,我们在这里只提到足够展示它在虚拟环境中的优势。创建了虚拟环境、激活了它并安装了依赖项后,你可以使用 pip freeze 命令来了解这些依赖项的确切版本:

(tempenv) $ pip freeze
appnope==0.1.0
decorator==4.0.10
ipython==5.1.0
ipython-genutils==0.1.0
pexpect==4.2.1
pickleshare==0.7.4
prompt-toolkit==1.0.8
ptyprocess==0.5.1
Pygments==2.1.3
requests==2.11.1
simplegeneric==0.8.1
six==1.10.0
traitlets==4.3.1
wcwidth==0.1.7

如果你将此命令的输出重定向到一个名为 filename 的文件中,你可以使用命令 pip install -r filename 在另一个虚拟环境中重新创建相同的依赖项集。

在为他人使用的代码分发时,Python 开发人员通常包含一个列出必要依赖项的 requirements.txt 文件。当从 PyPI 安装软件时,pip 会安装任何指示的依赖项以及你请求的包。在开发软件时,拥有一个要求文件也很方便,因为你可以使用它向活跃的虚拟环境添加必要的依赖项(除非它们已经安装),只需简单地执行 pip install -r requirements.txt

要在多个虚拟环境中保持相同的依赖关系集,使用同一个要求文件向每个虚拟环境添加依赖项。这是一个方便的方法来开发可以在多个 Python 版本上运行的项目:为每个所需版本创建基于的虚拟环境,然后在每个环境中从同一个要求文件安装。虽然前面的示例使用了由 pip freeze 生成的精确版本化的依赖规范,但在实践中,你可以以非常复杂的方式指定依赖关系和版本要求;详细信息请参阅 文档

其他环境管理解决方案

Python 虚拟环境专注于提供一个隔离的 Python 解释器,你可以在其中为一个或多个 Python 应用程序安装依赖项。最初创建和管理 virtualenv 的方式是使用 virtualenv 包。它具有广泛的功能,包括从任何可用的 Python 解释器创建环境的能力。现在由 Python Packaging Authority 团队维护,其部分功能已提取为标准库 venv 模块,但如果需要更多控制,还是值得了解 virtualenv。

pipenv 包是另一个用于 Python 环境的依赖管理器。它维护虚拟环境,其内容记录在名为 Pipfile 的文件中。类似于类似的 JavaScript 工具,通过 Pipfile.lock 文件提供确定性环境,允许部署与原始安装完全相同的依赖项。

conda,在 “Anaconda and Miniconda” 中提到,具有更广泛的范围,可以为任何语言提供包、环境和依赖管理。conda 使用 Python 编写,在基础环境中安装自己的 Python 解释器。而标准的 Python virtualenv 通常使用创建时的 Python 解释器;在 conda 中,Python 本身(当其包含在环境中时)只是另一个依赖项。这使得在需要时更新环境中使用的 Python 版本成为可能。如果愿意,你也可以在基于 Python 的 conda 环境中使用 pip 安装软件包。conda 可以将环境内容转储为 YAML 文件,你可以使用该文件在其他地方复制环境。

因其额外的灵活性,加上由其创始者 Anaconda, Inc.(前身为 Continuum)主导的全面开源支持,conda 在学术环境中被广泛使用,特别是在数据科学与工程、人工智能和金融分析领域。它从所谓的 channels 安装软件。Anaconda 维护的默认 channel 包含各种软件包,第三方维护专门的 channel(如生物信息学软件的 bioconda channel)。还有一个基于社区的 conda-forge channel,欢迎任何希望加入并添加软件的人。在 Anaconda.org 注册账户可以让你创建自己的 channel 并通过 conda-forge channel 分发软件。

使用虚拟环境的最佳实践

关于如何最佳管理虚拟环境的建议非常少,但有几个 sound tutorials:任何一个好的搜索引擎都可以让你访问到最新的教程。不过,我们可以提供一些希望能帮助你充分利用虚拟环境的简单建议。

当您在多个 Python 版本中使用相同的依赖关系时,在环境名称中指示版本并使用共同的前缀是有用的。因此,对于项目mutex,您可以维护称为mutex_39mutex_310的环境,以在两个不同版本的 Python 下进行开发。当环境名称在您的 shell 提示符中显而易见时(记住,您可以看到环境名称),测试使用错误版本的风险较小。您可以使用共同的需求来维护依赖项,以控制在两者中的资源安装。

将需求文件保留在源代码控制下,而不是整个环境。有了需求文件,重新创建虚拟环境很容易,它仅依赖于 Python 版本和需求。您可以分发您的项目,并让用户决定在哪些 Python 版本上运行它并创建适当的虚拟环境。

将您的虚拟环境保持在项目目录之外。这样可以避免显式强制源代码控制系统忽略它们。它们存储在哪里真的无关紧要。

您的 Python 环境独立于文件系统中项目的位置。您可以激活一个虚拟环境,然后切换分支并在变更控制的源代码树中移动以在任何方便的地方使用它。

要调查一个新的模块或包,创建并激活一个新的虚拟环境,然后pip install您感兴趣的资源。您可以尽情地在这个新环境中玩耍,确信您不会将不需要的依赖项安装到其他项目中。

在虚拟环境中进行实验可能需要安装不是当前项目需求的资源。与其“污染”您的开发环境,不如分叉它:从相同的需求创建一个新的虚拟环境,再加上测试功能。稍后,为了使这些更改永久化,使用变更控制从分叉分支合并您的源代码和需求更改回来。

如果您愿意,您可以基于 Python 的调试版本创建虚拟环境,从而可以访问关于您的 Python 代码性能(当然还有解释器本身)的丰富信息。

开发虚拟环境还需要变更控制,而虚拟环境创建的便利性在这方面也起到了帮助作用。假设你最近发布了模块的 4.3 版本,并且你想要用其两个依赖项的新版本来测试你的代码。如果你足够有技巧,可以说服 pip 替换现有虚拟环境中的依赖项副本。不过,更简单的方法是使用源代码控制工具分支你的项目,更新依赖项,并基于更新后的需求创建一个全新的虚拟环境。原始虚拟环境保持不变,你可以在不同的虚拟环境之间切换,以调查可能出现的任何迁移问题的特定方面。一旦调整了代码,使所有测试都通过了更新的依赖项,你就可以提交你的代码和需求更改,并合并到版本 4.4 完成更新,通知同事你的代码已准备好使用更新的依赖版本。

虚拟环境并不能解决所有 Python 程序员的问题:工具总是可以变得更复杂,或者更通用。但是,天哪,虚拟环境确实有效,我们应该尽可能充分利用它们。

¹ 我们的一位技术审阅员报告说在 Windows 上的 .pyw 文件是一个例外。

² 在 Python 3.12 中,计划删除 distutils。

³ 在运行这些命令时,如果使用减少了占用空间的 Linux 发行版,可能需要单独安装 venv 或其他支持包。

第八章:核心内置和标准库模块

术语内置在 Python 中有多个含义。在许多上下文中,内置意味着一个对象可以直接在 Python 代码中访问,而无需import语句。“Python 内置对象”节展示了 Python 允许这种直接访问的机制。Python 中的内置类型包括数字、序列、字典、集合、函数(所有在第 3 章中讨论)、类(在“Python 类”中讨论)、标准异常类(在“异常对象”中讨论)和模块(在“模块对象”中讨论)。“io 模块”涵盖了文件类型,而“内部类型”涵盖了 Python 内部操作的一些其他内置类型。本章在开篇部分提供了内置核心类型的额外覆盖,并在“内置函数”中介绍了模块 builtins 中可用的内置函数。

一些模块被称为“内置”,因为它们位于 Python 标准库中(尽管需要import语句才能使用),而不是附加模块,也称为 Python 扩展

本章涵盖了几个内置核心模块:即标准库模块 sys、copy、collections、functools、heapq、argparse 和 itertools。您将在各自部分“x 模块”中找到对每个模块 x 的讨论。

第 9 章涵盖了一些与字符串相关的内置核心模块(string、codecs 和 unicodedata),采用相同的部分名称约定。第 10 章介绍了 re 模块在“正则表达式和 re 模块”中的使用。

内置类型

表 8-1 提供了 Python 核心内置类型的简要概述。关于这些类型的许多细节以及关于其实例操作的详细信息可以在整个第 3 章中找到。在本节中,“任意数字”特指“任意非复数数字”。此外,许多内置函数至少以某些参数的位置方式接受参数;我们使用 3.8+ 的位置限定符 /,在“位置限定符”中进行了介绍。

表 8-1. Python 核心内置类型

bool bool(x=False, /) 当 x 评估为假时返回False;当 x 评估为真时返回True(参见“布尔值”)。bool 扩展自 int:内置名称FalseTrue分别指 bool 的唯一两个实例。这些实例也是等于 0 和 1 的 int,但 str(True) 是 'True',str(False) 是 'False'。
bytearray bytearray(x=b'', /[, codec[, errors]]) 返回一个可变的字节序列(值为 0 到 255 的 int),支持可变序列的通常方法,以及 str 的方法。当x是一个 str 时,您还必须传递 codec 并可能传递 errors;结果就像调用 bytearray(x.encode(codec, errors))一样。当x是一个 int 时,它必须>=0:生成的实例的长度为x,并且每个项目都初始化为 0。当x符合缓冲区协议时,从x读取的只读字节缓冲区初始化实例。否则,x必须是一个产生 int >=0 且<256 的可迭代对象;例如,bytearray([1,2,3,4]) == bytearray(b'\x01\x02\x03\x04')。
bytes bytes(x=b'', /[, codec[, errors]]) 返回一个不可变的字节序列,具有与 bytearray 相同的非变异方法和相同的初始化行为。
complex complex(real=0, imag=0) 将任何数字或适当的字符串转换为复数。当real是一个数字时,imag可以存在,并且在这种情况下imag也是一个数字:生成的复数的虚部。另请参阅“复数”。

| dict | dict(x=, /) 返回一个具有与x相同项目的新字典。(我们在“字典”中介绍字典。)当x是一个字典时,dict(x)返回x的浅拷贝,就像x.copy()一样。另外,x可以是一个其项目为成对项(每个项为两个项目的可迭代对象)的可迭代对象。在这种情况下,dict(x)返回一个字典,其键是x中每对的第一个项目,其值是对应的第二个项目。当x中的键出现多次时,Python 使用与键的最后一次出现对应的值。换句话说,当x是任何产生成对项的可迭代对象时,c = dict(x)与以下等效:

c = {}
`for` key, value `in` x:
    c[key] = value

您还可以使用命名参数调用 dict,除了或代替位置参数x。每个命名参数都成为字典中的一项,名称作为键:这样的额外项可能会“覆盖”x中的一项。

float float(x=0.0, /) 将任何数字或适当的字符串转换为浮点数。参见“浮点数”。
frozenset frozenset(seq=(), /) 返回一个新的冻结(即不可变)集合对象,其包含与可迭代对象seq相同的项目。当seq是一个 frozenset 时,frozenset(seq)返回seq本身,就像seq.copy()一样。另请参阅“集合操作”。
int int(x=0, /, base=10) 将任何数字或适当的字符串转换为整数。当 x 是一个数字时,int 朝向 0 截断,“舍弃”任何小数部分。当 x 是一个字符串时,base 可能存在:然后,base 是转换基数,介于 2 和 36 之间,其中 10 是默认值。你可以显式地将 base 传递为 0:然后,基数为 2、8、10 或 16,具体取决于字符串 x 的形式,就像对整数字面值一样,如“整数”中所述。
list list(seq=(), /) 返回一个具有与可迭代对象 seq 相同项、相同顺序的新列表对象。当 seq 是一个列表时,list(seq) 返回 seq 的一个浅拷贝,就像 seq[:] 一样。请参见“列表”。

| memoryview | memoryview(x, /) 返回一个对象 m,“查看”与 x 完全相同的底层内存,x 必须是支持缓冲区协议的对象(例如 bytes、bytearray 或 array.array 的实例),每个 m.itemsize 字节的项目。在 m 是“一维”的普通情况下(本书不涵盖“多维”memoryview 实例的复杂情况),len(m) 是项目数。你可以对 m 进行索引(返回 int)或对其进行切片(返回“查看”相同底层内存的适当子集的 memoryview 实例)。当 x 是可变的时,m 也是可变的(但你不能改变 m 的大小,所以,当你对一个切片赋值时,它必须来自与切片相同长度的可迭代对象)。m 是一个序列,因此可迭代,并且在 x 是可散列且 m.itemsize 是一个字节时是可散列的。

m 提供了几个只读属性和方法;详细信息请参见在线文档。两个特别有用的方法是 m.tobytes(将 m 的数据作为 bytes 实例返回)和 m.tolist(将 m 的数据作为整数列表返回)。

| object | object() 返回一个新的 object 实例,Python 中最基本的类型。类型为 object 的实例没有功能:这些实例的唯一用途是作为“哨兵”——即不等于任何不同对象的对象。例如,当函数接受一个可选参数,其中 None 是一个合法值时,你可以使用哨兵作为参数的默认值来指示参数被省略了:

MISSING = object()
`def` check_for_none(obj=MISSING):
    `if` obj `is` MISSING:
        `return` -1
    `return` 0 `if` obj `is` `None` `else` 1

|

set set(seq=(), /) 返回一个具有与可迭代对象 seq 相同项的新的可变集合对象。当 seq 是一个集合时,set(seq) 返回 seq 的一个浅拷贝,就像 seq.copy() 一样。请参见“集合”。
slice slice([start, ]stop[, step], /) 返回具有只读属性 start、stop 和 step 绑定到相应参数值的切片对象,当缺少时,默认为None。对于正索引,这样的切片表示与 range(start, stop, step)相同的索引。切片语法,obj[start:stop:step],将切片对象作为参数传递给对象objgetitemsetitemdelitem 方法。由obj的类来解释其方法接收的切片。还请参见“容器切片”。
str str(obj='', /) 返回obj的简明可读字符串表示。如果obj是字符串,则 str 返回obj。还请参见表 8-2 中的 repr 和表 4-1 中的 str
super super(), super(cls, obj, /) 返回对象obj的超级对象(obj必须是类cls或任何cls子类的实例),适合调用超类方法。只在方法代码内实例化此内建类型。super(cls, obj)语法是 Python 2 中保留的遗留形式,用于兼容性。在新代码中,通常在方法内部调用 super()而不带参数,Python 通过内省(如 type(self)和 self 分别确定clsobj)。参见“协作超类方法调用”。
tuple tuple(seq=(), /) 返回具有与可迭代对象seq中相同项目的元组,顺序相同。当seq是元组时,tuple 返回seq本身,类似于seq[:]。参见“元组”。
type type(obj, /) 返回类型对象,即obj的类型(即obj是实例的最终类型,也称为leafmost类型)。对于任何x,type(x)与x.class 相同。避免检查类型的相等性或身份(详情请参阅下面的警告)。这个函数通常用于调试;例如,当值x的行为不如预期时,插入 print(type(x), x)。它还可以用于在运行时动态创建类,如第四章中所述。

内建函数

表 8-2 涵盖了 Python 内建函数(实际上,有些类型仅在似乎它们被作为函数使用时)在模块 builtins 中按字母顺序排列。内建函数的名称是关键字。这意味着您可以在本地或全局作用域中绑定一个标识符作为内建名称,尽管我们建议避免这样做(请参见下面的警告!)。在本地或全局作用域中绑定的名称会覆盖内建作用域中绑定的名称,因此本地和全局名称隐藏内建名称。您还可以重新绑定内建作用域中的名称,如“Python 内建”中所述。 |

不要隐藏内建函数

避免意外隐藏内置函数:你的代码可能后面会用到它们。常常会诱人地使用像 input、list 或 filter 这样的自然命名作为你自己变量的名称,但是不要这样做:这些都是 Python 内置类型或函数的名称,如果重复使用它们作为你自己的变量名,会导致这些内置类型和函数无法访问。除非你养成永远不用自己的名字隐藏内置函数名的习惯,否则迟早会因为意外隐藏而在代码中遇到神秘的错误。

许多内置函数只能使用位置参数调用,不能使用命名参数。在表 8-2 中,我们提到了这种限制不适用的情况;当适用时,我们还使用了 3.8+的位置参数专用标记 /,详见“位置参数专用标记”。

表 8-2. Python 的核心内置函数

import import(module_name[, globals[, locals[, fromlist]]], /) 在现代 Python 中已弃用;请使用 importlib.import_module,详见“模块加载”。
abs abs(x, /) 返回数x的绝对值。当x为复数时,abs 返回x.imag ** 2 + x.real ** 2 的平方根(也称为复数的模)。否则,当x < 0 时,abs 返回-x;当x >= 0 时,abs 返回x。详见表 4-4 中的 absinvertnegpos

| all | all(seq, /) seq是一个可迭代对象。当seq的任何项为假时,all 返回False;否则,all 返回True。类似于操作符andor,详见“短路运算符”,all 在知道答案后立即停止评估并返回结果;对于 all 来说,这意味着当遇到假值项时停止,但如果seq的所有项都为真,则会遍历整个seq。下面是 all 的典型玩具示例:

`if` all(x>0 `for` x `in` the_numbers):
    print('all of the numbers are positive')
`else`:
    print('some of the numbers are not positive')

seq为空时,all 返回True。 |

| any | any(seq, /) seq是一个可迭代对象。如果seq的任何项为真,则 any 返回True;否则,any 返回False。类似于操作符andor,详见“短路运算符”,any 在知道答案后立即停止评估并返回结果;对于 any 来说,这意味着当遇到真值项时停止,但如果seq的所有项都为假,则会遍历整个seq。下面是 any 的典型玩具示例:

`if` any(x<0 `for` x `in` the_numbers):
    print('some of the numbers are negative')
`else`:
    print('none of the numbers are negative')

seq为空时,any 返回False。 |

ascii ascii(x, /) 类似于 repr,但在返回的字符串中转义非 ASCII 字符;结果通常与 repr 的结果相似。
bin bin(x, /) 返回整数x的二进制字符串表示。例如,bin(23)=='0b10111‘。
breakpoint breakpoint() 调用 pdb Python 调试器。如果要 breakpoint 调用替代调试器,请将 sys.breakpointhook 设置为可调用函数。
callable callable(obj, /) 当obj可调用时返回True;否则返回False。如果对象是函数、方法、类或类型,或者是具有 call 方法的类的实例,则可以调用对象。另请参见表 4-1 中的 call
chr chr(code, /) 返回长度为 1 的字符串,一个与 Unicode 中整数code对应的单个字符。另请参见本表中稍后的 ord。
compile compile(source, filename, mode) 编译字符串并返回可供 exec 或 eval 使用的代码对象。当source不是 Python 语法上有效时,compile 会引发 SyntaxError。当source是多行复合语句时,最后一个字符必须为'\n'。当source是表达式且结果用于 eval 时,mode必须为'eval';否则,当字符串用于 exec 时,mode必须为'exec'(对于单个或多个语句字符串)或'single'(对于包含单个语句的字符串)。filename必须是一个字符串,仅在出现错误消息时使用(如果发生错误)。另请参见本表中稍后的 eval,以及“编译和代码对象”。 (compile 还接受可选参数 flags、dont_inherit、optimize 和3.11+ _feature_version,尽管这些很少使用;有关这些参数的更多信息,请参见在线文档。)
delattr delattr(obj, name, /) 从obj中移除属性name。delattr(obj, 'ident')类似于 del obj.ident*。如果obj具有名为name的属性,仅因为其类具有该属性(通常情况下,例如,对于obj方法),则无法从obj本身删除该属性。如果元类允许,您可以从中删除该属性。如果您可以删除类属性,则obj将不再具有该属性,该类的每个其他实例也是如此。
dir dir([obj, ]/) 不带参数调用时,dir 返回当前作用域中绑定的所有变量名的排序列表。dir(obj)返回一个排序后的obj属性名称列表,其中包括来自obj类型或通过继承的属性。另请参见本表中稍后的 vars。
divmod divmod(dividend, divisor, /) 计算两个数的商和余数,并返回一对。另请参见表 4-4 中的 divmod

| enumerate | enumerate(iterable, start=0) 返回一个新的迭代器,其项为对。对于每对,第二项是 iterable 中的对应项,而第一项是一个整数:start,start+1,start+2.... 例如,以下代码段在整数列表 L 上循环,通过对每个偶数值除以二来就地更改 L: |

`for` i, num `in` enumerate(L):
    `if` num % 2 == 0:
        L[i] = num // 2

enumerate 是少数几个支持命名参数的内置函数之一。 |

eval eval(expr[, globals[, locals]], /) 返回表达式的结果。expr 可以是准备好进行评估的代码对象,或者是一个字符串;如果是字符串,eval 通过内部调用 compile(expr, '', 'eval') 来获取一个代码对象。eval 将代码对象作为表达式进行评估,使用 globalslocals 字典作为命名空间(当它们缺失时,eval 使用当前命名空间)。eval 不执行语句,它只评估表达式。尽管如此,eval 是危险的;除非你知道并信任 expr 来自于一个你确定是安全的源,否则应避免使用。另请参见 ast.literal_eval(在 “标准输入” 中介绍)和 “动态执行和 exec”。
exec exec(statement[, globals[, locals]], /) 类似于 eval,但适用于任何语句并返回 None。exec 非常危险,除非你知道并信任 statement 来自于一个你确定是安全的源。另请参见 “语句” 和 “动态执行和 exec”。

| filter | filter(func, seq, /) 返回 seq 中使得 func 为真的项的迭代器。func 可以是接受单个参数的任何可调用对象,或者 Noneseq 可以是任何可迭代对象。当 func 是可调用对象时,filter 对 seq 中的每个项调用 func,类似于以下的生成器表达式:

(*`item`* `for` *`item`* `in` *`seq`* `if` *`func`*(*`item`*)

funcNone 时,filter 测试真值项,就像:

(*`item`* `for` *`item`* `in` *`seq`* `if` *`item`*)

|

format format(x, format_spec='', /) 返回 x.format(format_spec)。参见 表 4-1。
getattr getattr(obj, name[, default], /) 返回 obj 的名为 name 的属性。getattr(obj, 'ident') 就像 obj.ident。当 default 存在且在 obj 中找不到 name 时,getattr 返回 default 而不是引发 AttributeError。另请参见 “对象属性和项目” 和 “属性引用基础”。
globals globals() 返回调用模块的 dict(即,在调用点用作全局命名空间的字典)。另请参见本表后面的 locals。 (与 locals() 不同,globals() 返回的字典是可读/写的,并且对该字典的更新等效于普通的名称定义。)
hasattr hasattr(obj, name, /) 当 obj 没有属性 name 时返回 False(即,当 getattr(obj, name) 会引发 AttributeError 时);否则,返回 True。另请参见 “属性引用基础”。
哈希 hash(obj, /) 返回 obj 的哈希值。obj 可以是字典键,或集合中的项,前提是 obj 可以被哈希。所有相等的对象必须具有相同的哈希值,即使它们的类型不同也是如此。如果 obj 的类型没有定义相等比较,hash(obj) 通常返回 id(obj)(参见本表中的 id 和 Table 4-1 中的 hash)。
帮助 help([obj, /]) 调用时如果没有 obj 参数,会开始一个交互式帮助会话,输入 quit 退出。如果给定了 obj,help 会打印 obj 及其属性的文档,并返回 None。在交互式 Python 会话中使用 help 可以快速查看对象功能的参考资料。
十六进制 hex(x, /) 返回 int x 的十六进制字符串表示。参见 Table 4-4 中的 hex
id id(obj, /) 返回 obj 的标识整数值。obj 的 id 在 obj 的生命周期内是唯一且恒定的^(a)(但在 obj 被垃圾回收后的任何时间后可能被重用,因此不要依赖存储或检查 id 值)。当一个类型或类没有定义相等比较时,Python 使用 id 来比较和哈希实例。对于任何对象 xy,身份检查 x is y 等同于 id(x)==id(y),但更可读且性能更佳。
输入 input(prompt='', /) 将 prompt 写入标准输出,从标准输入读取一行,并将该行(不带 \n)作为 str 返回。在文件结尾处,input 会引发 EOFError。
isinstance isinstance(obj, cls, /) 当 obj 是类 cls 的实例(或 cls 的任何子类,或者实现了协议或 ABC cls)时,返回 True;否则返回 Falsecls 可以是一个包含类的元组(或 3.10+ 使用 | 运算符连接的多个类型):在这种情况下,如果 objcls 的任何项的实例,则 isinstance 返回 True;否则返回 False。参见 “抽象基类” 和 “协议”。
是否为子类 issubclass(cls1, cls2, /) 当 cls1cls2 的直接或间接子类,或者定义了协议或 ABC cls2 的所有元素时,返回 True;否则返回 Falsecls1cls2 必须是类。cls2 也可以是一个包含类的元组。在这种情况下,如果 cls1cls2 的任何项的直接或间接子类,则 issubclass 返回 True;否则返回 False。对于任何类 C,issubclass(C, C) 返回 True

| 迭代器 | iter(obj, /), iter(func, sentinel, /)

创建并返回一个迭代器(一个你可以重复传递给 next 内置函数以逐个获取项的对象;参见 “迭代器”)。当带有一个参数调用时,iter(obj) 通常返回 obj.iter()。当 obj 是没有特殊方法 iter 的序列时,iter(obj) 等同于生成器:

`def` iter_sequence(obj):
    i = 0
    `while` `True`:
        `try`:
 `yield` obj[i]
 `except` IndexError:
 `raise` StopIteration
        i += 1

另请参见 “序列” 和 Table 4-2 中的 iter

| iter (cont.) | 当调用时带有两个参数时,第一个参数必须是可无参数调用的可调用对象,而 iter(func, sentinel) 相当于生成器:

`def` iter_sentinel(func, sentinel):
    `while` `True`:
        item = func()
        `if` item == sentinel:
            `raise` StopIteration
        `yield` item

不要在 for 语句中调用 iter

如 “for 语句” 中所讨论的,语句 for x in obj 等同于 for x in iter(obj);因此,在这样的 for 语句中不要显式调用 iter。那会是多余的,因此不符合良好的 Python 风格,还会更慢且不易读。

iter 是幂等的。换句话说,当 x 是一个迭代器时,iter(x) 就是 x,只要 x 的类提供了一个体现为 return self 的 iter 方法,就像迭代器类应该的那样。

len len(container, /) 返回 container 中的项数,container 可能是序列、映射或集合。另请参见 “容器方法” 中的 len
locals locals() 返回表示当前局部命名空间的字典。将返回的字典视为只读;试图修改它可能会影响局部变量的值,也可能会引发异常。另请参见本表中的 globals 和 vars。

| map | map(func, seq, /), map(func, /, *seqs)

map 在可迭代的 seq 的每个项上调用 func 并返回结果的迭代器。当你使用多个 seqs 可迭代对象调用 map 时,func 必须是一个接受 n 个参数的可调用对象(其中 nseqs 参数的数量)。map 将重复调用 func,每个可迭代对象中的相应项作为参数。

例如,map(func, seq) 就像生成器表达式一样:

(*`func`*(*`item`*) `for` *`item`* `in` *`seq`*).map(*`func`*, *`seq1`*, *`seq2`*)

就像生成器表达式一样:

(*`func`*(*`a``,` `b`*) `for` *`a`*, *`b`* `in` zip(*`seq1`*, *`seq2`*))

当 map 的可迭代参数长度不同时,map 行为就像将较长的那些截断一样(就像 zip 本身一样)。

| max | max(seq, /, *, key=None[, default=...]), max(*args, key=None[, default=...])

返回可迭代参数 seq 中的最大项,或多个位置参数 args 中的最大项。你可以传递一个 key 参数,具有与 “排序列表” 中介绍的相同语义。你还可以传递一个默认参数,即当 seq 为空时返回的值;当你不传递默认参数且 seq 为空时,max 将引发 ValueError。(当你传递 key 和/或 default 时,必须将其中一个或两个作为命名参数传递。)

| min | min(seq, /, , key=*None[, default=...]), min(args, key=*None[, default=...])

返回可迭代参数 seq 中的最小项,或多个位置参数 args 中的最小项之一。您可以传递一个 key 参数,其语义与 “排序列表” 中介绍的相同。您还可以传递一个 default 参数,如果 seq 为空则返回该值;当不传递 default 且 seq 为空时,min 将引发 ValueError 异常。(当您传递 key 和/或 default 时,必须将其中一个或两者作为命名参数传递。) |

next next(it[, default], /) 从迭代器 it 中返回下一个项目,并使其前进到下一个项目。当 it 没有更多项目时,next 返回 default;当没有传递 default 时,next 抛出 StopIteration 异常。
oct oct(x, /) 将整数 x 转换为八进制字符串。另见 表 4-4 中的 oct

| open | open(file, mode='r', buffering=-1) 打开或创建文件并返回新的文件对象。open 还接受许多更多的可选参数;详情请参见 “io 模块”。 |

open 是少数几个可以使用命名参数调用的内置函数。 |

ord ord(ch, /) 返回一个介于 0 和 sys.maxunicode(包括)之间的整数,对应于单字符 str 参数 ch。另见本表中较早的 chr。
pow pow(x, y[, z], /) 当 z 存在时,pow(x, y, z) 返回 (x ** y) % z。当 z 不存在时,pow(x, y) 返回 x ** y。另见 表 4-4 中的 pow。当 x 是整数且 y 是非负整数时,pow 返回一个整数,并使用 Python 的整数全值范围(尽管对于大的整数 xy 的值,计算 pow 可能需要一些时间)。当 xy 是浮点数,或 y < 0 时,pow 返回一个浮点数(或复数,当 x < 0 且 y 不等于 int(y) 时);在这种情况下,如果 xy 太大,pow 将引发 OverflowError。
print print(/, *args, sep=' ', end='\n', file=sys.stdout, flush=False) 使用 str 格式化并发送到文件流,对于 args 的每个项(如果有的话),以 sep 分隔,所有项打印完毕后,根据 flush 的真假决定是否刷新流。

| range | range([start=0, ]stop[, step=1], /) 返回一个整数迭代器,表示算术级数:

start, start+step, start+(2*step), ...

当缺少 start 参数时,默认为 0。当缺少 step 参数时,默认为 1。当 step 为 0 时,range 抛出 ValueError。当 step > 0 时,最后一个项是最大的 start+(istep) 严格小于 stop。当 step < 0 时,最后一个项是最小的 start+(istep) 严格大于 stop。当 start 大于或等于 stop 且 step 大于 0,或当 start 小于或等于 stop 且 step 小于 0 时,迭代器为空。否则,迭代器的第一个项总是 start。

当你需要一个算术级数的整数列表时,请调用 list(range(...))。 |

repr repr(obj, /) 返回obj的完整且明确的字符串表示形式。在可行的情况下,repr 返回一个字符串,您可以将其传递给 eval 以创建一个与obj具有相同值的新对象。还请参阅表 8-1 中的 str 和表 4-1 中的 repr
reversed reversed(seq, /) 返回一个新的迭代器对象,该对象按逆序产生seqseq必须明确地是一个序列,而不仅仅是任何可迭代对象)的项目。
round round(number, ndigits=0) 返回一个浮点数,其值为 int 或浮点数 number 四舍五入到小数点后 ndigits 位(即,最接近 number 的 10**-ndigits 的倍数)。当两个这样的倍数与 number 等距时,round 返回偶数倍数。由于当今的计算机以二进制而不是十进制表示浮点数,大多数 round 的结果都不精确,正如文档中的教程详细解释的那样。还请参阅“decimal 模块”和大卫·戈德伯格(David Goldberg)关于浮点算术的著名的与语言无关的文章
setattr setattr(obj, name, value, /) 将obj的属性name绑定到value。setattr(obj, 'ident', val) 类似于obj.ident=val。还请参阅此表中早期的 getattr,“对象属性和项目”以及“设置属性”。

| sorted | sorted(seq, /, , key=None, reverse=False) 返回一个与可迭代seq*中的相同项目以排序顺序排列的列表。与下面相同:

`def` sorted(*`seq`*, /, *, key=`None`, reverse=`False`):
    result = list(*`seq`*)
    result.sort(key, reverse)
    `return` result

查看“列表排序”以了解参数的含义。如果要传递键(key)和/或反转(reverse),必须按名称传递。

sum sum(seq, /, start=0) 返回可迭代seq(应为数字,并且特别是不能为字符串)的项目的总和,加上 start 的值。当seq为空时,返回 start。要对字符串可迭代对象进行“求和”(连接),请使用''.join(iterofstrs),如表 8-1 和“从片段构建字符串”中所述。
vars vars([obj, ]/) 调用时不带参数时,vars 返回一个字典,其中包含在当前范围(如此表中较早涵盖的局部变量)中绑定的所有变量。请将此字典视为只读。vars(obj) 返回一个字典,其中包含当前在obj中绑定的所有属性,类似于 dir,此表中较早涵盖的内容。此字典可以是可修改的也可以是不可修改的,具体取决于obj的类型。
zip zip(seq, /, seqs, strict=*False) Returns an iterator of tuples, where the nth tuple contains the nth item from each of the argument iterables. You must call zip with at least one (positional) argument, and all positional arguments must be iterable. zip returns an iterator with as many items as the shortest iterable, ignoring trailing items in the other iterable objects. 3.10+ When the iterables have different lengths and strict is True, zip raises ValueError once it reaches the end of the shortest iterable. See also map earlier in this table and zip_longest in Table 8-10.
^(a) Otherwise arbitrary; often, an implementation detail, obj’s address in memory.

sys 模块

sys 模块的属性绑定到提供关于 Python 解释器状态或直接影响解释器的数据和函数。 Table 8-3 涵盖了 sys 的最常用属性。大多数 sys 属性专为调试器、分析器和集成开发环境使用;详细信息请参阅 online docs

平台特定的信息最好使用 platform 模块访问,本书不涵盖此模块;详细信息请参阅 online docs

Table 8-3. Functions and attributes of the sys module

argv The list of command-line arguments passed to the main script. argv[0] is the name of the main script,^(a) or '-c' if the command line used the -c option. See “The argparse Module” for one good way to use sys.argv.
audit audit(event, /, *args) Raises an audit event whose name is str event and whose arguments are args. The rationale for Python’s audit system is laid out in exhaustive detail in PEP 578; Python itself raises the large variety of events listed in the online docs. To listen for events, call sys.addaudithook(hook), where hook is a callable whose arguments are a str, the event’s name, followed by arbitrary positional arguments. For more details, see the docs.
buil⁠t⁠i⁠n⁠_​m⁠o⁠d⁠ule_names A tuple of strs: the names of all the modules compiled into this Python interpreter.

| displayhook | displayhook(value, /) In interactive sessions, the Python interpreter calls displayhook, passing it the result of each expression statement you enter. The default displayhook does nothing when value is None; otherwise, it saves value in the built-in variable whose name is _ (an underscore) and displays it via repr:

`def` _default_sys_displayhook(value, /):
    `if` value `is` `not` `None`:
        __builtins__._ = value
        print(repr(value))

你可以重新绑定 sys.displayhook 以更改交互行为。原始值可通过 sys.displayhook 获取。

dont_wri⁠t⁠e⁠_​b⁠y⁠t⁠ecode 当为 True 时,Python 在导入源文件(扩展名为 .py)时不将字节码文件(扩展名为 .pyc)写入磁盘。
excepthook excepthook(type, value, traceback, /) 当异常没有被任何处理程序捕获并传播到调用堆栈的最顶层时,Python 调用 excepthook,传递给它异常类、对象和回溯,如 “异常传播” 中所述。默认的 excepthook 显示错误和回溯。您可以重新绑定 sys.excepthook 来更改未捕获异常(在 Python 返回交互循环或终止之前)的显示和/或记录方式。原始值可通过 sys.excepthook 获取。
exception exception() 3.11+ 在 except 子句中调用时,返回当前异常实例(等效于 sys.exc_info()[1])。

| exc_info | exc_info() 当前线程正在处理异常时,exc_info 返回一个包含三个元素的元组:异常的类、对象和回溯(traceback)。当线程没有处理异常时,exc_info 返回 (None, None, None)。要显示来自回溯的信息,请参阅 “回溯模块”。

持有回溯对象可能使一些垃圾成为不可回收

traceback 对象间接地持有对调用堆栈上所有变量的引用;如果您持有回溯的引用(例如通过将变量绑定到 exc_info 返回的元组间接地持有),Python 必须保留可能被垃圾回收的数据。确保对回溯对象的任何绑定持续时间很短,例如使用 try/finally 语句(在 “try/finally” 中讨论)。如果必须持有异常 e 的引用,请清除 e 的回溯:e.traceback=None。^(b) |

|

exit exit(arg=0, /) 触发一个 SystemExit 异常,通常在执行try/finally语句、with语句以及 atexit 模块安装的清理处理程序后终止执行。当 arg 是一个整数时,Python 使用 arg 作为程序的退出代码:0 表示成功终止;任何其他值表示程序未成功终止。大多数平台要求退出代码在 0 到 127 之间。当 arg 不是整数时,Python 将 arg 打印到 sys.stderr,并且程序的退出代码为 1(通用的“未成功终止”代码)。
float_info 一个只读对象,其属性保存了该 Python 解释器中浮点类型的底层实现细节。详细信息请参阅 在线文档
g⁠e⁠t⁠r⁠e⁠c⁠u⁠r⁠s⁠i⁠o⁠n​l⁠i⁠m⁠i⁠t getrecursionlimit() 返回 Python 调用堆栈深度的当前限制。也请参阅 “递归” 和本表后面的 setrecursionlimit。
getrefcount getrefcount(obj, /) 返回obj的引用计数。引用计数在“垃圾回收”中有介绍。
getsizeof getsizeof(obj[, default], /) 返回obj的大小,以字节为单位(不包括obj可能引用的任何项或属性),或在obj无法提供其大小的情况下返回default(在后一种情况下,如果default不存在,则 getsizeof 引发 TypeError)。
maxsize 在此 Python 版本中对象的最大字节数(至少为 2 ** 31 - 1,即 2147483647)。
maxunicode 在此 Python 版本中 Unicode 字符的最大代码点;当前始终为 1114111(0x10FFFF)。Python 使用的 Unicode 数据库版本在 unicodedata.unidata_version 中。
modules 一个字典,其项为所有已加载模块的名称和模块对象。有关 sys.modules 的更多信息,请参见“模块加载”。
path 一个字符串列表,指定 Python 在查找要加载的模块时搜索的目录和 ZIP 文件。有关 sys.path 的更多信息,请参见“在文件系统中搜索模块”。
platform 一个字符串,指定运行此程序的平台名称。典型的值是简短的操作系统名称,如'darwin'、'linux2'和'win32'。对于 Linux,检查 sys.platform.startswith('linux')以实现在不同 Linux 版本间的可移植性。还请参阅模块platform的在线文档,本书不涵盖该部分。

| ps1, ps2 | ps1 和 ps2 分别指定主提示符和次要提示符字符串,初始值分别为 >>> 和 ...。这些 sys 属性仅存在于交互式解释器会话中。如果将任何属性绑定到非字符串对象x,Python 将在每次输出提示时调用该对象的 str(x)方法来生成提示符。此功能允许动态提示:编写一个定义了 str 方法的类,然后将该类的实例分配给 sys.ps1 和/或 sys.ps2。例如,要获取编号提示符:

>>> `import` `sys`
>>> `class` Ps1(object):
...     `def` __init__(self):
...         self.p = 0
...     `def` __str__(self):
...         self.p += 1
...         `return` f'[{self.p}]>>> '
...
>>> `class` Ps2(object):
...     `def` __str__(self):
...         `return` f'[{sys.ps1.p}]... '
...
>>> sys.ps1, sys.ps2 = Ps1(), Ps2()
[1]>>> (2 +
[1]... 2)
4
[2]>>>

|

s⁠e⁠t⁠r⁠e⁠c⁠u⁠r⁠s⁠i⁠o⁠n​l⁠i⁠m⁠i⁠t setrecursionlimit(limit, /) 设置 Python 调用栈深度限制(默认为 1000)。该限制可防止递归无限扩展导致 Python 崩溃。对于依赖深度递归的程序,可能需要提高此限制,但大多数平台无法支持非常大的调用栈深度限制。更有用的是,降低此限制可以帮助您在测试和调试过程中检查程序是否能够优雅地降级,而不是在几乎无限递归的情况下突然崩溃并引发 RecursionError。还请参见“递归”和本表中 getrecursionlimit 的前文。

| stdin, stdout, |

stderr | stdin、stdout 和 stderr 是预定义的类似文件的对象,分别对应于 Python 的标准输入、输出和错误流。您可以将 stdout 和 stderr 重新绑定到打开以供写入的类似文件的对象(提供接受字符串参数的 write 方法的对象),以重定向输出和错误消息的目标。您可以将 stdin 重新绑定到打开以供读取的类似文件的对象(提供返回字符串的 readline 方法的对象),以重定向内置函数 input 读取的源。原始值可用作 stdinstdoutstderr。我们在 “io 模块” 中讨论文件对象。 |

tracebacklimit 未处理异常的回溯显示的最大级数。默认情况下,此属性未定义(即没有限制)。当 sys.tracebacklimit ⇐ 0 时,Python 仅打印异常类型和值,而不打印回溯。
version 描述 Python 版本、构建号和日期以及所使用的 C 编译器的字符串。仅在日志记录或交互式输出时使用 sys.version;要执行版本比较,请使用 sys.version_info。
version_info 运行中的 Python 版本的主要、次要、微版本、发布级别和序列号字段的命名元组。例如,在 Python 3.10 的第一个正式版之后,sys.version_info 是 sys.version_info(major=3, minor=10, micro=0, releaselevel='final', serial=0),相当于元组 (3, 10, 0, 'final', 0)。此形式被定义为版本间直接可比较;若要检查当前运行版本是否大于或等于 3.8,可以测试 sys.version_info[:3] >= (3, 8, 0)。(不要对 string sys.version 进行字符串比较,因为字符串 "3.10" 会被认为小于 "3.9"!)
(a) 当然,它也可以是脚本的路径,如果是这样,它也可以是对它的符号链接,如果是这样,它就是你给 Python 的(b)。本书的一位作者在 pyparsing 中记住了返回值和引发的异常时,遇到了这个问题:缓存的异常回溯包含了许多对象引用,并且干扰了垃圾收集。解决方案是在将异常放入缓存之前清除异常的回溯。

copy 模块

如 “赋值语句” 中所讨论的,Python 中的赋值不会 复制 被分配的右侧对象。相反,赋值 添加引用 到 RHS 对象。当您想要对象 x副本 时,请求 x 自身的副本,或者请求 x 的类型创建从 x 复制的新实例。如果 x 是列表,则 list(x) 返回 x 的副本,x[:] 也是如此。如果 x 是字典,则 dict(x) 和 x.copy() 返回 x 的副本。如果 x 是集合,则 set(x) 和 x.copy() 返回 x 的副本。在每种情况下,本书的作者更倾向于使用统一和可读的惯用法来调用类型,但在 Python 社区中对此风格问题没有共识。

复制模块提供了一个复制函数用于创建和返回多种类型对象的副本。正常副本,例如对于列表 x 返回的 list(x) 和对于任何 x 返回的 copy.copy(x),称为 浅层 副本:当 x 引用其他对象(作为项或属性时),x 的普通(浅层)副本具有对相同对象的不同引用。然而,有时您需要一个 深层 副本,在这种副本中,引用的对象被递归地进行深层复制(幸运的是,这种需求很少,因为深层复制可能需要大量内存和时间);对于这些情况,复制模块还提供了一个 deepcopy 函数。这些函数在 表 8-4 中进一步讨论。

表 8-4. 复制模块函数

copy copy(x) 创建并返回 x 的浅层副本,适用于多种类型的 x(模块、文件、帧以及其他内部类型,但不支持)。当 x 是不可变的时候,作为优化,copy.copy(x) 可能返回 x 本身。一个类可以通过拥有特殊方法 copy(self),返回一个新对象,即 self 的浅层副本,来自定义 copy.copy 复制其实例的方式。

| deepcopy | deepcopy(x,[memo]) 对 x 进行深层复制并返回。深层复制意味着对引用图进行递归遍历。请注意,为了重现图形的确切形状,当在遍历过程中多次遇到对同一对象的引用时,您不能制作不同的副本;相反,必须使用对同一复制对象的 引用。考虑以下简单示例:

sublist = [1,2]
original = [sublist, sublist]
thecopy = copy.deepcopy(original)

original[0] is original[1] is True(即,original 的两个项引用同一对象)。这是 original 的一个重要属性,任何宣称“一个副本”的东西都必须保留它。copy.deepcopy 的语义确保 thecopy[0] is thecopy[1] 也是 True:original 和 thecopy 的引用图具有相同的形状。避免重复复制具有一个重要的有益副作用:它防止引用图中存在循环时会发生的无限循环。

copy.deepcopy 接受第二个可选参数:memo,一个将每个已复制对象的 id 映射到其副本的字典。memo 被 deepcopy 的所有递归调用传递给自己;如果您需要获取原始对象和副本之间的对应映射(通常作为一个最初为空的字典),您也可以显式地传递它(memo 的最终状态将是这样一个映射)。

类可以通过拥有特殊方法 deepcopy(self, memo) 来定制 copy.deepcopy 复制其实例的方式,该方法返回一个新对象,即 self 的深层副本。当 deepcopy 需要深层复制一些引用对象 subobject 时,必须通过调用 copy.deepcopy(subobject, memo) 来实现。当一个类没有特殊方法 deepcopy 时,对该类的实例使用 copy.deepcopy 也会尝试调用特殊方法 getinitargsgetnewargsgetstatesetstate,详见 “实例的序列化”。

collections 模块

collections 模块提供了有用的类型,这些类型是集合(即,容器),以及 “抽象基类” 中涵盖的 ABC。自 Python 3.4 起,ABC 在 collections.abc 中提供;为了向后兼容,直到 Python 3.9 仍然可以直接在 collections 中访问,但此功能在 3.10 中已移除。

ChainMap

ChainMap 将多个映射“链接”在一起;给定 ChainMap 实例 c,访问 c[key] 返回具有该键的第一个映射中的值,而对 c 的所有更改仅影响 c 中的第一个映射。为了进一步解释,您可以如下近似:

`class` ChainMap(collections.abc.MutableMapping):
    `def` __init__(self, *maps):
        self.maps = list(maps)
        self._keys = set()
        `for` m `in` self.maps:
            self._keys.update(m)
    `def` __len__(self): `return` len(self._keys)
    `def` __iter__(self): `return` iter(self._keys)
    `def` __getitem__(self, key):
        `if` key `not` `in` self._keys: `raise` KeyError(key)
        `for` m `in` self.maps:
            `try`: `return` m[key]
            `except` KeyError: `pass`
    `def` __setitem__(self, key, value):
        self.maps[0][key] = value
        self._keys.add(key)
    `def` __delitem__(self, key):
        `del` self.maps[0][key]
        self._keys = set()
        `for` m `in` self.maps:
            self._keys.update(m)

其他方法可以为了效率而定义,但这是 MutableMapping 要求的最小集合。更多详情和一组关于如何使用 ChainMap 的示例,请参阅 在线文档

计数器

计数器是 int 值的 dict 的子类,用于计数键已经被看到多少次(尽管允许值 ⇐ 0);它大致相当于其他语言中称为“bag”或“multiset”类型的类型。通常,Counter 实例是从其项是可散列的可迭代对象构建的:c = collections.Counter(iterable)。然后,您可以使用 iterable 的任何项对 c 进行索引以获取该项出现的次数。当您使用任何缺失的键对 c 进行索引时,结果为 0(要 移除 c 中的条目,请使用 del c[entry];将 c[entry]=0 留下 c 中的 entry,其值为 0)。

c 支持字典的所有方法;特别是,c.update(otheriterable) 更新所有计数,根据 otheriterable 中的出现次数递增它们。例如:

>>> c = collections.Counter('moo')
>>> c.update('foo')

离开 c['o'] 得到 4,c['f'] 和 c['m'] 分别得到 1。注意,从 c 中移除条目(用 del)可能 不会 减少计数器,但是减去(在下表中描述)会:

>>> `del` c['foo']    
>>> c['o']
4
>>> c.subtract('foo')
>>> c['o']
2

除了字典方法外,c 还支持详细说明的额外方法,见 表 8-5。

表 8-5. 计数器实例 c 的方法

elements c.elements() 以任意顺序产生 cc[key]>0 的键,每个键产生的次数与其计数相同。
mo⁠s⁠t⁠_​c⁠o⁠m⁠mon c.most_common([n, /]) 返回 c 中计数最高的前 n 个键的列表(如果省略 n,则为所有键),按计数降序排序(具有相同计数的键之间的“平局”将任意解决);每对形式为 (k, c[k]),其中 kc 中前 n 个最常见的键之一。
subtract c.subtract(*iterable=*None, /, **kwds) 类似于 c.update(iterable) 的“反向操作”——即减去计数而不是增加它们。c 中的结果计数可以 ⇐ 0。
total *c.*total() 3.10+ 返回所有个体计数的总和。等同于 sum(c.values())。

Counter 对象支持常见的算术运算符,如 +、-、& 和 |,用于加法、减法、并集和交集。详见 在线文档 获取更多详情和一系列关于如何使用 Counter 的实用示例。

OrderedDict

OrderedDict 是 dict 的子类,具有额外的方法来按插入顺序访问和操作条目。o.popitem() 删除并返回最近插入的键的条目;o.move_to_end(key, last=True) 将具有键 key 的条目移动到末尾(当 last 为 True 时,默认为末尾)或开头(当 last 为 False 时)。两个 OrderedDict 实例之间的相等性测试是顺序敏感的; OrderedDict 实例与 dict 或其他映射的相等性测试则不是。自 Python 3.7 起,dict 插入顺序已保证保持不变:许多以前需要 OrderedDict 的用法现在可以直接使用普通的 Python dict。两者之间仍存在一个显著的差异,即 OrderedDict 与其他 OrderedDict 的相等性测试是顺序敏感的,而 dict 的相等性测试则不是。详见 在线文档 获取更多详情和一系列关于如何使用 OrderedDict 的示例。

defaultdict

defaultdict 是 dict 的扩展,每个实例都添加了一个名为 default_factory 的属性。当 defaultdict 实例 dd.default_factory 值为 None 时,d 表现得和 dict 完全一样。否则,d.default_factory 必须是一个可调用的无参数函数,d 的行为和 dict 类似,除非你使用一个不存在于 d 中的键 k 访问 d。在这种情况下,索引 d[k] 调用 d.default_factory(),将结果分配为 d[k] 的值,并返回结果。换句话说,defaultdict 类型的行为很像下面这样用 Python 编写的类:

`class` defaultdict(dict):
    `def` __init__(self, default_factory=`None`, *a, **k):
        super().__init__(*a, **k)
        self.default_factory = default_factory
    `def` __getitem__(self, key):
        `if` key `not` `in` self `and` self.default_factory `is` `not` `None`:
            self[key] = self.default_factory()
        `return` dict.__getitem__(self, key)

正如这个 Python 等效物所暗示的,要实例化 defaultdict,通常需要将额外的第一个参数传递给它(在任何其他参数之前,位置和/或命名,如果有的话,传递给普通 dict)。额外的第一个参数将成为 default_factory 的初始值;你也可以稍后访问和重新绑定 default_factory,尽管在正常的 Python 代码中这样做是不常见的。

defaultdict 的所有行为基本上与此 Python 等效内容暗示的行为相同(除了 str 和 repr,它们返回的字符串与 dict 返回的字符串不同)。命名方法,如 get 和 pop,并不受影响。与键相关的所有行为(方法 keys、迭代、通过运算符 in 进行成员测试等)完全反映了当前容器中的键(无论是显式放置还是通过调用 default_factory 隐式放置)。

defaultdict 的典型用途是,例如将 default_factory 设置为 list,以创建从键到值列表的映射:

`def` make_multi_dict(items):
    d = collections.defaultdict(list)
    `for` key, value `in` items:
        d[key].append(value)
    `return` d

使用任何元素为形式为 (key, value) 对的可迭代对象调用 make_multi_dict 函数时,此函数返回一个映射,将每个键关联到伴随其出现的一个或多个值列表(如果要得到纯字典结果,请将最后一条语句更改为 return dict(d)——这很少是必要的)。

如果不希望结果中有重复项,并且每个 value 都是可哈希的,请使用 collections.defaultdict(set),并在循环中添加而不是追加。²

deque

deque 是一种序列类型,其实例为“双端队列”(在任一端的添加和移除都很快且线程安全)。deque 实例 d 是可变序列,可选择具有最大长度,并且可以进行索引和迭代(但是,d 不能进行切片;它只能一次索引一个项目,无论是用于访问、重新绑定还是删除)。如果 deque 实例 d 具有最大长度,当向 d 的任一侧添加项目以使 d 的长度超过该最大长度时,将从另一侧静默删除项目。

deque 特别适用于实现先进先出(FIFO)队列。³ deque 也很适合维护“最新 N 个看到的事物”,在其他一些语言中也称为环形缓冲区。

表 8-6 列出了 deque 类型提供的方法。

表 8-6. deque 方法

deque deque(seq=(), /, maxlen=None) d 的初始项目是 seq 的项目,顺序相同。 d.maxlen 是只读属性:当其值为 None 时,d 没有最大长度;当为整数时,必须 >=0。 d 的最大长度是 d.maxlen。
append d.append(item, /) 将 item 追加到 d 的右侧(末尾)。
appendleft d.appendleft(item, /) 将 item 追加到 d 的左侧(开头)。
clear d.clear() 从 d 中移除所有项目,使其为空。
extend d.extend(iterable, /) 将 iterable 中的所有项目追加到 d 的右侧(末尾)。
extendleft d.extendleft(iterable, /) 将 iterable 中的所有项目以相反的顺序追加到 d 的左侧(开头)。
pop d.pop() 从 d 中移除并返回最后一个(最右边的)项目。如果 d 是空的,则引发 IndexError 异常。
popleft d.popleft() 从 d 中移除并返回第一个(最左边的)项目。如果 d 是空的,则引发 IndexError 异常。
rotate d.rotate(n=1, /) 将 d 向右旋转 n 步(如果 n < 0,则向左旋转)。

避免对 deque 进行索引或切片

deque 主要用于从 deque 的开头或结尾访问、添加和删除项目的情况。虽然可以对 deque 进行索引或切片,但当使用 deque[i] 形式访问内部值时,性能可能为 O(n)(而列表为 O(1))。如果必须访问内部值,请考虑使用列表。

functools 模块

functools 模块提供支持 Python 函数式编程的函数和类型,列在 表 8-7 中。

表 8-7. functools 模块的函数和属性

cach⁠e⁠d⁠_​p⁠r⁠o⁠p⁠erty cached_property(func) 3.8+ 一个缓存版本的属性修饰器。第一次计算属性时会缓存返回的值,因此后续调用可以返回缓存的值,而不是重复计算属性。cached_property 使用线程锁确保在多线程环境中只执行一次属性计算。^(a)

| lru_cache, cache | lru_cache(max_size=128, typed=False), cache()

适合装饰一个函数的 memoizing 装饰器,其所有参数都是可散列的,向函数添加一个缓存,存储最后的 max_size 个结果(max_size 应为 2 的幂,或 None 以保留所有先前的结果);当再次使用与缓存中的参数调用装饰函数时,它将立即返回先前缓存的结果,绕过底层函数的主体代码。当 typed 设置为 True 时,相等但类型不同的参数(例如 23 和 23.0)将分别缓存。3.9+ 如果将 max_size 设置为 None,则使用 cache 替代。有关更多详细信息和示例,请参阅 在线文档。3.8+ lru_cache 也可作为无括号的装饰器使用。

| partial | partial(func, /, **a, *k) 返回一个可调用对象 p,它与 func(任何可调用对象)类似,但某些位置参数和/或命名参数已绑定到给定的 ak 的值。换句话说,pfunc 的部分应用,通常也被称为 func 对给定参数的 柯里化(在数学家 Haskell Curry 的荣誉上,尽管正确性有争议,但用语色彩丰富)。例如,假设我们有一个数字列表 L,并希望将负数剪切为 0。一种方法是:

L = map(functools.partial(max, 0), L)

作为lambda使用的替代片段:

L = map(`lambda` x: max(0, x), L)

以及最简洁的方法,即列表推导:

L = [max(0, x) `for` x `in` L]

functools.partial 在需要回调的情况下特别适用,例如某些 GUI 和网络应用的事件驱动编程。

partial 返回一个带有 func(包装函数)、args(预绑定的位置参数元组)和 keywords(预绑定的命名参数字典,或者 None)属性的可调用对象。 |

| reduce | reduce(func, seq[, init], /) 将 func 应用于 seq 的项,从左到右,将可迭代对象减少为单个值。func 必须是可调用的,接受两个参数。reduce 首先将 func 应用于 seq 的前两个项,然后将第一次调用的结果和第三个项进行调用,依此类推,返回最后一次调用的结果。如果存在 init,reduce 在 seq 的第一项之前使用它(如果有的话)。如果 init 缺失,seq 必须非空。如果 init 缺失且 seq 只有一个项,则 reduce 返回 seq[0]。类似地,如果存在 initseq 为空,则 reduce 返回 init。因此,reduce 大致等同于:

`def` reduce_equiv(func, seq, init=`None`):
    seq = iter(seq)
    `if` init `is` `None`:
        init = next(seq)
    `for` item `in` seq: 
        init = func(init, item)
    `return` init

一个 reduce 的示例用法是计算一系列数字的乘积:

prod=reduce(operator.mul, seq, 1)

|

singledispatch, singledispatchmethod 函数装饰器,支持具有不同类型的第一个参数的方法的多个实现。有关详细描述,请参阅 在线文档
total_ordering 一个类装饰器,适用于装饰至少提供一个不等比较方法(如 lt)的类,并且最好也提供 eq。基于类的现有方法,total_ordering 类装饰器为类添加了所有其他不等比较方法,这些方法在类本身或任何超类中都没有实现,从而避免您为它们添加样板代码。
wraps wraps(wrapped) 适用于装饰包装另一个函数 wrapped 的函数的装饰器(通常是在另一个装饰器内部的嵌套函数)。wraps 复制了 wrapped 的 namedocmodule 属性到装饰函数上,从而改善了内置函数 help 和 doctests 的行为,详情请参阅 “The doctest Module”。
^(a) 在 Python 版本 3.8 到 3.11 中,cached_property 是使用类级锁实现的。因此,它对于类或任何子类的所有实例都同步,而不仅仅是当前实例。因此,在多线程环境中,cached_property 可能会降低性能,推荐使用。

heapq 模块

heapq 模块使用 min-heap 算法,在插入和提取项目时将列表保持“几乎排序”的顺序。heapq 的操作速度比每次插入后调用列表的排序方法要快得多,比 bisect(在 在线文档 中介绍)要快得多。对于许多目的,比如实现“优先队列”,heapq 支持的几乎排序顺序和完全排序顺序一样好,并且建立和维护速度更快。heapq 模块提供了 Table 8-8 中列出的函数。

Table 8-8. heapq 模块的函数列表

| heapify | heapify(alist, /) 根据需要重新排列列表 alist,使其满足(最小)堆条件:

  • 对于任何 i >= 0:

  • alist[i] ⇐ alist[2 * i + 1] and

  • alist[i] ⇐ alist[2 * i + 2]

  • 只要所有相关的索引都小于 len(alist)。

如果一个列表满足(最小)堆条件,则列表的第一个项是最小的(或者相等最小的)项。排序后的列表满足堆条件,但列表的许多其他排列也可以满足堆条件,而不需要列表完全排序。heapify 运行时间为 O(len(alist))。

heappop heappop(alist, /) 移除并返回 alist 中最小(第一个)项,一个满足堆条件的列表,并重新排列 alist 的一些剩余项,以确保在移除后仍满足堆条件。heappop 运行时间为 O(log(len(alist)))。
heappush heappush(alist, item, /) 在满足堆条件的 alist 中插入 item,并重新排列一些 alist 的项,以确保在插入后仍满足堆条件。heappush 运行时间为 O(log(len(alist)))。

| heappushpop | heappushpop(alist, item, /) 逻辑上等价于先执行 heappush,然后执行 heappop,类似于:

`def` heappushpop(alist, item):
    heappush(alist, item)
    `return` heappop(alist)

heappushpop 运行时间为 O(log(len(alist))),通常比逻辑上等价的函数更快。heappushpop 可以在空的 alist 上调用:在这种情况下,它返回 item 参数,就像当 item 小于 alist 的任何现有项时一样。

| heapreplace | heapreplace(alist, item, /) 逻辑上等价于先执行 heappop,然后执行 heappush,类似于:

`def` heapreplace(alist, item):
    `try`: `return` heappop(alist)
    `finally`: heappush(alist, item)

heapreplace 运行时间为 O(log(len(alist))),通常比逻辑上等价的函数更快。heapreplace 不能在空的 alist 上调用:heapreplace 总是返回已经在 alist 中的项,而不是刚刚被推送到 alist 上的 item

merge merge(*iterables) 返回一个迭代器,按照从小到大的顺序(排序顺序)产生 iterables 的项,其中每个项必须是从小到大排序的。
nlargest nlargest(n, seq, /, key=None) 返回一个逆序排列的列表,包含可迭代序列 seqn 个最大项(或者少于 n 项,如果 seq 的项少于 n 项);当 n 相对于 len(seq) 足够小时比 sorted(seq, reverse=True)[:n] 更快。您也可以指定一个(命名或位置的)key= 参数,就像您可以为 sorted 指定的那样。
nsmallest nsmallest(n, seq, /, key=None) 返回一个已排序的列表,包含可迭代序列 seqn 个最小项(或者少于 n 项,如果 seq 的项少于 n 项);当 n 相对于 len(seq) 足够小时比 sorted(seq)[:n] 更快。您也可以指定一个(命名或位置的)key= 参数,就像您可以为 sorted 指定的那样。
^(a) 要了解特定的 n 值和 len(seq) 如何影响 nlargest、nsmallest 和 sorted 在您的具体 Python 版本和机器上的时间,可以使用 timeit,详见“timeit 模块”。

装饰-排序-去装饰模式

堆模块中的几个函数虽然执行比较,但不接受 key=参数以自定义比较。这是不可避免的,因为这些函数在原地操作一个简单的项列表:它们没有地方“存放”一次性计算好的自定义比较键。

当你需要堆功能和自定义比较时,你可以应用古老的*装饰-排序-去装饰(DSU)*惯用语⁴(在 Python 早期版本引入 key=功能之前,这个惯用语曾是优化排序的关键)。

DSU 惯用语,适用于 heapq,具有以下组件:

装饰

建立一个辅助列表A,其中每个项都是一个元组,以排序键开头,并以原始列表L的项结尾。

排序

A上调用 heapq 函数,通常以 heapq.heapify(A)开始。⁵

去装饰

当你从A中提取一个项,通常通过调用 heapq.heappop(A),只返回结果元组的最后一项(这是原始列表L的一项)。

当你通过调用 heapq.heappush(A, /, item)向A中添加项时,装饰你要插入的实际项到以排序键开头的元组中。

这些操作序列可以封装在一个类中,如本例所示:

`import` `heapq`

`class` KeyHeap(object):
    `def` __init__(self, alist, /, key):
        self.heap = [(key(o), i, o) `for` i, o `in` enumerate(alist)]
        heapq.heapify(self.heap)
        self.key = key
        `if` alist:
            self.nexti = self.heap[-1][1] + 1
        `else`:
            self.nexti = 0

    `def` __len__(self):
        `return` len(self.heap)

    `def` push(self, o, /):
        heapq.heappush(self.heap, (self.key(o), self.nexti, o))
        self.nexti += 1

    `def` pop(self):
        `return` heapq.heappop(self.heap)[-1]

在本例中,我们在装饰的元组中间(排序键后、实际项前)使用一个递增的数字,以确保即使它们的排序键相等,实际项也永远不会直接比较(这种语义保证是排序功能中关键参数的重要方面)。

argparse 模块

当你编写一个 Python 程序,意图从命令行运行(或从类 Unix 系统的 shell 脚本,或从 Windows 的批处理文件),你通常希望让用户传递命令行参数(包括命令行选项,按约定以一个或两个破折号字符开头的参数)。在 Python 中,你可以访问这些参数作为 sys.argv,这是 sys 模块的一个属性,将这些参数作为字符串列表存放(sys.argv[0]是用户启动程序的名称或路径;参数在子列表 sys.argv[1:]中)。Python 标准库提供了三个模块来处理这些参数;我们只涵盖最新和最强大的一个,argparse,并且只涵盖了一个小而核心的 argparse 丰富功能子集。请参阅在线参考资料教程,了解更多内容。argparse 提供一个类,其签名如下:

ArgumentParser ArgumentParser(*kwargs) ArgumentParser 是执行参数解析的类。它接受许多命名参数,大多用于改善程序在命令行参数包含 -h 或 --help 时显示的帮助消息。你应该始终传递一个命名参数 description=,这是一个总结你的程序目的的字符串。

给定 ArgumentParser 的实例 ap,通过一个或多个调用 ap.add_argument 进行准备,然后通过调用 ap.parse_args()(不带参数,因此它解析 sys.argv)使用它。该调用返回一个 argparse.Namespace 的实例,其中包含你程序的参数和选项作为属性。

add_argument 有一个强制的第一个参数:用于位置命令行参数的标识符字符串,或用于命令行选项的标志名称。在后一种情况下,传递一个或多个标志名称;选项可以同时具有短名称(破折号,然后一个字符)和长名称(两个破折号,然后一个标识符)。

在位置参数之后,传递零个或多个命名参数以控制其行为。表 8-9 列出了最常用的几个。

表 8-9. add_argument 的常见命名参数

action 解析器对此参数的操作。默认值:'store',将参数的值存储在命名空间中(使用 dest 指定的名称,在本表后述)。还有几个有用的选项:'store_true' 和 'store_false',将选项转换为布尔值(如果选项不存在,则默认为相反的布尔值),以及 'append',将参数值附加到列表中(因此允许选项重复)。
choices 允许参数的一组值(如果值不在其中,则解析参数会引发异常)。默认值:无约束。
default 如果参数不存在时的值。默认值:None
dest 用于此参数的属性名称。默认值:与第一个位置参数去掉前导破折号相同,如果有的话。
help 描述参数用途的字符串,用于帮助消息。
nargs 逻辑参数使用的命令行参数的数量。默认值:1,存储在命名空间中。可以是大于 0 的整数(使用指定数量的参数,将它们存储为列表),'?'(1 或无,此时使用默认值),'*'(0 或多个,存储为列表),'+'(1 或多个,存储为列表),或 argparse.REMAINDER(所有剩余参数,存储为列表)。
type 接受字符串的可调用对象,通常是类型(如 int),用于将值从字符串转换为其他类型。可以是 argparse.FileType 的实例,将字符串作为文件名打开(如果 FileType('r'),则读取,如果 FileType('w'),则写入,等等)。

这是一个简单的 argparse 示例——将此代码保存在名为 greet.py 的文件中:

`import` argparse
ap = argparse.ArgumentParser(description='Just an example')
ap.add_argument('who', nargs='?', default='World')
ap.add_argument('--formal', action='store_true')
ns = ap.parse_args()
`if` ns.formal:
    greet = 'Most felicitous salutations, o {}.'
`else`:
    greet = 'Hello, {}!'
print(greet.format(ns.who))

现在,python greet.py 打印 Hello, World!,而 python greet.py --formal Cornelia 打印 Most felicitous salutations, o Cornelia.

itertools 模块

itertools 模块提供了高性能的构建块来构建和操作迭代器。为了处理大量的项目,迭代器通常比列表更好,这要归功于迭代器固有的“惰性评估”方法:迭代器逐个生成项目,而列表(或其他序列)的所有项目必须同时在内存中。这种方法甚至使得构建和使用无界迭代器成为可能,而列表必须始终具有有限数量的项目(因为任何计算机都具有有限的内存量)。

表 8-10 涵盖了 itertools 中最常用的属性;每个属性都是一个迭代器类型,您可以调用它们来获取相应类型的实例,或者行为类似的工厂函数。有关更多 itertools 属性,请参阅 在线文档,其中包括 组合 生成器的排列、组合和笛卡尔积,以及有用的 itertools 属性分类。

在线文档还提供了有关组合和使用 itertools 属性的配方。这些配方假设您在模块顶部有 from itertools import *;这 是推荐的用法,只是假设以使配方的代码更紧凑。最好 import itertools as it,然后使用引用如 it.something 而不是更冗长的 itertools.something。⁶

表 8-10. itertools 模块的函数和属性

accumulate accumulate(seq, func, /[, initial*=init*]) 类似于 functools.reduce(func, seq),但返回所有中间计算值的迭代器,而不仅仅是最终值。 3.8+ 您还可以传递一个初始值 init,其工作方式与 functools.reduce 中的相同(请参阅 表 8-7)。

| chain | chain(*iterables) 生成器,从第一个参数开始生成项目,然后从第二个参数开始生成项目,依此类推,直到最后一个参数结束。这就像生成器表达式一样:

(*`it`* `for` *`iterable`* `in` *`iterables`* `for` *`it`* `in` *`iterable`*)

|

| chain.from_ iterable | chain.from_iterable(iterables, /) 生成器,按顺序从参数中的可迭代对象中生成项目,就像生成器表达式一样:

(*`it`* `for` *`iterable`* `in` *`iterables`* `for` *`it`* `in` *`iterable`*)

|

| compress | compress(data, conditions, /) 生成器,生成 conditions 中的 true 项对应的 data 中的每个项目,就像生成器表达式一样:

(*`it`* `for` *`it`*, *`cond`* `in` zip(*`data`*, *`conditions`*) `if` *`cond`*)

|

| count | count(start=0, step=1) 生成连续整数,从 start 开始,就像生成器一样:

`def` count(start=0, step=1):
    `while` `True`:
        `yield` start
        start += step

count 返回一个无尽的迭代器,因此请小心使用,始终确保您显式终止对它的任何循环。 |

| cycle | cycle(iterable, /) 生成器,无限重复 iterable 中的每个项目,每次到达末尾时从开头重新开始,就像生成器一样:

`def` cycle(iterable):
    saved = []
    `for` item `in` iterable:
        `yield` item
        saved.append(item)
    `while` saved:
        `for` item `in` saved:
            `yield` item

cycle 返回一个无尽的迭代器,因此请小心使用,始终确保您显式终止对它的任何循环。 |

| dropwhile | dropwhile(func, iterable, /) 丢弃iterablefunc为真的前面的 0+项,然后产生每个剩余的项,就像生成器一样:

`def` dropwhile(func, iterable):
    iterator = iter(iterable)
    `for` item `in` iterator:
        `if` `not` func(item):
            `yield` item
 `break`
    `for` item `in` iterator:
        `yield` item

|

| filterfalse | filterfalse(func, iterable, /) 产生iterablefunc为假的项,就像生成器表达式一样:

(*`it`* `for` *`it`* `in` *`iterable`* `if` `not` *`func`*(*`it`*))

func可以是接受单个参数的任何可调用对象,或None时。当funcNone时,filterfalse 产生假的项,就像生成器表达式一样:

(*`it`* `for` *`it`* `in` *`iterable`* `if` `not` *`it`*)

|

| groupby | groupby(iterable, /, key=None) iterable通常需要根据 key(通常为None,表示恒等函数,lambda x: x)已排序。groupby 产生对(kg)的对,每个对表示iterable中具有相同值key(item)的相邻项组的group;每个g是一个迭代器,产生组中的项。当 groupby 对象前进时,先前的迭代器g变为无效(因此,如果需要稍后处理一组项,则最好在某处存储一个“快照”列表“它”,list(g))。

另一种查看 groupby 生成的组的方法是,每个组在key(item)更改时终止(这就是为什么通常只在已经按key排序的iterable上调用 groupby)。

例如,假设我们有一组小写单词,我们想要一个字典,将每个首字母映射到具有该首字母的最长单词(在“并列情况”下任意地打破)。我们可以写成:

`import` itertools `as` it
`import` operator
`def` set2dict(aset):
    first = operator.itemgetter(0)
    words = sorted(aset, key=first)
    adict = {}
    `for` init, group `in` it.groupby(words, key=first):
        adict[init] = max(group, key=len)
    `return` adict

|

| islice | islice(iterable[, start], stop[, step], /) 返回iterable的项目(默认情况下跳过第一个start个项目,通常为 0),直到但不包括stop,以step(默认为 1)递增。所有参数必须是非负整数(或None),step必须大于 0。除了检查和可选参数外,它类似于生成器:

`def` islice(iterable, start, stop, step=1):
    en = enumerate(iterable)
    n = stop
    `for` n, item `in` en:
        `if` n>=start:
            `break`
    `while` n<stop:
        `yield` item
        `for` x `in` range(step):
            n, item = next(en)

|

pairwise pairwise(seq, /) 3.10+seq中的项成对出现,允许重叠(例如,pairwise('ABCD')将生成'AB','BC'和'CD')。等同于 zip(seq, seq[1:])返回的迭代器。

| repeat | repeat(item, /[, times]) 重复地产生item,就像生成器表达式一样:

(item `for` _ `in` range(times))

times不存在时,迭代器是无界的,产生可能无限的项,每个项都是对象item,就像生成器一样:

`def` repeat_unbounded(item):
    `while` `True`:
        `yield` item

|

| starmap | starmap(func, iterable, /) 对iterable中的每个item(通常是元组的可迭代对象)产生 func(*item),就像生成器表达式一样:

`def` starmap(func, iterable):
    `for` item `in` iterable:
        `yield` func(*item)

|

| takewhile | takewhile(func, iterable, /) 只要func(item)为真,从iterable中产生项,然后完成,就像生成器表达式一样:

`def` takewhile(func, iterable):
    `for` item `in` iterable:
        `if` func(item):
            `yield` item
        `else`:
 `break`

|

tee tee(iterable, n=2, /) 返回n个独立迭代器的元组,每个迭代器产生与iterable的项目相同的项。返回的迭代器彼此独立,但它们与iterable不独立;在仍然使用返回的任何迭代器时,请避免以任何方式更改对象iterable
zip_longest zip_longest(可迭代对象, /, fillvalue=None) 从每个可迭代对象中产生一个元组;当最长的可迭代对象耗尽时停止,行为就像其他每个对象都使用 fillvalue“填充”到相同长度一样。如果None可迭代对象*中一个有效的值(以至于它可能会与用于填充的None值混淆),你可以使用 Python 省略号(...)或一个哨兵对象 FILL=object()作为 fillvalue。

我们已经展示了许多 itertools 属性的等效生成器和生成表达式,但重要的是要考虑 itertools 的速度优势。作为一个微不足道的例子,考虑重复某些操作 10 次:

`for` _ `in` itertools.repeat(None, 10): `pass`

结果是这种方式比直接的替代方式快大约 10 到 20%,这取决于 Python 版本和平台:

`for` _ `in` range(10): `pass`

¹ 即根据里氏替换原则,这是面向对象编程的核心概念之一。

² 首次引入时,defaultdict(int)常用于维护项目计数。由于 Counter 现在是 collections 模块的一部分,对于特定的项目计数任务,使用 Counter 而不是 defaultdict(int)。

³ 对于后进先出(LIFO)队列,又称为“堆栈”,列表及其附加和弹出方法完全足够。

⁴ 也称为施瓦茨变换

⁵ 这一步不完全是“排序”,但看起来足够接近,至少如果你眯起眼睛看的话。

⁶ 一些专家建议from itertools import *,但本书的作者持不同意见。

第九章:字符串和其他内容

Python 的 str 类型实现了 Unicode 文本字符串,支持运算符、内置函数、方法和专用模块。有些相似的 bytes 类型表示任意二进制数据作为一系列字节,也称为bytestringbyte string。可以对这两种类型的对象进行许多文本操作:由于这些类型是不可变的,方法大多数情况下会创建并返回一个新的字符串,除非返回原始字符串未更改。可变字节序列可以表示为 bytearray,在“bytearray objects”中简要介绍。

本章首先介绍了这三种类型可用的方法,然后讨论了字符串模块和字符串格式化(包括格式化字符串字面值),接着是 textwrap、pprint 和 reprlib 模块。专门涉及 Unicode 的问题在本章末尾讨论。

字符串对象的方法

str、bytes 和 bytearray 对象都是序列,如“Strings”中所述;其中只有 bytearray 对象是可变的。所有不可变序列操作(重复、连接、索引和切片)都适用于这三种类型的实例,并返回相同类型的新对象。除非在 Table 9-1 中另有规定,否则方法适用于这三种类型的对象。str、bytes 和 bytearray 对象的大多数方法返回相同类型的值,或者专门用于在表示之间转换。

术语如“letters”、“whitespace”等,指的是字符串模块的相应属性,在接下来的章节中会详细介绍。虽然 bytearray 对象是可变的,但是返回 bytearray 结果的方法不会改变对象,而是返回一个新的 bytearray,即使结果与原始字符串相同。

为了简洁起见,在下表中,术语 bytes 指代 bytes 和 bytearray 对象。但是在混合使用这两种类型时要小心:虽然它们通常是可互操作的,但结果的类型通常取决于操作数的顺序。

在 Table 9-1 中,由于 Python 中的整数值可以任意大,为了简洁起见,我们使用 sys.maxsize 表示整数默认值,实际上意味着“无限大的整数”。

表 9-1。重要的 str 和 bytes 方法

capitalize s.capitalize() 返回s的副本,其中第一个字符(如果是字母)大写,其余字符(如果有)小写。
casefold s.casefold() str only. 返回按照Unicode 标准第 3.13 节描述的算法处理过的字符串。这类似于后面在本表中描述的s.lower,但还考虑到例如德语中的 'ß' 和 'ss' 之间的等价性,因此在处理可以包含不止基本 ASCII 字符的文本时更为适用。
center s.center(n, fillchar=' ', /) 返回长度为 max(len(s), n)的字符串,其中s的副本位于中心部分,两侧分别用相同数量的字符fillchar填充。默认的fillchar是空格字符。例如,'ciao'.center(2)是'ciao','x'.center(4, '')是'x'。
count s.count(sub, start=0, end=sys.maxsize, /) 返回在s[start:end]中子字符串sub的非重叠出现次数。
decode s.decode(encoding='utf-8', errors='strict') bytes only. 根据给定的编码从字节s解码为 str 对象。errors 参数指定如何处理解码错误:'strict'会导致错误引发 UnicodeError 异常;'ignore'会忽略格式错误的值;'replace'会用问号替换它们(详见“Unicode”)。其他值可以通过 codecs.register_error 注册,见表 9-10。
encode s.encode(encoding='utf-8', errors='strict') str only. 返回从 str s按照给定编码和错误处理获得的 bytes 对象。详见“Unicode”了解更多详情。
endswith s.endswith(suffix, start=0, end=sys.maxsize, /) 当s[start:end]以字符串suffix结尾时返回True;否则返回Falsesuffix可以是字符串元组,此时当s[start:end]以元组中任一字符串结尾时返回True
expandtabs s.expandtabs(tabsize=8) 返回一个将每个制表符字符更改为一个或多个空格字符的副本,其中每tabsize个字符设置一个制表位。
find s.find(sub, start=0, end=sys.maxsize, /) 返回在s[start:end]中找到子字符串sub的最低索引,其中sub完全包含在内。例如,'banana'.find('na')返回 2,'banana'.find('na', 1)也返回 2,而'banana'.find('na', 3)返回 4,'banana'.find('na', -2)也返回 4。如果未找到sub,则返回-1。
format s.format(args, **kwargs) str only. 根据字符串s*中包含的格式说明,格式化位置和命名参数。详见“字符串格式化”了解更多详情。
format_map s.format_map(mapping) 仅限 str。根据字符串 s 中包含的格式化指令格式化映射参数。等同于 s.format(**mapping),但直接使用映射。详情请参见“字符串格式化”。
index s.index(sub, start=0, end=sys.maxsize, /) 类似于 find,但是当找不到 sub 时会引发 ValueError。
isalnum s.isalnum() 当 s 的长度大于 0 且 s 中的所有字符都是 Unicode 字母或数字时返回True。当 s 为空或者 s 中至少有一个字符既不是字母也不是数字时返回False
isalpha s.isalpha() 当 s 的长度大于 0 且 s 中的所有字符都是字母时返回True。当 s 为空或者 s 中至少有一个字符不是字母时返回False
isascii s.isascii() 当字符串为空或字符串中的所有字符都是 ASCII 时返回True,否则返回False。ASCII 字符的码点范围在 U+0000–U+007F。
isdecimal s.isdecimal() 仅限 str。当 s 的长度大于 0 且 s 中的所有字符都可用于形成十进制数时返回True。这包括被定义为阿拉伯数字的 Unicode 字符。^(a)
isdigit s.isdigit() 当 s 的长度大于 0 且 s 中的所有字符都是 Unicode 数字时返回True。当 s 为空或者 s 中至少有一个字符不是 Unicode 数字时返回False
isidentifier s.isidentifier() 仅限 str。根据 Python 语言的定义,当 s 是有效的标识符时返回True;关键字也满足该定义,因此,例如 'class'.isidentifier() 返回True
islower s.islower() 当 s 中的所有字母都是小写字母时返回True。当 s 不包含字母或者 s 中至少有一个大写字母时返回False
isnumeric s.isnumeric() 仅限 str。类似于 s.isdigit(),但使用了更广泛的数字符号定义,包括 Unicode 标准中定义的所有数字符号(如分数)。
isprintable s.isprintable() 仅限 str。当 s 中的所有字符都是空格 ('\x20') 或在 Unicode 标准中定义为可打印字符时返回True。因为空字符串不包含不可打印字符,''.isprintable() 返回True
isspace s.isspace() 当 s 的长度大于 0 且 s 中的所有字符都是空白字符时返回True。当 s 为空或者 s 中至少有一个字符不是空白字符时返回False
istitle s.istitle() 返回 True 当字符串 stitlecased:即每个连续字母序列的开头都是大写字母,其他字母都是小写字母时(例如,'King Lear'.istitle() 返回 True)。 当 s 不包含字母,或者 s 的至少一个字母不符合标题大小写条件时,istitle 返回 False(例如,'1900'.istitle() 和 'Troilus and Cressida'.istitle() 返回 False)。
isupper s.isupper() 返回 Trues 中所有字母都是大写时。 当 s 不包含字母,或者 s 的至少一个字母是小写时,isupper 返回 False
join s.join(seq, /) 返回由 seq 中的项目连接而成的字符串,每个项目之间由 s 的副本分隔(例如,''.join(str(x) for x in range(7)) 返回 '0123456','x'.join('aeiou') 返回 'axexixoxu')。
ljust s.ljust(n, fillchar=' ', /) 返回长度为 max(len(s),n) 的字符串,其中以 fillchar 字符填充的 s 的副本在开头,后跟零个或多个尾随 fillchar 的副本。
lower s.lower() 返回 s 的副本,其中所有字母(如果有)都转换为小写。
lstrip s.lstrip(x=string.whitespace, /) 返回删除字符串 x 中任何前导字符后的 s 的副本。 例如,'banana'.lstrip('ab') 返回 'nana'。
removeprefix s.removeprefix(prefix, /) 3.9+ 当 sprefix 开头时,返回 s 的剩余部分;否则返回 s
removesuffix s.removesuffix(suffix, /) 3.9+ 当 ssuffix 结尾时,返回 s 的剩余部分;否则返回 s
replace s.replace(old, new, count=sys.maxsize, /) 返回一个副本,其中第一个 count (或更少,如果更少)非重叠出现的子字符串 old 被字符串 new 替换(例如,'banana'.replace('a', 'e', 2) 返回 'benena')。
rfind s.rfind(sub, start=0, end=sys.maxsize, /) 返回 s 中子字符串 sub 的最高索引,使得 sub 完全包含在 s[start:end] 中。 如果未找到 sub,则 rfind 返回 -1。
rindex s.rindex(sub, start=0, end=sys.maxsize, /) 类似于 rfind,但如果未找到 sub,则引发 ValueError。
rjust s.rjust(n, fillchar=' ', /) 返回长度为 max(len(s),n) 的字符串,其中以 fillchar 字符填充的 s 的副本在末尾,前面跟零个或多个前导 fillchar 的副本。
rstrip s.rstrip(x=string.whitespace, /) 返回 s 的副本,删除在字符串 x 中找到的尾部字符。 例如,'banana'.rstrip('ab') 返回 'banan'。

| split | s.split(sep=None, maxsplit=sys.maxsize) 返回一个最多包含 maxsplit+1 个字符串的列表 LL 的每个项是 s 的一个“单词”,其中字符串 sep 分隔单词。当 s 的单词数大于 maxsplit 时,L 的最后一个项是 s 中跟随第一个 maxsplit 个单词之后的子字符串。当 sep 为 None 时,任何空白字符串分隔单词(例如,'four score and seven years'.split(None, 3) 返回 ['four', 'score', 'and', 'seven years'])。

注意在使用 None(任何连续的空白字符作为分隔符)和 ' '(每个单独空格字符作为分隔符,不包括制表符和换行符,也不包括空格字符串)之间的区别。例如:

>>> x = 'a  bB'  *`# two spaces between a and bB`*
>>> x.split()    *`# or x.split(`**`None`**`)`*
['a', 'bB']
>>> x.split(' ')
['a', '', 'bB']

在第一种情况下,中间的两个空格被视为单一分隔符;在第二种情况下,每个单独的空格被视为分隔符,因此在两个空格之间有一个空字符串。 |   |

splitlines s.splitlines(keepends=False) 类似于 s.split('\n')。但是,当 keepends 为 True 时,结果列表中每个项的末尾 '\n' 也包括在内(如果 s 不以 '\n' 结尾,则最后一个项除外)。
startswith s.startswith(prefix, start=0, end=sys.maxsize, /) 当 s[start:end] 以字符串 prefix 开头时返回 True;否则返回 Falseprefix 可以是字符串元组,此时如果 s[start:end] 以其中任何一个字符串开头,则返回 True
strip s.strip(x=string.whitespace, /) 返回 s 的副本,删除开头和结尾处位于字符串 x 中的字符。例如,'banana'.strip('ab') 返回 'nan'。
swapcase s.swapcase() 返回 s 的副本,所有大写字母转换为小写字母,所有小写字母转换为大写字母。
title s.title() 返回 s 的副本,转换为标题格式:每个连续字母序列的开头字母大写,其余字母(如果有)小写。

| translate | s.translate(table, /, delete*=*b'') 返回 s 的副本,其中 table 中的字符被翻译或删除。当 s 是字符串时,不能传递 delete 参数;table 是一个字典,其键是 Unicode 码点,值可以是 Unicode 码点、Unicode 字符串或 None(表示删除对应的字符)。例如:

tbl = {ord('a'):`None`, ord('n'):'ze'}
print('banana'.translate(tbl))  *`# prints:`* *`'bzeze'`*

s 是字节时,table 是长度为 256 的字节对象;s.translate(t, b) 的结果是一个字节对象,其中的每个项 b 如果是 delete 的项之一,则被省略,否则改为 t[ord(b)]。

bytes 和 str 各自有一个名为 maketrans 的类方法,可用于构建适合于相应 translate 方法的表。 |   |

upper s.upper() 返回 s 的副本,所有字母(如果有)都转换为大写。
^(a) 这不包括用作基数的标点符号,例如句点(.)或逗号(,)。

字符串模块

字符串模块提供了几个有用的字符串属性,列在 Table 9-2 中。

Table 9-2. 字符串模块中的预定义常量

ascii_letters 包含 ascii_lowercase 和 ascii_uppercase 这两个常量的字符串(将下列两个常量连接在一起)。
ascii_lowercase 字符串 'abcdefghijklmnopqrstuvwxyz'
ascii_uppercase 字符串 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
digits 字符串 '0123456789'
hexdigits 字符串 '0123456789abcdefABCDEF'
octdigits 字符串 '01234567'
punctuation 字符串 '!"#$%&'()*+,-./:;<=>?@[]^_'{|}~'(即在 C 区域中被视为标点字符的所有 ASCII 字符;不依赖于活动的区域设置)
printable 包含所有被认为是可打印字符的 ASCII 字符的字符串(即数字、字母、标点符号和空白字符)。
whitespace 包含所有被认为是空白字符的 ASCII 字符的字符串:至少包括空格、制表符、换行符和回车符,但可能会根据当前的区域设置包含更多字符(例如某些控制字符)。

不应重新绑定这些属性;这样做的效果是未定义的,因为 Python 库的其他部分可能依赖于它们。

字符串模块还提供了 Formatter 类,在下一节中介绍。

字符串格式化

Python 提供了一种灵活的机制来格式化字符串(但不适用于字节串:关于字节串,请参见 “使用 % 进行旧式字符串格式化”)。格式字符串 简单地是一个包含用大括号()括起来的 替换字段 的字符串,由 值部分、可选的 转换部分 和可选的 格式说明符 组成:

{*`value``-``part`*[!*`conversion``-``part`*][:*`format``-``specifier`*]}

值部分根据字符串类型而异:

  • 对于格式化字符串字面量或 f-strings,值部分会作为 Python 表达式进行求值(有关详细信息,请参见后续章节);表达式不能以感叹号结尾。

  • 对于其他字符串,值部分选择一个参数或参数的一个元素用于格式方法。

可选的转换部分是感叹号(!)后跟一个字符 s、r 或 a(在 “值转换” 中描述)。

可选的格式说明符以冒号(:)开头,并确定如何在格式字符串中替换字段的原始替换值。

格式化字符串字面量(F-Strings)

此特性允许您插入需要内插的值,用大括号括起来。要创建格式化字符串字面量,请在字符串的开头引号之前加上 f(这就是它们被称为 f-strings 的原因),例如 f'':

>>> name = 'Dawn'
>>> print(f'{name!r} is {len(name)} characters long')
'Dawn' is 4 characters long

您可以使用嵌套的大括号来指定格式表达式的组件:

>>> `for` width `in` 8, 11:
...     `for` precision `in` 2, 3, 4, 5:
...         print(f'{2.7182818284:{width}.{precision}}')
...
 2.7
 2.72
 2.718
 2.7183
 2.7
 2.72
 2.718
 2.7183

我们已经尝试更新书中的大多数示例以使用 f-strings,因为它们是 Python 中格式化字符串的最紧凑方式。但请记住,这些字符串文字并不是常量——它们在每次包含它们的语句执行时都会计算,这会涉及运行时开销。

在格式化字符串文字中的要格式化的值已经被引号包围:因此,在使用包含字符串引号的值部分表达式时要注意避免语法错误。使用四种不同的字符串引号以及能够使用转义序列,大多数情况都是可能的,尽管可读性可能会受到影响。

F-Strings 对国际化没有帮助

给定一个需要适应多种语言内容的格式,最好使用格式化方法,因为要插值的值可以在提交格式化之前独立计算。

使用 f-strings 进行调试打印

3.8+ 为了便于调试,在格式化字符串文字中的值表达式的最后一个非空字符后面可以跟一个等号(=),可选地包含空格。在这种情况下,表达式本身的文本和等号,包括任何前导和尾随空格,在值之前输出。在等号存在的情况下,当没有指定格式时,Python 使用值的 repr() 作为输出;否则,Python 使用值的 str(),除非指定了 !r 值转换:

>>> a = '*-'
>>> s = 12
>>> f'{a*s=}'
"a*s='*-*-*-*-*-*-*-*-*-*-*-*-'"
>>> f'{a*s = :30}'
'a*s = *-*-*-*-*-*-*-*-*-*-*-*-      '

请注意,此形式仅在格式化字符串文字中可用。

这里是一个简单的 f-string 示例。请注意,包括任何周围的文本,包括任何空白字符,在结果中都会字面复制:

>>> n = 10
>>> s = ('zero', 'one', 'two', 'three')
>>> i = 2
>>> f'start {"-"*n} : {s[i]} end'
'start ---------- : two end'

使用格式化调用格式化

在格式化字符串文字中可用的相同格式化操作也可以通过调用字符串的 format 方法执行。在这些情况下,替换字段以选择该调用的参数的值部分开始。您可以指定位置参数和命名参数。以下是一个简单的 format 方法调用示例:

>>> name = 'Dawn'
>>> print('{name} is {n} characters long'
... .format(name=name, n=len(name)))
'Dawn' is 4 characters long
>>> "This is a {1}, {0}, type of {type}".format("green", "large", 
...                                             type="vase")
'This is a large, green, type of vase'

简单起见,此示例中的替换字段均不包含转换部分或格式说明符。

如前所述,使用格式化方法时的参数选择机制可以处理位置参数和命名参数。最简单的替换字段是空括号对(),表示自动位置参数指定器。每个这样的替换字段自动引用下一个要格式化的位置参数的值:

>>> 'First: {} second: {}'.format(1, 'two')
'First: 1 second: two'

要重复选择参数或者按顺序使用参数,请使用编号的替换字段来指定参数在参数列表中的位置(从零开始计数):

>>> 'Second: {1}, first: {0}'.format(42, 'two')
'Second: two, first: 42'

您不能混合自动和编号的替换字段:这是二选一的选择。

对于命名参数,请使用参数名称。如果需要,可以将它们与(自动或编号)位置参数混合使用:

>>> 'a: {a}, 1st: {}, 2nd: {}, a again: {a}'.format(1, 'two', a=3)
'a: 3, 1st: 1, 2nd: two, a again: 3'
>>> 'a: {a} first:{0} second: {1} first: {0}'.format(1, 'two', a=3)
'a: 3 first:1 second: two first: 1'

如果参数是一个序列,则可以使用数值索引来选择参数的特定元素作为要格式化的值。这适用于位置(自动或编号)和命名参数:

>>> 'p0[1]: {[1]} p1[0]: {[0]}'.format(('zero', 'one'),
...                                    ('two', 'three'))
'p0[1]: one p1[0]: two'
>>> 'p1[0]: {1[0]} p0[1]: {0[1]}'.format(('zero', 'one'),
...                                      ('two', 'three'))
'p1[0]: two p0[1]: one'
>>> '{} {} {a[2]}'.format(1, 2, a=(5, 4, 3))
'1 2 3'

如果参数是一个复合对象,则可以通过将属性访问点符号应用于参数选择器来选择其各个属性作为要格式化的值。以下是使用复数的示例,复数具有分别保存实部和虚部的 real 和 imag 属性:

>>> 'First r: {.real} Second i: {a.imag}'.format(1+2j, a=3+4j)
'First r: 1.0 Second i: 4.0'

索引和属性选择操作可以根据需要多次使用。

值转换

您可以通过在选择器后跟!s 应用对象的 str 方法,!r 应用其 repr 方法,或者使用 ascii 内置!a 来为值应用默认转换:

>>> "String: {0!s} Repr: {0!r} ASCII: {0!a}".format("banana 😀")
"String: banana 😀 Repr: 'banana 😀' ASCII: 'banana\\U0001f600'"

当存在转换时,将在格式化之前应用该转换。由于同一值需要多次使用,在此示例中,格式调用比格式化字符串文字更为合理,后者需要重复三次相同的值。

值格式化:格式说明符

替换字段的最后(可选)部分称为格式说明符,由冒号(:)引入,提供(可能转换后的)值的进一步所需格式化。如果在替换字段中没有冒号,则意味着使用转换后的值(如果尚未以字符串形式表示)而不进行进一步格式化。如果存在格式说明符,则应按以下语法提供:

[[*`fill`*]*`align`*][*`sign`*][z][#][0][*`width`*][*`grouping_option`*][.*`precision`*][*`type`*]

详细信息在以下各小节中提供。

填充和对齐

默认填充字符为空格。要使用替代填充字符(不能是开括号或闭括号),请以填充字符开头。填充字符(如果有)应跟随对齐指示器(见表 9-3)。

表 9-3. 对齐指示器

字符 作为对齐指示器的重要性
'<' 将值左对齐在字段内
'>' 将值右对齐在字段内
'^' 将值居中对齐在字段内
'=' 仅适用于数值类型:在符号和数值的第一个数字之间添加填充字符

如果第一个和第二个字符都是有效的对齐指示器,则第一个用作填充字符,第二个用于设置对齐方式。

当未指定对齐时,除数字外的值均左对齐。除非稍后在格式说明符中指定字段宽度(见“字段宽度”),否则不添加填充字符,无论填充和对齐如何:

>>> s = 'a string'
>>> f'{s:>12s}'
'    a string'
>>> f'{s:>>12s}'
'>>>>a string'
>>> f'{s:><12s}'
'a string>>>>'

符号指示

仅适用于数值的情况下,您可以通过包含符号指示器来区分正负数(详见表 9-4)。

表 9-4. 符号指示器

字符 作为符号指示器的重要性
'+' 对于正数,插入+作为符号;对于负数,插入-作为符号
'-' 对于负数,插入-作为符号;对于正数,不插入任何符号(如果未包含符号指示符,则为默认行为)
' ' 对于正数,插入一个空格作为符号;对于负数,插入-作为符号

空格是默认的符号指示符。如果指定了填充,则会出现在符号和数值之间;在=之后放置符号指示符以避免其被用作填充字符:

>>> n = -1234
>>> f'{n:12}'    *`# 12 spaces before the number`*
'       -1234'
>>> f'{-n:+12}'  *`# - to flip n's sign, + as sign indicator`* 
'       +1234'
>>> f'{n:+=12}'  *`# + as fill character between sign and number`*
'-+++++++1234'
*`# + as sign indicator, spaces fill between sign and number`*
>>> f'{n:=+12}'
'-       1234'
*`# * as fill between sign and number, + as sign indicator`*
>>> f'{n:*=+12}'
'-*******1234'

零归一化(z)

3.11+某些数字格式能够表示负零,这往往是一个令人惊讶且不受欢迎的结果。当在格式说明符中的这个位置出现 z 字符时,这样的负零将被规范化为正零:

>>> x = -0.001
>>> f'{x:.1f}'
'-0.0'
>>> f'{x:z.1f}'
'0.0'
>>> f'{x:+z.1f}'
'+0.0'

基数指示符(#)

仅适用于数值整数格式,您可以包含基数指示符,即#字符。如果存在,则表示应在二进制格式化的数字的数字之前加上'0b',在八进制格式化的数字的数字之前加上'0o',在十六进制格式化的数字的数字之前加上'0x'。例如,'{23:x}'为'17',而'{23:#x}'为'0x17',清楚地标识出值为十六进制。

前导零指示符(0)

数值类型,当字段宽度以零开头时,数值将使用前导零而不是前导空格进行填充:

>>> f"{-3.1314:12.2f}"
'       -3.13'
>>> f"{-3.1314:012.2f}"
'-00000003.13'

字段宽度

你可以指定要打印的字段的宽度。如果指定的宽度小于值的长度,则使用值的长度(但对于字符串值,请参见下一节“精度规范”)。如果未指定对齐方式,则该值为左对齐(但对于数字,则为右对齐):

>>> s = 'a string'
>>> f'{s:¹²s}'
'  a string  '
>>> f'{s:.>12s}'
'....a string'

使用嵌套大括号时,调用格式方法,字段宽度也可以是格式参数:

>>> '{:.>{}s}'.format(s, 20)
'............a string'

有关此技术的更详细讨论,请参阅“嵌套格式规范”。

分组选项

对于十进制(默认)格式类型中的数值,您可以插入逗号()或下划线(_)来请求结果整数部分的每个三位数(数字组)之间用该字符分隔。例如:

>>> f'{12345678.9:,}'
'12,345,678.9'

此行为忽略了系统区域设置;对于区域设置感知的数字分组和小数点字符使用,请参阅表 9-5 中的格式类型 n。

精度规范

精度(例如,.2)对于不同的格式类型具有不同的含义(有关详细信息,请参见下一小节),大多数数字格式的默认值为.6。对于 f 和 F 格式类型,它指定应在格式化时四舍五入的小数点后的位数;对于 g 和 G 格式类型,它指定应四舍五入的有效数字的数量;对于非数值的值,它指定在格式化之前将值截断为其最左边的字符。例如:

>>> x = 1.12345
>>> f'as f: {x:.4f}'  *`# rounds to 4 digits after decimal point`*
'as f: 1.1235'
>>> f'as g: {x:.4g}'  *`# rounds to 4 significant digits`*
'as g: 1.123'
>>> f'as s: {"1234567890":.6s}'  *`# string truncated to 6 characters`*
'as s: 123456'

格式类型

格式规范以可选的格式类型结束,该类型确定如何在给定宽度和给定精度下表示该值。如果没有显式指定格式类型,则正在格式化的值确定默认格式类型。

s 格式类型始终用于格式化 Unicode 字符串。

整数数值具有一系列可接受的格式类型,在表 9-5 中列出。

表 9-5. 整数格式类型

格式类型 格式说明
b 二进制格式—一系列 1 和 0
c 其序数值是格式化值的 Unicode 字符
d 十进制(默认格式类型)
n 十进制格式,使用系统区域设置时的区域特定分隔符(在英国和美国为逗号)
o 八进制格式—一系列八进制数字
x 或 X 十六进制格式—一系列十六进制数字,相应的字母为小写或大写

浮点数具有不同的格式类型集,如表 9-6 所示。

表 9-6. 浮点格式类型

格式类型 格式说明
e 或 E 指数格式—科学记数法,整数部分介于一到九之间,指数之前使用 e 或 E
f 或 F 固定点格式,其中无穷大(inf)和非数字(nan)为小写或大写
g 或 G 通用格式(默认格式类型)—在可能的情况下使用固定点格式,否则使用指数格式;使用小写或大写表示 e、inf 和 nan,具体取决于格式类型的大小写
n 类似于通用格式,但在设置系统区域设置时使用区域特定分组的分隔符,用于每三位数字和小数点
% 百分比格式—将值乘以 100,并将其格式化为固定点,后跟%

当未指定格式类型时,浮点数使用 g 格式,小数点后至少有一位数字,默认精度为 12。

下面的代码接受一个数字列表,并将每个数字右对齐在九个字符宽的字段中;指定每个数字的符号始终显示,并在每组三位数字之间添加逗号,并将每个数字四舍五入到小数点后两位,根据需要将整数转换为浮点数:

>>> `for` num `in` [3.1415, -42, 1024.0]:
...     f'{num:>+9,.2f}' 
...
'    +3.14'
'   -42.00'
'+1,024.00'

嵌套格式规范

在某些情况下,您可能希望使用表达式值来帮助确定所使用的精确格式:您可以使用嵌套格式化来实现这一点。例如,要将字符串格式化为比字符串本身宽四个字符的字段,可以将一个宽度值传递给格式化,如下所示:

>>> s = 'a string'
>>> '{0:>{1}s}'.format(s, len(s)+4)
'    a string'
>>> '{0:_^{1}s}'.format(s, len(s)+4)
'__a string__'

通过仔细设置宽度规范和嵌套格式化,您可以将一系列元组打印成对齐的列。例如:

`def` columnar_strings(str_seq, widths):
    `for` cols `in` str_seq:
        row = [f'{c:{w}.{w}s}'
               `for` c, w `in` zip(cols, widths)]
        print(' '.join(row))

鉴于此函数,以下代码:

c = [
        'four score and'.split(),
        'seven years ago'.split(),
        'our forefathers brought'.split(),
        'forth on this'.split(),
    ]

columnar_strings(c, (8, 8, 8))

输出:

four     score    and
seven    years    ago
our      forefath brought
forth    on       this

用户编码类的格式化

最终,值通过调用其 format 方法并使用格式说明符作为参数进行格式化。内置类型要么实现自己的方法,要么继承自 object,其不太有用的 format 方法只接受空字符串作为参数:

>>> object().__format__('')
'<object object at 0x110045070>'
>>> import math
>>> math.pi.__format__('18.6')
'           3.14159'

您可以利用这些知识实现自己的完全不同的格式化迷你语言,如果您愿意的话。下面的简单示例演示了格式规范的传递和(恒定的)格式化字符串结果的返回。格式规范的解释由您控制,您可以选择实现任何格式化标记法:

>>> class S:
...     def __init__(self, value):
...         self.value = value
...     `def` __format__(self, fstr):
...         `match` fstr:
...             `case` 'U':
...                 `return` self.value.upper()
...             `case` 'L':
...                 `return` self.value.lower()
...             `case` 'T':
...                 `return` self.value.title()
...             `case` _:
...                 `return` ValueError(f'Unrecognized format code'
...                                   f' {fstr!r}')
>>> my_s = S('random string')
>>> f'{my_s:L}, {my_s:U}, {my_s:T}'
'random string, RANDOM STRING, Random String'

format 方法的返回值被替换为格式化输出中的替换字段,允许对格式字符串进行任何所需的解释。

此技术在 datetime 模块中使用,以允许使用 strftime 风格的格式字符串。因此,以下所有方式都会得到相同的结果:

>>> `import` datetime
>>> d = datetime.datetime.now()
>>> d.__format__('%d/%m/%y')
'10/04/22'
>>> '{:%d/%m/%y}'.format(d)
'10/04/22'
>>> f'{d:%d/%m/%y}'
'10/04/22'

为了更轻松地格式化对象,字符串模块提供了一个 Formatter 类,具有许多有用的方法来处理格式化任务。详细信息请参阅 在线文档

使用 % 进行遗留字符串格式化

Python 中的一个遗留字符串格式化表达式的语法是:

*`format`* % *`values`*

其中 format 是一个包含格式说明符的 str、bytes 或 bytearray 对象,而 values 是要格式化的值,通常作为一个元组。¹ 与 Python 的较新格式化功能不同,您也可以使用 % 格式化来处理 bytes 和 bytearray 对象,而不仅仅是 str 对象。

在日志记录中的等效用法,例如:

logging.info(*`format`*, **`values`*)

values 是在 format 后作为位置参数传入的。

遗留字符串格式化方法大致具有与 C 语言的 printf 相同的功能集,并以类似的方式运行。每个格式说明符都是以百分号(%)开头,并以 表 9-7 中显示的转换字符之一结束。

表 9-7. 字符串格式转换字符

字符 输出格式 注释
d, i 有符号十进制整数 值必须是一个数字
u 无符号十进制整数 值必须是一个数字
o 无符号八进制整数 值必须是一个数字
x 无符号十六进制整数(小写字母) 值必须是一个数字
X 无符号十六进制整数(大写字母) 值必须是一个数字
e 浮点数以指数形式表示(指数部分小写 e) 值必须是一个数字
E 浮点数以指数形式表示(指数部分大写 E) 值必须是一个数字
f, F 浮点数以十进制形式表示 值必须是一个数字
g, G exp >=4 或 < 精度时,类似于 e 或 E;否则类似于 f 或 F exp 是被转换的数的指数部分
a 字符串 使用 ascii 转换任何值
r 字符串 使用 repr 转换任何值
s 字符串 使用 str 转换任意值
% 百分号字符 不消耗任何值

日志模块最常使用的是 a、r 和 s 转换字符。在%和转换字符之间,可以指定一系列可选的修饰符,稍后我们将讨论。

格式表达式记录的内容是format,其中每个格式规范都会被values的相应项替换,并根据规范转换为字符串。以下是一些简单的示例:

`import` logging
logging.getLogger().setLevel(logging.INFO)
x = 42
y = 3.14
z = 'george'
logging.info('result = %d', x)        *`# logs:`* *`result = 42`*
logging.info('answers: %d %f', x, y)  *`# logs:`* *`answers: 42 3.140000`*
logging.info('hello %s', z)           *`# logs:`* *`hello george`*

格式规范语法

每个格式规范按位置对应于values中的一项。格式规范可以包括修改器,以控制如何将values中的对应项转换为字符串。格式规范的组成部分依次为:

  • 指示转换规范开始的强制前导百分号字符

  • 零个或多个可选的转换标志:

    '#'

    转换使用一个备用形式(如果其类型存在的话)。

    '0'

    转换进行零填充。

    '-'

    转换左对齐。

    ' '

    负数带有符号,正数前有一个空格。

    '+'

    数字前会放置一个符号(+或-)。

  • 可选的转换最小宽度:一个或多个数字,或者一个星号(),表示宽度从values*的下一项获取

  • 可选的转换精度:点(.)后跟零个或多个数字或*,表示精度从values的下一项获取

  • 来自表格 9-7 的强制转换类型

values必须与format的规范数量完全相同(对于由给出的宽度或精度,额外消耗一个values*中的项,该项必须是整数,并且用作该转换的宽度或精度的字符数)。

始终使用%r(或%a)来记录可能错误的字符串

大多数情况下,format字符串中的格式规范都将是%s;偶尔,您可能希望确保输出的水平对齐(例如,在六个字符的右对齐、可能被截断的空间中),在这种情况下,您将使用%6.6s。但是,对于%r 或%a,有一个重要的特殊情况。

当您记录可能错误的字符串值(例如,找不到的文件名)时,请不要使用%s:当错误是字符串具有不必要的前导或尾随空格,或者包含一些非打印字符如\b 时,%s 可能会使您通过研究日志难以发现。相反,请使用%r 或%a,以便所有字符都清晰显示,可能通过转义序列。 (对于 f-strings,相应的语法将是{variable!r}或{variable!a})。

文本包装和填充

textwrap 模块提供一个类和几个函数,以给定的最大长度将字符串格式化为多行。要微调填充和换行效果,可以实例化 textwrap 提供的 TextWrapper 类并应用详细控制。然而,大多数情况下,textwrap 提供的函数足够使用;最常用的函数在表 9-8 中介绍。

表 9-8. textwrap 模块的常用函数

dedent dedent(text) 函数接受一个多行字符串,并返回一个去除了所有行相同数量前导空白的副本,使得某些行没有前导空白。
fill fill(text, width=70) 返回一个等于 '\n'.join(wrap(text, width)) 的多行字符串。
wrap wrap(text, width=70) 返回一个字符串列表(不带结束换行符),每个字符串的长度不超过 width 个字符。wrap 还支持其他命名参数(相当于 TextWrapper 类实例的属性);对于这些高级用法,请参阅在线文档

pprint 模块

pprint 模块可以对数据结构进行漂亮打印,其格式化比内置函数 repr 更易读(详见表 8-2)。要微调格式,可以实例化 pprint 提供的 PrettyPrinter 类,并应用详细控制,辅助函数也由 pprint 提供。然而,大多数情况下,pprint 提供的函数足够使用(参见表 9-9)。

表 9-9. pprint 模块的常用函数

pformat pformat(object) 返回表示对象的漂亮打印的字符串。

| pp, pprint | pp(object, stream=sys.stdout), pprint(object, stream=sys.stdout) |

将对象的漂亮打印输出到打开写入文件对象流中,并以换行符结尾。

以下语句实际上完成了相同的操作:

print(pprint.pformat(x))
pprint.pprint(x)

在许多情况下,这两个构造在效果上基本相同于 print(x) —— 例如,对于可以在单行内显示的容器。但是,对于像 x=list(range(30)) 这样的情况,print(x) 会在 2 行显示 x,在任意点断开,而使用 pprint 模块会将 x 按每项一行的方式展示出来。在希望使用模块特定的显示效果而不是正常字符串表示的情况下,请使用 pprint。

pprint 和 pp 支持额外的格式化参数;详细信息请参阅在线文档。 |

reprlib 模块

reprlib 模块提供了一个替代内置函数 repr(在表格 8-2 中介绍),用于表示字符串的长度限制。要微调长度限制,您可以实例化或子类化 reprlib 模块提供的 Repr 类,并应用详细控制。然而,大多数情况下,模块公开的唯一函数就足够了:repr(obj),它返回表示obj的字符串,具有合理的长度限制。

Unicode

要将字节串转换为 Unicode 字符串,请使用字节串的 decode 方法(参见表格 9-1)。转换必须始终是显式的,并使用称为编解码器(缩写为编码器-解码器)的辅助对象执行。编解码器还可以使用字符串的 encode 方法将 Unicode 字符串转换为字节串。要标识编解码器,请将编解码器名称传递给 decode 或 encode。当您不传递编解码器名称时,Python 使用默认编码,通常为'utf-8'。

每次转换都有一个参数错误,一个字符串指定如何处理转换错误。合理地,默认值为'strict',意味着任何错误都会引发异常。当错误为'replace'时,转换将在字节串结果中用'?'替换导致错误的每个字符,在 Unicode 结果中用 u'\ufffd'替换。当错误为'ignore'时,转换会默默地跳过导致错误的字符。当错误为'xmlcharrefreplace'时,转换会将导致错误的每个字符替换为 XML 字符引用表示形式的结果中。您可以编写自己的函数来实现转换错误处理策略,并通过调用 codecs.register_error 并在接下来的部分的表格中覆盖的适当名称下注册它。

编解码器模块

编解码器名称到编解码器对象的映射由 codecs 模块处理。此模块还允许您开发自己的编解码器对象并注册它们,以便可以像内置编解码器一样按名称查找它们。它提供一个函数,让您可以显式查找任何编解码器,获取编码和解码使用的函数,以及用于包装类似文件对象的工厂函数。这些高级功能很少使用,我们在本书中不涵盖它们。

codecs 模块与 Python 标准库的编码包一起提供了对处理国际化问题有用的内置编解码器。Python 自带超过 100 种编解码器;您可以在在线文档中找到完整列表,并对每种编解码器进行简要解释。在模块 sitecustomize 中安装编解码器作为全局默认不是良好的做法;相反,推荐的用法是每次在字节和 Unicode 字符串之间转换时始终通过名称指定编解码器。Python 的默认 Unicode 编码是'utf-8'。

codecs 模块为大多数 ISO 8859 编码提供了 Python 实现的编解码器,编解码器名称从'iso8859-1'到'iso8859-15'。在西欧地区流行的编解码器是'latin-1',它是 ISO 8859-1 编码的快速内置实现,提供了一个每字符一个字节的编码,包括西欧语言中的特殊字符(注意,它缺少欧元货币符号'€';但如果需要该符号,请使用'iso8859-15')。仅在 Windows 系统上,名为'mbcs'的编解码器包装了平台的多字节字符集转换过程。codecs 模块还提供了各种代码页,名称从'cp037'到'cp1258',以及 Unicode 标准编码'utf-8'(可能是最佳选择,因此推荐使用,也是默认的)和'utf-16'(具有特定的大端和小端变体:'utf-16-be'和'utf-16-le')。对于 UTF-16 的使用,codecs 还提供了属性 BOM_BE 和 BOM_LE,分别用于大端和小端机器的字节顺序标记,以及 BOM,当前平台的字节顺序标记。

除了用于更高级用途的各种函数外,如前所述,codecs 模块还提供了一个函数,允许您注册自己的转换错误处理函数:

regis⁠t⁠e⁠r⁠_​e⁠r⁠r⁠o⁠r register_error(name, func, /) name必须是一个字符串。func必须是一个可调用的函数,接受一个参数e,它是 UnicodeDecodeError 的一个实例,并且必须返回一个包含两个项目的元组:要插入转换后字符串结果的 Unicode 字符串,以及继续转换的索引(通常为e.end)。该函数可以使用e.encoding,即此转换的编解码器名称,以及e.object[e.start:e.end],导致转换错误的子字符串。

unicodedata 模块

unicodedata 模块提供了对 Unicode 字符数据库的简单访问。对于任何 Unicode 字符,您可以使用 unicodedata 提供的函数获取字符的 Unicode 类别、官方名称(如果有的话)和其他相关信息。您还可以查找与给定官方名称相对应的 Unicode 字符(如果有的话):

>>> `import` unicodedata
>>> unicodedata.name('⚀')
'DIE FACE-1'
>>> unicodedata.name('Ⅵ')
'ROMAN NUMERAL SIX'
>>> int('Ⅵ')
ValueError: invalid literal for int() with base 10: 'Ⅵ'
>>> unicodedata.numeric('Ⅵ')  *`# use unicodedata to get numeric value`*
6.0
>>> unicodedata.lookup('RECYCLING SYMBOL FOR TYPE-1 PLASTICS')
'♳'

¹ 本书仅涵盖了这一遗留特性的子集,即格式说明符,您必须了解它以正确使用日志模块(讨论在“日志模块”)。

第十章:正则表达式

正则表达式(REs,也称为 regexps)允许程序员指定模式字符串并进行搜索和替换。正则表达式并不容易掌握,但它们是处理文本的强大工具。Python 通过内置的 re 模块提供了丰富的正则表达式功能。在本章中,我们全面介绍了关于 Python 的正则表达式的所有内容。

正则表达式和 re 模块

正则表达式是由表示模式的字符串构建而成的。使用 RE 功能,你可以检查任何字符串,并查看字符串的哪些部分(如果有的话)匹配了模式。

re 模块提供了 Python 的 RE 功能。compile 函数从模式字符串和可选标志中构建一个 RE 对象。RE 对象的方法查找字符串中的 RE 匹配项或执行替换。re 模块还公开了等效于 RE 对象方法的函数,但将 RE 的模式字符串作为第一个参数。

本章涵盖了在 Python 中使用 REs;它并不教授如何创建 RE 模式的每一个细节。对于正则表达式的一般覆盖,我们推荐 Jeffrey Friedl 的书《精通正则表达式》(O’Reilly),该书在教程和高级水平上都对正则表达式进行了详尽的介绍。许多关于正则表达式的教程和参考资料也可以在网上找到,包括 Python 的在线文档中的优秀详细教程。像Pythexregex101这样的网站可以让你交互式地测试你的 REs。或者,你可以启动 IDLE,Python REPL 或任何其他交互式解释器,import re,并直接进行实验。

REs 和 bytes 与 str

在 Python 中,REs 根据要匹配的对象的类型以两种方式工作:当应用于 str 实例时,REs 根据相应匹配(例如,Unicode 字符 c 如果 'LETTER' in unicodedata.name(c) 则被认为是“字母”);当应用于 bytes 实例时,REs 根据 ASCII 进行匹配(例如,字节 c 如果 c in string.ascii_letters 则被认为是“字母”)。例如:

`import` re
print(re.findall(r'\w+', 'cittá'))            *`# prints: ['cittá']`*
print(re.findall(rb'\w+', 'cittá'.encode())) *`# prints: [b'citt']`*

模式字符串语法

表示正则表达式的模式字符串遵循特定的语法:

  • 字母和数字字符代表它们自身。一个 RE,其模式是由字母和数字组成的字符串,匹配相同的字符串。

  • 许多字母数字字符在模式中以反斜杠(\)或 转义 的方式具有特殊含义。

  • 标点符号字符的工作方式正好相反:它们在转义时代表它们自身,但在未转义时具有特殊含义。

  • 反斜杠字符可以通过重复的反斜杠(\)来匹配。

RE 模式是一个将一个或多个模式元素串联起来的字符串;每个元素本身也是一个 RE 模式。例如,r'a' 是一个单元素 RE 模式,匹配字母 a,而 r'ax' 是一个两元素 RE 模式,匹配紧接着的 a 后面跟着的 x。

由于 RE 模式经常包含反斜杠,最好总是以原始字符串字面形式指定 RE 模式(见 “字符串”)。模式元素(例如 r'\t',等效于字符串字面量 '\t')确实匹配相应的特殊字符(在本例中是制表符 \t),因此即使需要字面匹配这些特殊字符,也可以使用原始字符串字面量。

表 10-1 列出了 RE 模式语法中的特殊元素。某些模式元素的确切含义会因可选标志与模式字符串一起构建 RE 对象而改变。可选标志在 “可选标志” 中介绍。

表 10-1. RE 模式语法

元素 含义
. 匹配任何单个字符,除了换行符 \n(如果启用 DOTALL,则也匹配换行符 \n)
^ 匹配字符串的开头(如果启用 MULTILINE,则也匹配换行符后面的位置)
$ 匹配字符串的结尾(如果启用 MULTILINE,则也匹配换行符前面的位置)
* 匹配前一个 RE 的零次或多次;贪婪模式(尽可能多地匹配)
+ 匹配前一个 RE 的一次或多次;贪婪模式(尽可能多地匹配)
? 匹配前一个 RE 的零次或一次;贪婪模式(如果可能,则匹配一次)
*?, +?, ?? 非贪婪版本 的 *, +, ?,分别匹配尽可能少的情况
{m} 匹配前一个 RE 的 m
{m, n} 匹配前一个 RE 的 mn 次;可以省略 mn(或两者),默认 m=0 和 n=无穷大(贪婪模式)
{m, n}? 匹配前一个 RE 的 mn 次;非贪婪模式
[...] 匹配括号内任一字符集合中的一个字符
[^...] 匹配括号内不含字符的一个字符,^ 后面紧跟的字符
| 匹配前一个 RE 或后一个 RE
(...) 匹配括号内的 RE 并指示一个
(?aiLmsux) 设置可选标志的另一种方式^(a)
(?:...) 类似于 (...),但不捕获匹配的字符组
(?P**...) 类似于 (...),但该组同时获取名称
(?P=) 匹配先前由名称为 的组匹配的内容
(?#...) 括号内的内容仅作为注释;对匹配没有影响
(?=...) 前行断言:如果 RE ... 匹配接下来的内容,则匹配,并且不消耗字符串的任何部分
(?!...) 负向前行断言:如果 RE ... 不匹配接下来的内容,则匹配,并且不消耗字符串的任何部分
(?<=...) 后行断言:如果 RE ... 的匹配正好结束于当前位置,则匹配
(?<!...) 负向后行断言:如果 RE ... 的匹配正好不结束于当前位置,则匹配
\ number 匹配之前由编号 number 的组匹配的内容(组自动从左到右编号,从 1 到 99)
\A 只在整个字符串的开头匹配一个空字符串
\b 只在单词的开头或结尾匹配一个空字符串(一个最大的字母数字字符序列;参见也 \w)
\B 匹配一个空字符串,但不匹配单词的开头或结尾
\d 匹配一个数字,类似于集合 [0-9](在 Unicode 模式下,许多其他 Unicode 字符也被视为“数字”对于 \d,但不适用于 [0-9])
\D 匹配一个非数字字符,类似于集合 [⁰-9](在 Unicode 模式下,许多其他 Unicode 字符也被视为“数字”对于 \D,但不适用于 [⁰-9])
\N{name} 3.8+ 匹配与 name 对应的 Unicode 字符
\s 匹配一个空白字符,类似于集合 [\t\n\r\f\v]
\S 匹配一个非空白字符,类似于集合 [^\t\n\r\f\v]
\w 匹配一个字母数字字符;除非在 Unicode 模式下,或者 LOCALE 或 UNICODE 已设置,否则 \w 就像 [a-zA-Z0-9_]
\W 匹配一个非字母数字字符,与 \w 的反义
\Z 只在整个字符串的结尾匹配一个空字符串
\ 匹配一个反斜杠字符
^(a) 总是将设置标志(如果有)的 (?...) 结构放在模式的开头,以提高可读性;在其他位置放置会引发 DeprecationWarning。

使用字符 \ 后跟字母字符(不包括此处列出的字符或 表 3-4)会引发 re.error 异常。

常见的正则表达式惯用法

始终使用 r'...' 语法来表示 RE 模式文字

对于所有 RE 模式文字,请使用原始字符串文字,仅限于它们。这样可以确保您永远不会忘记转义反斜杠 (),并提高代码的可读性,因为它使您的 RE 模式文字更加突出。

.* 作为正则表达式模式字符串的子串意味着“任意数量(零或多个)的任意字符。”换句话说,.* 匹配目标字符串的任何子串,包括空子串。. + 类似,但只匹配非空子串。例如,这样:

r'pre.*post'

匹配包含子字符串 'pre' 后接后续子字符串 'post' 的字符串,即使后者紧邻前者(例如,它同时匹配 'prepost' 和 'pre23post')。另一方面,此模式:

r'pre.+post'

只有当 'pre' 和 'post' 不相邻时才匹配(例如,它匹配 'pre23post' 但不匹配 'prepost')。这两种模式还会匹配在 'post' 后继续的字符串。为了将模式限制为仅匹配以 'post' 结尾的字符串,请在模式结尾处使用 \Z。例如,这样:

r'pre.*post\Z'

匹配 'prepost' 但不匹配 'preposterous'。

所有这些示例都是 贪婪 的,意味着它们匹配从第一个出现的 'pre' 开始到最后一个出现的 'post' 的子串。当您关心匹配字符串的哪个部分时,您通常会希望指定 非贪婪 匹配,在我们的例子中,它将匹配从第一个出现的 'pre' 开始,但仅限到下一个出现的 'post' 的第一次匹配。

例如,当字符串为 'preposterous and post facto' 时,贪婪的正则表达式模式 r'pre.post' 将匹配子串 'preposterous and post';非贪婪的变体 r'pre.?post' 则仅匹配子串 'prepost'。

正则表达式模式中另一个经常使用的元素是 \b,它匹配单词边界。要仅匹配单词 'his' 而不是它在诸如 'this' 和 'history' 中出现的子串,正则表达式模式为:

r'\bhis\b'

在单词边界之前和之后。要匹配以 'her' 开头的任何单词的开头,如 'her' 本身和 'hermetic',但不是其他地方仅包含 'her' 的单词,如 'ether' 或 'there',使用:

r'\bher'

在相关字符串之前的单词边界,但不是之后。要匹配以 'its' 结尾的任何单词的结尾,如 'its' 本身和 'fits',但不是其他地方包含 'its' 的单词,如 'itsy' 或 'jujitsu',使用:

r'its\b'

在相关字符串之后的单词边界,但不是之前。为了匹配这样受限的完整单词,而不仅仅是它们的开头或结尾,添加模式元素 \w* 来匹配零个或多个单词字符。要匹配以 'her' 开头的任何完整单词,使用:

r'\bher\w*'

要仅匹配以 'her' 开头的任何单词的前三个字母,但不包括单词 'her' 本身,使用负单词边界 \B:

r'\bher\B'

要匹配以 'its' 结尾的任何完整单词,包括 'its' 本身,使用:

r'\w*its\b'

字符集

您可以通过在方括号([])内列出字符来表示模式中的字符集。除了列出字符外,还可以通过用连字符(-)分隔的第一个和最后一个字符来表示范围。范围的最后一个字符包括在集合中,与其他 Python 范围不同。在集合内,特殊字符代表它们自己,除了 \、] 和 -,当它们的位置使它们不转义时(通过在它们前面加上反斜杠),它们将形成集合的语法部分。您可以通过转义字母表示法,如 \d 或 \S,在集合中表示字符类。在集合的模式中,\b 表示退格字符(chr(8)),而不是单词边界。如果集合模式中的第一个字符,紧跟在 [ 之后,是一个插入符(),则集合是 补充的:这样的集合匹配除了在集合模式表示法中的后跟随的字符之外的任何字符。

字符集的一个常见用法是匹配“单词”,使用与\w 默认值(字母和数字)不同的字符定义可以组成单词。要匹配一个或多个字符的单词,每个字符可以是 ASCII 字母,撇号或连字符,但不能是数字(例如,“Finnegan-O'Hara”),使用:

r"[a-zA-Z'\-]+"

总是需要在字符集中转义连字符。

在这种情况下,严格来说不必在字符集中使用反斜杠转义连字符,因为其位置位于集合的末尾,使得情况在语法上不模棱两可。然而,使用反斜杠是明智的,因为它使模式更可读,通过视觉上区分你希望作为集合中的字符的连字符,而不是用于表示范围的连字符。(当你想在字符集中包含反斜杠时,当然,你通过转义反斜杠本身来表示:将其写为\。)

替代项

正则表达式模式中的竖线(|),用于指定替代项,具有较低的语法优先级。除非括号改变了分组,|应用于两侧整个模式,直到模式的开始或结束,或者另一个|。模式可以由任意数量的由|连接的子模式组成。重要的是要注意,由|连接的子模式的 RE 将匹配第一个匹配的子模式,而不是最长的子模式。像 r'ab|abc'的模式永远不会匹配'abc',因为'ab'匹配首先得到评估。

给定单词列表L,匹配任意一个单词的 RE 模式是:

'|'.join(rf'\b{word}\b' `for` word `in` L)

转义字符串

如果L的项目可以是更一般的字符串,而不仅仅是单词,您需要使用 re.escape 函数(在表 10-6中介绍)对每个项目进行转义,并且可能不希望在两侧使用\b 单词边界标记。在这种情况下,您可以使用以下 RE 模式(通过长度逆序对列表进行排序,以避免意外“掩盖”较长单词的较短单词):

'|'.join(re.escape(s) `for` s `in` sorted(
         L, key=len, reverse=`True`))

分组

正则表达式可以包含从零到 99 个(甚至更多,但只完全支持前 99 个)分组。模式字符串中的括号表示一个组。元素(?P<id>...)也表示一个组,并为组命名一个名为id的名称,该名称可以是任何 Python 标识符。所有组,包括命名和未命名的,都按从左到右,从 1 到 99 进行编号;“组 0”表示整个 RE 匹配的字符串。

对于 RE 与字符串的任何匹配,每个组都匹配一个子字符串(可能为空)。当 RE 使用|时,一些组可能不匹配任何子字符串,尽管整个 RE 确实匹配字符串。当一个组不匹配任何子字符串时,我们说该组不参与匹配。对于任何不参与匹配的组,空字符串('')用作匹配的子字符串,除非本章后面另有说明。例如,这个:

r'(.+)\1+\Z'

匹配由两个或更多次重复的非空子字符串组成的字符串。模式的 (.+) 部分匹配任何非空子字符串(任何字符,一次或多次),并定义了一个组,因为有括号。模式的 \1+ 部分匹配组的一次或多次重复,并且 \Z 将匹配锚定到字符串的结尾。

可选标志

函数 compile 的可选标志参数是通过使用 Python 的按位或运算符(|)对模块 re 的以下属性之一或多个进行按位或运算构建的编码整数。每个属性都有一个简短名称(一个大写字母),以方便使用,以及一个更可读的长名称(一个大写的多字母标识符),因此通常更可取:

A or ASCII

仅使用 ASCII 字符来匹配 \w、\W、\b、\B、\d 和 \D;覆盖默认的 UNICODE 标志

I or IGNORECASE

使匹配不区分大小写

L or LOCALE

使用 Python LOCALE 设置来确定 \w、\W、\b、\B、\d 和 \D 标记的字符;你只能在字节模式中使用此选项

M or MULTILINE

使得特殊字符 ^ 和 $ 匹配每一行的开头和结尾(即,在换行符之后/之前),以及整个字符串的开头和结尾(\A 和 \Z 仅匹配整个字符串的开头和结尾)

S or DOTALL

导致特殊字符 . 匹配任何字符,包括换行符

U or UNICODE

使用完整的 Unicode 来确定 \w、\W、\b、\B、\d 和 \D 标记的字符;虽然保留了向后兼容性,但此标志现在是默认的

X or VERBOSE

导致忽略模式中的空格,除非转义或在字符集中,并使得模式中的非转义 # 字符成为从该行到行尾的注释的开始

标志还可以通过在 (? 和 ) 之间插入一个或多个字母 aiLmsux 的模式元素来指定,而不是通过 re 模块的 compile 函数的标志参数(这些字母对应于前述列表中给出的大写标志)。选项应始终放在模式的开头;不这样做会产生弃用警告。特别是,如果 x(用于详细 RE 解析的内联标志字符)位于选项中,则必须将其放在模式的开头,因为 x 会改变 Python 解析模式的方式。选项适用于整个 RE,但 aLu 选项可以在组内局部应用。

使用显式的标志参数比在模式中放置选项元素更易读。例如,以下是使用 compile 函数定义等效 RE 的三种方法。这些 RE 中的每一个都匹配任何大小写字母组合的单词“hello”:

`import` re
r1 = re.compile(r'(?i)hello')
r2 = re.compile(r'hello', re.I)
r3 = re.compile(r'hello', re.IGNORECASE)

第三种方法显然是最易读的,因此也是最易维护的,尽管稍微冗长。在这里使用原始字符串形式并非完全必要,因为模式不包含反斜杠。然而,使用原始字符串字面量没有坏处,我们建议您始终使用它们来改善 RE 模式的清晰度和可读性。

选项 re.VERBOSE(或 re.X)允许您通过适当使用空白和注释使模式更易读和理解。通常,复杂和冗长的 RE 模式最好由占据多行的字符串表示,因此您通常希望为这些模式字符串使用三重引号原始字符串字面量。例如,要匹配可能以八进制、十六进制或十进制格式表示的整数字符串,您可以使用以下任一种:

repat_num1 = r'(0o[0-7]*|0x[\da-fA-F]+|[1-9]\d*)\Z'
repat_num2 = r'''(?x) *`# (re.VERBOSE) pattern matching int literals`*
 (  0o [0-7]* *`# octal: leading 0o, 0+ octal digits`*
 | 0x [\da-fA-F]+ *`# hex: 0x, then 1+ hex digits`*
 | [1-9] \d* *`# decimal: leading non-0, 0+ digits`*
 )\Z *`# end of string`*
              '''

此示例中定义的两个模式是等效的,但第二个模式通过注释和自由使用空白来以逻辑方式可视化分组模式,使其更易读和理解。

匹配与搜索

到目前为止,我们一直在使用正则表达式来 匹配 字符串。例如,带有模式 r'box' 的 RE 可以匹配字符串 'box' 和 'boxes',但不能匹配 'inbox'。换句话说,RE 的 匹配 隐含地锚定在目标字符串的开头,就好像 RE 的模式以 \A 开头。

经常你会对 RE 中的可能匹配的位置感兴趣,无论在字符串的哪个位置(例如,在诸如 'inbox' 中找到 r'box' 的匹配,以及在 'box' 和 'boxes' 中)。在这种情况下,Python 中的术语称为 search,而不是匹配。对于这样的搜索,请使用 RE 对象的 search 方法,而不是 match 方法,后者仅从字符串的开头进行匹配。例如:

`import` re
r1 = re.compile(r'box')
`if` r1.match('inbox'):
    print('match succeeds')
`else`:
    print('match fails')          *`# prints: match fails`*

`if` r1.search('inbox'):
    print('search succeeds')      *`# prints: search succeeds`*
`else`:
    print('search fails')

如果你想检查整个字符串是否匹配,而不仅仅是其开头,你可以使用 fullmatch 方法。所有这些方法都包含在 表 10-3 中。

在字符串的起始和结束锚定

\A 和 \Z 是确保正则表达式匹配在字符串的起始或结尾处 锚定 的模式元素。元素 ^ 用于起始,而 \(用于结尾,也用于类似的角色。对于未标记为 MULTILINE 的 RE 对象,^ 等同于 \A,而\) 等同于 \Z。然而,对于多行 RE,^ 可以锚定在字符串的起始或任何行的起始(“行”是基于 \n 分隔符字符确定的)。类似地,对于多行 RE,$ 可以锚定在字符串的结尾或任何行的结尾。无论 RE 对象是否为多行,\A 和 \Z 始终只锚定在字符串的起始和结尾处。

例如,这里是检查文件是否有任何以数字结尾的行的方法:

`import` re
digatend = re.compile(r'\d$', re.MULTILINE)
`with` open('afile.txt') `as` f:
    `if` digatend.search(f.read()):
        print('some lines end with digits')
    `else`:
        print('no line ends with digits')

模式 r'\d\n' 几乎等效,但在这种情况下,如果文件的最后一个字符是一个不跟随换行符的数字,则搜索失败。使用前述示例,即使数字位于文件内容的最后,搜索也将成功,这与通常情况下数字后跟换行符的情况相同。

正则表达式对象

表 10-2 涵盖了正则表达式对象 r 的只读属性,详细说明了 r 是如何构建的(由模块 re 的 compile 函数完成,见 表 10-6)。

Table 10-2. RE 对象的属性

flags flags 参数传递给 compile 函数时的参数,或者当省略 flags 时为 re.UNICODE;还包括使用前导 (?...) 元素在模式本身中指定的任何标志
groupindex 一个字典,其键是由元素 (?P<id>...) 定义的组名;相应的值是命名组的编号
pattern 编译 r 的模式字符串

这些属性使得可以轻松地从已编译的 RE 对象中检索其原始模式字符串和标志,因此您不必单独存储它们。

RE 对象 r 也提供了方法来在字符串中查找 r 的匹配项,并对这些匹配项执行替换操作(参见 表 10-3)。这些匹配项由特殊对象表示,在下一节中详细介绍。

Table 10-3. RE 对象的方法

| findall | r.findall(s) 当 r 没有分组时,findall 返回一个字符串列表,其中每个字符串都是 s 中与 r 非重叠匹配的子串。例如,要打印文件中的所有单词,每行一个:

`import` re
reword = re.compile(r'\w+')
`with` open('afile.txt') `as` f:
    `for` aword `in` reword.findall(f.read()):
        print(aword)

|

| findall (cont.) | 当 r 恰好有一个分组时,findall 也返回一个字符串列表,但每个字符串都是与 r 的组匹配的 s 的子串。例如,要仅打印后面跟有空白字符的单词(而不是跟有标点符号或字符串末尾的单词),您只需更改前面示例中的一个语句:

reword = re.compile('(\w+)\s')

rn 个分组(其中 n > 1)时,findall 返回一个元组列表,每个元组对应 r 的一个非重叠匹配。每个元组有 n 个项,对应 r 的每个分组匹配的 s 中的子串。例如,要打印每行中至少有两个单词的第一个和最后一个单词:

`import` re
first_last = re.compile(r'^\W*(\w+)\b.*\b(\w+)\W*$', 
                        re.MULTILINE)
`with` open('afile.txt') `as` f:
    `for` first, last `in` first_last.findall(f.read()):
        print(first, last)

|

finditer r.finditer(s) finditer 类似于 findall,不同之处在于它返回一个迭代器,其项是匹配对象(在下一节中讨论)。因此,在大多数情况下,finditer 比 findall 更灵活,通常性能更好。
fullmatch r.fullmatch(s, start=0, end=sys.maxsize) 当完整子串 s(从索引 start 开始,结束于索引 end 之前)与 r 匹配时,返回一个匹配对象。否则,fullmatch 返回 None

| match | r.match(s, start=0, end=sys.maxsize) 当 s 中以索引 start 开始且不到索引 end 的子字符串与 r 匹配时,返回一个适当的匹配对象。否则,match 返回 None。match 在 s 中的起始位置 start 隐式锚定。要在 s 中的任何位置搜索 r 的匹配项,从 start 开始,请调用 r.search,而不是 r.match。例如,这是一种打印所有以数字开头的行的方法:

`import` re
digs = re.compile(r'\d')
`with` open('afile.txt') `as` f:
    `for` line `in` f:
        `if` digs.match(line):
            print(line, end='')

|

| search | r.search(s, start=0, end=sys.maxsize) 返回 s 的左侧子字符串的适当匹配对象,其起始位置不早于索引 start,且不达到索引 end,并与 r 匹配。当不存在这样的子字符串时,search 返回 None。例如,要打印包含数字的所有行,一个简单的方法如下:

`import` re
digs = re.compile(r'\d')
`with` open('afile.txt') `as` f:
    `for` line in f:
        `if` digs.search(line):
            print(line, end='')

|

| split | r.split(s, maxsplit=0) 返回一个由 sr 分割的列表 L(即由与 r 非重叠、非空匹配分隔的 s 的子字符串)。例如,这是一种从字符串中消除所有出现的 'hello'(不管大小写)的方法:

`import` re
rehello = re.compile(r'hello', re.IGNORECASE)
astring = ''.join(rehello.split(astring))

rn 组时,在 L 的每一对分割之间,n 个额外的项被交错插入。每个额外的项是 s 中与 r 对应组匹配的子字符串,如果该组未参与匹配,则为 None。例如,这是一种仅在冒号和数字之间出现空白时删除空白的方法:

`import` re
re_col_ws_dig = re.compile(r'(:)\s+(\d)')
astring = ''.join(re_col_ws_dig.split(astring))

如果 maxsplit 大于 0,则 L 中最多有 maxsplit 个分割,每个分割后跟随 n 项,而 s 的最后一个匹配 r 的尾随子字符串(如果有的话)是 L 的最后一项。例如,要仅删除子字符串 'hello' 的第一个出现而不是全部出现,将第一个示例中的最后一条语句更改为:

astring=''.join(rehello.split(astring, 1))

|

| sub | r.sub(repl, s, count=0) 返回一个 s 的副本,其中与 r 非重叠的匹配项被 repl 替换,repl 可以是字符串或可调用对象(如函数)。只有当空匹配不紧邻前一个匹配时,才替换空匹配。当 count 大于 0 时,仅替换 s 中前 count 次出现的 r。当 count 等于 0 时,替换 s 中所有的 r 匹配。例如,这是另一种更自然的方法,用于从任意大小写混合的字符串中仅删除子字符串 'hello' 的第一个出现:

`import` re
rehello = re.compile(r'hello', re.IGNORECASE)
astring = rehello.sub('', astring, 1)

未提供 sub 的最后一个参数 1(一个),示例将删除所有 'hello' 的出现。

repl 是一个可调用对象时,repl 必须接受一个参数(匹配对象),并返回一个字符串(或 None,等效于返回空字符串 '')作为匹配的替换内容。在这种情况下,sub 对每个与 r 匹配并替换的匹配调用 repl,并使用适当的匹配对象参数。例如,这是一种大写所有以 'h' 开头并以 'o' 结尾的单词的所有出现的方法:

`import` re
h_word = re.compile(r'\bh\w*o\b', re.IGNORECASE)
`def` up(mo):
    `return` mo.group(0).upper()
astring = h_word.sub(up, astring)

|

| sub (续) | 当repl是一个字符串时,sub 使用repl本身作为替换,除了它会扩展反向引用。反向引用是repl中形式为 \g<id> 的子串,其中idr中的一个组的名称(由r的模式字符串中的 (?P<id>...) 语法确定),或者 \dd,其中dd被视为一个组编号,可以是一位或两位数字。每个命名或编号的反向引用都将被替换为与所指示的r组匹配的s的子串。例如,这是一种在每个单词周围加上大括号的方法:

`import` re
grouped_word = re.compile('(\w+)')
astring = grouped_word.sub(r'{\1}', astring)

|

| subn | r.subn(repl, s, count=0) subn 与 sub 相同,只是 subn 返回一对(new_stringn),其中n是 subn 执行的替换数。例如,这是一种计算任何大小写混合中子字符串 'hello' 出现次数的方法:

`import` re
rehello = re.compile(r'hello', re.IGNORECASE)
_, count = rehello.subn('', astring)
print(f'Found {count} occurrences of "hello"') 

|

匹配对象

匹配对象由正则表达式对象的方法 fullmatch、match 和 search 创建并返回,并且是方法 finditer 返回的迭代器的项。当repl是可调用的时,它们也是方法 sub 和 subn 隐式创建的,因为在这种情况下,适当的匹配对象在每次调用repl时作为唯一参数传递。匹配对象m提供了以下只读属性,详细说明了搜索或匹配如何创建m,见 Table 10-4。

Table 10-4. 匹配对象的属性

pos 传递给 search 或 match 的start参数(即,在s中开始匹配的索引)
endpos 传递给 search 或 match 的end参数(即,匹配子串s必须结束的s中的索引)
lastgroup 最后匹配的组的名称(如果最后匹配的组没有名称或者没有组参与匹配,则为None
lastindex 最后匹配的组的整数索引(如果没有组参与匹配,则为None
re 创建m的方法所创建的 RE 对象r
string 传递给 finditer、fullmatch、match、search、sub 或 subn 的字符串s

此外,匹配对象提供了 Table 10-5 中详细介绍的方法。

Table 10-5. 匹配对象的方法

| end, span,

start | m.end(groupid=0), m.span(groupid=0),

m.start(groupid=0)

这些方法返回m.string 中与groupid(组号或名称;0,groupid 的默认值,表示“整个 RE”)标识的组匹配的子串的索引。当匹配的子串是m.string[i:j]时,m.start 返回im.end 返回jm.span 返回(ij)。如果该组没有参与匹配,则ij均为-1。 |

expand m.expand(s) 返回一个副本,其中转义序列和反向引用的替换方式与方法r.sub 中描述的方式相同,见 Table 10-3。

| group | m.group(groupid=0, *groupids) 使用单个参数 groupid(组号或名称),m.group 返回与由 groupid 标识的组匹配的子字符串,如果该组没有参与匹配,则返回 Nonem.group()—或 m.group(0)—返回整个匹配的子字符串(组 0 表示整个 RE)。也可以使用 m[index] 访问组,就像使用 m.group(index) 调用一样(在任一情况下,index 可以是 int 或 str)。

当 group 以多个参数调用时,每个参数必须是组号或名称。然后,group 返回一个元组,每个参数一个项目,对应组匹配的子字符串,如果该组没有参与匹配,则返回 None

groupdict m.groupdict(default=None) 返回一个字典,其键是 r 中所有命名组的名称。每个名称的值是与相应组匹配的子字符串,如果该组没有参与匹配,则为默认值。
groups m.groups(default=None) 返回一个元组,其中包含 r 中每个组的一个项目。每个项目是与相应组匹配的子字符串,如果该组没有参与匹配,则为默认值。元组不包括表示完整模式匹配的 0 组。

re 模块的函数

除了 “可选标志” 中列出的属性之外,re 模块还为正则表达式对象的每个方法提供了一个函数(findall、finditer、fullmatch、match、search、split、sub 和 subn,在 表 10-3 中描述),每个函数都有一个额外的第一个参数,即模式字符串,该函数隐式编译为 RE 对象。通常最好显式地将模式字符串编译为 RE 对象并调用 RE 对象的方法,但有时,对于一次性使用 RE 模式,调用 re 模块的函数可能更方便。例如,要计算任何大小写混合中 'hello' 出现的次数,一种简洁的、基于函数的方法是:

`import` re
_, count = re.subn(r'hello', '', astring, flags=re.I)
print(f'Found {count} occurrences of "hello"')

re 模块在内部缓存从传递给函数的模式创建的 RE 对象;要清除缓存并回收一些内存,调用 re.purge。

re 模块还提供了 error,即在出错时引发的异常类(通常是模式字符串的语法错误),以及另外两个函数,在 表 10-6 中列出。

表 10-6. 其他 re 函数

compile compile(pattern, flags=0) 创建并返回一个 RE 对象,解析字符串 pattern,其语法如 “模式字符串语法” 中所述,并使用整数标志,如 “可选标志” 中所描述
escape escape(s) 返回字符串 s 的副本,其中每个非字母数字字符都被转义(即,在其前面加上反斜杠,\);这对于将字符串 s 作为 RE 模式字符串的一部分进行文字匹配非常有用

RE 和 := 运算符

在 Python 3.8 中引入的 := 操作符支持了一种类似于 Perl 中常见的连续匹配习语。在这种习语中,一系列的 if/elsif 分支根据不同的正则表达式测试字符串。在 Perl 中,if ($var =~ /regExpr/) 语句既评估正则表达式,又将成功的匹配保存在变量 var 中:¹

if    ($statement =~ /I love (\w+)/) {
  print "He loves $1\n";
}
elsif ($statement =~ /Ich liebe (\w+)/) {
  print "Er liebt $1\n";
}
elsif ($statement =~ /Je t\'aime (\w+)/) {
  print "Il aime $1\n";
}

在 Python 3.8 之前,这种评估和存储行为在单个 if/elif 语句中是不可能的;开发者必须使用繁琐的嵌套 if/else 语句级联:

m = re.match('I love (\w+)', statement)
`if` m:
    print(f'He loves {m.group(1)}')
`else`:
    m = re.match('Ich liebe (\w+)', statement)
    `if` m:
        print(f'Er liebt {m.group(1)}')
    `else`:
         m = re.match('J'aime (\w+)', statement)
        `if` m:
            print(f'Il aime {m.group(1)}')

使用 := 操作符,此代码简化为:

`if` m := re.match(r'I love (\w+)', statement):
    print(f'He loves {m.group(1)}')

`elif` m := re.match(r'Ich liebe (\w+)', statement):
    print(f'Er liebt {m.group(1)}') 

`elif` m := re.match(r'J'aime (\w+)', statement):
    print(f'Il aime {m.group(1)}')

第三方 regex 模块

作为 Python 标准库 re 模块的替代方案,第三方正则表达式包 regex module,由 Matthew Barnett 开发,非常流行。regex 提供与 re 模块兼容的 API,并添加了多种扩展功能,包括:

  • 递归表达式

  • 通过 Unicode 属性/值定义字符集

  • 重叠匹配

  • 模糊匹配

  • 多线程支持(在匹配期间释放 GIL)

  • 匹配超时

  • Unicode 不区分大小写匹配中的大小写折叠

  • 嵌套集合

¹ 此示例取自正则表达式;参见“Python 中的匹配组”在 Stack Overflow 上的讨论。

第十一章:文件和文本操作

本章涵盖了 Python 中与文件和文件系统相关的问题。文件是程序可以读取和/或写入的文本或字节流;文件系统是计算机系统上文件的分层存储库。

其他同样涉及文件处理的章节

文件是编程中至关重要的概念:因此,尽管本章是本书中最大的章节之一,其他章节也包含处理特定类型文件相关的内容。特别是,第十二章涉及与持久性和数据库功能相关的多种文件(CSV 文件在第十二章,JSON 文件在“json 模块”,pickle 文件在“pickle 模块”,shelve 文件在“shelve 模块”,DBM 和类似 DBM 的文件在“dbm 包”,以及 SQLite 数据库文件在“SQLite”),第二十二章处理 HTML 格式文件,第二十三章处理 XML 格式文件。

文件和流具有多种变体。它们的内容可以是任意字节或文本。它们可能适合读取、写入或两者兼而有之,并且它们可能是缓冲的,因此数据在进出文件时在内存中暂时保留。文件还可以允许随机访问,在文件内前进和后退,或跳转到文件中的特定位置进行读取或写入。本章涵盖了这些每一个主题。

此外,本章还涵盖了文件类对象的多态概念(实际上不是文件但在某种程度上像文件的对象)、处理临时文件和文件类对象的模块,以及帮助您访问文本和二进制文件内容并支持压缩文件和其他数据存档的模块。Python 的标准库支持多种无损压缩,包括(按文本文件的压缩比例从高到低排序):

tarfile 模块可让您读取和写入使用任何这些算法压缩的 TAR 文件。zipfile 模块允许您读取和写入 ZIP 文件,并处理 bzip2 和 LZMA 压缩。本章中我们涵盖了这两个模块。本书不涵盖压缩的详细内容;详见在线文档

在本章的其余部分,我们将所有文件和类似文件的对象称为文件。

在现代 Python 中,输入/输出(I/O)由标准库的 io 模块处理。os 模块提供了许多操作文件系统的函数,因此本章还介绍了该模块。然后讨论了操作文件系统的内容(比较、复制和删除目录和文件;处理文件路径;以及访问低级文件描述符),这些功能由 os 模块、os.path 模块以及新的更可取的 pathlib 模块提供,后者提供了一种面向对象的文件系统路径的方法。有关称为内存映射文件的跨平台进程间通信(IPC)机制,请参见第 15 章中介绍的 mmap 模块。

虽然大多数现代程序依赖于图形用户界面(GUI),通常通过浏览器或智能手机应用程序,但基于文本的非图形“命令行”用户界面因其易用性、使用速度和可脚本化而仍然非常受欢迎。本章最后讨论了 Python 中的非 GUI 文本输入和输出,包括“文本输入和输出”中的终端文本 I/O,“更丰富的文本 I/O”,以及如何构建能够跨语言和文化理解的软件,在“国际化”中有所介绍。

io 模块

如本章介绍的那样,io 是 Python 标准库中负责提供读取或写入文件的最常见方式的模块。在现代 Python 中,内置函数 open 是函数 io.open 的别名。使用 io.open(或其内置别名 open)可以创建一个 Python 文件对象,用于从文件中读取和/或写入数据,这个文件对象在底层操作系统中是可见的。传递给 open 的参数决定了返回的对象类型。如果是文本类型,返回的对象可以是 io.TextIOWrapper 的实例;如果是二进制类型,则可能是 io.BufferedReader、io.BufferedWriter 或 io.BufferedRandom 中的一个,具体取决于它是只读、只写还是读写。本节涵盖了各种类型的文件对象,以及如何创建和使用临时文件(在磁盘上或甚至是在内存中)的重要问题。

I/O Errors Raise OSError

Python 对与文件对象相关的任何 I/O 错误作出响应,通过引发内置异常类 OSError 的实例来处理(有许多有用的子类存在,如“OSError 子类”中所述)。导致此异常的错误包括打开调用失败、对文件对象调用不适用于该方法的方法(例如,在只读文件上进行写入或在不可寻址文件上进行寻址),以及由文件对象的方法诊断出的实际 I/O 错误。

io 模块还提供了底层类(抽象和具体),通过继承和组合(也称为包装),构成了您的程序通常使用的文件对象。本书不涵盖这些高级主题。如果您可以访问数据的非常规通道或非文件系统数据存储,并希望为这些通道或存储提供文件接口,可以通过适当的子类化和包装使用 io 模块中的其他类来简化任务。有关这些高级任务的帮助,请参阅在线文档

使用 open 创建文件对象

要创建 Python 文件对象,请使用以下语法调用 open:

open(file, mode='r', buffering=-1, encoding=`None`, errors='strict', 
     newline=`None`, closefd=`True`, opener=os.open)

file 可以是字符串或 pathlib.Path 实例(作为底层操作系统所见的任何文件路径),也可以是整数(由 os.open 返回的操作系统级文件描述符,或您传递为 opener 参数的任何函数返回的值)。当 file 是路径(字符串或 pathlib.Path 实例)时,open 打开指定的文件(根据模式参数可能会创建文件——尽管其名称是 open,它不仅仅用于打开现有文件:它也可以创建新文件)。当 file 是整数时,操作系统文件必须已通过 os.open 打开。

以 Python 风格打开文件

open 是一个上下文管理器:请使用 with open(...) as f:,而不是 f = open(...),以确保文件 fwith 语句的主体完成后关闭。

open 创建并返回适当的 io 模块类的实例 f,具体取决于模式和缓冲设置。我们将所有这些实例称为文件对象;它们在彼此之间是多态的。

模式

mode 是一个可选的字符串,指示如何打开(或创建)文件。模式的可能取值列在表 11-1 中。

表 11-1. 模式设置

模式 含义
'a' 文件以只写方式打开。如果文件已经存在,则保持文件不变,并将写入的数据附加到现有内容。如果文件不存在,则创建文件。在此模式下打开的文件,调用f.seek 方法会改变f.tell 方法的结果,但不会改变写入位置:该写入位置始终保持在文件末尾。
'a+' 文件同时用于读取和写入,因此可以调用所有f的方法。如果文件已经存在,则保持不变,并且您写入的数据将附加到现有内容中。如果文件不存在,则创建文件。在文件上调用f.seek,根据底层操作系统的不同,可能在下一个f 数据的 I/O 操作时没有效果,但在下一个f 数据的 I/O 操作时通常会正常工作。
'r' 文件必须已经存在,并且以只读模式打开(这是默认设置)。
'r+' 文件必须存在,并且同时用于读取和写入,因此可以调用所有f的方法。
'w' 文件以只写模式打开。如果文件已经存在,则将其截断为零长度并覆盖,如果文件不存在,则创建文件。
'w+' 文件同时用于读取和写入,因此可以调用所有f的方法。如果文件已经存在,则将其截断为零长度并覆盖,如果文件不存在,则创建文件。

二进制和文本模式

模式字符串可以包括表 11-1 中的任何值,后跟 b 或 t。b 表示文件应以二进制模式打开(或创建),而 t 表示文本模式。当没有包括 b 或 t 时,默认为文本(即'r'类似于'rt','w+'类似于'w+t',依此类推),但根据Python 之禅,“显式胜于隐式”。

二进制文件允许您读取和/或写入字节类型的字符串,而文本文件允许您读取和/或写入 Unicode 文本字符串类型的 str。对于文本文件,当底层通道或存储系统处理字节(大多数情况下是这样)时,编码(已知 Python 的编码名称)和错误(一个错误处理程序名称,如'strict'、'replace'等,在表 9-1 的 decode 下有所涵盖)很重要,因为它们指定了如何在文本和字节之间进行转换,以及在编码和解码错误时该如何处理。

缓冲

缓冲是一个整数值,表示您请求的文件缓冲策略。当缓冲为 0 时,文件(必须为二进制模式)是无缓冲的;其效果就像每次写入文件时都刷新文件的缓冲区一样。当缓冲为 1 时,文件(必须在文本模式下打开)是行缓冲的,这意味着每次写入\n 到文件时都会刷新文件的缓冲区。当缓冲大于 1 时,文件使用大约缓冲字节数的缓冲区,通常向上舍入为某个方便驱动程序软件的值。当缓冲小于 0 时,默认值由文件流类型决定。通常,这个默认值是与交互式流相对应的行缓冲,并且对于其他文件使用 io.DEFAULT_BUFFER_SIZE 字节的缓冲区。

顺序和非顺序(“随机”)访问

文件对象f本质上是顺序的(一系列字节或文本)。读取时,按照它们出现的顺序获取字节或文本。写入时,按照写入的顺序添加字节或文本。

对于文件对象f支持非顺序访问(也称为随机访问),它必须跟踪其当前位置(存储中下一个读取或写入操作开始传输数据的位置),并且文件的底层存储必须支持设置当前位置。当f.seekable 返回True时,表示f支持非顺序访问。

打开文件时,默认的初始读/写位置是在文件的开头。使用模式'a'或'a+'打开ff的读/写位置设置为在写入数据之前的文件末尾。当向文件对象f写入或读取n字节时,f的位置将前进n。您可以通过调用f.tell 查询当前位置,并通过调用f.seek 更改位置,这两者在下一节中有详细介绍。

在文本模式的f上调用f.seek 时,传递的偏移量必须为 0(将f定位在开头或结尾,取决于f.seek 的第二个参数),或者是先前调用f.tell 返回的不透明结果¹,以将f定位回之前“书签”过的位置。

文件对象的属性和方法

文件对象f提供了表 11-2 中记录的属性和方法。

表 11-2. 文件对象的属性和方法

close close() 关闭文件。在f.close 之后不能再调用f的其他方法。允许多次调用f.close 而无害。
closed f.closed 是一个只读属性。当f.close()已调用时,返回True;否则返回False
encoding f.encoding 是一个只读属性,是指定的编码名称(如在“Unicode”中介绍)。该属性在二进制文件上不存在。
fileno fileno() 返回f的文件在操作系统级别的文件描述符(一个整数)。文件描述符在“os 模块的文件和目录函数”中有介绍。
flush flush() 请求将f的缓冲区写入操作系统,使得系统看到的文件与 Python 代码写入的内容完全一致。根据平台和f的底层文件性质,f.flush 可能无法确保所需的效果。
isatty isatty() 当f的底层文件是交互流(例如来自终端或到终端)时返回True;否则返回False
mode f.mode 是一个只读属性,其值是在创建f的 io.open 调用中使用的模式字符串。
name f.name 是一个只读属性,其值是创建f的 io.open 调用中使用的文件(str 或 bytes)或 int。当 io.open 使用 pathlib.Path 实例p调用时,f.name 是 str(p)。
read read(size=-1, /) 当f以二进制模式打开时,从f的文件中读取最多size字节并将它们作为字节字符串返回。如果文件在读取size字节之前结束,则 read 读取并返回少于size字节。当size小于 0 时,read 读取并返回直到文件末尾的所有字节。当文件的当前位置在文件末尾或size等于 0 时,read 返回一个空字符串。当f以文本模式打开时,size表示字符数而不是字节数,read 返回一个文本字符串。
readline readline(size=-1, /) 从f的文件中读取并返回一行,直到行尾(\n)为止。当size大于或等于 0 时,最多读取size字节。在这种情况下,返回的字符串可能不以\n 结尾。当文件的当前位置在文件末尾或size等于 0 时,readline 返回一个空字符串。当 readline 读取到文件末尾且未找到\n 时,\n 可能也会不存在。
readlines readlines(size=-1, /) 读取并返回文件f中所有行的列表,每行都是以\n 结尾的字符串。如果size > 0,则 readlines 在收集了约size字节的数据后停止并返回列表;在这种情况下,列表中的最后一个字符串可能不以\n 结尾。

| seek | seek(pos, how=io.SEEK_SET, /) 将f的当前位置设置为离参考点pos字节偏移的整数。how指示参考点。io 模块有名为 SEEK_SET、SEEK_CUR 和 SEEK_END 的属性,分别指定参考点为文件的开头、当前位置或结尾。 |

f以文本模式打开时,f.seek 必须为 0,或者对于 io.SEEK_SET,f.seek 的pos必须是上一次调用f.tell 的结果。

f以模式'a'或'a+'打开时,在某些平台上,写入f的数据会追加到已经存在f中的数据,而不管对f.seek 的调用。 |

tell tell() 返回f的当前位置:对于二进制文件,这是从文件开头的字节偏移量,对于文本文件,这是一个不透明的值,可在将来调用f.seek 将f定位回当前位置使用。
truncate truncate(size=None, /) 截断f的文件,f必须已经打开以进行写入。当size存在时,将文件截断为最多size字节。当size不存在时,使用f.tell()作为文件的新大小。size可能比当前文件大小大;在这种情况下,结果行为取决于平台。
write write(s, /) 将字符串s的字节(根据f的模式是二进制还是文本)写入文件。

| writelines | writelines(lst, /) 类似于:

`for` *`line`* `in` *`lst`*: *`f`*.write(*`line`*)

lst 可迭代的字符串是否为行并不重要:尽管其名称如此,writelines 方法只是将每个字符串依次写入文件。特别地,writelines 不会添加行结束标记:如果需要的话,这些标记必须已经存在于 lst 的项目中。

文件对象的迭代

一个用于读取的文件对象 f 也是一个迭代器,其项是文件的行。因此,下面的循环:

`for` *`line`* `in` *`f`*:

遍历文件的每一行。由于缓冲问题,如果在这样的循环中过早中断(例如使用 break),或者调用 next(f) 而不是 f.readline(),则文件的位置将设置为任意值。如果您希望从在 f 上进行迭代切换到在 f 上调用其他读取方法,请确保通过适当地调用 f.seek 将文件的位置设置为已知值。好处是,直接在 f 上进行循环具有非常好的性能,因为这些规格允许循环使用内部缓冲以最小化 I/O,即使对于大文件也不会占用过多的内存。

文件类对象与多态性

当一个对象 x 表现得像一个由 io.open 返回的文件对象时,它就是类似文件的。这意味着我们可以像使用文件一样使用 x。使用这样一个对象的代码(称为对象的 客户端代码)通常通过参数获取对象,或者通过调用返回该对象的工厂函数获取对象。例如,如果客户端代码在 x 上唯一调用的方法是 x.read(无参数),那么为了这段代码,x 需要提供的仅仅是一个可调用的、无参数的 read 方法,并且返回一个字符串。其他客户端代码可能需要 x 实现更大的文件方法子集。文件类对象和多态性并非绝对概念:它们是相对于某些特定客户端代码对对象提出的要求而言的。

多态是面向对象编程中强大的一个方面,文件类对象是多态的一个很好的例子。一个写入或读取文件的客户端模块可以自动地被重用于其他数据,只要该模块不通过类型检查来破坏多态性。当我们讨论内置类型和 isinstance 在表 8-1 中时,我们提到类型检查通常最好避免,因为它会阻碍 Python 的正常多态性。通常,为了支持你的客户端代码的多态性,你只需要避免类型检查。

你可以通过编写自己的类来实现类似文件的对象(如在第四章中介绍的),并定义客户端代码所需的特定方法,比如 read。文件样对象fl不必实现真实文件对象f的所有属性和方法。如果你可以确定客户端代码在fl上调用哪些方法,你可以选择只实现那些子集。例如,当fl只用于写入时,fl不需要“读取”方法,如 read、readline 和 readlines。

如果你想要一个类似文件的对象而不是真实文件对象的主要原因是将数据保存在内存中而不是磁盘上,可以使用 io 模块的 StringIO 或 BytesIO 类(详见“内存中的文件:io.StringIO 和 io.BytesIO”)。这些类提供了在内存中保存数据并且在大多数情况下表现得与其他文件对象多态的文件对象。如果你正在运行多个进程,希望通过类似文件的对象进行通信,考虑使用 mmap,详见第十五章。

tempfile 模块

tempfile 模块允许你以平台所允许的最安全方式创建临时文件和目录。当你处理的数据量可能不适合放入内存,或者你的程序必须写入稍后由另一个进程使用的数据时,临时文件通常是一个好主意。

此模块中函数的参数顺序有点混乱:为了使你的代码更易读,请始终使用命名参数语法调用这些函数。tempfile 模块公开了表格 11-3 中概述的函数和类。

表格 11-3. tempfile 模块的函数和类

mkdtemp mkdtemp(suffix=None, prefix=None, dir=None) 安全创建一个新的临时目录,只有当前用户可以读取、写入和搜索,然后返回临时目录的绝对路径。你可以选择传递参数来指定用作临时文件名开头(前缀)和结尾(后缀)的字符串,以及创建临时文件的目录路径(dir)。确保在使用完毕后删除临时目录是你程序的责任。

| mkdtemp (cont.) | 这里是一个典型的使用示例,它创建一个临时目录,将其路径传递给另一个函数,最后确保目录(及其所有内容)被删除:

`import` tempfile, shutil
path = tempfile.mkdtemp()
`try`:
    use_dirpath(path)
`finally`:
    shutil.rmtree(path)

|

| mkstemp | mkstemp(suffix=None, prefix=None, dir=None, text=False) 安全地创建一个新的临时文件,只有当前用户可读可写,不可执行,并且不被子进程继承;返回一对(fdpath),其中 fd 是临时文件的文件描述符(由 os.open 返回,见 Table 11-18), 字符串 path 是临时文件的绝对路径。可选参数 suffix,prefix 和 dir 类似于函数 mkdtemp。如果你想让临时文件成为文本文件,请显式传递参数 text=True

确保在使用完临时文件后将其删除由你来负责。mkstemp 不是一个上下文管理器,所以你不能使用 with 语句;最好使用 try/finally 代替。以下是一个典型的使用示例,创建一个临时文本文件,关闭它,将其路径传递给另一个函数,最后确保文件被删除:

`import` tempfile, os
fd, path = tempfile.mkstemp(suffix='.txt', 
                            text=`True`)
`try`:
    os.close(fd)
    use_filepath(path)
`finally`:
    os.unlink(path)

|

| Nam⁠e⁠d​T⁠e⁠m⁠p⁠o⁠r⁠a⁠r⁠y​F⁠i⁠l⁠e | NamedTemporaryFile(mode='w+b', bufsize=-1, suffix=None, prefix=None, dir=None)

与 TemporaryFile(稍后在此表中讨论)类似,不同之处在于临时文件确实在文件系统上有一个名称。使用文件对象的 name 属性来访问该名称。一些平台(主要是 Windows)不允许再次打开文件;因此,如果要确保程序跨平台运行,名称的有用性是有限的。如果需要将临时文件的名称传递给另一个打开文件的程序,可以使用函数 mkstemp 而不是 NamedTemporaryFile 来保证正确的跨平台行为。当然,当你选择使用 mkstemp 时,确保在完成后删除文件是需要注意的。从 NamedTemporaryFile 返回的文件对象是一个上下文管理器,因此可以使用 with 语句。|

Spoo⁠l⁠e⁠d​T⁠e⁠m⁠p⁠o⁠r⁠a⁠r⁠y​F⁠i⁠l⁠e SpooledTemporaryFile(mode='w+b', bufsize=-1, suffix=None, prefix=None, dir=None) 类似于 TemporaryFile(见下文),不同之处在于 SpooledTemporaryFile 返回的文件对象可以保留在内存中,如果空间允许,直到你调用其 fileno 方法(或其 rollover 方法,它确保文件被写入磁盘,无论其大小如何)。因此,只要有足够的内存未被其他方式使用,使用 SpooledTemporaryFile 可能性能更好。
TemporaryDirectory TemporaryDirectory(suffix=None, prefix=None, dir=None, ignore_cleanup_errors=False) 创建临时目录,类似于 mkdtemp(传递可选参数 suffix、prefix 和 dir)。返回的目录对象是一个上下文管理器,因此您可以使用with语句确保在完成后立即删除它。或者,当您不将其用作上下文管理器时,可以使用其内置类方法 cleanup(而不是 shutil.rmtree)显式删除和清理目录。将 ignore_cleanup_errors 设置为 True 可以忽略清理过程中的未处理异常。临时目录及其内容在目录对象关闭时(无论是隐式地由垃圾回收还是显式地通过 cleanup 调用)立即删除。

| TemporaryFile | TemporaryFile(mode='w+b', bufsize=-1, suffix=None, prefix=None, dir=None)

使用 mkstemp 创建临时文件(传递给 mkstemp 可选参数 suffix、prefix 和 dir),使用 os.fdopen 将其转换为文件对象,见 Table 11-18(传递给 fdopen 可选参数 mode 和 bufsize),然后返回文件对象。临时文件在文件对象关闭时立即删除(隐式或显式)。为了提高安全性,如果您的平台允许(类 Unix 平台允许,Windows 不允许),临时文件在文件系统上没有名称。从 TemporaryFile 返回的文件对象是一个上下文管理器,因此您可以使用with语句确保在完成后立即删除它。

用于文件 I/O 的辅助模块

文件对象提供了文件 I/O 所需的功能。然而,其他 Python 库模块提供了方便的附加功能,在几个重要情况下使 I/O 更加简单和方便。我们将在这里看到其中的两个模块。

fileinput 模块

fileinput 模块允许您循环遍历文本文件列表中的所有行。性能很好——与直接迭代每个文件的性能相当,因为使用缓冲区减少 I/O。因此,无论何时发现其丰富功能方便,都可以使用该模块进行基于行的文件输入,而不必担心性能问题。模块的关键函数是 input;fileinput 还提供了支持相同功能的 FileInput 类。两者在 Table 11-4 中描述。

Table 11-4. fileinput 模块的关键类和函数

FileInput FileInput(files=None, inplace=False, backup='', mode='r', openhook=None, encoding=None, errors=None) 创建并返回 FileInput 类的实例f。参数与接下来介绍的 fileinput.input 相同,f的方法具有与文件输入模块的其他函数相同的名称、参数和语义(见表 11-5)。f还提供了一个 readline 方法,它读取并返回下一行。使用 FileInput 类来嵌套或混合从多个文件序列中读取行的循环。

| input | input(files=None, inplace=False, backup='', mode='r', openhook=None, encoding=None, errors=None)

返回一个 FileInput 的实例,它是一个在文件中生成行的可迭代对象;该实例是全局状态,因此文件输入模块的所有其他函数(见表 11-5)都操作相同的共享状态。文件输入模块的每个函数直接对应于类 FileInput 的一个方法。

files 是要依次打开和读取的文件名序列。当 files 是一个字符串时,它是要打开和读取的单个文件名。当 files 为None时,input 使用 sys.argv[1:]作为文件名列表。文件名'-'表示标准输入(sys.stdin)。当文件名序列为空时,input 读取 sys.stdin。

当 inplace 为False(默认值)时,input 只读取文件。当 inplace 为True时,input 将正在读取的每个文件(除标准输入外)移动到备份文件,并重定向标准输出(sys.stdout)以写入具有与正在读取的原始文件相同路径的新文件。这样,您可以模拟就地覆盖文件。如果 backup 是以点开头的字符串,则 input 将 backup 用作备份文件的扩展名,并且不会删除备份文件。如果 backup 是一个空字符串(默认值),input 将使用*.bak*并在关闭输入文件时删除每个备份文件。关键字参数 mode 可以是'r',默认值,也可以是'rb'。

您可以选择传递一个 openhook 函数来替代 io.open。例如,openhook=fileinput.hook_compressed 可以解压缩带有扩展名*.gz.bz2的任何输入文件(与 inplace=True不兼容)。您可以编写自己的 openhook 函数来解压缩其他文件类型,例如使用 LZMA 解压缩.xz*文件;使用fileinput.hook_compressed 的 Python 源代码作为模板。3.10+ 您还可以传递 encoding 和 errors,它们将作为关键字参数传递给钩子。 |

^(a) LZMA 支持可能需要使用可选的附加库构建 Python。

列在表 11-5 中的文件输入模块的函数在由 fileinput.input 创建的全局状态上运行,如果有的话;否则,它们会引发 RuntimeError。

表 11-5. fileinput 模块的附加函数

close close() 关闭整个序列,停止迭代并且没有文件保持打开状态。
filelineno filelineno() 返回目前为止从正在读取的文件中读取的行数。例如,如果刚刚从当前文件读取了第一行,则返回 1。
filename filename() 返回当前正在读取的文件的名称,如果尚未读取任何行则返回 None
isfirstline isfirstline() 返回 TrueFalse,就像 filelineno() == 1 一样。
isstdin isstdin() 当前正在读取的文件是 sys.stdin 时返回 True;否则返回 False
lineno lineno() 返回自调用输入函数以来读取的总行数。
nextfile nextfile() 关闭当前正在读取的文件:下一行要读取的是下一个文件的第一行。

这里是使用 fileinput 进行“多文件搜索和替换”的典型示例,将一个字符串更改为另一个字符串,应用于通过命令行参数传递给脚本的文本文件:

`import` fileinput
`for` line `in` fileinput.input(inplace=`True`):
    print(line.replace('foo', 'bar'), end='')

在这种情况下,包括 end='' 参数来打印是很重要的,因为每行在末尾都有其换行符 \n,并且您需要确保 print 不会再添加另一个(否则每个文件最终会变成“双空格”)。

你也可以使用由 fileinput.input 返回的 FileInput 实例作为上下文管理器。与 io.open 一样,这将在退出 with 语句时关闭由 FileInput 打开的所有文件,即使发生异常也是如此:

`with` fileinput.input('file1.txt', 'file2.txt') `as` infile:
    dostuff(infile)

结构体 Module

结构体模块允许您将二进制数据打包成字节串,并将这样的字节串解包回它们所表示的 Python 数据。这对于许多类型的低级编程很有用。通常,您使用 struct 来解释来自具有某些指定格式的二进制文件的数据记录,或者准备要写入此类二进制文件的记录。模块的名称来自于 C 语言中的关键字 struct,用于类似的目的。在任何错误情况下,结构模块的函数会引发异常,这些异常是 struct.error 类的实例。

结构体模块依赖于遵循特定语法的 结构格式字符串。格式字符串的第一个字符给出了打包数据的字节顺序、大小和对齐方式;选项列在表 11-6 中。

表 11-6. 结构格式字符串可能的第一个字符

Character Meaning
@ 本地字节顺序、本地数据大小和本地对齐方式适用于当前平台;如果第一个字符不是列出的字符之一,则这是默认值(请注意,在 表 11-7 中,格式 P 仅适用于这种类型的结构格式字符串)。在需要检查系统字节顺序时,请查看字符串 sys.byteorder;大多数 CPU 今天使用 'little',但 'big' 是互联网核心协议 TCP/IP 的“网络标准”。
= 当前平台的本机字节顺序,但标准大小和对齐方式。
< 小端字节顺序;标准大小和对齐方式。
>, ! 大端/网络标准字节顺序;标准大小和对齐方式。

标准大小在 表 11-7 中指示。标准对齐方式表示没有强制对齐,根据需要使用显式填充字节。本机大小和对齐方式是平台的 C 编译器使用的内容。本机字节顺序可以将最高有效字节放在最低地址(大端)或最高地址(小端),这取决于平台。

在可选的第一个字符之后,格式字符串由一个或多个格式字符组成,每个格式字符可选择地由一个计数(用十进制数字表示的整数)前置。常用格式字符列在 表 11-7 中;完整列表请参阅 在线文档。对于大多数格式字符,计数表示重复(例如,'3h' 等同于 'hhh')。当格式字符为 s 或 p —— 即字节串时,计数不表示重复:它是字符串中的总字节数。您可以在格式之间自由使用空白字符,但计数和其格式字符之间不可有空格。格式 s 表示长度固定的字节串,与其计数相同(Python 字符串会被截断或根据需要使用空字节 b'\0' 进行填充)。格式 p 表示“类似帕斯卡”的字节串:第一个字节是随后的有效字节数,实际内容从第二个字节开始。计数是总字节数,包括长度字节在内。

表 11-7. struct 的常用格式字符

字符 C 类型 Python 类型 标准大小
B 无符号字符 int 1 byte
b 有符号字符 int 1 byte
c char bytes (length 1) 1 byte
d 双精度浮点数 float 8 bytes
f 浮点数 float 4 bytes
H 无符号短整型 int 2 bytes
h 有符号短整型 int 2 bytes
I 无符号整型 long 4 bytes
i 有符号整型 int 4 bytes
L 无符号长整型 long 4 bytes
l 有符号长整型 int 4 bytes
P void* int N/A
p char[] bytes N/A
s char[] bytes N/A
x 填充字节 无值 1 byte

struct 模块提供了 表 11-8 中涵盖的函数。

表 11-8. struct 模块的函数

calcsize calcsize(fmt, /) 返回格式字符串 fmt 对应的字节大小。
iter_unpack iter_unpack(fmt, buffer, /) 从 buffer 中按格式字符串 fmt 迭代解包。返回一个迭代器,该迭代器将从 buffer 中读取大小相等的块,直到消耗完所有内容;每次迭代都会产生一个由 fmt 指定的元组。buffer 的大小必须是格式所需的大小的倍数,如 struct.calcsize(fmt) 所反映的那样。
pack pack(fmt, *values, /) 将值按格式字符串 fmt 打包,并返回生成的字节串。values 必须与 fmt 所需的值的数量和类型匹配。
pack_into pack_into(fmt, buffer, offset, *values, /) 将值按格式字符串 fmt 打包到可写入的缓冲区 buffer(通常是 bytearray 的实例),从索引 offset 开始。values 必须与 fmt 所需的值的数量和类型匹配。len(buffer[offset:]) 必须 >= struct.calcsize(*fmt*)
unpack unpack(fmt, s, /) 根据格式字符串 fmt 解包字节串 s,并返回值的元组(如果只有一个值,则返回一个单项元组)。len(*s*) 必须等于 struct.calcsize(*fmt*)
unpack_from unpack_from(fmt, /, buffer, offset=0*) 从偏移量 offset 开始,根据格式字符串 fmt 解包字节串(或其他可读取的缓冲区)buffer,返回一个值的元组(如果只有一个值,则返回一个单项元组)。len(buffer[offset:]) 必须 >= struct.calcsize(*fmt*)

struct 模块还提供了一个 Struct 类,它以格式字符串作为参数进行实例化。该类的实例实现了 packpack_intounpackunpack_fromiter_unpack 方法,这些方法对应于前面表格中描述的函数;它们接受与相应模块函数相同的参数,但省略了在实例化时提供的 fmt 参数。这允许类只编译一次格式字符串并重复使用。Struct 对象还有一个 format 属性,保存对象的格式字符串,并且有一个 size 属性,保存结构的计算大小。

内存中的文件:io.StringIOio.BytesIO

你可以通过编写 Python 类来实现类似文件的对象,这些类提供你需要的方法。如果你只想让数据驻留在内存中,而不是作为操作系统可见的文件,可以使用 io 模块的 StringIOBytesIO 类。它们之间的区别在于 StringIO 的实例是文本模式文件,因此读取和写入消耗或产生文本字符串,而 BytesIO 的实例是二进制文件,因此读取和写入消耗或产生字节串。这些类在测试和其他需要将程序输出重定向用于缓冲或日志记录的应用程序中特别有用;"The print Function" 包括一个有用的上下文管理器示例 redirect,演示了这一点。

当你实例化任何一个类时,可以选择传递一个字符串参数,分别是 strbytes,作为文件的初始内容。此外,你可以向 StringIO(但不是 BytesIO)传递参数 newline='\n' 来控制如何处理换行符(如在 TextIoWrapper 中所述);如果 newlineNone,则在所有平台上都将换行符写为 \n。除了 表 11-2 中描述的方法之外,任何一个类的实例 f 还提供一个额外的方法:

getvalue getvalue() 返回 f 的当前数据内容作为字符串(文本或字节)。在调用 f.close 后不能再调用 f.getvalue:close 释放 f 内部保留的缓冲区,而 getvalue 需要将缓冲区作为其结果返回。

存档和压缩文件

存储空间和传输带宽变得越来越便宜和充裕,但在许多情况下,通过使用压缩可以节省这些资源,尽管需要额外的计算工作。计算能力的成本比其他资源(如带宽)增长更快,因此压缩的流行度不断增长。Python 使得您的程序能够轻松支持压缩。本书不涵盖压缩的详细信息,但您可以在在线文档中找到有关相关标准库模块的详细信息。

本节的其余部分涵盖了“存档”文件(在单个文件中收集文件和可选目录的集合),这些文件可能会压缩也可能不会压缩。Python 的标准库提供了两个模块来处理两种非常流行的存档格式:tarfile(默认情况下不会压缩其捆绑的文件)和 zipfile(默认情况下会压缩其捆绑的文件)。

tarfile 模块

tarfile 模块允许您读取和编写TAR 文件(与流行的打包程序(如 tar)处理的文件兼容的存档文件),可选择使用 gzip、bzip2 或 LZMA 压缩。TAR 文件通常以 .tar.tar.(压缩类型) 扩展名命名。3.8+ 新建存档的默认格式为 POSIX.1-2001(pax)。 python -m tarfile 提供了一个有用的命令行接口以访问模块的功能:运行它而不带参数可以获取简短的帮助消息。

tarfile 模块提供了 T 表 11-9 中列出的函数。处理无效的 TAR 文件时,tarfile 的函数会引发 tarfile.TarError 的实例。

表 11-9. tarfile 模块的类和函数

is_tarfile is_tarfile(filename) 当名为filename(可以是字符串,3.9+ 或文件或类文件对象)的文件在前几个字节上看起来像是一个有效的 TAR 文件(可能带有压缩),则返回 True;否则返回 False

| open | open(name=None, mode='r', fileobj=None, bufsize=10240, **kwargs) 创建并返回一个 TarFile 实例 f,用于通过类文件对象 fileobj 读取或创建 TAR 文件。当 fileobj 为 None 时,name 可以是指定文件的字符串名称或路径对象;open 使用给定的模式(默认为 'r')打开文件,并且 f 包装生成的文件对象。open 可以作为上下文管理器使用(例如,with tarfile.open(...) as f)。

f.close 可能不会关闭 fileobj

当使用未为 None 的 fileobj 打开 f 时,调用 f.close 并不会关闭 fileobj。当 fileobj 是 io.BytesIO 的实例时,此 f.close 的行为非常重要:您可以在 f.close 后调用 fileobj.getvalue 来获取归档的可能已压缩数据作为字符串。这种行为也意味着您必须在调用 f.close 后显式调用 fileobj.close。

mode 可以是 'r' 以读取具有任何压缩(如果有的话)的现有 TAR 文件;'w' 以编写新的 TAR 文件,或截断并重写现有文件,而不使用压缩;或 'a' 以追加到现有的 TAR 文件,而不使用压缩。不支持向已压缩的 TAR 文件追加。要使用压缩编写新的 TAR 文件,mode 可以是 'w:gz' 以进行 gzip 压缩,'w:bz2' 以进行 bzip2 压缩,或 'w:xz' 以进行 LZMA 压缩。您可以使用 mode 字符串 'r:' 或 'w:' 以使用 bufsize 字节缓冲区读取或写入未压缩的不可寻址 TAR 文件;对于读取 TAR 文件,请使用普通 'r',因为这将根据需要自动解压缩。

在指定压缩方式的模式字符串中,你可以使用竖线(|)而不是冒号(:),以强制顺序处理和固定大小的块;这在(尽管非常不太可能的)情况下很有用,你可能会发现自己处理磁带设备! |

TarFile 类

TarFile 是大多数 tarfile 方法的底层类,但不直接使用。通过使用 tarfile.open 创建的 TarFile 实例 f,提供了 Table 11-10 中详细描述的方法。

表 11-10. TarFile 实例 f 的方法

add f.add(name, arcname=None, recursive=True, *, filter=None) 向归档 f 添加名为 name 的文件(可以是任何类型的文件、目录或符号链接)。当 arcname 不为 None 时,将其用作归档成员的名称,而不是使用 name。当 name 是一个目录且 recursive 为 True 时,add 方法将以排序顺序递归地添加该目录中以该目录为根的整个文件系统子树。可选的(仅命名的)参数 filter 是一个函数,用于调用每个要添加的对象。它接受一个 TarInfo 对象参数,并返回可能修改后的 TarInfo 对象或 None。在后一种情况下,add 方法将从归档中排除此 TarInfo 对象。
addfile f.addfile(tarinfo, fileobj=None) 向归档 f 添加一个 TarInfo 对象 tarinfo。如果 fileobj 不为 None,则将二进制类文件对象 fileobj 的前 tarinfo.size 字节添加到归档中。
close f.close() 关闭存档 f。您必须调用 close,否则可能会在磁盘上留下一个不完整的、无法使用的 TAR 文件。最好使用 try/finally(在 “try/finally”中讨论)进行此类必需的最终操作,或者更好地,使用 “with 语句和上下文管理器”中讨论的 with 语句。调用 f.close 不会关闭 fileobj,如果 f 是使用非 None fileobj 创建的。这在 fileobj 是 io.BytesIO 的实例时尤为重要:您可以在 f.close 后调用 fileobj.getvalue 来获取压缩的数据字符串。因此,您始终必须在 f.close 调用 fileobj.close(显式地或通过使用 with 语句隐式地)。
extract *f.*extract(member, path='', set_attrs=True, numeric_owner=False) 将由 member(名称或 TarInfo 实例)标识的存档成员提取到目录(或类似路径的对象)中由 path(默认为当前目录)命名的相应文件中。如果 set_attrs 为 True,则所有者和时间戳将设置为它们在 TAR 文件中保存的值;否则,提取文件的所有者和时间戳将使用当前用户和时间值进行设置。如果 numeric_owner 为 True,则从 TAR 文件中使用的 UID 和 GID 数字将用于设置提取文件的所有者/组;否则,将使用 TAR 文件中的命名值。(在线文档建议使用 extractall 而不是直接调用 extract,因为 extractall 在内部执行了额外的错误处理。)

| extractall | f.extractall(path='.', members=None, numeric_owner=False) 类似于对 TAR 文件 f 的每个成员调用 extract,或者只对在成员参数中列出的成员调用 extract,并且对在写入提取的成员时发生的 chown、chmod 和 utime 错误进行额外的错误检查。

不要对来自不受信任来源的 Tarfile 使用 extractall

extractall 不检查提取文件的路径,因此存在提取文件具有绝对路径(或包含一个或多个..组件)并因此覆盖潜在敏感文件的风险。^(a) 最好逐个成员地读取并仅在其具有安全路径(即,没有绝对路径或相对路径带有任何..路径组件)时提取它。

|

extractfile f.extractfile(member) 提取由 member(名称或 TarInfo 实例)标识的存档成员,并返回一个具有方法 read、readline、readlines、seek 和 tell 的 io.BufferedReader 对象。
getmember f.getmember(name) 返回一个 TarInfo 实例,其中包含有关字符串 name 命名的存档成员的信息。
getmembers f.getmembers() 返回一个 TarInfo 实例列表,其中每个实例对应于存档 f 中的每个成员,顺序与存档中条目的顺序相同。
getnames f.getnames() 返回一个字符串列表,存档 f 中每个成员的名称,顺序与存档中条目的顺序相同。
gettarinfo f.gettarinfo(name=None, arcname=None, fileobj=None) 返回一个 TarInfo 实例,其中包含有关打开的文件对象 fileobj 的信息,当 fileobj 不是 None 时;否则,是路径字符串 name 的现有文件。name 可能是类似路径的对象。当 arcname 不是 None 时,它被用作生成的 TarInfo 实例的 name 属性。
list f.list(verbose=True, *, members=None) 输出存档 f 的目录到 sys.stdout。如果可选参数 verbose 是 False,则只输出存档成员的名称。如果给定可选参数 members,则必须是 getmembers 返回的列表的子集。
next f.next() 返回下一个可用的存档成员作为 TarInfo 实例;如果没有可用的,则返回 None
^(a) 进一步描述,请参阅 CVE-2007-4559

The TarInfo class

The methods getmember and getmembers of TarFile instances return instances of TarInfo, supplying information about members of the archive. You can also build a TarInfo instance with a TarFile instance’s method gettarinfo. The name argument may be a path-like object. The most useful attributes and methods supplied by a TarInfo instance t are listed in Table 11-11.

Table 11-11. TarInfo 实例 t 的有用属性

isdir() 如果文件是目录,则返回 True
isfile() 如果文件是常规文件,则返回 True
issym() 如果文件是符号链接,则返回 True
linkname t.type 是 LNKTYPE 或 SYMTYPE 时,目标文件的名称(一个字符串)
mode t 标识的文件的权限和其他模式位
mtime 最后修改时间,由 t 标识的文件
name t 标识的存档中文件的名称
size t 标识的文件的大小(未压缩,以字节为单位)
type 文件类型之一,是 tarfile 模块的属性常量之一(符号链接的 SYMTYPE,常规文件的 REGTYPE,目录的 DIRTYPE 等等;请参阅 在线文档 获取完整列表)

The zipfile Module

The zipfile module can read and write ZIP files (i.e., archive files compatible with those handled by popular compression programs such as zip and unzip, pkzip and pkunzip, WinZip, and so on, typically named with a .zip extension). python -m zipfile 提供了一个有用的命令行界面,用于访问模块的功能:运行它而不带其他参数,以获取简短的帮助消息。

有关 ZIP 文件的详细信息可以在PKWAREInfo-ZIP网站上找到。您需要研究这些详细信息以使用 zipfile 执行高级 ZIP 文件处理。如果您不需要与使用 ZIP 文件标准的其他程序进行互操作,则通常最好使用 lzma、gzip 和 bz2 模块来处理压缩,tarfile 则是创建(可选压缩的)存档的更好方法。

zipfile 模块不能处理多磁盘 ZIP 文件,也不能创建加密存档(它可以解密,尽管速度相对较慢)。该模块还不能处理使用除通常的stored(未压缩复制到存档的文件)和deflated(使用 ZIP 格式的默认算法压缩的文件)之外的压缩类型的存档成员。zipfile 还处理 bzip2 和 LZMA 压缩类型,但请注意:并非所有工具都能处理这些类型,因此如果您使用它们,则牺牲了一些可移植性以获得更好的压缩。

zipfile 模块提供 is_zipfile 函数和类 Path,如表 11-12 中列出。此外,它还提供了稍后描述的类 ZipFile 和 ZipInfo。与无效 ZIP 文件相关的错误,zipfile 的函数会引发 zipfile.error 异常的实例。

表 11-12. zipfile 模块的辅助函数和类

is_zipfile is_zipfile(file) 当由字符串、路径样式对象或类似文件对象file命名的文件在文件的前几个和最后几个字节中看起来是有效的 ZIP 文件时返回True;否则返回False
Path class Path(root, at='') 3.8+ 用于 ZIP 文件的 pathlib 兼容包装器。从root,一个 ZIP 文件(可以是 ZipFile 实例或适合传递给 ZipFile 构造函数的文件)返回一个 pathlib.Path 对象p。字符串参数 at 是指定p在 ZIP 文件中位置的路径:默认是根。p公开了几个 pathlib.Path 方法:详细信息请参阅在线文档

ZipFile 类

zipfile 提供的主要类是 ZipFile。其构造函数具有以下签名:

| ZipFile | class ZipFile(file, mode='r', compression=zipfile.ZIP_STORED, allowZip64=True, compresslevel=None, , strict_timestamps=True) 打开名为file*(字符串、类似文件对象或路径样式对象)的 ZIP 文件。mode 可以是'r'以读取现有的 ZIP 文件,'w'以写入新的 ZIP 文件或截断并重写现有文件,或者'a'以追加到现有文件。也可以是'x',类似于'w',但如果 ZIP 文件已存在则引发异常——这里的'x'表示“排他”。

当模式为 'a' 时,file 可以命名为现有的 ZIP 文件(在这种情况下,新成员将添加到现有存档中)或现有的非-ZIP 文件。在后一种情况下,将创建一个新的类似 ZIP 文件的存档,并将其附加到现有文件上。后一种情况的主要目的是让您构建一个在运行时自行解压缩的可执行文件。然后,现有文件必须是自解压缩可执行文件前缀的原始副本,由 www.info-zip.org 和其他 ZIP 文件压缩工具供应商提供。

压缩是写入存档时要使用的 ZIP 压缩方法:ZIP_STORED(默认值)请求存档不使用压缩,ZIP_DEFLATED 请求存档使用压缩的 deflation 模式(ZIP 文件中使用的最常见和有效的压缩方法)。它还可以是 ZIP_BZIP2 或 ZIP_LZMA(牺牲可移植性以获得更多压缩;这些需要分别使用 bz2 或 lzma 模块)。未被识别的值将引发 NotImplementedError。|

| ZipFile (cont.) | 当 allowZip64 为 True(默认值)时,允许 ZipFile 实例使用 ZIP64 扩展来生成大于 4 GB 的存档;否则,任何尝试生成这样一个大存档的尝试都会引发 LargeZipFile 异常。compresslevel 是一个整数(在使用 ZIP_STORED 或 ZIP_LZMA 时被忽略),从 0 表示 ZIP_DEFLATED(1 表示 ZIP_BZIP2),它请求适度的压缩但操作速度较快,到 9 请求最佳压缩以换取更多计算。

将 strict_timestamps 设置为 False 可以存储早于 1980-01-01(将时间戳设置为 1980-01-01)或晚于 2107-12-31(将时间戳设置为 2107-12-31)的文件。|

ZipFile 是一个上下文管理器;因此,您可以在 with 语句中使用它,以确保在完成后关闭底层文件。例如:

`with` zipfile.ZipFile('archive.zip') `as` z:    
    data = z.read('data.txt')

除了实例化时提供的参数之外,ZipFile 实例 z 还具有属性 fp 和 filename,它们是 z 工作的类似文件的对象和其文件名(如果已知);comment,可能是空字符串的存档评论;和 filelist,存档中的 ZipInfo 实例列表。此外,z 还有一个名为 debug 的可写属性,一个从 0 到 3 的 int,您可以分配它来控制在输出到 sys.stdout 时要发出多少调试输出:² 从 z.debug 为 0 时什么都不输出,到 z.debug 为 3 时发出的调试输出为可用的最大量。

ZipFile 实例 z 提供了 Table 11-13 中列出的方法。

表 11-13. ZipFile 实例 z 提供的方法

close close() 关闭存档文件 z。确保调用 z.close(),否则可能会在磁盘上留下不完整且无法使用的 ZIP 文件。这种强制性的最终化通常最好使用 “try/finally” 中介绍的 try/finally 语句执行,或者更好地使用 “The with Statement and Context Managers” 中介绍的 with 语句执行。

| extract | extract(member, path=None, pwd=None) 将存档成员提取到磁盘,到目录或类似路径的对象路径,或者默认情况下提取到当前工作目录;member 是成员的完整名称,或者标识成员的 ZipInfo 实例。extract 会在 member 中规范化路径信息,将绝对路径转换为相对路径,移除任何 .. 组件,并且在 Windows 上,将文件名中非法的字符转换为下划线 (_)。pwd(如果存在)是用于解密加密成员的密码。 |

extract 返回它创建的文件(如果已存在则覆盖),或者返回它创建的目录(如果已存在则不变)。在已关闭的 ZipFile 上调用 extract 会引发 ValueError。 |

extractall extractall(path=None, members=None, pwd=None) 将存档成员提取到磁盘(默认情况下为全部成员),到目录或类似路径的对象路径,或者默认情况下提取到当前工作目录;members 可选地限制要提取的成员,并且必须是由 z.namelist 返回的字符串列表的子集。extractall 会在提取的成员中规范化路径信息,将绝对路径转换为相对路径,移除任何 .. 组件,并且在 Windows 上,将文件名中非法的字符转换为下划线 (_)。pwd(如果存在)是用于解密加密成员(如果有)的密码。
getinfo getinfo(name) 返回一个 ZipInfo 实例,该实例提供有关由字符串 name 指定的存档成员的信息。
infolist infolist() 返回一个 ZipInfo 实例列表,其中包含存档 z 中的每个成员的信息,顺序与存档中的条目相同。
namelist namelist() 返回一个字符串列表,其中包含存档 z 中每个成员的名称,顺序与存档中的条目相同。
open open(name, mode='r', pwd=None, *, force_zip64=False) 提取并返回由 name(成员名称字符串或 ZipInfo 实例)标识的存档成员作为(可能是只读的)类文件对象。mode 可以是 'r' 或 'w'。pwd(如果存在)是用于解密加密成员的密码。在可能的情况下,当未知文件大小可能超过 2 GiB 时,请传递 force_zip64=True,以确保标题格式能够支持大文件。当您预先知道大文件大小时,请使用适当设置文件大小的 ZipInfo 实例进行 name 的操作。
printdir printdir() 将存档 z 的文本目录输出到 sys.stdout。
read read(name, pwd) 提取由 name(成员名称字符串或 ZipInfo 实例)标识的归档成员并返回其内容的字节串(如果在关闭的 ZipFile 上调用则引发 ValueError)。pwd(如果存在)是用于解密加密成员的密码。
setpassword setpassword(pwd) 将字符串 pwd 设置为解密加密文件时使用的默认密码。
testzip testzip() 读取并检查归档 z 中的文件。返回损坏的第一个归档成员的名称的字符串,如果归档完好则返回 None
write write(filename, arcname=None, compress_type=None, compresslevel=None) 将由字符串 filename 指定的文件写入归档 z,并使用 arcname 作为归档成员名称。当 arcname 为 None 时,write 使用 filename 作为归档成员名称。当 compress_type 或 compresslevel 为 None(默认值)时,write 使用 z 的压缩类型和级别;否则,compress_type 和/或 compresslevel 指定如何压缩文件。z 必须以 'w'、'x' 或 'a' 模式打开;否则将引发 ValueError。

| writestr | writestr(zinfo_arc, data, compress_type=None*,* compresslevel=None)

使用指定的元数据 zinfo_arcdata 中的数据向归档 z 添加成员。zinfo_arc 必须是指定至少 filenamedate_time 的 ZipInfo 实例,或者是用作归档成员名称的字符串,日期和时间设置为当前时刻。data 是 bytes 或 str 的实例。当 compress_type 或 compresslevel 为 None(默认值)时,writestr 使用 z 的压缩类型和级别;否则,compress_type 和/或 compresslevel 指定如何压缩文件。z 必须以 'w'、'x' 或 'a' 模式打开;否则将引发 ValueError。

当你有内存中的数据并需要将数据写入到 ZIP 文件归档 z 中时,使用 z.writestr 比 z.write 更简单更快。后者需要你首先将数据写入磁盘,然后再删除无用的磁盘文件;而前者可以直接编码:

`import` zipfile
`with` zipfile.ZipFile('z.zip', 'w') `as` zz:
    data = 'four score\nand seven\nyears ago\n'
    zz.writestr('saying.txt', data)

这里是如何打印由上一个示例创建的 ZIP 文件归档中包含的所有文件的列表,以及每个文件的名称和内容:

`with` zipfile.ZipFile('z.zip') `as` zz:
    zz.printdir()
    `for` name `in` zz.namelist():
        print(f'{name}: {zz.read(name)!r}')

|

ZipInfo 类

ZipFile 实例的 getinfo 和 infolist 方法返回 ZipInfo 类的实例,用于提供有关归档成员的信息。Table 11-14 列出了 ZipInfo 实例 z 提供的最有用的属性。

Table 11-14. ZipInfo 实例 z 的有用属性

comment 归档成员的评论字符串
compress_size 归档成员压缩数据的字节大小
compress_type 记录归档成员压缩类型的整数代码
date_time 一个由六个整数组成的元组,表示文件的最后修改时间:年(>=1980)、月、日(1+)、小时、分钟、秒(0+)
file_size 存档成员的未压缩数据大小,以字节为单位
filename 存档中文件的名称

os 模块

os 是一个综合模块,提供了对各种操作系统能力的几乎统一的跨平台视图。它提供了低级方法来创建和处理文件和目录,以及创建、管理和销毁进程。本节介绍了 os 的与文件系统相关的函数;“使用 os 模块运行其他程序” 则涵盖了与进程相关的函数。大多数情况下,你可以使用更高级的抽象级别的其他模块来提高生产力,但理解底层 os 模块中的“底层”内容仍然非常有用(因此我们进行了覆盖)。

os 模块提供了一个 name 属性,这是一个字符串,用于标识 Python 运行的平台类型。name 的常见值包括 'posix'(各种 Unix 类平台,包括 Linux 和 macOS)和 'nt'(各种 Windows 平台);'java' 是老旧但仍然想念的 Jython。你可以通过 os 提供的函数利用某个平台的一些独特功能。然而,本书关注的是跨平台编程,而不是特定于平台的功能,因此我们不涵盖仅存在于一个平台上的 os 部分,也不涵盖特定于平台的模块:本书涵盖的功能至少在 'posix' 和 'nt' 平台上可用。然而,我们确实涵盖了在各个平台上提供给定功能的方式之间的一些差异。

文件系统操作

使用 os 模块,你可以以多种方式操作文件系统:创建、复制和删除文件和目录;比较文件;以及检查关于文件和目录的文件系统信息。本节记录了你用于这些目的的 os 模块的属性和方法,并涵盖了一些操作文件系统的相关模块。

os 模块的路径字符串属性

文件或目录由一个字符串标识,称为其路径,其语法取决于平台。在类 Unix 和 Windows 平台上,Python 接受 Unix 路径的语法,斜线 (/) 作为目录分隔符。在非 Unix 类平台上,Python 还接受特定于平台的路径语法。特别是在 Windows 上,你可以使用反斜杠 () 作为分隔符。然而,在字符串文字中,你需要将每个反斜杠双写为 \,或者使用原始字符串文字语法(如 “字符串” 中所述);但这会使程序失去可移植性。Unix 路径语法更加方便,可以在任何地方使用,因此我们强烈建议始终使用它。在本章的其余部分,我们在解释和示例中都使用 Unix 路径语法。

os 模块提供了关于当前平台路径字符串的属性,详见 Table 11-15。通常应使用高级别的路径操作(见 “The os.path Module”³),而非基于这些属性的低级别字符串操作。然而,在某些情况下,这些属性可能会很有用。

Table 11-15. os 模块提供的属性

curdir 表示当前目录的字符串(在 Unix 和 Windows 上为 '.')
defpath 程序的默认搜索路径,如果环境缺少 PATH 环境变量则使用
extsep 文件名扩展部分与其余部分之间的分隔符(在 Unix 和 Windows 上为 '.')
linesep 终止文本行的字符串(在 Unix 上为 '\n';在 Windows 上为 '\r\n')
pardir 表示父目录的字符串(在 Unix 和 Windows 上为 '..')
pathsep 字符串列表中路径之间的分隔符,比如用于环境变量 PATH 的(在 Unix 上为 ':';在 Windows 上为 ';')
sep 路径组件的分隔符(在 Unix 上为 '/';在 Windows 上为 '\')

权限

类 Unix 平台为每个文件或目录关联九个位:三个位为文件的所有者、其组和其他人(即“others”或“the world”),指示该文件或目录能否被给定主体读取、写入和执行。这九个位称为文件的 权限位,是文件 模式 的一部分(一个包括描述文件的其他位的位字符串)。通常以八进制表示这些位,每个数字表示三个位。例如,模式 0o664 表示一个文件,其所有者和组可以读取和写入,任何其他人只能读取,不能写入。在 Unix 类似系统上,当任何进程创建文件或目录时,操作系统将应用称为进程的 umask 的位掩码,可以移除一些权限位。

非 Unix 类似平台以非常不同的方式处理文件和目录权限。然而,处理文件权限的 os 函数接受一个根据前述 Unix 类似方法的 模式 参数。每个平台将这九个权限位映射到适合其的方式。例如,在 Windows 上,它只区分只读和读写文件,并且不记录文件所有权,文件的权限位显示为 0o666(读/写)或 0o444(只读)。在这样的平台上,创建文件时,实现只关注位 0o200,当该位为 1 时文件为读/写,为 0 时为只读。

os 模块的文件和目录函数

os 模块提供了几个函数(在 Table 11-16 中列出)来查询和设置文件和目录状态。在所有版本和平台上,这些函数的参数 path 都可以是给定所涉及文件或目录路径的字符串,也可以是路径类对象(特别是 pathlib.Path 的实例,在本章后面介绍)。在一些 Unix 平台上还有一些特殊性:

  • 一些函数还支持 文件描述符fd)——一个整数,表示例如由 os.open 返回的文件作为 path 参数。模块属性 os.supports_fd 是支持此行为的 os 模块中的函数集合(在不支持此类功能的平台上,该模块属性将缺失)。

  • 一些函数支持可选的仅限关键字参数 follow_symlinks,默认为 True。当此参数为 True 时,如果 path 指示一个符号链接,则函数跟随它以达到实际的文件或目录;当此参数为 False 时,函数在符号链接本身上操作。模块属性 os.supports_follow_symlinks(如果存在)是支持此参数的 os 模块中的函数集合。

  • 一些函数支持可选的仅限命名参数 dir_fd,默认为 None。当 dir_fd 存在时,path(如果是相对路径)被视为相对于在该文件描述符上打开的目录;当缺少 dir_fd 时,path(如果是相对路径)被视为相对于当前工作目录。如果 path 是绝对路径,则忽略 dir_fd。模块属性 os.supports_dir_fd(如果存在)是支持该参数的 os 模块函数集合。

此外,某些平台上的命名参数 effective_ids,默认为 False,允许您选择使用有效的而不是真实的用户和组标识符。通过 os.supports_effective_ids 检查它在您的平台上是否可用。

Table 11-16. os 模块函数

| access | access(path, mode, *, dir_fd=None, effective_ids=False, follow_symlinks=True)

当文件或类似路径对象 path 具有整数 mode 编码的所有权限时返回 True;否则返回 Falsemode 可以是 os.F_OK 以测试文件存在性,或者是 os.R_OK、os.W_OK 和 os.X_OK 中的一个或多个(如果有多个,则使用按位或运算符 | 连接)以测试读取、写入和执行文件的权限。如果 dir_fd 不是 None,则 access 在相对于提供的目录上操作 path(如果 path 是绝对路径,则忽略 dir_fd)。传递关键字参数 effective_ids=True(默认为 False)以使用有效的而不是真实的用户和组标识符(这在所有平台上可能不起作用)。如果传递 follow_symlinks=False 并且 path 的最后一个元素是符号链接,则 access 在符号链接本身上操作,而不是在链接指向的文件上操作。

access 不使用其 mode 参数的标准解释,详细内容请参阅前一节。相反,access 仅测试此特定进程的真实用户和组标识符对文件的请求权限。如果需要更详细地研究文件的权限位,请参阅后面在此表中介绍的 stat 函数。

不要使用 access 来检查用户在打开文件之前是否被授权打开文件;这可能存在安全漏洞。 |

chdir chdir(path) 将进程的当前工作目录设置为 pathpath 可以是文件描述符或类似路径的对象。

| chmod, lchmod | chmod(path, *mode, , dir_fd=None, follow_symlinks=True) lchmod(path, mode)

更改文件(或文件描述符或类似路径的对象)path 的权限,权限由整数 mode 编码。 mode 可以是 os.R_OK、os.W_OK 和 os.X_OK 的任意组合(如果有多个则用按位或运算符 | 连接)来表示读、写和执行权限。在类 Unix 平台上,mode 可以是更复杂的位模式(如前一节所述),用于指定用户、组和其他对象的不同权限,还可以定义模块 stat 中的其他特殊且不常用的位。具体详细信息请参阅 在线文档。传递 follow_symlinks=False(或使用 lchmod)来更改符号链接的权限,而不是该链接的目标文件。 |

DirEntry 类 DirEntry 的实例 d 提供属性 namepath,分别保存条目的基本名称和完整路径,以及几种方法,其中最常用的是 is_dir、is_file 和 is_symlink。is_dir 和 is_file 默认会跟随符号链接:传递 follow_symlinks=False 可以避免此行为。d 在尽可能的情况下避免系统调用,并在需要时缓存结果。如果需要确保获取的信息是最新的,可以调用 os.stat(d.path) 并使用其返回的 stat_result 实例;不过这可能会牺牲 scandir 的性能提升。有关更详细的信息,请参阅 在线文档

| getcwd, getcwdb | getcwd(), getcwdb()

getcwd 返回一个 str,表示当前工作目录的路径。getcwdb 返回一个 bytes 字符串(在 Windows 上使用 UTF-8 编码,3.8+ 版本)。 |

| link | link(src, dst, ***, src_dir_fd=None, dst_dir_fd=None, follow_symlinks=True)

创建名为 dst 的硬链接,指向 srcsrcdst 都可以是类似路径的对象。设置 src_dir_fd 和/或 dst_dir_fd 以便链接操作使用相对路径,并传递 follow_symlinks=False 只在符号链接上操作,而不是该链接的目标文件。要创建符号(“软”)链接,请使用稍后在此表中介绍的 symlink 函数。 |

listdir listdir(path='.') 返回列表,其中的项是目录中所有文件和子目录的名称,文件描述符(指向目录)或类路径对象 path。列表顺序任意,并且包括特殊目录名称 '.'(当前目录)和 '..'(父目录)。当 path 类型为 bytes 时,返回的文件名也为 bytes 类型;否则为 str 类型。请参阅表中稍后的替代函数 scandir,在某些情况下可提供性能改进。在调用此函数期间,请勿从目录中删除或添加文件:这可能会产生意外的结果。

| mkdir, makedirs | mkdir(path, mode=0777, dir_fd=None), makedirs(path, mode=0777, exist_ok=False)

mkdir 仅创建 path 中最右侧的目录,如果 path 中的前面任何目录不存在,则引发 OSError。mkdir 可接受 dir_fd 以相对于文件描述符的方式访问路径。makedirs 创建 path 中尚不存在的所有目录(传递 exist_ok=True 可避免引发 FileExistsError)。

两个函数都使用 mode 作为它们创建的目录的权限位,但某些平台和某些新创建的中间级目录可能会忽略 mode;使用 chmod 明确设置权限。

| remove, unlink | remove(path, **, dir_fd=*None), unlink(path, ***, dir_fd=None)

删除文件或类路径对象 path(相对于 dir_fd)。请参阅表中稍后的 rmdir 来删除目录而不是文件。unlink 是 remove 的同义词。

removedirs removedirs(path) 从右到左遍历 path 的目录部分(可能是类路径对象),依次移除每个目录。循环在遇到异常(通常是因为目录不为空)时结束。只要 removedirs 至少删除了一个目录,就不会传播异常。

| rename, renames | rename(src, dst, ***, src_dir_fd=None, dst_dir_fd=None), renames(src, dst, /)

renames(“移动”)文件、类路径对象或名为 src 的目录到 dst。如果 dst 已存在,rename 可能会替换 dst 或引发异常;要保证替换,请调用 os.replace 函数。要使用相对路径,传递 src_dir_fd 和/或 dst_dir_fd。

renames 功能与 rename 类似,但它会为 dst 创建所有必要的中间目录。重命名后,renames 使用 removedirs 从路径 src 中移除空目录。如果重命名未清空 src 的起始目录,不会传播任何导致的异常;这并不算是错误。renames 无法接受相对路径参数。

rmdir rmdir(path, ***, dir_fd=None) 会删除名为 path(可能相对于 dir_fd)的空目录或类路径对象。如果删除失败,特别是如果目录不为空,则会引发 OSError。
scandir scandir(path='.') 返回一个迭代器,针对路径中的每个项目产生一个 os.DirEntry 实例,该路径可以是字符串、类似路径的对象或文件描述符。 使用 scandir 并调用每个结果项的方法以确定其特征可以提供性能改进,相比于使用 listdir 和 stat,这取决于底层平台。 scandir 可以用作上下文管理器:例如,with os.scandir(path) as itr: 在完成时确保关闭迭代器(释放资源)。

| stat, lstat,

fstat | stat(path, ***, dir_fd=None, follow_symlinks=True), lstat(path, **, dir_fd=*None),

fstat(fd)

stat 返回类型为 stat_result 的值 x,它提供关于路径的(至少)10 个信息项。 path 可以是文件、文件描述符(在这种情况下,您可以使用 stat(fd) 或 fstat,它仅接受文件描述符)、类似路径的对象或子目录。 path 可以是 dir_fd 的相对路径,也可以是符号链接(如果 follow_symlinks=False,或者使用 lstat;在 Windows 上,除非 follow_symlinks=False,否则会遵循操作系统可以解析的所有重解析点)。 stat_result 值是一个包含值元组,也支持对其包含值的每个命名访问(类似于 collections.namedtuple,尽管未实现为此类)。 访问 stat_result 的项目通过其数字索引是可能的,但不建议,因为结果代码不可读; 相反,请使用相应的属性名称。 表 11-17 列出了 stat_result 实例的主要 10 个属性以及相应项的含义。

表 11-17. stat_result 实例的项(属性)

| 项目索引 | 属性名称 | 含义 |

| --- | --- | --- |

| 0 | st_mode | 保护和其他模式位 |

| 1 | st_ino | inode 号码 |

| 2 | st_dev | 设备 ID |

| 3 | st_nlink | 硬链接数量 |

| 4 | st_uid | 所有者的用户 ID |

| 5 | st_gid | 所有者的组 ID |

| 6 | st_size | 大小(以字节为单位) |

| 7 | st_atime | 最后访问时间 |

| 8 | st_mtime | 最后修改时间 |

| 9 | st_ctime | 最后状态更改时间 |

例如,要打印文件 path 的大小(以字节为单位),您可以使用以下任何一种:

`import` os
print(os.stat(path)[6])       *`# works but unclear`*
print(os.stat(path).st_size)  *`# easier to understand`*
print(os.path.getsize(path))  *`# convenience function`*
                            *`# that wraps stat`*

时间值以自纪元以来的秒数表示,如 第十三章 中所述(在大多数平台上为整数)。 无法为项目提供有意义值的平台使用虚拟值。 对于 stat_result 实例的其他、特定于平台的属性,请参阅 在线文档

symlink symlink(target, symlink_path, target_is_directory=False, , dir_fd=None) 创建名为symlink_path的符号链接,指向文件、目录或路径对象target*。target可以相对于 dir_fd。target_is_directory 仅在 Windows 系统上使用,用于指定创建的符号链接应表示文件还是目录;在非 Windows 系统上,此参数将被忽略。(在 Windows 上运行 os.symlink 通常需要提升的权限。)
utime utime(path, times=None, , [ns, ]dir_fd=None, follow_symlinks=True) 设置文件、目录或路径对象path*的访问时间和修改时间。path可以相对于 dir_fd,并且如果 follow_symlinks=Falsepath可以是符号链接。如果 times 为None**,utime 使用当前时间。否则,times 必须是一对数字(自纪元以来的秒数,详见第十三章),顺序为(accessedmodified)。要指定纳秒,请将ns传递为(acc_nsmod_ns),其中每个成员都是表示自纪元以来的纳秒数的整数。不要同时指定 times 和ns

| walk, fwalk | walk(top, topdown=True, onerror=None, followlinks=False), fwalk(top='.', topdown=True, onerror=None, *, follow_symlinks=False, dir_fd=None)

walk 是一个生成器,为树的每个目录生成一个条目,树的根是目录或路径对象top。当 topdown 为True(默认)时,walk 从树的根向下访问目录;当 topdown 为False时,walk 从树的叶子向上访问目录。默认情况下,walk 捕获并忽略树遍历过程中引发的任何 OSError 异常;将 onerror 设置为可调用对象,以捕获树遍历过程中引发的任何 OSError 异常,并将其作为唯一参数传递给 onerror,可对其进行处理、忽略或引发以终止树遍历并传播异常(文件名可作为异常对象的 filename 属性访问)。

walk 每生成的条目是一个包含三个子项的元组:dirpath,是目录的路径字符串;dirnames,是该目录的直接子目录名称列表(特殊目录'.'和'..' 包括在内);filenames,是该目录中直接的文件名称列表。如果 topdown 为True,您可以直接修改dirnames列表,删除一些项目和/或重新排序其他项目,以影响从dirpath开始的子树遍历;walk 仅迭代剩余在dirnames中的子目录,按照它们留下的顺序。如果 topdown 为False,这些修改将不会生效(在这种情况下,walk 在访问当前目录和生成其条目时已经访问了所有子目录)。 |

| walk, fwalk

(续) | 默认情况下,walk 不会遍历解析为目录的符号链接。要获得这样的额外遍历,请传递 followlinks=True,但请注意:如果符号链接解析为其祖先的目录,这可能会导致无限循环。walk 对此异常没有采取预防措施。

followlinks 与 follow_symlinks

注意,在所有情况下,对于 os.walk ,名为 follow_symlinks 的参数实际上被命名为 followlinks。

fwalk(仅 Unix)类似于 walk,但 top 可能是文件描述符 dir_fd 的相对路径,并且 fwalk 产生四元组:前三个成员(dirpath、dirnames 和 filenames)与 walk 的生成值相同,第四个成员是 dirfd,即 dirpath 的文件描述符。请注意,无论是 walk 还是 fwalk,默认情况下 跟随符号链接。|

文件描述符操作

除了前面介绍的许多函数外,os 模块还提供了几个专门与文件描述符一起使用的函数。文件描述符 是操作系统用作不透明句柄以引用打开文件的整数。尽管通常最好使用 Python 文件对象(在 “The io Module” 中介绍),有时使用文件描述符可以让您执行某些操作更快,或者(可能牺牲可移植性)以不直接可用于 io.open 的方式执行操作。文件对象和文件描述符不能互换。

要获取 Python 文件对象 f 的文件描述符 n,请调用 n = f.fileno()。要使用现有打开的文件描述符 fd 创建新的 Python 文件对象 f,请使用 f = os.fdopen(fd),或者将 fd 作为 io.open 的第一个参数传递。在类 Unix 和 Windows 平台上,某些文件描述符在进程启动时预分配:0 是进程的标准输入的文件描述符,1 是进程的标准输出的文件描述符,2 是进程的标准错误的文件描述符。调用诸如 dup 或 close 等 os 模块方法来操作这些预分配的文件描述符,对于重定向或操作标准输入和输出流可能会很有用。

os 模块提供了许多处理文件描述符的函数;其中一些最有用的列在 Table 11-18 中。

表 11-18. 处理文件描述符的有用 os 模块函数

close close(fd) 关闭文件描述符 fd
closerange closerange(fd_low, fd_high) 关闭从 fd_low(包括 fd_low)到 fd_high(不包括 fd_high)的所有文件描述符,忽略可能发生的任何错误。
dup dup(fd) 返回复制文件描述符 fd 的文件描述符。
dup2 dup2(fd, fd2) 复制文件描述符 fd 到文件描述符 fd2。当文件描述符 fd2 已经打开时,dup2 首先关闭 fd2
fdopen fdopen(fd, *a, **k) 类似于 io.open,不同之处在于 fd 必须 是打开文件描述符的整数。
fstat fstat(fd) 返回一个 stat_result 实例 x,其中包含有关打开在文件描述符 fd 上的文件的信息。表 11-17 涵盖了 x 的内容。
lseek lseek(fd, pos, how) 将文件描述符 fd 的当前位置设置为有符号整数字节偏移量 pos,并返回从文件开头的结果字节偏移量。how 表示参考点(点 0)。当 how 是 os.SEEK_SET 时,pos 为 0 表示文件开头;对于 os.SEEK_CUR,它表示当前位置;对于 os.SEEK_END,它表示文件末尾。例如,lseek(fd, 0, os.SEEK_CUR) 返回从文件开头到当前位置的当前位置的字节偏移量,而不影响当前位置。普通磁盘文件支持寻址;对不支持寻址的文件(例如,向终端输出的文件)调用 lseek 会引发异常。

| open | open(file, flags, mode=0o777) 返回一个文件描述符,打开或创建名为 file 的文件。当 open 创建文件时,使用 mode 作为文件的权限位。flags 是一个整数,通常是 os 模块以下一个或多个属性的按位 OR 运算(使用操作符 | ):

O_APPEND

追加任何新数据到 file 当前的内容

O_BINARY

在 Windows 平台上以二进制而非文本模式打开 file(在类 Unix 平台上引发异常)

O_CREAT

如果 file 不存在,则创建 file

O_DSYNC, O_RSYNC, O_SYNC, O_NOCTTY

根据平台支持的情况设置同步模式

O_EXCL

如果 file 已经存在,则抛出异常

O_NDELAY, O_NONBLOCK

如果平台支持,以非阻塞模式打开 file

O_RDONLY, O_WRONLY, O_RDWR

分别打开 file 以进行只读、只写或读/写访问(互斥:这些属性中必须有且仅有一个 flags

O_TRUNC

丢弃 file 的先前内容(与 O_RDONLY 不兼容)

|

pipe pipe() 创建一个管道并返回一对文件描述符(r_fdw_fd),分别用于读取和写入。
read read(fd, n) 从文件描述符 fd 读取最多 n 字节,并将它们作为字节串返回。当只有 m 个字节当前可供从文件读取时,读取并返回 m < n 字节。特别地,在没有更多字节当前可用于读取时,通常因为文件已经结束,返回空字符串。
write write(fd, s) 将字节串 s 中的所有字节写入文件描述符 fd 并返回写入的字节数。

os.path 模块

os.path 模块提供了用于分析和转换路径字符串和类似路径对象的函数。该模块中最常用的有用函数列在 表 11-19 中。

表 11-19. os.path 模块的常用函数

| abspath | abspath(path) 返回与 path 等效的标准化绝对路径字符串,就像(在 path 是当前目录中文件名的情况下):

os.path.normpath(os.path.join(os.getcwd(), path))

例如,os.path.abspath(os.curdir) 与 os.getcwd() 相同。

basename basename(path) 返回 path 的基本名称部分,就像 os.path.split(path)[1]。例如,os.path.basename('b/c/d.e') 返回 'd.e'。
commonpath commonpath(list) 接受字符串或类似路径对象的序列,并返回最长公共子路径。与 commonprefix 不同,只返回有效路径;如果 list 为空、包含绝对和相对路径的混合或包含不同驱动器上的路径,则引发 ValueError。
com⁠m⁠o⁠n​p⁠r⁠e⁠fix commonprefix(list) 接受字符串或类似路径对象的列表,并返回列表中所有项目的最长公共前缀字符串,如果 list 为空则返回 '.'。例如,os.path.commonprefix(['foobar', 'foolish']) 返回 'foo'。可能返回无效路径;如果要避免此问题,请参阅 commonpath。
dirname dirname(path) 返回 path 的目录部分,就像 os.path.split(path)[0]。例如,os.path.dirname('b/c/d.e') 返回 'b/c'。
exists, lexists exists(path), lexists(path) 当 path 是现有文件或目录的名称时,exists 返回 Truepath 也可以是打开的文件描述符或类似路径对象);否则返回 False。换句话说,os.path.exists(x) 与 os.access(x, os.F_OK) 相同。lexists 也是如此,但在 path 是指示不存在的文件或目录的现有符号链接(有时称为损坏的符号链接)时,也返回 True,而在这种情况下,exists 返回 False。对于包含操作系统级别不可表示的字符或字节的路径,两者均返回 False

| expandvars, expanduser | expandvars(path), expanduser(path) 返回字符串或类似路径对象 path 的副本,在其中形如 $name 或 ${name}(仅在 Windows 上为 %name%)的每个子字符串都替换为环境变量 name 的值。例如,如果环境变量 HOME 设置为 /u/alex,则以下代码:

`import` os
print(os.path.expandvars('$HOME/foo/'))

发射 /u/alex/foo/.

os.path.expanduser 将前导的 ~ 或 ~user(如果有)扩展为当前用户的主目录路径。

| getatime, getctime,

getmtime,

getsize | getatime(path), getctime(path), getmtime(path), getsize(path) 每个函数调用 os.stat(path) 并从结果中返回一个属性:分别是 st_atime、st_ctime、st_mtime 和 st_size。有关这些属性的更多详细信息,请参阅 Table 11-17。

isabs isabs(path) 当 path 是绝对路径时返回 True(路径以斜杠 (/ 或 ) 或在某些非类 Unix 平台(如 Windows)上以驱动器指示符开头,后跟 os.sep)。否则,isabs 返回 False
isdir isdir(path) 当 path 指定的是现有目录时返回 True(isdir 跟随符号链接,因此 isdir 和 islink 可能都返回 True);否则返回 False
isfile isfile(path) 当 path 指定的是现有常规文件时返回 True(isfile 跟随符号链接,因此 islink 也可能为 True);否则返回 False
islink islink(path) 当 path 指定的是符号链接时返回 True;否则返回 False
ismount ismount(path) 当 path 指定的是 挂载点 时返回 True;否则返回 False

| join | join(path, *paths) 返回一个字符串,它使用当前平台的适当路径分隔符连接参数(字符串或类路径对象)。例如,在 Unix 上,相邻路径组件之间使用一个斜杠字符 / 分隔。如果任何参数是绝对路径,join 将忽略先前的参数。例如:

print(os.path.join('a/b', 'c/d', 'e/f'))
*`# on Unix prints: a/b/c/d/e/f`*
print(os.path.join('a/b', '/c/d', 'e/f'))
*`# on Unix prints:`* *`/c/d/e/f`*

第二次调用 os.path.join 忽略了其第一个参数 'a/b',因为其第二个参数 '/c/d' 是绝对路径。 |

normcase normcase(path) 返回 path 的大小写标准化副本,以适应当前平台。在区分大小写的文件系统(如 Unix 类系统)中,返回 path 本身。在不区分大小写的文件系统(如 Windows)中,将字符串转换为小写。在 Windows 上,normcase 还会将每个 / 转换为 \\。
normpath normpath(path) 返回一个等效于 path 的标准化路径名,移除冗余的分隔符和路径导航部分。例如,在 Unix 上,当 path 是 'a//b'、'a/./b' 或 'a/c/../b' 之一时,normpath 返回 'a/b'。normpath 使路径分隔符适合当前平台。例如,在 Windows 上,分隔符变成 \\。
realpath realpath(path, ***, strict=False) 返回指定文件或目录或路径类对象的实际路径,同时解析符号链接。3.10+ 设置 strict=True 可以在 path 不存在或存在符号链接循环时引发 OSError。
relpath relpath(path, start=os.curdir) 返回到目录 start 的相对于文件或目录 path(一个字符串或路径类对象)的路径。
samefile samefile(path1, path2) 如果两个参数(字符串或路径类对象)引用同一文件或目录,则返回 True
sameopenfile sameopenfile(fd1, fd2) 如果两个参数(文件描述符)引用同一文件或目录,则返回 True
samestat samestat(stat1, stat2) 如果两个参数(通常是 os.stat 调用的结果 os.stat_result 实例)引用同一文件或目录,则返回 True
split split(path) 返回一对字符串(dir, base),使得 join(dir, base)等于pathbase是最后的组成部分,永远不包含路径分隔符。当path以分隔符结尾时,base为空字符串。dirpath的前导部分,直到最后一个分隔符之前的部分。例如,os.path.split('a/b/c/d') 返回 ('a/b/c', 'd')。
splitdrive splitdrive(path) 返回一对字符串(drv, pth),使得drv+pth等于pathdrv是驱动器规范,或者是'';在没有驱动器规范的平台(如类 Unix 系统)上,它始终为''。在 Windows 上,os.path.splitdrive('c:d/e') 返回 ('c:', 'd/e')。
splitext splitext(path) 返回一对(root, ext),使得root+ext等于pathext要么是空字符串,要么以'.'开头且没有其他'.'或路径分隔符。例如,os.path.splitext('a.a/b.c.d') 返回一对 ('a.a/b.c', '.d')。

OSError 异常

当操作系统请求失败时,os 会引发异常,即 OSError 的一个实例。os 还使用 os.error 作为 OSError 的同义词。OSError 的实例具有三个有用的属性,详见 Table 11-20。

表 11-20. OSError 实例的属性

errno 操作系统错误的数值错误代码
filename 操作失败的文件名(仅限文件相关函数)
strerror 简要描述错误的字符串

OSError 有多个子类用于指定问题所在,详见“OSError 子类”。

当使用无效的参数类型或值调用 os 函数时,它们还可以引发其他标准异常,如 TypeError 或 ValueError,以便它们甚至不尝试基础操作系统功能。

errno 模块

errno 模块为操作系统错误代码数值提供了几十个符号名称。使用 errno 根据错误代码有选择地处理可能的系统错误,这将增强程序的可移植性和可读性。然而,使用适当的 OSError 子类进行选择性的异常处理通常比 errno 更好。例如,为了处理“文件未找到”错误,同时传播所有其他类型的错误,可以使用:

`import` errno
`try`:
    os.some_os_function_or_other()
`except` FileNotFoundError `as` err:
    print(f'Warning: file {err.filename!r} not found; continuing')
`except` OSError `as` oserr:
    print(f'Error {errno.errorcode[oserr.errno]}; continuing')

errno 提供了一个名为 errorcode 的字典:键是错误代码数值,对应的值是错误名称字符串,如'ENOENT'。在一些 OSError 实例的错误背后解释中显示 errno.errorcode[err.errno]通常可以使诊断对专门从事特定平台的读者更加清晰和易懂。

pathlib 模块

pathlib 模块提供了一种面向对象的文件系统路径处理方法,整合了多种处理路径和文件的方法,将它们作为对象而不是字符串处理(与 os.path 不同)。对于大多数用例,pathlib.Path 将提供所需的一切。在少数情况下,您可能需要实例化一个特定于平台的路径,或者一个不与操作系统交互的“纯”路径;如果需要此类高级功能,请参阅 在线文档。pathlib.Path 最常用的函数列在 Table 11-21 中,包括一个 pathlib.Path 对象 p 的示例。在 Windows 上,pathlib.Path 对象返回为 WindowsPath;在 Unix 上,返回为 PosixPath,如 Table 11-21 中的示例所示。(为了清晰起见,我们只是导入 pathlib 而不使用更常见和惯用的 from pathlib import Path。)

pathlib 方法返回路径对象,而非字符串

请记住,pathlib 方法通常返回路径对象,而不是字符串,因此与 os 和 os.path 中类似的方法的结果 相同。

Table 11-21. pathlib.Path 的常用方法

| chmod, lchmod | p.chmod(mode, follow_symlinks=True), p.lchmod(mode)

chmod 修改文件模式和权限,如 os.chmod(参见 Table 11-16)。在 Unix 平台上,3.10+ 将 follow_symlinks 设置为False,以修改符号链接的权限而不是其目标,或使用 lchmod。有关 chmod 设置的更多信息,请参见 在线文档。lchmod 类似于 chmod,但当 p 指向一个符号链接时,会更改符号链接而不是其目标。相当于 pathlib.Path.chmod(follow_symlinks=False)。 |

cwd pathlib.Path.cwd() 返回当前工作目录作为路径对象。
exists p.exists() 当 p 指定的是现有文件或目录(或指向现有文件或目录的符号链接)时返回 True;否则返回 False
expanduser p.expanduser() 返回一个新的路径对象,其中 leading ~ 扩展为当前用户的主目录路径,或者 ~user 扩展为给定用户的主目录路径。另请参见本表中稍后的 home。

| glob, rglob | p.glob(pattern), p.rglob(pattern)

p 目录中以任意顺序生成所有匹配的文件。pattern 可以包含 ** 以允许在 p 或任何子目录中进行递归匹配;rglob 总是在 p 和所有子目录中执行递归匹配,就像 pattern 以 '**/' 开始一样。例如:

>>> sorted(td.glob('*'))
[WindowsPath('tempdir/bar'), 
WindowsPath('tempdir/foo')]
>>> sorted(td.glob('**/*'))
[WindowsPath('tempdir/bar'),
WindowsPath('tempdir/bar/baz'), 
WindowsPath('tempdir/bar/boo'), 
WindowsPath('tempdir/foo')]
>>> sorted(td.glob('*/**/*')) *`# expanding at 2nd+ level`*
[WindowsPath('tempdir/bar/baz'), 
WindowsPath('tempdir/bar/boo')]
>>> sorted(td.rglob('*'))  *`# just like glob('**/*')`*
[WindowsPath('tempdir/bar'), 
WindowsPath('tempdir/bar/baz'), 
WindowsPath('tempdir/bar/boo'), 
WindowsPath('tempdir/foo')]

|

hardlink_to p.hardlink_to(target) 3.10+ 将 p 设置为与 target 相同文件的硬链接。取代了已弃用的 link_to 3.8+,-3.10 注意:link_to 的参数顺序类似于 os.link,在 Table 11-16 中描述;而 hardlink_to 的顺序与此表后面的 symlink_to 相似,正好相反。
home pathlib.Path.home() 返回用户的主目录作为一个路径对象。
is_dir p.is_dir() 当p表示现有目录(或指向目录的符号链接)时返回True;否则返回False
is_file p.is_file() 当p表示现有文件(或指向文件的符号链接)时返回True;否则返回False
is_mount p.is_mount() 当p是一个挂载点(文件系统中已经挂载了另一个文件系统的地方)时返回True;否则返回False。详细信息请参阅在线文档。在 Windows 上未实现。
is_symlink p.is_symlink() 当p表示现有符号链接时返回True;否则返回False
iterdir p.iterdir() 以任意顺序生成目录p的内容的路径对象(不包括'.'和'..')。当p不是目录时引发 NotADirectoryError。如果在创建迭代器后并且在使用完成之前,从p中删除文件或向p中添加文件,则可能产生意外结果。

| mkdir | p.mkdir(mode=0o777, parents=False, exist_ok=False) 在路径处创建一个新目录。使用 mode 设置文件模式和访问标志。设置 parents=True以根据需要创建任何缺少的父目录。设置 exist_ok=True以忽略 FileExistsError 异常。例如: |

>>> td=pathlib.Path('tempdir/')
>>> td.mkdir(exist_ok=True)
>>> td.is_dir()
True

查看详细内容请参阅在线文档。 |

open p.open(mode='r', buffering=-1, encoding=None, errors=None, newline=None) 打开路径指向的文件,类似于内置的 open(p)(其他参数相同)。
read_bytes p.read_bytes() 返回p的二进制内容作为一个 bytes 对象。
read_text p.read_text(encoding=None, errors=None) 返回以字符串形式解码后的p的内容。
readlink p.readlink() 3.9+ 返回符号链接指向的路径。
rename p.rename(target) 将p重命名为target,并且 3.8+返回一个指向target的新 Path 实例。target可以是字符串,绝对路径或相对路径;但是,相对路径将相对于当前工作目录而不是p的目录进行解释。在 Unix 上,当target为现有文件或空目录时,rename 在用户有权限时静默替换它;在 Windows 上,rename 会引发 FileExistsError。

| replace | p.replace(target) 类似p.rename(target),但在任何平台上,当target为现有文件(或在 Windows 以外的平台上为空目录)时,replace 会在用户有权限时静默替换它。例如: |

>>> p.read_text()
'spam'
>>> t.read_text()
'and eggs'
>>> p.replace(t)
WindowsPath('C:/Users/annar/testfile.txt')
>>> t.read_text()
'spam'
>>> p.read_text()
Traceback (most recent call last):
...
FileNotFoundError: [Errno 2] No such file...

|

| resolve | p.resolve(strict=False) 返回一个新的绝对路径对象,解析了符号链接并消除了任何'..'组件。设置 strict=True可引发异常:当路径不存在时引发 FileNotFoundError,或在遇到无限循环时引发 RuntimeError。例如,在此表格前面创建的临时目录中: |

>>> td.resolve()
PosixPath('/Users/annar/tempdir')

|

rmdir p.rmdir() 移除目录p。如果p不为空,则引发 OSError。
samefile p.samefile(target) 当ptarget指示相同的文件时返回True;否则返回Falsetarget可以是字符串或路径对象。
stat p.stat(*, follow_symlinks=True) 返回与路径对象有关的信息,包括权限和大小;参见 Table 11-16 中的 os.stat 以获取返回值。3.10+ 若要对符号链接本身进行 stat 操作,而不是其目标,请传递 follow_symlinks=False
symlink_to p.symlink_to(target, target_is_directory=False) 将p创建为指向target的符号链接。在 Windows 上,如果target是目录,则必须设置 target_is_directory=True。(POSIX 忽略此参数。)(在 Windows 10+上,与 os.symlink 类似,需要开发者模式权限;有关详细信息,请参见在线文档。)注意:参数顺序与 os.link 和 os.symlink 的顺序相反,这在 Table 11-16 中有描述。

| touch | p.touch(mode=0o666, exist_ok=True) 类似于 Unix 上的 touch,会在给定路径处创建一个空文件。当文件已存在时,如果 exist_ok=True,则更新修改时间为当前时间;如果 exist_ok=False,则引发 FileExistsError。例如: |

>>> d
WindowsPath('C:/Users/annar/Documents')
>>> f = d / 'testfile.txt'
>>> f.is_file()
False
>>> f.touch()
>>> f.is_file()
True

|

unlink p.unlink(missing_ok=False) 移除文件或符号链接p。(对于目录,请参见本表前面描述的 rmdir。)3.8+ 传递 missing_ok=True以忽略FileExistsError
write_bytes p.write_bytes(data) 以字节模式打开(或创建,如果需要的话)指向的文件,将data写入其中,然后关闭文件。如果文件已存在,则覆盖该文件。
write_text p.write_text(data, encoding=None, errors=None, newline=None) 以文本模式打开(或创建,如果需要的话)指向的文件,将data写入其中,然后关闭文件。如果文件已存在,则覆盖该文件。3.10+ 当 newline 为None(默认值)时,将任何'\n'转换为系统默认换行符;当为'\r'或'\r\n'时,将'\n'转换为给定的字符串;当为''或'\n'时,不进行任何转换。

pathlib.Path 对象还支持在 Table 11-22 中列出的属性,以访问路径字符串的各个组成部分。请注意,一些属性是字符串,而其他属性是 Path 对象。(为简洁起见,OS 特定类型(如 PosixPath 或 WindowsPath)仅使用抽象 Path 类显示。)

Table 11-22. pathlib.Path 实例 p 的属性

Attribute 描述 Unix 路径 Path('/usr/bin/ python')的值 Windows 路径 Path(r’c:\Python3\ python.exe')的值
anchor 驱动器和根的组合 '/' 'c:\'
drive p的驱动器字母 '' 'c:'
name p的末尾组件 'python' 'python.exe'
parent p的父目录 Path('/usr/bin') Path('c:\Python3')
parents p 的祖先目录 (Path('/usr/ bin'), Path('/usr'), Path('/')) (Path('c:\Python3'), Path('c:\'))
parts p 的所有组成部分的元组 ('/', 'usr', 'bin', 'python') ('c:\', 'Python3', 'python.exe')
root p 的根目录 '/' '\'
stem p 的名称,不包括后缀 'python' 'python'
suffix p 的结束后缀 '' '.exe'
suffixes 由 '.' 字符分隔的 p 的所有后缀的列表 [] ['.exe']

在线文档 包含了更多带有额外组件(如文件系统和 UNC 共享)的路径示例。

pathlib.Path 对象还支持/操作符,是 os.path.join 或 Path 模块中 Path.joinpath 的一个很好的替代方案。请参见 Table 11-21 中 Path.touch 描述中的示例代码。

stat 模块

函数 os.stat(在 Table 11-16 中介绍)返回 stat_result 的实例,其项索引、属性名称和含义也在那里介绍了。 stat 模块提供了类似于 stat_result 属性的大写名称的属性,并且相应的值是相应的项索引。

stat 模块的更有趣的内容是用于检查 stat_result 实例的 st_mode 属性并确定文件类型的函数。os.path 也提供了用于这种任务的函数,它们直接在文件的 path 上操作。与 os 的函数相比,在对同一文件执行多个测试时,stat 提供的函数(在 Table 11-23 中显示)更快:它们在一系列测试开始时只需要一个 os.stat 系统调用来获取文件的 st_mode,而 os.path 中的函数在每次测试时隐式地向操作系统请求相同的信息。每个函数在 mode 表示给定类型的文件时返回 True;否则,返回 False

Table 11-23. stat 模块函数用于检查 st_mode

S_ISBLK S_ISBLK(mode) 表示 mode 是否表示一个块设备文件
S_ISCHR S_ISCHR(mode) 表示 mode 是否表示一个字符设备文件
S_ISDIR S_ISDIR(mode) 表示 mode 是否表示一个目录
S_ISFIFO S_ISFIFO(mode) 表示 mode 是否表示一个 FIFO(也称为“命名管道”)
S_ISLNK S_ISLNK(mode) 表示 mode 是否表示一个符号链接
S_ISREG S_ISREG(mode) 表示 mode 是否表示一个普通文件(不是目录、特殊设备文件等)
S_ISSOCK S_ISSOCK(mode) 表示 mode 是否表示一个 Unix 域套接字

这些函数中的几个仅在类 Unix 系统上有意义,因为其他平台不会将设备和套接字等特殊文件保留在与常规文件相同的命名空间中;类 Unix 系统会这样做。

stat 模块还提供了两个函数,从文件的 mode (x.st_mode,对于 os.stat 函数的某些结果 x) 提取相关部分,列在表 11-24。

表 11-24. stat 模块从模式中提取位的函数

S_IFMT S_IFMT(mode) 返回描述文件类型的mode 中的位(即函数 S_ISDIRS_ISREG 等检查的位)
S_IMODE S_IMODE(mode) 返回mode 中可以由函数 os.chmod 设置的位(即权限位,以及在类 Unix 平台上,一些特殊位,如设置用户标识标志)

stat 模块提供了一个实用函数 stat.filemode(*mode*),将文件的模式转换为形如 '-rwxrwxrwx' 的人类可读字符串。

filecmp 模块

filecmp 模块提供了一些有用的用于比较文件和目录的函数,列在表 11-25。

表 11-25. filecmp 模块的有用函数

clear_cache clear_cache() 清除 filecmp 缓存,这在快速文件比较中可能很有用。
cmp cmp(f1, f2, shallow=True) 比较由路径字符串 f1f2 标识的文件(或 pathlib.Paths)。如果文件被认为相等,则 cmp 返回 True;否则返回 False。如果 shallowTrue,则如果它们的 stat 元组相等,文件被视为相等。当 shallowFalse 时,cmp 读取并比较 stat 元组相等的文件的内容。
cmpfiles cmpfiles(dir1, dir2, common, shallow=True) 循环处理序列 commoncommon 中的每个项都是同时存在于目录 dir1dir2 中的文件名。cmpfiles 返回一个元组,其项为三个字符串列表:(equaldifferrs)。equal 是两个目录中相等文件的名称列表,diff 是不同目录之间不同文件的名称列表,errs 是无法比较的文件名称列表(因为它们不同时存在于两个目录中,或者没有权限读取它们的其中一个或两个)。参数 shallowcmp 相同。

filecmp 模块还提供了 dircmp 类。该类的构造函数签名如下:

dircmp class dircmp(dir1, dir2, ignore=None, hide=None) 创建一个新的目录比较实例对象,比较目录 dir1dir2,忽略 ignore 中列出的名称,隐藏 hide 中列出的名称(默认为 '.' 和 '..' 当 hideNone 时)。ignore 的默认值由 filecmp 模块的 DEFAULT_IGNORE 属性提供;在撰写本文时,默认值为 ['RCS', 'CVS', 'tags', '.git', '.hg', '.bzr', '_darcs', '__pycache__']。目录中的文件与 filecmp.cmp 使用 shallow=**True** 进行比较。

dircmp 实例 d 提供三种方法,详见表 11-26。

Table 11-26. dircmp 实例 d 提供的方法

report report_full_closure() 输出 dir1dir2 以及它们所有共同的子目录之间的比较结果到 sys.stdout,递归进行
report_fu⁠l⁠l⁠_​c⁠l⁠o⁠sure report_full_closure() 输出 dir1dir2 以及它们所有共同的子目录之间的比较结果到 sys.stdout,递归进行
report_parti⁠a⁠l⁠_​c⁠l⁠o⁠sure report_partial_closure() 输出 dir1dir2 及它们共同的直接子目录之间的比较结果到 sys.stdout

另外,d 提供了几个属性,详见 Table 11-27。这些属性是“按需计算”的(即,只在需要时才计算,多亏了 getattr 特殊方法),因此使用 dircmp 实例不会产生不必要的开销。

Table 11-27. dircmp 实例 d 提供的属性

common dir1dir2 中的文件和子目录
common_dirs dir1dir2 中的子目录
common_files dir1dir2 中的文件
common_funny dir1dir2 中的名称,其中 os.stat 报告错误或者两个目录中版本的类型不同
diff_files dir1dir2 中内容不同的文件
funny_files dir1dir2 中的文件,但无法进行比较
left_list dir1 中的文件和子目录
left_only dir1 中的文件和子目录,但不在 dir2
right_list dir2 中的文件和子目录
right_only dir2 中的文件和子目录,但不在 dir1
same_files dir1dir2 中内容相同的文件
subdirs 一个字典,其键是 common_dirs 中的字符串;相应的值是 dircmp 的实例(或者 3.10+ 中与 d 同一种类的 dircmp 子类的实例),用于每个子目录

fnmatch 模块

fnmatch 模块(filename match 的缩写)用于匹配类 Unix shell 使用的模式的文件名字符串或路径,如 Table 11-28 所示。

Table 11-28. fnmatch 模式匹配惯例

模式 匹配项
* 任意字符序列
? 任意单个字符
[chars] chars 中的任意一个字符
[!chars] 不属于 chars 中任意一个字符

fnmatch 不遵循类 Unix shell 模式匹配的其他惯例,比如特殊对待斜杠 (/) 或前导点 (.)。它也不允许转义特殊字符:而是要匹配特殊字符,将其括在方括号中。例如,要匹配一个文件名为单个右括号,使用 '[]]'。

fnmatch 模块提供了 Table 11-29 中列出的函数。

Table 11-29. fnmatch 模块的函数

filter filter(names, pattern) 返回 names(一个字符串序列)中与 pattern 匹配的项目列表。
fnmatch fnmatch(filename, pattern) 当字符串 filenamepattern 匹配时返回 True;否则返回 False。当平台为大小写敏感时(例如典型的类 Unix 系统),匹配区分大小写;否则(例如在 Windows 上),不区分大小写;请注意,如果处理的文件系统的大小写敏感性与您的平台不匹配(例如 macOS 是类 Unix 的,但其典型文件系统不区分大小写)。
fnmatchcase fnmatchcase(filename, pattern) 当字符串 filenamepattern 匹配时返回 True;否则返回 False。此匹配在任何平台上均区分大小写。
translate translate(pattern) 返回等同于 fnmatch 模式 pattern 的正则表达式模式(详见“模式字符串语法”)。

glob 模块

glob 模块以任意顺序列出与 路径模式 匹配的文件路径名,使用与 fnmatch 相同的规则;此外,它特别处理前导点(.)、分隔符(/)和 **,就像 Unix shell 一样。Table 11-30 列出了 glob 模块提供的一些有用函数。

Table 11-30. glob 模块的函数

escape escape(pathname) 转义所有特殊字符('?'、'*' 和 ''),因此可以匹配可能包含特殊字符的任意字面字符串。
glob glob(pathname, *, root_dir=None, dir_fd=None, recursive=False) 返回与模式 pathname 匹配的文件的路径名列表。root_dir(如果不是 None)是指定搜索的根目录的字符串或类似路径的对象(这类似于在调用 glob 之前更改当前目录)。如果 pathname 是相对路径,则返回的路径是相对于 root_dir 的。要搜索相对于目录描述符的路径,请传递 dir_fd。可选地传递命名参数 recursive=True 以使路径组件递归地匹配零个或多个子目录级别。
iglob iglob(*pathname, , root_dir=None, dir_fd=None, recursive=False) 类似于 glob,但返回一个迭代器,每次生成一个相关的路径名。

shutil 模块

shutil 模块(shell utilities 的缩写)提供了复制、移动文件以及删除整个目录树的函数。在某些 Unix 平台上,大多数函数支持可选的仅关键字参数 follow_symlinks,默认为 True。当 follow_symlinks=True 时,如果路径指示为符号链接,则函数会跟随它以达到实际文件或目录;当 False 时,函数操作的是符号链接本身。[Table 11-31 列出了 shutil 模块提供的函数。

Table 11-31. shutil 模块的函数

copy copy(src, dst) 复制由src指定的文件的内容(src必须存在),并创建或覆盖由dst指定的文件(srcdst可以是字符串或 pathlib.Path 实例)。如果dst是一个目录,则目标是与src同名的文件,但位于dst中。copy 还复制权限位,但不复制最后访问和修改时间。返回已复制到的目标文件的路径。
copy2 copy2(src, dst) 类似于 copy,但也复制最后访问时间和修改时间。
copyfile copyfile(src, dst) 仅复制由src指定的文件的内容(不包括权限位、最后访问和修改时间),创建或覆盖由dst指定的文件。
copyfileobj copyfileobj(fsrc, fdst, bufsize=16384) 从已打开用于读取的文件对象fsrc向已打开用于写入的文件对象fdst复制所有字节。如果bufsize大于 0,则每次最多复制bufsize字节。文件对象的具体内容请参见“The io Module”。
copymode copymode(src, dst) 复制由src指定的文件或目录的权限位到由dst指定的文件或目录。srcdst都必须存在。不更改dst的内容,也不更改其作为文件或目录的状态。
copystat copystat(src, dst) 复制由src指定的文件或目录的权限位、最后访问时间和修改时间到由dst指定的文件或目录。srcdst都必须存在。不更改dst的内容,也不更改其作为文件或目录的状态。

| copytree | copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2, ignore_dangling_symlinks=False, dirs_exist_ok=False) 复制以src命名的目录为根的目录树到以dst命名的目标目录中。dst不得已存在:copytree 将创建它(以及创建任何缺失的父目录)。copytree 默认使用 copy2 函数复制每个文件;您可以选择作为命名参数 copy_function 传递不同的文件复制函数。如果在复制过程中发生任何异常,copytree 将在最后记录它们并继续执行,最终引发包含所有记录异常列表的错误。

当 symlinks 为True时,copytree 在新树中创建符号链接,如果在源树中发现符号链接。当 symlinks 为False时,copytree 跟随它找到的每个符号链接,并复制链接的文件,使用链接的名称记录异常(如果 ignore_dangling_symlinks 为True,则忽略此异常)。在不支持符号链接概念的平台上,copytree 会忽略 symlinks 参数。 |

| copytree (续) | 当 ignore 不为None时,它必须是一个接受两个参数(目录路径和目录的直接子项列表)并返回要在复制过程中忽略的子项列表的可调用对象。如果存在,ignore 通常是对 shutil.ignore_patterns 的调用结果。例如,以下代码:

`import` shutil
ignore = shutil.ignore_patterns('.*', '*.bak')
shutil.copytree('src', 'dst', ignore=ignore)

将源自目录 src 的树复制到新目录 dst 中的树,忽略任何以点开头的文件或子目录以及任何以*.bak*结尾的文件或子目录。

默认情况下,如果目标目录已经存在,copytree 会记录 FileExistsError 异常。从 3.8 版本开始,可以将 dirs_exist_ok 设置为True,允许 copytree 在复制过程中写入已存在的目录(可能会覆盖其内容)。

ignore_patterns ignore_patterns(patterns) 返回一个可调用对象,选出与patterns*匹配的文件和子目录,类似于 fnmatch 模块中使用的模式(见“fnmatch 模块”)。结果适合作为 copytree 函数的 ignore 参数传递。
move move(src, dst, copy_function=copy2) 将名为src的文件或目录移动到名为dst的位置。move 首先尝试使用 os.rename。然后,如果失败(因为srcdst位于不同的文件系统上,或者dst已经存在),move 将src复制到dst(默认情况下使用 copy2 复制文件或 copytree 复制目录;您可以选择传递名为 copy_function 的文件复制函数作为命名参数),然后删除src(对于文件使用 os.unlink,对于目录使用 rmtree)。
rmtree rmtree(path, ignore_errors=False, onerror=None) 删除以path为根的目录树。当 ignore_errors 为True时,rmtree 忽略错误。当 ignore_errors 为False且 onerror 为None时,错误会引发异常。当 onerror 不为None时,必须是一个可调用的函数,接受三个参数:func是引发异常的函数(os.remove 或 os.rmdir),path是传递给func的路径,ex是 sys.exc_info 返回的信息元组。当 onerror 引发异常时,rmtree 终止,并且异常传播。

除了提供直接有用的函数外,Python 标准库中的源文件shutil.py是如何使用许多 os 函数的优秀示例。

文本输入和输出

Python 将非 GUI 文本输入和输出流呈现为 Python 程序的文件对象,因此可以使用文件对象的方法(在“文件对象的属性和方法”中介绍)来操作这些流。

标准输出和标准错误

sys 模块(在“sys 模块”中讨论)具有 stdout 和 stderr 属性,它们都是可写的文件对象。除非你正在使用 shell 重定向或管道,否则这些流连接到运行你的脚本的“终端”。如今,实际的终端非常罕见:所谓的终端通常是支持文本 I/O 的屏幕窗口。

sys.stdout 和 sys.stderr 之间的区别是惯例的问题。sys.stdout,即标准输出,是程序输出结果的地方。sys.stderr,即标准错误,是错误、状态或进度消息等输出的地方。将程序输出与状态和错误消息分开有助于有效地使用 shell 重定向。Python 遵循这一惯例,将 sys.stderr 用于自身的错误和警告消息。

print 函数

将结果输出到标准输出的程序通常需要写入 sys.stdout。Python 的 print 函数(在表 8-2 中讨论)可以作为一个丰富而方便的替代方案,用于在开发过程中进行非正式输出,以帮助调试代码,但是对于生产输出,你可能需要比 print 提供的格式控制更多。例如,你可能需要控制间距、字段宽度、浮点值的小数位数等。如果是这样,你可以将输出准备为 f-string(在“字符串格式化”中讨论),然后将字符串输出,通常使用适当文件对象的 write 方法。(你可以将格式化的字符串传递给 print,但 print 可能会添加空格和换行;write 方法根本不会添加任何内容,因此更容易控制输出的确切内容。)

如果你需要将输出直接定向到一个打开写入模式的文件f,直接调用f.write 通常是最好的方法,而 print(..., file=f)有时是一个方便的替代方法。要重复地将 print 调用的输出定向到某个文件,你可以临时改变 sys.stdout 的值。下面的示例是一个通用的重定向函数,可用于这种临时更改;在多任务存在的情况下,请确保还添加一个锁以避免任何争用(参见也在表 6-1 中描述的 contextlib.redirect_stdout 装饰器):

`def` redirect(func: Callable, *a, **k) -> (str, Any):
    *`"""redirect(func, *a, **k) -> (func's results, return value)`*
 *`func is a callable emitting results to standard output.`*
 *`redirect captures the results as a str and returns a pair`*
 *`(output string, return value).`*
 *`"""`*
    `import` sys, io
    save_out = sys.stdout
    sys.stdout = io.StringIO()
    `try`:
        retval = func(*args, **kwds)
        `return` sys.stdout.getvalue(), retval
    `finally`:
        sys.stdout.close()
        sys.stdout = save_out

标准输入

除了 stdout 和 stderr 之外,sys 模块还提供了 stdin 属性,它是一个可读的文件对象。当你需要从用户那里获取一行文本时,可以调用内置函数 input(在表 8-2 中讨论),可选地使用一个字符串参数作为提示。

当你需要的输入不是字符串时(例如,当你需要一个数字时),使用 input 从用户那里获取一个字符串,然后使用其他内置函数如 intfloatast.literal_eval 将字符串转换为你需要的数字。要评估来自不受信任来源的表达式或字符串,建议使用标准库模块 ast 中的 literal_eval 函数(如在在线文档中描述)。ast.literal_eval(*astring*) 在可能时返回一个有效的 Python 值(例如 int、float 或 list),对于给定的字面值 astring,否则会引发 SyntaxError 或 ValueError 异常;它永远不会产生任何副作用。为了确保完全安全,astring 不能包含任何运算符或非关键字标识符;然而,+ 和 - 可以被接受作为数字的正负号,而不是运算符。例如:

`import` ast
print(ast.literal_eval('23'))     *`# prints 23`*
print(ast.literal_eval(' 23'))   *`# prints 23 (3.10++)`*
print(ast.literal_eval('[2,-3]')) *`# prints [2, -3]`*
print(ast.literal_eval('2+3'))    *`# raises ValueError`*
print(ast.literal_eval('2+'))     *`# raises SyntaxError`*

eval 可能存在危险

不要对任意未经处理的用户输入使用 eval:一个恶意(或者无意中疏忽的)用户可以通过这种方式突破安全性或者造成其他损害。没有有效的防御措施——避免在来自不完全信任的来源的输入上使用 eval(和 exec)。

getpass 模块

偶尔,你可能希望用户以一种屏幕上看不到用户输入内容的方式输入一行文本。例如,当你要求用户输入密码时。getpass 模块提供了这种功能,以及获取当前用户用户名的函数(参见 Table 11-32)。

Table 11-32. getpass 模块的功能

getpass getpass(prompt='Password: ')input 类似(在 Table 8-2 中介绍),不同之处在于用户输入的文本在用户输入时不会显示在屏幕上,并且默认提示与 input 不同。
getuser getuser() 返回当前用户的用户名。getuser 尝试将用户名作为环境变量 LOGNAMEUSERLNAMEUSERNAME 的一个值来获取,依次尝试。如果 os.environ 中没有这些变量的值,getuser 会向操作系统询问。

富文本 I/O

到目前为止,所涵盖的文本 I/O 模块在所有平台终端上提供基本的文本 I/O 功能。大多数平台还提供增强的文本 I/O 功能,例如响应单个按键(而不仅仅是整行)、在任何终端行和列位置打印文本,以及使用背景和前景颜色以及加粗、斜体和下划线等字体效果增强文本。对于这种功能,你需要考虑使用第三方库。我们在这里关注 readline 模块,然后快速查看一些控制台 I/O 选项,包括 mscvrt,并简要提及 cursesrichcolorama,我们不会进一步介绍它们。

readline 模块

readline 模块封装了 GNU Readline Library,允许用户在交互输入期间编辑文本行并回溯先前的行以进行编辑和重新输入。Readline 预安装在许多类 Unix 平台上,并且可在线获取。在 Windows 上,您可以安装和使用第三方模块 pyreadline

当可用时,Python 使用 readline 处理所有基于行的输入,例如 input。交互式 Python 解释器始终尝试加载 readline 以启用交互会话的行编辑和回溯。某些 readline 函数控制高级功能,特别是 history 用于回溯在先前会话中输入的行,以及 completion 用于正在输入的单词的上下文敏感完成(详见 Python readline 文档 关于配置命令的完整详细信息)。您可以使用 Table 11-33 中的函数访问模块的功能。

Table 11-33. readline 模块的函数

add_history add_history(s, /) 将字符串 s 作为一行添加到历史缓冲区的末尾。要临时禁用 add_history,请调用 set_auto_history(False),这将仅在本次会话中禁用 add_history(不会跨会话持久保存);set_auto_history 默认为 True
app⁠e⁠n⁠d⁠_​h⁠i⁠s⁠t⁠o⁠r⁠y_​f⁠i⁠l⁠e append_history_file(n, filename='~/.history', /) 将最后 n 项追加到现有文件 filename
clear_history clear_history() 清除历史缓冲区。
get_completer get_completer() 返回当前的补全函数(由 set_completer 最后设置),如果没有设置补全函数,则返回 None
g⁠e⁠t⁠_​h⁠i⁠s⁠tory_length get_history_length() 返回要保存到历史文件中的行数。当结果小于 0 时,保存所有历史行。

| parse_and_bind | parse_and_bind(readline_cmd, /) 给 readline 提供配置命令。要让用户按 Tab 键请求完成,请调用 parse_and_bind('tab: complete')。查看 readline 文档 获取字符串 readline_cmd 的其他有用值。

一个良好的补全函数位于标准库模块 rlcompleter 中。在交互式解释器中(或在交互会话开始时执行的启动文件中,见 “Environment Variables”),输入:

`import` readline, rlcompleter
readline.parse_and_bind('tab: complete')

在本次交互会话的其余部分,您可以在行编辑过程中按 Tab 键并获得全局名称和对象属性的完成。

re⁠a⁠d⁠_​h⁠i⁠s⁠t⁠o⁠r⁠y_​f⁠i⁠l⁠e read_history_file(filename='~/.history', /) 从路径 filename 的文本文件中加载历史行。
re⁠a⁠d⁠_​i⁠n⁠i⁠t_​f⁠i⁠l⁠e read_init_file(filename=None, /) 使 readline 加载文本文件:每行是一个配置命令。当 filenameNone 时,加载与上次相同的文件。
set_completer set_completer(func, /) 设置完成函数。当 funcNone 或省略时,readline 禁用完成。否则,当用户键入部分单词 start,然后按 Tab 键时,readline 调用 func(start, i),其中 i 最初为 0。func 返回以 start 开头的第 i 个可能的单词,或在没有更多单词时返回 None。readline 循环调用 funci 设置为 0、1、2 等,直到 func 返回 None
s⁠e⁠t⁠_​h⁠i⁠s⁠t⁠o⁠r⁠y⁠_​l⁠e⁠n⁠g⁠t⁠h set_history_length(x, /) 设置要保存到历史文件中的历史行数。当 x 小于 0 时,将保存历史中的所有行。
wri⁠t⁠e⁠_​h⁠i⁠s⁠t⁠o⁠r⁠y⁠_​f⁠i⁠l⁠e write_history_file(filename='~/.history') 将历史记录行保存到名为 filename 或路径为 filename 的文本文件中,覆盖任何现有文件。

控制台 I/O

如前所述,“终端”今天通常是图形屏幕上的文本窗口。理论上,您也可以使用真正的终端,或者(也许略微现实些,但这些天不太可能)个人计算机的控制台(主屏幕)以文本模式运行。今天使用的所有这些“终端”都提供平台相关的高级文本 I/O 功能。低级 curses 包适用于类 Unix 平台。对于跨平台(Windows、Unix、macOS)解决方案,您可以使用第三方包 rich;除了其出色的 在线文档 外,还有在线 教程 可帮助您入门。要在终端上输出彩色文本,请参阅 PyPI 上的 colorama。接下来介绍的 msvcrt 提供了一些低级(仅限 Windows)函数。

curses

增强的终端 I/O 的经典 Unix 方法因历史原因被称为 curses。Python 包 curses 允许您在需要时进行详细控制。本书不涵盖 curses;更多信息请参阅 A.M. Kuchling 和 Eric Raymond 的在线教程 “Curses Programming with Python”

msvcrt 模块

msvcrt 模块只能在 Windows 上使用(你可能需要使用 pip 安装),它提供了一些低级函数,使 Python 程序可以访问由 Microsoft Visual C++ 运行时库 msvcrt.dll 提供的专有附加功能。例如,Table 11-34 中列出的函数允许你逐字符而不是逐行读取用户输入。

Table 11-34. msvcrt 模块的一些有用函数

| getch, getche | getch(), getche() 从键盘输入读取并返回单个字符的字节,并在必要时阻塞,直到有一个可用(即按下一个键)。getche 将字符回显到屏幕上(如果可打印),而 getch 则不会。当用户按下特殊键(箭头键、功能键等)时,它被视为两个字符:首先是 chr(0) 或 chr(224),然后是第二个字符,这两个字符共同定义用户按下的特殊键。这意味着程序必须调用两次 getch 或 getche 才能读取这些按键。要了解 getch 对任何键返回什么,请在 Windows 机器上运行以下小脚本:

`import` msvcrt
print("press z to exit, or any other key "
      "to see the key's code:")
`while` `True`:
    c = msvcrt.getch()
    `if` c == b'z':
        `break`
    print(f'{ord(c)} ({c!r})')

|

kbhit kbhit() 当有字符可读时返回True(调用 getch 时立即返回);否则返回False(调用 getch 时等待)。
ungetch ungetch(c) “取消”字符c;接下来的 getch 或 getche 调用会返回c。调用两次 ungetch 而没有调用中间的 getch 或 getche 是错误的。

国际化

许多程序将某些信息以文本形式呈现给用户。这样的文本应该能够被不同地区的用户理解和接受。例如,在某些国家和文化中,“March 7”这个日期可以简洁地表示为“3/7”。而在其他地方,“3/7”表示“7 月 3 日”,而表示“March 7”的字符串则是“7/3”。在 Python 中,这样的文化习惯是通过标准库模块 locale 处理的。

同样,一个问候语可能用字符串“Benvenuti”在一种自然语言中表达,而在另一种语言中,则需要使用“Welcome”这个字符串。在 Python 中,这些翻译是通过标准库模块 gettext 处理的。

这两种问题通常在称为国际化的总称下处理(通常缩写为i18n,因为英文全拼中in之间有 18 个字母)——这个名称不准确,因为这些问题不仅适用于国家之间,还适用于同一国家内不同的语言或文化。⁵

模块 locale

Python 对文化习惯的支持模仿了 C 语言,稍作简化。程序在称为locale的文化习惯环境中运行。Locale 设置渗透到程序中,并通常在程序启动时设置。Locale 不是线程特定的,而且 locale 模块不是线程安全的。在多线程程序中,应在主线程中设置程序的 locale;即在启动次要线程之前设置。

locale 的局限性

locale 只对进程范围的设置有效。如果您的应用程序需要在同一个进程中同时处理多种 locale——无论是在线程中还是异步地——由于其进程范围的特性,locale 并不适合。考虑使用像PyICU这样的替代方案,见“更多国际化资源”。

如果程序没有调用 locale.setlocale,则使用 C locale(因为 Python 的 C 语言根源而得名);它与美国英语区域类似但不完全相同。另外,程序可以查找并接受用户的默认区域设置。在这种情况下,区域模块通过与操作系统的交互(通过环境或其他系统相关方式)尝试找到用户首选的区域设置。最后,程序可以设置特定的区域设置,据此确定要设置的区域设置,可能基于用户交互或持久配置设置。

区域设置通常会跨文化习惯的相关类别进行全面设置。这种广泛的设置由区域模块的常量属性 LC_ALL 表示。然而,由区域处理的文化习惯被分成不同类别,在某些罕见的情况下,程序可以选择混合和匹配这些类别,以构建合成的复合区域。这些类别由 表 11-35 中列出的属性标识。

表 11-35. 区域模块的常量属性

LC_COLLATE 字符串排序;影响区域设置下的函数 strcoll 和 strxfrm
LC_CTYPE 字符类型;影响与小写和大写字母相关的 string 模块(以及其方法)的某些方面
LC_MESSAGES 消息;可能影响操作系统显示的消息(例如函数 os.strerror 和模块 gettext 显示的消息)
LC_MONETARY 货币值格式化;影响区域设置下的函数 localeconv 和 currency
LC_NUMERIC 数字格式化;影响区域设置下的函数 atoi、atof、format_string、localeconv 和 str,以及在格式字符串(如 f-strings 和 str.format)中使用格式字符 'n' 时使用的数字分隔符
LC_TIME 时间和日期格式化;影响函数 time.strftime

某些类别的设置(由 LC_CTYPE、LC_MESSAGES 和 LC_TIME 表示)会影响其他模块中的行为(如 string、os、gettext 和 time)。其他类别(由 LC_COLLATE、LC_MONETARY 和 LC_NUMERIC 表示)仅影响区域本身的某些函数(以及在 LC_NUMERIC 情况下的字符串格式化)。

区域模块提供了 表 11-36 中列出的函数,用于查询、更改和操作区域设置,以及实施 LC_COLLATE、LC_MONETARY 和 LC_NUMERIC 类别的文化习惯的函数。

表 11-36. 区域模块的有用函数

atof atof(s) 使用当前 LC_NUMERIC 设置将字符串 s 解析为浮点数。
atoi atoi(s) 使用当前 LC_NUMERIC 设置将字符串 s 解析为整数。
currency currency(data, grouping=False, international=False) 返回带有货币符号的字符串或数字 data,如果 grouping 为 True,则使用货币千位分隔符和分组。当 international 为 True 时,使用后面表中描述的 int_curr_symbol 和 int_frac_digits。

| f⁠o⁠r⁠m⁠a⁠t⁠_​s⁠t⁠r⁠i⁠n⁠g | format_string(fmt, num, grouping=False, monetary=False) 返回通过根据格式字符串 fmt 和 LC_NUMERIC 或 LC_MONETARY 设置对 num 进行格式化而获得的字符串。除了文化习惯问题外,结果类似于旧式 fmt % num 字符串格式化,详见 “使用 % 进行传统字符串格式化”。如果 num 是数字类型的实例,并且 fmt%d%f,将 grouping 设置为 True 可以根据 LC_NUMERIC 设置在结果字符串中对数字进行分组。如果 monetary 为 True,字符串将使用 mon_decimal_point 格式化,并且 grouping 使用 mon_thousands_sep 和 mon_grouping 而不是 LC_NUMERIC 提供的设置(有关这些设置的更多信息,请参见表后的 localeconv)。例如:

>>> locale.setlocale(locale.LC_NUMERIC, 
...                  'en_us')
'en_us'
>>> n=1000*1000
>>> locale.format_string('%d', n)
'1000000'
>>> locale.setlocale(locale.LC_MONETARY, 
...                  'it_it')
'it_it'
>>> locale.format_string('%f', n)
'1000000.000000'  *# uses decimal_point*
>>> locale.format_string('%f', n, 
...                      monetary=True)
'1000000,000000'  *# uses mon_decimal_point*
>>> locale.format_string('%0.2f', n, 
...                      grouping=True)
'1,000,000.00' *# separators & decimal from*
 *# LC_NUMERIC*
>>> locale.format_string('%0.2f', n, 
...                      grouping=True,
...                      monetary=True)
'1.000.000,00'    *# separators & decimal from* 
 *# LC_MONETARY*

在这个例子中,由于数字区域设置为美国英语,在参数 grouping 为 True 时,format_string 会使用逗号将数字每三位分组,并使用点 (.) 作为小数点。然而,货币区域设置为意大利时,当参数 monetary 为 True 时,format_string 使用逗号 (,) 作为小数点,分组使用点 (.) 作为千位分隔符。通常,在任何给定的区域设置内,货币和非货币数字的语法是相同的。 |

g⁠e⁠t​d⁠e⁠f⁠a⁠u⁠l⁠t​l⁠o⁠c⁠a⁠l⁠e getdefaultlocale(envvars=('LANGUAGE', 'LC_ALL', 'LC_TYPE', 'LANG')) 检查按顺序指定的 envvars 环境变量。在环境中找到的第一个变量确定默认区域设置。getdefaultlocale 返回一对符合 RFC 1766 的字符串 (lang, encoding)(除了 'C' 区域设置),例如 ('en_US', 'UTF-8')。如果 gedefaultlocale 无法确定某个项的值,则每对中的每一项可能为 None
g⁠e⁠t​l⁠o⁠c⁠a⁠l⁠e getlocale(category=LC_CTYPE) 返回给定类别的当前设置的一对字符串 (lang, encoding)。类别不能是 LC_ALL。

| localeconv | localeconv() 返回一个字典d,其中包含当前区域设置的 LC_NUMERIC 和 LC_MONETARY 类别指定的文化约定。虽然 LC_NUMERIC 最好间接使用,通过其他 locale 函数,但 LC_MONETARY 的细节只能通过d访问。本地和国际使用的货币格式不同。例如,'$'符号仅用于本地使用;在国际使用中是模糊的,因为相同的符号用于许多称为“dollars”的货币(美元、加拿大元、澳大利亚元、香港元等)。因此,在国际使用中,美元货币的符号是明确的字符串'USD'。该函数临时将 LC_CTYPE 区域设置为 LC_NUMERIC 区域设置,或者如果区域设置不同且数字或货币字符串为非 ASCII,则为 LC_MONETARY 区域设置。此临时更改影响所有线程。用于货币格式的d中的键是以下字符串:

'currency_symbol'

用于本地使用的货币符号

'frac_digits'

本地使用的小数位数

'int_curr_symbol'

用于国际使用的货币符号

'int_frac_digits'

用于国际使用的小数位数

'mon_decimal_point'

用作货币值的“小数点”(又称基数点)的字符串

'mon_grouping'

货币值的数字分组数字列表

'mon_thousands_sep'

用于货币值的数字组分隔符的字符串

'negative_sign', 'positive_sign'

用作负(正)货币值符号的字符串

'n_cs_precedes', 'p_cs_precedes'

True 当货币符号位于负(正)货币值之前时

'n_sep_by_space', 'p_sep_by_space'

True 当符号和负(正)货币值之间有空格时

'n_sign_posn', 'p_sign_posn'

请参阅 Table 11-37 查看格式化负(正)货币值的数字代码列表。

CHAR_MAX

表示当前区域设置不指定此格式的任何约定

|

localeconv (cont.) d['mon_grouping'] 是一个数字列表,用于在格式化货币值时分组数字(但要注意:在某些区域设置中,d['mon_grouping'] 可能是一个空列表)。当d['mon_grouping'][-1] 为 0 时,除了指定的数字之外,没有进一步的分组。当d['mon_grouping'][-1] 为 locale.CHAR_MAX 时,分组将无限继续,就像d['mon_grouping'][-2] 无限重复一样。locale.CHAR_MAX 是用于当前区域设置不指定任何约定的所有d条目的值的常量。
localize localize(normstr, grouping=False, monetary=False) 从规范化的数字字符串normstr 返回一个按照 LC_NUMERIC(或当 monetary 为True时,LC_MONETARY)设置的格式化字符串。
normalize normalize(localename) 返回一个字符串,适合作为 setlocale 的参数,即 localename 的规范化形式。当 normalize 无法规范化字符串 localename 时,返回不变的 localename
resetlocale resetlocale(category=LC_ALL) 将类别 category 的区域设置为由 getdefaultlocale 给出的默认值。
setlocale setlocale(category, locale=None) 将类别 category 的区域设置为 locale,如果不是 None,则返回设置(当 locale 是 None 时返回现有设置;否则返回新设置)。 locale 可以是一个字符串,或者一个 (lang, encoding) 对。 lang 通常是基于 ISO 639 两字母代码的语言代码('en' 为英语,'nl' 为荷兰语等)。当 locale 是空字符串 '' 时,setlocale 设置用户的默认区域设置。要查看有效的区域设置,请查看 locale.locale_alias 字典。
str str(num) 类似于 locale.format_string('%f', num)。
strcoll strcoll(str1, str2) 在 LC_COLLATE 设置下,当 str1 在排序中排在 str2 之前时返回 -1,当 str2str1 之前时返回 1,在排序目的上两个字符串相等时返回 0。

| strxfrm | strxfrm(s) 返回一个字符串 sx,使得 Python 对两个或多个经过此转换的字符串进行比较时类似于调用 locale.strcoll。 strxfrm 让你可以轻松地在需要区域设置兼容的排序和比较中使用键参数。例如,

`def` locale_sort_inplace(list_of_strings):
    list_of_strings.sort(key=locale.strxfrm)

|

表 11-37. 格式化货币值的数值代码

0 值和货币符号放在括号内
1 标记放在值和货币符号之前
2 标记放在值和货币符号之后
3 标记直接放在值之前
4 标记直接放在值之后

gettext 模块

国际化的一个关键问题是能够在不同的自然语言中使用文本,这被称为本地化(有时是l10n)。 Python 通过标准库模块 gettext 支持本地化,受到 GNU gettext 的启发。 gettext 模块可以选择性地使用后者的基础设施和 API,但也提供了一种更简单、更高级的方法,因此你不需要安装或研究 GNU gettext 就能有效地使用 Python 的 gettext。

详细了解从不同角度覆盖 gettext,请参阅 在线文档

使用 gettext 进行本地化

gettext 不涉及自然语言之间的自动翻译。 相反,它帮助您提取、组织和访问程序使用的文本消息。 将每个需要翻译的字符串文字(也称为消息)传递给一个名为 _(下划线)的函数,而不是直接使用它。 gettext 通常在内置模块中安装一个名为 _ 的函数。 为确保您的程序能够有或没有 gettext 运行,有条件地定义一个名为 _ 的无操作函数,它只是返回其未更改的参数。 然后,您可以安全地在需要翻译的文字使用 message 的地方使用它。 以下示例显示了如何启动用于有条件使用 gettext 的模块:

`try`:
    _
`except` NameError:
    `def` _(s): `return` s
`def` greet():
    print(_('Hello world'))

如果在您运行此示例代码之前某个其他模块已安装了 gettext,则函数 greet 会输出一个适当本地化的问候语。 否则,greet 输出未更改的字符串'Hello world'。

编辑您的源代码,使用函数 _ 装饰消息文字。 然后使用各种工具之一将消息提取到一个文本文件中(通常命名为messages.pot),并将该文件分发给负责将消息翻译为各种自然语言的人员。 Python 提供了一个脚本pygettext.py(位于 Python 源分发中的Tools/i18n目录中)来执行对您的 Python 源文件的消息提取。

每位翻译员编辑messages.pot以生成一个翻译消息的文本文件,扩展名为*.po.* 使用各种工具之一将*.po文件编译成扩展名为.mo的二进制文件,适合快速搜索。 Python 提供了一个名为msgfmt.py*(也在Tools/i18n中)的脚本用于此目的。 最后,在适当的目录中使用适当的名称安装每个*.mo*文件。

关于哪些目录和名称适合的约定在平台和应用程序之间有所不同。 gettext 的默认目录是目录sys.prefix下的子目录share/locale//LC_MESSAGES/,其中*是语言代码(两个字母)。 每个文件命名为.mo*,其中**是您的应用程序或软件包的名称。

一旦您准备好并安装了您的*.mo*文件,通常在应用程序启动时,您会执行以下类似的代码:

`import` os, gettext
os.environ.setdefault('LANG', 'en')  *`# application-default`* *`language`*
gettext.install('your_application_name')

这确保像 _('message')这样的调用返回适当的翻译字符串。 您可以选择不同的方式在程序中访问 gettext 功能; 例如,如果您还需要本地化 C 编码的扩展,或者在运行期间切换语言。 另一个重要考虑因素是您是本地化整个应用程序还是仅分开分发的软件包。

重要的 gettext 函数

gettext 提供了许多功能。最常用的功能列在第 11-38 表中;请查看在线文档获取完整列表。

表 11-38. gettext 模块的有用函数

安装 安装(domain, localedir=None, names=None) 在 Python 的内置命名空间中安装一个名为 _ 的函数,以在目录 localedir 中给定的文件/LC_MESSAGES/.mo 中执行翻译,使用语言代码按照 getdefaultlocale。当 localedir 为None时,install 使用目录 os.path.join(sys.prefix,'share','locale')。当提供 names 时,它必须是包含要在内置命名空间中安装的函数名称的序列,以外加 _。支持的名称有'gettext','lgettext','lngettext','ngettext',3.8+'npgettext'和 3.8+'pgettext'。

| 翻译 | 翻译(domain, localedir=None, languages=None, class_=None, fallback=False) 搜索*.mo文件,就像 install 函数一样;如果找到多个文件,则翻译将较晚的文件用作较早的文件的回退。将 fallback 设置为True以返回一个 NullTranslations 实例;否则,当找不到任何.mo*文件时,函数会引发 OSError。

当 languages 为None时,翻译会在环境中查找要使用的,就像 install 一样。它按顺序检查环境变量 LANGUAGE、LC_ALL、LC_MESSAGES 和 LANG,并在第一个非空变量上用':'拆分以给出语言名称的列表(例如,它将'de:en'拆分为['de','en'])。当 languages 不为None时,languages 必须是一个包含一个或多个语言名称的列表(例如,['de','en'])。翻译使用列表中找到的第一个语言名称,该名称对应于找到的*.mo*文件。 |

| 翻译 (cont.) | 翻译返回一个翻译类的实例对象(默认为 GNUTranslations;如果存在,则类的构造函数必须接受单个文件对象参数),该对象提供了 gettext 方法(用于翻译 str)和 install 方法(将 gettext 安装在 Python 内置命名空间中的名称 _ 下)。翻译提供了比 install 更详细的控制,其功能类似于 translation(domain, localedir).install(unicode)。使用翻译,您可以在不影响内置命名空间的情况下,按模块绑定名称 _,以本地化单个包,例如:

_ = translation(*`domain`*).ugettext

|

更多国际化资源

国际化是一个非常庞大的主题。有关一般介绍,请参见Wikipedia。国际化的最佳代码和信息包之一,作者乐意推荐的是ICU,其中还包括 Unicode Consor 国际化是一个非常庞大的主题。有关一般介绍,请参见Wikipedia。国际化的最佳代码和信息包之一,作者乐意推荐的是ICU,其中还包括 Unicode Consortium 的通用区域设置数据存储库(CLDR)数据库的区域设置约定和访问 CLDR 的代码。要在 Python 中使用 ICU,请安装第三方包PyICU

¹ 对于文本文件,tell 的值对于不定长字符是不透明的,因为它们包含可变长度的字符。对于二进制文件,它只是一个直接的字节计数。

² 遗憾的是,是的——不是 sys.stderr,如常见做法和逻辑所建议的那样!

³ 或者更好的是,更高级别的 pathlib 模块,在本章后面进行介绍。

⁴ “诅咒” 很好地描述了程序员面对这种复杂的低级别方法时的典型呻吟。

⁵ I18n 包括了“本地化”的过程,即将国际软件适应本地语言和文化习惯。

第十二章:持久性和数据库

Python 支持几种持久化数据的方式。一种方式是序列化,将数据视为 Python 对象的集合。这些对象可以序列化(保存)到字节流中,稍后可以从字节流中反序列化(加载和重新创建)。对象持久性依赖于序列化,添加了诸如对象命名等功能。本章介绍了支持序列化和对象持久性的 Python 模块。

另一种使数据持久化的方法是将其存储在数据库(DB)中。一个简单的 DB 类别是使用键访问的文件,以便选择性地读取和更新数据的部分。本章涵盖了支持几种此类文件格式变体的 Python 标准库模块,称为DBM

关系型 DB 管理系统(RDBMS),如 PostgreSQL 或 Oracle,提供了一种更强大的方法来存储、搜索和检索持久化数据。关系型 DB 依赖于结构化查询语言(SQL)的方言来创建和更改 DB 的模式,在 DB 中插入和更新数据,并使用搜索条件查询 DB。(本书不提供 SQL 的参考资料;为此,我们推荐 O'Reilly 的SQL in a Nutshell,作者是 Kevin Kline、Regina Obe 和 Leo Hsu。)不幸的是,尽管存在 SQL 标准,但没有两个 RDBMS 实现完全相同的 SQL 方言。

Python 标准库没有提供 RDBMS 接口。然而,许多第三方模块让您的 Python 程序访问特定的 RDBMS。这些模块大多遵循Python 数据库 API 2.0标准,也称为DBAPI。本章介绍了 DBAPI 标准,并提到了一些最受欢迎的实现它的第三方模块。

特别方便的 DBAPI 模块(因为它随着每个 Python 标准安装而提供)是sqlite3,它封装了SQLite。SQLite 是“一个自包含的、无服务器的、零配置的、事务性的 SQL DB 引擎”,是世界上部署最广泛的关系型 DB 引擎。我们在“SQLite”中介绍 sqlite3。

除了关系型 DB 和本章介绍的更简单的方法之外,还存在几种NoSQL DB,如RedisMongoDB,每种都有 Python 接口。本书不涵盖高级非关系型 DB。

序列化

Python 提供了几个模块来将 Python 对象序列化(保存)到各种字节流中,并从流中反序列化(加载和重新创建)Python 对象。序列化也称为编组,意味着格式化用于数据交换

序列化方法涵盖了一个广泛的范围,从低级别的、特定于 Python 版本的 marshal 和独立于语言的 JSON(两者都限于基本数据类型),到更丰富但特定于 Python 的 pickle 和跨语言格式,如 XML、YAML协议缓冲区MessagePack

在本节中,我们涵盖了 Python 的 csv、json、pickle 和 shelve 模块。我们在 第二十三章 中介绍了 XML。marshal 过于低级,不适合在应用程序中使用;如果你需要维护使用它的旧代码,请参考在线文档。至于协议缓冲区、MessagePack、YAML 和其他数据交换/序列化方法(每种都具有特定的优点和缺点),我们无法在本书中覆盖所有内容;我们建议通过网络上可用的资源来学习它们。

CSV 模块

尽管 CSV(代表逗号分隔值¹)格式通常不被视为一种序列化形式,但它是一种广泛使用且方便的表格数据交换格式。由于许多数据是表格形式的,因此尽管在如何在文件中表示它存在一些争议,但 CSV 数据仍然被广泛使用。为了解决这个问题,csv 模块提供了一些方言(特定来源编码 CSV 数据方式的规范),并允许你定义自己的方言。你可以注册额外的方言,并通过调用 csv.list_dialects 函数列出可用的方言。有关方言的更多信息,请参阅模块文档

csv 函数和类

csv 模块公开了 表 12-1 中详细介绍的函数和类。它提供了两种读取器和写入器,让你在 Python 中处理 CSV 数据行时可以选择使用列表或字典。

表 12-1. csv 模块的函数和类

reader reader(csvfile, dialect='excel', **kw) 创建并返回一个 reader 对象 rcsvfile 可以是任何产生文本行(通常是行列表或使用 newline='' 打开的文件)的可迭代对象,dialect 是已注册方言的名称。要修改方言,请添加命名参数:它们的值将覆盖相同名称的方言字段。对 r 进行迭代将产生一个列表序列,每个列表包含 csvfile 的一行元素。
writer writer(csvfile, dialect='excel', **kw) 创建并返回一个写入对象 wcsvfile 是一个带有写入方法的对象(如果是文件,请使用 newline='' 打开); dialect 是一个已注册方言的名称。要修改方言,请添加命名参数:它们的值将覆盖同名的方言字段。 *w.*writerow 接受值序列,并将它们的 CSV 表示作为一行写入 csvfile。 *w.*writerows 接受这样的序列的可迭代对象,并对每个调用 *w.*writerow。您有责任关闭 csvfile

| D⁠i⁠c⁠t​R⁠e⁠a⁠d⁠e⁠r | DictReader(csvfile, fieldnames=None, restkey=None, restval=None, dialect='excel', **args,*kw)

创建并返回一个对象 r,该对象迭代 csvfile 以生成一个字典的可迭代对象(-3.8 有序字典),每一行一个字典。当给出 fieldnames 参数时,它用于命名 csvfile 中的字段;否则,字段名来自 csvfile 的第一行。如果一行包含比字段名更多的列,则额外的值保存为带有键 restkey 的列表。如果任何行中的值不足,则将这些列值设置为 restval。 dialect、kwargs 传递给底层的读取器对象。 |

| DictWriter | DictWriter(csvfile, fieldnames, restval='', extrasaction='raise', dialect='excel'*, *args, *kwds)

创建并返回一个对象 w,其 writerow 和 writerows 方法接受字典或字典的可迭代对象,并使用 csvfile 的写入方法写入它们。 fieldnames 是一个 strs 序列,字典的键。 restval 是用于填充缺少某些键的字典的值。 extrasaction 指定字典具有未列在 fieldnames 中的额外键时该如何处理:当 'raise' 时,默认时,函数在这些情况下引发 ValueError;当 'ignore' 时,函数忽略此类错误。 dialect、kwargs 传递给底层的读取器对象。您有责任关闭 csvfile(通常是使用 newline='' 打开的文件)。 |

^(a) 使用 newline='' 打开文件允许 csv 模块使用自己的换行处理,并正确处理文本字段可能包含换行符的方言。

一个 csv 示例

这里有一个简单的示例,使用 csv 从字符串列表中读取颜色数据:

`import` csv

color_data = '''\ color,r,g,b
red,255,0,0
green,0,255,0
blue,0,0,255
cyan,0,255,255
magenta,255,0,255
yellow,255,255,0
'''.splitlines()

colors = {row['color']: 
          row `for` row `in` csv.DictReader(color_data)}

print(colors['red']) 
*`# prints: {'color': 'red', 'r': '255', 'g': '0', 'b': '0'}`*

请注意,整数值被读取为字符串。csv 不执行任何数据转换;这需要通过您的程序代码与从 DictReader 返回的字典来完成。

json 模块

标准库的 json 模块支持 Python 本地数据类型(元组、列表、字典、整数、字符串等)的序列化。要序列化自定义类的实例,应实现继承自 JSONEncoder 和 JSONDecoder 的相应类。

json 函数

json 模块提供了四个关键函数,详见 表 12-2。

表 12-2. json 模块的函数

| dump | dump(value, fileobj, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=JSONEncoder, indent=None, separators=(', ', ': '), default=None, sort_keys=False, **kw) 将对象value的 JSON 序列化写入到文件对象fileobj中,fileobj必须以文本模式打开进行写入,通过调用fileobj.write 来传递文本字符串作为参数。

当 skipkeys 为True(默认为False)时,非标量类型(即不是 bool、float、int、str 或None的键)会引发异常。无论如何,标量类型的键会被转换为字符串(例如,None会变成'null'):JSON 只允许在其映射中使用字符串作为键。

| dump (cont.) | 当 ensure_ascii 为True(默认值)时,输出中的所有非 ASCII 字符都会被转义;当其为False时,它们将原样输出。当 check_circular 为True(默认值)时,value中的容器会检查循环引用,如果发现任何循环引用,则会引发 ValueError 异常;当其为False时,则跳过检查,并可能引发多种不同的异常(甚至可能导致崩溃)。

当 allow_nan 为True(默认值)时,浮点标量 nan、inf 和-inf 会输出为它们相应的 JavaScript 等效项 NaN、Infinity 和-Infinity;当其为False时,存在这些标量会引发 ValueError 异常。

您可以选择传递 cls 来使用 JSONEncoder 的自定义子类(这种高级定制很少需要,在本书中我们不涵盖这部分);在这种情况下,**kw会在实例化 cls 时传递给它的调用中使用。默认情况下,编码使用 JSONEncoder 类直接进行。

当缩进为大于 0 的整数时,dump 函数会在每个数组元素和对象成员前面加上相应数量的空格来实现“美观打印”;当缩进为小于等于 0 的整数时,dump 函数仅插入换行符。当缩进为None(默认值)时,dump 函数使用最紧凑的表示方式。缩进也可以是一个字符串,例如'\t',在这种情况下,dump 函数使用该字符串作为缩进。

separators 必须是一个包含两个元素的元组,分别是用于分隔项的字符串和用于分隔键值对的字符串。您可以显式地传递 separators=(',', ':')来确保 dump 函数不插入任何空白字符。

您可以选择传递 default 以将一些本来不能被序列化的对象转换为可序列化对象。default 是一个函数,接受一个非序列化对象作为参数,并且必须返回一个可序列化对象,或者引发 ValueError 异常(默认情况下,存在非序列化对象会引发 ValueError 异常)。

当 sort_keys 为True(默认为False)时,映射将按其键的排序顺序输出;当False时,它们将按照它们的自然迭代顺序输出(如今,对于大多数映射,是插入顺序)。

| dumps | dumps(value, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=JSONEncoder, indent=None, separators=(', ', ': '), default=None, sort_keys=False, **kw)

返回一个字符串,该字符串是对象value的 JSON 序列化结果,即 dump 将写入其文件对象参数的字符串。dumps 的所有参数与 dump 的参数完全相同。

JSON 仅序列化一个对象到每个文件中

JSON 并非所谓的框架格式:这意味着无法多次调用 dump 来将多个对象序列化到同一个文件中,也不能稍后多次调用 load 来反序列化对象,就像使用 pickle(在下一节讨论)那样。因此,从技术上讲,JSON 仅序列化单个对象到一个文件中。但是,该对象可以是一个列表或字典,其中可以包含任意数量的条目。

|

| load | load(fileobj, encoding='utf-8', cls=JSONDecoder, object_hook=None, parse_float=float, parse_int=int, parse_constant=None, object_pairs_hook=None, **kw) 创建并返回先前序列化为文件类对象fileobj中的对象vfileobj必须以文本模式打开,通过调用fileobj.read 获取fileobj的内容。调用fileobj.read 必须返回文本(Unicode)字符串。

函数 load 和 dump 是互补的。换句话说,单个调用 load(f)将反序列化在调用 dump(v, f)时序列化的相同值(可能会有一些修改:例如,所有字典键都变成字符串时)的值。

可以选择传递 cls 以使用 JSONDecoder 的自定义子类(这种高级定制很少需要,在本书中我们不涵盖它);在这种情况下,**kw将在调用 cls 时传递,并由其实例化。默认情况下,解码直接使用 JSONDecoder 类。

还可以选择传递 object_hook 或 object_pairs_hook(如果两者都传递,则 object_hook 将被忽略,只使用 object_pairs_hook),这是一个允许您实现自定义解码器的函数。当传递 object_hook 但没有传递 object_pairs_hook 时,每次将对象解码为字典时,load 都会使用 object_hook,并以该字典作为唯一参数调用 object_hook,并使用 object_hook 的返回值而不是该字典。当传递 object_pairs_hook 时,每次解码对象时,load 将使用 object_pairs_hook,并将对象的(key, value)对的列表作为唯一参数传递,顺序与输入中的顺序相同,并使用 object_pairs_hook 的返回值。这使您可以执行依赖于输入中(key, value)对顺序的专门解码。

parse_float、parse_int 和 parse_constant 是使用单个参数调用的函数:表示浮点数、整数或三个特殊常量之一('NaN'、'Infinity' 或 '-Infinity')的 str。每次识别输入中表示数字的 str 时,load 调用适当的函数,并使用函数的返回值。默认情况下,parse_float 是内置的 float 函数,parse_int 是 int,parse_constant 是一个返回特殊浮点数标量 nan、inf 或 -inf 的函数。例如,可以传递 parse_float=decimal.Decimal 以确保结果中的所有数字正常情况下都是小数(如 “decimal 模块” 中所述)。 |

loads loads(s, cls=JSONDecoder, object_hook=None, parse_float=float, parse_int=int, parse_constant=None, object_pairs_hook=None, **kw) 创建并返回之前已序列化为字符串 s 的对象 v。loads 的所有参数与 load 的参数完全相同。

一个 JSON 示例

如果需要读取多个文本文件,其文件名作为程序参数给出,并记录每个单词在文件中出现的位置,你需要记录每个单词的 (文件名, 行号) 对列表。以下示例使用 fileinput 模块迭代所有作为程序参数给出的文件,并使用 json 将 (文件名, 行号) 对列表编码为字符串,并存储在类似 DBM 的文件中(如 “DBM 模块” 中所述)。由于这些列表包含元组,每个元组包含字符串和数字,它们可以被 json 序列化:

`import` collections, fileinput, json, dbm
word_pos = collections.defaultdict(list)
`for` line `in` fileinput.input():
    pos = fileinput.filename(), fileinput.filelineno()
    `for` word `in` line.split():
        word_pos[word].append(pos)
`with` dbm.open('indexfilem', 'n') `as` dbm_out:
    `for` word, word_positions `in` word_pos.items():
        dbm_out[word] = json.dumps(word_positions)

然后,我们可以使用 json 解序列化存储在类似 DBM 文件 indexfilem 中的数据,如以下示例所示:

`import` sys, json, dbm, linecache
`with` dbm.open('indexfilem') `as` dbm_in:
 `for` word `in` sys.argv[1:]:
        `if` word `not` `in` dbm_in:
             print(f'Word {word!r} not found in index file',                                             file=sys.stderr)
 `continue`
        places = json.loads(dbm_in[word])
        `for` fname, lineno `in` places:
            print(f'Word {word!r} occurs in line {lineno}'
                  f' of file {fname!r}:')
            print(linecache.getline(fname, lineno), end='')

pickle 模块

pickle 模块提供了名为 Pickler 和 Unpickler 的工厂函数,用于生成对象(不可子类化类型的实例,而不是类),这些对象包装文件并提供 Python 特定的序列化机制。通过这些模块进行序列化和反序列化也称为 picklingunpickling

序列化与深拷贝具有某些相同的问题,如 “copy 模块” 中所述。pickle 模块处理这些问题的方式与 copy 模块非常相似。序列化,就像深拷贝一样,意味着在引用的有向图上进行递归遍历。pickle 保留了图的形状:当多次遇到相同的对象时,仅第一次序列化该对象,其他出现的相同对象序列化为对该单一值的引用。pickle 还正确地序列化具有引用循环的图。然而,这意味着如果可变对象 o 被序列化多次到同一个 Pickler 实例 p,则在第一次将 o 序列化到 p 后对 o 的任何更改都不会被保存。

在序列化正在进行时不要更改对象

为了清晰、正确和简单起见,在 Pickler 实例序列化过程中不要更改正在序列化的对象。

pickle 可以使用遗留 ASCII 协议或多个紧凑的二进制协议进行序列化。表 12-3 列出了可用的协议。

表 12-3. pickle 协议

协议 格式 Python 版本新增 描述
0 ASCII 1.4^(a) 可读性强,序列化/反序列化速度慢
1 二进制 1.5 早期二进制格式,被协议 2 取代
2 二进制 2.3 改进对后期 Python 2 特性的支持
3 二进制 3.0 (-3.8 默认)增加对字节对象的具体支持
4 二进制 3.4 (3.8+ 默认)支持非常大的对象
5 二进制 3.8 3.8+ 添加了支持作为传输过程中序列化的 pickling 特性,参见 PEP 574
^(a) 或可能更早。这是可在 Python.org 上找到的最古老版本的文档。

始终使用协议 2 或更高版本进行 Pickle

始终使用至少协议 2。尺寸和速度节省可观,并且二进制格式基本没有任何缺点,除了导致生成的 pickle 与真正古老版本的 Python 不兼容之外。

当重新加载对象时,pickle 会透明地识别并使用当前 Python 版本支持的任何协议。

pickle(腌制)通过名称而非数值序列化类和函数²。因此,pickle 只能在反序列化时从与 pickle 序列化时相同模块中导入类或函数。特别地,pickle 通常只能序列化和反序列化类和函数,如果它们是其各自模块的顶级名称(即属性)。考虑以下示例:

`def` adder(augend):
    `def` inner(addend, augend=augend):
        `return` addend+augend
    `return` inner
plus5 = adder(5)

此代码将一个闭包绑定到名称 plus5(如“嵌套函数和嵌套作用域”中所述)——一个内部函数 inner 与适当的外部作用域。因此,尝试对 plus5 进行 pickle 会引发 AttributeError:只有当函数处于顶级时,才能对其进行 pickle,而此代码中其闭包绑定到名称 plus5 的函数 inner 并非顶级,而是嵌套在函数 adder 内部。类似的问题也适用于序列化嵌套函数和嵌套类(即不在顶层的类)。

pickle 函数和类

pickle 模块公开了表 12-4 中列出的函数和类。

表 12-4. pickle 模块的函数和类

| dump, dumps | dump(value, fileobj, protocol=None, bin=None), dumps(value, protocol=None, bin=None)

dumps 返回表示对象 value 的字节串。dump 将相同的字符串写入类似文件的对象 fileobj,该对象必须已打开以供写入。dump(v, f) 就像 f.write(dumps(v))。protocol 参数可以是 0(ASCII 输出,最慢和最庞大的选项),或者更大的整数表示各种类型的二进制输出(参见 Table 12-3)。除非 protocol 是 0,否则传递给 dump 的 fileobj 参数必须已打开以供二进制写入。不要传递 bin 参数,它仅为与旧版本 Python 的兼容性而存在。 |

| load, loads | load(fileobj), loads(s, *, fix_imports=True, encoding="ASCII", errors="strict")

函数 load 和 dump 是互补的。换句话说,对 load(f) 的一系列调用将反序列化与之前通过对 dump(v, f) 进行一系列调用而创建 f 内容时序列化的相同值。load 从类似文件的对象 fileobj 中读取正确数量的字节,并创建并返回由这些字节表示的对象 v。load 和 loads 透明地支持在任何二进制或 ASCII 协议中执行的 pickles。如果数据以任何二进制格式进行 pickle,文件必须对于 dump 和 load 都以二进制方式打开。load(f) 就像 Unpickler(f).load()。 |

| load, loads

(cont.) | loads 创建并返回由字节串 s 表示的对象 v,因此对于任何支持的类型的对象 vv==loads(dumps(v))。如果 s 比 dumps(v) 长,loads 会忽略额外的字节。提供了可选参数 fix_imports、encoding 和 errors 来处理由 Python 2 代码生成的流;请参阅 pickle.loads 文档 以获取更多信息。

永远不要反序列化不受信任的数据

从不受信任的数据源进行反序列化是一种安全风险;攻击者可能利用此漏洞执行任意代码。

|

序列化器 序列化器(fileobj, protocol=None, bin=None) 创建并返回一个对象 p,使得调用 p.dump 相当于调用 dump 函数并传递给 Pickler fileobj、protocol 和 bin 参数。为了将多个对象序列化到文件中,Pickler 比重复调用 dump 更方便且更快。你可以子类化 pickle.Pickler 来覆盖 Pickler 方法(尤其是 persistent_id 方法)并创建你自己的持久化框架。然而,这是一个高级主题,在本书中不再进一步讨论。
反序列化器 反序列化器(fileobj) 创建并返回一个对象 u,使得调用 u.load 相当于调用 load 并传递 fileobj 参数给 Unpickler。为了从文件中反序列化多个对象,Unpickler 比重复调用 load 函数更方便且更快。你可以子类化 pickle.Unpickler 来覆盖 Unpickler 方法(尤其是 persistent_load 方法)并创建你自己的持久化框架。然而,这是一个高级主题,在本书中不再进一步讨论。

一个序列化的例子

以下示例处理与之前显示的 json 示例相同的任务,但使用 pickle 而不是 json 将(filename, linenumber)对的列表序列化为字符串:

`import` collections, fileinput, pickle, dbm
word_pos = collections.defaultdict(list)
`for` line `in` fileinput.input():
    pos = fileinput.filename(), fileinput.filelineno()
    `for` word `in` line.split():
        word_pos[word].append(pos)

`with` dbm.open('indexfilep', 'n') `as` dbm_out:
    `for` word, word_positions `in` word_pos.items():
        dbm_out[word] = pickle.dumps(word_positions, protocol=2)

然后,我们可以使用 pickle 从类似 DBM 的文件indexfilep中读回存储的数据,如下例所示:

`import` sys, pickle, dbm, linecache
`with` dbm.open('indexfilep') `as` dbm_in:
 `for` word `in` sys.argv[1:]:
        `if` word `not` `in` dbm_in:
            print(f'Word {word!r} not found in index file',
                  file=sys.stderr)
 `continue`
        places = pickle.loads(dbm_in[word])
        `for` fname, lineno `in` places:
            print(f'Word {word!r} occurs in line {lineno}'
                  f' of file {fname!r}:')
            print(linecache.getline(fname, lineno), end='')

对实例进行 pickle

为了让 pickle 重新加载实例x,pickle 必须能够从 pickle 保存实例时定义类的同一模块中导入x的类。以下是 pickle 如何保存类T的实例对象x的状态,并将保存的状态重新加载到类T的新实例y中(重新加载的第一步始终是创建T的新空实例y,除非另有明确说明):

  • T提供方法 getstate 时,pickle 保存调用T.getstate(x)的结果d

  • T提供方法 setstate 时,d可以是任何类型,并且 pickle 通过调用T.setstate(y, d)重新加载保存的状态。

  • 否则,d必须是一个字典,pickle 只需设置y.dict = d

  • 否则,当T提供方法 getnewargs,并且 pickle 使用协议 2 或更高版本进行 pickle 时,pickle 保存调用T.getnewargs(x)的结果tt必须是一个元组。

  • 在这种情况下,pickle 不会从空y开始,而是通过执行y = T.new(T, t)来创建y*,从而完成重新加载。

  • 否则,默认情况下,pickle 将x.dict 保存为字典d

  • T提供方法 setstate 时,pickle 通过调用T.setstate(y, d)重新加载保存的状态。

  • 否则,pickle 只需设置y.dict = d

pickle 保存和重新加载的dt对象中的所有项(通常是字典或元组)必须依次是适合进行 pickle 和 unpickle(即pickleable)的类型的实例,并且如有必要,该过程可以递归重复进行,直到 pickle 到达原始的 pickleable 内置类型(如字典、元组、列表、集合、数字、字符串等)。

如“copy 模块”中所述,getnewargsgetstatesetstate 特殊方法还控制实例对象的复制和深度复制方式。如果一个类定义了 slots,因此其实例没有 dict 属性,pickle 会尽力保存和恢复等同于 slots 名称和值的字典。然而,这样的类应该定义 getstatesetstate;否则,其实例可能无法正确进行 pickle 和复制。

使用 copyreg 模块进行 pickle 定制

通过向 copyreg 模块注册工厂和减少函数,可以控制 pickle 如何序列化和反序列化任意类型的对象。当您在 C 代码的 Python 扩展中定义类型时,这尤为有用。copyreg 模块提供了 表 12-5 中列出的函数。

表 12-5. copyreg 模块的函数

constructor constructor(fcon) 将 fcon 添加到构造函数表中,该表列出 pickle 可能调用的所有工厂函数。fcon 必须是可调用的,通常是一个函数。

| pickle | pickle(type, fred, fcon=None) 将函数 fred 注册为类型 type 的减少函数,其中 type 必须是一个类型对象。要保存类型为 type 的对象 o,模块 pickle 调用 fred(o) 并保存结果。fred(o) 必须返回一个元组 (fcon, t) 或 (fcon, t, d),其中 fcon 是一个构造函数,t 是一个元组。要重新加载 o,pickle 使用 o=fcon(*t)。然后,当 fred 还返回 d 时,pickle 使用 d 来恢复 o 的状态(如果 o 提供了 setstate,则 o.setstate(d);否则,o.dict.update(d)),如前一节所述。如果 fcon 不为 None,pickle 还会调用构造函数 (fcon) 来注册 fcon 作为构造函数。

pickle 不支持对代码对象的 pickle 操作,但 marshal 支持。以下是如何通过利用 copyreg 将 pickle 定制以支持代码对象的示例:

>>> `import` pickle, copyreg, marshal
>>> `def` marsh(x):
...     `return` marshal.loads, (marshal.dumps(x),)
...
>>> c=compile('2+2','','eval')
>>> copyreg.pickle(type(c), marsh)
>>> s=pickle.dumps(c, 2)
>>> cc=pickle.loads(s)
>>> print(eval(cc))
4

使用 marshal 使你的代码依赖于 Python 版本

在你的代码中使用 marshal 时要小心,就像前面的示例一样。marshal 的序列化不能保证跨版本稳定,因此使用 marshal 意味着其他版本的 Python 编写的程序可能无法加载你的程序序列化的对象。

|

shelve 模块

shelve 模块通过协调 pickle、io 和 dbm(及其底层访问 DBM 类型归档文件的模块,如下一节所述),提供了一个简单、轻量级的持久化机制。

shelve 提供了一个函数 open,其多态性类似于 dbm.open。shelve.open 返回的映射 s 比 dbm.open 返回的映射 a 更为灵活。a 的键和值必须是字符串。³ s 的键也必须是字符串,但 s 的值可以是任何可 pickle 的类型。pickle 定制(copyreg、getnewargsgetstatesetstate)同样适用于 shelve,因为 shelve 将序列化工作委托给 pickle。键和值以字节形式存储。使用字符串时,在存储之前会隐式地转换为默认编码。

当你在使用 shelve 与可变对象时要小心一个微妙的陷阱:当你对一个存储在 shelf 中的可变对象进行操作时,除非将更改的对象重新分配回相同的索引,否则更改不会存储回去。例如:

`import` shelve
s = shelve.open('data')
s['akey'] = list(range(4))
print(s['akey'])           *`# prints: [0, 1, 2, 3]`*
s['akey'].append(9)        *`# trying direct mutation`*
print(s['akey'])           *`# doesn't "take"; prints: [0, 1, 2, 3]`*
x = s['akey']              *`# fetch the object`*
x.append(9)                *`# perform mutation`*
s['akey'] = x              *`# key step: store the object back!`*
print(s['akey'])           *`# now it "takes", prints: [0, 1, 2, 3, 9]`*

当调用 shelve.open 时,通过传递命名参数 writeback=True,可以解决这个问题,但这可能严重影响程序的性能。

一个 shelve 示例

下面的示例处理与之前的 json 和 pickle 示例相同的任务,但使用 shelve 来持久化 (filename, linenumber) 对的列表:

`import` collections, fileinput, shelve
word_pos = collections.defaultdict(list)
`for` line `in` fileinput.input():
    pos = fileinput.filename(), fileinput.filelineno()
    `for` word `in` line.split():
        word_pos[word].append(pos)
`with` shelve.open('indexfiles','n') `as` sh_out:
    sh_out.update(word_pos)

然后,我们必须使用 shelve 读取存储到类似 DBM 的文件 indexfiles 中的数据,如下例所示:

`import` sys, shelve, linecache
`with` shelve.open('indexfiles') `as` sh_in:
 `for` word `in` sys.argv[1:]:
        if word `not` `in` sh_in:
            print(f'Word {word!r} not found in index file',
			      file=sys.stderr)
 `continue`
        places = sh_in[word]
        `for` fname, lineno `in` places:
            print(f'Word {word!r} occurs in line {lineno}'
                  f' of file {fname!r}:')
            print(linecache.getline(fname, lineno), end='')

这两个示例是本节中显示的各种等效示例中最简单、最直接的示例。这反映了 shelve 比之前示例中使用的模块更高级的事实。

DBM 模块

DBM,长期以来是 Unix 的主要组成部分,是一组支持包含字节串对 (key, data) 的数据文件的库。DBM 提供了根据键快速获取和存储数据的功能,这种使用模式称为 键访问。尽管键访问远不及关系数据库的数据访问功能强大,但它的开销较小,对于某些程序的需求可能足够。如果 DBM 类似的文件对您的目的足够,通过这种方法,您可以得到一个比使用关系数据库更小更快的程序。

DBM 数据库以字节为导向

DBM 数据库要求键和值都是字节值。稍后包含的示例中,您将看到文本输入在存储之前被明确编码为 UTF-8。类似地,在读取值时必须执行逆解码。

Python 标准库中的 DBM 支持以一种清晰而优雅的方式组织:dbm 包公开了两个通用函数,同一包中还有其他模块提供特定的实现。

Berkeley DB 接口

bsddb 模块已从 Python 标准库中移除。如果您需要与 BSD DB 存档交互,我们建议使用出色的第三方包 bsddb3

DBM 包

dbm 包提供了 表 12-6 中描述的顶层函数。

表 12-6. dbm 包的函数

| open | open(filepath, flag='r', mode=0o666) 打开或创建由 filepath(任何文件的路径)指定的 DBM 文件,并返回与 DBM 文件对应的映射对象。当 DBM 文件已经存在时,open 使用 whichdb 函数来确定哪个 DBM 子模块可以处理文件。当 open 创建新的 DBM 文件时,它会按照以下偏好顺序选择第一个可用的 dbm 子模块:gnu、ndbm、dumb。

flag 是一个告诉 open 如何打开文件以及是否创建文件的单字符字符串,根据 表 12-7 中所示的规则。mode 是一个整数,如果 open 创建文件,则 open 会将其用作文件的权限位,如 “使用 open 创建文件对象” 中所述。

表 12-7. dbm.open 的标志值

| 标志 | 只读? | 如果文件存在: | 如果文件不存在: |

| --- | --- | --- | --- |

| 'r' | 是 | 打开文件 | 报错 |

| 'w' | 否 | 打开文件 | 报错 |

| 'c' | 否 | 打开文件 | 创建文件 |

| 'n' | 否 | 截断文件 | 创建文件 |

dbm.open 返回一个映射对象m,其功能子集类似于字典(详见“Dictionary Operations”)。m只接受字节作为键和值,而且m提供的唯一非特殊映射方法是m.get、m.keys 和m.setdefault。你可以使用与字典相同的索引语法m[]绑定、重新绑定、访问和解绑m中的项目。如果标志是'r',则m是只读的,因此你只能访问m的项目,而不能绑定、重新绑定或解绑它们。你可以使用通常的表达式s in m检查字符串s是否是m的键;你不能直接在m上进行迭代,但可以等效地在m.keys()上进行迭代。

m提供的一个额外方法是m.close,其语义与文件对象的 close 方法相同。与文件对象一样,当你使用完m后,应确保调用m.close。使用“try/finally”中介绍的try/finally语句是确保最终化的一种方式,但使用“The with Statement and Context Managers”中介绍的with语句更好(因为m是上下文管理器,你可以使用with)。 |

| whichdb | whichdb(文件路径) 打开并读取指定的文件路径,以确定创建文件的 dbm 子模块。当文件不存在或无法打开和读取时,whichdb 返回None。当文件存在并可以打开和读取,但无法确定创建文件的 dbm 子模块时(通常意味着文件不是 DBM 文件),whichdb 返回''。如果可以找出哪个模块可以读取类似 DBM 的文件,whichdb 返回一个命名 dbm 子模块的字符串,例如'dbm.ndbm'、'dbm.dumb'或'dbm.gdbm'。

除了这两个顶级函数外,dbm 包还包含特定模块,如 ndbm、gnu 和 dumb,提供各种 DBM 功能的实现,通常你只通过这些顶级函数访问。第三方包可以在 dbm 中安装更多的实现模块。

DBM 包唯一保证在所有平台上存在的实现模块是 dumb。dumb 提供了最小的 DBM 功能和一般性能;其唯一优点是您可以在任何地方使用,因为 dumb 不依赖于任何库。通常您不会直接 import dbm.dumb:而是 import dbm,让 dbm.open 在当前 Python 安装中提供最好的可用 DBM 模块,如果没有更好的子模块,则默认使用 dumb。唯一需要直接导入 dumb 的情况是在需要创建一个在任何 Python 安装中都可读的类似 DBM 的文件时。dumb 模块提供了一个 open 函数,与 dbm 的 open 函数多态。

DBM-Like 文件的使用示例

DBM 的键控访问适合在程序需要持久记录类似 Python 字典的等效内容时使用,其中键和值都是字符串。例如,假设您需要分析几个文本文件,这些文件名是作为程序参数给出的,并记录每个单词在这些文件中出现的位置。在这种情况下,键是单词,因此本质上是字符串。您需要记录的每个单词的数据是一个 (filename, linenumber) 对的列表。然而,您可以通过几种方法将数据编码为字符串,例如利用路径分隔符字符串 os.pathsep(在“os 模块的路径字符串属性”中介绍),因为该字符串通常不会出现在文件名中。(关于将数据编码为字符串的更一般方法在本章开头的部分有介绍,使用了相同的例子。)在这种简化情况下,编写一个记录文件中单词位置的程序可能如下所示:

`import` collections, fileinput, os, dbm
word_pos = collections.defaultdict(list)
`for` line `in` fileinput.input():
    pos = f'{fileinput.filename()}{os.pathsep}{fileinput.filelineno()}'
    `for` word `in` line.split():
        word_pos[word].append(pos)
sep2 = os.pathsep * 2
`with` dbm.open('indexfile','n') `as` dbm_out:
    `for` word `in` word_pos:
        dbm_out[word.encode('utf-8')] = sep2.join(
			word_pos[word]
		).encode('utf-8')

您可以通过几种方式读取存储在类似 DBM 文件 indexfile 中的数据。下面的示例接受单词作为命令行参数,并打印请求单词出现的行:

`import` sys, os, dbm, linecache

sep = os.pathsep
sep2 = sep * 2
`with` dbm.open('indexfile') `as` dbm_in:
 `for` word `in` sys.argv[1:]:
        e_word = word.encode('utf-8')
        `if` e_word `not` `in` dbm_in:
            print(f'Word {word!r} not found in index file',
                  file=sys.stderr)
            `continue`
        places = dbm_in[e_word].decode('utf-8').split(sep2)
        `for` place `in` places:
            fname, lineno = place.split(sep)
            print(f'Word {word!r} occurs in line {lineno}'
                  f' of file {fname!r}:')
            print(linecache.getline(fname, int(lineno)), end='')

Python 数据库 API(DBAPI)

正如前面提到的,Python 标准库并不附带关系数据库管理系统接口(除了 sqlite3,在“SQLite”中介绍的,它是一个丰富的实现,而不仅仅是接口)。许多第三方模块允许您的 Python 程序访问特定的数据库。这些模块大多遵循 Python 数据库 API 2.0 标准,也称为 DBAPI,如 PEP 249 中所述。

导入任何符合 DBAPI 标准的模块后,您可以使用特定于 DB 的参数调用模块的 connect 函数。connect 返回 x,Connection 的一个实例,表示与 DB 的连接。x 提供 commit 和 rollback 方法来处理事务,提供一个 close 方法,在您完成 DB 操作后调用,以及一个 cursor 方法来返回 c,Cursor 的一个实例。c 提供了用于 DB 操作的方法和属性。符合 DBAPI 标准的模块还提供了异常类、描述性属性、工厂函数和类型描述属性。

异常类

符合 DBAPI 标准的模块提供了异常类 Warning、Error 和 Error 的几个子类。Warning 指示插入时的数据截断等异常。Error 的子类指示您的程序在处理与 DB 和与之接口的符合 DBAPI 标准的模块时可能遇到的各种错误。通常,您的代码使用以下形式的语句:

`try`:
    ...
`except` module.Error `as` err:
    ...

以捕获您需要处理而不终止的所有与 DB 相关的错误。

线程安全

当 DBAPI 兼容的模块具有大于 0 的 threadsafety 属性时,该模块在 DB 接口中断言了某种程度的线程安全性。与依赖于此不同,通常更安全,且始终更可移植,要确保单个线程对任何给定的外部资源(如 DB)具有独占访问权,如 “线程化程序架构” 中所述。

参数样式

符合 DBAPI 标准的模块有一个称为 paramstyle 的属性,用于识别用作参数占位符的标记样式。在传递给 Cursor 实例方法(如 execute 方法)的 SQL 语句字符串中插入这些标记,以使用运行时确定的参数值。举个例子,假设您需要获取字段 AFIELD 等于 Python 变量 x 当前值的 DB 表 ATABLE 的行。假设光标实例命名为 c,您理论上(但非常不建议!)可以使用 Python 的字符串格式化执行此任务:

c.execute(f'SELECT * FROM ATABLE WHERE AFIELD={x!r}')

避免使用 SQL 查询字符串格式化:使用参数替换

字符串格式化是推荐的方法。它为每个 x 的值生成不同的字符串,每次都需要解析和准备语句;它还存在安全弱点的可能性,如 SQL 注入 漏洞。使用参数替换,您将传递一个具有占位符而不是参数值的单个语句字符串给 execute。这样一来,execute 只需解析和准备语句一次,以获得更好的性能;更重要的是,参数替换提高了稳固性和安全性,阻碍了 SQL 注入攻击。

例如,当模块的 paramstyle 属性(下文描述)为 'qmark' 时,您可以将前面的查询表示为:

c.execute('SELECT * FROM ATABLE WHERE AFIELD=?', (some_value,))

只读字符串属性 paramstyle 告诉您的程序如何使用该模块进行参数替换。paramstyle 的可能值如 表 12-8 中所示。

表 12-8. paramstyle 属性的可能值

| format | 标记是 %s,就像旧式字符串格式化一样(始终使用 s:不要使用其他类型指示符字母,无论数据的类型是什么)。一个查询看起来像:

c.execute('SELECT * FROM ATABLE WHERE AFIELD=%s', 
          (some_value,))

|

| named | 标记是:name,参数是命名的。一个查询看起来像:

c.execute('SELECT * FROM ATABLE WHERE AFIELD=:x', 
          {'x':some_value})

|

| numeric | 标记是:n,给出参数的编号,从 1 开始。一个查询看起来像:

c.execute('SELECT * FROM ATABLE WHERE AFIELD=:1', 
          (some_value,))

|

| pyformat | 标记为 %(name)s,参数带有命名。始终使用 s:永不使用其他类型指示符,无论数据类型如何。查询的样子是:

c.execute('SELECT * FROM ATABLE WHERE AFIELD=%(x)s',
          {'x':some_value})

|

| qmark | 标记为 ?。查询的样子是:

c.execute('SELECT * FROM ATABLE WHERE AFIELD=?', (x,))

|

当参数被命名时(即 paramstyle 是 'pyformat' 或 'named'),execute 方法的第二个参数是一个映射。否则,第二个参数是一个序列。

format 和 pyformat 只接受类型指示符 s

格式或 pyformat 的唯一有效类型指示符是 s;不接受任何其他类型指示符——例如,永远不要使用 %d 或 %(name)d。无论数据类型如何,都要使用 %s 或 %(name)s 进行所有参数替换。

工厂函数

通过占位符传递给数据库的参数通常必须是正确的类型:这意味着 Python 数字(整数或浮点数值)、字符串(字节或 Unicode)以及None表示 SQL NULL。没有普遍用于表示日期、时间和二进制大对象(BLOBs)的类型。DBAPI 兼容模块提供工厂函数来构建这些对象。大多数 DBAPI 兼容模块用于此目的的类型由 datetime 模块提供(在第 13 章中详述),并且用于 BLOBs 的类型为字符串或缓冲区类型。DBAPI 指定的工厂函数列在表 12-9 中。(*FromTicks 方法接受整数时间戳 s,表示自模块 time 纪元以来的秒数,在第 13 章中详述。)

表 12-9. DBAPI 工厂函数

二进制 Binary(string) 返回表示给定字节string的对象作为 BLOB。
日期 Date(year, month, day) 返回表示指定日期的对象。
DateFromTicks DateFromTicks(s) 返回表示整数时间戳 s 的日期对象。例如,DateFromTicks(time.time()) 表示“今天”。
时间 Time(hour, minute, second) 返回表示指定时间的对象。
TimeFromTicks TimeFromTicks(s) 返回表示整数时间戳 s 的时间对象。例如,TimeFromTicks(time.time()) 表示“当前时间”。
时间戳 Timestamp(year, month, day, hour, minute, second) 返回表示指定日期和时间的对象。
TimestampFromTicks TimestampFromTicks(s) 返回表示整数时间戳 s 的日期和时间对象。例如,TimestampFromTicks(time.time()) 是当前日期和时间。

类型描述属性

游标实例的 description 属性描述了您在该游标上最后执行的每个 SELECT 查询的列的类型和其他特征。每列的类型(描述列的元组的第二项)等于 DBAPI 兼容模块的以下属性之一:

二进制 描述包含 BLOBs 的列
DATETIME 描述包含日期、时间或两者的列
NUMBER 描述包含任何类型数字的列
ROWID 描述包含行标识号的列
STRING 描述包含任何类型文本的列

一个 cursor 的描述,尤其是每一列的类型,对于了解程序所使用的 DB 是非常有用的。这种内省可以帮助你编写通用的模块,并使用不同模式的表,包括在编写代码时可能不知道的模式。

connect 函数

一个 DBAPI 兼容模块的 connect 函数接受依赖于 DB 类型和具体模块的参数。DBAPI 标准建议 connect 接受命名参数。特别是,connect 至少应接受以下名称的可选参数:

database 要连接的具体数据库名称
dsn 用于连接的数据源名称
host 数据库运行的主机名
password 用于连接的密码
user 用于连接的用户名

Connection 对象

一个 DBAPI 兼容模块的 connect 函数返回一个对象 x,它是 Connection 类的一个实例。 x 提供了 Table 12-10 中列出的方法。

Table 12-10. 类 Connection 的实例 x 的方法

close x.close() 终止 DB 连接并释放所有相关资源。在完成 DB 操作后立即调用 close。不必要地保持 DB 连接开启可能会严重消耗系统资源。
commit x.commit() 提交当前的 DB 事务。如果 DB 不支持事务,则 x.commit() 不会有任何操作。
cursor x.cursor() 返回 Cursor 类的一个新实例(在下一节中介绍)。
rollback x.rollback() 回滚当前的 DB 事务。如果 DB 不支持事务,则 x.rollback() 会引发异常。DBAPI 建议,对于不支持事务的 DB,Connection 类不应提供回滚方法,因此 x.rollback() 会引发 AttributeError:你可以通过 hasattr(x, 'rollback') 测试是否支持事务。

Cursor 对象

连接实例提供了一个 cursor 方法,返回一个名为 c 的 Cursor 类实例对象。SQL 游标表示查询结果集,并允许您按顺序逐个处理该集合中的记录。由 DBAPI 建模的游标是一个更丰富的概念,因为它是程序执行 SQL 查询的唯一方式。另一方面,DBAPI 游标只允许您在结果序列中前进(一些关系型数据库,但不是所有的,还提供更高功能的游标,可以前后移动),并且不支持 SQL 子句 WHERE CURRENT OF CURSOR。DBAPI 游标的这些限制使得 DBAPI 兼容模块能够在根本不提供真正 SQL 游标的 RDBMS 上提供 DBAPI 游标。类 Cursor 的实例 c 提供了许多属性和方法;最常用的属性和方法如 Table 12-11 所示。 |

表 12-11. 类 Cursor 的实例 c 的常用属性和方法

close c.close() 关闭游标并释放所有相关资源。

| description | 只读属性,是一个由七项元组组成的序列,每项对应最后执行的查询中的一个列:名称、类型代码、显示大小、内部大小、精度、比例。

可为空

c.description 如果最后对 c 的操作不是 SELECT 查询或返回的列描述不可用,则为 None。游标的描述主要用于关于程序正在使用的数据库的内省。这种内省可以帮助您编写通用模块,能够处理使用不同模式的表,包括编写代码时可能不完全了解的模式。 |

execute c.execute(statement, parameters=None) 在给定参数的情况下,在 DB 上执行 SQL statement 字符串。当模块的 paramstyle 为 'format'、'numeric' 或 'qmark' 时,parameters 是一个序列;当 paramstyle 为 'named' 或 'pyformat' 时,parameters 是一个映射。某些 DBAPI 模块要求序列必须明确为元组。

| executemany | c.executemany(statement, *parameters) 对 DB 执行 SQL statement,对给定的 parameters 中的每个项执行一次。当模块的 paramstyle 为 'format'、'numeric' 或 'qmark' 时,parameters 是一个序列的序列;当 paramstyle 为 'named' 或 'pyformat' 时,parameters 是映射的序列。例如,当 paramstyle 为 'qmark' 时,语句:

c.executemany('UPDATE atable SET x=? '
              'WHERE y=?',(12,23),(23,34))

等同于但比以下两个语句更快:

c.execute('UPDATE atable SET x=12 WHERE y=23')
c.execute('UPDATE atable SET x=23 WHERE y=34')

|

fetchall c.fetchall() 返回最后查询的所有剩余行作为元组序列。如果最后的操作不是 SELECT,则会引发异常。
fetchmany c.fetchmany(n) 返回最后查询的最多 n 行作为元组序列。如果最后的操作不是 SELECT,则会引发异常。
fetchone c.fetchone() 将从上次查询中返回下一行作为元组。如果上次操作不是 SELECT,则引发异常。
rowcount 一个只读属性,指定了最后一个操作获取或影响的行数,如果模块无法确定这个值,则为 -1。

DBAPI 兼容模块

无论你想使用哪种关系型数据库,至少有一个(通常是多个)Python DBAPI 兼容模块可以从互联网上下载。有这么多的数据库和模块,可能性的集合如此不断变化,我们不可能列出所有的,也无法长期维护这个列表。因此,我们建议你从社区维护的 wiki 页面 开始,这个页面有可能在任何时候都是完整和最新的。

因此,接下来只是一个非常短暂且特定时间的,与撰写时非常流行且与非常流行的开源数据库接口的非常少量 DBAPI 兼容模块的列表:

ODBC 模块

Open Database Connectivity (ODBC) 是连接许多不同的数据库的标准方法,包括一些其他 DBAPI 兼容模块不支持的数据库。对于具有自由开源许可的 ODBC 兼容 DBAPI 兼容模块,请使用 pyodbc;对于商业支持的模块,请使用 mxODBC

MySQL 模块

MySQL 是一个流行的开源关系型数据库管理系统,于 2010 年被 Oracle 收购。Oracle 提供的“官方”DBAPI 兼容接口是 mysql-connector-python。MariaDB 项目也提供了一个 DBAPI 兼容接口,mariadb,连接到 MySQL 和 MariaDB(一个 GPL 许可的分支)。

PostgreSQL 模块

PostgreSQL 是另一个流行的开源关系型数据库管理系统。它的一个广泛使用的 DBAPI 兼容接口是 psycopg3,它是受欢迎的 psycopg2 包的合理化重写和扩展。

SQLite

SQLite 是一个用 C 编写的库,实现了一个关系型数据库,可以存储在单个文件中,甚至在内存中用于足够小和短暂的情况。Python 的标准库提供了 sqlite3 包,它是与 SQLite 兼容的 DBAPI 接口。

SQLite 具有丰富的高级功能,有许多选项可供选择;sqlite3 提供了访问这些功能的大部分能力,同时提供更多可能性,使得 Python 代码与底层数据库之间的交互更加平稳和自然。我们无法在本书中涵盖这两个强大软件系统的每一个细节;相反,我们专注于最常用和最有用的函数子集。要获取更详细的信息,包括示例和最佳实践建议,请参阅 SQLitesqlite3 的文档,以及 Jay Kreibich 的 Using SQLite(O’Reilly)。

sqlite3 包还提供了 表 12-12 中的函数,以及其他函数。

表 12-12. sqlite3 模块的一些有用函数

| connect | connect(filepath, timeout=5.0, detect_types=0, isolation_level='', check_same_thread=True, factory=Connection, cached_statements=100, uri=False) 连接到名为 filepath 的 SQLite 数据库文件(如果需要则创建),并返回 Connection 类的实例(或传递的子类)。要创建内存中的数据库,请将 ':memory:' 作为第一个参数传递给 filepath

如果 True,uri 参数激活 SQLite 的 URI 功能,允许通过 filepath 参数一起传递一些额外的选项。

timeout 是在事务中有另一个连接锁定数据库时等待抛出异常之前的秒数。

sqlite3 直接支持以下 SQLite 原生类型,将其转换为相应的 Python 类型:

  • BLOB:转换为字节

  • INTEGER:转换为整数

  • NULL:转换为 None

  • REAL:转换为浮点数

  • TEXT:取决于 Connection 实例的 text_factory 属性,在 表 12-13 中有所涉及;默认为 str

除此以外的任何类型名称被视为 TEXT,除非经过适当的检测并通过函数 register_converter 注册的转换器传递。为了允许类型名称检测,可以传递 detect_types 参数,该参数可以是 sqlite3 包提供的 PARSE_COLNAMES 或 PARSE_DECLTYPES 常量(或两者都使用,通过 | 位 OR 运算符连接)。

当你传递 detect_types=sqlite3.PARSE_COLNAMES 时,类型名称取自于 SQL SELECT 语句中检索列的列名;例如,检索为 foo AS [foo CHAR(10)] 的列具有类型名称 CHAR。

当你传递 detect_types=sqlite3.PARSE_DECLTYPES 时,类型名称取自于原始 CREATE TABLE 或 ALTER TABLE SQL 语句中添加列的声明;例如,声明为 foo CHAR(10) 的列具有类型名称 CHAR。

当你传递 detect_types=sqlite3.PARSE_COLNAMES | sqlite3.PARSE_DECLTYPES 时,两种机制都会被使用,优先使用列名(当列名至少有两个单词时,第二个单词在这种情况下给出类型名),如果没有则回退到声明时给定的类型(在这种情况下,声明类型的第一个单词给出类型名)。

isolation_level 允许你在 SQLite 处理事务时行使一些控制;它可以是 ''(默认值)、None(使用 autocommit 模式)或三个字符串之一:'DEFERRED'、'EXCLUSIVE' 或 'IMMEDIATE'。SQLite 在线文档详细介绍了 事务类型 及其与 SQLite 固有执行的各种 文件锁定 的关系。

连接 (续) 默认情况下,连接对象只能在创建它的 Python 线程中使用,以避免因程序中的轻微错误(多线程编程中常见的不幸问题)而导致数据库损坏。如果你对线程在使用锁和其他同步机制方面完全自信,并且需要在多个线程之间重用连接对象,你可以传递 check_same_thread=False。sqlite3 将不再执行任何检查,相信你知道自己在做什么,并且你的多线程架构没有任何缺陷——祝你好运!cached_statements 是 sqlite3 缓存的 SQL 语句数量,以解析和准备的状态保存,以避免重复解析它们所带来的开销。你可以传递一个低于默认值 100 的值以节省一些内存,或者如果你的应用程序使用了多种多样的 SQL 语句,可以传递一个更大的值。
r⁠e⁠g⁠i⁠s⁠t⁠e⁠r⁠_​a⁠d⁠a⁠p⁠t⁠e⁠r register_adapter(type, callable) 将 callable 注册为从任何 Python 类型 type 的对象到 sqlite3 直接处理的几种 Python 类型之一(int、float、str 和 bytes)的相应值的 adaptercallable 必须接受一个参数,即要适配的值,并返回一个 sqlite3 直接处理的类型的值。
r⁠e⁠g⁠i⁠s⁠t⁠e⁠r⁠_​c⁠o⁠n⁠v⁠e⁠r⁠t⁠e⁠r register_converter(typename, callable) 将 callable 注册为从 SQL 中标识为某种 typename 类型的值(请参阅 connect 函数的 detect_types 参数的描述,了解类型名是如何确定的)到相应 Python 对象的 convertercallable 必须接受一个参数,即从 SQL 获取的值的字符串形式,并返回相应的 Python 对象。typename 的匹配区分大小写。

另外,sqlite3 还提供了 Connection、Cursor 和 Row 类。每个类都可以进一步定制为子类;但这是一个我们在本书中不再详细讨论的高级主题。Cursor 类是标准的 DBAPI 游标类,除了一个额外的便利方法 executescript,接受一个参数,即由多条语句以 ; 分隔的字符串(无参数)。其他两个类将在接下来的章节中介绍。

sqlite3.Connection 类

除了所有符合 DBAPI 标准模块 Connection 类通用的方法外,详情请参见 “Connection Objects”,sqlite3.Connection 还提供了 Table 12-13 中的方法和属性。

Table 12-13. sqlite3.Connection 类的附加方法和属性

crea⁠t⁠e⁠_​a⁠g⁠g⁠r⁠e⁠g⁠a⁠t⁠e create_aggregate(name, num_params, aggregate_class) aggregate_class 必须是一个类,提供两个实例方法:step,接受确切 num_param 个参数;finalize,不接受参数,并返回聚合的最终结果,即 sqlite3 原生支持的类型的值。这个聚合函数可以通过给定的 name 在 SQL 语句中使用。
c⁠r⁠e⁠a⁠t⁠e⁠_​c⁠o⁠l⁠l⁠a⁠t⁠i⁠o⁠n crea⁠t⁠e⁠_​c⁠o⁠l⁠l⁠a⁠t⁠i⁠o⁠n(name, callable) callable 必须接受两个字节字符串参数(编码为 'utf-8'),如果第一个参数应被视为“小于”第二个参数,则返回 -1;如果应被视为“大于”,则返回 1;如果应被视为“等于”,则返回 0。此类排序规则可以在 SQL SELECT 语句的 ORDER BY 子句中以 name 命名使用。
crea⁠t⁠e⁠_​f⁠u⁠n⁠c⁠t⁠i⁠o⁠n create_function(name, num_params, func) func 必须接受确切 num_params 个参数,并返回 sqlite3 原生支持的类型的值;这样的用户定义函数可以通过给定的 name 在 SQL 语句中使用。
interrupt interrupt() 可以从任何其他线程调用,以中止在此连接上执行的所有查询(在使用连接的线程中引发异常)。
isolati⁠o⁠n⁠_​l⁠e⁠v⁠e⁠l 一个只读属性,其值为连接函数的 isolation_level 参数提供的值。
iterdump iterdump() 返回一个迭代器,生成字符串:构建当前数据库的 SQL 语句,包括模式和内容。例如,可用于将内存中的数据库持久化到磁盘以供将来重用。
row_factory 一个可调用对象,接受游标和原始行作为元组,并返回用作真实结果行的对象。一个常见的惯用法是 x.row_factory=sqlite3.Row,使用在下一节详细介绍的高度优化的 Row 类,提供基于索引和不区分大小写的名称访问列,几乎没有额外开销。
text_factory 一个接受单一字节字符串参数并返回用于 TEXT 列值的对象的可调用函数——默认为 str,但你可以设置为任何类似的可调用函数。
total_changes 自连接创建以来已修改、插入或删除的总行数。

连接对象还可以作为上下文管理器使用,以自动提交数据库更新或在发生异常时回滚;然而,在这种情况下,你需要显式调用 Connection.close() 来关闭连接。

sqlite3.Row 类

sqlite3 还提供了 Row 类。Row 对象大部分类似于元组,但还提供了 keys 方法,返回列名列表,并支持通过列名而不是列编号进行索引。

一个 sqlite3 示例

下面的示例处理与本章早些时候展示的示例相同的任务,但使用 sqlite3 进行持久化,而不是在内存中创建索引:

`import` fileinput, sqlite3
connect = sqlite3.connect('database.db')
cursor = connect.cursor()
`with` connect:
    cursor.execute('CREATE TABLE IF NOT EXISTS Words '
                   '(Word TEXT, File TEXT, Line INT)')
 `for` line `in` fileinput.input():
        f, l = fileinput.filename(), fileinput.filelineno()
        cursor.executemany('INSERT INTO Words VALUES (:w, :f, :l)',
            [{'w':w, 'f':f, 'l':l} `for` w `in` line.split()])
connect.close()

然后我们可以使用 sqlite3 读取存储在 DB 文件 database.db 中的数据,如下例所示:

`import` sys, sqlite3, linecache
connect = sqlite3.connect('database.db')
cursor = connect.cursor()
`for` word `in` sys.argv[1:]:
    cursor.execute('SELECT File, Line FROM Words '
                   'WHERE Word=?', [word])
    places = cursor.fetchall()
    `if` `not` places:
         print(f'Word {word!r} not found in index file',
               file=sys.stderr)
 `continue`
    `for` fname, lineno `in` places:
        print(f'Word {word!r} occurs in line {lineno}'
              f' of file {fname!r}:')
        print(linecache.getline(fname, lineno), end='')
connect.close()

¹ 实际上,“CSV” 有点名不副实,因为一些方言使用制表符或其他字符作为字段分隔符,而不是逗号。更容易将它们视为“分隔符分隔的值”。

² 如果你需要在此及其他方面扩展 pickle,请考虑第三方包 dill

³ dbm 的键和值必须是字节;shelve 将接受字节或 str,并且会透明地对字符串进行编码。

第十三章:时间操作

Python 程序可以以多种方式处理时间。时间间隔是以秒为单位的浮点数(时间间隔的小数部分是间隔的小数部分):所有标准库函数接受以秒为单位表示时间间隔的参数,接受浮点数作为该参数的值。时间中的瞬间是自某个参考瞬间以来的秒数,称为* epoch *。(尽管每种语言和每个平台的 epoch 有所不同,但在所有平台上,Python 的 epoch 是 UTC 时间,1970 年 1 月 1 日午夜。)时间瞬间通常还需要以多种单位(例如年、月、日、小时、分钟和秒)的混合形式表示,特别是用于输入输出目的。当然,输入输出还需要能够将时间和日期格式化为人类可读的字符串,并从字符串格式解析它们回来。

时间模块

时间模块在某种程度上依赖于底层系统的 C 库,这限定了时间模块可以处理的日期范围。在旧的 Unix 系统中,1970 年和 2038 年是典型的截止点¹(这个限制可以通过使用 datetime 避免,后文将讨论)。时间点通常以 UTC(协调世界时,曾称为 GMT 或格林尼治平均时间)指定。时间模块还支持本地时区和夏令时(DST),但仅限于底层 C 系统库支持的范围²。

作为秒数自纪元以来的一个替代方法,时间点可以用一个包含九个整数的元组表示,称为* timetuple *(在 Table 13-1 中有介绍)。所有项目都是整数:timetuples 不跟踪秒的小数部分。一个 timetuple 是 struct_time 的一个实例。你可以将其用作元组;更有用的是,你可以通过只读属性访问项目,如 x.tm_year,x.tm_mon 等等,属性名称在 Table 13-1 中列出。在任何需要 timetuple 参数的函数中,你都可以传递 struct_time 的实例或任何其他项目是九个整数且范围正确的序列(表中的所有范围都包括下限和上限,都是包容的)。

表 13-1 元组形式的时间表示

项目 含义 字段名 范围 备注
0 Year tm_year 1970–2038 有些平台支持 0001–9999
1 Month tm_mon 1–12 1 代表一月;12 代表十二月
2 Day tm_mday 1–31
3 小时 tm_hour 0–23 0 表示午夜;12 表示中午
4 分钟 tm_min 0–59
5 tm_sec 0–61 60 和 61 表示闰秒
6 星期几 tm_wday 0–6 0 表示星期一;6 表示星期日
7 年内天数 tm_yday 1–366 年内的日期编号
8 夏令时标志 tm_isdst −1–1 −1 表示库确定夏令时

要将“自纪元以来的秒数”浮点值转换为时间元组,请将浮点值传递给函数(例如 localtime),该函数返回所有九个有效项目的时间元组。在反向转换时,mktime 忽略元组的多余项目 6(tm_wday)和 7(tm_yday)。在这种情况下,通常将项目 8(tm_isdst)设置为 −1,以便 mktime 自行确定是否应用 DST。

time 提供了 表 13-2 中列出的函数和属性。

表 13-2. time 模块的函数和属性

asctime asctime([tupletime]) 接受时间元组并返回可读的 24 字符串,例如 'Sun Jan 8 14:41:06 2017'。调用 asctime() 无参数相当于调用 asctime(time.localtime())(格式化当前本地时间)。
ctime ctime([secs]) 类似于 asctime(localtime(secs)),接受以自纪元以来的秒数表示的瞬时,并返回该瞬时的可读的 24 字符串形式,以本地时间显示。调用 ctime() 无参数相当于调用 asctime()(格式化当前本地时间)。
gmtime gmtime([secs]) 接受以自纪元以来的秒数表示的瞬时,并返回 UTC 时间的时间元组 tt.tm_isdst 总是 0)。调用 gmtime() 无参数相当于调用 gmtime(time())(返回当前时间瞬时的时间元组)。
localtime localtime([secs]) 接受自纪元以来经过的秒数的瞬时,并返回本地时间的时间元组 tt.tm_isdst 根据本地规则应用于瞬时 secs 是 0 或 1)。调用 localtime() 无参数相当于调用 localtime(time())(返回当前时间瞬时的时间元组)。
mktime mktime(tupletime) 接受作为本地时间的时间元组表示的瞬时,并返回以自纪元以来的秒数表示的浮点值(即使在 64 位系统上,只接受 1970–2038 之间的有限纪元日期,而不是扩展范围)^(a)。tupletime 中的最后一项 DST 标志具有意义:将其设置为 0 以获取标准时间,设置为 1 以获取 DST,或设置为 −1 让 mktime 计算给定瞬时时是否适用 DST。
monotonic monotonic() 类似于 time(),返回当前时间瞬时的浮点数秒数;然而,保证时间值在调用之间不会后退,即使系统时钟调整(例如由于闰秒或在切换到或从 DST 时刻)。
perf_counter perf_counter() 用于测量连续调用之间的经过时间(如秒表),perf_counter 返回使用最高分辨率时钟得到的秒数值,以获取短时间内的精确度。它是系统范围的,并且在休眠期间也包括经过的时间。只使用连续调用之间的差异,因为没有定义的参考点。
process_time process_time() 像 perf_counter 一样;但是,返回的时间值是进程范围的,并且包括在休眠期间经过的时间。仅使用连续调用之间的差异,因为没有定义的参考点。
sleep sleep(secs) 暂停调用线程* secs 秒。如果是主线程并且某些信号唤醒了它,则在 secs 秒(当它是唯一准备运行的当前线程时)之前或更长时间的暂停之后,调用线程可能会再次开始执行(取决于进程和线程的系统调度)。您可以将 secs *设置为 0 调用 sleep,以便为其他线程提供运行机会,如果当前线程是唯一准备运行的线程,则不会造成显著延迟。

| strftime | strftime(fmt[, tupletime]) 接受表示本地时间的时间元组* tupletime ,并返回字符串,该字符串表示按 fmt 指定的即时时间。如果省略 tupletime ,strftime 使用本地时间(time())(格式化当前即时时间)。 fmt 的语法类似于“使用%进行传统字符串格式化”,尽管转换字符不同,如表 13-3 所示。参考 tupletime *指定的时间即时;格式无法指定宽度和精度。

例如,你可以使用 asctime 格式(例如,'Tue Dec 10 18:07:14 2002')获取日期,格式字符串为'%a %b %d %H:%M:%S %Y'。

您可以使用格式字符串'%a, %d %b %Y %H:%M:%S %Z'获取与 RFC 822 兼容的日期(例如'Tue, 10 Dec 2002 18:07:14 EST')。

这些字符串也可用于使用“用户编码类的格式化”中讨论的机制进行日期时间格式化,允许您等效地编写,对于 datetime.datetime 对象 d,可以写为 f'{d:%Y/%m/%d}'或'{:%Y/%m/%d}'.format(d),两者都会给出例如'2022/04/17'的结果。对于 ISO 8601 格式的日期时间,请参阅“日期类”中涵盖的 isoformat()和 fromisoformat()方法。|

| strptime | strptime(str, fmt) 根据格式字符串fmt(例如'%a %b %d %H:%M:%S %Y',详见 strftime 讨论)解析str,并返回时间元组作为即时。如果未提供时间值,默认为午夜。如果未提供日期值,默认为 1900 年 1 月 1 日。例如:

>>> print(time.strptime("Sep 20, 2022", '%b %d, %Y'))
time.struct_time(tm_year=2022, tm_mon=9, tm_mday=20, 
tm_hour=0, tm_min=0, tm_sec=0, tm_wday=1, 
tm_yday=263, tm_isdst=-1)

|

time time() 返回当前时间即时,一个从纪元以来的浮点数秒数。在一些(主要是较旧的)平台上,此时间的精度低至一秒。如果系统时钟在调用之间向后调整(例如由于闰秒),则可能在后续调用中返回较低的值。
timezone 本地时区(无夏令时)与 UTC 的偏移量(<0 为美洲;>=0 为大部分欧洲、亚洲和非洲)。
tzname 本地时间区域依赖的一对字符串,即无夏令时和有夏令时的本地时区名称。
^(a) mktime 的结果小数部分总是 0,因为其 timetuple 参数不考虑秒的小数部分。

表 13-3. strftime 的转换字符

类型字符 含义 特殊说明
a 星期几名称(缩写) 取决于区域设置
A 星期几名称(完整) 取决于区域设置
b 月份名称(缩写) 取决于区域设置
B 月份名称(完整) 取决于区域设置
c 完整的日期和时间表示 取决于区域设置
d 月份中的第几天 从 1 到 31
f 微秒数以小数形式,零填充到六位数 一到六位数字
G ISO 8601:2000 标准的基于周的年份编号
H 小时数(24 小时制钟) 从 0 到 23
I 小时数(12 小时制钟) 从 1 到 12
j 年份中的第几天 从 1 到 366
m 月份编号 从 1 到 12
M 分钟数 从 0 到 59
p 上午或下午的等价项 取决于区域设置
S 秒数 从 0 到 61
u 星期几 星期一为 1,最多为 7
U 周数(以星期天为第一天) 从 0 到 53
V ISO 8601:2000 标准的基于周的周数
w 星期几编号 0 表示星期天,最大为 6
W 周数(以星期一为第一天) 从 0 到 53
x 完整的日期表示 取决于区域设置
X 完整的时间表示 取决于区域设置
y 世纪内的年份编号 从 0 到 99
Y 年份编号 从 1970 到 2038,或更宽
z UTC 偏移量作为字符串:±HHMM[SS[.ffffff]]
Z 时区名称 如果不存在时区则为空
% 字面上的 % 字符 编码为 %%

datetime 模块

datetime 提供了用于建模日期和时间对象的类,这些对象可以是有意识的时区或无意识的(默认)。类 tzinfo 的实例用于建模时区,是抽象的:datetime 模块仅提供一个简单的实现 datetime.timezone(更多详细信息,请参阅在线文档)。在下一节中讨论的 zoneinfo 模块提供了 tzinfo 的更丰富的具体实现,它允许您轻松创建时区感知的 datetime 对象。datetime 中的所有类型都有不可变的实例:属性是只读的,实例可以是字典中的键或集合中的项,所有函数和方法都返回新对象,从不改变作为参数传递的对象。

date 类

date 类的实例表示一个日期(特定日期内的无特定时间),满足 date.min ⇐ d ⇐ date.max,始终是无意识的,并假设格里高利历始终有效。date 实例具有三个只读整数属性:yearmonthday。此类的构造函数签名如下:

date class date(year, month, day) 返回给定 yearmonthday 参数的日期对象,有效范围为 1 ⇐ year ⇐ 9999,1 ⇐ month ⇐ 12,以及 1 ⇐ dayn,其中 n 是给定月份和年份的天数。如果给出无效值,则引发 ValueError。

日期类还提供了作为替代构造函数可用的三个类方法,列在 表 13-4 中。

表 13-4. 替代日期构造函数

fromordinal date.fromordinal(ordinal) 返回一个日期对象,对应于普通格里高利纪元中的 ordinal,其中值为 1 对应于公元 1 年的第一天。
fromtimestamp date.fromtimestamp(timestamp) 返回一个日期对象,对应于自纪元以来以秒表示的 timestamp
today date.today() 返回表示今天日期的日期对象。

日期类的实例支持一些算术操作。日期实例之间的差异是一个 timedelta 实例;您可以将 timedelta 添加到日期实例或从日期实例中减去 timedelta 以创建另一个日期实例。您也可以比较日期类的任意两个实例(后面的日期较大)。

类 date 的实例 d 提供了列表中列出的方法,详见 表 13-5。

表 13-5. 类 date 的实例 d 的方法

ctime d.ctime() 返回一个字符串,表示日期 d 的格式与 time.ctime 中的 24 字符格式相同(日期设置为 00:00:00,午夜)。
isocalendar d.isocalendar() 返回一个包含三个整数的元组(ISO 年份、ISO 周数和 ISO 工作日)。更多关于 ISO(国际标准化组织)日历的详细信息,请参见 ISO 8601 标准
isoformat d.isoformat() 返回一个字符串,表示日期 d 的格式为 'YYYY-MM-DD';与 str(d) 相同。
isoweekday d.isoweekday() 返回日期 d 的星期几作为整数,星期一为 1,星期日为 7;类似于 d.weekday() + 1。

| replace | d.replace(year=None, month=None, day=None) 返回一个新的日期对象,类似于 d,但显式指定为参数的那些属性被替换。例如:

date(x,y,z).replace(month=m) == date(x,m,z)

|

| strftime | d.strftime(fmt) 返回一个字符串,表示日期 d 按字符串 fmt 指定的格式。例如:

time.strftime(*`fmt`*, *`d`*.timetuple())

|

timetuple d.timetuple() 返回一个 timetuple,对应于日期 d 的时间为 00:00:00(午夜)。

| toordinal | d.toordinal() 返回日期 d 的普通格里高利日期。例如:

date(1,1,1).toordinal() == 1

|

weekday d.weekday() 返回日期 d 的星期几作为整数,星期一为 0,星期日为 6;类似于 d.isoweekday() - 1。

时间类

时间类的实例表示一天中的时间(没有特定的日期),可以是关于时区的明确或无意识的,并且总是忽略闰秒。它们有五个属性:四个只读整数(小时、分钟、秒和微秒)和一个可选的只读 tzinfo 属性(无意识实例的情况下为 None)。时间类的构造函数的签名为:

time class time(hour=0, minute=0, second=0, microsecond=0, tzinfo=None) 类时间的实例不支持算术运算。可以比较两个时间实例(时间较晚的为较大),但仅当它们都是明确或都是无意识的时才行。

类时间的实例 t 提供了 表 13-6 中列出的方法。

表 13-6. 类时间实例 t 的方法

isoformat t.isoformat() 返回一个表示时间 t 的字符串,格式为 'HH:MM:SS';与 str(t) 相同。如果 t.microsecond != 0,则结果字符串较长:'HH:MM:SS.mmmmmm'。如果 t 是明确的,则在末尾添加六个字符 '+HH:MM',表示时区与 UTC 的偏移量。换句话说,此格式化操作遵循 ISO 8601 标准

| replace | t.replace(hour=None, minute=None, second=None, microsecond=None[, tzinfo]) 返回一个新的时间对象,类似于 t,除了那些显式指定为参数的属性将被替换。例如:

time(x,y,z).replace(minute=m) == time(x,m,z)

|

strftime t.strftime(fmt) 返回一个字符串,表示按照字符串 fmt 指定的时间 t

类时间的实例 t 还提供了方法 dst、tzname 和 utcoffset,它们不接受参数并委托给 t.tzinfo,当 t.tzinfo 为 None 时返回 None

日期时间类

日期时间类的实例表示一个瞬间(一个日期,在该日期内具体的时间),可以是关于时区的明确或无意识的,并且总是忽略闰秒。日期时间扩展了日期并添加了时间的属性;它的实例有只读整数属性年、月、日、小时、分钟、秒和微秒,以及一个可选的 tzinfo 属性(无意识实例的情况下为 None)。此外,日期时间实例有一个只读的 fold 属性,用于在时钟回滚期间区分模糊的时间戳(例如夏令时结束时的“回退”,在凌晨 1 点到 2 点之间创建重复的无意识时间)。fold 取值为 0 或 1,0 对应于回滚前的时间;1 对应于回滚后的时间。

日期时间实例支持一些算术运算:两个日期时间实例之间的差异(均为明确或均为无意识)是一个 timedelta 实例,并且可以将 timedelta 实例添加到或从日期时间实例中减去以构造另一个日期时间实例。可以比较两个日期时间类的实例(较晚的为较大),只要它们都是明确或都是无意识的。此类的构造函数的签名为:

datetime class datetime(year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold=0) 返回一个日期时间对象,遵循与日期类构造函数类似的约束。fold 是一个整数,值为 0 或 1,如前所述。

datetime 还提供一些类方法,可用作替代构造函数,详见 Table 13-7。

Table 13-7. 替代日期时间构造函数

| combine | datetime.combine(date, time) 返回一个日期时间对象,日期属性来自 date,时间属性(包括时区信息)来自 time。datetime.combine(d, t) 的作用类似于: |

datetime(d.year, d.month, d.day,
         t.hour, t.minute, t.second,
         t.microsecond, t.tzinfo)

|

fromordinal datetime.fromordinal(ordinal) 返回一个日期对象,表示普通格里高利历的序数日期 ordinal,其中值为 1 表示公元 1 年的第一天午夜。
fromtimestamp datetime.fromtimestamp(timestamp, tz=None) 返回一个日期时间对象,表示自纪元以来经过的秒数 timestamp 对应的时刻,以本地时间表示。当 tz 不是 None 时,返回一个带有给定 tzinfo 实例 tz 的带时区信息的日期时间对象。
now datetime.now(tz=None) 返回一个表示当前本地日期和时间的无时区信息的日期时间对象。当 tz 不是 None 时,返回一个带有给定 tzinfo 实例 tz 的带时区信息的日期时间对象。
strptime datetime.strptime(str, fmt) 返回一个日期时间对象,表示按照字符串 fmt 指定的格式解析的 str。当 fmt 中包含 %z 时,生成的日期时间对象是带时区信息的。
today datetime.today() 返回一个表示当前本地日期和时间的无时区信息的日期时间对象;与 now 类方法相同,但不接受可选参数 tz
utcfromtimestamp datetime.utcfromtimestamp(timestamp) 返回一个表示自纪元以来经过的秒数 timestamp 对应时刻的无时区信息的日期时间对象,使用的是协调世界时(UTC)。
utcnow datetime.utcnow() 返回一个表示当前日期和时间的无时区信息的日期时间对象,使用的是协调世界时(UTC)。

日期时间实例 d 还提供了 Table 13-8 中列出的方法。

Table 13-8. datetime 实例 d 的方法

astimezone d.astimezone(tz) 返回一个新的带时区信息的日期时间对象,类似于 d,但日期和时间与时区 tz 一起转换。^(a) d 必须是带时区信息的,以避免潜在的 bug。传递一个无时区信息的 d 可能导致意外结果。
ctime d.ctime() 返回一个字符串,表示与 d 的日期时间在与 time.ctime 相同的 24 字符格式中。
date d.date() 返回一个表示与 d 相同日期的日期对象。
isocalendar d.isocalendar() 返回一个包含三个整数的元组(ISO 年份、ISO 周号和 ISO 工作日),表示 d 的日期。
isoformat d.isoformat(sep='T') 返回一个字符串,表示d的格式为'YYYY-MM-DDxHH:MM:SS',其中x是参数 sep 的值(必须是长度为 1 的字符串)。如果d.microsecond != 0,则在字符串的'SS'部分之后添加七个字符'.mmmmmm'。如果t是已知的,则在最后添加六个字符'+HH:MM',以表示时区与 UTC 的偏移量。换句话说,此格式化操作遵循 ISO 8601 标准。str(d)与d.isoformat(sep=' ')相同。
isoweekday d.isoweekday() 返回d日期的星期几,返回一个整数,星期一为 1,星期日为 7。

| replace | d.replace(year=None, month=None, day=None, hour=None, minute=None, second=None, microsecond=None, tzinfo=None,, fold=0) 返回一个新的 datetime 对象,类似于d*,但指定为参数的那些属性被替换(但不进行任何时区转换——如果要转换时间,请使用 astimezone)。您还可以使用 replace 从 naive 创建一个已知的 datetime 对象。例如:

*`# create datetime replacing just month with no`* 
*`# other changes (== datetime(x,m,z))`*
datetime(x,y,z).replace(month=m) 
*`# create aware datetime from naive datetime.now()`*
*`d`* = datetime.now().replace(tzinfo=ZoneInfo(
                    "US/Pacific"))

|

strftime d.strftime(fmt) 返回一个字符串,表示根据格式字符串fmt指定的格式显示的d
time d.time() 返回一个表示与d相同一天中的时间的 naive 时间对象。
timestamp d.timestamp() 返回自纪元以来的秒数的浮点数。假设 naive 实例处于本地时区。
timetuple d.timetuple() 返回与时刻d对应的时间元组。
timetz d.timetz() 返回一个时间对象,表示与d相同的一天中的时间,具有相同的时区信息。

| toordinal | d.toordinal() 返回d日期的公历序数。例如:

datetime(1, 1, 1).toordinal() == 1 |

utct⁠i⁠m⁠e​t⁠u⁠p⁠le d.utctimetuple() 返回一个时间元组,对应于时刻d,如果d是已知的,则规范化为 UTC。
weekday d.weekday() 返回d日期的星期几,返回一个整数,星期一为 0,星期日为 6。
^(a) 请注意d.astimezone(tz)与d.replace(tzinfo=tz)非常不同:replace 不进行时区转换,而只是复制了d的所有属性,但d.tzinfo 除外。

类 datetime 的实例d还提供了方法 dst、tzname 和 utcoffset,这些方法不接受参数,并委托给d.tzinfo,在d.tzinfo 为None(即d是 naive 时)时返回None

timedelta 类

timedelta 类的实例表示具有三个只读整数属性的时间间隔:days、seconds 和 microseconds。此类的构造函数的签名为:

| timedelta | timedelta(days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0) 将所有单位按照明显的因子转换(一周为 7 天,一小时为 3,600 秒等),并将一切标准化为三个整数属性,确保 0 ⇐ seconds < 24 * 60 * 60 且 0 ⇐ microseconds < 1000000。例如:

>>> print(repr(timedelta(minutes=0.5)))
datetime.timedelta(days=0, seconds=30)
>>> print(repr(timedelta(minutes=-0.5)))
datetime.timedelta(days=-1, seconds=86370)

timedelta 的实例支持算术运算:与 timedelta 类型的实例之间的 + 和 -;与整数之间的 *;与整数和 timedelta 实例之间的 /(地板除法、真除法、divmod、%)。它们还支持彼此之间的比较。 |

虽然可以使用此构造函数创建 timedelta 实例,但更常见的是通过两个日期、时间或 datetime 实例相减创建 timedelta,使得结果 timedelta 表示经过的时间段。timedelta 的实例 td 提供了一个方法 td.total_seconds(),返回表示 timedelta 实例的总秒数的浮点数。

tzinfo 抽象类

tzinfo 类定义了在表 13-9 中列出的抽象类方法,用于支持创建和使用带有时区意识的 datetime 和 time 对象。

表 13-9. tzinfo 类的方法

dst dst(dt) 返回给定 datetime 的夏令时偏移量,作为 timedelta 对象
tzname tzname(dt) 返回给定 datetime 的时区缩写
utcoffset utcoffset(dt) 返回给定 datetime 的与 UTC 的偏移量,作为 timedelta 对象

tzinfo 还定义了一个 fromutc 抽象实例方法,主要供 datetime.astimezone 方法内部使用。

timezone 类

timezone 类是 tzinfo 类的具体实现。您可以使用表示与 UTC 时间偏移量的 timedelta 构造一个 timezone 实例。timezone 提供一个类属性 utc,代表 UTC 时区(相当于 timezone(timedelta(0)))。

zoneinfo 模块

3.9+ zoneinfo 模块是 datetime 的 tzinfo 的具体实现,用于时间区域的表示。³ zoneinfo 默认使用系统的时区数据,以 tzdata 作为后备。(在 Windows 上,您可能需要 pip install tzdata;一旦安装完成,您不需要在程序中导入 tzdata—zoneinfo 会自动使用它。)

zoneinfo 提供一个类:ZoneInfo,它是 datetime.tzinfo 抽象类的具体实现。您可以在创建带有时区意识的 datetime 实例时将其赋给 tzinfo,或者在 datetime.replace 或 datetime.astimezone 方法中使用它。要构造 ZoneInfo,请使用定义的 IANA 时区名称之一,例如 "America/Los_Angeles" 或 "Asia/Tokyo"。您可以通过调用 zoneinfo.available_timezones() 获取这些时区名称的列表。更多有关每个时区的详细信息(例如与 UTC 的偏移和夏令时信息)可以在Wikipedia上找到。

这里有一些使用 ZoneInfo 的示例。我们将从获取加州当前本地日期和时间开始:

>>> `from` datetime `import` datetime
>>> `from` zoneinfo `import` ZoneInfo
>>> d=datetime.now(tz=ZoneInfo("America/Los_Angeles"))
>>> d
datetime.datetime(2021,10,21,16,32,23,96782,tzinfo=zoneinfo.ZoneInfo(key
='America/Los_Angeles'))

现在我们可以将时区更新为另一个时区,而不改变其他属性(即不将时间转换为新时区):

>>> dny=d.replace(tzinfo=ZoneInfo("America/New_York"))
>>> dny
datetime.datetime(2021,10,21,16,32,23,96782,tzinfo=zoneinfo.ZoneInfo(key
='America/New_York')) 

将 datetime 实例转换为 UTC:

>>> dutc=d.astimezone(tz=ZoneInfo("UTC"))
>>> dutc
datetime.datetime(2021,10,21,23,32,23,96782,tzinfo=zoneinfo.ZoneInfo(key
='UTC')) 

获取当前时间的明晰时间戳在 UTC 时区:

>>> daware=datetime.utcnow().replace(tzinfo=ZoneInfo("UTC"))
>>> daware
datetime.datetime(2021,10,*21,23*,32,23,96782,tzinfo=zoneinfo.ZoneInfo(key
='UTC'))

在不同时区显示 datetime 实例:

>>> dutc.astimezone(ZoneInfo("Asia/Katmandu")) *`# offset +5h 45m`*
datetime.datetime(2021,10,*22,5*,17,23,96782,tzinfo=zoneinfo.ZoneInfo(key
='Asia/Katmandu')) 

获取本地时区:

>>> tz_local=datetime.now().astimezone().tzinfo
>>> tz_local
datetime.timezone(datetime.timedelta(days=-1, seconds=61200), 'Pacific
Daylight Time')

将 UTC datetime 实例转换回本地时区:

>>> dt_loc=dutc.astimezone(tz_local)
>>> dt_loc
datetime.datetime(2021, 10, 21, 16, 32, 23, 96782, tzinfo=datetime.time
(datetime.timedelta(days=-1, seconds=61200), 'Pacific Daylight Time'))
>>> d==dt_local
True

并获取所有可用时区的排序列表:

>>> tz_list=zoneinfo.available_timezones()
>>> sorted(tz_list)[0],sorted(tz_list)[-1]
('Africa/Abidjan', 'Zulu')

始终在内部使用 UTC 时区

规避时区的陷阱和问题的最佳方法是始终在内部使用 UTC 时区,在输入时从其他时区转换,并仅在显示目的使用 datetime.astimezone。

即使您的应用程序仅在自己的位置运行,并且永远不打算使用其他时区的时间数据,也适用这个技巧。如果您的应用程序连续运行几天或几周,并且为您的系统配置的时区遵循夏令时,如果不在 UTC 内部工作,您将会遇到与时区相关的问题。

dateutil 模块

第三方包dateutil(您可以通过 pip install python-dateutil 安装)提供了许多操作日期的模块。表格 13-10 列出了它提供的主要模块,除了用于时区相关操作(现在最好使用 zoneinfo,在前一节中讨论)的模块。

表格 13-10. dateutil 模块

| easter | easter.easter(year) 返回给定 year 的复活节 datetime.date 对象。例如:

>>> `from` dateutil `import` easter
>>> print(easter.easter(2023))
2023-04-09

|

| parser | parser.parse(s) 返回由字符串 s 表示的 datetime.datetime 对象,具有非常宽松(或“模糊”)的解析规则。例如:

>>> `from` dateutil `import` parser
>>> print(parser.parse('Saturday, January 28,'
                       ' 2006, at 11:15pm'))
2006-01-28 23:15:00

|

relativedelta relativedelta.relativedelta(...) 提供了一种简便的方法,用于查找“下个星期一”、“去年”等。dateutil 的文档详细解释了定义 relativedelta 实例行为复杂性规则。
rrule rrule.rrule(freq, ...) 实现了RFC 2445(也称为 iCalendar RFC),完整呈现其超过 140 页的荣耀。rrule 允许您处理重复事件,提供了诸如 after、before、between 和 count 等方法。

详细信息请查看dateutil 模块的文档,了解其丰富功能。

sched 模块

sched 模块实现了事件调度程序,让您可以轻松处理在“真实”或“模拟”时间尺度上安排的事件。这个事件调度程序在单线程和多线程环境中使用都是安全的。sched 提供了一个调度程序类,它接受两个可选参数,timefunc 和 delayfunc。

| scheduler | class scheduler(timefunc=time.monotonic, delayfunc=time.sleep) 可选参数 timefunc 必须是可调用的,没有参数以获取当前时间时刻(以任何度量单位);例如,您可以传递 time.time。可选参数 delayfunc 是可调用的,具有一个参数(时间持续时间,与 timefunc 相同单位),以延迟当前线程的该时间。调度器在每个事件之后调用 delayfunc(0) 给其他线程一个机会;这与 time.sleep 兼容。通过接受函数作为参数,调度器可以让您使用适合应用程序需要的任何“模拟时间”或“伪时间”^(a)。

如果单调时间(即使系统时钟在调用之间向后调整也无法倒退的时间,例如由于闰秒导致)对您的应用程序至关重要,请为您的调度器使用默认的 time.monotonic。

^(a) 依赖注入设计模式 的一个很好的示例,用于与测试无关的目的。

调度器实例 s 提供了 表 13-11 中详细描述的方法。

表 13-11. 调度器实例 s 的方法

cancel s.cancel(event_token) 从 s 的队列中移除一个事件。event_token 必须是对 s.enter 或 s.enterabs 的先前调用的结果,并且事件尚未发生;否则,cancel 将引发 RuntimeError。
empty s.empty() 当 s 的队列当前为空时返回 True;否则返回 False

| enter | s.enter(delay, priority, func, argument=(), kwargs=) 类似于 enterabs,不同之处在于 delay 是相对时间(从当前时刻正向的正差),而 enterabs 的参数 when 是绝对时间(未来时刻)。要为 重复 执行安排事件,请使用一个小的包装函数;例如:

`def` enter_repeat(s, first_delay, period, priority,
        func, args):
    `def` repeating_wrapper():
        s.enter(period, priority,
                repeating_wrapper, ())
        func(*args)
    s.enter(first_delay, priority,
        repeating_wrapper, args)

|

enterabs s.enterabs(when, priority, func, argument=(), kwargs=) 在时间 when 安排一个未来事件(回调 func(args, kwargs))。when 使用 s 的时间函数使用的单位。如果为同一时间安排了几个事件,s 将按 priority 的增加顺序执行它们。enterabs 返回一个事件令牌 t,您可以稍后将其传递给 s.cancel 来取消此事件。
run s.run(blocking=True) 运行已安排的事件。如果 blocking 为Trues.run 会循环直到s.empty 返回True,使用s初始化时传递的 delayfunc 来等待每个已安排的事件。如果 blocking 为False,执行任何即将到期的事件,然后返回下一个事件的截止时间(如果有的话)。当回调函数func引发异常时,s会传播它,但s保持自己的状态,按顺序执行已安排的事件。如果回调函数func运行时间超过下一个已安排事件之前的可用时间,则s会落后但会继续按顺序执行已安排的事件,不会丢弃任何事件。如果不再对某个事件感兴趣,调用s.cancel 显式地丢弃事件。

日历模块

日历模块提供与日历相关的函数,包括打印给定月份或年份的文本日历的函数。默认情况下,calendar 将星期一作为一周的第一天,星期日作为最后一天。要更改此设置,请调用 calendar.setfirstweekday。calendar 处理模块时间范围内的年份,通常为 1970 到 2038(至少)。

日历模块提供了表 13-12 中列出的函数。

表 13-12. 日历模块的函数

calendar calendar(year, w=2, li=1, c=6) 返回一个多行字符串,其中包含year年的日历,以每个日期间隔 c 个空格分隔成三列。w 是每个日期的字符宽度;每行的长度为 21w+18+2c。li 是每周的行数。
firstweekday firstweekday() 返回每周起始的当前设置的工作日。默认情况下,当导入 calendar 时,这是 0(表示星期一)。
isleap isleap(year) 如果year是闰年则返回True;否则返回False
leapdays leapdays(y1, y2) 返回范围(y1, y2)内的闰年天数总计(注意,这意味着y2是不包括的)。
month month(year, month, w=2, li=1) 返回一个多行字符串,其中包含yearmonth月的日历,每周一行加上两个标题行。w 是每个日期的字符宽度;每行的长度为 7*w+6。li 是每周的行数。
mo⁠n⁠t⁠h​c⁠a⁠l⁠e⁠n⁠d⁠a⁠r monthcalendar(year, month) 返回一个整数列表的列表。每个子列表表示一周。年yearmonth之外的天数设为 0;该月内的天数设为它们的日期,从 1 开始。
monthrange monthrange(year, month) 返回两个整数。第一个整数是yearmonth月第一天的工作日代码;第二个整数是该月的天数。工作日代码为 0(星期一)到 6(星期日);月份编号为 1 到 12。
prcal prcal(year, w=2, li=1, c=6) 类似于 print(calendar.calendar(year, w, li, c))。
prmonth prmonth(year, month, w=2, li=1) 类似于 print(calendar.month(year, month, w, li))。
setfirstweekday setfirstweekday(weekday) 设置每周的第一天为星期代码 weekday。星期代码为从 0(星期一)到 6(星期日)。calendar 提供了 MONDAY、TUESDAY、WEDNESDAY、THURSDAY、FRIDAY、SATURDAY 和 SUNDAY 这些属性,它们的值为整数 0 到 6。在代码中表示工作日时(例如,calendar.FRIDAY 而不是 4),使用这些属性可以使您的代码更清晰和更易读。
timegm timegm(tupletime) 就像 time.mktime 一样:接受时间元组形式的时间点,并将该时间点作为距离纪元的浮点秒数返回。
weekday weekday(year, month, day) 返回给定日期的星期代码。星期代码为 0(星期一)到 6(星期日);月份编号为 1(一月)到 12(十二月)。

python -m calendar 提供了一个有用的命令行界面以访问该模块的功能:运行 python -m calendar -h 可以获取简短的帮助信息。

¹ 在旧的 Unix 系统中,1970-01-01 是纪元的开始,而 2038-01-19 是 32 位时间回到纪元的时间点。大多数现代系统现在使用 64 位时间,许多时间方法可以接受从 0001 到 9999 年的年份,但一些方法或旧系统(特别是嵌入式系统)可能仍然有限制。

² time 和 datetime 不考虑闰秒,因为它们的计划未来不可预知。

³ 在 3.9 之前,请使用第三方模块 pytz

第十四章:执行定制

Python 公开、支持并记录其许多内部机制。这可能帮助您在高级水平上理解 Python,并允许您将自己的代码连接到这些 Python 机制中,以某种程度上控制它们。例如,“Python 内置函数”介绍了 Python 安排内置函数可见的方式。本章还涵盖了一些其他高级 Python 技术,包括站点定制、终止函数、动态执行、处理内部类型和垃圾回收。我们将在第十五章中讨论使用多线程和进程控制执行的其他问题;第十七章涵盖了与测试、调试和性能分析相关的特定问题。

站点定制

Python 提供了一个特定的“钩子”来让每个站点在每次运行开始时定制 Python 行为的某些方面。Python 在主脚本之前加载标准模块 site。如果使用-S选项运行 Python,则不加载 site。-S允许更快的启动,但会为主脚本增加初始化任务。site 的主要任务是将 sys.path 放置在标准形式中(绝对路径,无重复项),包括根据环境变量、虚拟环境和在 sys.path 中找到的每个*.pth*文件的指示。

其次,如果启动的会话是交互式的,则 site 会添加几个方便的内置函数(例如 exit、copyright 等),并且如果启用了 readline,则配置 Tab 键的自动完成功能。

在任何正常的 Python 安装中,安装过程设置了一切以确保 site 的工作足以让 Python 程序和交互式会话“正常”运行,即与安装了该版本 Python 的任何其他系统上的运行方式相同。在特殊情况下,如果作为系统管理员(或在等效角色,例如已将 Python 安装在其主目录以供个人使用的用户)认为绝对需要进行一些定制,则在名为sitecustomize.py的新文件中执行此操作(在与site.py相同的目录中创建它)。

避免修改 site.py

我们强烈建议您不要修改执行基础定制的site.py文件。这样做可能会导致 Python 在您的系统上的行为与其他地方不同。无论如何,site.py文件每次更新 Python 安装时都会被覆盖,您的修改将会丢失。

sitecustomize.py存在的罕见情况下,它通常的作用是将更多字典添加到 sys.path 中——执行此任务的最佳方法是让sitecustomize.py import site,然后调用 site.addsitedir(path_to_a_dir)。

终止函数

atexit 模块允许你注册终止函数(即,在程序终止时按 LIFO 顺序调用的函数)。终止函数类似于由 try/finallywith 建立的清理处理程序。然而,终止函数是全局注册的,在整个程序结束时调用,而清理处理程序是在特定 try 子句或 with 语句结束时调用的。终止函数和清理处理程序在程序正常或异常终止时都会被调用,但不会在通过调用 os._exit 终止程序时调用(所以通常调用 sys.exit)。atexit 模块提供了一个名为 register 的函数,接受 funcargskwds 作为参数,并确保在程序终止时调用 funcargskwds)。

动态执行和 exec

Python 的内置函数 exec 可以在程序运行时执行你读取、生成或以其他方式获取的代码。exec 动态执行一个语句或一组语句。其语法如下:

exec(*`code`*, *`globals`*=`None`, *`locals`*=`None`, /)

code 可以是 str、bytes、bytearray 或 code 对象。globals 是一个字典,locals 可以是任何映射。

如果同时传递 globalslocals,它们分别是代码运行的全局和局部命名空间。如果只传递 globalsexec 将同时使用 globals 作为全局和局部命名空间。如果两者都不传递,code 将在当前作用域中运行。

永远不要在当前作用域中运行 exec

在当前作用域中运行 exec 是一个特别糟糕的主意:它可以绑定、重新绑定或解绑任何全局名称。为了保持控制,请只在特定、显式的字典中使用 exec,如果必须的话。

避免使用 exec

Python 中经常被问到的一个问题是“如何设置一个我刚刚读取或构建的变量的名称?”确实,对于一个 global 变量,exec 允许这样做,但是为此目的使用 exec 是一个非常糟糕的主意。例如,如果变量名是 varname,你可能会考虑使用:

exec(*`varname`* + ' = 23')

不要这样做。在当前作用域中这样的 exec 会使你失去命名空间的控制,导致极难找到的错误,并使你的程序难以理解。将你需要动态设置的“变量”保存为字典条目(例如,mydict)而不是实际变量。然后可以考虑使用:

exec(*`varname`*+'=23', *`mydict`*) *`# Still a bad idea`*

虽然这种方式不如前面的例子那么糟糕,但仍然不是一个好主意。将这些“变量”保存为字典条目意味着你不需要使用 exec 来设置它们!只需编写代码:

mydict[varname] = 23

这样,你的程序会更清晰、直接、优雅且更快。有一些情况下确实可以使用 exec,但这些情况非常罕见:最好是使用显式字典。

努力避免 exec

只有在确实不可或缺时才使用 exec,这种情况极为罕见。通常最好避免 exec,并选择更具体、更受控制的机制:exec 会削弱您对代码命名空间的控制,可能损害程序的性能,并使您面临许多难以发现的错误和巨大的安全风险。

表达式

exec 可以执行表达式,因为任何表达式也都是有效的语句(称为表达式语句)。但是,Python 会忽略表达式语句返回的值。要评估表达式并获取表达式的值,请使用内置函数 eval,如表格 8-2 中所述。(但请注意,exec 的几乎所有安全风险警告同样适用于 eval。)

编译和代码对象

要使一个代码对象用于 exec,调用内置函数 compile 并将最后一个参数设置为'exec'(如表格 8-2 中所述)。

一个代码对象c展示了许多有趣的只读属性,它们的名称都以'co_'开头,比如在表格 14-1 中列出的那些。

表格 14-1. 代码对象的只读属性

co_argcount c所代表的函数的参数个数(当c不是函数的代码对象而是直接由 compile 构建时为 0)
co_code 一个字节对象,包含c的字节码
co_consts c中使用的常量的元组
co_filename c编译生成的文件名(当c是这种方式构建时为 compile 的第二个参数的字符串)
c⁠o⁠_⁠f⁠i⁠r⁠s⁠t​l⁠i⁠n⁠e⁠n⁠o 源代码的初始行号(位于由 co_filename 命名的文件中),用于编译生成c,如果c是通过编译文件构建的话
co_name c所代表的函数的名称(当c不是函数的代码对象而是直接由 compile 构建时为'')
co_names c中使用的所有标识符的元组
co_varnames c中局部变量标识符的元组,以参数名称开头

这些属性大多仅用于调试目的,但有些可能有助于高级内省,正如本节后面所示的例子。

如果您从包含一个或多个语句的字符串开始,首先对字符串使用 compile,然后在生成的代码对象上调用 exec—这比直接将字符串传递给 exec 来编译和执行要好一些。 这种分离允许您单独检查语法错误和执行时错误。 您通常可以安排事务以便字符串只编译一次,而代码对象重复执行,这样可以加快速度。 eval 也可以从这种分离中受益。 此外,编译步骤本质上是安全的(如果在您不完全信任的代码上执行 exec 和 eval 非常危险),您可以在执行代码对象之前检查它,以减少风险(尽管风险永远不会完全为零)。

正如 表 14-1 中所述,代码对象具有只读属性 co_names,该属性是代码中使用的名称的元组。 例如,假设您希望用户输入仅包含文字常量和操作符的表达式—不包含函数调用或其他名称。 在评估表达式之前,您可以检查用户输入的字符串是否满足这些约束:

`def` safer_eval(s):
    code = compile(s, '<user-entered string>', 'eval')
    `if` code.co_names:
        `raise` ValueError(
            f'Names {code.co_names!r} not allowed in expression {s!r}')
    `return` eval(code)

函数 safer_eval 评估作为参数传递的表达式 s,仅当字符串是语法上有效的表达式(否则,compile 会引发 SyntaxError),且完全不包含任何名称(否则,safer_eval 明确引发 ValueError)。 (这类似于标准库函数 ast.literal_eval,详见 “标准输入”,但更为强大,因为它允许使用操作符。)

了解代码即将访问的名称有时可能有助于优化您需要传递给 exec 或 eval 作为命名空间的字典的准备工作。 由于您只需要为这些名称提供值,因此您可以通过不准备其他条目来节省工作。 例如,假设您的应用程序动态接受来自用户的代码,并且约定以 data_ 开头的变量名引用存储在子目录 data 中的文件,用户编写的代码不需要显式读取。 用户编写的代码反过来可能会计算并将结果留在以 result_ 开头的全局变量中,您的应用程序将这些结果作为文件写回 data 子目录。 由于这种约定,稍后您可以将数据移动到其他位置(例如,到数据库中的 BLOBs,而不是子目录中的文件),用户编写的代码不会受到影响。 这是您可能如何有效实现这些约定的方法:

`def` exec_with_data(user_code_string):
    user_code = compile(user_code_string, '<user code>', 'exec')
    datadict = {}
    `for` name `in` user_code.co_names:
        `if` name.startswith('data_'):
            `with` open(f'data/{name[5:]}', 'rb') `as` datafile:
                datadict[name] = datafile.read()
        `elif` name.startswith('result_'):
            `pass`  *`` # user code assigns to variables named `result_...` ``*
        `else`:
            `raise` ValueError(f'invalid variable name {name!r}')
    exec(user_code, datadict)
    `for` name `in` datadict:
        `if` name.startswith('result_'):
            `with` open(f'data/{name[7:]}', 'wb') `as` datafile:
                datafile.write(datadict[name])

永远不要执行或评估不受信任的代码。

早期版本的 Python 提供了旨在减轻使用 execeval 风险的工具,称为“受限执行”,但这些工具从未完全安全,无法抵御有能力的黑客的狡猾攻击。最近的 Python 版本已经放弃了这些工具,以避免为用户提供不合理的安全感。如果需要防范此类攻击,请利用操作系统提供的保护机制:在一个单独的进程中运行不受信任的代码,并尽可能限制其权限(研究操作系统提供的用于此目的的机制,如 chroot、setuid 和 jail;在 Windows 上,可以尝试第三方商业附加组件 WinJail,或者如果你是容器安全化专家,可以在一个单独且高度受限的虚拟机或容器中运行不受信任的代码)。为了防范服务拒绝攻击,主进程应监控单独的进程,并在资源消耗过多时终止后者。进程在 “运行其他程序” 中有详细描述。

execeval 在不受信任的代码中是不安全的。

在上一节中定义的 exec_with_data 函数对不受信任的代码根本不安全:如果将作为参数 user_code 传递给它的字符串来自于你不能完全信任的方式,那么它可能会造成无法估量的损害。不幸的是,这几乎适用于任何使用 execeval 的情况,除非你能对要执行或评估的代码设置极其严格和完全可检查的限制,就像 safer_eval 函数的情况一样。

内部类型

本节描述的一些内部 Python 对象使用起来很困难,事实上并不是大多数情况下你应该使用的。正确地使用这些对象并产生良好效果需要一些研究你的 Python 实现的 C 源码。这种黑魔法很少需要,除非用于构建通用开发工具和类似的高级任务。一旦你深入理解事物,Python 赋予你在必要时施加控制的能力。由于 Python 将许多种类的内部对象暴露给你的 Python 代码,即使需要理解 C 来阅读 Python 的源代码并理解正在发生的事情,你也可以通过在 Python 中编码来施加这种控制。

类型对象

名为type的内置类型充当可调用的工厂,返回类型对象。类型对象除了等价比较和表示为字符串外,不需要支持任何特殊操作。但是,大多数类型对象是可调用的,并在调用时返回该类型的新实例。特别地,内置类型如intfloatliststrtuplesetdict都是这样工作的;具体来说,当不带参数调用它们时,它们返回一个新的空实例,或者对于数字,返回等于 0 的实例。类型模块的属性是没有内置名称的内置类型。除了调用以生成实例外,类型对象之所以有用,还因为你可以从中继承,正如“类和实例”中所涵盖的那样。

代码对象类型

除了使用内置函数compile,你还可以通过函数或方法对象的__code__属性获取代码对象。(有关代码对象属性的讨论,请参见“编译和代码对象”。)代码对象本身不能被调用,但是你可以重新绑定函数对象的__code__属性,使用正确数量的参数将代码对象包装成可调用形式。例如:

`def` g(x): 
    print('g', x)
code_object = g.__code__
`def` f(x): 
 `pass`
f.__code__ = code_object
f(23)     *`# prints: g 23`*

没有参数的代码对象也可以与execeval一起使用。直接创建代码对象需要许多参数;请参阅 Stack Overflow 的非官方文档了解如何操作(但请记住,通常最好调用compile而不是直接创建)。

帧类型

模块sys中的函数_getframe从 Python 调用栈返回一个帧对象。帧对象具有属性,提供关于在帧中执行的代码和执行状态的信息。tracebackinspect模块帮助你访问和显示这些信息,特别是在处理异常时。第十七章提供了关于帧和回溯的更多信息,并涵盖了模块inspect,这是执行此类内省的最佳方式。如函数名_getframe中的前导下划线提示的那样,该函数是“有些私有”的;它仅供调试器等工具使用,这些工具不可避免地需要深入内省 Python 的内部。

垃圾回收

Python 的垃圾收集通常是透明且自动进行的,但您可以选择直接进行一些控制。一般原则是,Python 在对象 x 成为不可达时的某个时刻收集 x,即当没有从正在执行的函数实例的本地变量或从已加载模块的全局变量开始的引用链能够到达 x 时。通常,当没有任何引用指向 x 时,对象 x 变得不可达。此外,当一组对象彼此引用但没有全局或局部变量间接引用它们时,它们也可能是不可达的(这种情况称为相互引用循环)。

经典 Python 对每个对象 x 都保留一个称为引用计数的计数,记录了有多少引用指向 x。当 x 的引用计数降至 0 时,CPython 立即收集 x。模块 sys 的函数 getrefcount 接受任何对象并返回其引用计数(至少为 1,因为 getrefcount 本身对要检查的对象有一个引用)。其他版本的 Python(如 Jython 或 PyPy)依赖于由其运行的平台提供的其他垃圾收集机制(例如 JVM 或 LLVM)。因此,模块 gc 和 weakref 仅适用于 CPython。

当 Python 回收 x 并且没有对 x 的引用时,Python 会完成 x 的最终处理(即调用 x.del)并释放 x 占用的内存。如果 x 持有对其他对象的引用,Python 会移除这些引用,从而可能使其他对象因无法访问而可回收。

gc 模块

gc 模块公开了 Python 垃圾收集器的功能。gc 处理了属于相互引用循环的不可达对象。如前所述,在这样的循环中,循环中的每个对象都引用另一个或多个其他对象,保持所有对象的引用计数为正数,但没有外部引用指向这组相互引用的对象集。因此,整个组,也称为循环垃圾,是不可达的,因此可以进行垃圾收集。寻找这样的循环垃圾需要时间,这也是为什么 gc 模块存在的原因:帮助您控制程序是否以及何时花费这些时间。默认情况下,循环垃圾收集功能处于启用状态,并具有一些合理的默认参数;但是,通过导入 gc 模块并调用其函数,您可以选择禁用功能、更改其参数和/或详细了解这方面的情况。

gc 提供了属性和函数来帮助您管理和调整循环垃圾回收,包括 表 14-2 中列出的内容。这些函数可以帮助您追踪内存泄漏 —— 尽管 应该 不再有对它们的引用,但仍然没有被回收的对象 —— 通过帮助您发现确实持有对它们引用的其他对象来发现它们。请注意,gc 实现了计算机科学中称为 分代垃圾回收 的体系结构。

表 14-2. gc 函数和属性

callbacks 垃圾收集器将在收集之前和之后调用的回调函数列表。有关详细信息,请参阅 “仪器化垃圾回收”。
collect collect() 立即强制执行完整的循环垃圾回收运行。
disable disable() 暂停自动周期性循环垃圾回收。
enable enable() 重新启用先前使用 disable 暂停的周期性循环垃圾回收。
freeze freeze() 冻结 gc 跟踪的所有对象:将它们移动到“永久代”,即一组在所有未来收集中被忽略的对象。
garbage 不可达但不可收集的对象列表(仅读)。当循环垃圾回收环中的任何对象具有 del 特殊方法时,可能不存在明显安全的顺序来终结这些对象。
get_count get_count() 返回当前收集计数的元组,形式为 (count0, count1, count2)。
get_debug get_debug() 返回一个整数位串,表示使用 set_debug 设置的垃圾回收调试标志。
get_freeze_count get_freeze_count() 返回永久代中对象的数量。
get_objects get_objects(generation=None) 返回被收集器跟踪的对象列表。3.8+ 如果选择的 generation 参数不是 None,则仅列出所选代中的对象。
get_referents get_referents(*objs) 返回由参数的 C 级 tp_traverse 方法访问的对象列表,这些对象被参数中任何一个引用。
get_referrers get_referrers(*objs) 返回当前由循环垃圾回收器跟踪的所有容器对象列表,这些对象引用参数中的任意一个或多个对象。
get_stats get_stats() 返回三个字典的列表,每个字典代表一代,包含收集次数、收集的对象数和不可收集对象数。
get_threshold get_threshold() 返回当前收集阈值,以三个整数的元组形式返回。
isenabled isenabled() 当前循环垃圾回收启用时返回 True;否则返回 False
is_finalized is_finalized(obj) 3.9+ 当垃圾回收器已经完成对 obj 的终结时返回 True;否则返回 False
is_tracked is_tracked(obj) 当 obj 当前被垃圾收集器跟踪时返回 True;否则返回 False

| set_debug | set_debug(flags) 设置在垃圾收集期间的调试行为标志。 flags 是一个整数,被解释为通过按位 OR(位或运算符,|)零个或多个模块 gc 提供的常量来构建的位字符串。 每个位启用一个特定的调试功能:

DEBUG_COLLECTABLE

打印在垃圾收集期间发现的可收集对象的信息。

DEBUG_LEAK

结合 DEBUG_COLLECTABLE、DEBUG_UNCOLLECTABLE 和 DEBUG_SAVEALL 的行为。 这些通常是用于帮助诊断内存泄漏的最常见标志。

DEBUG_SAVEALL

将所有可收集对象保存到列表 gc.garbage 中(其中不可收集对象也始终保存)以帮助您诊断泄漏问题。

DEBUG_STATS

打印在垃圾收集期间收集的统计信息,以帮助您调整阈值。

DEBUG_UNCOLLECTABLE

打印在垃圾收集期间发现的不可收集对象的信息。

|

set_threshold set_threshold(thresh0[, thresh1[, thresh2]]) 设置控制循环垃圾收集周期运行频率的阈值。 thresh0 为 0 会禁用垃圾收集。 垃圾收集是一个高级的专题,Python 中使用的分代垃圾收集方法的详细内容(以及因此这些阈值的详细含义)超出了本书的范围; 有关详细信息,请参阅 在线文档
unfreeze unfreeze() 解冻永久代中的所有对象,将它们全部移回到最老的代中。

当您知道程序中没有循环垃圾环路,或者在某些关键时刻不能承受循环垃圾收集的延迟时,通过调用 gc.disable() 暂时停止自动垃圾收集。 您可以稍后通过调用 gc.enable() 再次启用收集。 您可以通过调用 gc.isenabled() 测试当前是否启用了自动收集,它返回 TrueFalse。 要控制收集时机,可以调用 gc.collect() 强制立即执行完整的循环收集运行。 要包装一些时间关键的代码:

`import` gc
gc_was_enabled = gc.isenabled()
`if` gc_was_enabled:
    gc.collect()
    gc.disable()
*`# insert some time-critical code here`*
`if` gc_was_enabled:
    gc.enable()

如果将其实现为上下文管理器,您可能会发现这更容易使用:

`import` gc
`import` contextlib

@contextlib.contextmanager
`def` gc_disabled():
    gc_was_enabled = gc.isenabled()
    `if` gc_was_enabled:
        gc.collect()
        gc.disable()
    `try`:
        `yield`
    `finally`:
        `if` gc_was_enabled:
            gc.enable()

`with` gc_disabled():
 *`# ...insert some time-critical code here...`*

该模块 gc 中的其他功能更为高级且很少使用,可以分为两个领域。 函数 get_threshold 和 set_threshold 以及调试标志 DEBUG_STATS 帮助您微调垃圾收集以优化程序的性能。 gc 的其余功能可以帮助您诊断程序中的内存泄漏。 虽然 gc 本身可以自动修复许多泄漏问题(只要避免在类中定义 del,因为存在 del 可以阻止循环垃圾收集),但是如果首先避免创建循环垃圾,程序运行速度将更快。

垃圾收集工具

gc.callbacks 是一个最初为空的列表,您可以向其中添加函数 f(phase, info),Python 将在垃圾回收时调用这些函数。当 Python 调用每个这样的函数时,phase 为 'start' 或 'stop',用于标记收集的开始或结束,info 是一个字典,包含由 CPython 使用的分代收集的信息。您可以向此列表添加函数,例如用于收集有关垃圾回收的统计信息。有关更多详细信息,请参阅文档

弱引用模块

谨慎的设计通常可以避免引用循环。然而,有时你需要对象彼此知道对方的存在,避免相互引用可能会扭曲和复杂化你的设计。例如,一个容器引用了其项目,但是对象知道容器持有它也常常很有用。结果就是引用循环:由于相互引用,容器和项目保持彼此存活,即使所有其他对象都忘记了它们。弱引用通过允许对象引用其他对象而不保持它们存活来解决了这个问题。

弱引用 是一个特殊对象 w,它引用某个其他对象 x 而不增加 x 的引用计数。当 x 的引用计数降至 0 时,Python 将终止并收集 x,然后通知 w x 的消亡。弱引用 w 现在可以消失或以受控方式标记为无效。在任何时候,给定的 w 要么引用创建 w 时的相同对象 x,要么完全不引用;弱引用永远不会被重新定位。并非所有类型的对象都支持成为弱引用 w 的目标 x,但是类、实例和函数支持。

弱引用模块公开了用于创建和管理弱引用的函数和类型,详见表格 14-3。

表格 14-3. 弱引用模块的函数和类

getweakrefcount getweakrefcount(x) 返回 len(getweakrefs(x))。
getweakrefs getweakrefs(x) 返回所有目标为 x 的弱引用和代理的列表。
proxy proxy(x[, f]) 返回类型为 ProxyType(当 x 是可调用对象时为 CallableProxyType)的弱代理 p,以 x 为目标。使用 p 就像使用 x 一样,但是当使用 p 时,x 被删除后,Python 将引发 ReferenceError。p 永远不可哈希(您不能将 p 用作字典键)。当 f 存在时,它必须是一个接受一个参数的可调用对象,并且是 p 的最终化回调(即在 x 不再从 p 可达时,Python 调用 f(p))。fx 不再从 p 可达后立即执行。
ref ref(x[, f]) 返回类型为 ReferenceType 的对象 x 作为目标的弱引用 ww 可以无参数地调用:调用 w() 在 x 仍然存活时返回 x;否则,调用 w() 返回 None。当 x 可散列时,w 是可散列的。您可以比较弱引用的相等性(==、!=),但不能进行顺序比较(<、>、<=、>=)。当它们的目标存活且相等时,或者 x 等于 y 时,两个弱引用 xy 是相等的。当存在 f 时,它必须是一个带有一个参数的可调用对象,并且是 w 的最终化回调(即,在 xw 不再可达之后,Python 调用 f(w))。fx 不再从 w 可达后立即执行。
弱键字典 class WeakKeyDictionary(adict=) 弱键字典 d 是一个弱引用其键的映射。当 d 中键 k 的引用计数为 0 时,项目 d[k] 消失。adict 用于初始化映射。
弱引用集 class WeakSet(elements=[]) 弱引用集 s 是一个弱引用其内容元素的集合,从元素初始化。当 s 中元素 e 的引用计数为 0 时,es 中消失。
弱值字典 class WeakValueDictionary(adict=) 弱值字典 d 是一个弱引用其值的映射。当 d 中值 v 的引用计数为 0 时,d 中所有 d[k] v 的项目消失。adict 用于初始化映射。

WeakKeyDictionary 允许您在一些可散列对象上非侵入式地关联附加数据,而无需更改这些对象。WeakValueDictionary 允许您非侵入式地记录对象之间的瞬时关联,并构建缓存。在每种情况下,使用弱映射而不是字典,以确保其他情况下可被垃圾回收的对象不会仅因在映射中使用而保持活动状态。类似地,WeakSet 在普通集合的位置提供了相同的弱包含功能。

典型示例是一个跟踪其实例但不仅仅是为了跟踪它们而使它们保持活动状态的类:

`import` weakref
`class` Tracking:
    _instances_dict = weakref.WeakValueDictionary()

    def __init__(self):
        Tracking._instances_dict[id(self)] = self

    @classmethod
    def instances(cls):
        `return` cls._instances_dict.values()

当跟踪实例是可散列的时候,可以使用一个实例的 WeakSet 类实现类似的类,或者使用一个 WeakKeyDictionary,其中实例作为键,None作为值。

第十五章:并发:线程和进程

进程是操作系统中彼此保护的运行程序的实例。想要通信的进程必须明确通过进程间通信(IPC)机制,以及/或通过文件(在第十一章讨论)、数据库(在第十二章讨论)、或网络接口(在第十八章讨论)来安排通信。进程之间使用文件和数据库等数据存储机制通信的一般方式是一个进程写入数据,另一个进程稍后读取该数据。本章介绍了处理进程的编程,包括 Python 标准库模块 subprocess 和 multiprocessing;模块 os 中与进程相关的部分,包括通过管道进行简单 IPC;一种称为内存映射文件的跨平台 IPC 机制,在模块 mmap 中可用;以及 3.8+及 multiprocessing.shared_memory 模块。

线程(最初称为“轻量级进程”)是与单个进程内的其他线程共享全局状态(内存)的控制流;所有线程看起来同时执行,尽管它们实际上可能在一个或多个处理器/核心上“轮流”执行。线程远非易于掌握,而多线程程序通常难以测试和调试;然而,如“线程、多进程还是异步编程?”所述,适当使用多线程时,与单线程编程相比性能可能会提高。本章介绍了 Python 提供的处理线程的各种功能,包括线程、队列和 concurrent.futures 模块。

在单个进程内共享控制的另一种机制是所谓的异步(或async)编程。当你阅读 Python 代码时,关键字asyncawait的存在表明它是异步的。这样的代码依赖于事件循环,它大致相当于进程内部使用的线程切换器。当事件循环是调度器时,每次执行异步函数变成一个任务,与多线程程序中的线程大致对应。

进程调度和线程切换都是抢占式的,这意味着调度器或切换器控制 CPU,并确定何时运行任何特定的代码。然而,异步编程是协作式的:每个任务一旦开始执行,可以在选择放弃控制之前运行多长时间(通常是因为正在等待完成某些其他异步任务,通常是面向 I/O 的任务)。

尽管异步编程提供了优化某些问题类别的灵活性,但这是许多程序员不熟悉的编程范例。由于其协作性质,不慎的异步编程可能导致死锁,而无限循环则可能使其他任务被剥夺处理器时间:弄清楚如何避免死锁为普通程序员增加了显著的认知负担。我们在本卷中不进一步讨论异步编程,包括模块asyncio,认为这是一个足够复杂的主题,值得单独撰写一本书进行探讨。¹

网络机制非常适合 IPC,并且在网络的不同节点上运行的进程之间,以及在同一节点上运行的进程之间同样有效。multiprocessing 模块提供了一些适合在网络上进行 IPC 的机制;第十八章涵盖了提供 IPC 基础的低级网络机制。其他更高级的分布式计算机制(如CORBADCOM/COM+EJBSOAPXML-RPC.NETgRPC等)可以使 IPC 变得更加容易,无论是本地还是远程;然而,本书不涵盖分布式计算。

当多处理器计算机出现时,操作系统必须处理更复杂的调度问题,而希望获得最大性能的程序员必须编写他们的应用程序,以便代码可以真正并行执行,即在不同的处理器或核心上(从编程角度看,核心只是在同一块硅片上实现的处理器)。这需要知识和纪律。CPython 实现通过实现全局解释器锁(GIL)简化了这些问题。在没有任何 Python 程序员的操作下,在 CPython 中只有持有 GIL 的线程被允许访问处理器,有效地阻止了 CPython 进程充分利用多处理器硬件。诸如NumPy这样的库通常需要进行长时间的计算,使用的是不使用解释器设施的编译代码,这些库在这些计算期间释放 GIL。这允许有效地使用多个处理器,但如果您的所有代码都是纯 Python,则不能使用这种技术。

Python 中的线程

Python 支持在支持线程的平台上进行多线程操作,如 Windows、Linux 和几乎所有 Unix 变体(包括 macOS)。当动作在开始和结束之间保证没有线程切换时,该动作被称为原子。在实践中,在 CPython 中,看起来是原子的操作(例如,简单赋值和访问)大多数情况下确实是原子的,但只适用于内置类型(但增强和多重赋值不是原子的)。尽管如此,依赖于这种“原子性”通常是一个好主意。您可能正在处理用户编写的类的实例,而不是内置类型的实例,在这种情况下,可能会有隐式调用 Python 代码的情况,这些调用会使原子性假设失效。此外,依赖于实现相关的原子性可能会将您的代码锁定到特定的实现中,从而阻碍未来的更改。建议您在本章的其余部分使用同步设施,而不是依赖于原子性假设。

多线程系统中的关键设计问题是如何最好地协调多个线程。线程模块在下一节中介绍,提供了几种同步对象。队列模块(在“队列模块”中讨论)对于线程同步也非常有用:它提供了同步的、线程安全的队列类型,方便线程间的通信和协调。concurrent 包(在“concurrent.futures 模块”中讨论)提供了一个统一的通信和协调接口,可以由线程池或进程池实现。

线程模块

线程模块提供了多线程功能。线程的方法是将锁和条件建模为单独的对象(例如,在 Java 中,此类功能是每个对象的一部分),并且线程不能直接从外部控制(因此没有优先级、组、销毁或停止)。线程模块提供的所有对象的方法都是原子的。

线程模块提供了以下专注于线程的类,我们将在本节中探讨所有这些类:Thread、Condition、Lock、RLock、Event、Semaphore、BoundedSemaphore、Timer 和 Barrier。

threading 还提供了许多有用的函数,包括在表 15-1 中列出的函数。

表 15-1. 线程模块的函数

active_count active_count() 返回一个整数,表示当前存活的线程对象数量(不包括已终止或尚未启动的线程)。
c⁠u⁠r⁠r⁠e⁠n⁠t⁠_​t⁠h⁠r⁠e⁠a⁠d current_thread() 返回调用线程的 Thread 对象。如果调用线程不是由 threading 创建的,则 current_thread 创建并返回一个具有有限功能的半虚拟 Thread 对象。
enumerate enumerate() 返回当前存活的所有 Thread 对象的列表(不包括已终止或尚未启动的线程)。
excepthook excepthook(args) 3.8+ 重写此函数以确定如何处理线程内的异常;有关详细信息,请参阅在线文档。args 参数具有属性,允许您访问异常和线程详细信息。3.10+ threading.excepthook 保存了模块的原始线程钩子值。
get_ident get_ident() 返回一个非零整数作为所有当前线程中唯一的标识符。用于管理和跟踪线程数据。线程标识符可能会在线程退出并创建新线程时重复使用。
get_native_id get_native_id() 3.8+ 返回由操作系统内核分配的当前线程的本机整数 ID。适用于大多数常见操作系统。
stack_size stack_size([size]) 返回用于新线程的当前堆栈大小(以字节为单位),并(如果提供 size)为新线程设定值。size 的可接受值受到平台特定约束的限制,例如至少为 32768(或某些平台上更高的最小值),并且(在某些平台上)必须是 4096 的倍数。传递值为 0 总是可接受的,表示“使用系统的默认值”。当您传递一个在当前平台上不可接受的 size 值时,stack_size 会引发 ValueError 异常。

Thread 对象

线程实例 t 模拟一个线程。在创建 t 时,您可以将一个函数作为 t 的主函数传递给 target 参数,或者您可以子类化 Thread 并重写其 run 方法(您还可以重写 init,但不应重写其他方法)。在创建时,t 还未准备好运行;要使 t 准备就绪(活动),请调用 t.start。一旦 t 处于活动状态,它将在其主函数正常结束或通过传播异常时终止。线程 t 可以是 daemon,这意味着即使 t 仍然活动,Python 也可以终止,而普通(非守护)线程则会一直保持 Python 运行,直到线程终止。Thread 类提供了在 Table 15-2 中详细描述的构造函数、属性和方法。

Table 15-2. Thread 类的构造函数、方法和属性

| Thread | class Thread(name=None, target=None, args=(), kwargs=, *, daemon=None)

始终使用命名参数调用 Thread:虽然参数的数量和顺序不受规范保证,但参数名是固定的。在构造 Thread 时有两个选项:

  • 使用目标函数(t.run 在线程启动时调用 target(*args, **kwargs))来实例化 Thread 类本身。

  • 扩展 Thread 类并重写其 run 方法。

在任何情况下,仅当调用 t.start 时执行将开始。name 成为 t 的名称。如果 name 是 None,Thread 为 t 生成一个唯一名称。如果 Thread 的子类 T 重写 initT.init 必须 在调用任何其他 Thread 方法之前调用 Thread.init(通常通过 super 内置函数)。daemon 可以分配布尔值,或者如果为 None,则将从创建线程的 daemon 属性获取此值。 |

daemon daemon 是一个可写的布尔属性,指示 t 是否为守护线程(即使 t 仍然活动,进程也可以终止;这样的终止也会结束 t)。只能在调用 t.start 之前对 t.daemon 赋值;将 true 值赋给 t 将其设置为守护线程。守护线程创建的线程的 t.daemon 默认为 True
is_alive t.is_alive() 当 t 处于活动状态时(即 t.start 已执行且 t.run 尚未终止),is_alive 返回 True;否则返回 False
join t.join(timeout=None) join 暂停调用线程(不能是 t)直到 t 终止(当 t 已经终止时,调用线程不会暂停)。timeout 在 “超时参数” 中讨论。只能在 t.start 之后调用 t.join。可以多次调用 join。
name t.name name 是返回 t 名称的属性;分配 name 重新绑定 t 的名称(名称仅用于调试帮助;名称在线程中无需唯一)。如果省略,则线程将接收生成的名称 Thread-n,其中 n 是递增的整数(3.10+ 并且如果指定了 target,则将附加 (target.name))。
run t.run() 是由 t.start 调用的方法,执行 t 的主函数。Thread 的子类可以重写 run 方法。如果未重写,则 run 方法调用 t 创建时传递的 target 可调用对象。不要直接调用 t.run;调用 t.run 的工作由 t.start 完成!
start t.start() 使 t 变为活动状态,并安排 t.run 在单独的线程中执行。对于任何给定的 Thread 对象 t,只能调用一次 t.start;再次调用会引发异常。

线程同步对象

threading 模块提供了几种同步原语(允许线程通信和协调的类型)。每种原语类型都有专门的用途,将在以下部分讨论。

也许你不需要线程同步原语

只要避免有(非队列)全局变量,这些变量会发生变化,多个线程可以访问,队列(在“队列模块”中介绍)通常可以提供所需的所有协调工作,同时并发(在“concurrent.futures 模块”中介绍)也可以。“线程化程序架构”展示了如何使用队列对象为您的多线程程序提供简单而有效的架构,通常无需显式使用同步原语。

超时参数

同步原语 Condition 和 Event 提供接受可选超时参数的 wait 方法。线程对象的 join 方法也接受可选的超时参数(参见表 15-2)。使用默认的超时值 None 会导致正常的阻塞行为(调用线程挂起并等待,直到满足所需条件)。当超时值不是 None 时,超时参数是一个浮点值,表示时间间隔,单位为秒(超时可以有小数部分,因此可以指示任何时间间隔,甚至非常短的间隔)。当超过超时秒数时,调用线程再次准备好,即使所需条件尚未满足;在这种情况下,等待方法返回 False(否则,方法返回 True)。超时让您设计能够处理少数线程偶发异常的系统,从而使系统更加健壮。但是,使用超时可能会降低程序的运行速度:在这种情况下,请务必准确测量您代码的速度。

锁和重入锁对象

锁和重入锁对象提供相同的三种方法,详见表 15-3。

表 15-3. 锁实例 L 的方法

| acquire | L.acquire(blocking=True, timeout=-1) 当 L 未锁定时,或者如果 L 是由同一个线程获取的重入锁,该线程立即锁定它(如果 L 是重入锁,则会增加内部计数器,如后面所述),并返回 True

L 已经被锁定且 blocking 为 False 时,acquire 立即返回 False。当 blocking 为 True 时,调用线程将被挂起,直到以下情况发生之一:

  • 另一个线程释放了锁,则该线程锁定它并返回 True

  • 在锁被获取之前操作超时,此时 acquire 返回 False。默认的 -1 值永不超时。

|

locked L.locked() 当 L 被锁定时返回 True;否则返回 False
release L.release() 解锁 L,必须已锁定(对于 RLock,这意味着减少锁计数,锁计数不得低于零——只有当锁计数为零时才能由新线程获取)。当 L 被锁定时,任何线程都可以调用 L.release,不仅仅是锁定 L 的线程。当多个线程被阻塞在 L 上时(即调用了 L.acquire,发现 L 被锁定,并等待 L 解锁),release 将唤醒其中任意一个等待的线程。调用 release 的线程不会挂起:它仍然准备好并继续执行。

下面的控制台会话示例说明了当锁被用作上下文管理器时(以及 Python 在锁的使用过程中维护的其他数据,例如所有者线程 ID 和锁的获取方法被调用的次数)自动获取/释放锁的情况:

>>> lock = threading.RLock()
>>> print(lock)
<unlocked _thread.RLock object owner=0 count=0 at 0x102878e00>
>>> `with` lock:
...     print(lock)
...
<locked _thread.RLock object owner=4335175040 count=1 at 0x102878e00>
>>> print(lock)
<unlocked _thread.RLock object owner=0 count=0 at 0x102878e00>

RLock 对象 r 的语义通常更为方便(除非在需要线程能够释放不同线程已获取的锁的特殊体系结构中)。 RLock 是一个可重入锁,意味着当 r 被锁定时,它会跟踪拥有它的线程(即锁定它的线程,对于 RLock 来说也是唯一能够释放它的线程——当任何其他线程尝试释放 RLock 时,会引发 RuntimeError 异常)。拥有它的线程可以再次调用 r.acquire 而不会阻塞;然后 r 只是增加一个内部计数。在涉及 Lock 对象的类似情况下,线程会阻塞直到某个其他线程释放该锁。例如,考虑以下代码片段:

lock = threading.RLock()
global_state = []
`def` recursive_function(some, args):
    `with` lock:  *`# acquires lock, guarantees release at end`*
        *`# ...modify global_state...`*
        `if` more_changes_needed(global_state):
            recursive_function(other, args)

如果锁是 threading.Lock 的一个实例,当 recursive_function 递归调用自身时,会阻塞其调用线程:with 语句会发现锁已经被获取(尽管是同一个线程获取的),然后会阻塞并等待……等待。而使用 threading.RLock 则不会出现这样的问题:在这种情况下,由于锁已经被同一线程获取,再次获取时只是增加其内部计数然后继续。

一个 RLock 对象 r 只有在释放次数与获取次数相同的情况下才会解锁。当对象的方法相互调用时,RLock 对象非常有用;每个方法在开始时可以获取,并在结束时释放同一个 RLock 实例。

使用 with 语句自动获取和释放同步对象

使用try/finally语句(在“try/finally”中介绍)是确保已获取的锁确实被释放的一种方法。使用with语句,通常更好,因为所有的锁、条件和信号量都是上下文管理器,所以这些类型的实例可以直接在with子句中使用,以获取它(隐式地带有阻塞)并确保在with块的末尾释放它。

条件对象

条件对象c封装了锁或者递归锁对象L。Condition 类公开了在表 15-4 中描述的构造函数和方法。

表 15-4. Condition 类的构造函数和方法

Condition class Condition(lock=None) 创建并返回一个新的 Condition 对象c,并使用锁L设置为 lock。如果 lock 为None,则L设置为新创建的 RLock 对象。
acquire, release c.acquire(blocking=True), c.release() 这些方法只是调用L的相应方法。线程除了持有(即已获取)锁L之外,绝不应调用c的任何其他方法。
notify, notify_all c.notify(), c.notify_all() notify 唤醒在c上等待的任意一个线程。在调用c.notify 之前,调用线程必须持有L,并且 notify 不会释放L。被唤醒的线程直到再次可以获取L时才变为就绪。因此,通常调用线程在调用 notify 后调用 release。notify_all 类似于 notify,但唤醒所有等待的线程,而不仅仅是一个。
wait c.wait(timeout=None) wait 释放L,然后挂起调用线程,直到其他线程在c上调用 notify 或 notify_all。在调用c.wait 之前,调用线程必须持有L。timeout 在“超时参数”中有描述。线程通过通知或超时唤醒后,当再次获取L时,线程变为就绪。当 wait 返回True(表示正常退出,而不是超时退出)时,调用线程总是再次持有L

通常,Condition 对象c调节一些在线程间共享的全局状态s的访问。当一个线程必须等待s改变时,线程循环:

`with` *`c`*:
    `while` `not` is_ok_state(s):
        *`c`*.wait()
    do_some_work_using_state(s)

同时,每个修改s的线程在每次s变化时调用 notify(或者如果需要唤醒所有等待的线程而不仅仅是一个,则调用 notify_all):

`with` *`c`*:
    do_something_that_modifies_state(*`s`*)
    *`c`*.notify()    *`# or, c.notify_all()`*
*`# no need to call c.release(), exiting 'with' intrinsically does that`*

您必须始终在每次使用c的方法周围获取和释放c:通过with语句这样做使得使用 Condition 实例更不容易出错。

事件对象

事件对象允许任意数量的线程挂起和等待。当任何其他线程调用 e.set 时,所有等待事件对象 e 的线程都变为就绪状态。e 有一个标志记录事件是否发生;在 e 创建时,该标志最初为 False。因此,事件类似于简化版的条件变量。事件对象适用于一次性的信号传递,但对于更一般的用途而言,可能不够灵活;特别是依赖于调用 e.clear 很容易出错。事件类公开了 表 15-5 中的构造函数和方法。

表 15-5. 事件类的构造函数和方法

Event class Event() 创建并返回一个新的事件对象 e,并将 e 的标志设置为 False
clear e.clear() 将 e 的标志设置为 False
is_set e.is_set() 返回 e 的标志值:TrueFalse
set e.set() 将 e 的标志设置为 True。所有等待 e 的线程(如果有)都将变为就绪状态。
wait e.wait(timeout=None) 如果 e 的标志为 True,则立即返回;否则,挂起调用线程,直到其他线程调用 set。timeout 参见 “超时参数”。

下面的代码显示了如何显式同步多个线程之间的处理过程使用事件对象:

`import` datetime, random, threading, time

`def` runner():
    print('starting')
    time.sleep(random.randint(1, 3))
    print('waiting')
    event.wait()
    print(f'running at {datetime.datetime.now()}')

num_threads = 10
event = threading.Event()

threads = [threading.Thread(target=runner) `for` _ `in` range(num_threads)]
`for` t `in` threads:
    t.start()

event.set()

`for` t `in` threads:
    t.join()

信号量和有界信号量对象

信号量(也称为计数信号量)是锁的一种泛化形式。锁的状态可以看作是 TrueFalse;信号量 s 的状态是一个在创建 s 时设置的介于 0 和某个 n 之间的数值(两个边界都包括)。信号量可以用来管理一组固定的资源,例如 4 台打印机或 20 个套接字,尽管对于这种目的,使用队列(本章后面描述)通常更为健壮。有界信号量类与此非常相似,但是如果状态超过初始值,则会引发 ValueError:在许多情况下,这种行为可以作为错误的有用指示器。 表 15-6 显示了信号量和有界信号量类的构造函数以及任何一类对象 s 所暴露的方法。

表 15-6. 信号量和有界信号量类的构造函数和方法

| Semaphore, Boun⁠d⁠e⁠d​S⁠e⁠m⁠aphore | class Semaphore(n=1), class BoundedSemaphore(n=1)

信号量使用指定状态 n 创建并返回一个信号量对象 s;有界信号量类似,但如果状态高于 n,则 s.release 会引发 ValueError。 |

acquire s.acquire(blocking=True) 当 s 的状态 >0 时,acquire 将状态减 1 并返回 True。当 s 的状态为 0 且 blocking 为 True 时,acquire 暂停调用线程并等待,直到其他线程调用 s.release。当 s 的状态为 0 且 blocking 为 False 时,acquire 立即返回 False
release s.release() 当 s 的状态大于 0 时,或者状态为 0 但没有线程在等待 s 时,release 将状态增加 1。当 s 的状态为 0 且有线程在等待 s 时,release 将保持 s 的状态为 0,并唤醒其中一个等待的线程。调用 release 的线程不会挂起;它保持就绪状态并继续正常执行。

Timer 对象

Timer 对象在给定延迟后,在新创建的线程中调用指定的可调用对象。Timer 类公开了构造函数和 Table 15-7 中的方法。

Table 15-7. Timer 类的构造函数和方法

Timer class Timer(interval, callback, args=None, kwargs=None) 创建一个对象 t,在启动后 interval 秒调用 callbackinterval 是一个浮点秒数)。
cancel t.cancel() 停止定时器并取消其动作的执行,只要在调用 cancel 时 t 仍在等待(尚未调用其回调)。
start t.start() 启动 t

Timer 继承自 Thread 并添加了属性 function、interval、args 和 kwargs。

Timer 是“一次性”的:t 仅调用其回调一次。要周期性地每隔 interval 秒调用 callback,这里是一个简单的方法——周期性定时器每隔 interval 秒运行 callback,只有在 callback 引发异常时才停止:

`class` Periodic(threading.Timer):
    `def` __init__(self, interval, callback, args=`None`, kwargs=`None`):
        super().__init__(interval, self._f, args, kwargs)
        self.callback = callback

    `def` _f(self, *args, **kwargs):
        p = type(self)(self.interval, self.callback, args, kwargs)
        p.start()
        `try`:
            self.callback(*args, **kwargs)
        `except` Exception:
            p.cancel()

Barrier 对象

Barrier 是一种同步原语,允许一定数量的线程等待,直到它们都达到执行中的某一点,然后它们全部恢复。具体来说,当线程调用 b.wait 时,它会阻塞,直到指定数量的线程在 b 上做出相同的调用;此时,所有在 b 上阻塞的线程都被允许恢复。

Barrier 类公开了构造函数、方法和 Table 15-8 中列出的属性。

Table 15-8. Barrier 类的构造函数、方法和属性

Barrier class Barrier(num_threads, action=None, timeout=None) 为 num_threads 个线程创建一个 Barrier 对象 b。action 是一个没有参数的可调用对象:如果传递了此参数,则它会在所有阻塞线程中的任何一个被解除阻塞时执行。timeout 在 “Timeout parameters” 中有说明。
abort b.abort() 将 Barrier b 置于 broken 状态,这意味着当前等待的任何线程都会恢复,并抛出 threading.BrokenBarrierException(在任何后续调用 b.wait 时也会引发相同的异常)。这是一种紧急操作,通常用于当等待线程遭遇异常终止时,以避免整个程序死锁。
broken b.broken 当 b 处于 broken 状态时为 True;否则为 False
n_waiting *b.*n_waiting 当前正在等待 b 的线程数。
parties parties 在 b 构造函数中作为 num_threads 传递的值。
重置 b.reset() 将b重置为初始的空、未损坏状态;但是,当前正在b上等待的任何线程都将恢复,带有 threading.BrokenBarrierException 异常。
等待 b.wait() 第一个b.parties-1 个调用b.wait 的线程会阻塞;当阻塞在b上的线程数为b.parties-1 时,再有一个线程调用b.wait,所有阻塞在b上的线程都会恢复。b.wait 向每个恢复的线程返回一个 int,所有返回值都是不同的且在 range(b.parties)范围内,顺序不确定;线程可以使用这个返回值来确定接下来应该做什么(尽管在 Barrier 的构造函数中传递 action 更简单且通常足够)。

下面的代码展示了 Barrier 对象如何在多个线程之间同步处理(与之前展示的 Event 对象的示例代码进行对比):

`import` datetime, random, threading, time

`def` runner():
    print('starting')
    time.sleep(random.randint(1, 3))
    print('waiting')
    `try`:
        my_number = barrier.wait()
    `except` threading.BrokenBarrierError:
        print('Barrier abort() or reset() called, thread exiting...')
        return
    print(f'running ({my_number}) at {datetime.datetime.now()}')

`def` announce_release():
    print('releasing')

num_threads = 10
barrier = threading.Barrier(num_threads, action=announce_release)

threads = [threading.Thread(target=runner) `for` _ `in` range(num_threads)]
`for` t `in` threads:
    t.start()

`for` t `in` threads:
    t.join()

线程本地存储

threading 模块提供了类 local,线程可以使用它来获取线程本地存储,也称为每个线程的数据。local 的一个实例L有任意命名的属性,你可以设置和获取,存储在字典L.dict 中,也可以直接访问。L是完全线程安全的,这意味着多个线程同时设置和获取L上的属性没有问题。每个访问L的线程看到的属性集合是独立的:在一个线程中进行的任何更改对其他线程没有影响。例如:

`import` threading

L = threading.local()
print('in main thread, setting zop to 42')
L.zop = 42

`def` targ():
  print('in subthread, setting zop to 23')
  L.zop = 23
  print('in subthread, zop is now', L.zop)

t = threading.Thread(target=targ)
t.start()
t.join()
print('in main thread, zop is now', L.zop)
*`# prints:`*
*`#`* *`in main thread, setting zop to 42`*
*`#`* *`in subthread, setting zop to 23`*
*`#`* *`in subthread, zop is now 23`*
*`#`* *`in main thread, zop is now 42`*

线程本地存储使得编写多线程代码更加容易,因为你可以在多个线程中使用相同的命名空间(即 threading.local 的一个实例),而不会相互干扰。

队列模块

队列模块提供支持多线程访问的队列类型,主要类是 Queue,还有一个简化的类 SimpleQueue,主类的两个子类(LifoQueue 和 PriorityQueue),以及两个异常类(Empty 和 Full),在表 15-9 中描述。主类及其子类实例暴露的方法详见表 15-10。

表 15-9. 队列模块的类

| 队列 | Queue(maxsize=0) Queue 是 queue 模块中的主要类,实现先进先出(FIFO)队列:每次检索的项是最早添加的项。

当 maxsize > 0 时,新的 Queue 实例qq达到 maxsize 项时被视为已满。当q已满时,插入带有 block=True的项的线程将暂停,直到另一个线程提取一个项。当 maxsize ⇐ 0 时,q永远不会被视为满,仅受可用内存限制,与大多数 Python 容器一样。 |

SimpleQueue class SimpleQueue SimpleQueue 是一个简化的 Queue:一个无界的 FIFO 队列,缺少 full、task_done 和 join 方法(请参见 Table 15-10),并且 put 方法忽略其可选参数但保证可重入性(这使其可在 del 方法和 weakref 回调中使用,而 Queue.put 则不能)。
LifoQueue class LifoQueue(maxsize=0) LifoQueue 是 Queue 的子类;唯一的区别是 LifoQueue 实现了后进先出(LIFO)队列,意味着每次检索到的项是最近添加的项(通常称为 stack)。
PriorityQueue class PriorityQueue(maxsize=0) PriorityQueue 是 Queue 的子类;唯一的区别是 PriorityQueue 实现了一个 priority 队列,意味着每次检索到的项是当前队列中最小的项。由于没有指定排序的方法,通常会使用 (priority, payload) 对作为项,其中 priority 的值较低表示较早的检索。
Empty Empty 是当 q 为空时 q.get(block=False) 抛出的异常。
Full Full 是当 q 满时 q.put(x, block=False) 抛出的异常。

实例 q 为 Queue 类(或其子类之一)的一个实例,提供了 Table 15-10 中列出的方法,所有方法都是线程安全的,并且保证是原子操作。有关 SimpleQueue 实例公开的方法的详细信息,请参见 Table 15-9。 |

Table 15-10. 类 Queue、LifoQueue 或 PriorityQueue 的实例 q 的方法

empty q.empty() 返回 Trueq 为空时;否则返回 False
full q.full() 返回 Trueq 满时;否则返回 False

| get, get_nowait | q.get(block=True, timeout=None), q.get_nowait() |

当 block 为 False 时,如果 q 中有可用项,则 get 移除并返回一个项;否则 get 抛出 Empty 异常。当 block 为 True 且 timeout 为 None 时,如果需要,get 移除并返回 q 中的一个项,挂起调用线程,直到有可用项。当 block 为 True 且 timeout 不为 None 时,timeout 必须是 >=0 的数字(可能包括用于指定秒的小数部分),get 等待不超过 timeout 秒(如果到达超时时间仍然没有可用项,则 get 抛出 Empty)。q.get_nowait() 类似于 q.get(False),也类似于 q.get(timeout=0.0)。get 移除并返回项:如果 q 是 Queue 的直接实例,则按照 put 插入它们的顺序(FIFO),如果 q 是 LifoQueue 的实例,则按 LIFO 顺序,如果 q 是 PriorityQueue 的实例,则按最小优先顺序。 |

| put, put_nowait | q.put(item, block=True, timeout=None) q.put_nowait(item) |

当 block 为False时,如果q不满,则 put 将item添加到q中;否则,put 会引发 Full 异常。当 block 为True且 timeout 为None时,如果需要,put 将item添加到q,挂起调用线程,直到q不再满为止。当 block 为True且 timeout 不为None时,timeout 必须是>=0 的数字(可能包括指定秒的小数部分),put 等待不超过 timeout 秒(如果到时q仍然满,则 put 引发 Full 异常)。q.put_nowait(item)类似于q.put(item, False),也类似于q.put(item, timeout=0.0)。

qsize q.qsize() 返回当前在q中的项目数。

q维护一个内部的、隐藏的“未完成任务”计数,起始为零。每次调用 put 都会将计数增加一。要将计数减少一,当工作线程完成处理任务时,它调用q.task_done。为了同步“所有任务完成”,调用q.join:当未完成任务的计数非零时,q.join 阻塞调用线程,稍后当计数变为零时解除阻塞;当未完成任务的计数为零时,q.join 继续调用线程。

如果你喜欢以其他方式协调线程,不必使用 join 和 task_done,但是当需要使用队列协调线程系统时,它们提供了一种简单实用的方法。

队列提供了“宁可请求宽恕,不要请求许可”(EAFP)的典型例子,见“错误检查策略”。由于多线程,q的每个非变异方法(empty、full、qsize)只能是建议性的。当其他线程变异q时,线程从非变异方法获取信息的瞬间和线程根据该信息采取行动的下一瞬间之间可能发生变化。因此,依赖“先看再跳”(LBYL)的习惯是徒劳的,而为了修复问题而摆弄锁定则是大量浪费精力。避免脆弱的 LBYL 代码,例如:

`if` *`q`*.empty():
    print('no work to perform')
`else`:  # Some other thread may now have emptied the queue!
    *`x`* = *`q`*.get_nowait()
    work_on(*`x`*)

而是采用更简单和更健壮的 EAFP 方法:

`try`:
    *`x`* = *`q`*.get_nowait()
`except` queue.Empty:  # Guarantees the queue was empty when accessed
    print('no work to perform')
`else`:
    work_on(*`x`*)

多进程模块

多进程模块提供了函数和类,几乎可以像多线程一样编写代码,但是将工作分布到进程而不是线程中:这些包括类 Process(类似于 threading.Thread)和用于同步原语的类(Lock、RLock、Condition、Event、Semaphore、BoundedSemaphore 和 Barrier,每个类似于线程模块中同名的类,以及 Queue 和 JoinableQueue,这两者类似于 queue.Queue)。这些类使得将用于线程的代码转换为使用多进程的版本变得简单;只需注意我们在下一小节中涵盖的差异即可。

通常最好避免在进程之间共享状态:而是使用队列来显式地在它们之间传递消息。然而,在确实需要共享一些状态的罕见情况下,multiprocessing 提供了访问共享内存的类(Value 和 Array),以及更灵活的(包括在网络上不同计算机之间的协调)但带有更多开销的过程子类 Manager,设计用于保存任意数据并让其他进程通过代理对象操作该数据。我们在“共享状态:类 Value、Array 和 Manager”中介绍状态共享。

在编写新代码时,与其移植最初使用线程编写的代码,通常可以使用 multiprocessing 提供的不同方法。特别是 Pool 类(在“进程池”中介绍)通常可以简化您的代码。进行多进程处理的最简单和最高级的方式是与 ProcessPoolExecutor 一起使用的 concurrent.futures 模块(在“concurrent.futures 模块”中介绍)。

基于由 Pipe 工厂函数构建或包装在 Client 和 Listener 对象中的 Connection 对象的其他高级方法,甚至更加灵活,但也更加复杂;我们在本书中不再进一步讨论它们。有关更详尽的 multiprocessing 覆盖,请参阅在线文档²以及像PyMOTW中的第三方在线教程。

multiprocessing 与线程之间的区别

您可以相对容易地将使用线程编写的代码移植为使用 multiprocessing 的变体,但是您必须考虑几个不同之处。

结构差异

您在进程之间交换的所有对象(例如通过队列或作为进程目标函数参数)都是通过 pickle 序列化的,详见“pickle 模块”。因此,您只能交换可以这样序列化的对象。此外,序列化的字节串不能超过约 32 MB(取决于平台),否则会引发异常;因此,您可以交换的对象大小存在限制。

尤其是在 Windows 系统中,子进程必须能够将启动它们的主脚本作为模块导入。因此,请确保将主脚本中的所有顶级代码(指不应由子进程再次执行的代码)用通常的if name == 'main'惯用语包围起来,详见“主程序”。

如果进程在使用队列或持有同步原语时被突然终止(例如通过信号),它将无法对该队列或原语执行适当的清理。因此,队列或原语可能会损坏,导致所有尝试使用它的其他进程出现错误。

进程类

类 multiprocessing.Process 与 threading.Thread 非常相似;它提供了所有相同的属性和方法(参见表 15-2),以及一些额外的方法,在表 15-11 中列出。它的构造函数具有以下签名:

进程 Process(name=None, target=None, args=(), kwargs=) 始终 使用命名参数 调用 Process:参数的数量和顺序不受规范保证,但参数名是固定的。要么实例化 Process 类本身,传递一个目标函数(p.run 在线程启动时调用 target(args, **kwargs));或者,而不是传递目标,扩展 Process 类并覆盖其 run 方法。在任一情况下,只有在调用 p.start 时执行将开始。name 成为 p 的名称。如果 name 为 None,Process 为 p 生成唯一名称。如果 Process 的子类 P 覆盖 initP.init 必须 在任何其他 Process 方法之前在 self 上调用 Process.init(通常通过 super 内置函数)。

表 15-11. Process 类的附加属性和方法

authkey 进程的授权密钥,一个字节串。这是由 os.urandom 提供的随机字节初始化的,但如果需要,您可以稍后重新分配它。用于授权握手的高级用途我们在本书中没有涵盖。
close close() 关闭进程实例并释放所有与之关联的资源。如果底层进程仍在运行,则引发 ValueError。
exitcode None 当进程尚未退出时;否则,进程的退出码。这是一个整数:成功为 0,失败为 >0,进程被杀死为 <0。
kill kill() 与 terminate 相同,但在 Unix 上发送 SIGKILL 信号。
pid None 当进程尚未启动时;否则,进程的标识符由操作系统设置。
terminate terminate() 终止进程(不给予其执行终止代码的机会,如清理队列和同步原语;当进程正在使用队列或持有同步原语时,可能会引发错误!)。

队列的区别

类 multiprocessing.Queue 与 queue.Queue 非常相似,不同之处在于 multiprocessing.Queue 的实例 q 不提供方法 join 和 task_done(在“队列模块”中描述)。当 q 的方法由于超时而引发异常时,它们会引发 queue.Empty 或 queue.Full 的实例。multiprocessing 没有 queue 的 LifoQueue 和 PriorityQueue 类的等效物。

类 multiprocessing.JoinableQueue 确实提供了方法 join 和 task_done,但与 queue.Queue 相比有语义上的区别:对于 multiprocessing.JoinableQueue 的实例 q,调用 q.get 的进程在处理完工作单元后 必须 调用 q.task_done(这不是可选的,就像使用 queue.Queue 时那样)。

您放入 multiprocessing 队列的所有对象必须能够通过 pickle 进行序列化。在执行 q.put 和对象从 q.get 可用之间可能会有延迟。最后,请记住,进程使用 q 的突然退出(崩溃或信号)可能会使 q 对于任何其他进程不可用。

共享状态:类 Value、Array 和 Manager

为了在两个或多个进程之间共享单个原始值,multiprocessing 提供了类 Value,并且对于固定长度的原始值数组,它提供了类 Array。为了获得更大的灵活性(包括共享非原始值和在网络连接的不同系统之间“共享”但不共享内存的情况),multiprocessing 提供了 Manager 类,它是 Process 的子类,但开销较高。我们将在以下小节中详细看看这些类。

Value 类

类 Value 的构造函数具有以下签名:

| Value | class Value(typecode, *args, ***, lock=True) typecode 是一个字符串,定义值的基本类型,就像在 “数组模块” 中所讨论的 array 模块一样。(另外,typecode 可以是来自 ctypes 模块的类型,这在 第二十五章“ctypes” 中讨论过,但这很少需要。)args 被传递到类型的构造函数中:因此,args 要么不存在(在这种情况下,基本类型会按其默认值初始化,通常为 0),要么是一个单一的值,用于初始化该基本类型。

当 lock 为 True(默认情况下),Value 将创建并使用一个新的锁来保护实例。或者,您可以将现有的 Lock 或 RLock 实例作为锁传递。甚至可以传递 lock=False,但这很少是明智的选择:当您这样做时,实例不受保护(因此在进程之间不同步),并且缺少方法 get_lock。如果传递 lock,则必须将其作为命名参数,使用 lock=something

一个类值的实例 v 提供了方法 get_lock,该方法返回(但不获取也不释放)保护 v 的锁,并且具有读/写属性 value,用于设置和获取 v 的基本原始值。

为了确保对 v 的基本原始值的操作是原子性的,请在 with v.get_lock(): 语句中保护该操作。一个典型的使用例子可能是增强赋值,如下所示:

`with` v.get_lock():
    v.value += 1

然而,如果任何其他进程对相同的原始值执行了不受保护的操作——甚至是原子操作,比如简单的赋值操作,如 v.value = x——那么一切都不确定:受保护的操作和不受保护的操作可能导致系统出现 竞态条件。[³] 为了安全起见:如果 v.value 上的 任何 操作都不是原子的(因此需要通过位于 with v.get_lock(): 块中的保护),那么通过将它们放在这些块中来保护 v.value 上的 所有 操作。

Array

multiprocessing.Array 是一个固定长度的原始值数组,所有项都是相同的原始类型。Array 类的构造函数具有如下签名:

| Array | class Array(typecode, size_or_initializer, ***, lock=True) typecode 是定义值的原始类型的字符串,就像在 “数组模块” 中所介绍的那样,与模块 array 中的处理方式相同。(另外,typecode 可以是模块 ctypes 中的类型,讨论在 第二十五章“ctypes” 中,但这很少是必要的。)size_or_initializer 可以是可迭代对象,用于初始化数组,或者是用作数组长度的整数,在这种情况下,数组的每个项都初始化为 0。

lockTrue(默认值)时,Array 会创建并使用一个新的锁来保护实例。或者,您可以将现有的 LockRLock 实例作为锁传递给 lock。您甚至可以传递 lock=False,但这很少是明智的:当您这样做时,实例不受保护(因此在进程之间不同步),并且缺少 get_lock 方法。如果您传递 lock,则必须将其作为命名参数传递,使用 lock=*something*。 |

Array 类的实例 a 提供了方法 get_lock,该方法返回(但不获取也不释放)保护 a 的锁。

a 通过索引和切片访问,并通过对索引或切片赋值来修改。a 是固定长度的:因此,当您对切片赋值时,您必须将一个与您要赋值的切片完全相同长度的可迭代对象赋值给它。a 也是可迭代的。

在特殊情况下,当 a 使用 'c' 类型码构建时,您还可以访问 a.value 以获取 a 的内容作为字节串,并且您可以将任何长度不超过 len(a) 的字节串分配给 a.value。当 s 是长度小于 len(a) 的字节串时,a.value = s 意味着 a[:len(s)+1] = s + b'\0';这反映了 C 语言中字符串的表示,以 0 字节终止。例如:

a = multiprocessing.Array('c', b'four score and seven')
a.value = b'five'
print(a.value)   *`# prints`* *`b'five'`*
print(a[:])      *`# prints`* *`b'five\xOOscore and seven'`*

Manager

multiprocessing.Manager 是 multiprocessing.Process 的子类,具有相同的方法和属性。此外,它还提供方法来构建任何 multiprocessing 同步原语的实例,包括 Queue、dict、list 和 Namespace。Namespace 是一个类,允许您设置和获取任意命名属性。每个方法都以它所构建实例的类名命名,并返回一个代理到这样一个实例,任何进程都可以使用它来调用方法(包括 dict 或 list 实例的索引等特殊方法)。

代理对象大多数操作符、方法和属性访问都会传递给它们代理的实例;但是,它们不会传递比较操作符 —— 如果你需要比较,就需要获取代理对象的本地副本。例如:

manager = multiprocessing.Manager()
p = manager.list()

p[:] = [1, 2, 3]
print(p == [1, 2, 3])       *`# prints`* *`False`**`,`* * `it compares with p itself`*
print(list(p) == [1, 2, 3]) *`# prints`* *`True`**`,`* * `it compares with copy`*

Manager 的构造函数不接受任何参数。有高级方法来定制 Manager 子类,以允许来自无关进程的连接(包括通过网络连接的不同计算机上的进程),并提供不同的构建方法集,但这些内容不在本书的讨论范围之内。使用 Manager 的一个简单而通常足够的方法是显式地将它生成的代理传输给其他进程,通常通过队列或作为进程的目标函数的参数。

例如,假设有一个长时间运行的 CPU 绑定函数 f,给定一个字符串作为参数,最终返回对应的结果;给定一组字符串,我们希望生成一个字典,其键为字符串,值为相应的结果。为了能够跟踪 f 运行在哪些进程上,我们在调用 f 前还打印进程 ID。示例 15-1 展示了如何实现这一点。

示例 15-1. 将工作分配给多个工作进程
`import` multiprocessing `as` mp
`def` f(s):
    *`"""Run a long time, and eventually return a result."""`*
 `import` time`,` random
    time.sleep(random.random()*2)  *`# simulate slowness`*
    `return` s+s                     *`# some computation or other`*

`def` runner(s, d):
    print(os.getpid(), s)
    d[s] = f(s)

`def` make_dict(strings):
    mgr = mp.Manager()
    d = mgr.dict()
    workers = []
    `for` s `in` strings:
        p = mp.Process(target=runner, args=(s, d))
        p.start()
        workers.append(p)
    `for` p `in` workers:
        p.join()
    `return` {**d}

进程池

在实际应用中,应始终避免创建无限数量的工作进程,就像我们在示例 15-1 中所做的那样。性能的提升仅限于您计算机上的核心数(可通过调用 multiprocessing.cpu_count 获取),或者略高于或略低于此数,具体取决于您的平台、代码是 CPU 绑定还是 I/O 绑定,计算机上运行的其他任务等微小差异。创建比这个最佳数量多得多的工作进程会带来大量额外的开销,却没有任何补偿性的好处。

因此,一个常见的设计模式是启动一个带有有限数量工作进程的,并将工作分配给它们。multiprocessing.Pool 类允许您编排这种模式。

池类

类 Pool 的构造函数签名为:

| 池 | Pool(processes=None, initializer=None, initargs=(), maxtasksperchild=None)

processes 是池中的进程数;默认值为 cpu_count 返回的值。当 initializer 不为 None 时,它是一个函数,在池中每个进程开始时调用,带有 initargs 作为参数,例如 initializer(*initargs)。

当 maxtasksperchild 不为 None 时,它是池中每个进程可以执行的最大任务数。当池中的进程执行了这么多任务后,它终止,然后一个新的进程启动并加入池。当 maxtasksperchild 为 None(默认值)时,每个进程的生命周期与池一样长。

类 Pool 的实例 p 提供了 表 15-12 中列出的方法(每个方法只能在构建实例 p 的进程中调用)。

表 15-12. 类 Pool 的实例 p 的方法

apply apply(func, args=(), kwds=) 在工作进程中的任意一个,运行 func(*args, **kwds),等待其完成,并返回 func 的结果。
apply_async apply_async(func, args=(), kwds=, callback=None) 在工作进程中的任意一个,开始运行 func(*args, **kwds),并且不等待其完成,立即返回一个 AsyncResult 实例,该实例最终提供 func 的结果,当结果准备就绪时。 (AsyncResult 类在下一节中讨论。)当 callback 不为 None 时,它是一个函数,用 func 的结果作为唯一参数调用(在调用 apply_async 的进程中的新线程中),当结果准备就绪时;callback 应该快速执行,因为否则会阻塞调用进程。如果参数是可变的,则 callback 可以改变其参数;callback 的返回值是无关紧要的(因此,最佳、最清晰的风格是让它返回 None)。
close close() 设置一个标志,禁止进一步向池提交任务。工作进程在完成所有未完成的任务后终止。
imap imap(func, iterable, chunksize=1) 返回一个迭代器,在每个 iterable 的项上调用 func,顺序执行。chunksize 确定连续发送给每个进程的项数;在非常长的 iterable 上,较大的 chunksize 可以提高性能。当 chunksize 为 1(默认值)时,返回的迭代器有一个方法 next(即使迭代器方法的规范名称是 next),接受一个可选的 timeout 参数(浮点数值,单位秒),在 timeout 秒后仍未准备就绪时引发 multiprocessing.TimeoutError。
im⁠a⁠p⁠_​u⁠n⁠o⁠r⁠d⁠e⁠r⁠e⁠d imap_unordered(func, iterable, chunksize=1) 与 imap 相同,但结果的顺序是任意的(在迭代顺序不重要时,有时可以提高性能)。如果函数的返回值包含足够的信息,以允许将结果与用于生成它们的 iterable 的值关联,则通常很有帮助。
join join() 等待所有工作进程退出。您必须在调用 join 之前调用 close 或 terminate。
map map(func, iterable, chunksize=1) 在池中的工作进程上按顺序对 iterable 中的每个项目调用 func;等待它们全部完成,并返回结果列表。chunksize 确定每个进程发送多少连续项目;在非常长的 iterable 上,较大的 chunksize 可以提高性能。

| map_async | map_async(func, iterable, chunksize=1, callback=None) 安排在池中的工作进程上对可迭代对象 iterable 中的每个项目调用 func;在等待任何操作完成之前,立即返回一个 AsyncResult 实例(在下一节中描述),该实例最终提供 func 的结果列表,当该列表准备就绪时。

当 callback 不为 None 时,它是一个函数(在调用 map_async 的进程的单独线程中调用),其参数是按顺序排列的 func 的结果列表,当该列表准备就绪时。callback 应该快速执行,否则会阻塞进程。callback 可能会改变其列表参数;callback 的返回值是无关紧要的(因此,最好、最清晰的风格是让其返回 None)。|

terminate terminate() 一次性终止所有工作进程,而无需等待它们完成工作。

例如,这是一种基于池的方法来执行与 Example 15-1 中代码相同的任务:

`import` os, multiprocessing `as` mp
`def` f(s):
    *`"""Run a long time, and eventually return a result."""`*
 `import` time, random
    time.sleep(random.random()*2)  *`# simulate slowness`*
    `return` s+s                     *`# some computation or other`*

`def` runner(s):
    print(os.getpid(), s)
    `return` s, f(s)

`def` make_dict(strings):
    `with` mp.Pool() `as` pool:
        d = dict(pool.imap_unordered(runner, strings))
        `return` d

异步结果类

类 Pool 的方法 apply_async 和 map_async 返回类 AsyncResult 的一个实例。类 AsyncResult 的一个实例 r 提供了 Table 15-13 中列出的方法。

表 15-13. 类 AsyncResult 实例 r 的方法

get get(timeout=None) 阻塞并在结果准备就绪时返回结果,或在计算结果时重新引发引发的异常。当 timeout 不为 None 时,它是以秒为单位的浮点值;如果在超时秒后结果尚未准备就绪,则 get 会引发 multiprocessing.TimeoutError。
ready ready() 不阻塞;如果调用已完成并返回结果或已引发异常,则返回 True;否则返回 False
successful successful() 不阻塞;如果结果已准备就绪且计算未引发异常,则返回 True;如果计算引发异常,则返回 False。如果结果尚未准备就绪,successful 将引发 AssertionError。
wait wait(timeout=None) 阻塞并等待结果准备就绪。当 timeout 不为 None 时,它是以秒为单位的浮点值:如果在超时秒后结果尚未准备就绪,则 wait 会引发 multiprocessing.TimeoutError。

线程池类

multiprocessing.pool 模块还提供了一个名为 ThreadPool 的类,其接口与 Pool 完全相同,但在单个进程中使用多个线程实现(而不是多个进程,尽管模块的名称如此)。使用 ThreadPool 的等效 make_dict 代码,与 示例 15-1 中使用 ThreadPoolExecutor 的代码类似:

`def` make_dict(strings):
    num_workers=3
    `with` mp.pool.ThreadPool(num_workers) `as` pool:
        d = dict(pool.imap_unordered(runner, strings))
        `return` d

由于 ThreadPool 使用多个线程但限于在单个进程中运行,因此最适合于其各个线程执行重叠 I/O 的应用程序。正如前面所述,当工作主要受 CPU 限制时,Python 线程提供的优势很小。

在现代 Python 中,您通常应优先使用模块 concurrent.futures 中的抽象类 Executor(下一节将介绍),以及其两个实现,ThreadPoolExecutor 和 ProcessPoolExecutor。特别是,由 concurrent.futures 实现的执行器类的 submit 方法返回的 Future 对象与 asyncio 模块兼容(正如前面提到的,我们不在本书中涵盖 asyncio,但它仍然是 Python 最近版本中许多并发处理的重要部分)。由 multiprocessing 实现的池类的 apply_async 和 map_async 方法返回的 AsyncResult 对象不兼容 asyncio。

并发.futures 模块

并发包提供了一个单一模块,即 futures。concurrent.futures 提供了两个类,ThreadPoolExecutor(使用线程作为工作者)和 ProcessPoolExecutor(使用进程作为工作者),它们实现了相同的抽象接口 Executor。通过调用该类并指定一个参数 max_workers 来实例化任何一种池,该参数指定池应包含的线程或进程数。您可以省略 max_workers,让系统选择工作者数。

Executor 类的实例 e 支持 表 15-14 中的方法。

表 15-14。类 Executor 的实例 e 的方法

| map | map(func, *iterables, timeout=None, chunksize=1) 返回一个迭代器 it,其项是按顺序使用多个工作线程或进程并行执行 func 的结果。当 timeout 不为 None 时,它是一个浮点秒数:如果在 timeout 秒内 next(it) 未产生任何结果,则引发 concurrent.futures.TimeoutError。

您还可以选择(仅按名称)指定参数 chunksize:对于 ThreadPoolExecutor 无效;对于 ProcessPoolExecutor,它设置每个可迭代项中的每个项目传递给每个工作进程的数量。 |

shutdown shutdown(wait=True) 禁止进一步调用 map 或 submit。当 wait 为 True 时,shutdown 阻塞直到所有待处理的 Future 完成;当 False 时,shutdown 立即返回。在任何情况下,进程直到所有待处理的 Future 完成后才终止。
submit submit(*func*, **a*, ***k*) 确保 func(*a, **k) 在线程池的任一进程或线程中执行。不会阻塞,立即返回一个 Future 实例。

任何 Executor 实例也是上下文管理器,因此适合在 with 语句中使用(exit 行为类似于 shutdown(wait=True))。

例如,这里是一个基于并发的方法来执行与 示例 15-1 中相同任务的方法:

`import` concurrent.futures `as` cf
`def` f(s):
    *`"""run a long time and eventually return a result"""`*
    *`# ...`* *`like before!`*

`def` runner(s):
    `return` s, f(s)

`def` make_dict(strings):
    `with` cf.ProcessPoolExecutor() `as` e:
        d = dict(e.map(runner, strings))
    `return` d

Executor 的 submit 方法返回一个 Future 实例。Future 实例 f 提供了表 15-15 中描述的方法。

表 15-15. Future 类实例 f 的方法

add_do⁠n⁠e⁠_​c⁠a⁠l⁠lback add_done_callback(*func*) 将可调用对象 func 添加到 f 上;当 f 完成(即取消或完成)时,会调用 func,并以 f 作为唯一参数。
cancel cancel() 尝试取消调用。当调用正在执行且无法被取消时,返回 False;否则返回 True
cancelled cancelled() 返回 True 如果调用成功取消;否则返回 False
done done() 返回 True 当调用完成(即已完成或成功取消)。
exception exception(timeout=**None**) 返回调用引发的异常,如果调用没有引发异常则返回 None。当 timeout 不为 None 时,它是一个浮点数秒数。如果调用在 timeout 秒内未完成,exception 抛出 concurrent.futures.TimeoutError;如果调用被取消,exception 抛出 concurrent.futures.CancelledError。
result result(timeout=**None**) 返回调用的结果。当 timeout 不为 None 时,它是一个浮点数秒数。如果调用在 timeout 秒内未完成,result 抛出 concurrent.futures.TimeoutError;如果调用被取消,result 抛出 concurrent.futures.CancelledError。
running running() 返回 True 当调用正在执行且无法被取消;否则返回 False

concurrent.futures 模块还提供了两个函数,详见 表 15-16。

表 15-16. concurrent.futures 模块的函数

a⁠s⁠_​c⁠o⁠m⁠pleted as_completed(*fs*, timeout=**None**) 返回一个迭代器 it,迭代 Future 实例,这些实例是可迭代对象 fs 的项。如果 fs 中有重复项,则每个项只返回一次。it 按顺序逐个返回已完成的 Future 实例。如果 timeout 不为 None,它是一个浮点数秒数;如果在 timeout 秒内从上一个已完成实例之后尚未返回新的实例,则 as_completed 抛出 concurrent.futures.Timeout。

| wait | wait(fs, timeout=None, return_when=ALL_COMPLETED) 等待 iterable fs中作为项目的未来实例。返回一个命名的 2 元组集合:第一个集合名为 done,包含在 wait 返回之前已完成(意味着它们已经完成或被取消)的未来实例;第二个集合名为 not_done,包含尚未完成的未来实例。

如果 timeout 不为None,则 timeout 是一个浮点数秒数,表示在返回之前允许等待的最长时间(当 timeout 为None时,wait 仅在满足 return_when 时返回,不管之前经过了多少时间)。

return_when 控制 wait 何时返回;它必须是 concurrent.futures 模块提供的三个常量之一:

ALL_COMPLETED

当所有未来实例完成或被取消时返回。

FIRST_COMPLETED

当任何一个未来完成或被取消时返回。

FIRST_EXCEPTION

当任何未来引发异常时返回;如果没有未来引发异常,则变成等价于 ALL_COMPLETED。

|

这个版本的 make_dict 演示了如何使用 concurrent.futures.as_completed 来在每个任务完成时处理它(与使用 Executor.map 的前一个示例形成对比,后者总是按照提交顺序返回任务):

`import` concurrent.futures `as` cf

`def` make_dict(strings):
    `with` cf.ProcessPoolExecutor() `as` e:
        futures = [e.submit(runner, s) `for` s `in` strings]
        d = dict(f.result() `for` f `in` cf.as_completed(futures))
    `return` d

线程程序架构

一个线程程序应该总是尽量安排一个单一线程“拥有”任何外部于程序的对象或子系统(如文件、数据库、GUI 或网络连接)。虽然有多个处理同一外部对象的线程是可能的,但通常会造成难以解决的问题。

当您的线程程序必须处理某些外部对象时,为这类交互专门分配一个线程,并使用一个队列对象,从中外部交互线程获取其他线程提交的工作请求。外部交互线程通过将结果放在一个或多个其他队列对象中来返回结果。以下示例展示了如何将此架构打包成一个通用的可重用类,假设外部子系统上的每个工作单元可以用可调用对象表示:

`import` threading, queue

`class` ExternalInterfacing(threading.Thread):
    `def` __init__(self, external_callable, **kwds):
        super().__init__(**kwds)
        self.daemon = `True`
        self.external_callable = external_callable
        self.request_queue = queue.Queue()
        self.result_queue = queue.Queue()
        self.start()

    `def` request(self, *args, **kwds):
        *`"""called by other threads as external_callable would be"""`*
        self.request_queue.put((args, kwds))
        `return` self.result_queue.get()

    `def` run(self):
        `while` `True`:
            a, k = self.request_queue.get()
            self.result_queue.put(self.external_callable(*a, **k))

一旦实例化了某个 ExternalInterfacing 对象ei,任何其他线程都可以调用ei.request,就像在缺乏这种机制的情况下调用 external_callable 一样(根据需要带有或不带参数)。ExternalInterfacing 的优点在于对 external_callable 的调用是串行化的。这意味着仅有一个线程(绑定到ei的 Thread 对象)按照某个定义好的顺序顺序执行它们,没有重叠、竞争条件(依赖于哪个线程“恰好先到达”)或其他可能导致异常情况的问题。

如果您需要将多个可调用对象串行化在一起,可以将可调用对象作为工作请求的一部分传递,而不是在类 ExternalInterfacing 的初始化时传递它,以获得更大的通用性。以下示例展示了这种更通用的方法:

`import` threading, queue

`class` Serializer(threading.Thread):
    def __init__(self, **kwds):
        super().__init__(**kwds)
        self.daemon = `True`
        self.work_request_queue = queue.Queue()
        self.result_queue = queue.Queue()
        self.start()

    `def` apply(self, callable, *args, **kwds):
        *``"""called by other threads as `callable` would be"""``*
        self.work_request_queue.put((callable, args, kwds))
        `return` self.result_queue.get()

    `def` run(self):
        `while` `True`:
            callable, args, kwds = self.work_request_queue.get()
            self.result_queue.put(callable(*args, **kwds))

一旦实例化了一个 Serializer 对象ser,任何其他线程都可以调用ser.apply(external_callable),就像没有这种机制时会调用 external_callable 一样(根据需要可能带有或不带有进一步的参数)。Serializer 机制与 ExternalInterfacing 具有相同的优点,不同之处在于,所有调用由同一个或不同的可调用对象包装在单个ser实例中的调用现在都是串行化的。

整个程序的用户界面是一个外部子系统,因此应该由一个单独的线程来处理——具体来说,是程序的主线程(对于某些用户界面工具包,这是强制性的,即使使用其他不强制的工具包,这样做也是建议的)。因此,Serializer 线程是不合适的。相反,程序的主线程应仅处理用户界面问题,并将所有实际工作委派给接受工作请求的工作线程,这些工作线程在一个队列对象上接受工作请求,并在另一个队列上返回结果。一组工作线程通常被称为线程池。正如下面的示例所示,所有工作线程应共享一个请求队列和一个结果队列,因为只有主线程才会发布工作请求并获取结果:

`import` threading

`class` Worker(threading.Thread):
    IDlock = threading.Lock()
    request_ID = 0

    `def` __init__(self, requests_queue, results_queue, **kwds):
        super().__init__(**kwds)
        self.daemon = `True`
        self.request_queue = requests_queue
        self.result_queue = results_queue
        self.start()

    `def` perform_work(self, callable, *args, **kwds):
        *``"""called by main thread as `callable` would be, 			   but w/o return"""``*
        `with` self.IDlock:
            Worker.request_ID += 1
            self.request_queue.put(
                (Worker.request_ID, callable, args, kwds))
            `return` Worker.request_ID

    `def` run(self):
        `while` `True`:
            request_ID, callable, a, k = self.request_queue.get()
            self.result_queue.put((request_ID, callable(*a, **k)))

主线程创建两个队列,然后实例化工作线程,如下所示:

`import` queue
requests_queue = queue.Queue()
results_queue = queue.Queue()
number_of_workers = 5
`for` i `in` range(number_of_workers):
    worker = Worker(requests_queue, results_queue)

每当主线程需要委派工作(执行可能需要较长时间来生成结果的可调用对象),主线程调用worker.perform_work(callable),就像没有这种机制时会调用callable一样(根据需要可能带有或不带有进一步的参数)。然而,perform_work 并不返回调用的结果。主线程得到的是一个标识工作请求的 ID。当主线程需要结果时,它可以跟踪该 ID,因为请求的结果在出现时都标有该 ID。这种机制的优点在于,主线程不会阻塞等待可调用的执行完成,而是立即变为可用状态,可以立即回到处理用户界面的主要业务上。

主线程必须安排检查 results_queue,因为每个工作请求的结果最终会出现在那里,标记有请求的 ID,当从队列中取出该请求的工作线程计算出结果时。主线程如何安排检查用户界面事件和从工作线程返回到结果队列的结果,取决于使用的用户界面工具包,或者——如果用户界面是基于文本的——取决于程序运行的平台。

一个广泛适用但并非始终最佳的一般策略是让主线程轮询(定期检查结果队列的状态)。在大多数类 Unix 平台上,模块 signal 的 alarm 函数允许轮询。tkinter GUI 工具包提供了一个 after 方法,可用于轮询。某些工具包和平台提供了更有效的策略(例如,让工作线程在将某些结果放入结果队列时警告主线程),但没有通用的、跨平台、跨工具包的方法可以安排这样的操作。因此,以下人工示例忽略了用户界面事件,只是通过在几个工作线程上评估随机表达式并引入随机延迟来模拟工作,从而完成了前面的示例:

`import` random, time, queue, operator
*`# copy here class Worker as defined earlier`*

requests_queue = queue.Queue()
results_queue = queue.Queue()

number_of_workers = 3
workers = [Worker(requests_queue, results_queue)
           `for` i `in` range(number_of_workers)]
work_requests = {}

operations = {
    '+': operator.add,
    '-': operator.sub,
    '*': operator.mul,
    '/': operator.truediv,
    '%': operator.mod,
}

`def` pick_a_worker():
    return random.choice(workers)

`def` make_work():
    o1 = random.randrange(2, 10)
    o2 = random.randrange(2, 10)
    op = random.choice(list(operations))
    `return` f'{o1} {op} {o2}'

`def` slow_evaluate(expression_string):
    time.sleep(random.randrange(1, 5))
    op1, oper, op2 = expression_string.split()
    arith_function = operations[oper]
    `return` arith_function(int(op1), int(op2))

`def` show_results():
    `while` `True`:
        `try`:
            completed_id, results = results_queue.get_nowait()
        `except` queue.Empty:
            `return`
        work_expression = work_requests.pop(completed_id)
        print(f'Result {completed_id}: {work_expression} -> {results}')

`for` i `in` range(10):
    expression_string = make_work()
    worker = pick_a_worker()
    request_id = worker.perform_work(slow_evaluate, expression_string)
    work_requests[request_id] = expression_string
    print(f'Submitted request {request_id}: {expression_string}')
    time.sleep(1.0)
    show_results()

`while` work_requests:
    time.sleep(1.0)
    show_results()

进程环境

操作系统为每个进程P提供一个环境,即一组变量,变量名为字符串(通常按约定为大写标识符),其值也为字符串。在“环境变量”中,我们讨论了影响 Python 操作的环境变量。操作系统 shell 通过 shell 命令和其他在该部分提到的方法提供了检查和修改环境的方式。

进程环境是自包含的

任何进程P的环境在P启动时确定。启动后,只有P自己能够改变P的环境。对P环境的更改仅影响P本身:环境不是进程间通信的手段。P的任何操作都不会影响P的父进程(启动P的进程)的环境,也不会影响任何P之前启动的子进程现在正在运行的环境,或者与P无关的任何进程。P的子进程通常会在P创建该进程时获取一个P环境的副本作为启动环境。从这个狭义的意义上说,对P环境的更改确实会影响在此类更改之后由P启动的子进程。

模块 os 提供了属性 environ,这是一个映射,表示当前进程的环境。当 Python 启动时,它从进程环境初始化 os.environ。如果平台支持此类更新,对 os.environ 的更改将更新当前进程的环境。os.environ 中的键和值必须是字符串。在 Windows 上(但在类 Unix 平台上不是这样),os.environ 中的键隐式转换为大写。例如,以下是如何尝试确定您正在运行的 shell 或命令处理器:

`import` os
shell = os.environ.get('COMSPEC')
`if` shell `is` `None`:
    shell = os.environ.get('SHELL')
`if` shell `is` `None`:
    shell = 'an unknown command processor'
print('Running under ', shell)

当 Python 程序改变其环境(例如通过os.environ['X'] = 'Y'),这不会影响启动该程序的 shell 或命令处理器的环境。正如已经解释过的,对于所有编程语言,包括 Python,进程环境的更改仅影响进程本身,而不影响当前正在运行的其他进程。

运行其他程序

您可以通过 os 模块中的低级函数运行其他程序,或者(在更高且通常更可取的抽象级别上)使用 subprocess 模块。

使用子进程模块

subprocess 模块提供了一个非常广泛的类:Popen,支持许多不同的方式来运行另一个程序。Popen 的构造函数签名如下:

| Popen | class Popen(args, bufsize=0, executable=None, capture_output=False, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, text=None, universal_newlines=False, startupinfo=None, creationflags=0) Popen 启动一个子进程来运行一个独立的程序,并创建并返回一个对象p,代表该子进程。args是必需的参数,而许多可选的命名参数控制子进程运行的所有细节。

当在子进程创建过程中发生任何异常(在明确程序启动之前),Popen 会将该异常与名为 child_traceback 的属性一起重新引发到调用过程中,child_traceback 是子进程的 Python traceback 对象。这样的异常通常是 OSError 的一个实例(或者可能是 TypeError 或 ValueError,表示您传递给 Popen 的参数在类型或值上是无效的)。

subprocess.run()是 Popen 的一个便捷包装函数。

subprocess 模块包括 run 函数,该函数封装了一个 Popen 实例,并在其上执行最常见的处理流程。run 接受与 Popen 构造函数相同的参数,运行给定的命令,等待完成或超时,并返回一个 CompletedProcess 实例,其中包含返回码以及 stdout 和 stderr 的内容。

如果需要捕获命令的输出,则最常见的参数值将是将 capture_output 和 text 参数设置为True

要运行什么,以及如何运行

args是一个字符串序列:第一项是要执行的程序的路径,后续的项(如果有)是要传递给程序的参数(当不需要传递参数时,args也可以只是一个字符串)。当 executable 不为None时,它将覆盖args以确定要执行的程序。当 shell 为True时,executable 指定要用来运行子进程的 shell;当 shell 为True且 executable 为None时,在类 Unix 系统上使用的 shell 是*/bin/sh*(在 Windows 上,它是 os.environ['COMSPEC'])。

子进程文件

stdin、stdout 和 stderr 分别指定了子进程的标准输入、输出和错误文件。每个可以是 PIPE,这会创建一个到/从子进程的新管道;None,意味着子进程要使用与此(“父”)进程相同的文件;或者是已经适当打开的文件对象(或文件描述符)(对于读取,用于标准输入;对于写入,用于标准输出和标准错误)。stderr 还可以是 subprocess.STDOUT,这意味着子进程的标准错误必须使用与其标准输出相同的文件。⁴ 当 capture_output 为 true 时,不能指定 stdout 或 stderr:行为就像每个都指定为 PIPE 一样。bufsize 控制这些文件的缓冲(除非它们已经打开),其语义与 open 函数中的相同参数的语义相同(默认为 0,表示“无缓冲”)。当 text(或其同义词 universal_newlines,提供向后兼容性)为 true 时,stdout 和 stderr(除非它们已经打开)将被打开为文本文件;否则,它们将被打开为二进制文件。当 close_fds 为 true 时,在子进程执行其程序或 shell 之前,所有其他文件(除了标准输入、输出和错误)将被关闭。

其他,高级参数

当 preexec_fn 不为 None 时,必须是一个函数或其他可调用对象,并且在子进程执行其程序或 shell 之前调用它(仅适用于类 Unix 系统,其中调用发生在 fork 之后和 exec 之前)。

当 cwd 不为 None 时,必须是一个给出现有目录的完整路径的字符串;在子进程执行其程序或 shell 之前,当前目录会切换到 cwd。

当 env 不为 None 时,必须是一个映射,其中键和值都是字符串,并完全定义了新进程的环境;否则,新进程的环境是当前父进程中活动环境的副本。

startupinfo 和 creationflags 是传递给 CreateProcess Win32 API 调用的 Windows-only 参数,用于创建子进程,用于特定于 Windows 的目的(本书不进一步涵盖它们,因为本书几乎完全专注于 Python 的跨平台使用)。

subprocess.Popen 实例的属性

类 Popen 的实例 p 提供了 Table 15-17 中列出的属性。

Table 15-17. 类 Popen 的实例 p 的属性

args Popen 的 args 参数(字符串或字符串序列)。
pid 子进程的进程 ID。
returncode None 表示子进程尚未退出;否则,是一个整数:0 表示成功终止,>0 表示以错误代码终止,或 <0 如果子进程被信号杀死。
stderr, stdin, stdout 当 Popen 的相应参数是 subprocess.PIPE 时,这些属性中的每一个都是包装相应管道的文件对象;否则,这些属性中的每一个都是 None。使用 p 的 communicate 方法,而不是从这些文件对象读取和写入,以避免可能的死锁。

subprocess.Popen 实例的方法

类 Popen 的实例 p 提供了表 15-18 中列出的方法。

表 15-18. 类 Popen 的实例 p 的方法

communicate p.communicate(input=None, timeout=None) 将字符串 input 作为子进程的标准输入(当 input 不为 None 时),然后将子进程的标准输出和错误文件读入内存中的字符串 sose,直到两个文件都完成,最后等待子进程终止并返回对(两项元组)(sose)。
poll p.poll() 检查子进程是否已终止;如果已终止,则返回 p.returncode;否则返回 None
wait p.wait(timeout=None) 等待子进程终止,然后返回 p.returncode。如果子进程在 timeout 秒内未终止,则引发 TimeoutExpired 异常。

使用 os 模块运行其他程序

你的程序通常运行其他进程的最佳方式是使用前一节介绍的 subprocess 模块。然而,os 模块(在第十一章介绍)也提供了几种较低级别的方式来实现这一点,在某些情况下可能更简单。

运行另一个程序的最简单方法是通过 os.system 函数,尽管这种方法没有办法控制外部程序。os 模块还提供了几个以 exec 开头的函数,这些函数提供了精细的控制。由 exec 函数之一运行的程序会替换当前程序(即 Python 解释器)在同一进程中。因此,在实践中,您主要在支持使用 fork 的平台上使用 exec 函数(即类 Unix 平台)。以 spawn 和 popen 开头的 os 函数提供了中间简单性和强大性:它们是跨平台的,并且不像 system 那样简单,但对于许多目的来说足够简单。

exec 和 spawn 函数运行给定的可执行文件,给定可执行文件的路径、传递给它的参数,以及可选的环境映射。system 和 popen 函数执行一个命令,这是一个字符串传递给平台的默认 shell 的新实例(通常在 Unix 上是 /bin/sh,在 Windows 上是 cmd.exe)。命令是一个比可执行文件更一般的概念,因为它可以包含特定于当前平台的 shell 功能(管道、重定向和内置 shell 命令)使用的 shell 语法。

os 提供了表 15-19 列出的函数。

表 15-19. 与进程相关的 os 模块的函数

| execl, execle,

execlp,

execv,

execve,

execvp,

execvpe | execl(path, *args), execle(path, *args),

execlp(path,*args),

execv(path, args),

execve(path, args, env),

execvp(path, args),

execvpe(path, args, env)

运行由字符串 path 指示的可执行文件(程序),替换当前进程中的当前程序(即 Python 解释器)。函数名中编码的区别(在前缀 exec 之后)控制新程序的发现和运行的三个方面:

  • path 是否必须是程序可执行文件的完整路径,还是该函数可以接受一个名称作为 path 参数并在多个目录中搜索可执行文件,就像操作系统 shell 一样?execlp、execvp 和 execvpe 可以接受一个 path 参数,该参数只是一个文件名而不是完整路径。在这种情况下,函数将在 os.environ['PATH'] 中列出的目录中搜索具有该名称的可执行文件。其他函数要求 path 是可执行文件的完整路径。

  • 函数是否接受新程序的参数作为单个序列参数 args,还是作为函数的单独参数?以 execv 开头的函数接受一个参数 args,该参数是要用于新程序的参数序列。以 execl 开头的函数将新程序的参数作为单独的参数(特别是 execle,它使用其最后一个参数作为新程序的环境)。

  • 函数是否接受新程序的环境作为显式映射参数 env,或者隐式使用 os.environ?execle、execve 和 execvpe 接受一个参数 env,该参数是要用作新程序环境的映射(键和值必须是字符串),而其他函数则使用 os.environ 用于此目的。

    每个 exec 函数使用 args 中的第一个项作为告知新程序其正在运行的名称(例如,在 C 程序的 main 中的 argv[0]);只有 args[1:] 是新程序的真正参数。

|

| popen | popen(cmd, mode='r', buffering=-1) 运行字符串命令 cmd 在一个新进程 P 中,并返回一个文件对象 f,该对象包装了与 P 的标准输入或来自 P 的标准输出的管道(取决于模式);f 使用文本流而不是原始字节。模式和缓冲区的含义与 Python 的 open 函数相同,见 “使用 open 创建文件对象”。当模式为 'r'(默认)时,f 是只读的,并包装 P 的标准输出。当模式为 'w' 时,f 是只写的,并包装 P 的标准输入。

f 与其他类似文件的主要区别在于方法 f.close 的行为。 f.close 等待 P 终止并返回 None,正如文件类对象的关闭方法通常所做的那样,当 P 成功终止时。然而,如果操作系统将整数错误码 cP 的终止关联起来,表示 P 的终止失败,f.close 返回 c。在 Windows 系统上,c 是子进程的有符号整数返回码。 |

| spawnv, spawnve | spawnv(mode, path, args), spawnve(mode, path, args, env)

这些函数在新进程 P 中运行由 path 指示的程序,参数作为序列 args 传递。spawnve 使用映射 env 作为 P 的环境(键和值必须是字符串),而 spawnv 则使用 os.environ 来实现。仅在 Unix-like 平台上,还有其他 os.spawn 的变体,对应于 os.exec 的变体,但 spawnv 和 spawnve 是 Windows 上唯一存在的两个。

mode 必须是 os 模块提供的两个属性之一:os.P_WAIT 表示调用进程等待新进程终止,而 os.P_NOWAIT 表示调用进程继续与新进程同时执行。当 mode 是 os.P_WAIT 时,函数返回 P 的终止码 c:0 表示成功终止,c < 0 表示 P信号 杀死,c > 0 表示正常但终止失败。当 mode 是 os.P_NOWAIT 时,函数返回 P 的进程 ID(或在 Windows 上是 P 的进程句柄)。没有跨平台的方法来使用 P 的 ID 或句柄;Unix-like 平台上的平台特定方法包括 os.waitpid,而在 Windows 上则包括第三方扩展包 pywin32

例如,假设您希望交互式程序给用户一个机会编辑一个即将读取和使用的文本文件。您必须事先确定用户喜欢的文本编辑器的完整路径,例如在 Windows 上为 c:\windows\notepad.exe 或在类 Unix 平台上为 /usr/bin/vim。假设这个路径字符串绑定到变量 editor,并且您要让用户编辑的文本文件的路径绑定到 textfile:

`import` os
os.spawnv(os.P_WAIT, editor, (editor, textfile))

参数 args 的第一项作为“调用程序的名称”传递给被生成的程序。大多数程序不关心这一点,因此通常可以放置任何字符串。以防编辑程序确实查看这个特殊的第一个参数(例如某些版本的 Vim),最简单和最有效的方法是将与 os.spawnv 的第二个参数相同的字符串 editor 传递给它。

系统 系统(cmd) 在新进程中运行字符串命令 cmd,当新进程成功终止时返回 0。当新进程终止失败时,系统返回一个非零整数错误代码(具体的错误代码依赖于你运行的命令:这方面没有广泛接受的标准)。

mmap 模块

mmap 模块提供了内存映射文件对象。mmap 对象的行为类似于字节串,因此通常可以在需要字节串的地方传递 mmap 对象。然而,它们也有一些区别:

  • mmap 对象不提供字符串对象的方法。

  • mmap 对象是可变的,类似于 bytearray,而字节对象是不可变的。

  • mmap 对象也对应于一个打开的文件,并且在多态性方面表现为 Python 文件对象(如“类似文件对象和多态性”中所述)。

mmap 对象 m 可以进行索引或切片操作,产生字节串。由于 m 是可变的,你也可以对 m 的索引或切片进行赋值。然而,当你对 m 的切片赋值时,赋值语句的右侧必须是与你要赋值的切片具有完全相同长度的字节串。因此,许多在列表切片赋值中可用的有用技巧(如“修改列表”中所述)不适用于 mmap 切片赋值。

mmap 模块在 Unix-like 系统和 Windows 上提供了稍有不同的工厂函数:

| mmap | Windows: mmap(filedesc, length, tagname='', access=None, offset=None) Unix: mmap(filedesc, length, flags=MAP_SHARED, prot=PROT_READ|PROT_WRITE, access=None, offset=0)

创建并返回一个 mmap 对象 m,它映射到内存中文件描述符 filedesc 指示的文件的前 length 字节。filedesc 必须是一个同时打开读写的文件描述符,除非在类 Unix 平台上,参数 prot 仅请求读或仅请求写。(文件描述符在“文件描述符操作”中有介绍。)要为 Python 文件对象 f 获取 mmap 对象 m,可以使用 m=mmap.mmap(f.fileno(), length)。filedesc 可以为 -1,以映射匿名内存。

在 Windows 上,所有内存映射都是可读写的,并在进程之间共享,因此所有在文件上有内存映射的进程都可以看到其他进程所做的更改。仅在 Windows 上,你可以传递一个字符串 tagname 来为内存映射指定显式的 标签名。这个标签名允许你在同一个文件上拥有几个独立的内存映射,但这很少是必需的。仅使用两个参数调用 mmap 的优点是在 Windows 和 Unix-like 平台之间保持代码的可移植性。 |

| mmap (续) | 仅在类 Unix 平台上,您可以传递 mmap.MAP_PRIVATE 作为标志以获得一个对您的进程私有并且写时复制的映射。mmap.MAP_SHARED 是默认值,它获取一个与其他进程共享的映射,以便所有映射文件的进程都可以看到一个进程(与 Windows 上相同)所做的更改。您可以将 mmap.PROT_READ 作为 prot 参数传递,以获取仅可读而不可写的映射。传递 mmap.PROT_WRITE 获取仅可写而不可读的映射。默认值,位或 mmap.PROT_READ|mmap.PROT_WRITE,获取一个既可读又可写的映射。您可以传递命名参数 access 而不是标志和 prot(传递 access 和其他两个参数中的一个或两个是错误的)。access 的值可以是 ACCESS_READ(只读),ACCESS_WRITE(写透传,Windows 上的默认值)或 ACCESS_COPY(写时复制)。

您可以传递命名参数 offset 以在文件开始后开始映射;offset 必须是大于等于 0 的整数,是 ALLOCATIONGRANULARITY 的倍数(或者在 Unix 上是 PAGESIZE 的倍数)。

mmap 对象的方法

mmap 对象 m 提供了详细方法,参见 Table 15-20。

Table 15-20. mmap 实例 m 的方法

close m.close() 关闭 m 的文件。
find m.find(sub, start=0, end=None) 返回大于等于 start 的最低 i,使得 sub == m[i:i+len(sub)](并且当您传递 end 时,i+len(sub)-1 ⇐ end)。如果没有这样的 im.find 返回 -1。这与 str 的 find 方法的行为相同,该方法在 Table 9-1 中介绍。
flush m.flush([offset, n]) 确保所有对 m 所做的更改都存在于 m 的文件中。在调用 m.flush 之前,文件是否反映了 m 的当前状态是不确定的。您可以传递起始字节偏移量 offset 和字节计数 n 来将刷新效果的保证限制为 m 的一个切片。传递这两个参数,或者两者都不传递:仅传递一个参数调用 m.flush 是错误的。
move m.move(dstoff, srcoff, n) 类似于切片赋值 m[dstoff:dstoff+n] = m[srcoff:srcoff+n],但可能更快。源切片和目标切片可以重叠。除了可能的重叠外,move 方法不会影响源切片(即,move 方法复制字节但不移动它们,尽管该方法的名称为“移动”)。
read m.read(n) 读取并返回一个字节字符串 s,包含从 m 的文件指针开始的最多 n 个字节,然后将 m 的文件指针前进 s 的长度。如果 m 的文件指针和 m 的长度之间的字节数少于 n,则返回可用的字节。特别是,如果 m 的文件指针在 m 的末尾,则返回空字节串 b''。
read_byte m.read_byte() 返回包含m的文件指针处的字节的长度为 1 的字节字符串,然后将m的文件指针推进 1。m.read_byte()类似于m.read(1)。但是,如果m的文件指针在m的末尾,则m.read(1)返回空字符串 b''且不推进,而m.read_byte()会引发 ValueError 异常。
readline m.readline() 从m文件的当前文件指针读取并返回一个字节字符串,直到下一个'\n'(包括'\n'),然后将m的文件指针推进到刚刚读取的字节之后。如果m的文件指针在m的末尾,则 readline 返回空字符串 b''。
resize m.resize(n) 改变m的长度,使得 len(m)变为n。不影响m的文件大小。m的长度和文件大小是独立的。要将m的长度设置为文件的大小,请调用m.resize(m.size())。如果m的长度大于文件的大小,则m将填充空字节(\x00)。
rfind rfind(sub, start=0, end=None) 返回大于等于 start 的最高i,使得sub == m[i:i+len(sub)](当你传递 end 时,i+len(sub)-1 ⇐ end)。如果不存在这样的im.rfind 返回-1。这与字符串对象的 rfind 方法相同,详见 Table 9-1。

| seek | m.seek(pos, how=0) 将m的文件指针设置为整数字节偏移量pos,相对于由 how 指示的位置:

0 or os.SEEK_SET

偏移量相对于m的起始位置

1 or os.SEEK_CUR

偏移量相对于m的当前文件指针

2 or os.SEEK_END

偏移量相对于m的末尾

试图将m的文件指针设置为负偏移量或超出m长度的偏移量会引发 ValueError 异常。

size m.size() 返回m文件的长度(以字节为单位),而不是m本身的长度。要获取m的长度,使用 len(m)。
tell m.tell() 返回m的当前文件指针位置,即m文件中的字节偏移量。
write m.write(b) 将字节串b写入m的当前文件指针位置,覆盖已有的字节,然后将m的文件指针推进 len(b)。如果m的文件指针与m的长度之间的字节数少于 len(b),write 会引发 ValueError 异常。
write_byte m.write_byte(byte) 向映射 m 的当前位置写入必须是整数的 byte,覆盖原有的字节,然后将 m 的文件指针前移 1。 m.write_byte(x) 与 m.write(x.to_bytes(1, 'little')) 类似。然而,如果 m 的文件指针位于 m 的末尾,则 m.write_byte(x) 静默不做任何操作,而 m.write(x.to_bytes(1, 'little')) 会引发 ValueError 异常。请注意,这与文件末尾的 read 和 read_byte 之间的关系相反:write 和 read_byte 可能会引发 ValueError,而 read 和 write_byte 则从不会。

使用 mmap 对象进行 IPC

进程使用 mmap 通信的方式与它们使用文件基本相同:一个进程写入数据,另一个进程稍后读取相同的数据。由于 mmap 对象有一个底层文件,因此可以有一些进程在文件上进行 I/O(如在 “The io Module” 中介绍的),而其他进程在同一个文件上使用 mmap。在便利性和功能性之间选择 mmap 和文件对象的 I/O,性能大致相当。例如,这里是一个简单的程序,反复使用文件 I/O,使文件的内容等于用户交互式输入的最后一行:

fileob = open('xxx','wb')
`while` `True`:
    data = input('Enter some text:')
    fileob.seek(0)
    fileob.write(data.encode())
    fileob.truncate()
    fileob.flush()

并且这里有另一个简单的程序,当在与前者相同的目录中运行时,使用 mmap(以及在 Table 13-2 中涵盖的 time.sleep 函数)每秒钟检查文件的变化,并打印出文件的新内容(如果有任何变化的话):

`import` mmap, os, time
mx = mmap.mmap(os.open('xxx', os.O_RDWR), 1)
last = `None`
`while` `True`:
    mx.resize(mx.size())
    data = mx[:]
    `if` data != last:
        print(data)
        last = data
    time.sleep(1)

¹ 我们遇到的最好的关于异步编程的入门工作,尽管现在已经过时(因为 Python 中的异步方法不断改进),是 Using Asyncio in Python,作者是 Caleb Hattingh(O’Reilly)。我们建议你也学习 Brad Solomon 的 Asyncio 演示 在 Real Python 上。

² 在线文档包括一个特别有用的 “编程指南”部分,列出了使用 multiprocessing 模块时的许多额外实用建议。

³ 竞争条件是一种情况,其中不同事件的相对时间通常是不可预测的,可能会影响计算的结果... 这从来都不是一件好事!

⁴ 就像在 Unix 风格的 shell 命令行中指定 2>&1 一样。

第十六章:数值处理

您可以使用运算符(见“数值操作”)和内置函数(见“内置函数”)执行一些数值计算。Python 还提供了支持额外数值计算的模块,本章详细介绍:math 和 cmath、statistics、operator、random 和 secrets、fractions 和 decimal。数值处理经常需要更具体地处理数字数组;这个主题在“数组处理”中有所涉及,重点介绍标准库模块 array 和流行的第三方扩展 NumPy。最后,“额外的数值包”列出了 Python 社区生产的几个额外数值处理包。本章大多数示例假定您已经导入了适当的模块;import 语句仅在情况不明确时包含。

浮点数值

Python 用类型为 float 的变量表示实数值(即非整数)。与整数不同,由于其内部实现为固定大小的二进制整数尾数(通常错误地称为“尾数”)和固定大小的二进制整数指数,计算机很少能够精确表示浮点数。浮点数有几个限制(其中一些可能导致意外结果)。

对于大多数日常应用程序,浮点数足以进行算术运算,但它们在能够表示的小数位数上有限:

>>> f = 1.1 + 2.2 - 3.3  *`# f should be equal to 0`*
>>> f
4.440892098500626e-16

它们在能够精确存储的整数值范围上也有限制(即能够区分下一个最大或最小整数值):

>>> f = 2 ** 53
>>> f
9007199254740992
>>> f + 1
9007199254740993    *# integer arithmetic is not bounded*
>>> f + 1.0
9007199254740992.0  *# float conversion loses integer precision at 2**53*

请始终记住,由于其在计算机中的内部表示,浮点数并不完全精确。对复数的考虑也是同样。

不要在浮点数或复数之间使用 == 运算符

鉴于浮点数和运算只能近似地模拟数学中的“实数”行为,几乎没有必要检查两个浮点数 xy 是否完全相等。每个数的计算方式微小的差异很容易导致意外的差异。

要测试浮点数或复数是否相等,请使用内置模块 math 导出的函数 isclose。以下代码说明了原因:

>>> `import` math
>>> f = 1.1 + 2.2 - 3.3  *`# f intuitively equal to 0`*
>>> f == 0
False
>>> f
4.440892098500626e-16
>>> *`# default tolerance fine for this comparison`*
>>> math.isclose(-1, f-1)
True

对于某些值,您可能需要显式设置容差值(这总是必需当您与 0 进行比较时):

>>> *`# near-0 comparison with default tolerances`*
>>> math.isclose(0, f)
False
>>> *`# must use abs_tol for comparison with 0`*
>>> math.isclose(0, f, abs_tol=1e-15)
True

你还可以使用 isclose 来进行安全的循环。

不要将浮点数用作循环控制变量

常见错误是将浮点值用作循环的控制变量,假设它最终会等于某个结束值,如 0。相反,它很可能最终将永远循环。

以下循环预期循环五次然后结束,但实际上将永远循环:

>>> f = 1
>>> `while` f != 0:
... f -= 0.2 

尽管 f 最初是一个整数,现在它是一个浮点数。这段代码展示了为什么:

>>> 1-0.2-0.2-0.2-0.2-0.2  *`# should be 0, but...`*
5.551115123125783e-17

即使使用不等操作符 > 也会导致不正确的行为,循环六次而不是五次(因为剩余的浮点值仍然大于 0):

>>> f = 1
>>> count = 0
>>> `while` f > 0:
...     count += 1
...     f -= 0.2
>>> print(count)
6   *# one loop too many!*

如果您使用 math.isclose 来比较f与 0,for循环将重复正确的次数:

>>> f = 1
>>> count = 0
>>> `while` `not` math.isclose(0,f,abs_tol=1e-15):
...     count += 1
...     f -= 0.2
>>> print(count)
5   *# just right this time!*

通常情况下,尽量使用 int 作为循环控制变量,而不是 float。

最终,导致非常大的浮点数的数学运算通常会引发 OverflowError,或者 Python 可能会将它们返回为 inf(无穷大)。在您的计算机上可用的最大浮点数值是 sys.float_info.max:在 64 位计算机上,它是 1.7976931348623157e+308。当处理非常大的数时,这可能导致意外的结果。当您需要处理非常大的数时,我们建议使用 decimal 模块或第三方gmpy

math 和 cmath 模块

math 模块提供了用于处理浮点数的数学函数;cmath 模块提供了处理复数的等效函数。例如,math.sqrt(-1)会引发异常,但 cmath.sqrt(-1)会返回 1j。

就像对于任何其他模块一样,使用这些模块的最干净、最易读的方法是在代码顶部import math,并明确调用,例如 math.sqrt。然而,如果您的代码包含大量调用模块的已知数学函数,您可以(尽管可能会失去一些清晰度和可读性)要么使用from math import *,要么使用from math import sqrt,然后仅调用 sqrt。

每个模块都公开了三个浮点属性,绑定到基本数学常数 e、pi 和tau,以及各种函数,包括表 16-1 中显示的函数。math 和 cmath 模块并非完全对称,因此表格中每个方法都指示它是在 math、cmath 还是两者中。所有示例假定您已导入适当的模块。

表 16-1。math 和 cmath 模块的方法和属性

math cmath
acos, asin, atan, cos, sin, tan acos(x)等,返回x的反三角函数,余弦、正弦、正切,单位为弧度。
acosh, asinh, atanh, cosh, sinh, tanh acosh(x)等,返回x的反双曲余弦、反双曲正弦、反双曲正切、双曲余弦、双曲正弦或双曲正切的值,单位为弧度。

| atan2 | atan2(y, x) 类似于 atan(y/x),但 atan2 正确考虑了两个参数的符号。例如:

>>> math.atan(-1./-1.)
0.7853981633974483
>>> math.atan2(-1., -1.)
-2.356194490192345

x等于 0 时,atan2 返回π/2,而除以x会引发 ZeroDivisionError。 | ✓ |   |

cbrt cbrt(x) 3.11+ 返回x的立方根。
ceil ceil(x) 返回不小于x的最小整数i的浮点数i
comb comb(n, k) 3.8+ Returns the number of combinations of n items taken k items at a time, regardless of order. When counting the number of combinations taken from three items A, B, and C, two at a time, comb(3, 2) returns 3 because, for example, A-B and B-A are considered the same combination (contrast this with perm, later in this table). Raises ValueError when k or n is negative; raises TypeError when k or n is not an int. When k>n, just returns 0, raising no exceptions.
copysign copysign(x, y) Returns the absolute value of x with the sign of y.
degrees degrees(x) Returns the degree measure of the angle x given in radians.
dist dist(pt0, pt1) 3.8+ Returns the Euclidean distance between two n-dimensional points, where each point is represented as a sequence of values (coordinates). Raises ValueError if pt0 and pt1 are sequences of different lengths.
e The mathematical constant e (2.718281828459045).
erf erf(x) Returns the error function of x as used in statistical calculations.
erfc erfc(x) Returns the complementary error function at x, defined as 1.0 - erf(x).
exp exp(x) Returns eˣ.
exp2 exp2(x) 3.11+ Returns 2ˣ.
expm1 expm1(x) Returns eˣ - 1. Inverse of log1p.
fabs fabs(x) Returns the absolute value of x. Always returns a float, even if x is an int (unlike the built-in abs function).
factorial factorial(x) Returns the factorial of x. Raises ValueError when x is negative, and TypeError when x is not integral.
floor floor(x) Returns float(i), where i is the greatest integer such that i<=x.
fmod fmod(x, y) Returns the float r, with the same sign as x, such that r==x-n**y* for some integer n, and abs(r)<abs(y). Like x%y, except that, when x and y differ in sign, x%y has the same sign as y, not the same sign as x.
frexp frexp(x) Returns a pair (m, e) where m is a floating-point number and e is an integer such that x==m*(2**e) and 0.5<=abs(m)<1,^(a) except that frexp(0) returns (0.0, 0).
fsum fsum(iterable) Returns the floating-point sum of the values in iterable to greater precision than the sum built-in function.
gamma gamma(x) Returns the Gamma function evaluated at x.
gcd gcd(x, y) Returns the greatest common divisor of x and y. When x and y are both zero, returns 0. (3.9+ gcd can accept any number of values; gcd() without arguments returns 0.)
hypot hypot(x, y) Returns sqrt(xx*+yy*). (3.8+ hypot can accept any number of values, to compute a hypotenuse length in n dimensions.)
inf A floating-point positive infinity, like float('inf').
infj 一个复数的无穷虚数,等于 complex(0, float('inf'))。

| isclose | isclose(x, y, rel_tol=1e-09, abs_tol=0.0) 返回当xy在相对容差 rel_tol 内近似相等时为True,最小绝对容差 abs_tol 为 0.0;否则返回False。默认情况下,rel_tol 是九位小数。rel_tol 必须大于 0。abs_tol 用于接近零的比较:必须至少为 0.0。NaN 不被认为接近任何值(包括 NaN 本身);+/- inf 除外,isclose 类似于: |   |   |

abs(x-y) <= max(rel_tol*max(abs(x), 
                abs(y)),abs_tol)
isfinite isfinite(x) 返回当x(在 cmath 中,x的实部和虚部)既不是无穷大也不是 NaN 时为True;否则返回False
isinf isinf(x) 返回当x(在 cmath 中,x的实部或虚部,或两者)是正无穷大或负无穷大时为True;否则返回False
isnan isnan(x) 返回当x(在 cmath 中,x的实部或虚部,或两者)是 NaN 时为True;否则返回False
isqrt isqrt(x) 3.8+ 返回 int(sqrt(x))。
lcm lcm(x, ...) 3.9+ 返回给定整数的最小公倍数。如果不是所有值都是整数,则引发 TypeError。
ldexp ldexp(x, i) 返回x乘以2**ii必须是整数;当i为浮点数时,ldexp 引发 TypeError)。是 frexp 的反函数。
lgamma lgamma(x) 返回 Gamma 函数在x处的绝对值的自然对数。
log log(x) 返回x的自然对数。
log10 log10(x) 返回x的以 10 为底的对数。
log1p log1p(x) 返回 1+x的自然对数。是 expm1 的反函数。
log2 log2(x) 返回x的以 2 为底的对数。
modf modf(x) 返回一个由x的小数部分和整数部分组成的元组(f, i),即两个浮点数,其符号与x相同,使得iint(i)且xf+i
nan nan 一个浮点数“非数字”(NaN)值,类似于 float('nan')或 complex('nan')。
nanj 一个实部为 0.0 且虚部为浮点数“非数字”(NaN)的复数。
nextafter nextafter(a, b) 3.9+ 返回从a开始向b方向上的下一个更高或更低的浮点值。
perm perm(n, k) 3.8+ 返回从n个项目中取k个项目的排列数,其中相同项目的不同顺序选择被单独计算。例如,对于三个项目ABC,取两个项目的排列数为 perm(3, 2),返回 6,因为例如A-BB-A被视为不同的排列(与此表中较早的 comb 相对比)。当kn为负时引发 ValueError;当kn不是整数时引发 TypeError。
π 数学常数π,3.141592653589793。
相位 phase(x) 返回x的相位,为一个范围在(–π, π)的浮点数。类似于 math.atan2(x.imag, x.real)。详见在线文档
极坐标 polar(x) 返回复数x的极坐标表示,即一对(r, phi),其中rx的模,phix的相位。类似于(abs(x), cmath.phase(x))。详见在线文档
pow pow(x, y) 返回 float(x)的 float(y)次幂。对于大整数值的xy,为避免 OverflowError 异常,请使用x**y或不转换为浮点数的内置 pow 函数。
prod prod(seq, start=1) 3.8+ 返回序列中所有值的乘积,从给定的起始值(默认为 1)开始。如果seq为空,则返回起始值。
radians radians(x) 返回角度x的弧度值。
rect rect(r, phi) 返回以极坐标(r, phi)表示的复数值,转换为矩形坐标形式为(x + yj)。
remainder remainder(x, y) 返回x/y的有符号余数(如果xy为负,则结果可能为负)。
sqrt sqrt(x) 返回x的平方根。
τ 数学常数τ = 2π,即 6.283185307179586。
trunc trunc(x) 返回被截断为整数的x
ulp ulp(x) 3.9+ 返回浮点数x的最低有效位。对于正值,等同于 math.nextafter(x, x+1) - x。对于负值,等同于 ulp(-x)。如果x为 NaN 或 inf,则返回x。ulp 代表最小精度单位
^(a) 形式上,m为尾数,e为指数。用于渲染浮点值的跨平台可移植表示。

统计模块

统计模块提供了 NormalDist 类用于执行分布分析,并提供了表 16-2 中列出的函数来计算常见统计数据。

表 16-2. 统计模块的函数(包括版本 3.8 和 3.10 新增的函数)

3.8+ 3.10+

| harmonic_mean mean

中位数

中位数(分组)

中位数(高)

中位数(低)

众数

总体标准差

总体方差

标准差

| 方差 | fmean geometric_mean

多模态

分位数

NormalDist | 相关性 协方差

线性回归 |

在线文档详细介绍了这些函数的签名和用法。

运算符模块

运算符模块提供了一些等效于 Python 操作符的函数。这些函数在需要存储可调用对象、作为参数传递或作为函数结果返回的情况下非常方便。运算符模块中的函数与相应的特殊方法(在“特殊方法”中介绍)具有相同的名称。每个函数都有两个名称,一个带有“dunder”(前导和尾随双下划线)和一个不带有:“例如,operator.add(a, b)和 operator.add(a, b)都返回a + b

添加了用于中缀运算符 @ 的矩阵乘法支持,但必须通过定义自己的 matmulrmatmul 和/或 imatmul 方法来实现它;NumPy 目前支持 @(但在撰写本文时尚未支持 @=)进行矩阵乘法。

表 16-3 列出了运算符模块提供的一些函数。有关这些函数及其用法的详细信息,请参阅在线文档

表 16-3. 运算符模块提供的函数

Function Signature 行为类似
abs abs(a) abs(a)
add add(a, b) a + b
and_ and_(a, b) a & b
concat concat(a, b) a + b
contains contains(a, b) b a
countOf countOf(a, b) a.count(b)
delitem delitem(a, b) del a[b]
delslice delslice(a, b, c) del a[b:c]
eq eq(a, b) a == b
floordiv floordiv(a, b) a // b
ge ge(a, b) a >= b
getitem getitem(a, b) a[b]
getslice getslice(a, b, c) a[b:c]
gt gt(a, b) a > b
index index(a) a.index()
indexOf indexOf(a, b) a.index(b)
invert, inv invert(a), inv(a) ~a
is_ is_(a, b) a b
is_not is_not(a, b) a 不是 b
le le(a, b) ab
lshift lshift(a, b) a << b
lt lt(a, b) a < b
matmul matmul(m1, m2) m1 @ m2
mod mod(a, b) a % b
mul mul(a, b) a * b
ne ne(a, b) a != b
neg neg(a) -a
not_ not_(a) not a
or_ or_(a, b) a | b
pos pos(a) +a
pow pow(a, b) a ** b
repeat repeat(a, b) a * b
rshift rshift(a, b) a >> b
setitem setitem(a, b, c) a[b] = c
setslice setslice(a, b, c, d) a[b:c] = d
sub sub(a, b) a - b
truediv truediv(a, b) a/b # 不截断
truth truth(a) bool(a), 非非 a
xor xor(a, b) a ^ b

operator 模块还提供了其他额外的高阶函数,列在表 16-4 中。其中三个函数,attrgetter、itemgetter 和 methodcaller,返回适合作为命名参数键传递给列表的 sort 方法、内置函数 sorted、min 和 max,以及标准库模块(例如 heapq 和 itertools 中讨论的第八章中的几个函数)的函数的函数。

表 16-4. operator 模块提供的高阶函数

| attrgetter | attrgetter(attr), attrgetter(*attrs)

返回一个可调用对象f,使得f(o)与 getattr(o, attr)相同。字符串attr可以包含点号(.),在这种情况下,attrgetter 的可调用结果会重复调用 getattr。例如,operator.attrgetter('a.b')等效于lambda o: getattr(getattr(o, 'a'), 'b')。

当您使用多个参数调用 attrgetter 时,生成的可调用对象将提取每个命名的属性并返回结果值的元组。 |

| itemgetter | itemgetter(key), itemgetter(*keys)

返回一个可调用对象f,使得f(o)与 getitem(o, key)相同。

当您使用多个参数调用 itemgetter 时,生成的可调用对象将提取每个以此方式键入的项目,并返回结果值的元组。

例如,假设L是一个至少三项的子列表列表:您希望基于每个子列表的第三项对L进行原地排序,对于具有相同第三项的子列表,则按其第一项排序。这样做的最简单方法是:

L.sort(key=operator.itemgetter(2, 0))

|

length_hint length_hint(iterable, default=0) 用于尝试预分配iterable中项目的存储空间。调用对象iterablelen 方法尝试获取准确长度。如果 len 未实现,则 Python 尝试调用iterablelength_hint 方法。如果这也未实现,length_hint 返回给定的默认值。在使用此“提示”助手时出现任何错误可能会导致性能问题,但不会导致静默的错误行为。
methodcaller methodcaller(methodname, args...) 返回一个可调用对象f,使得f(o)与 o.methodname(args, ...)相同。可选的 args 可以作为位置参数或命名参数传递。

随机数与伪随机数

标准库的随机模块使用各种分布生成伪随机数。底层均匀伪随机生成器采用强大且流行的Mersenne Twister 算法,周期长度为 2¹⁹⁹³⁷-1(巨大!)。

随机模块

随机模块的所有函数都是 random.Random 类的一个隐藏全局实例的方法。您可以显式实例化 Random 以获取不共享状态的多个生成器。如果需要在多个线程中使用随机数(线程在 第十五章 中讨论),则建议显式实例化。或者,如果需要更高质量的随机数,请实例化 SystemRandom(有关详细信息,请参阅下一节)。Table 16-5 记录了随机模块提供的最常用函数。

Table 16-5. 随机模块提供的实用函数

choice choice(seq) 从非空序列 seq 中返回一个随机项。
choices choices(seq, weights=None, *, cum_weights=None, k=1) 从非空序列 seq 中返回 k 个元素(可重复选择)。默认情况下,元素以相等概率被选择。如果传递了可选的 weights 或命名参数 cum_weights(作为浮点数或整数列表),则在选择过程中将按照相应的权重进行加权。cum_weights 参数接受一个浮点数或整数列表,如 itertools.accumulate(weights) 返回的那样;例如,如果 seq 包含三个项目的 weights 为 [1, 2, 1],则相应的 cum_weights 将为 [1, 3, 4]。(只能指定 weights 或 cum_weights 中的一个,并且必须与 seq 长度相同。如果使用,cum_weights 和 k 必须作为命名参数给出。)
getrandbits getrandbits(k) 返回一个大于等于 0 的整数,具有 k 个随机位,类似于 randrange(2 ** k)(但更快,并且对于大 k 没有问题)。
getstate getstate() 返回一个可散列且可 pickle 的对象 S,表示生成器的当前状态。稍后可以将 S 传递给 setstate 函数以恢复生成器的状态。
jumpahead jumpahead(n) 将生成器状态推进,仿佛已生成 n 个随机数。这比生成和忽略 n 个随机数要快。
randbytes randbytes(k) 3.9+ 生成 k 个随机字节。要为安全或加密应用生成字节,请使用 secrets.randbits(k * 8),然后将其返回的整数解包成 k 个字节,使用 int.to_bytes(k, 'big')。
randint randint(start, stop) 返回一个随机整数 i,满足 startistop 的均匀分布。两个端点都包括在内:在 Python 中这很不自然,因此通常会优先选择 randrange。
random random() 返回一个来自均匀分布的随机浮点数 r,满足 0 ⇐ r < 1。
randrange randrange([start,]stop[,step]) 类似于 choice(range(start, stop, step)),但速度更快。
sample sample(seq, k) 返回一个新列表,其中的k个项是从seq中随机抽取的唯一项。列表是随机排序的,因此它的任何片段都是同样有效的随机样本。seq可能包含重复项。在这种情况下,每个项的每次出现都可能被选为样本的候选项,因此样本中也可能包含这些重复项。
seed seed(x=None) 初始化生成器状态。x可以是任何 int、float、str、bytes 或 bytearray 类型。当xNone时,在第一次加载 random 模块时,seed 使用当前系统时间(或某些特定于平台的随机源,如果有的话)获取种子。通常情况下,x是一个最多为 2²⁵⁶的 int、一个 float 或最多 32 字节大小的 str、bytes 或 bytearray。接受更大的x值,但可能产生与较小值相同的生成器状态。seed 在仿真或建模中很有用,用于可重复运行的运行或编写需要可重复随机值序列的测试。
setstate setstate(S) 恢复生成器的状态。S必须是先前调用 getstate 的结果(这样的调用可能发生在另一个程序中,或者在本程序的上一次运行中,只要对象S已经正确地被传输或保存并恢复)。
shuffle shuffle(seq) 就地对可变序列seq进行洗牌。
uniform uniform(a, b) 返回一个从均匀分布中取出的随机浮点数r,使得ar < b
^(a) 如 Python 语言规范定义的那样。特定的 Python 实现可能支持更大的种子值以生成唯一的随机数序列。

random 模块还提供了几个其他函数,用于从其他概率分布(Beta、Gamma、指数、高斯、帕累托等)生成伪随机浮点数,内部调用 random.random 作为它们的随机源。详细信息请参见在线文档

密码质量随机数:secrets 模块

random 模块提供的伪随机数虽然足以用于仿真和建模,但不具有密码学质量。要获取用于安全和密码学应用的随机数和序列,请使用 secrets 模块中定义的函数。这些函数使用 random.SystemRandom 类,该类又调用 os.urandom。os.urandom 返回从物理随机位源(例如旧版 Linux 上的/dev/urandom 或 Linux 3.17 及以上版本上的 getrandom()系统调用)读取的随机字节。在 Windows 上,os.urandom 使用像 CryptGenRandom API 这样的密码强度源。如果当前系统上没有合适的源,则 os.urandom 会引发 NotImplementedError 异常。

secrets 函数不能使用已知的种子运行

与 random 模块不同,后者包括一个种子函数来支持生成可重复的随机值序列,secrets 模块没有这样的功能。要编写依赖于 secrets 模块函数生成特定随机值序列的测试,开发人员必须使用自己的模拟版本来模拟这些函数。

secrets 模块提供了 表 16-6 中列出的函数。

表 16-6. secrets 模块的函数

choice choice(seq) 从非空序列 seq 中随机选择一个项目。
randbelow randbelow(n) 返回一个范围在 0 ⇐ x < n 的随机整数 x
randbits randbits(k) 返回一个具有 k 个随机位的整数。
token_bytes token_bytes(n) 返回一个包含 n 个随机字节的 bytes 对象。当省略 n 时,使用默认值,通常为 32。
token_hex token_hex(n) 返回一个由 n 个随机字节的十六进制字符组成的字符串,每字节两个字符。当省略 n 时,使用默认值,通常为 32。
token_urlsafe token_urlsafe(n) 返回一个由 n 个随机字节的 Base64 编码字符组成的字符串;结果字符串的长度约为 n 的 1.3 倍。当省略 n 时,使用默认值,通常为 32。

Python 的 在线文档 提供了额外的示例和最佳的加密实践。

其他来源的物理随机数可以在线获取,例如来自 Fourmilab

fractions 模块

fractions 模块提供了一个有理数类 Fraction,其实例可以由一对整数、另一个有理数或字符串构造。Fraction 类实例具有只读属性 numerator 和 denominator。您可以传递一对(可选带符号的)整数作为 numeratordenominator。如果 denominator 是 0,则会引发 ZeroDivisionError。字符串可以是形如 '3.14' 的形式,或者可以包括一个可选带符号的分子,一个斜杠 (/),和一个分母,例如 '-22/7'。

Fraction 分数化简至最低项

Fraction 将分数化简至最低项——例如,f = Fraction(226, 452) 创建一个与 Fraction(1, 2) 等效的实例 f。无法从生成的实例中恢复最初传递给 Fraction 的特定分子和分母。

Fraction 还支持从 decimal.Decimal 实例和浮点数构造(后者可能由于浮点数的有限精度而提供您不期望的结果)。以下是使用不同输入使用 Fraction 的一些示例。

>>> `from` fractions `import` Fraction
>>> `from` decimal `import` Decimal
>>> Fraction(1,10)
Fraction(1, 10)
>>> Fraction(Decimal('0.1'))
Fraction(1, 10)
>>> Fraction('0.1')
Fraction(1, 10)
>>> Fraction('1/10')
Fraction(1, 10)
>>> Fraction(0.1)
Fraction(3602879701896397, 36028797018963968)
>>> Fraction(-1, 10)
Fraction(-1, 10)
>>> Fraction(-1,-10)
Fraction(1, 10)

Fraction 类提供了包括 limit_denominator 在内的方法,允许您创建浮点数的有理数近似——例如,Fraction(0.0999).limit_denominator(10) 返回 Fraction(1, 10)。Fraction 实例是不可变的,可以作为字典的键或集合的成员,以及与其他数字进行算术运算。完整内容请参阅 在线文档

fractions 模块还提供了一个与 math.gcd 完全相同的函数 gcd,详细介绍在表 16-1 中。

十进制模块

Python 浮点数是二进制浮点数,通常根据现代计算机硬件上实现的 IEEE 754 标准。关于浮点运算及其问题的出色、简洁、实用介绍可以在 David Goldberg 的论文“计算机科学家应该知道的浮点运算”中找到。关于相同问题的针对 Python 的专题论文是 Python 文档中的教程;另一篇出色的摘要(不专注于 Python),Bruce Bush 的“The Perils of Floating Point”,也可在线上找到。

经常情况下,特别是对于与货币相关的计算,您可能更喜欢使用十进制浮点数。Python 提供了一个符合 IEEE 854 标准的实现,适用于十进制,在标准库模块 decimal 中。该模块有很好的文档:在那里,您可以找到完整的参考资料、适用标准的指针、教程以及关于十进制的宣传。在这里,我们仅涵盖了该模块功能的一个小子集,即模块的最常用部分。

decimal 模块提供了 Decimal 类(其不可变实例是十进制数)、异常类以及处理算术上下文的类和函数,该上下文指定精度、四舍五入以及在发生诸如零除以、溢出、下溢等计算异常时引发的异常。在默认上下文中,精度为 28 位小数数字,四舍五入为“银行家舍入”(将结果四舍五入为最接近的可表示十进制数;当结果恰好在两个这样的数中间时,舍入到最后一位是偶数的数),引发异常的异常包括无效操作、零除以及溢出。

要构建十进制数,请使用一个参数调用 Decimal:一个 int、float、str 或元组。如果以浮点数开始,Python 会无损地将其转换为精确的十进制等效值(可能需要 53 位或更多的精度):

>>> `from` decimal `import` Decimal
>>> df = Decimal(0.1)
>>> df
Decimal('0.1000000000000000055511151231257827021181583404541015625')

如果这不是您想要的行为,则可以将浮点数作为 str 传递;例如:

>>> ds = Decimal(str(0.1))  *`# or, more directly, Decimal('0.1')`*
>>> ds
Decimal('0.1')

您可以轻松地编写一个工厂函数,以便通过十进制进行交互式实验:

`def` dfs(x):
    `return` Decimal(str(x))

现在 dfs(0.1)与 Decimal(str(0.1))或 Decimal('0.1')完全相同,但更简洁和更方便书写。

或者,您可以使用 Decimal 的 quantize 方法来通过指定的有效数字四舍五入一个浮点数以构造一个新的十进制数:

>>> dq = Decimal(0.1).quantize(Decimal('.00'))
>>> dq
Decimal('0.10')

如果您以元组开始,则需要提供三个参数:符号(0 表示正数,1 表示负数),数字的元组和整数指数:

>>> pidigits = (3, 1, 4, 1, 5)
>>> Decimal((1, pidigits, -4))
Decimal('-3.1415')

一旦你有 Decimal 的实例,你可以对它们进行比较,包括与浮点数的比较(使用 math.isclose 进行比较);对它们进行 pickle 和 unpickle 操作;并将它们用作字典的键和集合的成员。你还可以在它们之间进行算术运算,以及与整数进行运算,但不能与浮点数进行运算(以避免结果中意外丢失精度),如下所示:

>>> `import` math
>>> `from` decimal `import` Decimal
>>> a = 1.1
>>> d = Decimal('1.1')
>>> a == d
False
>>> math.isclose(a, d)
True
>>> a + d
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'float' and 
'decimal.Decimal'
>>> d + Decimal(a) *`# new decimal constructed from 'a'`*
Decimal('2.200000000000000088817841970')
>>> d + Decimal(str(a)) *`# convert 'a' to decimal with str(a)`*
Decimal('2.20')

在线文档包括有用的配方,用于货币格式化、一些三角函数以及常见问题解答。

数组处理

大多数语言称为数组或向量的内容,你可以使用列表(在 “列表” 中介绍)或者使用数组标准库模块(在后续小节中介绍)。你可以使用循环、索引和切片、列表推导式、迭代器、生成器和生成表达式(在 第三章 中介绍)来操作数组;使用诸如 map、reduce 和 filter 等内置函数(在 “内置函数” 中介绍);以及标准库模块如 itertools(在 “itertools 模块” 中介绍)。如果你只需要一个简单类型的轻量级、一维数组的实例,请使用数组。然而,要处理大量的数字数组,这些函数可能比 NumPy 和 SciPy 等第三方扩展更慢、不太方便(在 “用于数值数组计算的扩展” 中介绍)。在进行数据分析和建模时,建议使用 Pandas,它是建立在 NumPy 之上的(但本书未讨论),可能更合适。

数组模块

数组模块提供了一种类型,也称为数组,其实例是可变序列,类似于列表。数组 a 是一个一维序列,其项可以是字符,或者仅限于你创建 a 时指定的某种具体数值类型。数组的构造函数为:

array array(typecode, init='', /) 创建并返回一个具有给定 typecode 的数组对象 ainit 可以是一个字符串(字节串,除了 typecode 为 'u' 外),其长度是 itemsize 的倍数:字符串的字节直接初始化 a 的项。另外,init 可以是可迭代的对象(当 typecode 为 'u' 时是字符,否则是数字):迭代对象的每一项初始化 a 的一项。

array.array 的优势在于,与列表相比,当你需要保存一系列对象,而这些对象都是同一类型(数值或字符)时,它可以节省内存。数组对象 a 在创建时具有一个字符的只读属性 a.typecode,用于指定 a 的项的类型。表 16-7 展示了数组的可能 typecode 值。

表 16-7. 数组模块的类型代码

typecode C 类型 Python 类型 最小大小
'b' char int 1 字节
'B' 无符号 char int 1 字节
'u' Unicode 字符 字符串(长度为 1) 参见注释
'h' short int 2 bytes
'H' unsigned short int 2 bytes
'i' int int 2 bytes
'I' unsigned int int 2 bytes
'l' long int 4 bytes
'L' unsigned long int 4 bytes
'q' long long int 8 bytes
'Q' unsigned long long int 8 bytes
'f' float float 4 bytes
'd' double float 8 bytes

类型码 'u' 的最小大小

'u' 在一些平台(特别是 Windows)上的项大小为 2,在几乎所有其他平台上为 4。可以通过使用 array.array('u').itemsize 检查 Python 解释器的构建类型。

数组 a 中每个项的字节大小可能大于最小值,取决于机器的体系结构,并且可作为只读属性 a.itemsize 获取。

Array 对象暴露了所有可变序列的方法和操作(如“序列操作”中所述),但不包括排序。使用 + 或 += 进行连接以及切片赋值,要求两个操作数都是具有相同类型码的数组;相反,a.extend 的参数可以是任何可迭代对象,其项可被 a 接受。除了可变序列的方法(append、extend、insert、pop 等),数组对象 a 还公开了表 16-8 中列出的方法和属性。

表 16-8. 数组对象 a 的方法和属性

buffer_info a.buffer_info() 返回一个二元元组(addressarray_length),其中 array_lengtha 中可以存储的项数。a 的大小(以字节为单位)为 a.buffer_info()[1] * a.itemsize。
byteswap a.byteswap() 交换 a 中每个项的字节顺序。
frombytes a.frombytes(s) 将字节串 s(以机器值解释)附加到 as 的长度必须正好是 a.itemsize 的整数倍。
fromfile a.fromfile(f, n) 从文件对象 f 中读取 n 个项(以机器值形式),并将这些项附加到 af 应该以二进制模式打开进行读取——通常是 'rb' 模式(参见“使用 open 创建文件对象”)。当 f 中的项少于 n 时,fromfile 在附加可用项后引发 EOFError(因此,在您的应用程序中如果可以,请务必捕获此异常!)。
fromlist a.fromlist(L) 将列表 L 的所有项附加到 a
fromunicode a.fromunicode(s) 将字符串 s 的所有字符附加到 aa 必须具有类型码 'u';否则,Python 将引发 ValueError。
itemsize a.itemsize 返回 a 中每个项的字节大小。
tobytes a.tobytes() tobytes 返回数组 a 中项的字节表示。对于任何 a,len(a.tobytes()) == len(a)*a.itemsize。f.write(a.tobytes()) 等同于 a.tofile(f)。
tofile a.tofile(f) 将 a 的所有项目作为机器值写入文件对象 f。请注意 f 应该以二进制写入模式打开,例如使用模式 'wb'。
tolist a.tolist() 创建并返回一个与 a 中项目相同的列表对象,类似于 list(a)。
tounicode a.tounicode() 创建并返回一个与 a 中项目相同的字符串,类似于 ''.join(a)。a 必须有类型码 'u',否则 Python 会引发 ValueError。
typecode a.typecode 返回用于创建 a 的类型码的属性。

数值数组计算的扩展

正如你所见,Python 在数值处理方面有很好的内置支持。第三方库 SciPy,以及许多其他库,如 NumPyMatplotlibSymPyNumbaPandasPyTorchCuPyTensorFlow,提供了更多工具。我们在这里介绍 NumPy,然后简要描述了 SciPy 和其他一些库,并提供指向它们文档的链接。

NumPy

如果你需要一个轻量级的一维数值数组,标准库中的 array 模块可能足够了。如果你的工作涉及科学计算、图像处理、多维数组、线性代数或其他涉及大量数据的应用程序,流行的第三方 NumPy 包能够满足你的需求。在线文档详尽可查看 这里;Travis Oliphant 的 Guide to NumPy 也提供免费的 PDF 下载。²

NumPy 还是 numpy?

文档中有时将该包称为 NumPy 或 Numpy;但在编码中,该包通常称为 numpy,并使用 import numpy as np 导入。本节遵循这些约定。

NumPy 提供了 ndarray 类,你可以 派生子类 来添加特定需求的功能。一个 ndarray 对象包含 n 维同类项(项可能包括异类类型的容器)。每个 ndarray 对象 a 有一定数量的维度(也称为 ),称为其 。一个 标量(即单个数)的秩为 0,一个 向量 的秩为 1,一个 矩阵 的秩为 2,依此类推。ndarray 对象还有一个 shape 属性,可以通过属性 shape 访问。例如,对于一个有 2 列 3 行的矩阵 mm.shape 是 (3,2)。

NumPy 支持比 Python 更广泛的 数值类型(dtype 实例);默认数值类型为 bool_(1 字节)、int_(根据平台为 int64 或 int32)、float_(float64 的简写)和 complex_(complex128 的简写)。

创建 NumPy 数组

有几种方式可以在 NumPy 中创建数组。最常见的包括:

  • 使用工厂函数 np.array,从序列(通常是嵌套序列)中进行数组创建,可以进行type inference或显式指定dtype

  • 使用工厂函数 np.zeros、np.ones 或 np.empty,默认使用dtype为 float64

  • 使用工厂函数 np.indices,默认使用dtype为 int64

  • 使用工厂函数 np.random.uniform、np.random.normal、np.random.binomial 等,默认使用dtype为 float64

  • 使用工厂函数 np.arange(通常是startstopstride)、或者使用工厂函数 np.linspace(startstopquantity)以获得更好的浮点行为

  • 通过使用其他 np 函数从文件中读取数据(例如,使用 np.genfromtxt 读取 CSV 文件)

这里是使用刚才描述的各种技术创建数组的一些示例:

>>> `import` numpy `as` np
>>> np.array([1, 2, 3, 4])  *`# from a Python list`*
array([1, 2, 3, 4])
>>> np.array(5, 6, 7)  *`# a common error: passing items separately (they`*
                       *`# must be passed as a sequence, e.g. a list)`*
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: array() takes from 1 to 2 positional arguments, 3 were given
>>> s = 'alph', 'abet'  *`# a tuple of two strings`*
>>> np.array(s)
array(['alph', 'abet'], dtype='<U4')
>>> t = [(1,2), (3,4), (0,1)]  *`# a list of tuples`*
>>> np.array(t, dtype='float64')  *`# explicit type designation`*
array([[1., 2.],
 [3., 4.],
 [0., 1.]])
>>> x = np.array(1.2, dtype=np.float16)  *`# a scalar`*
>>> x.shape
()
>>> x.max()
1.2
>>> np.zeros(3)  *`# shape defaults to a vector`*
array([0., 0., 0.])
>>> np.ones((2,2))  *`# with shape specified`*
array([[1., 1.],
[1., 1.]])
>>> np.empty(9)  *`# arbitrary float64s`*
array([ 6.17779239e-31, -1.23555848e-30,  3.08889620e-31,
       -1.23555848e-30,  2.68733969e-30, -8.34001973e-31,  
	    3.08889620e-31, -8.34001973e-31,  4.78778910e-31])
>>> np.indices((3,3))
array([[[0, 0, 0],
 [1, 1, 1],
 [2, 2, 2]],

 [[0, 1, 2],
 [0, 1, 2],
 [0, 1, 2]]])
>>> np.arange(0, 10, 2)  *`# upper bound excluded`*
array([0, 2, 4, 6, 8])
>>> np.linspace(0, 1, 5)  *`# default: endpoint included`*
array([0\.  , 0.25, 0.5 , 0.75, 1\.  ])
>>> np.linspace(0, 1, 5, endpoint=False)  *`# endpoint not included`*
array([0\. , 0.2, 0.4, 0.6, 0.8])
>>> np.genfromtxt(io.BytesIO(b'1 2 3\n4 5 6'))  *`# using a pseudo-file`*
array([[1., 2., 3.],
 [4., 5., 6.]])
>>> `with` open('x.csv', 'wb') as f:
...     f.write(b'2,4,6\n1,3,5')
...
11
>>> np.genfromtxt('x.csv', delimiter=',')  *`# using an actual CSV file`*
array([[2., 4., 6.],
 [1., 3., 5.]])

形状、索引和切片

每个 ndarray 对象a都有一个属性a.shape,它是一个整数元组。len(a.shape)是a的秩;例如,一个一维数字数组(也称为vector)的秩为 1,a.shape 只有一个项目。更一般地,a.shape 的每个项目是a相应维度的长度。a的元素数量,称为其size,是a.shape 所有项目的乘积(也可以作为属性a.size 获得)。a的每个维度也称为一个axis。轴索引从 0 开始,如 Python 中通常所见。负轴索引是允许的,并且从右边计数,因此-1 是最后(最右边)的轴。

每个数组a(除了标量,即秩为 0 的数组)都是 Python 序列。a的每个项a[i]是a的子数组,意味着它的秩比a低一级:a[i].shape == a.shape[1:]。例如,如果a是一个二维矩阵(a的秩为 2),对于任何有效的索引ia[i]是a的一个一维子数组,对应于矩阵的一行。当a的秩为 1 或 0 时,a的项是a的元素(对于秩为 0 的数组,只有一个元素)。由于a是一个序列,您可以使用常规索引语法对a进行索引以访问或更改a的项。请注意,a的项是a的子数组;只有对于秩为 1 或 0 的数组,数组的items才与数组的elements相同。

与任何其他序列一样,您也可以对a进行slice操作。在b = a[i:j]之后,ba具有相同的秩,且b.shape 等于a.shape,除了b.shape[0]是切片a[i:j]的长度(即当a.shape[0] > j >= i >= 0 时,切片的长度为j - i,如“切片序列”中所述)。

一旦您有了数组a,您可以调用a.reshape(或等效地,使用a作为第一个参数调用 np.reshape)。结果的形状必须与a.size 相匹配:当a.size 为 12 时,您可以调用a.reshape(3, 4)或a.reshape(2, 6),但a.reshape(2, 5)会引发 ValueError。请注意,reshape 不会在原地工作:您必须显式地绑定或重新绑定数组,例如,a = a.reshape(i, j)或b = a.reshape(i, j)。

你也可以像处理任何其他序列一样,使用 for 对(非标量)a 进行循环。例如,这样:

`for` x `in` a:
    process(x)

意思与以下相同:

`for` _ `in` range(len(*`a`*)):
    x = *`a`*[_]
    process(*`x`*)

在这些示例中,循环中的每个 x 都是 a 的子数组。例如,如果 a 是二维矩阵,则在任何一个循环中,每个 x 都是对应于矩阵行的一维子数组。

你也可以通过元组对 a 进行索引或切片。例如,当 a 的秩 >= 2 时,你可以将 a[i][j] 写为 a[i, j],用于重新绑定和访问;元组索引更快更方便。不要在括号内使用括号来指示你正在通过元组对 a 进行索引:只需将索引逐个写出,用逗号分隔。a[i, j] 和 a[(i, j)] 的意思完全相同,但没有括号的形式更易读。

索引是一种切片,其中元组的一个或多个项是切片,或者(每个切片最多一次)是特殊形式 ...(Python 内置的省略号)。... 扩展为尽可能多的全轴切片(:),以“填充”你正在切片的数组的秩。例如,a[1,...,2] 当 a 的秩是 4 时,相当于 a[1,:,:,2],但当 a 的秩是 6 时,相当于 a[1,:,:,:,:,2]。

下面的代码段展示了循环、索引和切片:

>>> a = np.arange(8)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7])
>>> a = a.reshape(2,4)
>>> a
array([[0, 1, 2, 3],
 [4, 5, 6, 7]])
>>> print(a[1,2])
6
>>> a[:,:2]
array([[0, 1],
 [4, 5]])
>>> for row in a:
...     print(row)
...
[0 1 2 3]
[4 5 6 7]
>>> for row in a:
...     for col in row[:2]:  *`# first two items in each row`*
...         print(col)
...
0
1
4
5

在 NumPy 中的矩阵操作

正如在“运算符模块”中提到的,NumPy 使用运算符 @ 来实现数组的矩阵乘法。a1 @ a2 就像 np.matmul(a1, a2)。当两个矩阵是二维的时,它们被视为传统的矩阵。当一个参数是向量时,你可以概念上将其提升为二维数组,方法是根据需要临时添加或预置 1 到其形状。不要使用 @ 来与标量相乘;请使用 *。矩阵还可以与标量以及具有兼容形状的向量和其他矩阵进行加法(使用 +)。矩阵的点积也可使用 np.dot(a1, a2)。下面是几个这些运算符的简单示例:

>>> a = np.arange(6).reshape(2,3)  *`# a 2D matrix`*
>>> b = np.arange(3)               *`# a vector`*
>>>
>>> a
array([[0, 1, 2],
 [3, 4, 5]])
>>> a + 1    *`# adding a scalar`*
array([[1, 2, 3],
 [4, 5, 6]])
>>> a + b    *`# adding a vector`*
array([[0, 2, 4],
 [3, 5, 7]])
>>> a * 2    *`# multiplying by a scalar`*
array([[ 0,  2,  4],
 [ 6,  8, 10]])
>>> a * b    *`# multiplying by a vector`*
array([[ 0,  1,  4],
 [ 0,  4, 10]])
>>> a @ b    *`# matrix-multiplying by a vector`*
array([ 5, 14])
>>> c = (a*2).reshape(3,2)  *`# using scalar multiplication to create`*
>>> c
array([[ 0,  2],
 [ 4,  6],
 [ 8, 10]])
>>> a @ c    *`# matrix-multiplying two 2D matrices`*
array([[20, 26],
 [56, 80]])

NumPy 强大而丰富到足以值得整本书来详细讨论;我们只是浅尝辄止。请参阅 NumPy 的文档以深入了解其众多特性。

SciPy

虽然 NumPy 包含用于处理数组的类和函数,但 SciPy 库支持更高级的数值计算。例如,虽然 NumPy 提供了一些线性代数方法,但 SciPy 提供了高级分解方法,并支持更高级的函数,例如允许第二个矩阵参数来解决广义特征值问题。一般来说,当进行高级数值计算时,安装 SciPy 和 NumPy 是个好主意。

SciPy.org 也托管了许多其他包的文档,这些包与 SciPy 和 NumPy 集成,包括提供 2D 绘图支持的Matplotlib;支持符号数学的SymPy;强大的交互式控制台和 Web 应用内核Jupyter Notebook;以及支持数据分析和建模的Pandas。您可能还想看一看用于任意精度的mpmath,以及用于更丰富功能的sagemath

额外的数值处理包

Python 社区在数值处理领域产生了更多的包。其中一些包括:

Anaconda

一个整合环境,简化了 Pandas、NumPy 和许多相关数值处理、分析和可视化包的安装,并通过其自己的 conda 包安装程序提供包管理。

gmpy2

一个模块³,支持 GMP/MPIR、MPFR 和 MPC 库,以扩展和加速 Python 在多精度算术方面的能力。

Numba

一个即时编译器,用于将 Numba 装饰的 Python 函数和 NumPy 代码转换为 LLVM。Python 中使用 Numba 编译的数值算法可以接近 C 或 FORTRAN 的速度。

PyTorch

一个开源机器学习框架。

TensorFlow

一个全面的机器学习平台,可以在大规模和混合环境中运行,使用数据流图来表示计算、共享状态和状态操作。TensorFlow 支持在集群中跨多台机器进行处理,在单台机器上跨多核 CPU、GPU 和定制 ASIC 进行处理。TensorFlow 的主要 API 使用 Python。

¹ 在技术上被更近期的、非常相似的标准754-2008取代,但在实际中仍然有用!

² Python 和 NumPy 项目多年来密切合作,Python 引入了专门为 NumPy 引入的语言特性(如@操作符和扩展切片),尽管这些新颖的语言特性(至今?)尚未在 Python 标准库的任何地方使用。

³ 最初来源于本书的一位作者的工作。

第十七章:测试、调试和优化

编程任务完成并不意味着你写完了代码;只有当代码正确运行并且性能可接受时,任务才算真正完成。测试意味着在已知条件下自动运行代码,并检查结果是否符合预期,以验证代码是否正确运行。调试则是发现不正确行为的原因并修复它们(一旦找到原因,修复通常很容易)。

优化通常作为确保性能可接受的活动的总称。优化包括基准测试(为给定任务测量性能,以确保其在可接受范围内)、性能分析(在程序中插入额外代码以识别性能瓶颈)和实际优化(消除瓶颈以提高程序性能)。显然,除非通过性能分析找到了性能瓶颈的位置,否则无法消除性能瓶颈,而要找到性能瓶颈,则需要知道是否存在性能问题(通过基准测试获知)。

本章按照开发中的自然顺序进行讨论:首先是测试,其次是调试,最后是优化。大多数程序员通常将热情集中在优化上:测试和调试经常(错误地!)被视为苦差事,而优化则被认为是有趣的。如果你只打算读本章的一部分,我们建议你阅读“开发足够快速的 Python 应用程序”,该节总结了 Python 优化的方法——与 Jackson 经典的“优化法则:法则 1. 不要优化。法则 2(仅限专家)。暂时不要优化。”相近。

所有这些任务都很重要;每个任务的讨论都至少能填满一本书。本章甚至无法详尽探讨每个相关技术;它着重于 Python 特定的方法和工具。通常情况下,为了取得最佳效果,你应该从系统分析和设计的更高层视角来解决问题,而不仅仅专注于实现(无论是 Python 还是其他编程语言的混合)。首先,可以通过学习如 Alan Dennis、Barbara Wixom 和 Roberta Roth 合著的《Systems Analysis and Design》(Wiley 出版)等优秀的通用书籍入手。

测试

在这一章中,我们区分了两种不同类型的测试:单元测试系统测试。测试是一个丰富而重要的领域:还可以做更多的区分,但我们专注于大多数软件开发者最关心的问题。许多开发者不愿意花时间进行测试,认为这是从“真正”的开发中偷走的时间,但这是短视的:在早期发现问题时更容易修复代码。花费额外的时间开发测试将充分回报自己,因为您早期发现缺陷,节省了后续软件开发周期中需要的大量调试时间。¹

单元测试和系统测试

单元测试 意味着编写和运行测试来执行单个模块,甚至更小的单元,例如类或函数。系统测试(也称为功能集成端到端测试)涉及使用已知输入运行整个程序。一些经典的测试书籍还在白盒测试(具有程序内部知识进行的测试)与黑盒测试(没有此类知识进行的测试)之间划分。这种经典观点与现代单元与系统测试的观点类似,但并非完全相同。

单元测试和系统测试有不同的目标。单元测试随着开发进程同时进行;您可以并且应该在开发每个单元时进行测试。一种比较现代的方法(首次在 1971 年由杰拉尔德·温伯格在他的经典著作 计算机编程心理学 [Dorset House] 中提出)被称为测试驱动开发(TDD):对于程序必须具备的每个特性,您首先编写单元测试,然后再编写实现该特性并使测试通过的代码。TDD 可能看起来颠倒了,但它有优势;例如,它确保您不会漏掉某些功能的单元测试。这种方法有助于您首先专注于某个函数、类或方法应该完成的确切任务,仅在此后处理如何实现该函数、类或方法。沿着 TDD 的思路的一项创新是行为驱动开发(BDD)

要测试一个单元——它可能依赖于尚未完全开发的其他单元——通常需要编写存根,也称为模拟:各种单元接口的虚假实现,在需要测试其他单元的情况下提供已知的正确响应。模拟模块(Python 标准库的一部分,位于包 unittest 中)帮助您实现这些存根。

系统测试在后面进行,因为它需要系统的存在,且至少有一部分系统功能(基于单元测试)被认为是正常工作的。系统测试提供了一种健壮性检查:程序中的每个模块都正常工作(通过单元测试),但整个程序是否正常工作呢?如果每个单元都没问题,但系统不正常,那么问题就在于单元之间的集成——单元的协作方式。因此,系统测试也被称为集成测试。

系统测试类似于在生产环境中运行系统,除了你提前固定输入,这样你可能发现的问题就容易重现了。系统测试中的故障成本低于生产环境中的故障,因为系统测试的输出不会用于决策、服务客户、控制外部系统等等。相反,系统测试的输出会系统地与系统应该生成的输出进行比较,前提是已知输入。其目的是以廉价且可重现的方式,找出程序应该做的事情和程序实际的事情之间的差异。

系统测试发现的故障(就像生产环境中的系统故障一样)可能会暴露单元测试中的缺陷,以及代码中的缺陷。单元测试可能不足够:一个模块的单元测试可能未能测试模块的所有必需功能。在这种情况下,需要加强单元测试。你更改代码以修复问题之前,先做这个,然后运行新的增强单元测试,确认它们现在显示了问题。然后修复问题,再次运行单元测试,以确认它们不再显示问题。最后,重新运行系统测试,以确认问题确实已经解决。

错误修复最佳实践

这一最佳实践是我们毫无保留推荐的测试驱动设计的一个具体应用:在添加能够揭示错误的单元测试之前,绝不要修复一个错误。这为防止软件回归错误提供了一个很好的、廉价的保险。

通常,系统测试中的故障会揭示开发团队内部的通信问题:³一个模块正确实现了某种功能,但另一个模块期望不同的功能。这种问题(严格意义上的集成问题)在单元测试中很难定位。在良好的开发实践中,单元测试必须经常运行,因此它们运行速度必须很快。因此,在单元测试阶段,至关重要的是每个单元都能假设其他单元是正常工作且按预期工作的。

在开发的相对后期运行单元测试时,如果系统架构是分层的(这是一种常见且合理的组织方式),可以揭示集成问题。在这样的架构中,低级模块不依赖于其他模块(除了库模块,通常可以假设是正确的),因此这些低级模块的单元测试如果完备的话,就足以提供正确性的信心。高级模块依赖于低级模块,因此也依赖于正确理解每个模块期望和提供的功能。对高级模块进行完整的单元测试(使用真正的低级模块,而不是存根)可以测试模块之间的接口,以及高级模块本身的代码。

因此,高级模块的单元测试有两种运行方式。在开发的早期阶段,当低级模块尚未准备好或者后来仅需要检查高级模块的正确性时,使用低级模块的存根来运行测试。在开发的后期阶段,还会定期使用真正的低级模块运行高级模块的单元测试。通过这种方式,可以检查从高级到低级的整个子系统的正确性。即使在这种有利的情况下,你仍然需要运行系统测试,以确保检查系统的所有功能是否被使用,并且没有忽略模块之间的任何接口。

系统测试类似于以正常方式运行程序。你只需要特殊支持来确保提供已知输入并捕获预期输出以进行比较。对于在文件上执行 I/O 的程序,这很容易,但对于依赖 GUI、网络或其他与外部实体通信的程序来说则更为困难。要模拟这些外部实体并使它们可预测和完全可观察,通常需要依赖平台相关的基础设施。系统测试的另一个有用的支持基础设施是测试框架,用于自动运行系统测试,并记录成功和失败。这样的框架还可以帮助测试人员准备一系列已知输入及其对应的预期输出。

这些目的都有免费和商业程序,并且通常不依赖于系统测试中使用的编程语言。系统测试与经典的黑盒测试密切相关:这种测试独立于被测试系统的实现(特别是独立于用于实现的编程语言)。而测试框架通常依赖于它们运行的操作系统平台,因为它们执行的任务是平台相关的。这些任务包括:

  • 运行带有指定输入的程序

  • 捕获它们的输出

  • 模拟/捕获 GUI、网络和其他进程间通信的 I/O

由于系统测试的框架取决于平台,而不是编程语言,因此我们在本书中不再详细介绍它们。有关 Python 测试工具的全面列表,请参阅 Python wiki

doctest 模块

doctest 模块旨在让您在代码的文档字符串中创建良好的示例,检查这些示例确实生成了文档字符串所展示的结果。doctest 通过查找文档字符串中的交互式 Python 提示符 >>> 来识别这些示例,后跟同一行上的 Python 语句,并且语句的预期输出在接下来的行中。

开发模块时,请保持文档字符串的更新,并用示例丰富它们。每当模块的一部分(例如函数)准备好或部分准备好时,请养成向其文档字符串添加示例的习惯。将模块导入交互式会话,并使用刚开发的部分提供示例,包括典型案例、极限案例和失败案例的混合。仅为此特定目的使用 from module import *,以便您的示例不会为每个模块提供的名称加前缀 module。将交互式会话复制并粘贴到编辑器中的文档字符串中,调整任何错误,然后几乎完成。

现在您的文档已经丰富了示例,读者将更容易地跟随它(假设您选择了一些良好的示例混合,明智地加入非示例文本)。确保您对整个模块以及导出的每个函数、类和方法都有文档字符串和示例。您可以选择跳过名称以 _ 开头的函数、类和方法,因为(正如它们的名称所示)它们是私有实现细节;默认情况下,doctest 会忽略它们,您的模块源代码的读者也应该如此。

使您的示例符合现实

与代码不匹配的示例比毫无用处更糟糕。文档和注释仅在与现实匹配时才有用;“撒谎”的文档和注释可能会造成严重损害。

随着代码的更改,文档字符串和注释通常会变得过时,从而成为误导信息,不利于源代码的读者。最好的选择是根本没有注释和文档字符串,尽管这样的选择会很差劲,而不是有虚假信息的注释和文档字符串。doctest 可以通过运行和检查文档字符串中的示例来帮助您。如果 doctest 运行失败,应提示您审查包含失败示例的文档字符串,从而提醒您保持整个文档字符串的更新。

在模块源的末尾插入以下片段:

`if` __name__ == '__main__':
 `import` doctest
    doctest.testmod()

当你将模块作为主程序运行时,此代码会调用 doctest 模块的 testmod 函数。testmod 会检查文档字符串(模块的文档字符串以及所有公共函数、类和方法的文档字符串)。在每个文档字符串中,testmod 会找到所有示例(通过查找解释器提示符 >>> 的出现,可能会先于空格),并运行每个示例。testmod 检查每个示例的结果是否与示例之后的文档字符串中给出的输出相匹配。在出现异常时,testmod 会忽略回溯,并只检查预期和观察到的错误消息是否相等。

当一切顺利时,testmod 会悄无声息地终止。否则,它会输出关于失败的示例的详细信息,显示预期和实际输出。示例 17-1 展示了 doctest 在 mod.py 模块上运行的典型示例。

示例 17-1. 使用 doctest
*`"""`*
*`This module supplies a single function reverse_words that reverses`*
*`a string word by word.`*

*`>>> reverse_words('four score and seven years')`*
*`'years seven and score four'`*
*`>>> reverse_words('justoneword')`*
*`'justoneword'`*
*`>>> reverse_words('')`*
*`''`*

*`You must call reverse_words with a single argument, a string:`*

*`>>> reverse_words()`*
*`Traceback (most recent call last):`*
 *`...`*
*`TypeError: reverse_words() missing 1 required positional argument: 'astring'`*
*`>>> reverse_words('one', 'another')`*
*`Traceback (most recent call last):`*
 *`...`*
*`TypeError: reverse_words() takes 1 positional argument but 2 were given`*
*`>>> reverse_words(1)`*
*`Traceback (most recent call last):`*
 *`...`*
*`AttributeError: 'int' object has no attribute 'split'`*
*`>>> reverse_words('𝒰𝓷𝓲𝓬𝓸𝓭𝓮 is all right too')`*
*`'too right all is 𝒰𝓷𝓲𝓬𝓸𝓭𝓮'`*

*`As a side effect, reverse_words eliminates any redundant spacing:`*

*`>>> reverse_words('with  redundant   spacing')`*
*`'spacing redundant with'`*
*`"""`*

`def` reverse_words(astring):
    words = astring.split()
    words.reverse()
    `return` ' '.join(words)

`if` __name__ == '__main__':
    `import` doctest
    doctest.testmod()

在此模块的文档字符串中,我们从文档字符串中剪切了回溯,并用省略号 (...) 替换了它们:这是一个良好的做法,因为 doctest 会忽略回溯,回溯对于失败的情况没有任何解释价值。除了这个剪切之外,文档字符串是交互式会话的复制粘贴,加上一些解释性文本和空行以提高可读性。将此源保存为 mod.py,然后用 python mod.py 运行它。它不会产生任何输出,这意味着所有示例都运行正确。尝试 python mod.py -v 以获取它尝试的所有测试情况,并在最后获取详细摘要。最后,修改模块文档字符串中的示例结果,使其不正确,以查看 doctest 为错误示例提供的消息。

虽然 doctest 并非用于通用单元测试,但很容易诱人将其用于此目的。在 Python 中进行单元测试的推荐方法是使用测试框架,如 unittest、pytest 或 nose2(在下面的章节中介绍)。但是,使用 doctest 进行单元测试可能更容易且更快速,因为它几乎只需要从交互式会话中复制粘贴。如果你需要维护一个缺乏单元测试的模块,通过 doctest 将这些测试添加到模块中是一个合理的短期折衷方案,作为第一步。与其完全没有单元测试,不如只使用基于 doctest 的单元测试,否则你可能会决定从一开始就用 unittest 正确地设置测试需要花费太长时间。⁴

如果您决定使用 doctest 进行单元测试,请不要将额外的测试内容塞入模块的文档字符串中。这样会破坏文档字符串,使其过长且难以阅读。保留文档字符串中正确数量和类型的示例,严格用于解释目的,就像单元测试不在考虑范围内一样。而是将额外的测试内容放入模块的全局变量中,一个名为 test 的字典。test 中的键是用作任意测试名称的字符串;相应的值是 doctest 获取并像使用文档字符串一样使用的字符串。test 中的值也可以是函数和类对象,在这种情况下,doctest 会检查它们的文档字符串以运行测试。这种后一种功能是运行 doctest 的便捷方式,可用于具有私有名称的对象,这些对象默认情况下 doctest 会跳过。

doctest 模块还提供了两个函数,这些函数基于 doctest 返回 unittest.TestSuite 类的实例,因此您可以将这些测试集成到基于 unittest 的测试框架中。有关此高级功能的完整文档可在线查看online

unittest 模块

unittest 模块是由 Kent Beck 为 Smalltalk 最初开发的单元测试框架的 Python 版本。类似的,广泛使用的框架版本也存在于许多其他编程语言中(例如,Java 的 JUnit 包),通常被统称为 xUnit。

要使用 unittest,请不要将测试代码放在与被测试模块相同的源文件中:而是为每个要测试的模块编写一个单独的测试模块。一个流行的约定是将测试模块命名为要测试的模块的名称,以'test_'为前缀,并将其放在名为test的源目录的子目录中。例如,mod.py的测试模块可以是test/test_mod.py。一个简单而一致的命名约定有助于编写和维护辅助脚本,以查找并运行包的所有单元测试。

在模块的源代码与其单元测试代码之间进行分离可以更轻松地重构模块,甚至可能在不干扰单元测试代码的情况下在 C 中重新编码一些功能。知道test_mod.py保持不变,无论您对mod.py做了哪些更改,都可以增强您对在更改后test_mod.py中通过测试指示mod.py仍然正确工作的信心。

单元测试模块定义了 unittest 的 TestCase 类的一个或多个子类。每个这样的子类通过定义测试用例方法来指定一个或多个测试用例:这些方法可以无参数调用,并且其名称以 test 开头。

通常,子类会重写 setUp 方法,这是框架在每个测试用例之前调用以准备新实例的方法,并且通常也会重写 tearDown 方法,这是框架在每个测试用例之后调用以清理事物的方法;整个设置/清理安排被称为测试夹具

每个测试用例都在TestCase类的实例上调用以assert开头的方法,以表达测试必须满足的条件。unittest 在TestCase子类内以任意顺序运行测试用例方法,每个测试用例在子类的新实例上运行,在每个测试用例之前运行setUp,在每个测试用例之后运行tearDown

使用setUp时需要使用addCleanup

setUp传播异常时,tearDown不会执行。因此,当setUp准备了几个需要最终清理的事情,并且一些准备步骤可能导致未捕获的异常时,不应依赖tearDown进行清理工作。相反,在每个准备步骤成功之后立即调用self.addCleanup(*f*, **a*, ***k*),传递一个清理可调用的*f*(以及可选的*f*的位置参数和命名参数)。在这种情况下,*f*(**a*, ***k*)会在测试用例之后被调用(当setUp不传播异常时在tearDown之后,但无条件地,即使setUp传播异常时也会调用),因此必要的清理代码始终会执行。

unittest 提供其他功能,例如将测试用例分组到测试套件中,每个类和每个模块的固件,测试发现以及其他更高级的功能。除非您正在构建一个基于 unittest 的自定义单元测试框架,或者至少正在为同样复杂的软件包构建复杂的测试程序,否则您不需要这些额外功能。在大多数情况下,本节介绍的概念和细节足以执行有效和系统化的单元测试。示例 17-2 演示了如何使用 unittest 为示例 17-1 的模块mod.py提供单元测试。这个例子纯粹是为了演示目的,使用 unittest 执行与示例 17-1 中在文档字符串中使用 doctest 作为示例的完全相同的测试。

示例 17-2. 使用 unittest
"""This module tests function reverse_words
provided by module mod.py."""
`import` unittest
`import` mod

`class` ModTest(unittest.TestCase):

    `def` testNormalCaseWorks(self):
        self.assertEqual(
            'years seven and score four',
            mod.reverse_words('four score and seven years'))

    `def` testSingleWordIsNoop(self):
        self.assertEqual(
            'justoneword',
            mod.reverse_words('justoneword'))

    `def` testEmptyWorks(self):
        self.assertEqual('', mod.reverse_words(''))

    `def` testRedundantSpacingGetsRemoved(self):
        self.assertEqual(
            'spacing redundant with',
            mod.reverse_words('with   redundant   spacing'))

    `def` testUnicodeWorks(self):
        self.assertEqual(
            'too right all is 𝒰𝓷𝓲𝓬𝓸𝓭𝓮'
            mod.reverse_words('𝒰𝓷𝓲𝓬𝓸𝓭𝓮 is all right too'))

    `def` testExactlyOneArgumentIsEnforced(self):
        `with` self.assertRaises(TypeError):
            mod.reverse_words('one', 'another')

    `def` testArgumentMustBeString(self):
        `with` self.assertRaises((AttributeError, TypeError)):
            mod.reverse_words(1)

`if` __name__=='__main__':
    unittest.main()

使用python test/test_mod.py(或者等价地,python -m test.test_mod)运行此脚本比使用python mod.py运行 doctest 稍微复杂一点。test_mod.py对于它运行的每个测试用例输出一个.(点),然后是一行破折号分隔线,最后是一个摘要行,例如“在 0.110s 内运行了 7 个测试”,如果每个测试都通过,则最后一行是“OK”。

每个测试用例方法都调用一个或多个以assert开头的方法。在这里,没有方法调用超过一个这样的调用;然而,在更复杂的情况下,从单个测试用例方法调用多个assert方法是常见的。

即使在这种简单的情况下,一个小方面也表明,对于单元测试,unittest 比 doctest 更强大、更灵活。 在 testArgumentMustBeString 方法中,我们将一对异常类作为 assertRaises 的参数传递,这意味着我们接受任何一种异常。 test_mod.py 因此将这些视为 mod.py 的有效多个实现。 它接受 示例 17-1 中的实现,该示例尝试在其参数上调用 split 方法,因此在用非字符串参数调用时引发 AttributeError。 但是,它也接受了一个不同的假设实现,当使用错误类型的参数调用时引发 TypeError。 使用 doctest 可以编写此类检查,但这将是笨拙和不明显的,而 unittest 则使其变得简单和自然。

这种灵活性对于现实生活中的单元测试至关重要,某种程度上它们是其模块的可执行规范。 你可以悲观地认为,对于测试的灵活性的需要意味着你正在测试的代码的接口没有定义好。 但是,最好将接口视为具有对于实现者来说有用的灵活性:在情况 X(在这个例子中传递给函数 reverse_words 的无效类型的参数)下,两种情况(引发 AttributeError 或 TypeError)都是允许的。

因此,具有这两种行为之一的实现都是正确的:实现者可以根据诸如性能和清晰度等考虑因素之间进行选择。 从这种方式来看——作为其模块的可执行规范(现代观点,以及测试驱动开发的基础),而不是作为严格限定在特定实现上的白盒测试(如某些传统测试分类中)——单元测试成为软件开发过程中更加重要的组成部分。

TestCase 类

使用 unittest,您可以通过扩展 TestCase 编写测试用例,添加不带参数的方法,其名称以 test 开头。 这些测试用例方法反过来调用从 TestCase 继承的方法,其名称以 assert 开头,以指示必须满足的条件,以使测试成功。

TestCase 类还定义了两个方法,您的类可以选择性地覆盖以分组执行每个测试用例方法之前和之后的操作。 这并没有耗尽 TestCase 的功能,但除非您正在开发测试框架或执行其他高级任务,否则您不需要其余部分。 表 17-1 列出了 TestCase 实例 t 的经常调用方法。

表 17-1. TestCase 实例 t 的方法

a⁠s⁠s⁠e⁠r⁠t⁠A⁠l⁠m⁠o⁠s⁠t​E⁠q⁠u⁠a⁠l assertAlmostEqual(first, second, places=7, msg=None) 当first != second到小数点 places 位时失败并输出 msg;否则,什么也不做。此方法比 assertEqual 更适合比较浮点数,因为它们是可能在不重要的小数位数上有所不同的近似值。在测试失败时生成诊断消息时,unittest 会假定first是期望的值,second是观察到的值。
assertEqual assertEqual(first, second, msg=None) 当first != second时失败并输出 msg;否则,什么也不做。在测试失败时生成诊断消息时,unittest 会假定first是期望的值,second是观察到的值。
assertFalse assertFalse(condition, msg=None) 当condition为真时失败并输出 msg;否则,什么也不做。
assertNotAlm⁠o⁠s⁠t​E⁠q⁠u⁠al assertNotAlmostEqual(first, second, places=7, msg=None) 当first == second到小数点 places 位时失败并输出 msg;否则,什么也不做。
assertNotEqual assertNotEqual(first, second, msg=None) 当first == second时失败并输出 msg;否则,什么也不做。

| assertRaises | assertRaises(exceptionSpec, callable, args, **kwargs) 调用callable*(args, **kwargs)。调用未引发任何异常时失败。当调用引发不符合exceptionSpec的异常时,assertRaises 传播异常。当调用引发符合exceptionSpec*的异常时,assertRaises 什么也不做。exceptionSpec可以是异常类或类元组,就像try/except语句中except子句的第一个参数一样。

使用 assertRaises 的首选方式是作为上下文管理器——也就是说,在with语句中:

`with` self.assertRaises(*`exceptionSpec`*):
 *`# ...a block of code...`*

这里,缩进在with语句中的代码块执行,而不仅仅是调用callable带有特定参数。期望(避免构造失败)是代码块引发符合给定异常规范(异常类或类元组)的异常。这种替代方法比传递可调用对象更通用和可读。 |

| a⁠s⁠s⁠e⁠r⁠t⁠R⁠a⁠i⁠s⁠e⁠s​R⁠e⁠g⁠e⁠x | assertRaisesRegex(exceptionSpec, expected_regex, callable, args, **kwargs) 就像assertRaises,但还检查异常的错误消息是否与regex*匹配;regex可以是正则表达式对象或要编译成一个的字符串模式,测试(当引发预期异常时)通过调用 RE 对象上的 search 检查错误消息。

就像assertRaises一样,assertRaisesRegex最好作为上下文管理器使用——也就是说,在with语句中:

`with` self.assertRaisesRegex(*`exceptionSpec``,` `regex`*):
 *`# ...a block of code...`*

|

enterContext enterContext(ctx_manager) 3.11+ 在 TestCase.setup() 方法中使用此调用。返回调用 ctx_manager.enter 的值,并将 ctx_manager.exit 添加到清理方法列表中,框架在 TestCase 清理阶段运行这些方法。

| fail | fail(msg=None) 无条件失败并输出 msg。例如代码段可能是:

`if` `not` complex_check_if_its_ok(some, thing):
    self.fail(
      'Complex checks failed on'
      f' {some}, {thing}'
    )

|

setUp setUp() 框架在调用每个测试用例方法之前调用 t.setUp()。在 TestCase 中的 setUp 不执行任何操作;它存在只是为了让你的类在需要为每个测试准备一些东西时重写这个方法。
subTest subTest(msg*=*None, **k) 返回一个上下文管理器,可以在测试方法内定义测试的部分。当一个测试方法使用不同的参数多次运行相同的测试时,使用 subTest 可以确保所有情况都会运行,即使其中一些失败或引发异常。
tearDown tearDown() 框架在每个测试用例方法后调用 t.tearDown()。TestCase 类中的 tearDown 不执行任何操作;它存在只是为了让你的类在需要在每个测试后执行一些清理操作时重写这个方法。

此外,一个 TestCase 实例维护一个 清理函数 的后进先出(LIFO)栈。当你的一个测试中的代码(或者在 setUp 中)执行需要清理的操作时,请调用 self.addCleanup,传递一个清理可调用函数 f,以及可选的位置参数和命名参数 f。要执行堆叠的清理操作,你可以调用 doCleanups;然而,框架本身会在 tearDown 后调用 doCleanups。表 17-2 列出了一个 TestCase 实例 t 的两个清理方法的签名。

表 17-2. TestCase 实例 t 的清理方法

addCleanup addCleanup(func, *a, **k) 将 (func, a, k) 添加到清理列表的末尾。

| doCleanups | doCleanups() 执行所有已堆叠的清理操作。实质上等同于:

`while` self.list_of_cleanups:
    func, a, k = self.list_of_cleanups.pop()
    func(*a, **k)

对于假设的栈 self.list_of_cleanups,当然还包括错误检查和报告。 |

单元测试处理大量数据

单元测试必须快速,因为开发过程中应经常运行它们。因此,在可能的情况下,对模块的每个方面进行小数据量的单元测试。这样可以使单元测试更快,并允许您将数据嵌入到测试的源代码中。当您测试从文件对象读取或写入的函数时,对于文本文件使用 io.TextIO 类的实例(或对于二进制文件使用 io.BytesIO 类的实例,如 “内存中的文件:io.StringIO 和 io.BytesIO” 中所述),以获得包含数据的内存中文件:这种方法比写入磁盘快,并且不需要清理(在测试后删除磁盘文件)。

在罕见的情况下,可能无法在不提供和/或比较大量数据的情况下执行模块的功能。在这种情况下,您的单元测试必须依赖于辅助的外部数据文件来保存供模块测试所需的数据以及需要比较的输出数据。即便如此,我们通常建议使用上述 io 类的实例,而不是直接让被测试的模块执行真实的磁盘 I/O。更重要的是,我们强烈建议通常使用存根来单元测试与外部实体交互的模块,例如数据库、GUI 或通过网络与其他程序交互的模块。使用存根而不是真实的外部实体进行测试可以更容易地控制测试的各个方面。另外,再次强调,测试单元的运行速度非常重要,使用存根执行模拟操作比使用真实操作更快。

通过提供种子使测试的随机性可重现

如果您的代码使用伪随机数(例如在 “random 模块” 中介绍的内容),您可以通过确保其“随机”行为是可重现的,使其更容易测试:具体来说,确保您的测试可以轻松调用 random.seed 并使用已知的参数,以便生成的伪随机数是完全可预测的。当您使用伪随机数来设置测试的输入时,这一点同样适用:默认应使用已知的种子来生成大多数测试,仅在特定技术(如 fuzzing)中才更改种子。

使用 nose2 进行测试

nose2 是一个可通过 pip 安装的第三方测试实用工具和框架,它构建在 unittest 之上,提供额外的插件、类和装饰器,以帮助编写和运行测试套件。nose2 会在项目中“嗅探”出测试用例,通过查找以 test.py* 命名的文件中存储的 unittest 测试用例来构建其测试套件。

这里有一个示例,展示了如何使用 nose2 的 params 装饰器将数据参数传递给测试函数:

`import` unittest
`from` nose2.tools `import` params

`class` TestCase(unittest.TestCase):

    @params((5, 5), (-1, 1), ('a', `None`, TypeError))
    `def` test_abs_value(self, x, expected, should_raise=`None`):
        `if` should_raise `is` `not` `None`:
            `with` self.assertRaises(should_raise):
                abs(x)
        `else`:
            assert abs(x) == expected

nose2 还包括额外的装饰器,如上下文管理器来定义测试函数组,并且一个插件框架来提供测试元函数,例如日志记录、调试和覆盖报告。更多信息请参阅 在线文档

使用 pytest 进行测试

pytest 模块是一个可通过 pip 安装的第三方单元测试框架,它会检查项目的模块以查找 test_.py* 或 *_test.py 文件中以 test 开头的方法名称的测试用例。与内置的 unittest 框架不同,pytest 不要求测试用例扩展任何测试类层次结构;它运行发现的测试方法,这些方法使用 Python 的 assert 语句来确定每个测试的成功或失败。⁵ 如果测试引发除 AssertionError 外的任何异常,则表示测试中存在错误,而不仅仅是简单的测试失败。

pytest 提供了一些辅助方法和装饰器来简化编写单元测试,而不是使用测试用例类的层次结构。最常用的方法列在 表 17-3 中;请参阅 在线文档 获取更完整的方法列表和可选参数。

表 17-3. 常用 pytest 方法

| approx | approx(float_value) 用于支持对浮点数值进行比较的断言。float_value 可以是单个值或值序列:

`assert` 0.1 + 0.2 == approx(0.3)
`assert` [0.1, 0.2, 0.1+0.2] == approx([0.1, 0.2, 0.3])

|

fail fail(failure_reason) 强制当前测试失败。比注入 assert False 语句更明确,但在其他方面等效。
raises raises(expected_exception, match=regex_match) 上下文管理器,除非其上下文引发了一个 exc,使得 isinstance(exc, expected_exception) 为真。当给定 match 时,除非 exc 的字符串表示也与 re.search(match, str(exc)) 匹配,否则测试失败。
skip skip(skip_reason) 强制跳过当前测试;例如,在先前的测试已经失败时使用。
warns warns(expected_warning, match*=regex_match*) 类似于 raises,用于包装测试预期警告是否被触发的代码。

pytest.mark 子包含有装饰器,可以用来“标记”测试方法,赋予其额外的测试行为,包括在 表 17-4 中列出的方法。

表 17-4. pytest.mark 子包中的装饰器

| parametrize | @parametrize(args_string, arg_test_values) 调用装饰的测试方法,将逗号分隔列表 args_string 中命名的参数设置为 arg_test_values 中每个参数元组的值。

以下代码运行 test_is_greater 两次,一次为 x=1, y=0,expected=True;另一次为 x=0, y=1,expected=False

@pytest.mark.parametrize
("x,y,expected", 
 [(1, 0, `True`), (0, 1, `False`)])
`def` test_is_greater(x, y, expected):
`assert` (x > y) == expected

|

| skip, skipif | @skip(skip_reason), @skipif(condition, skip_reason)

可选地根据一些全局条件跳过测试方法。 |

调试

由于 Python 的开发周期很快,通常调试的最有效方式是编辑代码,在关键点输出相关信息。Python 提供了许多方法,让您的代码探索自身状态,提取可能与调试相关的信息。具体支持此类探索的是 inspect 和 traceback 模块,这也被称为反射内省

一旦您有与调试相关的信息,打印通常是显示它的自然方式(pprint,详见“pprint 模块”,通常也是一个很好的选择)。然而,将调试信息记录到文件中通常更好。对于无人看管运行的程序(例如服务器程序),记录日志非常有用。显示调试信息就像显示其他信息一样,如第十一章所述。记录此类信息就像写入文件一样(在同一章节中讨论);但是,Python 的标准库提供了一个记录模块,详见“logging 模块”,以帮助处理此频繁任务。如表 8-3 中所述,通过在 sys 模块中重新绑定 excepthook,您的程序可以在以传播异常终止之前记录错误信息。

Python 还提供了启用交互式调试的钩子。pdb 模块提供了一个简单的文本模式交互式调试器。其他强大的 Python 交互式调试器包含在诸如 IDLE 和各种商业软件提供的 IDE 中,如“Python 开发环境”所述;本书不进一步介绍这些高级调试器。

在您开始调试之前

在您开始漫长的调试探索之前,请确保使用第二章中提到的工具彻底检查了您的 Python 源代码。这些工具仅捕获代码中的一部分 bug,但比交互式调试快得多:它们的使用绝对物有所值。

再次在开始调试会话之前,请确保所有涉及的代码都有充分的单元测试覆盖,如“unittest 模块”中所述。正如本章前面提到的,一旦找到 bug,修复之前,请为您的单元测试套件(或者如果必要,系统测试套件)添加一两个测试,这些测试如果从一开始就存在,本应能发现该 bug,并再次运行测试以确认它们现在能够揭示和隔离该 bug;只有在完成这些步骤之后,才能继续修复该 bug。定期遵循此过程将帮助您学会编写更好、更彻底的测试,确保最终拥有更健壮的测试套件,并对代码的持久正确性更有信心。

记住,即使 Python 及其标准库以及你喜欢的任何 IDE 提供了所有的设施,调试仍然很难。在你开始设计和编码之前,请考虑写并运行大量的单元测试,并保持你的设计和代码简单,以尽量减少你需要进行的调试工作!Brian Kernighan 提供了这个经典建议:“调试比一开始编写代码难两倍。因此,如果你把代码写得越聪明,你就越不能够调试它。”这也是为什么“聪明”一词在描述 Python 代码或程序员时并不是一个正面词语的部分。

inspect 模块

inspect 模块提供了获取各种对象信息的函数,包括 Python 调用堆栈(记录当前正在执行的所有函数调用)和源文件。inspect 的最常用函数列在表 17-5 中。

表 17-5. inspect 模块的有用函数

currentframe currentframe() 返回当前函数的帧对象(调用 currentframe 的函数的调用者)。formatargvalues(*getargvalues(currentframe())),例如,返回表示调用函数参数的字符串。
getargspec, formatargspec getargspec(f) -3.11 在 Python 3.5 中弃用,在 Python 3.11 中移除。检查可调用对象的前向兼容方法是调用 inspect.signature(f)并使用 inspect.Signature 类的实例(在以下小节中介绍)。

| get⁠a⁠r⁠g​v⁠a⁠l⁠ues, formatargvalues | getargvalues(f) f是一个帧对象,例如,是 sys 模块中 _getframe 函数调用(在“帧类型”中涵盖)的结果,或者是 inspect 模块中 currentframe 函数的调用结果。getargvalues 返回一个具有四个条目的命名元组:(args, varargs, keywords, locals)。argsf函数参数名称的序列。varargs是形如a的特殊参数的名称,或者当f*函数没有这样的参数时为None**。keywords是形如k的特殊参数的名称,或者当f函数没有这样的参数时为None**。localsf的本地变量字典。由于参数特别是本地变量,因此可以通过将参数名称与locals字典索引结合来获取每个参数的值。

formatargvalues 接受一个到四个参数,与 getargvalues 返回的命名元组的项目相同,并返回带有此信息的字符串。formatargvalues(getargvalues(f))返回带有f*参数的字符串,用括号括起来,并以调用语句中使用的命名形式表示。例如:

`def` f(x=23):
    `return` inspect.currentframe()
print(inspect.formatargvalues(
      *inspect.getargvalues(f())))
*`# prints:`* *`(x=23)`*

|

getdoc getdoc(obj) 返回obj的文档字符串,一个多行字符串,其中制表符扩展为空格,并且每行的冗余空格被剥离。

| getfile, getsourcefile | getfile(obj),getsourcefile(obj)

getfile 返回定义 obj 的二进制或源文件的名称。在无法确定文件时(例如,obj 是内置对象),会引发 TypeError。getsourcefile 返回定义 obj 的源文件的名称;当只能找到二进制文件而非相应的源文件时,会引发 TypeError。 |

| getmembers | getmembers(obj, filter=None) 返回 obj 的所有属性(成员),包括数据和方法(包括特殊方法),以 (name, value) 对的排序列表形式返回。当 filter 不为 None 时,仅返回过滤器在属性的 value 上调用时返回真值的属性,相当于:

((n, v) `for` n, v `in` getmembers(obj) 
        `if` filter(v))

|

getmodule getmodule(obj) 返回定义 obj 的模块,或者在无法确定时返回 None

| getmro | getmro(c) 返回类 c 的基类和祖先类的方法解析顺序(详见 “继承”)。元组的第一项是 c,每个类在元组中只出现一次。例如:

`class` A: `pass`
`class` B(A): `pass`
`class` C(A): `pass`
`class` D(B, C): `pass`
`for` c `in` inspect.getmro(D):
    print(c.__name__, end=' ')
*`# prints: D B C A object`*

|

| getsource, getsourcelines | getsource(obj),getsourcelines(obj)

getsource 返回一个多行字符串,这是 obj 的源代码,并在无法确定或获取时引发 IOError。getsourcelines 返回一个对:第一项是 obj 的源代码(行列表),第二项是其文件中第一行的行号。 |

| isbuiltin, isclass, iscode, isframe,

isfunction,

ismethod,

ismodule,

isroutine | isbuiltin(obj) 等。这些函数每个接受一个参数 obj 并在 obj 是函数名中指示的类型时返回 True。接受的对象分别是:内置(C 编码)函数、类对象、代码对象、帧对象、Python 编码函数(包括 lambda 表达式)、方法、模块,以及对于 isroutine,所有方法或函数,无论是 C 编码还是 Python 编码。这些函数通常用作 getmembers 的过滤器参数。 |

stack stack(context=1) 返回一个六项元组列表。第一个元组关于堆栈的调用者,第二个关于调用者的调用者,依此类推。每个元组中的项是:帧对象、文件名、行号、函数名、当前行周围上下文源代码行列表、当前行在列表中的索引。

检视可调用对象

要检视可调用对象的签名,调用 inspect.signature(f),它返回一个 inspect.Signature 类的实例 s

s.parameters 是一个字典,将参数名映射到 inspect.Parameter 实例。调用 s.bind(*a, **k) 将所有参数绑定到给定的位置参数和命名参数,或者调用 s.bind_partial(*a, **k) 绑定它们的子集:每个调用都返回一个 inspect.BoundArguments 实例 b

欲了解如何通过这些类及其方法检视可调用对象的签名的详细信息和示例,请参阅 PEP 362

使用 inspect 的示例

假设在程序的某个地方执行了如下语句:

x.f()

并意外地接收到一个 AttributeError,通知您对象 x 没有名为 f 的属性。这意味着对象 x 不如您所预期,因此您希望在确定为什么 x 是那样以及您应该对其采取什么行动之前更多地了解 x。一个简单的第一步方法可能是:

print(type(x), x)
*`# or, from v3.8, use an f-string with a trailing '=' to show repr(x)`*
*`# print(f'{x=}')`*
x.f()

这通常会提供足够的信息以继续进行;或者您可以将其更改为 print(type(x), dir(x), x) 以查看 x 的方法和属性是什么。但如果这还不够,将语句更改为:

`try`:
    x.f()
`except` AttributeError:
 `import` sys`,` inspect
    print(f'x is type {type(x).__name__}, ({x!r})', file=sys.stderr)
    print("x's methods are:", file=sys.stderr, end='')
    `for` n, v `in` inspect.getmembers(x, callable):
        print(n, file=sys.stderr, end=' ')
    print(file=sys.stderr)
 `raise`

本示例适当地使用 sys.stderr(在 表 8-3 中有详细讲述),因为它显示与错误相关的信息,而不是程序结果。inspect 模块的 getmembers 函数获取模块 x 上所有可用方法的名称以显示它们。如果您经常需要此类诊断功能,可以将其打包成一个单独的函数,例如:

import sys, inspect
`def` show_obj_methods(obj, name, show=sys.stderr.write):
    show(f'{name} is type {type(obj).__name__}({obj!r})\n')
    show(f"{name}'s methods are: ")
    `for` n, v `in` inspect.getmembers(obj, callable):
       show(f'{n} ')
    show('\n')

然后示例变成:

`try`:
    x.f()
`except` AttributeError:
    show_obj_methods(x, 'x')
 `raise`

在旨在用于诊断和调试目的的代码中,良好的程序结构和组织同样必不可少,正如在实现程序功能的代码中一样。另请参见 “assert 语句” 以获取定义诊断和调试函数时的良好技巧。

traceback 模块

traceback 模块允许您提取、格式化和输出未捕获异常通常产生的回溯信息。默认情况下,此模块复制 Python 用于回溯的格式。然而,traceback 模块还允许您精细控制。该模块提供许多函数,但在典型用法中,您只需要其中一个:

print_exc print_exc(limit=**None**, file=sys.stderr) 从异常处理程序中调用 print_exc,或从直接或间接被异常处理程序调用的函数中调用 print_excprint_exc 输出 Python 为未捕获异常输出到 stderr 的回溯。当 limit 是整数时,print_exc 仅输出限定的回溯嵌套层级。例如,在异常处理程序中,如果您希望引发诊断消息,就像异常已传播一样,但停止异常进一步传播(以使程序继续运行且不涉及更多处理程序),则调用 traceback.print_exc()

pdb 模块

pdb 模块利用 Python 解释器的调试和跟踪钩子来实现一个简单的命令行交互式调试器。pdb 允许您设置断点,逐步执行和跳转到源代码,检查堆栈帧等等。

要在 pdb 控制下运行代码,请导入 pdb,然后调用 pdb.run,传递作为唯一参数的要执行的代码字符串。要使用 pdb 进行事后调试(在交互提示处通过传播异常终止的代码调试),调用 pdb.pm() 而无需参数。要直接从应用程序代码中触发 pdb,请使用内置函数 breakpoint。

当 pdb 启动时,它首先读取位于您的主目录和当前目录中名为 .pdbrc 的文本文件。这些文件可以包含任何 pdb 命令,但通常您会在其中放置别名命令,以定义其他经常使用的命令的有用同义词和缩写。

当 pdb 控制时,它会显示字符串 (Pdb),您可以输入 pdb 命令。命令 help(您可以用 h 的缩写形式输入)会列出可用的命令。使用带有参数的 help(参数之间用空格分隔)可以获取有关任何特定命令的帮助。大多数命令可以缩写为第一个或前两个字母,但是必须始终以小写输入命令:pdb 与 Python 本身一样,区分大小写。输入空行会重复上一条命令。最常用的 pdb 命令列在 表 17-6 中。

表 17-6. 常用的 pdb 命令

! ! statement 使用当前选择的堆栈帧(请参见本表后面的 d 和 u 命令)作为本地命名空间执行 Python 语句 statement

| 别名, 取消别名 | 别名 [name [command]], unalias name

定义经常使用的命令的简短形式。command 是任何带有参数的 pdb 命令,并且可以包含 %1、%2 等以引用传递给正在定义的新别名 name 的特定参数,或者使用 %* 引用所有这些参数。不带参数的 alias 列出当前定义的别名。alias name 输出别名 name 的当前定义。unalias name 删除别名。

args, a args 列出当前正在调试的函数传递的所有参数。
断点, b 断点 [location[, condition]] 如果没有参数,列出当前定义的断点及每个断点触发的次数。带有参数时,break 在给定的 location 处设置断点。location 可以是行号或函数名,可选择在函数名之前加上 filename: 以在不是当前文件或函数名模糊的情况下设置断点(即存在于多个文件中的函数)。当存在 condition 时,它是一个表达式,每次执行给定行或函数时(在调试上下文中)都会进行评估;仅当表达式返回真值时才会中断执行。设置新断点时,break 返回断点编号,稍后可以在任何其他与断点相关的 pdb 命令中使用该编号引用新断点。
clear, cl clear [断点编号] 清除(删除)一个或多个断点。无参数清除所有断点,需确认后执行。要临时停用断点而不是删除它,请参见后面的 disable。
condition condition 断点编号 [表达式] condition n expression 设置或更改断点 n 的条件。condition n,不带 expression,将断点 n 设置为无条件断点。
continue, c, cont continue 继续执行正在调试的代码,直到遇到断点为止。
disable disable [断点编号] 禁用一个或多个断点。无参数禁用所有断点(需确认)。与 clear 不同的是,调试器会记住断点,并可以通过 enable 重新激活。
down, d down 在堆栈中向下移动一帧(即向最近的函数调用移动)。通常情况下,堆栈中的当前位置位于最底部(即正在调试的最近调用的函数),所以 down 不能再向下移动。然而,如果之前执行了 up 命令(该命令将当前位置向上移动到堆栈中的上一级),则 down 命令会很有用。
enable enable [断点编号] 启用一个或多个断点。无参数启用所有断点,需确认后执行。

| ignore | ignore 断点编号 [次数] 设置断点的忽略计数(如果省略 count,则为 0)。触发忽略计数大于 0 的断点时,只会减少计数而不会停止执行,而是会显示交互式 pdb 提示。只有当触发忽略计数为 0 的断点时,执行才会停止。例如,假设模块 fob.py 包含以下代码:

`def` f():
 `for` i `in` range(1000):
        g(i)
`def` g(i):
 `pass`

Now consider the following interactive pdb session (minor formatting details may change depending on the Python version you’re running):

>>> `import` pdb
>>> `import` fob
>>> pdb.run('fob.f()')
> <string>(1)?()
(Pdb) break fob.g
Breakpoint 1 at C:\mydir\fob.py:5
(Pdb) ignore 1 500
Will ignore next 500 crossings of breakpoint 1.
(Pdb) continue
> C:\mydir\fob.py(5)
g()-> pass
(Pdb) print(i)
500

The ignore command, as pdb says, tells pdb to ignore the next 500 hits on breakpoint 1, which we set at fob.g in the previous break statement. Therefore, when execution finally stops, the function g has already been called 500 times, as we show by printing its argument i, which indeed is now 500. The ignore count of breakpoint 1 is now 0; if we execute another continue and print i, i shows as 501. In other words, once the ignore count decrements to 0, execution stops every time the breakpoint is hit. If we want to skip some more hits, we must give pdb another ignore command, setting the ignore count of breakpoint 1 to some value greater than 0 yet again. |

| jump, j | jump 行号 设置下一个要执行的行号。可以用此命令跳过某些代码,也可以回到已执行过的代码。注意,跳到前面的源代码行不是撤销命令:在该行之后对程序状态的任何更改都会被保留。 |

jump 确实有一些限制——例如,您只能在底部帧内跳转,不能跳入循环或跳出finally块——但它仍然可以是一个极其有用的命令。

list, l list [first [, last] ] 如果没有参数,则列出当前行为中心的 11(十一)行,或者如果上一个命令也是列表,则列出接下来的 11 行。列表命令的参数可选地指定要在当前文件中列出的第一行和最后一行;使用点(.)表示当前调试行。list 命令列出物理行,包括注释和空行在内,而不是逻辑行。list 的输出标记当前行为 →;如果当前行是在处理异常时到达的,则标记引发异常的行为 >>。
ll ll list 的长版本,在当前函数或帧中显示所有行。
next, n next 执行当前行,而不“进入”从当前行直接或间接调用的任何函数中。但是,在从当前行直接或间接调用的函数中命中断点会停止执行。

| print, p | print(expression), p expression

在当前上下文中评估expression并显示结果。 |

quit, q quit 立即终止 pdb 和正在调试的程序。
return, r return 执行当前函数的其余部分,仅在有断点时停止。
step, s step 执行当前行,进入从当前行调用的任何函数中。
tbreak tbreak [location[, condition]] 类似于 break,但断点是临时的(即 pdb 在触发断点后自动删除断点)。
up, u up 在堆栈中向上移动一帧(即远离最近的函数调用,并向调用函数靠近)。
where, w where 显示帧堆栈并指示当前帧(即在哪个帧的上下文中执行命令!、显示参数、评估表达式等)。

你还可以在(Pdb)提示符处输入 Python 表达式,pdb 将对其进行评估并显示结果,就像在 Python 解释器提示符处一样。但是,当您输入的表达式的第一个术语与 pdb 命令重合时,pdb 命令将执行。在调试带有单字母变量(如pq)的代码时,这尤其棘手。在这些情况下,必须以!开头或以 print 或 p 命令开始表达式。

其他调试模块

虽然 pdb 内置于 Python 中,但也有第三方包提供了增强的调试功能。

ipdb

就像 ipython 扩展了 Python 提供的交互式解释器一样,ipdb 为 pdb 添加了相同的检查、制表完成、命令行编辑和历史记录功能(以及魔术命令)。Figure 17-1 显示了一个示例交互过程。

一个 ipdb 会话示例

图 17-1. 一个 ipdb 会话示例

ipdb 还在其 set_trace 版本中添加了配置和条件表达式,以便更好地控制何时程序可以中断进入调试会话。(在此示例中,断点是在 i 等于 2 时条件触发的。)

pudb

pudb 是一个轻量级的“类图形化”调试器,运行在终端控制台中(见图 17-2),利用 urwid 控制台 UI 库。特别适用于使用 ssh 等终端会话连接远程 Python 环境,其中安装或运行窗口化 GUI 调试器并不容易。

一个 pudb 会话示例

图 17-2. 一个 pudb 会话示例

pudb 拥有其自己的调试命令和界面,需要一些实践才能使用;然而,在紧凑的计算环境中工作时,它提供了一个便捷的可视化调试环境。

警告模块

警告是关于错误或异常情况的消息,这些情况不足以打断程序的控制流(如通过引发异常来打断)。警告模块提供了对输出哪些警告以及处理它们的细粒度控制。您可以通过调用警告模块中的 warn 函数来有条件地输出警告。模块中的其他函数允许您控制警告的格式化方式、设置其目标以及有条件地抑制某些警告或将某些警告转换为异常。

表示警告的异常类不由 warnings 提供:相反,它们是内置的。Warning 类是 Exception 的子类,是所有警告的基类。您可以定义自己的警告类,它们必须直接或通过现有子类(包括)继承 Warning。:

DeprecationWarning

用于使用已弃用的特性,这些特性仅供向后兼容使用

RuntimeWarning

用于使用语义容易出错的特性

SyntaxWarning

用于使用语法容易出错的特性

UserWarning

用于其他用户定义的不适合上述任何情况的警告

对象

Python 没有具体的“警告对象”。而是,警告由一个消息(一个字符串)、一个类别(Warning 的子类)和两个用于标识引发警告位置的信息组成:module(引发警告的模块名)和lineno(源代码中引发警告的行号)。从概念上讲,您可以将这些看作是警告对象 w 的属性:我们稍后使用属性表示法,严格来说,仅出于清晰度考虑,但实际上并不存在具体的对象 w

过滤器

任何时候,警告模块都会保留警告的活动过滤器列表。当你在运行中首次导入警告模块时,该模块会检查 sys.warnoptions 来确定初始过滤器集。你可以使用 Python 选项 -W 来设置 sys.warnoptions 以供给定运行使用。不要依赖于初始过滤器具体在 sys.warnoptions 中的保持,因为这是可能在未来的 Python 版本中会更改的实现细节。

每当发生一个警告 w,警告会对每个过滤器进行测试,直到找到一个匹配的过滤器。第一个匹配的过滤器确定 w 的处理方式。每个过滤器是一个包含五个项目的元组。第一项 action 是一个字符串,定义了匹配时的处理方式。其他四项 message*,* category*,* module, 和 lineno 控制了 w 与过滤器匹配的条件:要进行匹配,必须满足所有条件。以下是这些项目的含义(使用属性表示法指示 w 的概念属性):

message

正则表达式模式字符串;匹配条件是 re.match(message, w.message, re.I)(匹配不区分大小写)

category

警告或其子类;匹配条件是 issubclass(w.category, category)

module

正则表达式模式字符串;匹配条件是 re.match(module, w.module)(匹配区分大小写)

lineno

一个整数;匹配条件是 lineno (0, w.lineno):即,lineno 要么是 0,表示 w.lineno 无关紧要,要么 w.lineno 必须精确等于 lineno

匹配时,过滤器的第一个字段 action 决定了会发生什么。它可以具有以下值:

'always'

w.message 无论 w 是否已经发生都输出。

'default'

w.message 只有在这是第一次从此特定位置(即此特定 (w.module, w.location) 对)发生 w 时才输出。

'error'

w.category(w.message) 被作为异常抛出。

'ignore'

w 被忽略。

'module'

w.message 只有在这是第一次从 w.module 处发生 w 时才输出。

'once'

w.message 只有在这是第一次从任何位置发生 w 时才输出。

当一个模块发出警告时,警告会向该模块的全局变量添加一个名为 warningsgregistry 的字典,如果该字典尚不存在。字典中的每个键都是一个二元组 (message, category),或者是一个包含三个项目的元组 (message, category, lineno);相应的值为 True,表示进一步抑制该消息的发生。因此,例如,你可以通过执行 m.warningsregistry.clear() 来重置模块 m 中所有警告的抑制状态:这样做后,所有消息都可以再次(一次)输出,即使例如它们以前触发过具有 'module' action 的过滤器。

函数

警告模块提供了 表 17-7 中列出的函数。

表 17-7. warnings 模块的函数

| fil⁠t⁠e⁠r​w⁠a⁠r⁠nings | filterwarnings(action, message='.', category=Warning, module='.', lineno=0, append=False)

添加一个过滤器到活动过滤器列表中。当appendTrue时,filterwarnings在所有现有过滤器之后添加过滤器(即将过滤器附加到现有过滤器列表之后);否则,filterwarnings将过滤器插入到任何现有过滤器之前。除了action之外,所有组件都有默认值,意味着“匹配所有”。如上所述,messagemodule都是正则表达式的模式字符串,category是某个Warning的子类,lineno是一个整数,action是一个字符串,用于确定当消息匹配此过滤器时发生的情况。

for⁠m⁠a⁠t​w⁠a⁠r⁠ning formatwarning(message, category, filename, lineno) 返回一个字符串,表示带有标准格式的给定警告信息。
re⁠s⁠e⁠t​w⁠a⁠r⁠nings resetwarnings() 从过滤器列表中移除所有过滤器。resetwarnings还会丢弃使用-W命令行选项最初添加的任何过滤器。
showwarning showwarning(message, category, filename, lineno, file=sys.stderr) 将给定的警告输出到给定的文件对象。输出警告的过滤器操作调用showwarning,让参数file默认为sys.stderr。要更改过滤器操作输出警告时的行为,请编写一个具有此签名的自定义函数,并将其绑定到warnings.showwarning,从而覆盖默认实现。

| warn | warn(message, category=UserWarning, stacklevel=1) 发送一个警告,以便过滤器检查并可能输出它。警告的位置是当前函数(warn的调用者)如果stacklevel为 1,或者当前函数的调用者如果stacklevel为 2。因此,将 2 作为stacklevel的值使您能编写函数,以代表其调用者发送警告,例如:

`def` to_unicode(bytestr):
    `try`:
        `return` bytestr.decode()
    `except` UnicodeError:
        warnings.warn(f'Invalid characters in 
                      {bytestr!r}',
                      stacklevel=2)
        `return` bytestr.decode(errors='ignore')

由于参数stacklevel=2,警告似乎来自于to_unicode的调用者,而不是to_unicode本身。当匹配此警告的过滤器的action是'default'或'module'时,这一点非常重要,因为这些操作仅在从给定位置或模块第一次发生警告时输出警告。

优化

“首先让它能运行。然后让它正确。最后让它快速。” 这句引文,通常略有变化,广为人知为“编程的黄金法则”。据我们所知,这句话的来源是 Kent Beck,他归功于他的父亲。这个原则经常被引用,但很少被遵循。稍微夸张地说,唐纳德·克努斯(他归功于托尼·霍尔)的一句略微夸张的负面引用是:“过早优化是编程中一切邪恶之源。”

优化过早进行,如果你的代码还没有工作,或者你不确定你的代码应该做什么(因为这样你不能确定它是否工作)。首先让它工作:确保你的代码正确执行它应该执行的任务。

如果你的代码正在工作,但你对整体架构和设计不满意,优化也是过早的。在担心优化之前,解决结构性缺陷:先让它工作,然后再做正确的事情。这些步骤不是可选的;工作良好的架构代码总是必须的。⁶

在尝试任何优化之前,拥有一个良好的测试套件是关键。毕竟,优化的目的是增加速度或减少内存消耗——或者两者兼而有之——而不改变代码的行为。

相反,你并不总是需要让它变得快。基准测试可能显示,前两个步骤后,你的代码性能已经足够可接受。当性能不可接受时,分析常常显示所有性能问题都集中在代码的一个小部分,你的程序可能在 10%到 20%的代码中花费 80%或 90%的时间。⁷ 这种性能关键的代码区域被称为瓶颈热点。优化大部分代码(占程序运行时间的 10%)是一种浪费。即使让这部分运行速度提升 10 倍(这是罕见的成就),你的程序整体运行时间只会减少 9%。⁸ 这种加速对用户来说可能根本察觉不到。如果需要优化,把注意力集中在真正重要的瓶颈上。你通常可以在保持代码 100%纯 Python 的同时优化瓶颈,这样就不会阻止未来移植到其他 Python 实现。

开发一个足够快的 Python 应用程序

首先在 Python 中设计、编码和测试你的应用程序,如果可用的扩展模块可以节省你的工作,可以使用它们。这比使用经典的编译语言要少很多时间。然后对应用程序进行基准测试,以确定生成的代码是否足够快。通常是这样的,你就完成了——恭喜!发布它!

由于 Python 本身的大部分代码都是用高度优化的 C 编写的(包括其许多标准库和扩展模块),你的应用程序甚至可能比典型的 C 代码更快。但是,如果应用程序速度太慢,首先需要重新思考算法和数据结构。检查由于应用程序架构、网络流量、数据库访问和操作系统交互而导致的瓶颈。对于许多应用程序而言,每个这些因素引起的减速概率比语言选择或编码细节更大。调整大规模架构方面的细节通常可以显著加速应用程序,并且 Python 是进行此类实验的优秀媒介。如果你正在使用版本控制系统(你应该使用!),创建实验分支或克隆以尝试不同的技术是很容易的,以查看哪些——如果有的话——能够显著改进性能,而不会危及你的工作代码。然后,你可以将通过测试的任何改进合并回来。

如果你的程序仍然太慢,请对其进行性能分析:找出时间花在哪里!如前所述,应用程序通常表现出计算瓶颈,源代码中的小部分区域占据了绝大多数的运行时间。优化这些瓶颈,应用本章其余部分建议的技术。

如果常规的 Python 级优化仍然存在一些显著的计算瓶颈,你可以将它们重写为 Python 扩展模块,详见第二十五章。最终,你的应用程序将以接近使用 C、C++或 FORTRAN 编写的速度运行,甚至更快,尤其是在进行大规模实验后找到更好的架构时。使用这个过程的整体编程效率不会比你完全使用 Python 编码低多少。未来的变更和维护非常简单,因为你使用 Python 来表达程序的整体结构,而使用低级别、更难维护的语言仅用于少数特定的计算瓶颈。

当你按照这个过程在特定领域构建应用程序时,你将积累一套可重复使用的 Python 扩展模块库。因此,你在开发其他快速运行的 Python 应用程序时会变得越来越高效。

即使外部约束最终迫使你将整个应用程序重新编写为低级语言,你也会因为最初选择 Python 而受益良多。快速原型设计长期以来被公认为正确软件架构的最佳方式。一个工作原型能让你检查是否正确地识别了问题,并采取了良好的解决路径。原型还能进行大规模的架构实验,这对性能可能产生真正的巨大影响。如果需要,你可以通过扩展模块逐步将代码迁移到其他语言,并且每个阶段应用仍然完全可用和可测试。这可以防止在编码阶段损害设计架构完整性的风险。

即使要求整个应用程序使用低级语言,首先在 Python 中编写通常更高效(特别是如果您对应用程序的领域不熟悉)。一旦有了工作的 Python 版本,您可以试验用户界面、网络接口或库 API 以及架构。此外,相比于低级语言,Python 代码更容易找到和修复错误,并进行更改。最后,您将非常熟悉代码,因此将其移植到低级语言应该会非常快速和简单,安全地知道大多数设计错误已在 Python 实现中发现和修复。

使用高级语言编写的软件,比起一开始就采用低级编码,结果会更快、更健壮。尽管与纯 Python 应用相比,你的生产力可能稍逊色一些,但仍然比一直在低级语言编码要高。

基准测试

基准测试(也称为负载测试)类似于系统测试:这两种活动都很像为了生产目的运行程序。在这两种情况下,您需要至少有程序预期功能的某些子集,并且需要使用已知的可重现输入。对于基准测试,您无需捕获和检查程序的输出:因为在使其工作和使其正确之前,您已经对程序的正确性充满信心。您需要输入,这些输入代表典型系统操作的代表性,最好是那些可能对程序性能构成最大挑战的输入。如果您的程序执行几种操作,请确保对每种不同操作运行一些基准测试。

用手表测量的经过时间可能足够精确地为大多数程序进行基准测试。在实际使用中,除了在极端约束下的程序外,性能上的 5%或 10%的差异几乎没有实际意义。(在硬实时约束的程序中情况不同,因为它们在大多数方面与普通程序的需求非常不同。)

当您为了帮助选择算法或数据结构而对“玩具”程序或片段进行基准测试时,您可能需要更高的精度:Python 标准库的 timeit 模块(在“timeit 模块”中介绍)非常适合这些任务。本节讨论的基准测试是一种不同类型的基准测试:它是对程序在每个任务上的性能是否可接受进行检查的近似实际程序操作的情况,然后进行性能分析和其他优化活动。对于这样的“系统”基准测试,最好是近似程序正常操作条件的情况,而时间的高精度并不是非常重要的。

大规模优化

对于性能最重要的方面是大规模的选择:整体架构、算法和数据结构。

您经常必须考虑的性能问题与传统计算机科学的大 O 符号有关。非正式地,如果您将N称为算法的输入大小,则大 O 符号表达算法性能,对于较大的N值,与N的某个函数成比例。

O(1)算法(也称为“常数时间”)是一种不随N增长而需要一定时间的算法。 O(N)算法(也称为“线性时间”)是一种对于足够大的N,处理两倍数据需要大约两倍时间,三倍数据需要三倍时间,依此类推,与N成比例增长的算法。 O(N²)算法(也称为“二次时间”算法)是一种对于足够大的N,处理两倍数据需要大约四倍时间,三倍数据需要九倍时间,依此类推,与N的平方成比例增长的算法。相同的概念和符号用于描述程序对内存消耗(“空间”)而非时间的情况。

要了解更多关于大 O 符号、算法及其复杂性的信息,任何一本好的算法和数据结构书都可以帮助;我们推荐 Magnus Lie Hetland 的优秀著作《Python 算法:精通 Python 语言中的基本算法》,第二版(Apress)。

要理解在程序中大 O 符号考虑的实际重要性,考虑两种不同的方式来接受输入可迭代对象中的所有项目,并将它们以相反顺序累积到一个列表中。

`def` slow(it):
    result = []
    `for` item `in` it:
        result.insert(0, item)
    `return` result

`def` fast(it):
    result = []
    `for` item `in` it:
        result.append(item)
    result.reverse()
    `return` result

我们可以更简洁地表达这些函数,但关键差异最好通过这些基本术语来理解。函数slow通过在每次接收到的输入项之前插入来构建结果列表。函数fast将每个输入项添加到先前接收到的所有项之后,然后在最后反转结果列表。直觉上,人们可能认为最后的反转代表了额外的工作,因此slow应该比fast更快。但事实并非如此。

每次调用result.append花费的时间大致相同,与列表result中已有的项数量无关,因为列表末尾几乎总是有一个空位可以放置额外的项(严格来说,append的时间复杂度是摊销 O(1),但本书不涵盖摊销)。函数fast中的for循环执行N次以接收N个项。由于循环的每次迭代都需要恒定的时间,因此整个循环的时间复杂度是 O(N)。result.reverse同样需要 O(N)的时间,因为它与项的总数成正比。因此,函数fast的总运行时间是 O(N)。(如果你不理解为什么两个时间复杂度均为 O(N)的量的和也是 O(N),可以考虑任何两个N的线性函数的和也是N的线性函数——“是 O(N)”的含义与“消耗与N成线性关系的时间量”完全相同。)

另一方面,每次调用result.insert都为要插入的新项在索引 0 处创建空间,将已在列表result中的所有项向前移动一个位置。这需要的时间与列表中已有的项数量成正比。因此,接收N个项的总时间与 1+2+3+...N-1 成正比,其值为 O(N²)。因此,函数slow的总运行时间是 O(N²)。

几乎总是值得将 O(N²)的解决方案替换为 O(N)的解决方案,除非你可以以某种方式为输入大小N分配严格的小限制。如果N可以无严格边界地增长,那么对于大的N值,O(N²)的解决方案将比 O(N)的解决方案慢得令人难以置信(无论各自情况中的比例常数是多少(以及无论分析器告诉你的是什么)。除非你无法消除其他地方的 O(N²)或更差的瓶颈,否则程序中 O(N²)部分将成为程序的瓶颈,在大的N值下支配运行时间。请自己做个好处,留意大 O 符号:与其他性能问题相比,通常几乎可以忽略不计。

顺便说一句,你可以通过更具 Python 风格的表达方式使函数fast运行得更快。只需用以下单个语句替换前两行:

result = list(it)

这种改变不会影响fast的大 O 特性(改变后仍然是 O(N)),但确实通过较大的常数因子加快了运行速度。

简单胜于复杂,通常也更快!

在 Python 中,表达某些内容的最简单、最清晰、最直接和最惯用的方式通常也是最快的。

在 Python 中选择具有良好大 O 性能的算法与在任何其他语言中大致相同。您只需了解 Python 的基本构建块的大 O 性能的一些提示,我们在以下各节中提供了这些提示。

列表操作

Python 列表在内部实现为vectors(也称为dynamic arrays),而不是“linked lists”。这种实现选择在大 O 符号术语中决定了 Python 列表的性能特征。

将两个长度为N1N2的列表L1L2链接(即L1+L2)的复杂度是 O(N1+N2)。将长度为N的列表L乘以整数M(即LM*)的复杂度是 O(NM*)。访问或重新绑定任何列表项的复杂度是 O(1)。对列表进行 len()操作也是 O(1)。访问长度为M的任何切片的复杂度是 O(M)。将长度相同的切片重新绑定为另一个长度相同的切片的复杂度也是 O(M)。将长度为M1的切片重新绑定为长度不同的M2的切片的复杂度是 O(M1+M2+N1),其中N1是目标列表中切片之后的项数(因此,在列表末尾进行长度更改的切片重新绑定相对较便宜,但在列表开始或长列表中间进行则更昂贵)。如果需要先进先出操作,列表可能不是用于此目的的最快数据结构:相反,请尝试类型 collections.deque,详见“deque”。

大多数列表方法,如表 3-5 所示,等效于切片重新绑定,并具有相同的大 O 性能。方法 count、index、remove 和 reverse,以及运算符in,都是 O(N)。方法 sort 通常是 O(N log N),但在某些重要的特殊情况下,例如列表已经排序或反向排序除了少数项外,sort 被高度优化⁹为 O(N)。range(a, b, c)是 O(1),但在结果的所有项上循环的复杂度是 O((b - a) // c)。

字符串操作

对于长度为N的字符串(无论是字节还是 Unicode),大多数方法的复杂度都是 O(N)。len(astring)的复杂度是 O(1)。产生具有字符转换和/或删除指定字符的副本的最快方法是字符串的方法 translate。涉及字符串的单个最实用的大 O 考虑在“Building up a string from pieces”中有所涵盖。

字典操作

Python 的字典是用哈希表实现的。这种实现选择在大 O 符号术语中决定了 Python 字典的所有性能特征。

访问、重新绑定、添加或移除字典项的操作是 O(1),get、setdefault 和 popitem 方法以及操作符in也是如此。d1.update(d2)是 O(len(d2))。len(adict)是 O(1)。方法 keys、items 和 values 是 O(1),但直接在字典上循环遍历所有项的开销为 O(N)。

当字典中的键是定义了 hash 和相等比较方法的类的实例时,字典的性能当然会受到这些方法的影响。本节中提供的性能指标在键的哈希和相等比较为 O(1)时成立。

集合操作

Python 集合像字典一样是用哈希表实现的。集合的所有性能特征在大 O 术语中与字典相同。

在集合中添加或移除项目的操作是 O(1),同样的是操作符in。len(aset)是 O(1)。在集合中的项目是定义了 hash 和相等比较方法的类的实例时,集合的性能当然会受到这些方法的影响。本节中提供的性能提示在项目的哈希和相等比较为 O(1)时成立。

Python 内置类型的操作的大 O 时间摘要

L是任何列表,T是任何字符串(str 或 bytes),D是任何字典,S是任何集合(例如,具有数字作为项,仅用于确保 O(1)的哈希和比较),x是任何数字(同上):

O(1)

len(L),len(T),len(D),len(S),L[i],T[i],D[i],del D[i],if x in D,if x in SS.add(x),S.remove(x),对L最右端的附加或移除

O(N)

LTDS上的循环,对L进行一般的附加或移除(除非在最右端),T的所有方法,if x in Lif x in TL的大多数方法,所有浅复制

O(N log N)

L.sort(),大多数情况下是 O(N)(但如果L已经几乎排序或逆序排序,则为 O(N))

性能分析

如本节开头所述,大多数程序有热点:源代码中相对较小的区域占据了程序运行时间的大部分。不要试图猜测程序的热点在哪里:程序员在这一领域的直觉经常不可靠。相反,使用 Python 标准库模块 profile 在一个或多个运行中收集已知输入的配置文件数据。然后使用模块 pstats 来整理、解释和显示该配置文件数据。

为了提高准确性,您可以校准 Python 分析器以适合您的机器(即确定分析在该机器上产生的开销)。然后,profile 模块可以从它测量的时间中减去这些开销,使您收集的配置文件数据更接近实际情况。标准库模块 cProfile 具有与 profile 类似的功能;cProfile 更可取,因为它更快,这意味着它施加的开销更小。

还有许多值得考虑的第三方分析工具,例如pyinstrumentEliot;Itamar Turner-Trauring 的一篇优秀文章解释了这些工具的基础和优点。

profile 模块

profile 模块提供了一个经常使用的函数:

| run | run(code, filename=None) code 是一个字符串,可用于 exec,通常是对你要分析的程序的主函数的调用。filename 是 run 创建或重写带有分析数据的文件的路径。通常情况下,您会多次调用 run,指定不同的文件名和程序主函数的不同参数,以便按照您对它们在实际使用中的预期使用比例来执行各种程序部分。然后,您使用 pstats 模块来显示跨各种运行的汇总结果。

您可以调用 run 而不使用文件名来获取摘要报告,类似于 pstats 模块提供的报告,在标准输出上。但是,这种方法无法控制输出格式,也无法将几次运行合并到一个报告中。在实践中,您应该很少使用此功能:最好将分析数据收集到文件中,然后使用 pstats。

profile 模块还提供了类 Profile(在下一节中简要讨论)。通过直接实例化 Profile,您可以访问高级功能,例如在指定的本地和全局字典中运行命令的能力。在本书中,我们不再深入讨论类 profile.Profile 的这种高级功能。

校准

要为您的机器校准 profile,请使用 profile 提供并在函数 run 中内部使用的类 Profile。Profile 的一个实例 p 提供了一个用于校准的方法:

| calibrate | p.calibrate(N) 循环 N 次,然后返回一个数字,该数字是您机器上每次调用的分析开销。如果您的机器速度很快,则 N 必须很大。多次调用 p.calibrate(10000),并检查它返回的各种数字是否接近,然后选择其中最小的一个。如果数字变化很大,请尝试使用更大的 N 再次尝试。

校准过程可能会耗时。但是,您只需要执行一次,只有在进行可能改变机器特性的更改时才需要重复执行,例如对操作系统应用补丁、添加内存或更改 Python 版本。一旦您了解了机器的开销,您就可以在每次导入它之前,就在使用 profile.run 之前告诉 profile 关于它。做到这一点的最简单方法如下:

`import` profile
profile.Profile.bias = *`.``.``.``the` `overhead` `you` `measured``.``.``.`*
profile.run('main()', 'somefile')

|

pstats 模块

pstats 模块提供了一个单一的类,Stats,用于分析、 consoliidate 和报告由函数 profile.run 写入的一个或多个文件中包含的分析数据。它的构造函数具有如下签名:

Stats class Stats(filename, filenames, stream=sys.stdout*) 使用由函数 profile.run 写入的一个或多个配置数据文件的文件名来实例化 Stats,并将分析输出发送到 stream。

类 Stats 的实例 s 提供了添加配置数据、排序和输出结果的方法。每个方法返回 s,因此可以在同一表达式中链式调用多个方法。s 的主要方法列在 Table 17-8 中。

Table 17-8. 类 Stats 实例 s 的方法

add add(filename) 将另一个配置数据文件添加到 s 用于分析的集合中。

| pr⁠i⁠n⁠t⁠_​c⁠a⁠l⁠lees, pri⁠n⁠t⁠_​c⁠a⁠l⁠lers | print_callees(*restrictions), print_callers(*restrictions)

输出 s 配置数据中函数列表,根据最新调用 s.sort_stats 进行排序并受到给定限制的影响。您可以用零个或多个 restrictions 调用每个打印方法,以依次应用它们,以减少输出行数。整数 n 限制输出为前 n 行。介于 0.0 和 1.0 之间的浮点数 f 限制输出为行数的 f 分数。字符串限制被编译为正则表达式模式(详见 “正则表达式和 re 模块”);只输出满足正则表达式搜索方法调用的行。限制是累积的。例如,s.print_callees(10, 0.5) 输出前 5 行(10 的一半)。摘要和标头行只有在无条件情况下输出后才适用限制。

输出中的每个函数 f 都附带 f 的调用者(调用 f 的函数)或 f 的被调用者(f 调用的函数),具体取决于方法的名称。 |

| print_stats | print_stats(*restrictions) 输出关于 s 的配置数据统计,根据最新调用 s.sort_stats 进行排序并受到给定限制的影响(如果有的话),详见 print_callees 和 print_callers。在几行摘要(收集配置数据的日期和时间、函数调用数以及使用的排序标准)之后,输出结果(无限制)为每个函数一行,每行有六个字段,由标头行标记。对于每个函数 f,print_stats 输出以下字段:

  1. 调用 f 的总次数

  2. f 的累计时间,不包括 f 调用的其他函数

  3. f 的每次调用所花费的总时间(即字段 2 除以字段 1)

  4. f 所花费的累计时间,以及直接或间接调用 f 的所有函数

  5. 每次调用 f 的累计时间(即字段 4 除以字段 1)

  6. 函数 f 的名称

|

| sort_stats | sort_stats(*keys) 给出一个或多个用于将未来输出排序的键。每个键可以是一个字符串或者枚举 pstats.SortKey 的成员。对于表示时间或数字的键,排序是降序的;对于键 'nfl',则按字母顺序排序。在调用 sort_stats 时最常用的键包括:

SortKey.CALLS 或 'calls'

函数调用的次数(例如 print_stats 输出中的字段 1)

SortKey.CUMULATIVE 或 'cumulative'

函数及其调用的累计时间(例如 print_stats 输出中的字段 4)

SortKey.NFL 或 'nfl'

函数的名称、其模块以及函数在其文件中的行号(例如 print_stats 输出中的字段 6)

SortKey.TIME 或 'time'

函数本身消耗的总时间,不包括调用的函数(例如 print_stats 输出中的字段 2)

|

strip_dirs strip_dirs() 通过剥离所有模块名称中的目录名来修改 s,以使未来输出更加紧凑。在 s.strip_dirs 之后,s 是未排序的,因此通常在调用 s.strip_dirs 后立即调用 s.sort_stats。

小规模优化

程序操作的微调很少重要。它可能在一些特别热点中带来小但有意义的差异,但几乎从未是决定性因素。然而,微调——在追求大多数无关紧要的微小效率时——是程序员本能的结果。这在很大程度上是因为大多数优化是过早的,并且最好避免。对微调的最有利论点是,如果一种习惯性的表达方式总是比另一种更快,当差异可测量时,那么你值得养成总是使用更快方法的习惯。¹⁰

在 Python 中,如果你顺其自然,选择简单和优雅,你通常会得到性能良好、清晰易懂且可维护的代码。换句话说,让 Python 来做工作:当 Python 提供一种简单直接的方法来执行任务时,很可能也是最快的方式。在少数情况下,即使可能不是直观上首选的方法,在性能上也有优势,正如本节其余部分所讨论的。

最简单的优化是使用 python -O-OO 运行你的 Python 程序。-OO-O 相比在性能上几乎没有区别,但可能会节省一些内存,因为它从字节码中删除了文档字符串,而内存有时(间接地)是性能瓶颈。在当前 Python 版本中,优化器并不强大,但如果你使用 assert 语句和 if debug: 保护(如 “assert 语句” 中建议的那样),它可能为你带来 5-10% 的性能优势(在某些情况下可能更大)。-O 的最大优点是它几乎不花费任何代价——当然前提是你的优化不是过早的(当你仍在开发中的程序不要使用 -O)。

timeit 模块

标准库模块 timeit 对于测量特定代码片段的精确性能非常方便。您可以导入 timeit 以在程序中使用其功能,但最简单和最常见的用法是从命令行:

$ python -m timeit -s '*setup statement(s)*' '*statement(s) to be timed*'

“设置语句”仅执行一次,用于设置事物;“要计时的语句”重复执行,以准确测量它们的平均时间。

例如,假设你想知道 x=x+1x+=1 的性能差异,其中 x 是一个整数。在命令提示符下,你可以轻松尝试:

$ python -m timeit -s 'x=0' 'x=x+1'
1000000 loops, best of 3: 0.0416 usec per loop
$ python -m timeit -s 'x=0' 'x+=1'
1000000 loops, best of 3: 0.0406 usec per loop

并发现两种情况下的性能在所有实际情况下都几乎相同(在本例中为 2.5% 的微小差异最好视为“噪音”)。

记忆化

记忆化 是一种技术,用于保存重复使用相同参数值调用的函数返回值。当使用之前未见过的参数调用函数时,记忆化函数计算结果,然后在缓存中保存用于调用它的参数及其相应的结果。当以后再次使用相同参数调用函数时,函数只需在缓存中查找计算出的值,而不需要重新运行函数计算逻辑。通过这种方式,对于任何特定的参数或参数,计算仅执行一次。

这里是一个计算给定角度值正弦值的函数示例:

`import` math
`def` sin_degrees(x):
    return math.sin(math.radians(x))

如果我们确定 sin_degrees 是一个瓶颈,并且在调用时 x 的值重复出现(比如在显示模拟时钟时使用的 0 到 360 的整数值),我们可以添加一个记忆缓存:

_cached_values = {}
`def` sin_degrees(x):
    `if` x `not` `in` _cached_values:
        _cached_values[x] = math.sin(math.radians(x))
    return _cached_values[x]

对于接受多个参数的函数,参数值的元组将用作缓存键。

我们在函数外部定义了 _cached_values,这样每次调用函数时它不会被重置。为了明确地将缓存与函数关联起来,我们可以利用 Python 的对象模型,该模型允许我们将函数视为对象并为其分配属性:

`def` sin_degrees(x):
    cache = sin_degrees._cached_values
    `if` x `not` `in` cache:
        cache[x] = math.sin(math.radians(x))
    `return` cache[x]
sin_degrees._cached_values = {}

缓存是通过使用内存来获得性能的经典方法(时间-内存折衷)。此示例中的缓存是无界限的,因此,当 sin_degrees 以许多不同的 x 值被调用时,缓存将继续增长,消耗越来越多的程序内存。缓存通常配置有“驱逐策略”,该策略确定何时可以从缓存中删除值。删除最旧的缓存值是一种常见的驱逐策略。由于 Python 将字典条目保持按插入顺序,如果我们迭代字典,那么“最旧”的键将是我们找到的第一个键:

`def` sin_degrees(x):
    cache = sin_degrees._cached_values
    `if` x `not` `in` cache:
        cache[x] = math.sin(math.radians(x))
 *`# remove oldest cache entry if exceed maxsize limit`*
        `if` len(cache) > sin_degrees._maxsize:
            oldest_key = next(iter(cache))
            del cache[oldest_key]
    `return` cache[x]
sin_degrees._cached_values = {}
sin_degrees._maxsize = 512

您可以看到,这开始使代码复杂化,原始计算给定角度值正弦值的逻辑隐藏在所有缓存逻辑内部。Python 标准库模块 functools 包括缓存装饰器 lru_cache、3.9+ cache 和 3.8+ cached_property,以清晰地执行记忆化。例如:

`import` functools
@functools.lru_cache(maxsize=512)
`def` sin_degrees(x):
    `return` math.sin(math.radians(x))

这些装饰器的签名在“functools 模块”中有详细描述。

缓存浮点数值可能会导致不良行为

正如在“浮点数值”中所描述的,当浮点值的比较相等时,实际上可能返回False,因为这些值在某些期望的公差范围内被认为是相等的。对于无界缓存,包含浮点键的缓存可能因缓存仅在第 18 位小数处不同的多个值而意外增长。对于有界缓存,许多几乎相等的浮点键可能导致意外地驱逐其他明显不同的值。

这里列出的所有缓存技术都使用相等匹配,因此使用一个或多个浮点参数缓存函数的代码应采取额外的步骤来缓存四舍五入的值,或者使用 math.isclose 进行匹配。

预计算查找表

在某些情况下,您可以预测在调用特定函数时代码将使用的所有值。这使您可以预先计算这些值并将它们保存在查找表中。例如,在我们的应用程序中,将为整数度值 0 到 360 计算 sin 函数的工作可以在程序启动时仅执行一次,并将结果保存在 Python 字典中:

_sin_degrees_lookup = {x: math.sin(math.radians(x))
                       `for` x `in` range(0, 360+1)}
sin_degrees = _sin_degrees_lookup.get

将 sin_degrees 绑定到 _sin_degrees_lookup 字典的 get 方法意味着我们程序的其余部分仍然可以将 sin_degrees 作为函数调用,但现在值检索速度等于字典查找速度,没有额外的函数开销。

通过片段构建字符串

单个 Python“反模式”,最有可能损害程序性能的是,通过循环在大字符串上逐片构建字符串,例如 big_string += piece。Python 字符串是不可变的,因此每次这样的连接意味着 Python 必须释放之前为 big_string 分配的M字节,并为新版本分配和填充M + K字节。在循环中重复执行此操作,最终会导致大约 O(N²)的性能,其中N是总字符数。通常情况下,如果可以轻松获得 O(N)的性能而获得 O(N²)的性能,那么会是一场灾难。¹¹ 在某些平台上,由于释放许多逐渐变大的区域而导致的内存碎片效应,情况可能更为糟糕。

要实现 O(N)性能,累积中间片段使用列表,而不是逐片构建字符串。列表与字符串不同,是可变的,因此向列表附加是 O(1)(摊销)。将每个 big_string += piece 的出现更改为 temp_list.append(piece)。然后,在累积完成时,使用以下代码在 O(N)时间内构建所需的字符串结果:

big_string = ''.join(temp_list)

使用列表推导、生成器表达式或其他直接方法(如调用 map,或使用标准库模块 itertools)来构建 temp_list,通常可以进一步(重要但不是大 O)优化,而不是重复调用 temp_list.append。其他构建大字符串的 O(N) 方法,一些 Python 程序员认为更可读的方法是将这些片段连接到 array.array('u') 的实例上,使用 array 的 extend 方法,使用 bytearray,或将片段写入 io.TextIO 或 io.BytesIO 的实例。

在特殊情况下,如果你想要输出结果字符串,通过在 temp_list 上使用 writelines(而不是在内存中构建 big_string),可能会获得进一步的小幅性能提升。如果可行(即在循环中已打开并可用输出文件对象,并且文件已缓冲),每个片段执行一次写入调用同样有效,而不需要任何累积。

虽然不像在循环中对大字符串使用 += 那样关键,但另一种情况是,移除字符串串联可能会在表达式中连接多个值时带来轻微的性能提升:

oneway = str(x) + ' eggs and ' + str(y) + ' slices of ' + k + ' ham'
another = '{} eggs and {} slices of {} ham'.format(x, y, k)
yetanother = f'{x} eggs and {y} slices of {k} ham'

使用格式化方法或 f-string 来格式化字符串(在 第八章 讨论过)通常是一个很好的性能选择,而且比串联方法更符合惯用法,因此更清晰。在前面示例的样本运行中,格式化方法比串联方法快两倍以上(也许更直观的串联方法),而 f-string 方法比格式化方法更快两倍以上。

搜索和排序

运算符 in 在搜索时是最自然的工具,当右操作数是集合或字典时是 O(1),但当右操作数是字符串、列表或元组时是 O(N)。如果你必须在容器上执行许多此类检查,则最好使用集合或字典,而不是列表或元组,作为容器。Python 的集合和字典对于搜索和按键提取项进行了高度优化。但是,从其他容器构建集合或字典是 O(N) 的,因此为了使这种关键的优化值得,你必须能够在多个搜索中保留集合或字典,并在底层序列变化时可能进行适当的更改。

Python 列表的排序方法也是一个高度优化和复杂的工具。你可以依赖排序的性能。标准库中的大多数执行比较的函数和方法都接受键参数,以确定如何精确比较项目。你可以提供一个键函数,为列表中的每个元素计算一个键值。列表元素按其键值排序。例如,你可以编写一个按属性 attr 排序对象的键函数,如 lambda ob: ob.attr,或者按字典键 'attr' 排序字典的键函数,如 lambda d: d['attr']。 (operator 模块的 attrgetter 和 itemgetter 方法是这些简单键函数的有用替代品;它们比 lambda 更清晰和更精确,并且还提供了性能增益。)

Python 的旧版本使用了一个 cmp 函数,该函数会获取列表元素对(A, B)并根据 A < BA == BA > B 的情况返回 -1、0 或 1。使用 cmp 函数进行排序非常慢,因为它可能需要比较每个元素和其他每个元素(可能是 O(N²) 的性能)。在当前的 Python 版本中,排序函数不再接受 cmp 函数参数。如果你正在迁移古老的代码,并且只有一个适合作为 cmp 参数的函数,你可以使用 functools.cmp_to_key 来构建一个适合作为新 key 参数传递的键函数。

然而,在模块堆 heapq 中,涵盖了 “堆模块” ,有几个函数不接受键参数。在这种情况下,可以使用 DSU 惯用法,详见 “装饰-排序-去装饰惯用法” 。(在某些情况下,堆非常值得注意,因为它们可以帮助你避免对所有数据进行排序。)

避免使用 exec 和 from ... import *。

函数中的代码比模块顶层的代码运行得更快,因为访问函数的局部变量比访问全局变量更快。然而,如果函数包含一个没有显式字典的 exec,那么函数会变慢。这样的 exec 的存在迫使 Python 编译器避免它通常执行的关于访问局部变量的轻微但重要的优化,因为 exec 可能会改变函数的命名空间。形如:

`from` *`my_module`* `import` *

还浪费性能,因为它也可能无法预测地改变函数的命名空间,并因此抑制 Python 的局部变量优化。

exec 本身也非常慢,如果应用于源代码字符串而不是代码对象,则更是如此。在性能、正确性和清晰度方面,避免使用 exec 是迄今为止最好的方法。通常可以找到更好的(更快、更健壮和更清晰)解决方案。如果必须使用 exec,则始终使用显式字典,虽然完全避免使用 exec 是更好的选择。如果需要多次执行动态获取的字符串,则应该先编译该字符串,然后重复执行生成的代码对象。

eval 适用于表达式而非语句;因此,虽然它仍然很慢,但避免了 exec 的最糟糕的性能影响。对于 eval,最好使用显式字典。与 exec 一样,如果需要多次评估同一动态获取的字符串,应该先编译该字符串,然后重复使用 eval 生成的代码对象。完全避免使用 eval 更好。

更多关于 execevalcompile 的细节和建议,请参见“动态执行和 exec”。

布尔表达式的短路计算

Python 根据操作符 notandor 的优先级,从左到右逐一评估布尔表达式。当仅仅通过评估前导项就能确定整体表达式为 TrueFalse 时,Python 将跳过剩余部分。这个特性被称为 短路计算,因为 Python 跳过不必要的处理,就像电气短路绕过电路的部分一样。

在这个例子中,两个条件都必须为 True 才能继续执行:

`if` slow_function() `and` fast_function():
 *`#`* *`...`* *`proceed with processing`* *`...`*

fast_function 准备返回 False 时,首先评估它可能会更快,从而避免完全调用 slow_function

`if` fast_function() `and` slow_function():
 *`#`* *`...`* *`proceed with processing`* *`...`*

当操作符是 or 时,也适用这种优化,只要其中一个条件为 True 就继续执行:当 fast_function 返回 True 时,Python 完全跳过 slow_function

你可以通过考虑表达式操作符和项的顺序来优化这些表达式,让 Python 首先评估更快的子表达式。

短路计算可能绕过所需的函数

在前面的例子中,当 slow_function 执行一些重要的“副作用”行为(比如记录到审计文件或者通知系统管理员某个条件),短路计算可能会意外地跳过这些行为。在将必要行为包含在布尔表达式中时要小心,不要过度优化或者移除重要的功能。

迭代器的短路计算

类似于布尔表达式的短路计算,你也可以在迭代器中短路计算值的评估。Python 的内置函数 allanynext 在找到满足给定条件的第一项后立即返回,而不会生成更多的值:

any(x**2 > 100 `for` x `in` range(50)) 
*`# returns`* *`True`* *`once it reaches 10, skips the rest`*

odd_numbers_greater_than_1 = range(3, 100, 2)
all(is_prime(x) `for` x `in` odd_numbers_greater_than_1) 
*`# returns`* *`False`**`: 3, 5, and 7 are prime but 9 is not`*

next(c `for` c `in` string.ascii_uppercase `if` c in "AEIOU")
*`# returns 'A' without checking the remaining characters`*

当迭代器特别是生成器时,你的代码将获得额外的优势,如这三种情况所示。当项目序列生成成本高昂(例如从数据库提取的记录),使用生成器检索这些项目,并短路以仅检索所需的最小值,可以带来显著的性能优势。

优化循环

大部分程序的瓶颈将出现在循环中,特别是嵌套循环中,因为循环体重复执行。Python 不会自动执行任何 代码提升:如果你有任何代码在循环内部,通过提升它们出循环以减少执行次数,而循环是一个瓶颈,那么你应该自己提升代码。有时,代码提升的必要性可能不是显而易见的:

`def` slower(anobject, ahugenumber):
    `for` i `in` range(ahugenumber):
        anobject.amethod(i)

`def` faster(anobject, ahugenumber):
    themethod = anobject.amethod
    `for` i `in` range(ahugenumber):
        themethod(i)

在这种情况下,更快地提升出循环的代码是属性查找 anobject.amethod。 slower 每次重复查找,而 faster 只执行一次。这两个函数并不完全相同:可以(勉强)想象执行 amethod 可能会导致 anobject 上的更改,使得下一个查找同名属性的对象检索到不同的方法对象。这也是为什么 Python 自身不执行此类优化的部分原因。实际上,这类微妙、隐晦且棘手的情况很少发生;你几乎可以肯定地自己执行这些优化,以从某些瓶颈中挤出最后一丝性能。

Python 使用局部变量比全局变量更快。如果一个循环反复访问一个在迭代之间不会改变值的全局变量,你可以在局部变量中“缓存”该值,并访问它。这也适用于内置函数:

`def` slightly_slower(asequence, adict):
    `for` x `in` asequence:
        adict[x] = hex(x)

`def` slightly_faster(asequence, adict):
    myhex = hex
    `for` x `in` asequence:
        adict[x] = myhex(x)

这里,加速效果非常有限。

不要缓存 NoneTrueFalse。这些常量是关键字:不需要进一步优化。

列表推导式和生成器表达式可能比循环更快,有时 map 和 filter 也是如此。为了优化,请尝试将循环转换为列表推导式、生成器表达式或者可能的 map 和 filter 调用。如果必须使用 lambda 或者额外的函数调用,那么 map 和 filter 的性能优势将被抵消,甚至更差。只有当 map 或 filter 的参数是内置函数,或者是你必须从显式循环、列表推导式或生成器表达式中调用的函数时,才有可能获得一些微小的加速。

你可以用列表推导式、map 和 filter 调用最自然地替换的循环是那些通过重复调用列表的 append 来构建列表的循环。以下示例展示了这种优化在一个微性能基准测试脚本中(该示例包括对 timeit 便捷函数 repeat 的调用,该函数只是调用 timeit.timeit 指定次数):

`import` timeit`,` operator

`def` slow(asequence):
    result = []
    `for` x `in` asequence:
        result.append(-x)
    `return` result

`def` middling(asequence):
    `return` list(map(operator.neg, asequence))

`def` fast(asequence):
    `return` [-x `for` x `in` asequence]

`for` afunc `in` slow, middling, fast:
    timing = timeit.repeat('afunc(big_seq)',
                           setup='big_seq=range(500*1000)',
                           globals={'afunc': afunc},
                           repeat=5,
                           number=100)
    `for` t `in` timing:
        print(f'{afunc.__name__},{t}')

正如我们在本书的上一版中报道的(使用不同的测试参数):

在旧笔记本电脑上运行此示例 v2 显示,快函数大约需要 0.36 秒,中等函数需要 0.43 秒,慢函数需要 0.77 秒。换句话说,在该设备上,慢函数(追加方法调用的循环)比中等函数(单个映射调用)慢约 80%,而中等函数反过来比快函数(列表推导式)慢约 20%。

列表推导式是在本例中微基准化任务的最直接方式,因此毫不奇怪,它也是最快的——大约比使用追加方法调用的循环快两倍。

当时,使用 Python 2.7 时,使用中等函数而不是慢函数具有明显优势,并且使用快函数而不是中等函数会带来适度的速度提升。对于本版本涵盖的情况,快函数相对于中等函数的改进要少得多,如果有的话。更有趣的是,慢函数现在开始接近优化函数的性能。此外,可以看到在连续版本的 Python 中逐步的性能改进,特别是 Python 3.11(见图 17-3)。

明显的教训是,在升级到较新的 Python 版本时,应重新审视性能调优和优化措施。

在各种 Python 版本上的示例性能

图 17-3. 在各种 Python 版本上的示例性能

用于重型 CPU 工作的多处理

如果您有大量可以在独立片段中完成的 CPU 密集型处理,则优化的一个重要方式是使用多处理,如第十五章中所述。您还应考虑是否适用于应用于大数据集的数值处理的某个数值包,如第十六章中所述。

优化 I/O

如果您的程序大量进行 I/O 操作,那么性能瓶颈很可能是由 I/O 而不是计算引起的。这类程序称为 I/O 限制而不是 CPU 限制。您的操作系统会尝试优化 I/O 性能,但您可以通过几种方式来帮助它。

从程序便利性和简单性的角度来看,一次读取或写入的理想数据量通常很小(一个字符或一行)或非常大(一次整个文件)。这通常没问题:Python 和您的操作系统在幕后工作,使您的程序可以使用方便的逻辑块进行 I/O 操作,同时安排物理 I/O 操作使用更适合性能的块大小。一次读取和写入整个文件在性能上通常是可以接受的,只要文件不是非常大。具体来说,每次整个文件的 I/O 操作都可以接受,只要文件的数据非常舒适地适合于物理 RAM,留出充足的内存供您的程序和操作系统在执行其他任务时使用。I/O 限制性能的难题通常出现在大文件时。

如果性能是一个问题,绝对不要使用文件的 readline 方法,因为它在执行分块和缓冲时有限制。相反,如果该方法对您的程序方便的话,使用 writelines 不会造成性能问题。当读取文本文件时,直接在文件对象上循环以获得最佳性能的一行数据。如果文件不太大,因此可以方便地放入内存中,请测试您的程序的两个版本的时间:一个直接在文件对象上循环,另一个将整个文件读入内存。任何一种方式都可能稍微快一些。

对于二进制文件,特别是那些每次运行程序时只需要其中一部分内容的大型二进制文件,模块 mmap(在“mmap 模块”中介绍)有时可以帮助简化程序并提升性能。

如果能够根据需要安排架构,使 I/O 限制的程序多线程化,有时会大幅提升性能。启动几个专注于 I/O 的工作线程,让计算线程通过队列实例请求 I/O 操作,并在确定最终需要数据时立即发布请求。仅当计算线程能够在 I/O 线程等待数据时执行其他任务时,性能才会提高。通过不同线程进行计算和等待数据,重叠计算和数据等待,才能以此方式获得更好的性能。(详见“Python 线程”以及建议的架构的详细介绍。)

另一方面,可能更快、更可扩展的方法是放弃线程,转而采用异步(事件驱动)架构,正如第十五章中所提到的。

¹ 这个问题与“技术债务”等主题有关,这些主题在本书的一位作者的“好即是好”技术演讲中有所涉及(该作者在众多演讲中最喜欢的一次!),由 Martin Michlmayr 在LWN.net上进行了精彩的总结和讨论。

² 在这个领域使用的语言是混乱而令人困惑的:像dummies, fakes, spies, mocks, stubstest doubles这样的术语被不同的人用来表示略有不同的含义。对于术语和概念的权威性方法(虽然不是我们使用的确切方法),请参阅 Martin Fowler 的文章“Mocks Aren't Stubs”

³ 这部分原因是因为系统的结构往往反映了组织的结构,根据康威定律

⁴ 但是,请确保你确切地知道在任何给定情况下你正在使用 doctest 做什么:引用 Peter Norvig,在这个主题上写得很精确:“知道你的目标是什么;如果你同时瞄准两个目标,你通常会两个都错过。”

⁵ 在评估 assert a == b时,pytest 将a解释为观察到的值,将b解释为预期值(与 unittest 相反)。

⁶ “哦,但我只会运行这段代码很短的时间!”这不是草率行事的借口:俄罗斯谚语“没有比临时解决方案更持久的东西”在软件领域特别适用。全世界有很多“临时”的代码正在执行关键任务,但已经超过 50 年了。

⁷ 典型的Pareto 原则案例。

⁸ 根据Amdahl 定律

⁹ 使用专为 Python 发明的自适应排序算法Timsort

¹⁰ 一种曾经较慢的习惯用法可能在未来的 Python 版本中被优化,因此在升级到较新版本的 Python 时重新进行 timeit 测量以检查这一点是值得的。

¹¹ 即使当前的 Python 实现竭尽全力帮助减少这种特定的、可怕的但常见的反模式的性能损失,它们也无法捕捉到每一种情况,所以不要指望它们!

第十八章:网络基础

面向连接协议的工作方式类似于打电话。您请求与特定的网络端点建立连接(类似于拨打某人的电话号码),您的对方要么接听要么不接听。如果接听,您可以与他们交谈并听到他们的回答(如果需要可以同时进行),并且您知道没有任何信息丢失。在对话结束时,您都会说再见并挂断电话,因此如果没有发生这种关闭事件,就明显表明出了问题(例如,如果突然听不到对方的声音)。传输控制协议(TCP)是互联网的主要面向连接传输协议,被 Web 浏览器、安全外壳、电子邮件和许多其他应用程序使用。

无连接或者数据报协议更像是通过发送明信片进行通信。大多数情况下,消息可以传递,但是如果出了问题,你必须准备好应对后果——协议不会通知你消息是否已接收,而且消息可能会无序到达。对于交换短消息并获取答案,数据报协议的开销比面向连接的协议小,前提是整体服务能够处理偶发的中断。例如,域名服务(DNS)服务器可能无法响应:直到最近,大多数 DNS 通信都是无连接的。用户数据报协议(UDP)是互联网通信的主要无连接传输协议。

如今,安全性变得越来越重要:理解安全通信的基础知识有助于确保您的通信达到所需的安全水平。如果这个摘要让您在没有充分了解问题和风险的情况下尝试实现这样的技术,那么它将发挥出有价值的作用。

所有网络接口之间的通信都是通过字节串交换的。要传输文本或者其他大多数信息,发送方必须将其编码为字节,接收方必须解码。在本章中,我们将讨论单个发送方和单个接收方的情况。

伯克利套接字接口

如今大多数网络使用套接字。套接字提供了独立端点之间的管道访问,使用传输层协议在这些端点之间传输信息。套接字的概念足够通用,使得端点可以位于同一台计算机上,也可以位于联网的不同计算机上,无论是在本地还是通过广域网连接。

如今最常用的传输层是 UDP(用于无连接的网络)和 TCP(用于面向连接的网络);每种传输层在通用 Internet 协议(IP)网络层上运行。这些协议堆栈以及运行在其上的许多应用协议总称为TCP/IP。Gordon McMillan 的(有些陈旧但仍然有效的)Socket 编程指南提供了很好的介绍。

两种最常见的套接字家族是基于 TCP/IP 通信的互联网套接字(提供现代 IPv6 和更传统的 IPv4 两种版本)和Unix 套接字,尽管还有其他家族可用。互联网套接字允许任何两台可以交换 IP 数据报的计算机进行通信;Unix 套接字只能在同一 Unix 机器上的进程之间通信。

为了支持许多并发的互联网套接字,TCP/IP 协议栈使用由 IP 地址、端口号和协议标识的端点。端口号允许协议处理软件在相同 IP 地址上使用相同协议时区分不同的端点。连接的套接字还与一个远程端点关联,即连接并能够通信的对方套接字。

大多数 Unix 套接字在 Unix 文件系统中具有名称。在 Linux 平台上,以零字节开头的套接字存在于内核维护的名称池中。例如,这些对于与chroot-jail 进程通信非常有用,例如,两个进程之间没有共享文件系统。

互联网套接字和 Unix 套接字均支持无连接和面向连接的网络,因此如果您仔细编写程序,它们可以在任何套接字家族上工作。本书不讨论其他套接字家族的范围,尽管我们应该提到原始套接字,它是互联网套接字家族的一个子类型,允许直接发送和接收链路层数据包(例如以太网数据包)。这对于一些实验性应用和数据包嗅探很有用。

创建互联网套接字后,您可以将特定的端口号与套接字关联(只要该端口号没有被其他套接字使用)。这是许多服务器使用的策略,提供所谓的众所周知的端口号,由互联网标准定义为 1-1,023 范围内的端口号。在 Unix 系统上,需要root权限才能访问这些端口。典型的客户端不关心使用的端口号,因此通常请求由协议驱动程序分配并保证在主机上唯一的临时端口。无需绑定客户端端口。

考虑同一台计算机上的两个进程,每个进程都作为同一个远程服务器的客户端。它们套接字的完整关联具有五个组成部分,(本地 IP 地址、本地端口号、协议、远程 IP 地址、远程端口号)。当数据包到达远程服务器时,目标 IP 地址、源 IP 地址、目标端口号和协议对两个客户端来说是相同的。短暂端口号的唯一性保证了服务器能区分来自两个客户端的流量。这就是 TCP/IP 处理同一对 IP 地址之间的多个会话的方式。¹

套接字地址

不同类型的套接字使用不同的地址格式:

  • Unix 套接字地址是命名文件系统中节点的字符串(在 Linux 平台上,是以 b'\0' 开头的字节串,并对应内核表中的名称)。

  • IPv4 套接字地址是 (地址, 端口) 对。第一项是 IPv4 地址,第二项是范围在 1 到 65,535 的端口号。

  • IPv6 套接字地址是四项 (地址, 端口, 流信息, 作用域 ID) 元组。在提供地址作为参数时,通常可以省略 流信息作用域 ID,只要 地址作用域 不重要即可。

客户端/服务器计算

我们接下来讨论的模式通常称为 客户端/服务器 网络,其中 服务器 在特定端点上监听来自需要服务的 客户端 的流量。我们不涵盖 点对点 网络,因为缺少任何中央服务器,必须包含对等方发现的能力。

大多数(虽然并非全部)网络通信是通过客户端/服务器技术进行的。服务器在预定或公布的网络端点处监听传入的流量。在缺少此类输入时,它不做任何操作,只是坐在那里等待来自客户端的输入。连接在面向无连接和面向连接的端点之间的通信有所不同。

在面向无连接的网络中(例如通过 UDP),请求随机到达服务器并立即处理:响应立即发送给请求者。每个请求单独处理,通常不参考先前在两方之间可能发生的任何通信。面向无连接的网络非常适合短期无状态交互,例如 DNS 或网络引导所需的交互。

在面向连接的网络中,客户端与服务器进行初始交换,有效地在两个进程之间建立了网络管道上的连接(有时称为虚拟电路),在这些进程可以进行通信,直到两者表示愿意结束连接。在这种情况下,服务需要使用并行处理(通过线程、进程或异步机制:参见第十五章)来异步或同时处理每个传入的连接。如果没有并行处理,服务器将无法在早期的连接终止之前处理新的传入连接,因为对套接字方法的调用通常会阻塞(即它们会暂停调用它们的线程,直到它们终止或超时)。连接是处理诸如邮件交换、命令行交互或传输 Web 内容等长时间交互的最佳方法,并在使用 TCP 时提供自动错误检测和纠正。

无连接的客户端和服务器结构。

无连接服务器的整体逻辑流程如下:

  1. 通过调用 socket.socket 创建类型为 socket.SOCK_DGRAM 的套接字。

  2. 通过调用套接字的 bind 方法将套接字与服务端点关联。

  3. 重复以下步骤无限期

    1. 通过调用套接字的 recvfrom 方法请求来自客户端的传入数据报;此调用会阻塞,直到接收到数据报。

    2. 计算或查找结果。

    3. 通过调用套接字的 sendto 方法将结果发送回客户端。

服务器大部分时间都在步骤 3a 中等待来自客户端的输入。

无连接客户端与服务器的交互如下:

  1. 通过调用 socket.socket 创建类型为 socket.SOCK_DGRAM 的套接字。

  2. 可选地,通过调用套接字的 bind 方法将套接字与特定端点关联。

  3. 通过调用套接字的 sendto 方法向服务器端点发送请求。

  4. 通过调用套接字的 recvfrom 方法等待服务器的回复;此调用会阻塞,直到收到响应。必须对此调用应用超时,以处理数据报丢失的情况,程序必须重试或中止尝试:无连接套接字不保证传递。

  5. 在客户端程序的剩余逻辑中使用结果。

单个客户端程序可以与同一个或多个服务器执行多次交互,具体取决于需要使用的服务。许多这样的交互对应用程序员来说是隐藏在库代码中的。一个典型的例子是将主机名解析为适当的网络地址,通常使用 gethostbyname 库函数(在 Python 的 socket 模块中实现,稍后讨论)。无连接的交互通常涉及向服务器发送一个数据包并接收一个响应数据包。主要的例外情况涉及协议,如实时传输协议(RTP),² 这些协议通常构建在 UDP 之上,以最小化延迟和延迟:在流式传输中,发送和接收许多数据报。

连接导向客户端和服务器结构

连接导向服务器的逻辑流程如下:

  1. 通过调用 socket.socket 创建 socket.SOCK_STREAM 类型的套接字。

  2. 通过调用套接字的 bind 方法将套接字与适当的服务器端点关联起来。

  3. 通过调用套接字的 listen 方法开始端点监听连接请求。

  4. 无限重复以下步骤ad infinitum

    1. 通过调用套接字的 accept 方法等待传入的客户端连接;服务器进程会阻塞,直到收到传入的连接请求。当这样的请求到达时,会创建一个新的套接字对象,其另一个端点是客户端程序。

    2. 创建一个新的控制线程或进程来处理这个特定的连接,将新创建的套接字传递给它;主控线程然后通过回到步骤 4a 来继续。

    3. 在新的控制线程中,使用新套接字的 recvsend 方法与客户端进行交互,分别用于从客户端读取数据和向其发送数据。recv 方法会阻塞,直到从客户端接收到数据(或客户端指示希望关闭连接,在这种情况下 recv 返回空结果)。当服务器希望关闭连接时,可以通过调用套接字的 close 方法来实现,可选择先调用其 shutdown 方法。

服务器大部分时间都在步骤 4a 中等待来自客户端的连接请求。

连接导向客户端的总体逻辑如下:

  1. 通过调用 socket.socket 创建 socket.SOCK_STREAM 类型的套接字。

  2. 可选地,通过调用套接字的 bind 方法将套接字与特定端点关联。

  3. 通过调用套接字的 connect 方法建立与服务器的连接。

  4. 使用套接字的 recv 和 send 方法与服务器进行交互,分别用于从服务器读取数据和向其发送数据。recv 方法会阻塞,直到从服务器接收到数据(或者服务器指示希望关闭连接,在这种情况下,recv 调用会返回空结果)。send 方法仅在网络软件缓冲区有大量数据时才会阻塞,导致通信暂停,直到传输层释放部分缓冲内存。当客户端希望关闭连接时,可以调用套接字的 close 方法,可选地先调用其 shutdown 方法。

面向连接的交互通常比无连接的更复杂。具体来说,确定何时读取和写入数据更加复杂,因为必须解析输入以确定何时传输完毕。在面向连接的网络中使用的更高层协议适应了这种确定性;有时通过在内容中指示数据长度来实现,有时则采用更复杂的方法。

socket 模块

Python 的 socket 模块通过 socket 接口处理网络。虽然各平台间有些许差异,但该模块隐藏了大部分差异,使得编写可移植的网络应用相对容易。

该模块定义了三个异常类,均为内置异常类 OSError 的子类(见 Table 18-1)。

Table 18-1. socket 模块异常类

herror 用于识别主机名解析错误:例如,socket.gethostbyname 无法将名称转换为网络地址,或者 socket.gethostbyaddr 找不到网络地址对应的主机名。相关的值是一个两元组(h_errnostring),其中 h_errno 是来自操作系统的整数错误号,string 是错误的描述。
gaierror 用于识别在 socket.getaddrinfo 或 socket.getnameinfo 中遇到的地址解析错误。
timeout 当操作超过超时限制时(依据 socket.setdefaulttimeout,可以在每个套接字上覆盖),引发此异常。

该模块定义了许多常量。其中最重要的是地址家族(AF_)和套接字类型(SOCK_),列在 Table 18-2 中,作为 IntEnum 集合的成员。此外,该模块还定义了许多其他用于设置套接字选项的常量,但文档未对其进行详细定义:要使用它们,您必须熟悉 C 套接字库和系统调用的文档。

Table 18-2. socket 模块中定义的重要常量

AF_BLUETOOTH 用于创建蓝牙地址家族的套接字,用于移动和个人区域网络(PAN)应用中。
AF_CAN 用于创建 Controller Area Network (CAN) 地址家族的套接字,在自动化、汽车和嵌入式设备应用中广泛使用。
AF_INET 用于创建 IPv4 地址族的套接字。
AF_INET6 用于创建 IPv6 地址族的套接字。
AF_UNIX 用于创建 Unix 地址族的套接字。此常量仅在支持 Unix 套接字的平台上定义。
SOCK_DGRAM 用于创建无连接套接字,提供尽力而为的消息传递,无连接能力或错误检测。
SOCK_RAW 用于创建直接访问链路层驱动程序的套接字;通常用于实现较低级别的网络功能。
SOCK_RDM 用于创建在透明进程间通信(TIPC)协议中使用的可靠连接的无连接消息套接字。
SOCK_SEQPACKET 用于创建在 TIPC 协议中使用的可靠连接的面向连接的消息套接字。
SOCK_STREAM 用于创建面向连接的套接字,提供完整的错误检测和修正功能。

该模块定义了许多函数来创建套接字、操作地址信息,并辅助标准数据的表示。本书未涵盖所有函数,因为套接字模块的文档非常全面;我们只处理编写网络应用程序中必需的部分。

套接字模块包含许多函数,其中大多数仅在特定情况下使用。例如,当网络端点之间进行通信时,端点可能存在架构差异,并以不同方式表示相同的数据,因此存在处理有限数据类型转换的函数,以及从网络中立形式转换的函数。Table 18-3 列出了此模块提供的一些更普遍适用的函数。

Table 18-3. 套接字模块的有用函数

getaddrinfo socket.getaddrinfo(host, port, family=0, type=0, proto=0, flags=0) 接受hostport,返回形如(family, type, proto, canonical_name, socket)的五元组列表,可用于创建到特定服务的套接字连接。当您传递主机名而不是 IP 地址时,getaddrinfo 返回一个元组列表,每个 IP 地址与名称关联。
getdefa⁠u⁠l⁠t​t⁠i⁠m⁠eout socket.getdefaulttimeout() 返回套接字操作的默认超时值(以秒为单位),如果未设置值则返回None。某些函数允许您指定显式超时。
getfqdn socket.getfqdn([host]) 返回与主机名或网络地址关联的完全限定域名(默认情况下是调用它的计算机的域名)。
gethostbyaddr socket.gethostbyaddr(ip_address) 接受包含 IPv4 或 IPv6 地址的字符串,并返回一个形如 (hostname, aliaslist, ipaddrlist) 的三元组。 hostname 是 IP 地址的规范名称,aliaslist 是一个替代名称列表,ipaddrlist 是一个 IPv4 和 IPv6 地址列表。
gethostbyname socket.gethostbyname(hostname) 返回一个包含与给定主机名关联的 IPv4 地址的字符串。如果使用 IP 地址调用,则返回该地址。此函数不支持 IPv6:请使用 getaddrinfo 获取 IPv6。
getnameinfo socket.getnameinfo(sock_addr, flags=0) 接受套接字地址并返回一个 (host, port) 对。没有标志*,* host 是一个 IP 地址,port 是一个整数。
setdefaulttimeout socket.setdefaulttimeout(timeout) 将套接字的默认超时设置为浮点秒值。新创建的套接字按照 timeout 值确定的模式运行,如下一节所述。将 timeout 作为 None 传递以取消随后创建的套接字上的隐式超时使用。

Socket 对象

socket 对象是 Python 中网络通信的主要手段。当 SOCK_STREAM 套接字接受连接时,也会创建一个新的套接字,每个这样的套接字都用于与相应的客户端通信。

Socket 对象和 with 语句

每个 socket 对象都是上下文管理器:您可以在 with 语句中使用任何 socket 对象,以确保在退出语句体时正确终止套接字。有关详细信息,请参见“with 语句和上下文管理器”。

有几种创建 socket 的方式,如下一节所述。根据超时值,套接字可以在三种不同的模式下运行,如表 18-4 所示,可以通过不同的方式设置超时值:

  • 通过在创建 socket 时提供超时值作为参数

  • 通过调用 socket 对象的 settimeout 方法

  • 根据 socket 模块的默认超时值,由 socket.getdefaulttimeout 函数返回

建立每种可能模式的超时值列在 表 18-4 中。

表 18-4. 超时值及其关联模式

None 设置 阻塞 模式。每个操作都会暂停线程(阻塞),直到操作完成,除非操作系统引发异常。
0 设置 非阻塞 模式。每个操作在无法立即完成或发生错误时引发异常。使用 selectors 模块 查找操作是否可以立即完成。
>0.0 设置 超时 模式。每个操作都会阻塞,直到完成,或超时(在这种情况下会引发 socket.timeout 异常),或发生错误。

套接字对象表示网络端点。socket 模块提供了多个函数来创建套接字(参见 表 18-5)。

表 18-5. 套接字创建函数

| cre⁠a⁠t⁠e⁠_​c⁠o⁠n⁠n⁠e⁠c⁠t⁠i⁠o⁠n | create_connection([address[, timeout[, source_address]]]) 创建一个连接到地址(一个(host, port)对)的 TCP 端点的套接字。host 可以是数字网络地址或 DNS 主机名;在后一种情况下,将尝试为 AF_INET 和 AF_INET6(顺序不确定)进行名称解析,然后依次尝试连接返回的每个地址——这是创建既能使用 IPv6 又能使用 IPv4 的客户端程序的便捷方式。

timeout 参数(如果提供)指定连接超时时间(单位为秒),从而设置套接字的模式(参见表 18-4);当参数不存在时,将调用 socket.getdefaulttimeout 函数来确定该值。如果提供 source_address 参数,那么它也必须是一个(host, port)对,远程套接字将其作为连接端点传递。当 host 为 '' 或 port 为 0 时,将使用默认的操作系统行为。

| socket | socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)

创建并返回适当地址族和类型的套接字(默认为 IPv4 上的 TCP 套接字)。子进程不会继承创建的套接字。协议编号 proto 仅在 CAN 套接字中使用。当传递 fileno 参数时,将忽略其他参数:函数返回已关联给定文件描述符的套接字。

socketpair socketpair([family[, type[, proto]]]) 返回给定地址族、套接字类型和(仅对于 CAN 套接字)协议的连接对套接字。当未指定 family 时,在支持该族的平台上,套接字为 AF_UNIX;否则,它们为 AF_INET。当未指定 type 时,默认为 SOCK_STREAM。

套接字对象 s 提供了 表 18-6 中列出的方法。那些涉及连接或需要已连接套接字的方法仅适用于 SOCK_STREAM 套接字,而其他方法适用于 SOCK_STREAM 和 SOCK_DGRAM 套接字。对于带有 flags 参数的方法,可用的确切标志集取决于您的特定平台(可用值在 Unix 手册页面的 recv(2)send(2) 中以及 Windows 文档 中有记录);如果省略,flags 默认为 0。

表 18-6. 套接字实例 s 的方法

accept accept() 阻塞直到客户端与 s 建立连接(s 必须已绑定到一个地址(通过调用 s.bind)并设置为侦听状态(通过调用 s.listen))。返回一个套接字对象,可用于与连接的另一端点通信。
bind bind(address) 将 s 绑定到特定地址。address 参数的形式取决于套接字的地址族(参见“套接字地址”)。
close close() 标记套接字为关闭状态。调用 s.close 并不一定会立即关闭连接,这取决于是否还有对套接字的其他引用。如果需要立即关闭连接,首先调用 s.shutdown 方法。确保套接字及时关闭的最简单方法是在 with 语句中使用它,因为套接字是上下文管理器。
connect connect(address) 连接到地址为 address 的远程套接字。address 参数的形式取决于地址族(参见“套接字地址”)。
detach detach() 将套接字置于关闭模式,但允许套接字对象用于进一步的连接(通过再次调用 connect)。
dup dup() 返回套接字的副本,不能被子进程继承。
fileno fileno() 返回套接字的文件描述符。
getblocking getblocking() 如果套接字被设置为阻塞模式,则返回True,可以通过调用 s.setblocking(True) 或 s.settimeout(None) 进行设置。否则,返回False
g⁠e⁠t⁠_​i⁠n⁠h⁠e⁠r⁠i⁠t⁠a⁠b⁠l⁠e get_inheritable() 当套接字能够被子进程继承时返回True。否则,返回False
getpeername getpeername() 返回此套接字连接的远程端点的地址。
getsockname getsockname() 返回此套接字正在使用的地址。
gettimeout gettimeout() 返回与此套接字关联的超时时间。
listen listen([backlog]) 开始监听套接字的关联端点上的流量。如果给定,整数参数 backlog 确定操作系统在开始拒绝连接之前允许排队的未接受连接数量。
makefile makefile(mode, buffering=None, *, encoding=None, newline=None) 返回一个文件对象,允许套接字用于类似文件的操作,如读和写。参数类似于内置的 open 函数(参见“使用 open 创建文件对象”)。mode 可以是 'r' 或 'w';对于二进制传输,可以添加 'b'。套接字必须处于阻塞模式;如果设置了超时值,当超时发生时可能会观察到意外的结果。
recv recv(bufsiz[, flags]) 接收并返回来自套接字 s 的最多 bufsiz 字节数据。
recvfrom recvfrom(bufsiz[, flags]) 从 s 接收最多 bufsiz 字节的数据。返回一对(bytesaddress):bytes 是接收到的数据,address 是发送数据的对方套接字的地址。
recvfrom_into recvfrom_into(buffer[, nbytes[, flags]]) 从 s 接收最多 nbytes 字节的数据,并将其写入给定的 buffer 对象中。如果省略 nbytes 或为 0,则使用 len(buffer)。返回一个二元组 (nbytes, address):nbytes 是接收的字节数,address 是发送数据的对方套接字的地址(**_into* 函数比分配新缓冲区的“普通”函数更快)。
recv_into recv_into(buffer[, nbytes[, flags]]) 从 s 接收最多 nbytes 字节的数据,并将其写入给定的 buffer 对象中。如果省略 nbytes 或为 0,则使用 len(buffer)。返回接收的字节数。
recvmsg recvmsg(bufsiz[, ancbufsiz[, flags]]) 在套接字上接收最多 bufsiz 字节的数据和最多 ancbufsiz 字节的辅助(“带外”)数据。返回一个四元组 (data, ancdata, msg_flags, address),其中 data 是接收的数据,ancdata 是表示接收的辅助数据的三元组 (cmsg_level, cmsg_type, cmsg_data) 列表,msg_flags 包含与消息一起接收的任何标志(在 Unix 手册页中记录了 recv(2) 系统调用或 Windows 文档中有详细说明),address 是发送数据的对方套接字的地址(如果套接字已连接,则此值未定义,但可以从套接字中确定发送方)。
send send(bytes[, flags]) 将给定的数据 bytes 发送到已连接到远程端点的套接字上。返回发送的字节数,应检查:调用可能不会传输所有数据,此时必须单独请求剩余部分的传输。
sendall sendall(bytes[, flags]) 将所有给定的数据 bytes 发送到已连接到远程端点的套接字上。套接字的超时值适用于所有数据的传输,即使需要多次传输也是如此。
sendfile sendfile(file, offset=0, count=None) 将文件对象 file 的内容(必须以二进制模式打开)发送到连接的端点。在支持 os.sendfile 的平台上,使用该函数;否则,使用 send 调用。如果指定了 offset,则确定从文件中哪个字节位置开始传输;count 设置要传输的最大字节数。返回传输的总字节数。
sendmsg sendmsg(buffers[, ancdata[, flags[, address]]]) 向连接的端点发送普通和辅助(带外)数据。 buffers 应该是类字节对象的可迭代对象。 ancdata 参数应该是 (data, ancdata, msg_flags, address) 元组的可迭代对象,表示辅助数据。 msg_flags 是在 Unix 手册页上的 send(2) 系统调用或在 Windows 文档 中记录的标志位。 address 应仅在未连接的套接字中提供,并确定要发送数据的端点。
sendto sendto(bytes,[flags,]address) 将 bytess 不能连接)传输到给定的套接字地址,并返回发送的字节数。 可选的 flags 参数与 recv 中的含义相同。
setblocking setblocking(flag) 根据 flag 的真值确定 s 是否以阻塞模式运行(见 “套接字对象”)。 s.setblocking(True) 的作用类似于 s.settimeout(None); s.set_blocking(False) 的作用类似于 s.settimeout(0.0)。
set_inheritable set_inheritable(flag) 根据 flag 的真值确定套接字是否由子进程继承。
settimeout settimeout(timeout) 根据 timeout 的值(见 “套接字对象”)建立 s 的模式。

| shutdown | shutdown(how) 根据 how 参数的值关闭套接字连接的一个或两个部分,如此处详细说明:

socket.SHUT_RD

s 上不能再执行更多的接收操作。

socket.SHUT_RDWR

s 上不能再执行更多的接收或发送操作。

socket.SHUT_WR

s 上不能再执行更多的发送操作。

|

套接字对象 s 还具有属性 family(s 的套接字家族)和 type(s 的套接字类型)。

无连接套接字客户端

考虑一个简单的数据包回显服务,在这个服务中,客户端将使用 UTF-8 编码的文本发送到服务器,服务器将相同的信息返回给客户端。 在无连接服务中,客户端只需将每个数据块发送到定义的服务器端点:

`import` socket

UDP_IP = 'localhost'
UDP_PORT = 8883
MESSAGE = """\ This is a bunch of lines, each
of which will be sent in a single
UDP datagram. No error detection
or correction will occur.
Crazy bananas! £€ should go through."""

server = UDP_IP, UDP_PORT
encoding = 'utf-8'
`with` socket.socket(socket.AF_INET,    *`# IPv4`*
                   socket.SOCK_DGRAM, *`# UDP`*
                  ) `as` sock:
 `for` line `in` MESSAGE.splitlines():
        data = line.encode(encoding)
        bytes_sent = sock.sendto(data, server)
        print(f'SENT {data!r} ({bytes_sent} of {len(data)})'
		      f' to {server}')
        response, address = sock.recvfrom(1024)  *`# buffer size: 1024`*
        print(f'RCVD {response.decode(encoding)!r}'
              f' from {address}')

print('Disconnected from server')

请注意,服务器仅执行基于字节的回显功能。 因此,客户端将其 Unicode 数据编码为字节串,并使用相同的编码将从服务器接收的字节串响应解码为 Unicode 文本。

无连接套接字服务器

前一节描述的数据包回显服务的服务器也非常简单。 它绑定到其端点,在该端点接收数据包(数据报),并向客户端返回每个数据报具有完全相同数据的数据包。 服务器平等地对待所有客户端,不需要使用任何类型的并发(尽管这种最后一个方便的特性可能不适用于处理请求时间更长的服务)。

以下服务器工作,但除了通过中断(通常从键盘上的 Ctrl-C 或 Ctrl-Break)无其他终止服务的方法:

`import` socket

UDP_IP = 'localhost'
UDP_PORT = 8883
with socket.socket(socket.AF_INET,    *`# IPv4`*
                   socket.SOCK_DGRAM  *`# UDP`*
                   ) as sock:
    sock.bind((UDP_IP, UDP_PORT))
    print(f'Serving at {UDP_IP}:{UDP_PORT}')
 `while` `True`:
        data, sender_addr = sock.recvfrom(1024)  *`# 1024-byte buffer`*
        print(f'RCVD {data!r}) from {sender_addr}')
        bytes_sent = sock.sendto(data, sender_addr)
        print(f'SENT {data!r} ({bytes_sent}/{len(data)})'
              f' to {sender_addr}')

同样没有任何机制来处理丢包和类似的网络问题;这在简单服务中通常是可以接受的。

可以使用 IPv6 运行相同的程序:只需将套接字类型 AF_INET 替换为 AF_INET6。

面向连接的套接字客户端

现在考虑一个简单的面向连接的“回显式”协议:服务器允许客户端连接到其监听套接字,从客户端接收任意字节,并将服务器接收到的相同字节发送回每个客户端,直到客户端关闭连接。以下是一个基本测试客户端的示例:³

`import` socket

IP_ADDR = 'localhost'
IP_PORT = 8881
MESSAGE = """\ A few lines of text
including non-ASCII characters: €£
to test the operation
of both server
and client."""

encoding = 'utf-8'
`with` socket.socket(socket.AF_INET,     *`# IPv4`*
                   socket.SOCK_STREAM  *`# TCP`*
                   ) `as` sock:
    sock.connect((IP_ADDR, IP_PORT))
    print(f'Connected to server {IP_ADDR}:{IP_PORT}')
    `for` line `in` MESSAGE.splitlines():
        data = line.encode(encoding)
        sock.sendall(data)
        print(f'SENT {data!r} ({len(data)})')
        response, address = sock.recvfrom(1024)  *`# buffer size: 1024`*
        print(f'RCVD {response.decode(encoding)!r}'
              f' ({len(response)}) from {address}')

print('Disconnected from server')

注意数据是文本,因此必须用适当的表示方法进行编码。我们选择了常见的 UTF-8 编码。服务器以字节为单位工作(因为是字节(也称为八位字节)在网络上传输);接收到的字节对象在打印之前会用 UTF-8 解码为 Unicode 文本。也可以选择其他合适的编解码器:关键是在传输之前对文本进行编码,在接收后进行解码。服务器在字节方面工作,甚至不需要知道使用的是哪种编码,除了可能用于日志记录之类的目的。

面向连接的套接字服务器

这是一个简单的服务器,对应于前一节中显示的测试客户端,使用并发.future 进行多线程处理(详见“concurrent.futures 模块”):

`import` concurrent
`import` socket

IP_ADDR = 'localhost'
IP_PORT = 8881

`def` handle(new_sock, address):
    print('Connected from', address)
    `with` new_sock:
        `while` True:
            received = new_sock.recv(1024)
            `if` `not` received:
                `break`
            s = received.decode('utf-8', errors='replace')
            print(f'Recv: {s!r}')
            new_sock.sendall(received)
            print(f'Echo: {s!r}')
    print(f'Disconnected from {address}')

`with` socket.socket(socket.AF_INET,     # IPv4 
                   socket.SOCK_STREAM  # TCP
                   ) `as` servsock:
    servsock.bind((IP_ADDR, IP_PORT))
    servsock.listen(5)
    print(f'Serving at {servsock.getsockname()}')
    `with` cconcurrent.futures.ThreadPoolExecutor(20) `as` e:
        `while` True:
            new_sock, address = servsock.accept()
            e.submit(handle, new_sock, address)

此服务器有其局限性。特别是,它仅运行 20 个线程,因此无法同时为超过 20 个客户端提供服务;当 20 个其他客户端正在接受服务时,试图连接的进一步客户端将等待在 servsock 的监听队列中。如果队列填满了等待接受的五个客户端,试图连接的进一步客户端将被直接拒绝。此服务器仅作为演示示例而非坚固、可扩展或安全系统。

与之前一样,可以通过将套接字类型 AF_INET 替换为 AF_INET6 来使用 IPv6 运行相同的程序。

传输层安全性

传输层安全性(TLS)是安全套接字层(SSL)的后继者,提供 TCP/IP 上的隐私和数据完整性,帮助防止服务器冒充、窃听交换的字节以及恶意修改这些字节。对于 TLS 的介绍,我们推荐阅读广泛的Wikipedia 条目

在 Python 中,您可以通过标准库的 ssl 模块使用 TLS。要很好地使用 ssl,您需要深入理解其丰富的在线文档,以及 TLS 本身的深入广泛理解(尽管作为一个庞大而复杂的主题,维基百科的文章只是开始涵盖这个话题)。特别是,您必须研究并彻底理解在线文档安全考虑部分,以及该部分提供的众多有用链接中的所有材料。

如果这些警告使您觉得完美实施安全预防措施是一项艰巨的任务,那是因为它确实如此。在安全领域,您需要将智慧和技能与那些可能更熟悉所涉问题细节的高级攻击者的智慧和技能相比较:他们专注于发现漏洞和入侵方法,而您的焦点通常不仅限于此类问题——相反,您试图在代码中提供一些有用的服务。将安全视为事后或次要问题是有风险的——它必须始终处于核心位置,以赢得技术和智慧之战。

尽管如此,我们强烈建议所有读者进行上述 TLS 学习——开发者对安全考虑的理解越深入,我们就越安全(除了那些渴望破解安全系统的人)。

除非您已经深入广泛地了解了 TLS 和 Python 的 ssl 模块(在这种情况下,您会知道应该做什么——比我们可能的建议更好!),我们建议使用 SSLContext 实例来保存 TLS 使用的所有细节。使用 ssl.create_default_context 函数构建该实例,如果需要,添加您的证书(如果您正在编写安全服务器,则需要这样做),然后使用实例的 wrap_socket 方法将您创建的每个 socket.socket 实例包装成 ssl.SSLSocket 实例——它的行为几乎与包装的 socket 对象相同,但几乎透明地添加了安全检查和验证“在一侧”。

默认的 TLS 上下文在安全性和广泛适用性之间取得了良好的折衷,我们建议您坚持使用它们(除非您足够了解以便为特殊需求调整和加强安全性)。如果您需要支持无法使用最新、最安全的 TLS 实现的过时对应方案,您可能会有种放松安全要求的冲动。但请自行承担风险——我们绝对不建议冒险进入这样的领域!

在接下来的章节中,我们将介绍您在仅想遵循我们建议时需要熟悉的 ssl 的最小子集。但即使是这种情况,也请务必阅读有关 TLS 和 ssl 的内容,以便对所涉及的复杂问题有所了解。这可能在某一天对您大有裨益!

SSLContext

ssl 模块提供了 ssl.SSLContext 类,其实例保存关于 TLS 配置的信息(包括证书和私钥),并提供许多方法来设置、更改、检查和使用这些信息。如果你确切知道自己在做什么,你可以手动实例化、设置和使用自己专门目的的 SSLContext 实例。

但是,我们建议你使用经过良好调整的函数 ssl.create_default_context 实例化 SSLContext,只需一个参数:ssl.Purpose.CLIENT_AUTH 如果你的代码是服务器(因此可能需要对客户端进行认证),或者 ssl.Purpose.SERVER_AUTH 如果你的代码是客户端(因此绝对需要对服务器进行认证)。如果你的代码既是某些服务器的客户端又是其他客户端的服务器(例如一些互联网代理),那么你将需要两个 SSLContext 实例,每个目的一个。

对于大多数客户端使用场景,你的 SSLContext 已经准备好了。如果你在编写服务器端或者一个少数需要对客户端进行 TLS 认证的服务器的客户端,你需要有一个证书文件和一个密钥文件(参见在线文档了解如何获取这些文件)。通过将证书和密钥文件的路径传递给 load_cert_chain 方法,将它们添加到 SSLContext 实例中(以便对方可以验证你的身份),例如:

ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ctx.load_cert_chain(certfile='mycert.pem', keyfile='mykey.key')

一旦你的上下文实例 ctx 准备好了,如果你在编写客户端,只需调用 ctx.wrap_socket 来包装你即将连接到服务器的任何套接字,并使用包装后的结果(一个 ssl.SSLSocket 实例)而不是刚刚包装的套接字。例如:

sock = socket.socket(socket.AF_INET)
sock = ctx.wrap_socket(sock, server_hostname='www.example.com')
sock.connect(('www.example.com', 443))  
*`# use 'sock' normally from here on`*

注意,在客户端的情况下,你还应该传递一个 server_hostname 参数给 wrap_socket,该参数对应你即将连接的服务器;这样,连接可以验证你最终连接到的服务器的身份是否确实正确,这是绝对关键的安全步骤。

在服务器端,不要 包装绑定到地址、监听或接受连接的套接字;只需包装 accept 返回的新套接字。例如:

sock = socket.socket(socket.AF_INET)
sock.bind(('www.example.com', 443))
sock.listen(5)
`while` `True``:`
    newsock, fromaddr = sock.accept()
    newsock = ctx.wrap_socket(newsock, server_side=`True`)
    *`# deal with 'newsock' as usual; shut down, then close it, when done`*

在这种情况下,你需要向 wrap_socket 传递参数 server_side=True,以便它知道你是服务器端的操作。

再次强烈建议查阅在线文档,尤其是示例,以便更好地理解,即使你仅仅使用 SSL 操作的这个简单子集。

¹ 当你编写应用程序时,通常会通过更高级别的抽象层(如第十九章中涵盖的那些)来使用套接字。

² 还有相对较新的多路复用连接传输协议 QUIC,在 Python 中由第三方 aioquic 支持。

³ 这个客户端示例并不安全;参见“传输层安全性”了解如何使其安全。

⁴ 我们说“几乎”是因为,当你编写服务器时,你不会包装你绑定、监听和接受连接的套接字。

第十九章:客户端网络协议模块

Python 的标准库提供了几个模块,简化了客户端和服务器端使用互联网协议的过程。如今,被称为 PyPIPython 包索引 提供了更多类似的包。因为许多标准库模块可以追溯到上个世纪,您会发现现在第三方包支持更多的协议,并且一些比标准库提供的 API 更好。当您需要使用标准库中缺少的或者您认为标准库中的方式不尽如人意的网络协议时,请务必搜索 PyPI——您很可能会在那里找到更好的解决方案。

本章介绍了一些标准库包,允许相对简单地使用网络协议:这些包使您可以在不需要第三方包的情况下编码,使您的应用程序或库更容易安装在其他计算机上。因此,在处理遗留代码时可能会遇到它们,它们的简单性也使它们成为 Python 学习者感兴趣的阅读材料。

对于 HTTP 客户端和其他最好通过 URL 访问的网络资源(如匿名 FTP 站点)的频繁使用情况,第三方 requests 包 甚至被 Python 文档推荐,因此我们涵盖了它并推荐使用,而不是使用标准库模块。

电子邮件协议

今天,大多数电子邮件是通过实现简单邮件传输协议(SMTP)的服务器发送,并通过使用邮局协议版本 3(POP3)和/或互联网消息访问协议版本 4(IMAP4)的服务器和客户端接收。¹ 这些协议的客户端由 Python 标准库模块 smtplib、poplib 和 imaplib 支持,我们在本书中介绍了前两者。当您需要处理电子邮件消息的解析生成时,请使用电子邮件包,本书在 第二十一章 中有所介绍。

如果你需要编写一个既可以通过 POP3 也可以通过 IMAP4 连接的客户端,一个标准的建议是选择 IMAP4,因为它更强大,并且——根据 Python 自己的在线文档——通常在服务器端实现得更精确。不幸的是,imaplib 非常复杂,远超过本书的涵盖范围。如果你选择这条路,需要使用在线文档,并且不可避免地要补充 IMAP RFCs,可能还包括其他相关的 RFCs,如 5161 和 6855(用于能力)以及 2342(用于命名空间)。除了标准库模块的在线文档外,还必须使用 RFCs:imaplib 函数和方法传递的许多参数以及调用它们的结果,都是字符串格式,只有 RFCs 中有详细说明,而 Python 的文档中没有。一个强烈推荐的替代方案是使用更简单、更高抽象级别的第三方包IMAPClient,可以通过pip install安装,并且有很好的在线文档

poplib 模块

poplib 模块提供了一个访问 POP 邮箱的 POP3 类。² 构造函数具有以下签名:

POP3 class POP3(host, port=110) 返回一个连接到指定host和端口的 POP3 类实例p。类 POP3_SSL 的行为完全相同,但通过安全的 TLS 通道连接到主机(默认端口 995);这对需要连接到要求一定最小安全性的电子邮件服务器(如 pop.gmail.com)是必需的。^(a)
^(a) 要连接到 Gmail 账户,特别是,你需要配置该账户以启用 POP,“允许不安全的应用程序”,并避免两步验证——这些一般情况下我们不推荐,因为它们会削弱你的电子邮件安全性。

类 POP3 的实例p提供了许多方法;最常用的列在表 19-1 中。在每种情况下,msgnum,一个消息的标识号,可以是包含整数值的字符串或整数。

表 19-1. POP3 类实例p的方法

dele p.dele(msgnum) 标记消息msgnum以便删除,并返回服务器响应字符串。服务器会排队这样的删除请求,只有在你通过调用p.quit 终止连接时才执行。^(a)
list p.list(msgnum=None) 返回一个三元组(response, messages, octets),其中response是服务器响应字符串;messages是一个由字节串组成的列表,每个字节串由两个词组成 b'msgnum bytes',每个消息的消息编号和字节长度;octets是总响应的字节长度。当msgnum不为None时,list 返回一个字符串,给定msgnum的响应,而不是一个元组。
pass_ p.pass_(password) 向服务器发送密码,并返回服务器响应字符串。必须在 p.user 之后调用。名称中的下划线是因为 pass 是 Python 的关键字。
quit p.quit() 结束会话并告知服务器执行调用 p.dele 请求的删除操作。返回服务器响应字符串。
retr p.retr(msgnum) 返回一个三元组(response, lines, bytes),其中 response 是服务器响应字符串,lines 是消息 msgnum 的所有行的列表(以字节串形式),bytes 是消息的总字节数。
s⁠e⁠t⁠_​d⁠e⁠b⁠u⁠g⁠l⁠e⁠v⁠e⁠l p.set_debuglevel(debug_level) 将调试级别设置为 debug_level,一个整数,值为 0(默认)表示无调试输出,1 表示适量的调试输出,2 或更高表示所有与服务器交换的控制信息的完整输出跟踪。
stat p.stat() 返回一个二元组(num_msgs, bytes),其中 num_msgs 是邮箱中的消息数,bytes 是总字节数。
top p.top(msgnum, maxlines) 类似 retr,但最多返回消息体的 maxlines 行(除了所有的头部行)。对于查看长消息开头很有用。
user p.user(username) 向服务器发送用户名;随后必然调用 p.pass_。
^(a) 标准规定,如果在 quit 调用之前发生断开连接,不应执行删除操作。尽管如此,某些服务器在任何断开连接后(计划或非计划)都会执行删除操作。

smtplib 模块

smtplib 模块提供一个 SMTP 类来通过 SMTP 服务器发送邮件。³ 构造函数的签名如下:

SMTP class SMTP([host, port=25]) 返回 SMTP 类的实例 s。当给定 host(和可选的 port)时,隐式调用 s.connect(host, port)。SMTP_SSL 类的行为完全相同,但通过安全的 TLS 通道连接到主机(默认端口 465),用于连接要求一定最小安全性的电子邮件服务器,如 smtp.gmail.com。

SMTP 类的实例 s 提供许多方法。其中最常用的列在 表 19-2 中。

表 19-2. SMTP 实例 s 的方法

connect s.connect(host=127.0.0.1, port=25) 连接到给定主机(默认为本地主机)和端口的 SMTP 服务器(SMTP 服务的默认端口为 25;更安全的“SMTP over TLS”默认端口为 465)。
login s.login(user, password) 使用给定的 userpassword 登录服务器。只有在 SMTP 服务器需要身份验证时才需要(几乎所有服务器都需要)。
quit s.quit() 终止 SMTP 会话。
sendmail s.sendmail(from_addr, to_addrs, msg_string) 从字符串from_addr中发送邮件消息msg_string,并分别发送给列表to_addrs中的每个收件人。(a) msg_string必须是一个完整的 RFC 822 消息,是一个多行的字节字符串:包括头部、用于分隔的空行,然后是正文。邮件传输机制仅使用from_addrto_addrs来确定路由,忽略msg_string中的任何头部。(b) 要准备符合 RFC 822 的消息,请使用 email 包,该包在“MIME 和电子邮件格式处理”中介绍。
send_message s.send_message(msg, from_addr=None, to_addrs=None) 这是一个便捷函数,第一个参数为 email.message.Message 对象。如果from_addrto_addrs中任一或两者为None,则会从消息中提取它们。
(a) 标准并未限制from_addr中收件人的数量,但是各个邮件服务器可能会限制,因此建议每个批处理消息中的收件人数量不要太多。(b) 这样做可以支持邮件系统实现密送(Bcc)邮件,因为路由不依赖于消息信封。

HTTP 和 URL 客户端

绝大多数情况下,您的代码会通过更高级别的 URL 层使用 HTTP 和 FTP 协议,这些协议由下一节介绍的模块和包支持。Python 标准库还提供了较低级别的、特定于协议的模块,不过这些模块使用频率较低:对于 FTP 客户端,使用ftplib;对于 HTTP 客户端,使用 http.client(我们在第二十章介绍 HTTP 服务器)。如果需要编写 FTP 服务器,请考虑第三方模块pyftpdlib。在撰写本书时,较新的HTTP/2实现可能尚不完全成熟,但目前最佳选择是第三方模块HTTPX。我们在本书中不涉及这些较低级别的模块,而是专注于整个下一节的更高级别、URL 级别的访问。

URL 访问

URI 是一种统一资源标识符(URI)的一种类型。URI 是一个字符串,用于标识资源(但不一定定位资源),而 URL 用于在互联网上定位资源。URL 由几个部分(某些可选)组成,称为组件scheme, location, path, queryfragment。(第二个组件有时也被称为网络位置或简称netloc。)具有所有部分的 URL 看起来像这样:

*scheme://lo.ca.ti.on/pa/th?qu=ery#fragment*

例如,在https://www.python.org/community/awards/psf-awards/#october-2016*中,方案为*http*,位置为*www.python.org*,路径为*/community/awards/psf-awards/*,无查询,片段为*#october-2016*。(大多数方案在未明确指定端口时会默认使用一个*众所周知的端口*;例如,HTTP 方案的众所周知端口为 80。)某些标点符号是其分隔的组件的一部分;其他标点字符只是分隔符,并非任何组件的一部分。省略标点符号意味着缺少组件。例如,在mailto:me@you.com中,方案为mailto*,路径为me@you.commailto:me@you.com),无位置、查询或片段。没有//表示 URI 无位置,没有?表示 URI 无查询,没有#表示 URI 无片段。

如果位置以冒号结尾,后跟一个数字,则表示终点的 TCP 端口。否则,连接使用与方案关联的众所周知端口(例如,HTTP 的端口 80)。

urllib 包

urllib 包提供了几个模块来解析和利用 URL 字符串及其相关资源。除了这里描述的 urllib.parse 和 urllib.request 模块外,还包括 urllib.robotparser 模块(专门用于根据RFC 9309解析站点的robots.txt文件)和 urllib.error 模块,包含其他 urllib 模块引发的所有异常类型。

urllib.parse 模块

urllib.parse 模块提供了用于分析和合成 URL 字符串的函数,并通常使用from urllib import parse as urlparse 导入。其最常用的函数列在表 19-3 中。

表 19-3. urllib.parse 模块的常用函数

| urljoin | urljoin(base_url_string, relative_url_string) 返回一个 URL 字符串u,该字符串通过将relative_url_string(可能是相对的)与base_url_string连接而得到。urljoin 执行的连接过程可总结如下:

  • 当任一参数字符串为空时,u即为另一个参数。

  • relative_url_string明确指定与base_url_string不同的方案时,urelative_url_string。否则,u的方案为base_url_string的方案。

  • 当方案不允许相对 URL(例如,mailto)或relative_url_string明确指定位置(即使与base_url_string的位置相同)时,u的所有其他组件均为relative_url_string的组件。否则,u的位置为base_url_string的位置。

  • u的路径通过根据绝对和相对 URL 路径的标准语法连接base_url_stringrelative_url_string的路径而获得。^(a) 例如:

urlparse.urljoin(
  'http://host.com/some/path/here','../other/path')
*`# Result is: 'http://host.com/some/other/path'`*

|

| urlsplit | urlsplit(url_string, default_scheme='', allow_fragments=True) 分析 url_string 并返回一个五个字符串项的元组(实际上是 SplitResult 实例,可以将其视为元组或与命名属性一起使用):schemenetlocpathqueryfragment*。当 allow_fragments 为 False 时,无论 url_string 是否具有片段,元组的最后一项始终为''。对应缺少部分的项也为''。例如:

urlparse.urlsplit(
  'http://www.python.org:80/faq.cgi?src=file')
*`# Result is:`*
*`# 'http','www.python.org:80','/faq.cgi','src=file',''`*

|

| urlunsplit | urlunsplit(url_tuple) url_tuple 是任何具有确切五个项的可迭代对象,全部为字符串。从 urlsplit 调用的任何返回值都是 urlunsplit 的可接受参数。urlunsplit 返回具有给定组件和所需分隔符但无冗余分隔符的 URL 字符串(例如,当 fragment,url_tuple 的最后一项为''时,结果中没有 #)。例如:

urlparse.urlunsplit((
  'http','www.python.org','/faq.cgi','src=fie',''))
*`# Result is: 'http://www.python.org/faq.cgi?src=fie'`*

urlunsplit(urlsplit(x)) 返回 URL 字符串 x 的规范形式,这不一定等于 x,因为 x 不一定是规范化的。例如:

urlparse.urlsplit('http://a.com/path/a?'))
*`# Result is: 'http://a.com/path/a'`*

在这种情况下,规范化确保结果中不存在冗余分隔符,例如在 urlsplit 参数中的尾部 ?。

^(a) 根据 RFC 1808

urllib.request 模块

urllib.request 模块提供了访问标准互联网协议上的数据资源的函数,其中最常用的列在 Table 19-4 中(表中的示例假定您已导入了该模块)。

表 19-4. urllib.request 模块的有用函数

| urlopen | urlopen(url, data=None*,* timeout*,* context=None) 返回一个响应对象,其类型取决于 url 中的方案:

  • HTTP 和 HTTPS URL 返回一个 http.client.HTTPResponse 对象(具有修改的 msg 属性以包含与 reason 属性相同的数据;详情请参阅在线文档)。您的代码可以像处理可迭代对象一样使用此对象,并作为上下文管理器在 with 语句中使用。

  • FTP、文件和数据 URL 返回一个 urllib.response.addinfourl 对象。

    url 是要打开的 URL 的字符串或 urllib.request.Request 对象。data 是一个可选的 bytes 对象、类文件对象或 bytes 的可迭代对象,用于编码发送到 URL 的额外数据,格式为 application/x-www-form-urlencodedtimeout 是一个可选参数,用于指定 URL 打开过程中阻塞操作的超时时间,单位为秒,仅适用于 HTTP、HTTPS 和 FTP URL。当给出 context 时,必须包含一个 ssl.SSLContext 对象,指定 SSL 选项;context 取代了已弃用的 cafilecapathcadefault 参数。以下示例从 HTTPS URL 下载文件并提取为本地的 bytes 对象,unicode_db:

unicode_url = ("`https://www.unicode.org/Public`"
               "/14.0.0/ucd/UnicodeData.txt")
`with` urllib.request.urlopen(unicode_url 
     )`as` url_response:
    unicode_db = url_response.read()

|

| u⁠r⁠l​r⁠e⁠t⁠r⁠i⁠e⁠v⁠e | urlretrieve(url_string, filename=None, report_hook=None, data=None) 一个兼容性函数,用于支持从 Python 2 遗留代码的迁移。url_string 给出要下载资源的 URL。filename 是一个可选的字符串,用于命名从 URL 检索的数据存储在本地文件中。report_hook 是一个可调用对象,支持在下载过程中报告进度,每次检索数据块时调用一次。data 类似于 urlopen 的 data 参数。在其最简单的形式下,urlretrieve 等效于:

`def` urlretrieve(url, filename=`None`):
    `if` filename `is` `None`:
        filename = *`.``.``.``parse filename from url...`*
    `with` urllib.request.urlopen(url 
         )`as` url_response:
        `with` open(filename, "wb") `as` save_file:
            save_file.write(url_response.read())
        `return` filename, url_response.info()

由于这个函数是为了 Python 2 的兼容性而开发的,您可能仍然会在现有的代码库中看到它。新代码应该使用 urlopen。

要全面了解 urllib.request,请参阅在线文档和 Michael Foord 的 HOWTO,其中包括根据 URL 下载文件的示例。在 “使用 BeautifulSoup 进行 HTML 解析的示例” 中有一个使用 urllib.request 的简短示例。

第三方 requests 包

第三方 requests package(非常好地在线文档记录)是我们推荐您访问 HTTP URL 的方式。像其他第三方包一样,最好通过简单的 pip install requests 进行安装。在本节中,我们总结了如何在相对简单的情况下最佳地使用它。

请求模块本地仅支持 HTTP 和 HTTPS 传输协议;要访问其他协议的 URL,您需要安装其他第三方包(称为协议适配器),例如 requests-ftp 用于 FTP URL,或作为丰富的 requests-toolbelt 包的一部分提供的其他实用工具包。

requests 包的功能主要依赖于它提供的三个类:Request,模拟发送到服务器的 HTTP 请求;Response,模拟服务器对请求的 HTTP 响应;以及 Session,提供在一系列请求中的连续性,也称为会话。对于单个请求/响应交互的常见用例,您不需要连续性,因此通常可以忽略 Session。

发送请求

通常情况下,您不需要显式考虑 Request 类:而是调用实用函数 request,它内部准备并发送请求,并返回 Response 实例。request 有两个必需的位置参数,都是字符串:method,要使用的 HTTP 方法,和 url,要访问的 URL。然后,可能会跟随许多可选的命名参数(在下一节中,我们涵盖了 request 函数最常用的命名参数)。

为了进一步方便,requests 模块还提供了一些函数,它们的名称与 HTTP 方法 delete、get、head、options、patch、post 和 put 相同;每个函数都接受一个必需的位置参数 url,然后是与函数 request 相同的可选命名参数。

当您希望在多个请求中保持一致性时,调用 Session 创建一个实例 s,然后使用 s 的方法 request、get、post 等,它们就像直接由 requests 模块提供的同名函数一样(然而,s 的方法将 s 的设置与准备发送到给定 url 的每个请求的可选命名参数合并)。

request 的可选命名参数

函数 request(就像函数 get、post 等以及类 Session 的实例 s 上的同名方法一样)接受许多可选的命名参数。如果您需要高级功能,比如控制代理、身份验证、特殊的重定向处理、流式传输、cookies 等,请参阅 requests 包的优秀在线文档获取完整的参数集合。表 19-5 列出了最常用的命名参数。

表 19-5. request 函数接受的命名参数列表

data 一个字典、一组键值对、一个字节串或者一个类似文件的对象,用作请求的主体
files 一个以名称为键、文件对象或文件元组为值的字典,与 POST 方法一起使用,用于指定多部分编码文件上传(我们将在下一节中讨论文件值的格式)
headers 发送到请求中的 HTTP 头的字典
json Python 数据(通常是一个字典)编码为 JSON 作为请求主体
params 一个(name, value)项的字典,或者作为查询字符串发送的字节串与请求一起
timeout 秒数的浮点数,等待响应的最长时间,在引发异常之前

data、json 和 files 是用于指定请求主体的相互不兼容的方式;通常您应该最多使用其中一个,只用于使用主体的 HTTP 方法(即 PATCH、POST 和 PUT)。唯一的例外是您可以同时使用传递一个字典的 data 参数和一个 files 参数。这是非常常见的用法:在这种情况下,字典中的键值对和文件形成一个请求主体,作为一个multipart/form-data整体。⁴

files 参数(以及其他指定请求主体的方法)

当您使用 json 或 data(传递一个字节串或者一个必须打开以供读取的类文件对象,通常在二进制模式下)指定请求主体时,生成的字节直接用作请求的主体。当您使用 data(传递一个字典或者一组键值对)指定时,主体以表单的形式构建,从键值对按application/x-www-form-urlencoded格式进行格式化,根据相关的网络标准

当你用 files 指定请求的主体时,该主体也作为一个表单构建,在这种情况下格式设置为multipart/form-data(在 PATCH、POST 或 PUT HTTP 请求中上传文件的唯一方式)。你上传的每个文件都被格式化为表单的一个单独部分;如果你还希望表单向服务器提供进一步的非文件参数,则除了 files 外,还需要传递一个数据参数,其值为字典(或键/值对序列)用于这些进一步的参数。这些参数被编码为多部分表单的补充部分。

为了灵活性,files 参数的值可以是一个字典(其条目被视为(name, value)对的序列),或者是(name, value)对的序列(结果请求体中的顺序保持不变)。

无论哪种方式,(name, value)对中的每个值可以是一个 str(或者更好地,⁵ 是一个字节或字节数组),直接用作上传文件的内容,或者是一个打开用于读取的类文件对象(此时,requests 调用 .read() 方法并使用结果作为上传文件的内容;我们强烈建议在这种情况下以二进制模式打开文件,以避免任何关于内容长度的歧义)。当满足这些条件之一时,requests 使用对的 name 部分(例如,字典中的键)作为文件的名称(除非它能够改进这一点,因为打开的文件对象能够显示其基础文件名),尝试猜测内容类型,并为文件的表单部分使用最小的标头。

或者,每个(name, value)对中的值可以是一个包含两到四个项目的元组,(fn, fp[, ft[, fh]])(使用方括号作为元语法来表示可选部分)。在这种情况下,fn 是文件的名称,fp 提供内容(与前一段中的方式相同),可选的 ft 提供内容类型(如果缺失,requests 将猜测它,如前一段中所示),可选的字典 fh 提供文件表单部分的额外标头。

如何解释 requests 的示例

在实际应用中,通常不需要考虑类 requests.Request 的内部实例 r,该类函数类似于 requests.post 在你的代表中构建、准备,然后发送。然而,为了确切了解 requests 的操作,以较低的抽象级别(构建、准备和检查 r —— 无需发送它!)是有益的。例如,在导入 requests 后,传递数据如下示例中所示:

r = requests.Request('GET', 'http://www.example.com',
    data={'foo': 'bar'}, params={'fie': 'foo'})
p = r.prepare()
print(p.url)
print(p.headers)
print(p.body)

输出(为了可读性,将 p.headers 字典的输出拆分):

http://www.example.com/?fie=foo
{'Content-Length': '7',
 'Content-Type': 'application/x-www-form-urlencoded'}
foo=bar

类似地,在传递文件时:

r = requests.Request('POST', 'http://www.example.com',
    data={'foo': 'bar'}, files={'fie': 'foo'})
p = r.prepare()
print(p.headers)
print(p.body)

这将输出(几行被拆分以提高可读性):

{'Content-Length': '228',
 'Content-Type': 'multipart/form-data; boundary=dfd600d8aa58496270'}
b'--dfd600d8aa58496270\r\nContent-Disposition: form-data;
="foo"\r\n\r\nbar\r\n--dfd600d8aa584962709b936134b1cfce\r\n
Content-Disposition: form-data; name="fie" filename="fie"\r\n\r\nfoo\r\n
--dfd600d8aa584962709b936134b1cfce--\r\n'

愉快的交互式探索!

响应类

requests 模块中你总是需要考虑的一个类是 Response:每个请求,一旦发送到服务器(通常是通过 get 等方法隐式完成),都会返回一个 requests.Response 实例 r

你通常想要做的第一件事是检查r.status_code,一个告诉你请求进行情况的整数,在典型的“HTTPese”中:200 表示“一切正常”,404 表示“未找到”,等等。如果你希望对指示某种错误的状态码只获取异常,请调用r.raise_for_status;如果请求成功,它将不执行任何操作,但否则将引发 requests.exceptions.HTTPError 异常(其他异常,不对应任何特定的 HTTP 状态码,可能会被引发,而不需要任何明确的调用:例如任何网络问题的 ConnectionError,或者超时的 TimeoutError)。

接下来,你可能想要检查响应的 HTTP 头:为此,请使用r.headers,一个字典(具有特殊功能,其大小写不敏感的字符串键指示头名称,例如在Wikipedia中列出的 HTTP 规范)。大多数头部可以安全地忽略,但有时你可能更愿意检查。例如,你可以通过r.headers.get('content-language')验证响应是否指定了其主体所写的自然语言,以提供不同的呈现选择,例如使用某种语言翻译服务使响应对用户更有用。

通常情况下,你不需要对重定向进行特定的状态或头检查:默认情况下,requests 会自动对除 HEAD 以外的所有方法进行重定向(你可以在请求中显式传递 allow_redirection 命名参数来更改该行为)。如果允许重定向,你可能需要检查r.history,一个沿途累积的 Response 实例列表,从最旧到最新,但不包括r本身(如果没有重定向,r.history 为空)。

大多数情况下,可能在检查状态和头之后,你想使用响应的主体。在简单情况下,只需将响应的主体作为字节字符串访问,r.content,或者通过调用r.json 将其解码为 JSON(一旦你检查到它是如何编码的,例如通过r.headers.get('content-type'))。

通常,你更倾向于将响应的主体作为(Unicode)文本访问,使用属性r.text。后者使用编解码器解码(从实际构成响应主体的八位字节),编解码器由请求根据 Content-Type 头和对主体本身的粗略检查认为最佳来确定。你可以通过属性r.encoding 检查使用的(或即将使用的)编解码器;其值将是与 codecs 模块注册的编解码器名称,详见“codecs 模块”。你甚至可以通过将r.encoding 分配为你选择的编解码器的名称来覆盖编解码器的选择。

我们不在本书中涵盖其他高级问题,例如流式传输;请参阅 requests 包的在线文档获取更多信息。

其他网络协议

许多,许多 其他网络协议在使用中—一些最好由 Python 标准库支持,但大多数你会在 PyPI 上找到更好和更新的第三方模块。

要像登录到另一台机器(或自己节点上的单独登录会话)一样连接,可以使用 Secure Shell (SSH) 协议,由第三方模块 paramiko 或围绕它的更高抽象层包装器,第三方模块 spur 支持。(你也可以,虽然可能存在某些安全风险,仍然使用经典的 Telnet,由标准库模块 telnetlib 支持。)

其他网络协议包括,但不限于:

  • NNTP,用于访问 Usenet 新闻服务器,由标准库模块 nntplib 支持。

  • XML-RPC,用于基本的远程过程调用功能,由 xmlrpc.client 支持。

  • gRPC,用于更现代的远程过程功能,由第三方模块 grpcio 支持。

  • NTP,用于通过网络获取精确时间,由第三方模块 ntplib 支持。

  • SNMP,用于网络管理,由第三方模块 pysnmp 支持。

没有单一的书籍(甚至包括本书!)能够涵盖所有这些协议及其支持模块。相反,我们在这个问题上的最佳建议是战略性的:每当你决定通过某种网络协议使你的应用程序与其他系统交互时,不要急于实现自己的模块来支持该协议。而是搜索并询问,你很可能会找到优秀的现有 Python 模块(第三方或标准库),支持该协议。⁶

如果你发现这些模块中存在错误或缺失功能,请提交错误或功能请求(并且最好提供一个修复问题并满足你应用程序需求的补丁或拉取请求)。换句话说,成为开源社区的积极成员,而不仅仅是被动用户:你将会受到欢迎,解决你自己的问题,并在此过程中帮助许多其他人。“给予回馈”,因为你无法“回馈”给所有为你提供大部分工具的了不起的人们!

¹ IMAP4,参见 RFC 1730;或 IMAP4rev1,参见 RFC 2060

² POP 协议的规范可在 RFC 1939 中找到。

³ SMTP 协议的规范可在 RFC 2821 中找到。

⁴ 根据 RFC 2388

⁵ 它能让你完全、明确地控制上传的八位字节。

⁶ 更重要的是,如果你认为需要发明一个全新的协议并在套接字之上实现它,再三考虑,并仔细搜索:很可能已经有大量现有的互联网协议完全符合你的需求!

第二十章:提供 HTTP

当浏览器(或任何其他 Web 客户端)从服务器请求页面时,服务器可以返回静态或动态内容。提供动态内容涉及服务器端 Web 程序根据存储在数据库中的信息动态生成和传递内容。

在 Web 的早期历史上,服务器端编程的标准是 通用网关接口(CGI),它要求服务器在客户端请求动态内容时每次运行一个单独的程序。进程启动时间、解释器初始化、连接数据库和脚本初始化累积起来会带来可衡量的开销;CGI 的扩展性不佳。

如今,Web 服务器支持许多特定于服务器的方式来减少开销,从可以为多次访问提供动态内容的进程中服务,而不是每次访问都启动新进程。因此,本书不涵盖 CGI。要维护现有的 CGI 程序,或更好地将它们移植到更现代的方法,请参阅在线文档(特别是 PEP 594 的建议)并查看标准库模块 cgi(自 3.11 版起已弃用)和 http.cookies。¹

随着基于 微服务 的系统的出现,HTTP 对于分布式系统设计变得更加基础,提供了一种方便的方式来传输经常使用的 JSON 内容之间的数据。互联网上有成千上万的公共可用 HTTP 数据 API。虽然 HTTP 的原则自其于 1990 年代中期问世以来几乎未发生变化,但多年来已显著增强其功能以扩展其能力。² 对于深入学习并具有优秀参考资料,我们推荐阅读 HTTP: The Definitive Guide(David Gourley 等著,O’Reilly)。

http.server

Python 的标准库包括一个模块,其中包含服务器和处理程序类,用于实现简单的 HTTP 服务器。

你可以通过命令行直接运行此服务器:

$ python -m http.server *port_number*

默认情况下,服务器监听所有接口,并提供对当前目录中文件的访问。一位作者将其用作文件传输的简单方式:在源系统的文件目录中启动 Python http.server,然后使用 wget 或 curl 等工具将文件复制到目标系统。

http.server 的安全功能非常有限。你可以在在线文档中找到有关 http.server 的更多信息。对于生产使用,我们建议您使用以下章节中提到的框架之一。

WSGI

Python 的Web 服务器网关接口(WSGI)是所有现代 Python Web 开发框架与底层 Web 服务器或网关交互的标准方式。WSGI 不适用于你的应用程序直接使用;相反,你使用众多高级抽象框架之一编写你的程序,然后框架使用 WSGI 与 Web 服务器交互。

只有在为尚未提供 WSGI 接口的 Web 服务器实现 WSGI 接口(如果有这样的服务器存在的话),或者如果你正在构建一个新的 Python Web 框架时,你才需要关注 WSGI 的细节。³在这种情况下,你需要研究 WSGI PEP,标准库包wsgiref的文档,以及WSGI.org存档

如果你使用轻量级框架(即与 WSGI 紧密匹配的框架),一些 WSGI 概念可能对你很重要。WSGI 是一个接口,这个接口有两个方面:Web 服务器/网关方面和应用程序/框架方面。

框架的工作是提供一个WSGI 应用对象,一个可调用对象(通常是一个具有 call 特殊方法的类的实例,但这是一个实现细节),符合 PEP 中的约定,并通过特定服务器文档中指定的任何手段连接应用程序对象(通常是几行代码,或配置文件,或只是一个约定,例如将 WSGI 应用对象命名为模块中的顶级属性 application)。服务器为每个传入的 HTTP 请求调用应用程序对象,应用程序对象适当地响应,以便服务器可以形成传出的 HTTP 响应并将其发送出去——都按照上述约定进行。一个框架,即使是一个轻量级的框架,也会屏蔽这些细节(除非你可能需要根据具体的服务器来实例化和连接应用程序对象)。

WSGI 服务器

你可以在线上找到一个广泛的服务器和适配器列表,用于运行 WSGI 框架和应用程序(用于开发和测试,在生产 Web 设置中,或两者兼而有之)——这是一个广泛的列表,但仅仅是部分列表。例如,它没有提到 Google App Engine 的 Python 运行时也是一个 WSGI 服务器,准备根据app.yaml配置文件指示调度 WSGI 应用。

如果你正在寻找一个用于开发或部署在生产环境中的 WSGI 服务器,例如在基于 Nginx 的负载均衡器后面,那么你应该会对 Gunicorn 感到满意:纯 Python 的良好支持,仅支持 WSGI,非常轻量级。一个值得考虑的(也是纯 Python 和仅支持 WSGI 的)替代方案,目前在 Windows 上支持更好的是 Waitress。如果你需要更丰富的功能(例如对 Perl 和 Ruby 的支持以及许多其他形式的可扩展性),请考虑更大、更复杂的 uWSGI⁴。

WSGI 还有 middleware 的概念,这是一个实现了 WSGI 服务器和应用程序两端的子系统。中间件对象“包装”了一个 WSGI 应用程序;可以选择性地更改请求、环境和响应;并向服务器呈现自身为“应用程序”。允许并且常见的是多层包装,形成为实际应用级别代码提供服务的中间件“堆栈”。如果你想编写一个跨框架的中间件组件,那么你可能确实需要成为一个 WSGI 专家。

ASGI

如果你对异步 Python(本书不涵盖)感兴趣,你应该一定要调查 ASGI,它旨在做的基本上与 WSGI 做的一样,但是是异步的。通常情况下,在网络环境中进行异步编程时,它可以提供大大提高的性能,尽管(有人认为)会增加开发者的认知负担。

Python Web 框架

对于 Python web 框架的调查,请参阅 Python 维基页面。它权威性的原因在于它位于官方 Python.org 网站上,并且是由社区共同维护的,因此随着时间的推移始终保持更新。该维基列出并指向数十个被识别为“活跃”的框架⁵,以及被识别为“已停用/不活跃”的许多其他框架。此外,它还指向了关于 Python 内容管理系统、Web 服务器以及相关 Web 组件和库的单独维基页面。

“全栈”与“轻量级”框架

大致来说,Python web 框架可以被分类为 全栈(试图提供构建 Web 应用程序所需的所有功能)或 轻量级(仅提供与 Web 服务本身的便利接口,并让您选择自己喜欢的组件用于诸如与数据库接口和模板化等任务)。当然,像所有分类法一样,这个分类法并不精确和完整,并且需要价值判断;然而,这是开始理解众多 Python web 框架的一种方式。

在本书中,我们不深入研究任何全栈框架——每一个都太复杂了。尽管如此,其中之一可能是您特定应用的最佳选择,因此我们提到了一些最流行的框架,并建议您访问它们的网站。

几种流行的全栈框架

迄今为止最流行的全栈框架是Django,它庞大而可扩展。Django 所谓的应用程序实际上是可重用的子系统,而 Django 称之为项目的通常被称为“应用程序”。Django 需要其独特的思维模式,但换取巨大的力量和功能。

一个很好的选择是web2py:它几乎和 Django 一样强大,更易学,并因其对向后兼容性的奉献而闻名(如果它保持其良好的记录,今天编写的任何 web2py 应用程序将长期保持运行)。web2py 还有出色的文档。

第三个值得一提的是TurboGears,它开始时是一个轻量级框架,但通过完全集成其他独立的第三方项目来实现“全栈”状态,以满足大多数 Web 应用程序中所需的数据库接口和模板等各种功能,而不是设计自己的功能。另一个在哲学上类似的“轻量但功能丰富”的框架是Pyramid

使用轻量级框架时的考虑事项

每当您使用轻量级框架时,如果您需要任何数据库、模板或其他与 HTTP 不严格相关的功能,您将需要挑选和集成单独的组件来实现。然而,框架越轻量级,您就需要理解和集成的组件越多,例如对用户进行身份验证或通过给定用户的 Web 请求保持状态。许多 WSGI 中间件包可以帮助您完成这些任务。一些优秀的中间件集中于特定任务——例如,Oso用于访问控制,Beaker用于以轻量级会话形式维护状态等等。

然而,当我们(本书的作者)需要用于几乎任何目的的良好 WSGI 中间件时,我们几乎总是首先检查Werkzeug,这是一个令人惊叹的组件集合,具有广泛的应用和高质量。我们在本书中不涵盖 Werkzeug(就像我们不涵盖其他中间件一样),但我们强烈推荐它(Werkzeug 也是 Flask 的基础,Flask 是我们最喜欢的轻量级框架,在本章后面我们会详细介绍)。

你可能注意到,正确使用轻量级框架要求你理解 HTTP(换句话说,知道你在做什么),而全栈框架试图手把手地指导你做正确的事情,而不需要真正理解为什么或如何是正确的——这是以时间和资源为代价,并接受全栈框架的概念图和思维方式。本书的作者们热衷于知识密集、资源轻的轻量级框架方法,但我们承认,在许多情况下,富有、重量级、全面性的全栈框架更为合适。各取所需!

几个流行的轻量级框架

如前所述,Python 有多个框架,包括许多轻量级框架。我们在这里介绍了两个后者:流行的通用框架 Flask 和面向 API 的 FastAPI。

Flask

最受欢迎的 Python 轻量级框架是Flask,一个第三方可通过 pip 安装的包。尽管轻巧,它包含了开发服务器和调试器,并且显式地依赖于其他精选的包,如 Werkzeug 用于中间件和Jinja用于模板(这两个包最初由 Flask 的作者 Armin Ronacher 编写)。

除了项目网站(包含丰富详细的文档),还可以查看GitHub 上的源代码PyPI 条目。如果你想在 Google App Engine 上运行 Flask(在本地计算机上或在 Google 的服务器上*appspot.com*),Dough Mahugh 的Medium 文章可能非常方便。

我们还强烈推荐 Miguel Grinberg 的书籍Flask Web Development(O'Reilly):尽管第二版在撰写本文时已经过时(几乎四年),但它仍然为你提供了一个优秀的基础,使你更容易学习最新的新增内容。

Flask 包提供的主要类被命名为 Flask。一个 flask.Flask 的实例,除了作为一个 WSGI 应用程序外,还通过其 wsgi_app 属性包装了一个 WSGI 应用程序。当你需要在 WSGI 中间件中进一步包装 WSGI 应用程序时,请使用以下习惯用法:

`import` flask

app = flask.Flask(__name__)
app.wsgi_app = *`some_middleware`*(app.wsgi_app)

当你实例化 flask.Flask 时,始终将应用程序名称作为第一个参数传递(通常只是模块中 name 特殊变量的值;如果你在一个包内实例化它,通常在*__init__.py中,使用 name.partition('.')[0]也可以)。可选地,你还可以传递命名参数,如 static_folder 和 template_folder 来自定义静态文件和 Jinja 模板的位置;但这很少需要——默认值(分别位于与实例化 flask.Flask 的 Python 脚本相同的文件夹中的子文件夹statictemplates*)非常合理。

flask.Flask 的实例 app 提供了超过 100 个方法和属性,其中许多是装饰器,用于将函数绑定到 app 中的各种角色,例如 视图函数(在 URL 上提供 HTTP 动词)或 钩子(在处理请求前或构建响应后修改请求、处理错误等)。

flask.Flask 在实例化时只需几个参数(而且这些参数通常不需要在你的代码中计算),它提供了一些装饰器,你在定义例如视图函数时会用到。因此,在 Flask 中的正常模式是在你的主脚本早期实例化 app,就像你的应用程序启动时一样,这样 app 的装饰器和其他方法属性在你 def 视图函数等时就可用了。

由于存在单个全局 app 对象,你可能会想知道在访问、修改和重新绑定 app 的属性和属性时,它的线程安全性如何。不用担心:你看到的名称实际上只是特定请求上下文中实际对象的代理,在特定线程或 greenlet 的上下文中。永远不要对这些属性进行类型检查(它们的类型实际上是不透明的代理类型),你就没问题。

Flask 还提供许多其他实用函数和类;通常,后者会从其他包中的类继承或包装,以添加无缝、便捷的 Flask 集成。例如,Flask 的 Request 和 Response 类通过子类化相应的 Werkzeug 类添加了一些便捷的功能。

Flask 请求对象

类 flask.Request 提供了大量 详细记录的属性。表 20-1 列出了你经常使用的属性。

表 20-1. flask.Request 的有用属性

args 一个 MultiDict,包含请求的查询参数
cookies 一个包含请求中的 cookies 的字典
data 一个字节字符串,请求的主体(通常用于 POST 和 PUT 请求)
files 一个 MultiDict,包含请求中上传的文件,将文件名映射到包含每个文件数据的类文件对象
form 一个 MultiDict,包含请求体中提供的表单字段
headers 一个 MultiDict,包含请求的头部
values 一个 MultiDict,合并了 args 和 form 属性

MultiDict 类似于字典,但可以为一个键拥有多个值。对 MultiDict 实例 m 进行索引和获取时会返回该键的任意一个值;要获取一个键的值列表(如果该键不在 m 中则返回空列表),可以调用 m.getlist(key)。

Flask 响应对象

通常,Flask 视图函数可以直接返回一个字符串(它将成为响应的主体):Flask 会自动在字符串周围包装一个 flask.Response 实例 r,因此您无需担心响应类。然而,有时您需要修改响应的标头;在这种情况下,在视图函数中调用 r = flask.make_response(astring),按您的要求修改 r.headers,然后返回 r。(要设置一个 cookie,请勿使用 r.headers;而是调用 r.set_cookie。)

Flask 与其他系统的许多内置集成无需子类化:例如,模板集成会将 Flask 全局对象 config、request、session 和 g(后者是方便的“全局捕获”对象 flask.g,在应用上下文中,您的代码可以存储请求处理期间想要“存放”的任何内容)隐式注入到 Jinja 上下文中,以及函数 url_for(将端点转换为相应的 URL,与 flask.url_for 相同)和 get_flashed_messages(支持 flashed messages,在本书中我们不涵盖;与 flask.get_flashed_messages 相同)。Flask 还提供了方便的方式,让您的代码将更多过滤器、函数和值注入到 Jinja 上下文中,无需任何子类化。

大多数官方认可或批准的 Flask 扩展(在撰写本文时有数百种可在 PyPI 上获取)采用类似的方法,提供类和实用函数,以无缝集成其他流行的子系统到您的 Flask 应用程序中。

此外,Flask 还引入了其他功能,如 signals,以提供“发布/订阅”模式中更松散的动态耦合,以及 blueprints,以一种高度模块化、灵活的方式提供 Flask 应用程序功能的大部分子集,以便于重构大型应用程序。我们在本书中不涵盖这些高级概念。

示例 20-1 展示了一个简单的 Flask 示例。(使用 pip 安装 Flask 后,使用命令 flask --app flask_example run 运行该示例。)

示例 20-1. 一个 Flask 示例
`import` datetime, flask
app = flask.Flask(__name__)

*`# secret key for cryptographic components such as encoding session cookies;`*
*`# for production use, use secrets.token_bytes()`*
app.secret_key = b`'``\xc5``\x8f``\xbc``\xa2``\x1d``\xeb``\xb3``\x94``;:d``\x03``'`

@app.route('/')
`def` greet():
    lastvisit = flask.session.get('lastvisit')
    now = datetime.datetime.now()
    newvisit = now.ctime()
    template = '''
 <html><head><title>Hello, visitor!</title>
 </head><body>
      {% if lastvisit %}
 <p>Welcome back to this site!</p>
 <p>You last visited on {{lastvisit}} UTC</p>
 <p>This visit on {{newvisit}} UTC</p>
      {% else %}
 <p>Welcome to this site on your first visit!</p>
 <p>This visit on {{newvisit}} UTC</p>
 <p>Please Refresh the web page to proceed</p>
      {% endif %}
 </body></html>'''
    flask.session['lastvisit'] = newvisit
    `return` flask.render_template_string(
      template, newvisit=newvisit, lastvisit=lastvisit)

此示例展示了如何使用 Flask 提供的众多构建模块中的一小部分:Flask 类、视图函数以及渲染响应(在本例中,使用 Jinja 模板的 render_template_string;在实际应用中,通常将模板保存在单独的文件中,并使用 render_template 渲染)。该示例还展示了如何通过方便的 flask.session 变量,在同一浏览器中多次交互与服务器时保持状态的连续性。(也可以使用 Python 代码直接组合 HTML 响应,而不是使用 Jinja,并直接使用 cookie 而非 session;然而,实际的 Flask 应用程序更倾向于使用 Jinja 和 session。)

如果此应用程序有多个视图函数,可能希望在会话中设置lastvisit为触发请求的任何 URL。以下是如何编写和装饰钩子函数以在每个请求后执行的代码:

@app.after_request
`def` set_lastvisit(response):
    now = datetime.datetime.now()
    flask.session['lastvisit'] = now.ctime()
    `return` response

现在,您可以从视图函数greet中删除flask.session['lastvisit'] = newvisit语句,应用程序将继续正常工作。

FastAPI

FastAPI 的设计比 Flask 或 Django 更为新颖。尽管后者都有非常可用的扩展以提供 API 服务,但 FastAPI 的目标直指生成基于 HTTP 的 API,正如其名称所示。它也完全能够生成面向浏览器消费的动态网页,使其成为一款多才多艺的服务器。FastAPI 的主页提供了简单、简洁的示例,展示了它的工作原理和优势,支持非常全面和详细的参考文档。

由于类型注释(在第五章中介绍)进入了 Python 语言,它们在工具中的使用范围超出了最初的意图,例如pydantic,它使用它们来执行运行时解析和验证。FastAPI 服务器利用此支持来创建清晰的数据结构,通过内置和定制的输入转换和验证来展示通过对输入进行转换和验证的内置和定制功能,从而极大地提高了 Web 编码的生产力。

FastAPI 还依赖于Starlette,一个高性能的异步 Web 框架,该框架又使用 ASGI 服务器,如UvicornHypercorn。您无需直接使用异步技术即可利用 FastAPI。您可以使用更传统的 Python 风格编写您的应用程序,尽管如果您切换到异步风格,它可能会表现得更快。

FastAPI 能够提供类型准确的 API(以及自动生成的文档),与您的注释所指示的类型相符,这意味着它可以对输入和输出的数据进行自动解析和转换。

请考虑示例 20-2 中显示的示例代码,该示例为 pydantic 和 mongoengine 定义了一个简单的模型。每个模型都有四个字段:name 和 description 是字符串,price 和 tax 是十进制数。对于 name 和 price 字段,需要值,但 description 和 tax 是可选的。pydantic 为后两个字段建立了默认值None;mongoengine 不存储值为None的字段的值。

示例 20-2. models.py:pydantic 和 mongoengine 数据模型
`from` decimal `import` Decimal
`from` pydantic `import` BaseModel, Field
`from` mongoengine `import` Document, StringField, DecimalField
`from` typing `import` Optional

`class` PItem(BaseModel):
    "pydantic typed data class."
    name: str
    price: Decimal
    description: Optional[str] = `None`
    tax: Optional[Decimal] = `None`

`class` MItem(Document):
    "mongoengine document."
    name = StringField(primary_key=`True`)
    price = DecimalField()
    description = StringField(required=`False`)
    tax = DecimalField(required=`False`)

假设您希望通过 Web 表单或 JSON 接受此类数据,并能够将数据作为 JSON 检索或在 HTML 中显示。骨架 示例 20-3(不提供维护现有数据的功能)展示了您如何使用 FastAPI 实现这一点。此示例使用 Uvicorn HTTP 服务器,但未显式使用 Python 的异步特性。与 Flask 一样,程序从创建应用程序对象 app 开始。此对象具有用于每种 HTTP 方法的装饰器方法,但是它避免了 app.route 装饰器,而是选择 app.get 用于 HTTP GET,app.post 用于 HTTP POST 等,这些确定了哪个视图函数处理不同 HTTP 方法的路径请求。

示例 20-3. server.py:FastAPI 接受并显示项目数据的示例代码
`from` decimal `import` Decimal
`from` fastapi `import` FastAPI, Form
`from` fastapi.responses `import` HTMLResponse, FileResponse
`from` mongoengine `import` connect
`from` mongoengine.errors `import` NotUniqueError
`from` typing `import` Optional
`import` json
`import` uvicorn
`from` models `import` PItem, MItem

DATABASE_URI = "mongodb://localhost:27017"
db=DATABASE_URI+"/mydatabase"
connect(host=db)
app = FastAPI()

`def` save(item):
    `try`:
        return item.save(force_insert=`True`)
    `except` NotUniqueError:
        `return` `None`

@app.get('/')
`def` home_page():
    "View function to display a simple form."
    `return` FileResponse("index.xhtml")

@app.post("/items/new/form/", response_class=HTMLResponse)
`def` create_item_from_form(name: str=Form(...),
                          price: Decimal=Form(...),
                          description: Optional[str]=Form(""),
                          tax: Optional[Decimal]=Form(Decimal("0.0"))):
    "View function to accept form data and create an item."
    mongoitem = MItem(name=name, price=price, description=description, 
                      tax=tax)
    value = save(mongoitem)
    `if` value:
        body = f"Item({name!r}, {price!r}, {description!r}, {tax!r})"
    `else`:
        body = f"Item {name!r} already present."
    `return` f"""<html><body><h2>{body}</h2></body></html>"""

@app.post("/items/new/")
`def` create_item_from_json(item: PItem):
    "View function to accept JSON data and create an item."
    mongoitem = MItem(**item.dict())
    value = save(mongoitem)
    `if` `not` value:
        `return` f"Primary key {item.name!r} already present"
    `return` item.dict()

@app.get("/items/{name}/")
`def` retrieve_item(name: str):
    "View function to return the JSON contents of an item."
    m_item = MItem.objects(name=name).get()
    `return` json.loads(m_item.to_json())

`if` __name__ == "__main__":
    # host as "localhost" or "127.0.0.1" allows only local apps to access the
    # web page. Using "0.0.0.0" will accept access from apps on other hosts,
    # but this can raise security concerns, and is generally not recommended.
    uvicorn.run("__main__:app", host="127.0.0.1", port=8000, reload=True)

home_page 函数不带参数,简单地呈现包含来自 index.xhtml 文件的表单的最小 HTML 主页。该表单提交到 /items/new/form/ 端点,触发调用 create_item_from_form 函数,在路由装饰器中声明生成 HTML 响应而不是默认的 JSON。

示例 20-4. index.xhtml 文件
<!DOCTYPE html>
<html lang="en">
  <body>
  <h2>FastAPI Demonstrator</h2>
  <form method="POST" action="/items/new/form/">
    <table>
    <tr><td>Name</td><td><input name="name"></td></tr>
    <tr><td>Price</td><td><input name="price"></td></tr>
    <tr><td>Description</td><td><input name="description"></td></tr>
    <tr><td>Tax</td><td><input name="tax"></td></tr>
    <tr><td></td><td><input type="submit"></td></tr>
    </table>
  </form>
  </body>
</html>

表单,显示在 图 20-1 中,由 create_item_from_form 函数处理,其签名为每个表单字段指定一个参数,并使用注解定义每个字段为表单字段。注意,签名为描述和税收定义了自己的默认值。该函数从表单数据创建一个 MItem 对象,并尝试将其保存到数据库中。save 函数强制插入,抑制更新现有记录,并通过返回 None 报告失败;返回值用于构建简单的 HTML 回复。在生产应用中,通常会使用像 Jinja 这样的模板引擎来渲染响应。

FastAPI 演示程序的输入表单

图 20-1. FastAPI 演示程序的输入表单

create_item_from_json 函数,从 /items/new/ 端点路由,接收来自 POST 请求的 JSON 输入。其签名接受一个 pydantic 记录,在这种情况下,FastAPI 将使用 pydantic 的验证来确定输入是否可接受。该函数返回一个 Python 字典,FastAPI 会自动将其转换为 JSON 响应。可以通过一个简单的客户端轻松测试,如 示例 20-5 所示。

示例 20-5. FastAPI 测试客户端
`import` requests, json

result = requests.post('http://localhost:8000/items/new/',
                       json={"name": "Item1",
                             "price": 12.34,
                             "description": "Rusty old bucket"})
print(result.status_code, result.json())
result = requests.get('http://localhost:8000/items/Item1/')
print(result.status_code, result.json())
result = requests.post('http://localhost:8000/items/new/',
                       json={"name": "Item2",
                             "price": "Not a number"})
print(result.status_code, result.json())

运行此程序的结果如下:

200 {'name': 'Item1', 'price': 12.34, 'description': 'Rusty old
bucket'> 'tax': None}
200 {'_id': 'Item1', 'price': 12.34, 'description': 'Rusty old bucket'}
422 {'detail': [{'loc': ['body', 'price'], 'msg': 'value is not a valid
decimal', 'type': 'type_error.decimal'}]}

第一个 POST 请求到 /items/new/ 会看到服务器返回与其展示的相同数据,确认其已保存在数据库中。请注意,未提供税收字段,因此这里使用了 pydantic 的默认值。第二行显示了检索到的新存储项的输出(mongoengine 使用名称 _id 标识主键)。第三行显示了一个错误消息,由于尝试将非数值值存储在价格字段中而生成。

最后,retrieve_item 视图函数,由诸如 /items/Item1/ 这样的 URL 路由,提取第二个路径元素作为键,并返回给定项的 JSON 表示。它在 mongoengine 中查找给定的键,并将返回的记录转换为字典,FastAPI 将其呈现为 JSON。

¹ 一个历史遗留问题是,在 CGI 中,服务器通过操作系统环境(在 Python 中为 os.environ)向 CGI 脚本提供关于要服务的 HTTP 请求的信息;直至今日,Web 服务器和应用程序框架之间的接口仍然依赖于“一个环境”,这本质上是一个字典,并且泛化并加速了相同的基本思想。

² 还存在更 高级版本的 HTTP,但本书不涉及它们。

³ 请不要。正如 Titus Brown 曾指出的那样,Python 因拥有比关键字还多的 Web 框架而(臭名昭著)。本书的一位作者曾在 Guido 设计 Python 3 时向他展示了如何轻松解决这个问题——只需添加几百个新关键字——但出于某种原因,Guido 对这一建议并不十分接受。

⁴ 在 Windows 上安装 uWSGI 目前需要使用 Cygwin 进行编译。

⁵ 由于 Python 关键字少于 40 个,你可以理解为什么 Titus Brown 曾指出 Python 拥有比关键字更多的 Web 框架。

第二十一章:电子邮件、MIME 和其他网络编码

网络上传输的是字节流,也被网络行话称为八位字节。字节当然可以表示文本,通过多种可能的编码方式之一。然而,你希望通过网络发送的内容往往比单纯的文本或字节流有更复杂的结构。多用途互联网邮件扩展(MIME)和其他编码标准填补了这一差距,它们规定了如何将结构化数据表示为字节或文本。虽然这些编码通常最初是为电子邮件设计的,但也被用于网络和许多其他网络系统。Python 通过各种库模块支持这些编码,如 base64、quopri 和 uu(在“将二进制数据编码为 ASCII 文本”中介绍),以及 email 包的模块(在下一节中介绍)。这些编码允许我们无缝地创建一个编码中包含附件的消息,避免了许多麻烦的任务。

MIME 和电子邮件格式处理

email 包处理 MIME 文件(如电子邮件消息)、网络新闻传输协议(NNTP)帖子、HTTP 交互等的解析、生成和操作。Python 标准库还包含其他处理这些工作部分的模块。然而,email 包提供了一种完整和系统的方法来处理这些重要任务。我们建议您使用 email,而不是部分重叠 email 功能的旧模块。尽管名为 email,但它与接收或发送电子邮件无关;对于这些任务,请参见 imaplib、poplib 和 smtplib 模块(在“电子邮件协议”中介绍)。相反,email 处理的是在接收到 MIME 消息之后或在发送之前正确构造它们的任务。

email 包中的函数

email 包提供了四个工厂函数,从字符串或文件中返回一个类 email.message.Message 的实例 m(参见表 21-1)。这些函数依赖于类 email.parser.Parser,但工厂函数更方便、更简单。因此,本书不再深入介绍 email.parser 模块。

表 21-1. 构建来自字符串或文件的消息对象的电子邮件工厂函数

m⁠e⁠s⁠s⁠a⁠g⁠e⁠_⁠f⁠r⁠o⁠m⁠_⁠b⁠i⁠n⁠a⁠r⁠y⁠_​f⁠i⁠l⁠e 使用二进制文件对象 f(必须已打开以供读取)的内容解析构建 m
message_from_bytes 使用字节串 s 解析构建 m
message_from_file 使用文本文件对象 f(必须已打开以供读取)的内容解析构建 m
message_from_string 使用字符串 s 解析构建 m

email.message 模块

email.message 模块提供了 Message 类。电子邮件包的所有部分都创建、修改或使用 Message 实例。 Message 的一个实例 m 模拟了 MIME 消息,包括 headerspayload(数据内容)。 m 是一个映射,以头部名称为键,以头部值字符串为值。

要创建一个最初为空的 m,请不带参数调用 Message。更常见的情况是,通过 Table 21-1 中的工厂函数之一解析来创建 m,或者通过 “创建消息” 中涵盖的其他间接方式。 m 的有效载荷可以是字符串、另一个 Message 实例或者 多部分消息(一组递归嵌套的其他 Message 实例)。

你可以在构建的电子邮件消息上设置任意头部。几个互联网 RFC(请求评论)指定了各种目的的头部。主要适用的 RFC 是 RFC 2822;你可以在非规范性的 RFC 2076 中找到关于头部的许多其他 RFC 的摘要。

为了使 m 更方便,作为映射的语义与字典的语义不同。 m 的键大小写不敏感。 m 保持您添加的顺序的头部,方法 keys、values 和 items 返回按照该顺序排列的头部列表(而不是视图!)。 m 可以有多个名为 key 的头部:m[key] 返回任意一个这样的头部(或者头部缺失时返回 None),del m[key] 删除所有这样的头部(如果头部缺失则不会报错)。

要获取所有具有特定名称的头部的列表,请调用 m.get_all(key)。 len(m) 返回总头部数,计算重复项,而不仅仅是不同头部名称的数量。当没有名为 key* 的头部时,m[key] 返回 None 并且不会引发 KeyError(即它的行为类似于 m.get(key): del m[key] 在这种情况下不起作用,而 m.get_all(key) 返回 None)。您可以直接在 m 上循环:这就像在 m.keys() 上循环一样。

Message 的一个实例 m 提供了各种处理 m 的头部和有效载荷的属性和方法,列在 Table 21-2 中。

表 21-2. Message 实例 m 的属性和方法

| add_header | m.add_header(name, _value, **_params) 类似于 m[name]=_value,但您还可以作为命名参数提供头部参数。对于每个命名参数 pname=pvalue,add_header 将 pname 中的任何下划线更改为破折号,然后将一个形式为的字符串附加到头部的值:

; pname="pvalue"

pvalueNone 时,add_header 仅附加形式为的字符串:

; pname

当参数的值包含非 ASCII 字符时,将其指定为一个三项元组,(CHARSET, LANGUAGE, VALUE)。CHARSET 指定用于值的编码。LANGUAGE 通常为 None 或 '',但可以根据 RFC 2231 设置任何语言值。VALUE 是包含非 ASCII 字符的字符串值。

as_string m.as_string(unixfrom=False) 返回整个消息作为字符串。当 unixfrom 为 true 时,还包括第一行,通常以 'From ' 开头,称为消息的 envelope header
attach m.attach(payload) 将 payload,即消息,添加到 m 的载荷中。当 m 的载荷为 None 时,m 的载荷现在是单个项目列表 [payload]。当 m 的载荷为消息列表时,将 payload 追加到列表中。当 m 的载荷为其他任何内容时,m.attach(payload) 引发 MultipartConversionError。
epilogue 属性 m.epilogue 可以是 None,或者是一个字符串,在最后一个边界线之后成为消息字符串形式的一部分。邮件程序通常不显示此文本。epilogue 是 m 的正常属性:在处理任何 m 时,您的程序可以访问它,并在构建或修改 m 时将其绑定。
get_all m.get_all(name, default=None) 返回一个列表,其中包含按照添加到 m 的顺序命名为 name 的所有头部的所有值。当 m 没有名为 name 的头部时,get_all 返回 default。
get_boundary m.get_boundary(default=None) 返回 m 的 Content-Type 头部的 boundary 参数的字符串值。当 m 没有 Content-Type 头部或头部没有 boundary 参数时,get_boundary 返回 default。
get_charsets m.get_charsets(default=None) 返回参数 charset 在 m 的 Content-Type 头部中的字符串值列表 L。当 m 是多部分时,L 每个部分有一项;否则,L 的长度为 1。对于没有 Content-Type 头部、没有 charset 参数或者主类型与 'text' 不同的部分,L 中对应的项目是 default。
g⁠e⁠t⁠_⁠c⁠o⁠n⁠t⁠e⁠n⁠t⁠_​m⁠a⁠i⁠n⁠t⁠y⁠p⁠e m.get_content_maintype(default=None) 返回 m 的主内容类型:从头部 Content-Type 中取出的小写字符串 'maintype'。例如,当 Content-Type 是 'Text/Html' 时,get_content_maintype 返回 'text'。当 m 没有 Content-Type 头部时,get_content_maintype 返回 default。
g⁠e⁠t⁠_⁠c⁠o⁠n⁠t⁠e⁠n⁠t⁠_​s⁠u⁠b⁠t⁠y⁠p⁠e m.get_content_subtype(default=None) 返回 m 的内容子类型:从头部 Content-Type 中取出的小写字符串 'subtype'。例如,当 Content-Type 是 'Text/Html' 时,get_content_subtype 返回 'html'。当 m 没有 Content-Type 头部时,get_content_subtype 返回 default。
g⁠e⁠t⁠_⁠c⁠o⁠n⁠t⁠e⁠n⁠t⁠_​t⁠y⁠p⁠e m.get_content_type(default=None) 返回 m 的内容类型:从头部 Content-Type 中取得一个小写字符串 'maintype/subtype'。例如,当 Content-Type 为 'Text/Html' 时,get_content_type 返回 'text/html'。当 m 没有 Content-Type 头部时,get_content_type 返回 default。
get_filename m.get_filename(default=None) 返回 m 的 Content-Disposition 头部的 filename 参数的字符串值。当 m 没有 Content-Disposition 头部,或头部没有 filename 参数时,get_filename 返回 default。
get_param m.get_param(param, default=None, header='Content-Type') 返回 m 的头部 header 中参数 param 的字符串值。对于仅由名称指定(没有值)的参数,返回 ''。当 m 没有头部 header,或头部没有名为 param 的参数时,get_param 返回 default。
get_params m.get_params(default=None, header='Content-Type') 返回 m 的头部 header 的参数,一个由字符串对组成的列表,每个参数给出其名称和值。对于仅由名称指定(没有值)的参数,使用 '' 作为值。当 m 没有头部 header 时,get_params 返回 default。

| get_payload | m.get_payload(i=None, decode=False) 返回 m 的载荷。当 m.is_multipart 为 False 时,i 必须为 Nonem.get_payload 返回 m 的整个载荷,一个字符串或消息实例。如果 decode 为 true,并且头部 Content-Transfer-Encoding 的值为 'quoted-printable' 或 'base64',m.get_payload 还会对载荷进行解码。如果 decode 为 false,或头部 Content-Transfer-Encoding 缺失或具有其他值,m.get_payload 返回未更改的载荷。

m.is_multipart 为 True 时,decode 必须为 false。当 i 为 None 时,m.get_payload 返回 m 的载荷作为列表。否则,m.get_payload(i) 返回载荷的第 i 项,如果 i < 0 或 i 太大,则引发 TypeError。

get_unixfrom m.get_unixfrom() 返回 m 的信封头字符串,当 m 没有信封头时返回 None
is_multipart m.is_multipart() 当 m 的载荷为列表时返回 True;否则返回 False
preamble 属性 m.preamble 可以是 None,或成为消息的字符串形式的一部分,出现在第一个边界线之前。邮件程序仅在不支持多部分消息时显示此文本,因此您可以使用此属性来提醒用户需要使用其他邮件程序来查看。preamble 是 m 的普通属性:在处理由任何手段构建的 m 时,您的程序可以访问它,并在构建或修改 m 时绑定、重新绑定或解绑它。
set_boundary m.set_boundary(boundary) 将 m 的 Content-Type 头部的 boundary 参数设置为 boundary*.*。当 m 没有 Content-Type 头部时,引发 HeaderParseError。
set_payload m.set_payload(payload) 将 m 的 payload 设置为 payload,它必须是字符串或适合 m 的 Content-Type 的 Message 实例列表。
set_unixfrom m.set_unixfrom(unixfrom) 设置 m 的信封头字符串为 unixfromunixfrom 是整个信封头行,包括前导的 'From ' 但不包括尾部的 '\n'。
walk m.walk() 返回一个迭代器,用于遍历 m 的所有部分和子部分,深度优先(参见 “递归”)。

email.Generator 模块

email.Generator 模块提供了 Generator 类,您可以使用它来生成消息 m 的文本形式。m.as_string() 和 str(m) 或许已经足够,但 Generator 提供了更多的灵活性。使用必需参数 outfp 和两个可选参数实例化 Generator 类:

| Generator | class Generator(outfp, mangle_from_=False, maxheaderlen=78) outfp 是一个文件或类文件对象,供应写入方法。当 mangle_from_ 为真时,g 会在 payload 中以 'From ' 开头的任何行前添加大于号 (>),以便更容易解析消息的文本形式。g 将每个标题行在分号处包装成不超过 maxheaderlen 字符的物理行。要使用 g,调用 g.flatten;例如:

*`g`*.flatten(*`m`*, unixfrom=`False`)

这将 m 以文本形式发射到 outfp,类似于(但比以下代码消耗更少的内存):

*`outfp`*.write(*`m`*.as_string(*`unixfrom`*))

. |

创建消息

email.mime 子包提供各种模块,每个模块都有一个名为模块的 Message 子类。模块的名称为小写(例如,email.mime.text),而类名为混合大小写。这些类列在 Table 21-3 中,帮助您创建不同 MIME 类型的 Message 实例。

Table 21-3. email.mime 提供的类

MIMEAudio class MIMEAudio(_audiodata, _subtype=None, encoder=None, **params) 创建主类型为 'audio' 的 MIME 消息对象。_audiodata 是音频数据的字节串,用于打包为 'audio/_subtype' MIME 类型的消息。当 _subtype 为 None 时,_audiodata 必须可被标准 Python 库模块 sndhdr 解析以确定子类型;否则,MIMEAudio 会引发 TypeError。3.11+ 由于 sndhdr 已被弃用,您应该始终指定 _subtype。当 _encoder 为 None 时,MIMEAudio 会将数据编码为 Base64,这通常是最佳的。否则,_encoder 必须是可调用的,带有一个参数 m,即正在构建的消息;_encoder 必须调用 m.get_payload 获取载荷,对载荷进行编码,通过调用 m.set_payload 将编码形式放回,并设置 m 的 Content-Transfer-Encoding 头。MIMEAudio 将 _params 字典的命名参数名和值传递给 m.add_header 以构造 m 的 Content-Type 头。

| MIMEBase | class MIMEBase(_maintype, subtype, **params) MIME 类的基类,扩展自 Message。实例化:

*`m`* = MIMEBase(*`mainsub`*, ***`params`*)

等同于更长且稍微不太方便的习语:

*`m`* = Message()
*`m`*.add_header('Content-Type', f'{*`main`*}/{*`sub`*}', 
             ***`params`*)
*`m`*.add_header('Mime-Version', '1.0')

|

MIMEImage class MIMEImage(_imagedata, _subtype=None, _encoder=None, **_params) 类似于 MIMEAudio,但主类型为 'image';使用标准 Python 模块 imghdr 来确定子类型(如果需要)。3.11+ 由于 imghdr 已被弃用,因此应始终指定 _subtype。
MIMEMessage class MIMEMessage(msg, _subtype='rfc822') 将 msg(必须是 Message 的一个实例(或子类))打包为 MIME 类型为 'message/_subtype' 的消息的有效载荷。
MIMEText class MIMEText(_text, _subtype='plain', _charset='us-ascii', _encoder=None) 将文本字符串 _text 打包为 MIME 类型为 'text/_subtype' 的消息的有效载荷,并使用给定的 _charset。当 _encoder 为 None 时,MIMEText 不对文本进行编码,这通常是最佳选择。否则,_encoder 必须是可调用的,带有一个参数 m,即正在构造的消息;然后,_encoder 必须调用 m.get_payload 获取有效载荷,对有效载荷进行编码,通过调用 m.set_payload 将编码形式放回,然后适当设置 m 的 Content-Transfer-Encoding 标题。

email.encoders 模块

email.encoders 模块提供了一些函数,这些函数以一个 nonmultipart 消息 m 作为它们唯一的参数,对 m 的有效载荷进行编码,并适当设置 m 的标题。这些函数列在表 21-4 中。

表 21-4. email.encoders 模块的函数

encode_base64 encode_base64(m) 使用 Base64 编码,通常对任意二进制数据最优(参见“The base64 Module”)。
encode_noop encode_noop(m) 不对 m 的有效载荷和标题进行任何操作。
encode_quopri encode_quopri(m) 使用 Quoted Printable 编码,通常对几乎但不完全是 ASCII 的文本最优(参见“The quopri Module”)。
encode_7or8bit encode_7or8bit(m) 不对 m 的有效载荷进行任何操作,但在 m 的有效载荷的任何字节具有高位设置时,将标题 Content-Transfer-Encoding 设置为 '8bit';否则,将其设置为 '7bit'。

email.utils 模块

email.utils 模块提供了几个用于电子邮件处理的函数,这些函数在表 21-5 中列出。

表 21-5. email.utils 模块的函数

formataddr formataddr(pair) 接受一对字符串(realname, email_address),并返回一个字符串 s,该字符串可插入到标题字段(如 To 和 Cc)中。当 realname 为假(例如空字符串 '')时,formataddr 返回 email_address
formatdate formatdate(timeval=None, localtime=False) 返回一个按照 RFC 2822 指定格式的时间瞬间的字符串。timeval 是自纪元以来的秒数。当 timeval 为 None 时,formatdate 使用当前时间。当 localtime 为 True 时,formatdate 使用本地时区;否则,它使用 UTC。
getaddresses getaddresses(L) 解析 L 的每个项目,L 是地址字符串的列表,如标题字段 To 和 Cc 中使用的,返回字符串对的列表 (name, address)。当 getaddresses 无法将 L 的项目解析为电子邮件地址时,它将 ('', '') 设置为列表中相应的项目。
mktime_tz mktime_tz(t) 返回一个浮点数,表示自纪元以来的秒数(UTC 时间),对应于 t 所指示的时刻。t 是一个包含 10 项的元组。t 的前九项与模块 time 中使用的格式相同,详见“时间模块”。t[-1] 是一个时间偏移量,单位为秒,相对于 UTC(与 time.timezone 的相反符号,由 RFC 2822 指定)。当 t[-1] 为 None 时,mktime_tz 使用本地时区。
parseaddr parseaddr(s) 解析字符串 s,其中包含像 To 和 Cc 这样的标题字段中通常指定的地址,并返回一个字符串对 (realname, address)。当 parseaddr 无法将 s 解析为地址时,它返回 ('', '')。
parsedate parsedate(s) 根据 RFC 2822 中的规则解析字符串 s,并返回一个包含九项的元组 t,如模块 time 中使用的(项 t[-3:] 无意义)。parsedate 还尝试解析一些通常遇到的邮件客户端使用的 RFC 2822 的错误变体。当 parsedate 无法解析 s 时,它返回 None。
parsedate_tz parsedate_tz(s) 类似于 parsedate,但返回一个包含 10 项的元组 t,其中 t[-1] 是 s 的时区,单位为秒,与 mktime_tz 接受的参数一样,但符号与 time.timezone 相反,如 RFC 2822 所指定。t[-4:-1] 项无意义。当 s 没有时区时,t[-1] 为 None
quote quote(s) 返回字符串 s 的副本,其中每个双引号 (") 都变为 '"',每个现有的反斜杠都重复。
unquote unquote(s) 返回字符串 s 的副本,其中移除了前导和尾随的双引号 (") 和尖括号 (<>),如果它们包围着 s 的其余部分。

邮件包的示例用法

邮件包不仅帮助您阅读和撰写邮件和类似邮件的消息(但不涉及接收和传输此类消息:这些任务属于单独的模块,在第十九章中涵盖)。以下是如何使用邮件来读取可能是多部分消息并将每个部分解包到给定目录中的文件的示例:

`import` pathlib, email
`def` unpack_mail(mail_file, dest_dir):
    *`'''Given file object mail_file, open for reading, and dest_dir,`*
       *`a string that is a path to an existing, writable directory,`*
       *`unpack each part of the mail message from mail_file to a`*
       *`file within dest_dir.`*
    *`'''`*
    dest_dir_path = pathlib.Path(dest_dir)
    `with` mail_file:
        msg = email.message_from_file(mail_file)
    `for` part_number, part `in` enumerate(msg.walk()):
        `if` part.get_content_maintype() == 'multipart':
            *`# we get each specific part later in the loop,`*
            *`# so, nothing to do for the 'multipart' itself`*
 `continue`
        dest = part.get_filename()
        `if` dest `is` `None`: dest = part.get_param('name')
        `if` dest `is` `None`: dest = f'part-{part_number}'
        *`# in real life, make sure that dest is a reasonable filename`*
        *`# for your OS; otherwise, mangle that name until it is`*
        part_payload = part.get_payload(decode=`True`)
        (dest_dir_path / dest).write_text(part_payload)

这里有一个执行大致相反任务的示例,将直接位于给定源目录下的所有文件打包成一个适合邮件发送的单个文件:

`def` pack_mail(source_dir, **headers):
     *`'''Given source_dir, a string that is a path to an existing,`*
        *`readable directory, and arbitrary header name/value pairs`*
        *`passed in as named arguments, packs all the files directly`*
        *`under source_dir (assumed to be plain text files) into a`*
        *`mail message returned as a MIME-formatted string.`*
     *`'''`*
     source_dir_path = pathlib.Path(source_dir)
     msg = email.message.Message()
     `for` name, value `in` headers.items():
         msg[name] = value
     msg['Content-type'] = 'multipart/mixed'
     filepaths = [path for path in source_dir_path.iterdir() 
                  if path.is_file()]
     `for` filepath `in` filepaths:
         m = email.message.Message()
         m.add_header('Content-type', 'text/plain', name=filename)
         m.set_payload(filepath.read_text())
         msg.attach(m)
     `return` msg.as_string()

将二进制数据编码为 ASCII 文本

几种媒体(例如电子邮件消息)只能包含 ASCII 文本。当您想通过这些媒体传输任意二进制数据时,需要将数据编码为 ASCII 文本字符串。Python 标准库提供支持名为 Base64、Quoted Printable 和 Unix-to-Unix 的标准编码的模块,下面将对这些进行描述。

模块 base64

base64 模块支持 RFC 3548 中指定的编码,包括 Base16、Base32 和 Base64。这些编码是一种将任意二进制数据表示为 ASCII 文本的紧凑方式,没有尝试生成可读的结果。base64 提供 10 个函数:6 个用于 Base64,以及 2 个用于 Base32 和 Base16。六个 Base64 函数列在 表 21-6 中。

表 21-6. base64 模块的 Base64 函数

b64decode b64decode(s, altchars=None, validate=False) 解码 B64 编码的字节串 s,并返回解码后的字节串。altchars,如果不为 None,必须是至少两个字符的字节串(多余字符将被忽略),指定要使用的两个非标准字符,而不是 + 和 /(可能对解码 URL 安全或文件系统安全的 B64 编码字符串有用)。当 validate 为 True 时,如果 s 包含任何无效的 B64 编码字符串(默认情况下,这些字节只是被忽略和跳过),则调用会引发异常。如果 s 没有按照 Base64 标准正确填充,则调用会引发异常。
b64encode b64encode(s, altchars=None) 编码字节串 s,并返回具有相应 B64 编码数据的字节串。altchars,如果不为 None,必须是至少两个字符的字节串(多余字符将被忽略),指定要使用的两个非标准字符,而不是 + 和 /(可能对制作 URL 安全或文件系统安全的 B64 编码字符串有用)。
standa⁠r⁠d⁠_​b⁠6⁠4⁠decode standard_b64decode(s) 类似于 b64decode(s)。
standa⁠r⁠d⁠_​b⁠6⁠4⁠encode standard_b64encode(s) 类似于 b64encode(s)。
urlsa⁠f⁠e⁠_​b⁠6⁠4⁠decode urlsafe_b64decode(s) 类似于 b64decode(s, '-_')。
urlsa⁠f⁠e⁠_​b⁠6⁠4⁠encode urlsafe_b64encode(s) 类似于 b64encode(s, '-_')。

四个 Base16 和 Base32 函数列在 表 21-7 中。

表 21-7. base64 模块的 Base16 和 Base32 函数

b16decode b16decode(s, casefold=False) 解码 B16 编码的字节串 s,并返回解码后的字节串。当 casefold 为 True 时,s 中的小写字符将被视为其大写等价字符;默认情况下,如果存在小写字符,调用会引发异常。
b16encode b16encode(s) 编码字节串 s,并返回具有相应 B16 编码数据的字节串。
b32decode b32decode(s, casefold=False, map01=None) 解码 B32 编码的字节串 s,返回解码后的字节串。当 casefold 为 True 时,将 s 中的小写字符视为它们的大写形式;默认情况下,如果 s 中存在小写字符,调用将引发异常。当 map01 为 None 时,输入中不允许字符 0 和 1;当 map01 不为 None 时,必须是一个指定 1 映射到的单字符字节串,即小写 'l' 或大写 'L';0 总是映射到大写 'O'。
b32encode b32encode(s) 编码字节串 s,返回相应的 B32 编码数据的字节串。

该模块还提供了用于编码和解码非标准但流行的编码 Base85 和 Ascii85 的函数,这些编码虽然未在 RFC 中规范,也不互通,但使用更大的字母表对编码的字节串进行了空间节省,节省率达 15%。详细信息请参阅在线文档中的相关函数。

quopri 模块

quopri 模块支持 RFC 1521 指定的 Quoted Printable(QP)编码。QP 可将任何二进制数据表示为 ASCII 文本,但主要用于大部分为文本且带有高位设置(即 ASCII 范围之外的字符)的数据。对于这样的数据,QP 生成的结果既紧凑又易读。quopri 模块提供了四个函数,列在表 21-8 中。

表 21-8. quopri 模块的功能

decode decode(infile, outfile, header=False) 通过调用 infile.readline 读取类似文件的二进制对象 infile,直到文件末尾(即直到 infile.readline 返回空字符串),解码读取的 QP 编码的 ASCII 文本,并将结果写入类似文件的二进制对象 outfile*.* 当 header 为 true 时,decode 也会将 _(下划线)转换为空格(根据 RFC 1522)。
decodestring decodestring(s, header=False) 解码 QP 编码的 ASCII 文本字节串 s,返回解码后的字节串。当 header 为 true 时,decodestring 也会将 _(下划线)转换为空格。
encode encode(infile, outfile, quotetabs, header=False) 通过调用 infile.readline 读取类似文件的二进制对象 infile,直到文件末尾(即直到 infile.readline 返回空字符串),使用 QP 编码读取的数据,并将编码的 ASCII 文本写入类似文件的二进制对象 outfile*.* 当 quotetabs 为 true 时,encode 也会编码空格和制表符。当 header 为 true 时,encode 将空格编码为 _(下划线)。
encodestring encodestring(s, quotetabs=False, header=False) 编码包含任意字节的字节串 s,返回包含 QP 编码的 ASCII 文本的字节串。当 quotetabs 为 true 时,encodestring 也会编码空格和制表符。当 header 为 true 时,encodestring 将空格编码为 _(下划线)。

uu 模块

支持经典Unix-to-Unix(UU)编码的uu模块¹受到 Unix 程序uuencodeuudecode的启发。 UU 将编码数据以开始行开头,包括正在编码的文件的文件名和权限,并以结束行结尾。 因此,UU 编码允许您将编码数据嵌入其他非结构化文本中,而 Base64 编码(在“base64 模块”中讨论)依赖于其他指示编码数据开始和结束的存在。 uu 模块提供了两个函数,列在表 21-9 中。

表 21-9. uu 模块的函数

decode decode(infile, outfile=None, mode=None) 通过调用infile.readline 读取类文件对象infile,直到文件末尾(即,直到调用infile.readline 返回空字符串)或终止行(由任意数量的空白包围的字符串'end')。 decode 解码读取的 UU 编码文本,并将解码后的数据写入类文件对象 outfile。 当 outfile 为None时,decode 会根据 UU 格式的开始行创建文件,并使用 mode 指定的权限位(当 mode 为None时,在开始行中指定的权限位)。 在这种情况下,如果文件已经存在,decode 会引发异常。
encode encode(infile, outfile, name='-', mode=0o666) 通过调用infile.read(每次读取 45 字节数据,这是 UU 编码后每行 60 个字符数据的量)读取类文件对象infile,直到文件末尾(即,直到调用infile.read 返回空字符串)。 它将读取的数据以 UU 编码方式编码,并将编码文本写入类文件对象outfile。 encode 还在文本之前写入一个 UU 格式的开始行,并在文本之后写入 UU 格式的结束行。 在开始行中,encode 指定文件名为 name,并指定 mode 为 mode。

¹ 在 Python 3.11 中已弃用,将在 Python 3.13 中删除;在线文档建议用户更新现有代码,使用 base64 模块处理数据内容,并使用 MIME 头处理元数据。

第二十二章:结构化文本:HTML

网络上的大多数文档使用 HTML,即超文本标记语言。 标记 是在文本文档中插入特殊标记(称为 标签 )以结构化文本。HTML 理论上是一种大而普遍的标准应用,称为 标准通用标记语言(SGML)。然而,在实践中,许多网络文档以松散或不正确的方式使用 HTML。

HTML 设计用于在浏览器中呈现文档。随着网络内容的发展,用户意识到它缺乏 语义标记 的能力,其中标记指示划分文本的意义而不仅仅是其外观。完全、精确地提取 HTML 文档中的信息通常是不可行的。一个更严格的标准称为 XHTML 试图弥补这些缺点。XHTML 类似于传统的 HTML,但是它是以 XML(可扩展标记语言)的术语来定义的,比 HTML 更加精确。您可以使用 第二十三章 中涵盖的工具处理格式良好的 XHTML。然而,截至本文撰写时,XHTML 并未取得压倒性成功,而是被更为实用的 HTML5 所取代。

尽管存在困难,通常可以从 HTML 文档中提取至少一些有用信息(称为 网页抓取蜘蛛行动 或仅为 抓取 的任务)。Python 标准库尝试帮助您,提供了用于解析 HTML 文档的 html 包,无论是为了呈现文档还是更典型地作为尝试从中提取信息的一部分。然而,当处理有些不完整的网页(这几乎总是情况!)时,第三方模块 BeautifulSoup 通常是您最后的、最好的希望。在本书中,出于实际原因,我们主要涵盖 BeautifulSoup,忽略与其竞争的标准库模块。寻求替代方案的读者也应该调查越来越流行的 scrapy 包

生成 HTML 和在 HTML 中嵌入 Python 也是相当频繁的任务。标准的 Python 库不支持 HTML 生成或嵌入,但可以使用 Python 字符串格式化,并且第三方模块也可以提供帮助。BeautifulSoup 允许您修改 HTML 树(因此,特别是可以程序化地构建一个,甚至“从头”开始);一个常见的替代方法是 模板化,例如由第三方模块 jinja2 支持,我们在 “jinja2 包” 中提供了基本内容。

html.entities 模块

Python 标准库中的 html.entities 模块提供了几个属性,全部都是映射关系(参见 表 22-1)。无论你用什么一般方法解析、编辑或生成 HTML,包括下一节介绍的 BeautifulSoup 包,这些属性都很有用。

表 22-1. html.entities 的属性

codepoi⁠n⁠t⁠2​n⁠a⁠m⁠e 将 Unicode 代码点映射到 HTML 实体名称。例如,entities.codepoint2name[228] 是 'auml',因为 Unicode 字符 228,ä,“带分音符的小写 a”,在 HTML 中编码为 'ä'。
entitydefs 将 HTML 实体名称映射到相应的 Unicode 等效单字符字符串。例如,entities.entitydefs['auml'] 是 'ä',而 entities.entitydefs['sigma'] 是 'σ'。
html5 将 HTML5 命名字符引用映射到等效的单字符字符串。例如,entities.xhtml5['gt;'] 是 '>'。键中的尾部分号 确实 重要 —— 少数但远非所有 HTML5 命名字符引用可以选择性地省略尾部分号,在这些情况下,entities.xhtml5 中会同时存在带有和不带有尾部分号的键。
name2codepoint 将 HTML 实体名称映射到 Unicode 代码点。例如,entities.name2codepoint['auml'] 是 228。

BeautifulSoup 第三方包

BeautifulSoup 允许你解析 HTML,即使它的格式相当糟糕。它使用简单的启发式方法来弥补典型的 HTML 损坏,并且在大多数情况下成功地完成这一艰巨任务。当前的 BeautifulSoup 主要版本是版本 4,也称为 bs4。在本书中,我们特别涵盖了版本 4.10;截至撰写本文时,这是 bs4 的最新稳定版本。

安装与导入 BeautifulSoup

BeautifulSoup 是那些包装要求你在 Python 内外使用不同名称的烦人模块之一。你可以通过在 shell 命令提示符下运行 pip install beautifulsoup4 来安装该模块,但在 Python 代码中导入时,你使用 import bs4。

BeautifulSoup 类

bs4 模块提供了 BeautifulSoup 类,通过调用它并传入一个或两个参数来实例化:第一个是 htmltext —— 可以是类似文件的对象(读取其中的 HTML 文本以解析),或者是字符串(作为要解析的文本)—— 第二个是可选的解析器参数。

BeautifulSoup 使用的解析器

如果您没有传递解析器参数,BeautifulSoup 会“嗅探周围”以选择最佳解析器(但在这种情况下可能会收到 GuessedAtParserWarning 警告)。 如果您没有安装其他解析器,则 BeautifulSoup 将默认使用 Python 标准库中的 html.parser; 如果您已安装其他解析器,则 BeautifulSoup 将默认使用其中之一(目前首选的是 lxml)。 除非另有说明,否则以下示例使用默认的 Python html.parser。 为了获得更多控制并避免 BeautifulSoup 文档中提到的解析器之间的差异,请在实例化 BeautifulSoup 时将要使用的解析器库的名称作为第二个参数传递。¹

例如,如果你已经安装了第三方包 html5lib(以与所有主流浏览器相同的方式解析 HTML,尽管速度较慢),你可以调用:

soup = bs4.BeautifulSoup(thedoc, 'html5lib')

当您将'xml'作为第二个参数传递时,您必须已经安装了第三方包 lxml。 BeautifulSoup 然后将文档解析为 XML,而不是 HTML。 在这种情况下,soup 的属性 is_xml 为True; 否则,soup.is_xml 为False。 如果您将'xml'作为第二个参数传递,也可以使用 lxml 解析 HTML。 更一般地说,您可能需要根据传递给 bs4.BeautifulSoup 调用的第二个参数选择安装适当的解析器库; 如果您没有这样做,BeautifulSoup 会通过警告消息提醒您。

这是在同一字符串上使用不同解析器的示例:

>>> `import` bs4, lxml, html5lib
>>> sh = bs4.BeautifulSoup('<p>hello', 'html.parser')
>>> sx = bs4.BeautifulSoup('<p>hello', 'xml')
>>> sl = bs4.BeautifulSoup('<p>hello', 'lxml')
>>> s5 = bs4.BeautifulSoup('<p>hello', 'html5lib')
>>> `for` s `in` [sh, sx, sl, s5]:
...   print(s, s.is_xml)
...
<p>hello</p> False
<?xml version="1.0" encoding="utf-8"?>
<p>hello</p> True
<html><body><p>hello</p></body></html> False
<html><head></head><body><p>hello</p></body></html> False

修复无效 HTML 输入中解析器之间的差异

在上面的示例中,'html.parser'仅插入了输入中缺失的结束标记

。 其他解析器在通过添加所需标记修复无效的 HTML 输入方面有所不同,例如、和,您可以在示例中看到。

BeautifulSoup、Unicode 和编码

BeautifulSoup 使用 Unicode,根据输入是否为字节串或二进制文件来推断或猜测编码²。 对于输出,prettify 方法返回树的 str 表示,包括标签及其属性。 prettify 使用空格和换行符添加到元素中以缩进元素,显示嵌套结构。 为了使其返回给定编码的 bytes 对象(字节串),请将编码名称作为参数传递给它。 如果您不想结果“漂亮”,请使用 encode 方法获取字节串,并使用 decode 方法获取 Unicode 字符串。 例如:

>>> s = bs4.BeautifulSoup('<p>hello', 'html.parser')
>>> print(s.prettify())
<p>
 hello
</p>
>>> print(s.decode())
<p>hello</p>
>>> print(s.encode())
b'<p>hello</p>'

bs4 的可导航类

类 BeautifulSoup 的实例b提供了“导航”解析 HTML 树的属性和方法,返回navigable classes Tag 和 NavigableString 的实例,以及 NavigableString 的子类(CData、Comment、Declaration、Doctype 和 ProcessingInstruction,仅在输出时的不同)。 。

每个可导航类的实例都可以让您继续导航——即几乎使用与b本身相同的一组导航属性和搜索方法获取更多信息。存在一些差异:Tag 的实例可以在 HTML 树中具有 HTML 属性和子节点,而 NavigableString 的实例不能(NavigableString 的实例始终具有一个文本字符串,一个父 Tag 和零个或多个同级,即同一父标记的其他子节点)。

可导航类术语

当我们说“NavigableString 的实例”时,我们包括其任何子类的实例;当我们说“Tag 的实例”时,我们包括 BeautifulSoup 的实例,因为后者是 Tag 的子类。可导航类的实例也称为树的元素节点

所有可导航类的实例都有属性名称:对于 Tag 实例,它是标签字符串,对于 BeautifulSoup 实例,它是'[document]',对于 NavigableString 实例,它是None

Tag 的实例允许您通过索引访问它们的 HTML 属性,或者您可以通过实例的.attrs 属性将它们全部作为字典获取。

索引 Tag 的实例

t是 Tag 的实例时,t['foo']会查找t的 HTML 属性中名为 foo 的属性,并返回 foo 属性的字符串。当t没有名为 foo 的 HTML 属性时,t['foo']会引发 KeyError 异常;类似于字典上的操作,可以调用t.get('foo', default=None)来获取默认参数值,而不是异常。

一些属性,如 class,在 HTML 标准中被定义为可以具有多个值(例如,...)。在这些情况下,索引返回一个值列表,例如 soup.body['class']将是['foo', 'bar'](如果属性不存在,再次,您将得到一个 KeyError 异常;使用 get 方法而不是索引来获取默认值)。

要获得将属性名称映射到值(或在 HTML 标准中定义的少数情况下,值列表)的字典,请使用属性t.attrs:

>>> s = bs4.BeautifulSoup('<p foo="bar" class="ic">baz')
>>> s.get('foo')
>>> s.p.get('foo')
'bar'
>>> s.p.attrs
{'foo': 'bar', 'class': ['ic']}

如何检查 Tag 实例是否具有某个特定属性

要检查 Tag 实例t的 HTML 属性是否包含名为'foo'的属性,请不要使用 if 'foo' in t:——在 Tag 实例上的 in 运算符会在 Tag 的子级中查找,而不是在其属性中查找。而是,请使用 if 'foo' in t.attrs:或者更好地,使用 if t.has_attr('foo'):。

获取实际字符串

当您有一个 NavigableString 的实例时,通常希望访问它包含的实际文本字符串。当您有一个 Tag 的实例时,您可能希望访问它包含的唯一字符串,或者如果包含多个字符串,则希望访问所有这些字符串,也许还带有其周围任何空格的文本剥离。以下是您可以完成这些任务的最佳方法。

当你有一个 NavigableString 实例 s 并且需要将其文本存储或处理在其他地方,而不需要进一步对其进行导航时,请调用 str(s)。或者,使用 s.encode(codec='utf8') 得到一个字节串,或者 s.decode() 得到一个文本字符串(即 Unicode)。这些方法给出了实际的字符串,而不包含对 BeautifulSoup 树的引用,这会妨碍垃圾回收(s 支持 Unicode 字符串的所有方法,因此如果这些方法满足你的需求,可以直接调用它们)。

给定一个包含单个 NavigableString 实例 s 的 Tag 实例 t,你可以使用 t.string 来获取 s(或者,如果只想从 s 获取你想要的文本,可以使用 t.string.decode())。当 t 有一个单一子项是 NavigableString,或者有一个单一子项是 Tag,其唯一子项是 NavigableString 时,t.string 才有效;否则,t.string 为 None

作为所有包含的(可导航的)字符串的迭代器,使用 t.strings。你可以使用 ''.join(t.strings) 将所有字符串连接成一个单独的字符串,一次完成。要忽略每个包含字符串周围的空白,请使用迭代器 t.stripped_strings(它还会跳过所有空白字符串)。

或者,调用 t.get_text():这将返回一个单一的(Unicode)字符串,其中包含 t 的后代中所有的文本,按照树的顺序(等同于访问属性 t.text)。你可以选择传递一个字符串作为分隔符的唯一位置参数。默认为空字符串,''。传递命名参数 strip=True 可以使每个字符串去除周围的空白,并跳过所有空白字符串。

以下示例演示了从标签内获取字符串的这些方法:

>>> soup = bs4.BeautifulSoup('<p>Plain <b>bold</b></p>')
>>> print(soup.p.string)
None
>>> print(soup.p.b.string)
bold
>>> print(''.join(soup.strings))
Plain bold
>>> print(soup.get_text())
Plain bold
>>> print(soup.text)
Plain bold
>>> print(soup.get_text(strip=True))
Plainbold

BeautifulSoup 和 Tag 的实例上的属性引用。

在 bs4 中,导航 HTML 树或子树的最简单、最优雅的方法是使用 Python 的属性引用语法(只要你命名的每个标签都是唯一的,或者你只关心每个下降级别的第一个命名标签)。

给定 Tag 的任何实例 t,类似 t.foo.bar 的结构会查找 t 的后代中的第一个 foo 标签,并获取它的 Tag 实例 ti,然后查找 ti 的后代中的第一个 bar 标签,并返回 bar 标签的 Tag 实例。

当你知道在可导航实例的后代中有某个标签的单一出现,或者当你只关心第一个出现的几个时,这是一种简洁而优雅的导航树的方式。但要注意:如果任何查找层级找不到正在寻找的标签,则属性引用的值为 None,然后任何进一步的属性引用都会引发 AttributeError。

警惕标签实例属性引用中的拼写错误。

由于这种 BeautifulSoup 的行为,如果在 Tag 实例的属性引用中存在任何拼写错误,将会得到 None 的值,而不是 AttributeError 异常——因此,请特别小心!

bs4 还提供了更一般的方法沿树向下、向上和侧向导航。特别地,每个可导航类实例都有属性,用于标识单个“相对”的或者复数形式下的所有相同类的迭代器。

contents、children 和 descendants

给定 Tag 的实例t,您可以获取其所有子节点的列表作为t.contents,或者作为所有子节点的迭代器的t.children。要获取所有descendants(子节点、子节点的子节点等),请使用t.descendants 的迭代器:

>>> soup = bs4.BeautifulSoup('<p>Plain <b>bold</b></p>')
>>> list(t.name `for` t `in` soup.p.children)
[None, 'b']
>>> list(t.name `for` t `in` soup.p.descendants)
[None, 'b', None]

None的名称对应于 NavigableString 节点;它们中的第一个是 p 标签的child,但两者都是该标签的descendants

parent 和 parents

给定任何可导航类的实例n,其父节点是n.parent:

>>> soup = bs4.BeautifulSoup('<p>Plain <b>bold</b></p>')
>>> soup.b.parent.name
'p'

一个在树中向上迭代所有祖先节点的迭代器是n.parents。这也包括 NavigableString 的实例,因为它们也有父节点。BeautifulSoup 的实例bb.parent 是None,并且b.parents 是一个空迭代器。

next_sibling、previous_sibling、next_siblings 和 previous_siblings

给定任何可导航类的实例n,其紧邻左侧的兄弟节点是n.previous_sibling,紧邻右侧的兄弟节点是n.next_sibling;如果n没有这样的兄弟节点,则可以是None。在树中向左迭代所有左侧兄弟节点的迭代器是n.previous_siblings;在树中向右迭代所有右侧兄弟节点的迭代器是n.next_siblings(这两个迭代器都可能为空)。这也包括 NavigableString 的实例,因为它们也有兄弟节点。对于 BeautifulSoup 的实例bb.previous_sibling 和b.next_sibling 都是None,其兄弟节点迭代器都是空的:

>>> soup = bs4.BeautifulSoup('<p>Plain <b>bold</b></p>')
>>> soup.b.previous_sibling, soup.b.next_sibling
('Plain ', None)

next_element、previous_element、next_elements 和 previous_elements

给定任何可导航类的实例n,其解析的前一个节点是n.previous_element,解析的后一个节点是n.next_element;当n是第一个或最后一个解析的节点时,其中一个或两者可以是None。在树中向后迭代所有先前元素的迭代器是n.previous_elements;在树中向前迭代所有后续元素的迭代器是n.next_elements(这两个迭代器都可能为空)。NavigableString 的实例也具有这些属性。对于 BeautifulSoup 的实例bb.previous_element 和b.next_element 都是None,其元素迭代器都是空的:

>>> soup = bs4.BeautifulSoup('<p>Plain <b>bold</b></p>')
>>> soup.b.previous_element, soup.b.next_element
('Plain ', 'bold')

如前例所示,b 标签没有 next_sibling(因为它是其父节点的最后一个子节点);但是,它确实有一个 next_element(紧随其后解析的节点,在本例中是其包含的'bold'字符串)。

bs4 的 find…方法(又称搜索方法)

每个可导航类在 bs4 中提供了几种方法,这些方法的名称以 find 开头,称为搜索方法,用于定位满足指定条件的树节点。

搜索方法成对出现——每对方法中的一个方法遍历树的所有相关部分并返回满足条件的节点列表,而另一个方法在找到满足所有条件的单个节点时停止并返回它(或在找不到这样的节点时返回 None)。因此,调用后者的方法就像调用前者的方法,并使用参数 limit=1,然后索引结果为单项目列表以获得其单个项目,但更快更优雅。

因此,例如,对于任何标签实例 t 和由 ... 表示的任何位置参数和命名参数组,以下等价性总是成立:

just_one = *`t`*.find(...)
other_way_list = *`t`*.find_all(..., limit=1)
other_way = other_way_list[0] `if` other_way_list `else` `None`
`assert` just_one == other_way

方法对列在 表 22-2 中。

表格 22-2. bs4 find... 方法对

| find, find_all | b.find(...),b.find_all(...)

搜索 bdescendants 或者当你传递命名参数 recursive=False(仅适用于这两种方法,而不适用于其他搜索方法)时,仅限于 bchildren。这些方法在 NavigableString 实例上不可用,因为它们没有后代;所有其他搜索方法在标签和 NavigableString 实例上都可用。

由于经常需要 find_all,bs4 提供了一个优雅的快捷方式:调用一个标签就像调用它的 find_all 方法一样。换句话说,当 b 是一个标签时,b(...) 等同于 b.find_all(...)。

另一个快捷方式,已在 “BeautifulSoup 和 Tag 实例上的属性引用” 中提到,即 b.foo.bar 等同于 b.find('foo').find('bar')。

| find_next, find_all_next | b.find_next(...),b.find_all_next(...)

搜索 b 的下一个元素。

| find_next_sibling, find_next_siblings | b.find_next_sibling(...),b.find_next_siblings(...)

搜索 b 的下一个兄弟。

| find_parent, find_parents | b.find_parent(...),b.find_parents(...)

搜索 b 的父元素。

| find_previous, find_all_previous | b.find_previous(...),b.find_all_previous(...)

搜索 b 的前一个元素。

find_previous_sibling, find_previous_siblings b.find_previous_sibling(...),b.find_previous_siblings(...) 搜索 b 的前一个兄弟。

搜索方法的参数

每个搜索方法都有三个可选参数:nameattrsstringnamestringfilters,如下一小节所述;attrs 是一个字典,如本节后面所述。另外,如 表 22-2 中所述,仅 find 和 find_all(而不是其他搜索方法)可以选择使用命名参数 recursive=False 进行调用,以限制搜索范围仅限于子代,而不是所有后代。

返回列表的任何搜索方法(即其名称为复数或以 find_all 开头)可以选择接受命名参数 limit:其值(如果有)为整数,将返回的列表长度上限化(当您传递 limit 时,如有必要,返回的列表结果将被截断)。

在这些可选参数之后,每个搜索方法可以选择具有任意数量的任意命名参数:参数名称可以是任何标识符(除了搜索方法的特定参数名称),而值是筛选器。

筛选器

filter应用于targettarget可以是标签的名称(当作为name参数传递时)、Tag 的字符串或 NavigableString 的文本内容(当作为string参数传递时)、或 Tag 的属性(当作为命名参数的值传递或在attrs参数中)。 每个筛选器可以是:

Unicode 字符串

筛选器成功时,字符串完全等于目标。

字节字符串

使用 utf-8 解码为 Unicode,当生成的 Unicode 字符串完全等于目标时,筛选器成功。

正则表达式对象(由 re.compile 生成,详见“正则表达式和 re 模块”)

当 RE 的搜索方法以目标作为参数调用成功时,筛选器成功。

字符串列表

如果任何字符串完全等于目标(如果任何字符串为字节字符串,则使用 utf-8 解码为 Unicode)则筛选器成功。

函数对象

当使用 Tag 或 NavigableString 实例作为参数调用函数时返回 True 时,筛选器成功。

True

筛选器总是成功。

作为“筛选器成功”的同义词,我们也说“目标与筛选器匹配”。

每个搜索方法都会查找所有与其所有筛选器匹配的相关节点(即,在每个候选节点上隐式执行逻辑and操作)。 (不要将此逻辑与具有列表作为参数值的特定筛选器的逻辑混淆。其中一个筛选器匹配列表中的任何项时,该筛选器隐式执行逻辑or操作。)

名称

要查找名称匹配筛选器的标签,请将筛选器作为搜索方法的第一个位置参数传递,或将其作为 name=filter传递:

*`# return all instances of Tag 'b' in the document`*
soup.find_all('b') *`# or soup.find_all(name='b')`*

*`# return all instances of Tags 'b' and 'bah' in the document`*
soup.find_all(['b', 'bah'])

*`# return all instances of Tags starting with 'b' in the document`*
soup.find_all(re.compile(r'^b'))

*`# return all instances of Tags including string 'bah' in the document`*
soup.find_all(re.compile(r'bah'))

*`# return all instances of Tags whose parent's name is 'foo'`*
`def` child_of_foo(tag):
    `return` tag.parent.name == 'foo'

soup.find_all(child_of_foo)

字符串

要查找其.string 文本与筛选器匹配的 Tag 节点,或者文本与筛选器匹配的 NavigableString 节点,请将筛选器作为字符串=filter传递:

*`# return all instances of NavigableString whose text is 'foo'`*
soup.find_all(string='foo')

*`# return all instances of Tag 'b' whose .string's text is 'foo'`*
soup.find_all('b', string='foo')

属性

要查找具有值匹配筛选器的属性的 Tag 节点,请使用将属性名称作为键和相应筛选器作为相应值的字典d。 然后,将d作为搜索方法的第二个位置参数传递,或将 attrs=d传递。

作为特例,您可以使用d中的值None而不是筛选器;这将匹配缺少相应属性的节点。

作为单独的特殊情况,如果 attrs 的值 f 不是字典,而是过滤器,则相当于具有 attrs={'class': *f*}。 (此便捷快捷方式非常有用,因为查找具有特定 CSS 类的标签是频繁的任务。)

你不能同时应用这两种特殊情况:要搜索没有任何 CSS 类的标签,必须显式地传递 attrs={'class': **None**}(即使用第一个特殊情况,但不能同时使用第二个):

*`# return all instances of Tag 'b' w/an attribute 'foo' and no 'bar'`*
soup.find_all('b', {'foo': `True`, 'bar': `None`})

匹配具有多个 CSS 类的标签

与大多数属性不同,标签的 'class' 属性可以具有多个值。这些在 HTML 中显示为以空格分隔的字符串(例如,'<p class='foo bar baz'>...'),在 bs4 中作为字符串列表显示(例如,t['class'] 为 ['foo', 'bar', 'baz'])。

在任何搜索方法中按 CSS 类过滤时,如果标签的多个 CSS 类中有一个匹配,过滤器将匹配该标签。

要通过多个 CSS 类匹配标签,可以编写自定义函数并将其作为过滤器传递给搜索方法;或者,如果不需要搜索方法的其他增加功能,则可以避免搜索方法,而是使用后续部分中介绍的 *t*.select 方法,并按 CSS 选择器的语法进行操作。

其他命名参数

命名参数,超出搜索方法已知名称的参数,用于增强已指定的 attrs 约束(如果有)。例如,调用搜索方法带有 *foo*=*bar* 相当于带有 attrs={'*foo*': *bar*}

bs4 CSS 选择器

bs4 标签提供 selectselect_one 方法,大致相当于 find_allfind,但接受一个字符串作为参数,该字符串是 CSS 选择器,分别返回满足该选择器的 Tag 节点列表或第一个这样的 Tag 节点。例如:

`def` foo_child_of_bar(t):
    `return` t.name=='foo' `and` t.parent `and` t.parent.name=='bar'

*`# return tags with name 'foo' children of tags with name 'bar'`*
soup.find_all(foo_child_of_bar)

*`# equivalent to using find_all(), with no custom filter function needed`*
soup.select('bar > foo')

bs4 仅支持丰富的 CSS 选择器功能的子集,在本书中不再详细介绍 CSS 选择器。(要完整了解 CSS,建议阅读 O’Reilly 的 CSS: The Definitive Guide,作者是 Eric Meyer 和 Estelle Weyl。)在大多数情况下,前一节中介绍的搜索方法是更好的选择;然而,在一些特殊情况下,调用 select 可以避免编写自定义过滤函数(稍微麻烦的小事)。

使用 BeautifulSoup 进行 HTML 解析的示例

以下示例使用 bs4 执行典型任务:从 Web 获取页面、解析页面并输出页面中的 HTTP 超链接:

`import` urllib.request, urllib.parse, bs4

f = urllib.request.urlopen('http://www.python.org')
b = bs4.BeautifulSoup(f)

seen = set()
`for` anchor `in` b('a'):
    url = anchor.get('href')
    `if` url `is` `None` `or` url `in` seen:
        `continue`
    seen.add(url)
    pieces = urllib.parse.urlparse(url)
    `if` pieces[0].startswith('http'):
        print(urllib.parse.urlunparse(pieces))

首先调用类 bs4.BeautifulSoup 的实例(等同于调用其 find_all 方法),以获取特定标签(这里是 <a> 标签)的所有实例,然后再获取该标签实例的 get 方法来获取属性的值(这里是 'href'),或者在该属性缺失时返回 None

生成 HTML

Python 没有专门用于生成 HTML 的工具,也没有让你直接在 HTML 页面中嵌入 Python 代码的工具。通过模板化(在“模板化”中讨论),通过分离逻辑和表示问题来简化开发和维护。还可以使用 bs4 在 Python 代码中创建 HTML 文档,逐步修改非常简单的初始文档。由于这些修改依赖于 bs4 解析某些 HTML,因此使用不同的解析器会影响输出,如在“BeautifulSoup 使用哪个解析器”中提到的那样。

使用 bs4 编辑和创建 HTML

编辑 Tag 的实例t有各种选项。你可以通过赋值给t.name 改变标签名,通过将t视为映射来改变t的属性:赋值给索引以添加或更改属性,或删除索引以移除属性(例如,del t['foo']移除属性 foo)。如果你将一些字符串赋给t.string,那么所有先前的t.contents(标签和/或字符串—t的整个子树)都将被丢弃,并替换为具有该字符串作为其文本内容的新 NavigableString 实例。

给定 NavigableString 实例s,你可以替换其文本内容:调用s.replace_with('other')将s的文本替换为'other'。

构建和添加新节点

修改现有节点很重要,但从头开始构建 HTML 文档时创建新节点并将其添加到树中至关重要。

要创建一个新的 NavigableString 实例,请调用类并将文本内容作为唯一参数:

s = bs4.NavigableString(' some text ')

要创建一个新的 Tag 实例,请调用 BeautifulSoup 实例的 new_tag 方法,将标签名作为唯一的位置参数,并(可选地)为属性命名参数。

>>> soup = bs4.BeautifulSoup()
>>> t = soup.new_tag('foo', bar='baz')
>>> print(t)
<foo bar="baz"></foo>

要将节点添加到 Tag 的子节点中,请使用 Tag 的 append 方法。这将在任何现有子节点之后添加节点:

>>> t.append(s)
>>> print(t)
<foo bar="baz"> some text </foo>

如果你希望新节点不是在结尾,而是在t的子节点中的某个索引处,请调用t.insert(n, s)将s放置在t.contents 的索引n处(t.append 和t.insert 的工作方式就像t是其子节点列表一样)。

如果你有一个可导航的元素b,想要将一个新节点x添加为b的 previous_sibling,请调用b.insert_before(x)。如果你希望x代替b的 next_sibling,请调用b.insert_after(x)。

如果你想将新的父节点t包裹在b周围,调用b.wrap(t)(这也返回新包裹的标签)。例如:

>>> print(t.string.wrap(soup.new_tag('moo', zip='zaap')))
<moo zip="zaap"> some text </moo>
>>> print(t)
<foo bar="baz"><moo zip="zaap"> some text </moo></foo>

替换和移除节点

你可以在任何标签t上调用t.replace_with:该调用将替换t及其先前的所有内容为参数,并返回具有其原始内容的t。例如:

>>> soup = bs4.BeautifulSoup(
...        '<p>first <b>second</b> <i>third</i></p>', 'lxml')
>>> i = soup.i.replace_with('last')
>>> soup.b.append(i)
>>> print(soup)
<html><body><p>first <b>second<i>third</i></b> last</p></body></html>

你可以在任何标签t上调用t.unwrap:该调用将替换t及其内容,并返回“清空”的t(即,没有内容)。例如:

>>> empty_i = soup.i.unwrap()
>>> print(soup.b.wrap(empty_i))
<i><b>secondthird</b></i>
>>> print(soup)
<html><body><p>first <i><b>secondthird</b></i> last</p></body></html>

t.clear 移除t的内容,销毁它们,并将t留空(但仍然位于树中的原始位置)。t.decompose 移除并销毁t本身及其内容:

>>> *`# remove everything between <i> and </i> but leave tags`*	
>>> soup.i.clear()
>>> print(soup)
<html><body><p>first <i></i> last</p></body></html>
>>> *`# remove everything between <p> and </p> incl. tags`*
>>> soup.p.decompose()
>>> print(soup)
<html><body></body></html>
>>> *`# remove <body> and </body>`*
>>> soup.body.decompose()
>>> print(soup)
<html></html>

最后,t.extract 提取并返回t及其内容,但不销毁任何内容。

使用 bs4 构建 HTML

下面是一个示例,展示了如何使用 bs4 的方法生成 HTML。具体来说,以下函数接受一个“行”(序列)的序列,并返回一个字符串,该字符串是一个 HTML 表格,用于显示它们的值:

`def` mktable_with_bs4(seq_of_rows):
    tabsoup = bs4.BeautifulSoup('<table>')
    tab = tabsoup.table
    `for` row `in` seq_of_rows:
        tr = tabsoup.new_tag('tr')
        tab.append(tr)
        `for` item `in` row:
            td = tabsoup.new_tag('td')
            tr.append(td)
            td.string = str(item)
    `return` tab

这里是使用我们刚刚定义的函数的示例:

>>> example = (
...     ('foo', 'g>h', 'g&h'),
...     ('zip', 'zap', 'zop'),
... )
>>> print(mktable_with_bs4(example))
<table><tr><td>foo</td><td>g&gt;h</td><td>g&amp;h</td></tr>
<tr><td>zip</td><td>zap</td><td>zop</td></tr></table>

注意,bs4 会自动将标记字符如<, >和&转换为它们对应的 HTML 实体;例如,'g>h'呈现为'g>h'。

模板化

要生成 HTML,通常最好的方法是模板化。您可以从一个模板开始——一个文本字符串(通常从文件、数据库等读取),它几乎是有效的 HTML,但包含占位符(称为占位符),在动态生成的文本必须插入的位置;您的程序生成所需的文本并将其替换到模板中。

在最简单的情况下,您可以使用形式为{name}的标记。将动态生成的文本设置为某个字典d中键'name'的值。Python 的字符串格式化方法.format(在“字符串格式化”中讨论)让您完成剩下的工作:当t是模板字符串时,t.format(d)是模板的副本,所有值都得到了正确的替换。

一般来说,除了替换占位符之外,您还会希望使用条件语句,执行循环,并处理其他高级格式和展示任务;在将“业务逻辑”与“展示问题”分离的精神下,您更喜欢所有后者作为模板的一部分。这就是专门的第三方模板化包的用武之地。这里有许多这样的包,但本书的所有作者,都曾使用过并编写过其中一些,目前更倾向于使用jinja2,接下来进行详细介绍。

jinja2 包

对于严肃的模板化任务,我们推荐使用 jinja2(在PyPI上可用,像其他第三方 Python 包一样,因此可以轻松通过pip install jinja2安装)。

jinja2 文档非常出色和详尽,涵盖了模板语言本身(在概念上模仿 Python,但有许多不同之处,以支持在 HTML 中嵌入它和特定于展示问题的独特需求);您的 Python 代码用于连接到 jinja2 的 API,并在必要时扩展或扩展它;以及其他问题,从安装到国际化,从代码沙箱到从其他模板引擎移植——更不用说宝贵的提示和技巧了。

在本节中,我们仅涵盖了 jinja2 强大功能的一小部分,这些足以让你在安装后开始使用。我们强烈建议阅读 jinja2 的文档,以获取大量额外有用的信息。

jinja2.Environment 类

当你使用 jinja2 时,总会涉及一个 Environment 实例——在少数情况下,你可以让它默认为一个通用的“共享环境”,但这不推荐。只有在非常高级的用法中,当你从不同来源获取模板(或使用不同的模板语言语法)时,才会定义多个环境实例——通常情况下,你会实例化一个单独的 Environment 实例 env,用于渲染所有需要的模板。

你可以在构建 env 时通过向其构造函数传递命名参数的方式进行多种方式的定制(包括修改关键的模板语言语法方面,比如哪些定界符用于开始和结束块、变量、注释等)。在实际使用中,你几乎总是会传递一个名为 loader 的命名参数(其他很少设置)。

环境的 loader 指定了如何在请求时加载模板——通常是文件系统中的某个目录,或者也许是某个数据库(你需要编写 jinja2.Loader 的自定义子类来实现后者),但也有其他可能性。你需要一个 loader 来让模板享受 jinja2 的一些最强大的特性,比如 template inheritance

你可以在实例化 env 时配备自定义 filters, tests, extensions 等(这些也可以稍后添加)。

在稍后的示例中,我们假设 env 是通过 loader=jinja2.FileSystemLoader('/path/to/templates') 实例化的,没有进一步的增强——事实上,为简单起见,我们甚至不会使用 loader 参数。

env.get_template(name) 获取、编译并返回基于 env.loader(name) 返回内容的 jinja2.Template 实例。在本节末尾的示例中,为简单起见,我们将使用罕见的 env.from_string(s) 来从字符串 s 构建 jinja2.Template 的实例。

jinja2.Template 类

jinja2.Template 的一个实例 t 拥有许多属性和方法,但在实际生活中你几乎只会使用以下这个:

| 渲染 | t.render(...context...) context 参数与传递给 dict 构造函数的内容相同——一个映射实例,和/或丰富和潜在覆盖映射键值连接的命名参数。

t.render(context) 返回一个(Unicode)字符串,该字符串是应用于模板 tcontext 参数后生成的结果。

使用 jinja2 构建 HTML

这里是一个使用 jinja2 模板生成 HTML 的示例。具体来说,就像在“用 bs4 构建 HTML”中一样,以下函数接受一个“行”(序列)的序列,并返回一个 HTML 表格来显示它们的值:

TABLE_TEMPLATE = '''\ <table>
{% for s in s_of_s %}
 <tr>
  {% for item in s %}
 <td>{{item}}</td>
  {% endfor %}
 </tr>
{% endfor %}
</table>'''
`def` mktable_with_jinja2(s_of_s):
    env = jinja2.Environment(
        trim_blocks=`True`,
        lstrip_blocks=`True`,
        autoescape=`True`)
    t = env.from_string(TABLE_TEMPLATE)
    `return` t.render(s_of_s=s_of_s)

函数使用选项 autoescape=True,自动“转义”包含标记字符如<, >和&的字符串;例如,使用 autoescape=True,'g>h'渲染为'g>h'。

选项 trim_blocks=True和 lstrip_blocks=True纯粹是为了美观起见,以确保模板字符串和渲染的 HTML 字符串都能被良好地格式化;当然,当浏览器渲染 HTML 时,HTML 文本本身是否良好格式化并不重要。

通常情况下,您会始终使用加载器参数构建环境,并通过方法调用如t = env.get_template(template_name)从文件或其他存储加载模板。在这个示例中,为了一目了然,我们省略了加载器,并通过调用方法env.from_string 从字符串构建模板。请注意,jinja2 不是 HTML 或 XML 特定的,因此仅使用它并不能保证生成内容的有效性,如果需要符合标准,您应该仔细检查生成的内容。

该示例仅使用 jinja2 模板语言提供的众多功能中最常见的两个特性:循环(即,用{% for ... %}和{% endfor %}括起来的块)和参数替换(内联表达式用{{和}}括起来)。

这里是我们刚定义的函数的一个示例用法:

>>> example = (
...   ('foo', 'g>h', 'g&h'),
...   ('zip', 'zap', 'zop'),
... )
>>> print(mktable_with_jinja2(example))
<table>
 <tr>
 <td>foo</td>
 <td>g&gt;h</td>
 <td>g&amp;h</td>
 </tr>
 <tr>
 <td>zip</td>
 <td>zap</td>
 <td>zop</td>
 </tr>
</table>

¹ BeautifulSoup 的文档提供了关于安装各种解析器的详细信息。

² 正如在 BeautifulSoup 的文档中解释的那样,它还展示了各种指导或完全覆盖 BeautifulSoup 关于编码猜测的方法。

第二十三章:结构化文本:XML

XML,即 可扩展标记语言,是广泛使用的数据交换格式。除了 XML 本身外,XML 社区(主要在万维网联盟(W3C)内)还标准化了许多其他技术,如模式语言、命名空间、XPath、XLink、XPointer 和 XSLT。

行业联盟还定义了基于 XML 的行业特定标记语言,用于其各自领域应用程序之间的数据交换。XML、基于 XML 的标记语言和其他与 XML 相关的技术经常用于特定领域中的应用程序间、跨语言、跨平台的数据交换。

出于历史原因,Python 标准库在 xml 包下支持多个支持 XML 的模块,具有重叠的功能;本书并未覆盖所有内容,但感兴趣的读者可以在 在线文档 中找到详细信息。

本书(特别是本章)仅涵盖了处理 XML 的最 Pythonic 方法:ElementTree,由我们深感怀念的 Fredrik Lundh,即“effbot”创建。它的优雅、速度、通用性、多种实现和 Pythonic 架构使其成为 Python XML 应用程序的首选包。有关 xml.etree.ElementTree 模块的教程和完整详情,超出了本章提供的内容,请参阅在线文档。本书默认读者具有一些关于 XML 本身的基础知识;如果你需要了解更多关于 XML 的知识,我们推荐 XML in a Nutshell 由 Elliotte Rusty Harold 和 W. Scott Means(O’Reilly)编写。

从不受信任的源解析 XML 会使你的应用程序面临许多可能的攻击风险。我们并未专门讨论这个问题,但 在线文档 建议使用第三方模块来帮助保护你的应用程序,如果你确实需要从无法完全信任的源解析 XML。特别是,如果你需要具有针对解析不受信任源的安全防护的 ElementTree 实现,请考虑使用 defusedxml.ElementTree

ElementTree

Python 和第三方插件提供了几种 ElementTree 功能的替代实现;你始终可以依赖的是标准库中的模块 xml.etree.ElementTree。只需导入 xml.etree.ElementTree,即可获得你的 Python 安装中标准库中最快的实现。本章介绍的第三方包 defusedxml,提供了略慢但更安全的实现,如果你需要从不受信任的源解析 XML;另一个第三方包 lxml 则提供了更快的性能和一些额外的功能,通过 lxml.etree

传统上,您可以使用类似以下的 from...import...as 语句获取您喜欢使用的 ElementTree 实现:

`from` xml.etree `import` ElementTree `as` et

或者尝试导入 lxml,并在无法导入时退回到标准库中提供的版本:

`try`:
    `from` lxml `import` etree `as` et
`except` ImportError:
    `from` xml.etree `import` ElementTree `as` et

成功导入实现后,将其作为 et 使用(有些人喜欢大写变体 ET)在您的代码的其余部分中使用它。

ElementTree 提供了一个表示 XML 文档中节点的基本类:Element 类。ElementTree 还提供了其他重要的类,主要是代表整个树的类,具有输入和输出方法以及许多方便的类,等效于其 Element root 上的类——即 ElementTree 类。此外,ElementTree 模块提供了几个实用函数和次要重要的辅助类。

Element 类

Element 类代表了 XML 文档中的一个节点,是整个 ElementTree 生态系统的核心。每个元素有点像映射,具有将字符串键映射到字符串值的属性,也有点像序列,具有其他元素(有时称为元素的“子元素”)的子节点。此外,每个元素还提供了一些额外的属性和方法。每个 Element 实例 e 有四个数据属性或属性,详见 Table 23-1。

Table 23-1. Element 实例 e 的属性

| attrib | 包含 XML 节点所有属性的字典,以字符串为键(通常相应的值也是字符串)。例如,解析 XML 片段 bc,得到一个 e 其中 e.attrib 是 {'x': 'y'}。

避免访问 Element 实例上的 attrib

在可能的情况下,最好避免访问 e.attrib,因为实现可能需要在访问时动态构建它。e 本身提供了一些典型的映射方法(列在 Table 23-2 中),您可能希望在 e.attrib 上调用这些方法;通过 e 自己的方法让实现可以为您优化性能,而不是通过实际的字典 e.attrib 获得的性能。

|

tag 节点的 XML 标签:一个字符串,有时也称为元素的类型。例如,解析 XML 片段 bc,得到一个 e 其中 e.tag 设置为 'a'。
tail 紧随元素之后的任意数据(字符串)。例如,解析 XML 片段 bc,得到一个 e 其中 e.tail 设置为 'c'。
text 直接“在”元素内的任意数据(字符串)。例如,解析 XML 片段 bc,得到一个 e 其中 e.text 设置为 'b'。

e 有一些类似映射的方法,避免了需要显式请求 e.attrib 字典。这些方法在 Table 23-2 中列出。

表 23-2. Element 实例 e 的类似映射方法

clear e.clear() “清空” e,除了其标签外,移除所有属性和子元素,并将文本和尾部设置为 None
get e.get(key, default=None) 类似于 e.attrib.get(key, default),但可能更快。不能使用 e[key],因为在 e 上进行索引用于访问子元素,而不是属性。
items e.items() 返回所有属性的 (name, value) 元组列表,顺序任意。
keys e.keys() 返回所有属性名的列表,顺序任意。
set e.set(key, value) 将名为 key 的属性设置为 value

e 的其他方法(包括使用 e[i] 语法进行索引和获取长度的方法,如 len(e))处理 e 的所有子元素作为一个序列,或者在某些情况下——如本节其余部分所示——处理 e 的所有后代(以 e 为根的子树中的元素,也称为 e 的子元素)。

不要依赖于 Element 的隐式布尔转换

在所有 Python 3.11 及之前的版本中,如果 Element 实例 e 没有子元素,e 的布尔值为假,这遵循了 Python 容器隐式布尔转换的常规规则。然而,文档记录表明,这种行为可能会在未来的某个版本中发生变化。为了未来的兼容性,如果你想检查 e 是否没有子元素,请显式地检查 if len(e) == 0: 而不是使用通常的 Python 习惯用法 if not e:。

e 的命名方法处理子元素或后代的详细信息列在 表 23-3 中(本书不涵盖 XPath:有关该主题的信息,请参阅 在线文档)。许多以下方法接受一个可选参数 namespaces,默认为 None。当存在时,namespaces 是一个映射,XML 命名空间前缀作为键,相应的 XML 命名空间全名作为值。

表 23-3. Element 实例 e 处理子元素或后代的方法

append e.append(se) 在 e 的子元素末尾添加子元素 sese 必须是一个 Element)。
extend e.extend(ses) 将可迭代对象 ses 中的每个元素(每个元素必须是一个 Element)添加到 e 的子元素末尾。
find e.find(match, namespaces=None) 返回第一个匹配 match 的后代元素,match 可以是标签名或 XPath 表达式(在当前 ElementTree 实现支持的子集内)。如果没有后代元素匹配 match,则返回 None
findall e.findall(match, namespaces=None) 返回匹配 match 的所有后代元素列表,match 可以是标签名或 XPath 表达式(在当前 ElementTree 实现支持的子集内)。如果没有后代元素匹配 match,则返回 []。
findtext e.findtext(match, default=None, namespaces=None) 返回匹配 match 的第一个后代的文本,match 可以是标签名或当前 ElementTree 实现支持的 XPath 表达式的子集。如果匹配的第一个后代没有文本,则结果可能是空字符串 ''。如果没有后代匹配 match,则返回 default。
insert e.insert(index, se) 在 e 的子元素序列中的索引 index 处添加子元素 sese 必须是 Element 类型)。
iter e.iter(tag='') 返回一个迭代器,按深度优先顺序遍历所有 e 的后代。当 tag 不为 '' 时,仅产生标签等于 tag 的子元素。在循环 e.iter 时,请不要修改以 e 为根的子树。
iterfind e.iterfind(match, namespaces=None) 返回一个迭代器,按深度优先顺序遍历所有匹配 match 的后代,match 可以是标签名或当前 ElementTree 实现支持的 XPath 表达式的子集。当没有后代匹配 match 时,结果迭代器为空。
itertext e.itertext(match, namespaces=None) 返回一个迭代器,按深度优先顺序遍历所有匹配 match 的后代的文本(不包括尾部),match 可以是标签名或当前 ElementTree 实现支持的 XPath 表达式的子集。当没有后代匹配 match 时,结果迭代器为空。
remove e.remove(se) 删除元素 se(如在 表 3-4 中所述)。

ElementTree 类

ElementTree 类表示映射 XML 文档的树。ElementTree 实例 et 的核心附加值是具有用于整体解析(输入)和写入(输出)整个树的方法。这些方法在 表 23-4 中描述。

表 23-4. ElementTree 实例解析和写入方法

parse et.parse(source, parser=None) source 可以是打开以供读取的文件,或要打开并读取的文件名(要解析字符串,请将其包装在 io.StringIO 中,如 “内存文件:io.StringIO 和 io.BytesIO” 中所述),其中包含 XML 文本。et.parse 解析该文本,构建其元素树作为 et 的新内容(丢弃 et 的先前内容(如果有)),并返回树的根元素。parser 是一个可选的解析器实例;默认情况下,et.parse 使用由 ElementTree 模块提供的 XMLParser 类的实例(本书不涵盖 XMLParser;请参阅 在线文档)。
write et.write(file, encoding='us-ascii', xml_declaration=None, default_namespace=None, method='xml', short_empty_elements=True) file 可以是已打开并用于写入的文件,或要打开并写入的文件名称(要写入字符串,请将 file 作为 io.StringIO 的实例传递,详见 “内存文件:io.StringIO 和 io.BytesIO”)。et.write 将文本写入该文件,表示树的 XML 文档内容,该树是 et 的内容。

| write (续) | encoding 应该按照 标准 拼写,而不是使用常见的“昵称” — 例如,'iso-8859-1',而不是 'latin-1',尽管 Python 本身接受这两种编码拼写方式,并且类似地,'utf-8' 带有破折号,而不是 'utf8' 没有破折号。通常最好选择将 encoding 传递为 'unicode'。当 file.write 接受这样的字符串时,这会输出文本(Unicode)字符串;否则,file.write 必须接受字节串,而 et.write 输出的字符串类型将是这种类型,对于不在编码中的字符,将使用 XML 字符引用输出 — 例如,默认的 US-ASCII 编码,“带重音符的 e”,é,将输出为 é。您可以将 xml_declaration 传递为 False 以避免在生成的文本中包含声明,或者传递为 True 以包含声明;默认情况下,仅在编码不是 'us-ascii'、'utf-8' 或 'unicode' 之一时才包含声明。

您可以选择性地传递 default_namespace 来设置 xmlns 结构的默认命名空间。

您可以将 method 传递为 'text' 以仅输出每个节点的文本和尾部(无标记)。您可以将 method 传递为 'html' 以 HTML 格式输出文档(例如,在 HTML 中不需要的结束标记,如
将被省略)。默认为 'xml',以 XML 格式输出。

您可以通过名称(而不是位置)选择性地将 short_empty_elements 传递为 False,以始终使用显式的开始和结束标记,即使对于没有文本或子元素的元素也是如此;默认情况下,对于这种空元素使用 XML 简短形式。例如,默认情况下,具有标签 a 的空元素将输出为 ,如果将 short_empty_elements 传递为 False,则将输出为 。 |

此外,ElementTree 的一个实例 et 提供了方法 getroot(返回树的根)和便利方法 find、findall、findtext、iter 和 iterfind,每个方法与在树的根上调用相同的方法完全等效,也就是说,在 et.getroot 的结果上调用。

ElementTree 模块中的函数

ElementTree 模块还提供了几个函数,详见 表 23-5。

表 23-5. ElementTree 函数

Comment Comment(text=None) 返回一个元素,在插入 ElementTree 作为节点后,将作为 XML 注释输出,注释文本字符串被封闭在''之间。XMLParser 跳过任何文档中的 XML 注释,因此这个函数是插入注释节点的唯一方法。
dump dump(e) 将 e(可以是 Element 或 ElementTree)以 XML 形式写入 sys.stdout。此函数仅用于调试目的。
fromstring fromstring(text, parser=None) 从 text 字符串解析 XML 并返回一个 Element,就像刚刚介绍的 XML 函数一样。
fromstringlist fromstringlist(sequence, parser=None) 就像 fromstring(''.join(sequence)),但通过避免连接,可能会更快一些。
iselement iselement(e) 如果 e 是一个 Element,则返回 True;否则返回 False

| iterparse | iterparse(source, events=['end'], parser=None) 解析 XML 文档并逐步构建相应的 ElementTree。source 可以是打开进行读取的文件,或要打开并读取的文件名,包含 XML 文档作为文本。iterparse 返回一个迭代器,产生两项元组 (event, element),其中 event 是参数 events 中列出的字符串之一(每个字符串必须是 'start'、'end'、'start-ns' 或 'end-ns'),随着解析的进行而变化。element 是 'start' 和 'end' 事件的 Element,'end-ns' 事件的 None,以及 'start-ns' 事件的两个字符串元组(namespace_prefix, namespace_uri)。parser 是一个可选的解析器实例;默认情况下,iterparse 使用 ElementTree 模块提供的 XMLParser 类的实例(有关 XMLParser 类的详细信息,请参阅在线文档)。

iterparse 的目的是在可行的情况下,允许你逐步解析一个大型 XML 文档,而不必一次性将所有生成的 ElementTree 存储在内存中。我们在“逐步解析 XML”中详细讨论了 iterparse。

parse parse(source, parser=None) 就像 ElementTree 的 parse 方法,在表 23-4 中介绍的一样,但它返回它创建的 ElementTree 实例。
P⁠r⁠o⁠c⁠e⁠s⁠s⁠i⁠n⁠g​I⁠n⁠s⁠t⁠r⁠u⁠c⁠t⁠i⁠o⁠n ProcessingInstruction(target, text=None) 返回一个元素,在插入 ElementTree 作为节点后,将作为 XML 处理指令输出,目标和文本字符串被封闭在''之间。XMLParser 跳过任何文档中的 XML 处理指令,因此这个函数是插入处理指令节点的唯一方法。
r⁠e⁠g⁠i⁠s⁠t⁠e⁠r⁠_​n⁠a⁠m⁠e⁠s⁠p⁠a⁠c⁠e register_namespace(prefix, uri) 将字符串 prefix 注册为字符串 uri 的命名空间前缀;命名空间中的元素将使用此前缀进行序列化。
SubElement SubElement(parent, tag, attrib=, **extra) 创建一个带有给定tag和来自字典 attrib 的属性以及作为额外命名参数传递的其他内容的 Element,并将其作为 Element parent的最右边子节点添加;返回它创建的 Element。
tostring tostring(e, encoding='us-ascii', method='xml', short_empty_elements=True) 返回一个字符串,其中包含以 Element e为根的子树的 XML 表示。参数的含义与 ElementTree 的 write 方法相同,见表 23-4。
tostringlist tostringlist(e, encoding='us-ascii', method='xml', short_empty_elements=True) 返回一个字符串列表,其中包含以 Element e为根的子树的 XML 表示。参数的含义与 ElementTree 的 write 方法相同,见表 23-4。
XML XML(text, parser=None) 从文本字符串text解析 XML 并返回一个 Element。parser 是可选的解析器实例;默认情况下,XML 使用由 ElementTree 模块提供的 XMLParser 类的实例(本书不涵盖 XMLParser 类;详见在线文档)。
XMLID XMLID(text, parser=None) 从文本字符串text解析 XML 并返回一个包含两个条目的元组:一个 Element 和一个将 id 属性映射到每个唯一 Element 的字典(XML 禁止重复 id)。parser 是可选的解析器实例;默认情况下,XMLID 使用由 ElementTree 模块提供的 XMLParser 类的实例(本书不涵盖 XMLParser 类;详见在线文档)。

ElementTree 模块还提供了 QName、TreeBuilder 和 XMLParser 类,这些我们在本书中不涵盖,以及 XMLPullParser 类,见“迭代解析 XML”。

使用 ElementTree.parse 解析 XML

在日常使用中,创建 ElementTree 实例最常见的方法是从文件或类似文件的对象中解析它,通常使用模块函数 parse 或 ElementTree 类实例的方法 parse。

在本章剩余的示例中,我们使用在http://www.w3schools.com/xml/simple.xml找到的简单 XML 文件;它的根标记是'breakfast_menu',根的子节点是标记为'food'的元素。每个'food'元素都有一个标记为'name'的子元素,其文本是食物的名称,以及一个标记为'calories'的子元素,其文本是该食物一份中的卡路里数的整数表示。换句话说,对于示例感兴趣的 XML 文件内容的简化表示如下:

`<breakfast_menu``>`
 `<food``>`
    `<name``>`Belgian Waffles`</name>`
    `<calories``>`650`</calories>`
 `</food>`
 `<food``>`
    `<name``>`Strawberry Belgian Waffles`</name>`
    `<calories``>`900`</calories>`
 `</food>`
 `<food``>`
    `<name``>`Berry-Berry Belgian Waffles`</name>`
    `<calories``>`900`</calories>`
 `</food>`
 `<food``>`
    `<name``>`French Toast`</name>`
    `<calories``>`600`</calories>`
 `</food>`
 `<food``>`
    `<name``>`Homestyle Breakfast`</name>`
    `<calories``>`950`</calories>`
 `</food>`
`</breakfast_menu>`

因为 XML 文档位于 WWW URL 上,所以首先获取一个具有该内容的类似文件的对象,并将其传递给 parse;最简单的方法使用 urllib.request 模块:

`from` `urllib` `import` request
`from` `xml``.``etree` `import` ElementTree `as` et
content = request.urlopen('http://www.w3schools.com/xml/simple.xml')
tree = et.parse(content)

从 ElementTree 中选择元素

假设我们想要在标准输出上打印出各种食物的卡路里和名称,按升序卡路里排序,按字母顺序打破平局。以下是此任务的代码:

`def` bycal_and_name(e):
    `return` int(e.find('calories').text), e.find('name').text

`for` `e` `in` sorted(tree.findall('food'), key=bycal_and_name):
    print(f"{e.find('calories').text} {e.find('name').text}")

当运行时,这将打印:

600 French Toast
650 Belgian Waffles
900 Berry-Berry Belgian Waffles
900 Strawberry Belgian Waffles
950 Homestyle Breakfast

编辑 ElementTree

构建好一个 ElementTree(无论是通过解析还是其他方式),你可以通过 ElementTree 和 Element 类的各种方法以及模块函数来“编辑”它——插入、删除和/或修改节点(元素)。例如,假设我们的程序可靠地通知我们菜单上添加了一种新食物——涂了黄油的烤面包,两片白面包烤过涂了黄油,含有“berry”字样的食物已被删除(不区分大小写)。针对这些规格的“编辑树”部分可以编码如下:

*`# add Buttered Toast to the menu`*
menu = tree.getroot()
toast = et.SubElement(menu, 'food')
tcals = et.SubElement(toast, 'calories')
tcals.text = '180'
tname = et.SubElement(toast, 'name')
tname.text = 'Buttered Toast'
*`# remove anything related to 'berry' from the menu`*
`for` `e` `in` menu.findall('food'):
    name = e.find('name').text
    `if` 'berry' `in` name.lower():
        menu.remove(e)

一旦我们在解析树的代码和从中选择性打印的代码之间插入这些“编辑”步骤,后者将打印:

180 Buttered Toast
600 French Toast
650 Belgian Waffles
950 Homestyle Breakfast

有时,编辑 ElementTree 的便捷性可能是一个关键考虑因素,值得你将其全部保留在内存中。

从头开始构建 ElementTree

有时,你的任务并不是从现有 XML 文档开始:相反,你需要根据代码从不同来源(如 CSV 文件或某种类型的数据库)获得的数据制作一个 XML 文档。

对于这类任务的代码类似于我们展示的用于编辑现有 ElementTree 的代码——只需添加一个小片段来构建一个最初为空的树。

例如,假设你有一个 CSV 文件,menu.csv,其中两列逗号分隔的是各种食物的卡路里和名称,每行一种食物。你的任务是构建一个 XML 文件,menu.xml,与我们在之前示例中解析过的类似。下面是你可以这样做的一种方式:

import csv
`from` `xml``.``etree` `import` ElementTree `as` et

menu = et.Element('menu')
tree = et.ElementTree(menu)
`with` open('menu.csv') `as` f:
    r = csv.reader(f)
    `for` calories, namestr `in` r:
        food = et.SubElement(menu, 'food')
        cals = et.SubElement(food, 'calories')
        cals.text = calories
        name = et.SubElement(food, 'name')
        name.text = namestr

tree.write('menu.xml')

逐步解析 XML

针对从现有 XML 文档中选择元素的任务,有时你不需要将整个 ElementTree 构建在内存中——这一点特别重要,如果 XML 文档非常大时(对于我们处理的微小示例文档不适用,但可以想象类似的以菜单为中心的文档,列出了数百万种不同的食物)。

假设我们有这样一个大型文档,并且我们想要在标准输出上打印出卡路里最低的 10 种食物的卡路里和名称,按升序卡路里排序,按字母顺序打破平局。我们的 menu.xml 文件现在是一个本地文件,假设它列出了数百万种食物,因此我们宁愿不将其全部保存在内存中(显然,我们不需要一次性完全访问所有内容)。

以下代码代表了一种无需在内存中构建整个结构的简单尝试来解析:

import heapq
`from` `xml``.``etree` `import` ElementTree `as` et

def cals_and_name():
 *`# generator for (calories, name) pairs`*
 `for` _, elem `in` et.iterparse('menu.xml'):
        `if` elem.tag != 'food':
            `continue`
     *`# just finished parsing a food, get calories and name`*
     cals = int(elem.find('calories').text)
    name = elem.find('name').text
        `yield` (cals, name)

lowest10 = heapq.nsmallest(10, cals_and_name())

`for` cals, name `in` lowest10:
    print(cals, name)

这种方法确实有效,但不幸的是,它消耗的内存几乎与基于完整 et.parse 的方法相同!这是因为 iterparse 在内存中逐步构建了整个 ElementTree,尽管它仅仅回传事件,如(默认情况下仅)'end',意味着“我刚刚完成了对这个元素的解析”。

要真正节省内存,我们至少可以在处理完元素后立即丢弃每个元素的所有内容——也就是说,在 yield 后,我们可以添加 elem.clear() 使刚处理过的元素为空。

这种方法确实可以节省一些内存,但并非全部,因为树的根仍然会有一个巨大的空子节点列表。要真正节省内存,我们需要获取'开始'事件,以便获取正在构建的 ElementTree 的根,并在使用每个元素后从中移除每个元素,而不仅仅是清除元素。也就是说,我们希望将生成器改为:

def cals_and_name():
 *`# memory-thrifty generator for (calories, name) pairs`*
    root = `None`
    `for` event, elem `in` et.iterparse('menu.xml', ['start', 'end']):
        `if` event == 'start':
            `if` root `is` `None`:
                root = elem
    `continue`
        `if` elem.tag != 'food':
            `continue`
        *`# just finished parsing a food, get calories and name`*
        cals = int(elem.find('calories').text)
        name = elem.find('name').text
        `yield` (cals, name)
        root.remove(elem)

这种方法尽可能地节省内存,同时完成任务!

在异步循环中解析 XML

虽然 iterparse 在正确使用时可以节省内存,但仍不足以在异步循环中使用。这是因为 iterparse 对传递给它的文件对象进行阻塞读取调用:在异步处理中这种阻塞调用是不可取的。

ElementTree 提供了 XMLPullParser 类来解决这个问题;请参阅在线文档了解该类的使用模式。

¹ Alex 太谦虚了,不过从 1995 年到 2005 年,他和 Fredrik 以及 Tim Peters 一起,都是 Python 的权威。他们以其对语言的百科全书式和详细的了解而闻名,effbot、martellibot 和 timbot 创建的软件和文档对数百万人至关重要。

第二十四章:打包程序和扩展

本章内容有所删减以适应印刷出版,我们在这本书的 GitHub 存储库中的在线版本提供了更多材料。在线版本中我们还描述了诗歌(poetry),这是一个符合现代标准的 Python 构建系统,并将其与传统的 setuptools 方法进行了比较,此外还涵盖了其他主题(完整列表请参见“在线材料”)。

假设您有一些 Python 代码需要交付给其他人和团体。它在您的机器上运行正常,但现在您需要使其在其他人那里也能正常运行。这涉及将您的代码打包成适合的格式并提供给预期的受众。

自上一版以来,Python 打包生态系统的质量和多样性有了很大改善,其文档组织更为完善、内容更加完整。这些改进基于对 Python 源树格式的仔细规定,独立于任何特定构建系统的规定,详见PEP 517,“独立于构建系统的 Python 源树格式”,以及PEP 518,“为 Python 项目指定最低构建系统要求”,后者的“原理”部分简要描述了为何需要进行这些改变,其中最重要的是不再需要运行 setup.py 文件来发现(据推测是通过观察回溯信息)构建的要求。

PEP 517 的主要目的是指定一个名为 pyproject.toml 的文件中的构建定义格式。该文件被组织成称为“表”的部分,每个表的标题包含在方括号中的表名,类似于配置文件。每个表包含各种参数的值,包括名称、等号和值。Python 3.11+ 包括了用于提取这些定义的tomllib模块,具有类似于 json 模块中的 load 和 loads 方法。¹

虽然 Python 生态系统中越来越多的工具使用这些现代标准,但您仍应该预期会继续遇到更传统的基于 setuptools 的构建系统(它本身正在过渡到 PEP 517 推荐的pyproject.toml基础)。关于可用的打包工具的优秀概述,请参阅由 Python Packaging Authority (PyPA) 维护的列表

为了解释打包,我们首先描述其发展,然后讨论 poetry 和 setuptools。其他符合 PEP 517 标准的构建工具包括 flithatch,随着互操作性的不断改善,你应该期待它们的数量将继续增长。对于分发相对简单的纯 Python 包,我们还介绍了标准库模块 zipapp,并在本章中通过一个简短的部分解释如何访问作为包一部分捆绑的数据。

本章未涵盖的内容

除了 PyPA 认可的方法外,还有许多其他可能的 Python 代码分发方式,远远超出了单章节的覆盖范围。我们不涵盖以下打包和分发主题,这些主题可能会对希望分发 Python 代码的人感兴趣:

  • 使用 conda

  • 使用 Docker

  • 从 Python 代码创建二进制可执行文件的各种方法,例如以下工具(这些工具对于复杂项目的设置可能有些棘手,但通过扩大应用程序的潜在受众来回报努力):

    • PyInstaller,它接受一个 Python 应用程序并将所有必需的依赖(包括 Python 解释器和必要的扩展库)打包成一个单独的可执行程序,可以作为独立应用程序分发。每个体系结构都有适用于 Windows、macOS 和 Linux 的版本,但每个体系结构只能生成自己的可执行文件。

    • PyOxidizer,这是同名实用工具集中的主要工具,不仅允许创建独立的可执行文件,还可以创建 Windows 和 macOS 安装程序及其他工件。

    • cx_Freeze,它创建一个包含 Python 解释器、扩展库和 Python 代码 ZIP 文件的文件夹。你可以将其转换为 Windows 安装程序或 macOS 磁盘映像。

Python 打包的简要历史

在虚拟环境出现之前,维护多个 Python 项目并避免它们不同依赖需求的冲突是一项复杂的任务,需要仔细管理 sys.pathPYTHONPATH 环境变量。如果不同项目需要同一个依赖的不同版本,没有一个单独的 Python 环境可以同时支持它们。如今,每个虚拟环境(参见“Python 环境”以深入了解此主题)都有自己的 site_packages 目录,可以通过多种便捷的方式安装第三方和本地的包和模块,大大减少了对机制的需求,使得这些问题基本上不再需要考虑。²

当 Python 包索引于 2003 年构思时,没有这样的功能可用,也没有统一的方法来打包和分发 Python 代码。开发人员必须仔细地为他们所工作的每个不同项目调整他们的环境。随着 distutils 标准库包的开发,情况发生了变化,很快被第三方的 setuptools 包及其 easy_install 实用工具所利用。现在已过时的跨平台 egg 打包格式是 Python 包分发的第一个单文件格式的定义,允许从网络源轻松下载和安装 eggs。安装一个包使用了 setup.py 组件,其执行将使用 setuptools 的特性将包的代码集成到现有的 Python 环境中。要求使用第三方(即不是标准发行版的一部分)模块,如 setuptools,显然不是一个完全令人满意的解决方案。

与这些发展同时进行的是 virtualenv 包的创建,它通过为不同项目使用的 Python 环境提供清晰的分离大大简化了普通 Python 程序员的项目管理。在此之后不久,基于 setuptools 背后的思想,pip 实用程序被引入。使用源树而不是 eggs 作为其分发格式,pip 不仅可以安装软件包,还可以卸载它们。它还可以列出虚拟环境的内容,并接受项目依赖项的带版本的列表,按照约定存储在名为 requirements.txt 的文件中。

setuptools 的开发有些古怪,对社区需求反应不够灵活,因此创建了一个名为 distribute 的分支作为一个可直接替换的解决方案(它安装在 setuptools 名下),以便允许更具合作性的开发工作进行。这最终被合并回了 setuptools 代码库中,现在由 PyPA 控制:能够做到这一点肯定了 Python 的开源许可政策的价值。

-3.11 distutils 包最初设计为标准库组件,帮助安装扩展模块(特别是那些用编译语言编写的模块,在 第二十五章 中有介绍)。尽管它目前仍然存在于标准库中,但它已被弃用,并计划在版本 3.12 中删除,届时可能会并入 setuptools。出现了许多其他工具,符合 PEP 517 和 518。在本章中,我们将介绍不同的方法来将额外的功能安装到 Python 环境中。

随着PEP 425,“内置分发的兼容性标签”,以及PEP 427,“Wheel 二进制包格式”,Python 终于有了一个二进制分发格式的规范(wheel,其定义已经更新),允许在不同架构下分发编译的扩展包,当没有合适的二进制 wheel 可用时则回退到源码安装。

PEP 453,“Python 安装中 pip 的显式引导”,决定 pip 实用程序应成为 Python 中首选的安装包的方式,并建立了一个独立于 Python 的更新过程,以便可以不必等待新的语言发布而提供新的部署功能。

这些发展以及许多其他使 Python 生态系统合理化的努力都归功于 PyPA,Python 的领导“Steering Council”已将与打包和分发相关的大多数事项委托给他们。要深入了解本章节的更高级内容,请参阅“Python 打包用户指南”,该指南为希望广泛发布其 Python 软件的任何人提供了明智的建议和有用的指导。

在线资料

正如本章节开头所提到的,本章节的在线版本包含额外的材料。讨论的主题包括:

  • 构建过程

  • 入口点

  • 发布格式

  • poetry

  • setuptools

  • 分发您的包

  • zipapp

  • 访问与您的代码一起提供的数据

¹ 旧版本的用户可以使用pip install toml从 PyPI 安装该库。

² 请注意,某些软件包对虚拟环境不太友好。幸运的是,这种情况很少见。

第二十五章:经典 Python 的扩展和嵌入

本章内容已经为本书的印刷版本进行了缩编。完整内容可在在线版中找到,详见“在线资料”。

CPython 运行在一个可移植的、用 C 编写的虚拟机上。Python 的内置对象,比如数字、序列、字典、集合和文件,都是用 C 编写的,Python 标准库中也有几个模块是如此。现代平台支持动态加载库,文件扩展名为 .dll(Windows)、.so(Linux)和 .dylib(Mac),构建 Python 时会生成这些二进制文件。你可以用 C(或任何可以生成 C 可调用库的语言)编写自己的 Python 扩展模块,使用本章节介绍的 Python C API。有了这个 API,你可以生成和部署动态库,供 Python 脚本和交互会话后续使用,使用 import 语句导入,详见“导入语句”。

扩展 Python 意味着构建模块,供 Python 代码import以访问模块提供的功能。嵌入 Python 意味着在另一种语言编写的应用程序中执行 Python 代码。为了使这种执行有用,Python 代码反过来必须能够访问一些你的应用程序的功能。因此,实际上,嵌入暗示了一些扩展,以及一些特定于嵌入的操作。希望扩展 Python 的三个主要原因可以总结如下:

  • 在较低级语言中重新实现一些功能(最初用 Python 编写),希望能获得更好的性能。

  • 让 Python 代码访问由低级语言编写(或至少可从中调用)的库提供的一些现有功能

  • 让 Python 代码访问一个正在将 Python 作为应用程序脚本语言嵌入到应用程序中的应用程序的一些现有功能

Python 的在线文档涵盖了嵌入和扩展;在那里,你可以找到深入的教程和广泛的参考手册。许多细节最好通过 Python 的广泛文档化的 C 源代码学习。下载 Python 的源代码分发包,并学习 Python 核心的源代码、C 编写的扩展模块以及为此目的提供的示例扩展。

在线资料

本章假设读者具备一些 C 的知识

虽然我们包括一些非 C 扩展选项,但要使用 C API 扩展或嵌入 Python,你必须了解 C 和/或 C++ 编程语言。我们在本书中不涵盖 C 和 C++,但有许多印刷和在线资源可供学习。本章的在线内容大多假设你至少有一些 C 的知识。

本章的在线版本中,你会找到以下章节:

“用 Python 的 C API 扩展 Python”

包括参考表格和示例,用于创建 C 代码 Python 扩展模块,可以导入到你的 Python 程序中,展示如何编码和构建这些模块。本节包含两个完整示例:

  • 一个实现自定义方法以操作字典的扩展

  • 一个定义自定义类型的扩展

“不使用 Python 的 C API 扩展 Python”

讨论(或至少提到和链接到)几个工具和库,支持创建 Python 扩展,而无需直接使用 C 或 C++ 编程,¹ 包括第三方工具 F2PY, SIP, CLIF, cppyy, pybind11, Cython, CFFIHPy,以及标准库模块 ctypes。本节包含一个使用 Cython 创建扩展的完整示例。

“嵌入 Python”

包括参考表格和嵌入 Python 解释器到更大应用中的概念概述,使用 Python 的 C API 进行嵌入。

¹ 还有许多其他类似工具,但我们试图仅挑选最流行和有前景的工具。

第二十六章:从 v3.7 到 v3.n 的迁移

这本书跨越了几个版本的 Python,并涵盖了一些重要的(仍在发展中的!)新功能,包括:

  • 保持顺序的字典

  • 类型注解

  • := 赋值表达式(俗称“海象操作符”)

  • 结构化模式匹配

个别开发者可能会在每个新的 Python 版本发布后进行安装,并在解决兼容性问题时逐步升级。但对于在企业环境中工作或维护共享库的 Python 开发者来说,从一个版本迁移到下一个版本需要深思熟虑和计划。

本章讨论了 Python 语言的变化,从 Python 程序员的视角来看。(Python 内部也有许多变化,包括 Python C API 的变化,但这些超出了本章的范围:详情请参见每个发布版本的在线文档中的“Python 3.n 新特性”部分。)

Python 3.11 中的重大更改

大多数版本都有几个显著的新功能和改进,这些特性和改进可以作为选择特定版本的高层次原因。表 26-1 详细介绍了版本 3.6–3.11 的主要新功能和破坏性更改,这些更改可能影响许多 Python 程序;更完整的列表请参见附录。

表 26-1. 近期 Python 发布的重大更改

版本 新功能 破坏性更改
3.6
  • 字典保留顺序(作为 CPython 的实现细节)

  • 添加了 F-字符串

  • 支持数字文字中的下划线

  • 注解可以用于类型,可以通过外部工具如 mypy 进行检查

  • asyncio 不再是一个临时模块

    初始发布时间:2016 年 12 月

    支持结束时间:2021 年 12 月

|

  • 不再支持在大多数 re 函数的模式参数中使用未知转义的 \ 和 ASCII 字母(仅在 re.sub() 中仍然允许)

|

3.7
  • 字典保留顺序(作为正式语言的保证)

  • 添加了 dataclasses 模块

  • 添加了 breakpoint() 函数

    初始发布时间:2018 年 6 月

    计划支持结束时间:2023 年 6 月

|

  • 不再支持在 re.sub() 的模式参数中使用未知转义的 \ 和 ASCII 字母

  • 不再支持在 bool()、float()、list() 和 tuple() 中使用命名参数

  • 不再支持在 int() 中使用前置命名参数

|

3.8
  • 添加了赋值表达式(:=,也称为海象操作符)

  • 在函数参数列表中使用 / 和 * 表示位置参数和命名参数

  • 在 f-字符串中使用尾部 = 进行调试(f'{x=}' 的简写形式为 f'x={x!r}')

  • 添加了类型类(Literal, TypedDict, Final, Protocol)

    初始发布时间:2019 年 10 月

    计划支持结束时间:2024 年 10 月

|

  • 移除了 time.clock();使用 time.perf_counter()

  • 移除了 pyvenv 脚本;使用 python -m venv 替代

  • yieldyield from 不再允许在推导式或生成器表达式中使用

  • 添加了对isis not对 str 和 int 字面值的语法警告

|

3.9
  • 字典上支持联合运算符|和|=

  • 添加了 str.removeprefix()和 str.removesuffix()方法

  • 添加了 zoneinfo 模块以支持 IANA 时区(替换第三方 pytz 模块)

  • 类型提示现在可以在泛型中使用内置类型(例如 list[int]而不是 List[int])

    初始发布时间:2020 年 10 月

    预计支持结束时间:2025 年 10 月

|

  • 移除了 array.array.tostring()和 fromstring()方法

  • 移除了 threading.Thread.isAlive()(请使用 is_alive()代替)

  • 移除了 ElementTree 和 Element 的 getchildren()和 getiterator()方法

  • 移除了 base64.encodestring()和 decodestring()(请改用 encodebytes()和 decodebytes())

  • 移除了 fractions.gcd()(请使用 math.gcd()代替)

  • 移除了 typing.NamedTuple._fields(请改用 annotations 代替)

|

3.10
  • 支持match/case结构模式匹配

  • 允许将联合类型写为 X | Y(在类型注解中和作为 isinstance()的第二个参数)

  • 添加了 zip()内置函数的可选 strict 参数,以检测长度不同的序列

  • 官方支持了带括号的上下文管理器;例如,with(ctxmgr, ctxmgr, ...)

    初始发布时间:2021 年 10 月

    预计支持结束时间:2026 年 10 月

|

  • 从 collections 中移除了 ABCs 导入(现在必须从 collections.abc 导入)

  • 大多数 asyncio 高级 API 中的循环参数已被移除

|

3.11
  • 改进了错误消息

  • 总体性能提升

  • 添加了异常组和 except*表达式

  • 添加了类型提示类(Never,Self)

  • 将 tomllib TOML 解析器添加到标准库

    初始发布时间:2022 年 10 月

    预计支持结束时间:2027 年 10 月(估计)

|

  • 移除了 binhex 模块

  • 将 int 转换为 str 的限制扩展到 4300 位数字

|

规划 Python 版本升级

首先为什么要升级?如果您有一个稳定运行的应用程序和一个稳定的部署环境,那么一个合理的决定可能是不做任何更改。但是版本升级确实带来了好处:

  • 新版本通常引入新功能,这可能使您能够简化代码。

  • 更新的版本包括错误修复和重构,可以提高系统的稳定性和性能。

  • 旧版本中发现的安全漏洞可能在新版本中得到修复。²

最终,旧版 Python 版本将不再受支持,运行在旧版本上的项目将变得难以维护且成本更高。因此,升级可能成为必要。

选择目标版本

在决定迁移哪个版本之前,有时你必须首先弄清楚,“我现在正在运行哪个版本?”你可能会不愉快地发现,你公司系统中存在运行不受支持的 Python 版本的旧软件。通常情况是,这些系统依赖于某些第三方包,而这些包本身版本落后或没有可用的升级版本。当这种系统在公司运营中扮演重要角色时,情况会更为严峻。你可以通过远程访问 API 隔离落后的包,允许该包在旧版本上运行,同时让你自己的代码安全升级。必须向高级管理层展示存在升级约束的系统,以便他们了解保留、升级、隔离或替换的风险和权衡。

目标版本的选择通常默认为“最新版本”。这是一个合理的选择,因为在进行升级时,它通常是成本效益最高的选项:最新发布的版本将具有最长的支持期。更保守的立场可能是“最新版本减一”。你可以相当确信,版本 N–1 在其他公司进行了一段生产测试期间,并且其他人已经解决了大部分问题。

确定工作范围

在选择了 Python 的目标版本之后,识别从当前软件使用的版本到目标版本(包括目标版本)之间的所有突破性变化(请参阅附录中的详细功能和版本变化表;更多详细信息可以在在线文档的“Python 3.n 新特性”部分找到)。通常会有适用于当前版本和目标版本的兼容形式文档化的突破性变化。记录并传达开发团队在升级之前需要进行的源代码更改。(如果您的代码受到大量突破性变化或与相关软件的兼容性问题的影响,直接升级到所选目标版本可能涉及比预期更多的工作。您甚至可能需要重新考虑目标版本的选择,或者考虑采取较小的步骤。也许您会决定首先升级到 目标–1,然后推迟升级到 目标目标+1 的任务,作为后续升级项目。)

确定您的代码库使用的任何第三方或开源库,并确保它们与目标 Python 版本兼容(或计划与之兼容)。即使您自己的代码库已准备好升级到目标版本,落后的外部库可能会阻碍您的升级项目。必要时,您可以将这样的库隔离在单独的运行时环境中(使用虚拟机或容器技术),如果该库提供远程访问编程接口的话。

在开发环境中提供目标 Python 版本,并可选择在部署环境中提供,以便开发人员确认其升级更改是完整和正确的。

应用代码更改

一旦确定了目标版本并识别了所有破坏性更改,您将需要在代码库中进行更改,使其与目标版本兼容。理想情况下,您的目标是使代码以与当前版本和目标 Python 版本均兼容的形式存在。

导入自 future

future 是一个标准库模块,包含各种功能,文档在 在线文档 中,用于简化版本之间的迁移。它不同于任何其他模块,因为导入功能可能影响您程序的语法,而不仅仅是语义。这些导入必须是代码的初始可执行语句。

每个“未来特性”都使用以下语句激活:

`from` __future__ `import` *`feature`*

其中 feature 是您想要使用的功能名称。

在本书涵盖的版本范围内,您可能考虑使用的唯一未来特性是:

`from` __future__ `import` annotations

允许引用尚未定义的类型而无需将其括在引号中(如 第五章 所述)。如果您当前的版本是 Python 3.7 或更高版本,则添加此 future 导入将允许在类型注释中使用未引用的类型,因此您以后无需重新执行它们。

首先检查在多个项目中共享的库。从这些库中移除阻碍性更改将是一个关键的第一步,因为在完成此步骤之前,您将无法在目标版本上部署任何依赖的应用程序。一旦库与两个版本兼容,它就可以用于迁移项目中。未来,库代码必须保持与当前 Python 版本和目标版本的兼容性:共享库可能是最后一个能够利用目标版本新功能的项目。

独立应用程序将有更早的机会使用目标版本中的新功能。一旦应用程序移除了所有受到破坏性更改影响的代码,请将其提交到您的源代码控制系统中作为跨版本兼容的快照。之后,您可以向应用程序代码添加新功能,并将其部署到支持目标版本的环境中。

如果版本兼容性变化影响到类型注释,你可以使用*.pyi*存根文件来隔离与版本相关的类型信息与源代码。

使用 pyupgrade 进行升级自动化

你可以使用自动化工具(如pyupgrade 包)来自动化大部分代码升级过程中的枯燥工作。pyupgrade 分析 Python 的 ast.parse 函数返回的抽象语法树(AST),以定位问题并对源代码进行修正。你可以通过命令行开关选择特定的目标 Python 版本。

每当你使用自动代码转换时,请审核转换过程的输出。像 Python 这样的动态语言使得完美的转换是不可能的;尽管测试有所帮助,但无法捕捉所有的不完美之处。

多版本测试

确保你的测试尽可能覆盖项目的大部分内容,以便在测试过程中可能会发现版本间错误。目标至少达到 80%的测试覆盖率;超过 90%可能难以达到,因此不要花费过多精力试图达到过高的标准。(模拟, 在“单元测试和系统测试”中提到,可以帮助你增加单元测试覆盖的广度,尽管深度不变。)

tox 包对于帮助你管理和测试多版本代码非常有用。它允许你在多个不同的虚拟环境下测试你的代码,并支持多个 CPython 版本以及 PyPy。

使用受控部署流程

在部署环境中使目标 Python 版本可用,通过应用环境设置来指示应用程序是否应该使用当前或目标 Python 版本运行。持续跟踪并定期向管理团队报告完成百分比。

你应该多久进行一次升级?

PSF 按年度发布 Python 的小版本,每个版本发布后享有五年的支持期。如果采用最新版本减一策略,它为你提供了一个稳定、经过验证的版本来迁移,拥有四年的支持时间窗口(以防未来需要推迟升级)。考虑到四年的时间窗口,每一到两年升级到最新版本减一应该能在升级成本和平台稳定性之间提供合理的平衡。

总结

维护组织系统所依赖的软件版本更新是一种持续的良好“软件卫生”习惯,无论是 Python 还是其他任何开发堆栈都是如此。通过每次只升级一到两个版本的常规升级,你可以将这项工作保持在稳定和可管理的水平上,并且它将成为你组织中公认和重视的活动。

¹ 尽管 Python 3.6 超出了本书涵盖范围的版本,但它引入了一些重要的新特性,我们在此提及它以供历史背景参考。

² 当这种情况发生时,通常是“全体出动”的紧急情况,必须赶快进行升级。正是通过实施稳定持续的 Python 版本升级计划,您才能避免或至少减少这些事件。

附录. Python 3.7 至 3.11 的新功能和更改

下表列出了 Python 版本 3.7 到 3.11 中语言和标准库的变更,这些变更最可能出现在 Python 代码中。使用这些表格来规划您的升级策略,受您代码库中断变更的限制。

以下类型的更改被视为“breaking”,并在最后一列标有 ! 符号:

  • 引入新关键字或内置函数(可能与现有 Python 源代码中使用的名称冲突)

  • 从标准库模块或内置类型中删除方法

  • 更改内置或标准库方法的签名,这种更改不向后兼容(例如删除参数或重命名命名参数)

新警告(包括 DeprecatedWarning)也显示为“breaking”,但在最后一列用 ***** 符号标记。

另请参阅标准库中拟议的弃用和移除表(“死电池”)在 PEP 594 中列出,该表列出了计划在哪些版本中删除的模块(从 Python 3.12 开始)及其推荐的替代方案。

Python 3.7

以下表格总结了 Python 版本 3.7 的更改。更多细节请参阅在线文档中的“Python 3.7 新特性”。

Python 3.7 Added Deprecated Removed Breaking change
函数接受 > 255 个参数 +
argparse.ArgumentParser.parse_intermixed_args() +
ast.literal_eval() 不再评估加法和减法 !
asyncawait 成为保留语言关键字 + !

| asyncio.all_tasks(), asyncio.create_task(),

asyncio.current_task(),

asyncio.get_running_loop(),

asyncio.Future.get_loop(),

asyncio.Handle.cancelled(),

asyncio.loop.sock_recv_into(),

asyncio.loop.sock_sendfile(),

asyncio.loop.start_tls(),

asyncio.ReadTransport.is_reading(),

asyncio.Server.is_serving(),

asyncio.Server.get_loop(),

asyncio.Task.get_loop(),

asyncio.run()(暂定) | + |   |   |   |

asyncio.Server 是异步上下文管理器 +

| asyncio.loop.call_soon(), asyncio.loop.call_soon_threadsafe(),

asyncio.loop.call_later(),

asyncio.loop.call_at(), and

asyncio.Future.add_done_callback() 都接受可选的命名 context 参数 | + |   |   |   |

| asyncio.loop.create_server(), asyncio.loop.create_unix_server(),

asyncio.Server.start_serving(),

并且 asyncio.Server.serve_forever() 都接受可选的命名 start_serving 参数 | + |   |   |   |

| asyncio.Task.current_task() 和 asyncio.Task.all_tasks() 已弃用;请使用 asyncio.current_task() 和

asyncio.all_tasks() |   | |   | ***** |

binascii.b2a_uu() 接受命名的 backtick 参数 +
bool() 构造函数不再接受命名参数(仅位置参数) !
breakpoint() built-in function + !
bytearray.isascii() +
bytes.isascii() +
collections.namedtuple supports default values +
concurrent.Futures.ProcessPoolExecutor and concurrent.Futures.ThreadPoolExecutor constructors accept optional initializer and initargs arguments +
  • contextlib.AbstractAsyncContextManager, contextlib.asynccontextmanager(),

  • contextlib.AsyncExitStack,

contextlib.nullcontext() | + |   |   |   |

contextvars module (similar to thread-local vars, with asyncio support) +
dataclasses module +
datetime.datetime.fromisoformat() +
DeprecationWarning shown by default in main module + *****
dict maintaining insertion order now guaranteed; dict.popitem() returns items in LIFO order +
dir() at module level +
dis.dis() method accepts named depth argument +
float() constructor no longer accepts a named argument (positional only) !
fpectl module removed X !
from future import annotations enables referencing as-yet-undefined types in type annotations without enclosing in quotes +
gc.freeze() +
getattr() at module level +
hmac.digest() +
http.client.HTTPConnection and http.client.HTTPSConnection constructors accept optional blocksize argument +
http.server.ThreadingHTTPServer +
  • importlib.abc.ResourceReader, importlib.resources module,

  • importlib.source_hash() | + |   |   |   |

int() constructor no longer accepts a named x argument (positional only; named base argument is still supported) !
io.TextIOWrapper.reconfigure() +
ipaddress.IPvNetwork.subnet_of(), ipaddress.IPvNetwork.supernet_of() +
list() constructor no longer accepts a named argument (positional only) !
logging.StreamHandler.setStream() +
math.remainder() +
multiprocessing.Process.close(), multiprocessing.Process.kill() +
ntpath.splitunc() removed; use ntpath.splitdrive() X !
os.preadv(), os.pwritev(), os.register_at_fork() +
os.stat_float_times() removed (compatibility function with Python 2; all timestamps in stat result are floats in Python 3) X !
pathlib.Path.is_mount() +
pdb.set_trace() accepts named header argument +
  • plist.Dict, plist.Plist, and

plist._InternalDict removed |   |   | X | ! |

queue.SimpleQueue +
re 编译表达式和匹配对象可以使用 copy.copy 和 copy.deepcopy 进行复制 +
re.sub() no longer supports unknown escapes of \ and an ASCII letter X !

| socket.close(), socket.getblocking(), socket.TCP_CONGESTION,

socket.TCP_USER_TIMEOUT,

socket.TCP_NOTSENT_LOWAT(仅限 Linux 平台)| + |   |   |   |

sqlite3.Connection.backup() +
生成器中的 StopIteration 处理 +
str.isascii() +
subprocess.run() 的命名参数 capture_output=True 简化了 stdin/stdout 捕获 +

| subprocess.run() 和 subprocess.Popen() 的命名参数 text,

通用换行符的别名 | + |   |   |   |

| subprocess.run(), subprocess.call(), 和 subprocess.Popen() 已改进

中断处理 | + |   |   |   |

| sys.breakpointhook(), sys.getandroidapilevel(),

sys.get_coroutine_origin_tracking_depth(),

sys.set_coroutine_origin_tracking_depth() | + |   |   |   |

| time.clock_gettime_ns(), time.clock_settime_ns(),

time.monotonic_ns(),

time.perf_counter_ns(),

time.process_time_ns(), time.time_ns(),

time.CLOCK_BOOTTIME, time.CLOCK_PROF,

time.CLOCK_UPTIME | + |   |   |   |

time.thread_time() 和 time.thread_time_ns() 用于线程级别的 CPU 计时 +
tkinter.ttk.Spinbox +
tuple() 构造函数不再接受命名参数(仅限位置参数) !

| types.ClassMethodDescriptorType, types.MethodDescriptorType, types.MethodWrapperType,

types.WrapperDescriptorType | + |   |   |   |

types.resolve_bases() +
uuid.UUID.is_safe +
yieldyield from 在推导式或生成器表达式中已废弃 *****
zipfile.ZipFile 构造函数接受了命名的 compresslevel 参数 +

Python 3.8

以下表格总结了 Python 3.8 版本中的变更。更多详情请参见在线文档中的“What’s New in Python 3.8”。

Python 3.8 添加 废弃 移除 破坏性变更
赋值表达式(:= “海象” 运算符) +
位置参数和命名参数(/ 和 * 参数分隔符) +
F-string trailing = for debugging +
对于 str 和 int 字面量的 isis not 测试会发出 SyntaxWarning *****
ast AST 节点的 end_lineno 和 end_col_offset 属性 +
ast.get_source_segment() +
ast.parse() 现在接受了命名参数 type_comments, mode, 和 feature_version +
async REPL 可通过 python -m asyncio 运行 +
asyncio 任务可以命名 +
asyncio.coroutine 装饰器已废弃 *****
asyncio.run() 可直接执行协程 +
asyncio.Task.get_coro() +
bool.as_integer_ratio() +
collections.namedtuple._asdict() returns dict instead of OrderedDict +
continue permitted in finally block +
cgi.parse_qs, cgi.parse_qsl, and cgi.escape removed; import from urllib.parse and html modules X !
csv.DictReader returns dicts instead of OrderedDicts +
datetime.date.fromisocalendar(), datetime.datetime.fromisocalendar() +
dict comprehensions compute key first, value second !
dict and dictviews returned from dict.keys(), dict.values() and dict.items() now iterable with reversed() +
fractions.Fraction.as_integer_ratio() +
functools.cached_property() decorator (see cautionary notes here and here) +
functools.lru_cache can be used as a decorator without () +
functools.singledispatchmethod decorator +
gettext.pgettext() +
importlib.metadata module +
int.as_integer_ratio() +
itertools.accumulate() accepts named initial argument +
macpath module removed X !
math.comb(), math.dist(), math.isqrt(), math.perm(), math.prod() +
math.hypot() added support for > 2 dimensions +
multiprocessing.shared_memory module +
namedtuple._asdict() returns dict instead of OrderedDict +
os.add_dll_directory() on Windows +
os.memfd_create() +
pathlib.Path.link_to() +
platform.popen() removed; use os.popen() X !
pprint.pp() +
pyvenv script removed; use python -m venv X !
re regular expression patterns support \N{name} escapes +
shlex.join() (inverse of shlex.split()) +
shutil.copytree() accepts named dirs_exist_ok argument +
slots accepts a dict of {name: docstring} +
socket.create_server(), socket.has_dualstack_ipv6() +
socket.if_nameindex(), socket.if_nametoindex(), and socket.if_indextoname() are all supported on Windows +
sqlite3 Cache and Statement objects no longer user-visible X !
ssl.post_handshake_auth(), ssl.verify_client_post_handshake() +

| statistics.fmean(), statistics.geometric_mean(),

statistics.multimode(),

statistics.NormalDist,

statistics.quantiles() | + |   |   |   |

sys.get_coroutine_wrapper() and sys.set_coroutine_wrapper() removed X !
sys.unraisablehook() +
tarfile.filemode() 已移除 X !

| threading.excepthook(),threading.get_native_id(),

threading.Thread.native_id | + |   |   |   |

time.clock() 已移除;请使用 time.perf_counter() X !

| tkinter.Canvas.moveto(),tkinter.PhotoImage.transparency_get(),

tkinter.PhotoImage.transparency_set(),

tkinter.Spinbox.selection_from(),

tkinter.Spinbox.selection_present(),

tkinter.Spinbox.selection_range(),

tkinter.Spinbox.selection_to() | + |   |   |   |

| typing.Final,typing.get_args(),typing.get_origin(),typing.Literal,

typing.Protocol,typing.SupportsIndex,typing.TypedDict | + |   |   |   |

typing.NamedTuple._field_types 已弃用 *****
unicodedata.is_normalized() +
unittest 支持协程作为测试用例 +
unittest.addClassCleanup(),unittest.addModuleCleanup(),unittest.AsyncMock +

| xml.etree.Element.getchildren(),xml.etree.Element.getiterator(),xml.etree.ElementTree.getchildren(),以及

xml.etree.ElementTree.getiterator() 已弃用 |   | |   | ***** |

XMLParser.doctype() 已移除 X !
xmlrpc.client.ServerProxy 接受命名的 headers 参数 +
yieldreturn 解包不再需要括号 +
yieldyield from 不再允许在推导式或生成器表达式中使用 X !

Python 3.9

下表总结了 Python 版本 3.9 的更改。更多详情,请参阅在线文档中的“Python 3.9 新特性”部分。

Python 3.9 添加 弃用 移除 破坏性变更
类型注解现在可以在泛型中使用内置类型(例如,list[int] 而非 List[int]) +

| array.array.tostring() 和 array.array.fromstring() 已移除;

使用 tobytes() 和 frombytes() |   |   | X | ! |

ast.unparse() +
asyncio.loop.create_datagram_endpoint() 参数 reuse_address 禁用 !

| asyncio.PidfdChild Watcher,asyncio.shutdown_default_executor(),

asyncio.to_thread() | + |   |   |   |

asyncio.Task.all_tasks 已移除;请使用 asyncio.all_tasks() X !
asyncio.Task.current_task 已移除;请使用 asyncio.current_task() X !
base64.encodestring() 和 base64.decodestring() 已移除;请使用 base64.encodebytes() 和 base64.decodebytes() X !
concurrent.futures.Executor.shutdown() 接受命名的 cancel_futures 参数 +

| curses.get_escdelay(),curses.get_tabsize(),

curses.set_escdelay(),

curses.set_tabsize() | + |   |   |   |

dict 支持联合运算符 | 和 |= +

| fcntl.F_OFD_GETLK,fcntl.F_OFD_SETLK,

fcntl.F_OFD_SETKLW | + |   |   |   |

fractions.gcd() 已移除;请使用 math.gcd() X !
functools.cache() (lightweight/faster version of lru_cache) +
gc.is_finalized() +
graphlib module with TopologicalSorter class +
html.parser.HTMLParser.unescape() removed X !
imaplib.IMAP4.unselect() +
importlib.resources.files() +
inspect.BoundArguments.arguments returns dict instead of OrderedDict +
ipaddress module does not accept leading zeros in IPv4 address strings !
logging.getLogger('root') returns the root logger + !
math.gcd() accepts multiple arguments +
math.lcm(), math.nextafter(), math.ulp() +
multiprocessing.SimpleQueue.close() +
nntplib.NNTP.xpath() and nntplib.xgtitle() removed X !
os.pidfd_open() +
os.unsetenv() available on Windows +
os.waitstatus_to_exitcode() +
parser module deprecated *****
pathlib.Path.readlink() +
plistlib API removed X !
pprint supports types.SimpleNamespace +
random.choices() with weights argument raises ValueError if weights are all 0 !
random.Random.randbytes() +
socket.CAN_RAW_JOIN_FILTERS, socket.send_fds(), socket.recv_fds() +
str.removeprefix(), str.removesuffix() +
symbol module deprecated *****
sys.callstats(), sys.getcheckinterval(), sys.getcounts(), and sys.setcheckinterval() removed X !

| sys.getcheckinterval() and sys.setcheckinterval() removed;

use sys.getswitchinterval() and

sys.setswitchinterval() |   |   | X | ! |

sys.platlibdir attribute +
threading.Thread.isAlive() removed; use threading.Thread.is_alive() X !
tracemalloc.reset_peak() +
typing.Annotated type +
typing.Literal deduplicates values; equality matching is order independent (3.9.1) !
typing.NamedTuple._field_types removed; use annotations X !
urllib.parse.parse_qs() and urllib.parse.parse_qsl() accept ; or & query parameter separator, but not both (3.9.2) !
urllib.parse.urlparse() changed handling of numeric paths; a string like 'path:80' is no longer parsed as a path but as a scheme ('path') and a path ('80') !

| with (await asyncio.Condition) and with (yield from asyncio.Condition) removed;

use async with condition |   |   | X | ! |

| with (await asyncio.lock) and with (yield from asyncio.lock) removed;

use async with lock |   |   | X | ! |

with (await asyncio.Semaphore) and with (yield from asyncio.Semaphore) removed; use async with semaphore X !

| xml.etree.Element.getchildren(), xml.etree.Element.getiterator(),

xml.etree.ElementTree.getchildren(), and

xml.etree.ElementTree.getiterator() removed |   |   | X | ! |

zoneinfo module for IANA time zone support +

Python 3.10

以下表格总结了 Python 3.10 版本的变更。更多详情请参见 在线文档 中的“Python 3.10 新特性”。

Python 3.10 Added Deprecated Removed Breaking change
Building requires OpenSSL 1.1.1 or newer +
Debugging improved with precise line numbers +
Structural pattern matching using match, case, and _ soft keywords^(a) +
aiter() and anext() built-ins + !
array.array.index() accepts optional arguments start and stop +
ast.literal_eval(s) strips leading spaces and tabs from input string s +
asynchat module deprecated *****
asyncio functions remove loop parameter X !
asyncio.connect_accepted_socket() +
asyncore module deprecated *****
base64.b32hexdecode, base64.b32hexencode +
bdb.clearBreakpoints() +
bisect.bisect, bisect.bisect_left, bisect.bisect_right, bisect.insort, bisect.insort_left, and bisect.insert_right all accept optional key argument +
cgi.log deprecated *****
codecs.unregister() +
collections module compatibility definitions of ABCs removed; use collections.abc X !
collections.Counter.total() +
contextlib.aclosing() decorator, contextlib.AsyncContextDecorator +
curses.has_extended_color_support() +
dataclasses.dataclass() decorator accepts optional slots argument +
dataclasses.KW_ONLY +
distutils deprecated, to be removed in Python 3.12 *****
enum.StrEnum +
fileinput.input() and fileinput.FileInput accept optional encoding and errors arguments +
formatter module removed X !
glob.glob() and glob.iglob() accept optional root_dir and dir_fd arguments to specify root search directory +
importlib.metadata.package_distributions() +
inspect.get_annotations() +
int.bit_count() +
isinstance(obj, (atype, btype)) can be written isinstance(obj, atype|btype) +
issubclass(cls, (atype, btype)) 可以写作 issubclass(cls, atype|btype) +
itertools.pairwise() +
os.eventfd(), os.splice() +
os.path.realpath() 接受可选的严格参数 +
os.EVTONLY, os.O_FSYNC, os.O_SYMLINK 和 os.O_NOFOLLOW_ANY 在 macOS 上新增 +
parser 模块已移除 X !

| pathlib.Path.chmod() 和 pathlib.Path.stat() 接受可选

follow_symlinks 关键字参数 | + |   |   |   |

pathlib.Path.hardlink_to() +
pathlib.Path.link_to() 已弃用;请使用 hardlink_to() *****
platform.freedesktop_os_release() +
pprint.pprint() 接受可选的 underscore_numbers 关键字参数 +
smtpd 模块已弃用 *****
ssl.get_server_certificate 接受可选的超时参数 +

| statistics.correlation(), statistics.covariance(),

statistics.linear_regression() | + |   |   |   |

SyntaxError.end_line_no 和 SyntaxError.end_offset 属性 +
sys.flags.warn_default_encoding 发出 EncodingWarning + *****
sys.orig_argv 和 sys.stdlib_module_names 属性 +
threading.excepthook +
threading.getprofile(), threading.gettrace() +
threading.Thread 将生成的线程名称附加了'(<target.name>)' +

| traceback.format_exception(), traceback.format_exception_only(),

和 traceback.print_exception() 签名变更 |   |   |   | ! |

types.EllipsisType, types.NoneType, types.NotImplementedType +
typing 模块包括用于指定 Callable 类型的参数规范变量 +
typing.io 模块已弃用;请使用 typing *****
typing.is_typeddict() +
typing.Literal 去重值;相等匹配是无序的 !
typing.Optional[X] 可以写成 X | None +
typing.re 模块已弃用;请使用 typing *****
typing.TypeAlias 用于定义显式类型别名 +
typing.TypeGuard +
typing.Union[X, Y] 可以使用 | 运算符表示为 X | Y +
unittest.assertNoLogs() +
urllib.parse.parse_qs() 和 urllib.parse.parse_qsl() 接受 ; 或 & 查询参数分隔符,但不能同时使用 !
with 语句接受括号内的上下文管理器: with(ctxmgr, ctxmgr, ...) +
xml.sax.handler.LexicalHandler +
zip 内建函数接受可选的严格命名参数以进行长度检查 +

| zipimport.find_spec(), zipimport.zipimporter.create_module(),

zipimport.zipimporter.exec_module(),

zipimport.zipimporter.invalidate_caches() | + |   |   |   |

^(a) 由于这些被定义为 soft 关键字,因此它们不会破坏使用同样名称的现有代码。

Python 3.11

下表总结了 Python 版本 3.11 的更改。更多详情,请参见在线文档中的“Python 3.11 新功能”。

Python 3.11 已添加 已弃用 已移除 破坏性变更

| 在 Python 3.11.0 中发布的安全补丁并回溯到版本 3.7–3.10: int 转换为 str 和 str 转换为

int 在除了 2、4、8、16 或 32 进制外的其他基数中引发

当生成的字符串 > 4,300 位时引发 ValueError(涉及 CVE-2020-10735) |   |   |   | ! |

性能改进 +
改进的错误消息 +
新语法:for x in *values +
aifc 模块已弃用 *****
asynchat 和 asyncore 模块已弃用 *****

| asyncio.Barrier,asyncio.start_tls(),

asyncio.TaskGroup | + |   |   |   |

asyncio.coroutine 装饰器已移除 X !
asyncio.loop.create_datagram_endpoint() 参数 reuse_address 已移除 X !
asyncio.TimeoutError 已弃用;使用 TimeoutError *****
音频操作模块已弃用 *****
BaseException.add_note(),BaseException.notes 属性 +

| binascii.a2b_hqx(),binascii.b2a_hqx(),

binascii.rlecode_hqx(),和

binascii.rledecode_hqx() 已移除 |   |   | X | ! |

binhex 模块已移除 X !
cgi 和 cgitb 模块已弃用 *****
chunk 模块已弃用 *****
concurrent.futures.ProcessPoolExecutor() max_tasks_per_child 参数 +
concurrent.futures.TimeoutError 已弃用;使用内置的 TimeoutError *****
contextlib.chdir 上下文管理器(更改当前工作目录然后恢复它) +
crypt 模块已弃用 *****
dataclasses 对于可变默认值的检查不允许任何非可哈希值(以前允许任何非 dict、list 或 set 的值) !
datetime.UTC 作为 datetime.timezone.utc 的方便别名 +
enum.Enum 的 str() 输出仅提供名称 +
enum.EnumCheck,enum.FlagBoundary,enum.global_enum() 装饰器,enum.member() 装饰器,enum.nonmember() 装饰器,enum.property,enum.ReprEnum,enum.StrEnum 和 enum.verify() +
ExceptionGroups 和 except* +
fractions.Fraction 从字符串初始化 +
gettext.l*gettext() 方法已移除 X !
glob.glob() 和 glob.iglob() 接受可选的 include_hidden 参数 +
hashlib.file_digest() +
imghdr 模块已弃用 *****
inspect.formatargspec() 和 inspect.getargspec() 已移除;请使用 inspect.signature() X !
inspect.getmembers_static()、inspect.ismethodwrapper() +
locale.getdefaultlocale() 和 locale.resetlocale() 已弃用 *****
locale.getencoding() +
logging.getLevelNamesMapping() +
mailcap 模块已弃用 *****
math.cbrt()(立方根)、math.exp2()(计算 2ⁿ) +
msilib 模块已弃用 *****
nis 模块已弃用 *****
nntplib 模块已弃用 *****
operator.call +
ossaudiodev 模块已弃用 *****
pipes 模块已弃用 *****
re 模式语法支持 *+, ++, ?+ 和 {m,n}+ 占有量词,以及 (?>...) 原子分组 +
re.template() 已弃用 *****
smtpd 模块已弃用 *****
sndhdr 模块已弃用 *****
spwd 模块已弃用 *****

| sqlite3.Connection.blobopen()、sqlite3.Connection.create_window_function()、sqlite3.Connection.deserialize()、

sqlite3.Connection.getlimit()、

sqlite3.Connection.serialize()、

sqlite3.Connection.setlimit() | + |   |   |   |

sre_compile、sre_constants 和 sre_parse 已弃用 *****
statistics.fmean() 可选的 weights 参数 +
sunau 模块已弃用 *****
sys.exception()(相当于 sys.exc_info()[1]) +
telnetlib 模块已弃用 *****
time.nanosleep()(仅适用于类 Unix 系统) +
tomllib TOML 解析模块 +

| typing.assert_never()、typing.assert_type()、

typing.LiteralString、typing.Never、

typing.reveal_type()、typing.Self | + |   |   |   |

typing.Text 已弃用;请使用 str *****
typing.TypedDict 的条目可以标记为 Required 或 NotRequired +
typing.TypedDict(a=int, b=str) 形式已弃用 *****
unicodedata 更新至 Unicode 14.0.0 +

| unittest.enterModuleContext()、unittest.IsolatedAsyncioTestCase.enterAsyncContext()、

unittest.TestCase.enterClassContext()、

unittest.TestCase.enterContext() | + |   |   |   |

| unittest.findTestCases()、unittest.getTestCaseName()、

和 unittest.makeSuite() 已弃用;

请使用 unittest.TestLoader 的方法 |

uu 模块已弃用 *****
with 语句现在对不支持上下文管理器协议的对象抛出 TypeError 异常 !
xdrlib 模块已弃用 *****
添加了 z 字符串格式说明符,用于接近零值的负数标志 +
添加了 zipfile.ZipFile.mkdir() +
在此处添加您自己的笔记:
posted @ 2024-06-17 19:07  绝不原创的飞龙  阅读(2)  评论(0编辑  收藏  举报