Python 技术手册第三版(一)
原文:
annas-archive.org/md5/9e375b08cb0be52e8b7c2a9eba6f5313译者:飞龙
前言
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),以及几个较早开发阶段的高性能实现,如Nuitka,RustPython,GraalVM Python和Pyston,我们不会进一步介绍。在“其他发展、实现和分发”中,我们还提到了一些其他更早期的实现。
这本书主要介绍 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 的显著成功案例之一。
图 1-1. 一个示例 Jupyter Notebook,嵌入了 matplotlib 图形
MicroPython
小型化趋势持续推动着 Python 向业余爱好者的范围内发展。像Raspberry Pi和Beagle boards这样的单板计算机让您在完整的 Linux 环境中运行 Python。在此级别以下,有一类被称为微控制器的设备,它们是可编程的具有可配置硬件的芯片,通过简化模拟和数字传感器的使用,例如轻松实现光和温度测量等应用,使得业余和专业项目的范围扩大。
业余爱好者和专业工程师都在越来越多地使用这些设备,这些设备时常出现(有时又会消失)。多亏了MicroPython项目,许多这类设备(micro:bit,Arduino,pyboard,LEGO^Ⓡ MINDSTORMS^Ⓡ EV3,HiFive,等等)现在可以用(有限的方言)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 脚本来实现这一点,这些脚本动态计算所需的版本,顺序如下查看各种信息源:
-
如果设置了 PYENV_VERSION 环境变量。
-
当前目录中的 .pyenv_version 文件(如果存在)— 您可以使用 pyenv local 命令设置此文件。
-
当在目录树中向上查找第一个 .pyenv_version 文件(如果找到的话)。
-
在 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 对于初学者来说非常适用,因此也有例外情况。非程序员的良好入门免费在线文本包括:
-
Josh Cogliati 的 “Python 3 非程序员教程”(目前以 Python 3.9 为中心)。
-
Alan Gauld 的 “学习编程”(目前以 Python 3.6 为中心)
-
Allen Downey 的 Think Python,第二版(以 Python 3.x 版本为中心)。
一个学习 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 打包管理局(PyPA)改进和维护 Python 打包生态系统,并发布“Python 打包用户指南”。
-
Python 教育工作组致力于推广使用 Python 进行教育和学习。
-
多样性与包容性工作组支持和促进 Python 程序员多样化和国际化社区的增长。
Python 会议
世界各地有许多 Python 会议。一般的 Python 会议包括国际和地区性的会议,如PyCon和EuroPython,以及其他更本地化的会议,如PyOhio和PyCon Italia。专题会议包括SciPy和PyData。会议通常会跟随编程冲刺,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 平台,即流行的iPhone和iPad设备的操作系统。
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 之前未先运行 ./configure,make 隐式地运行 ./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时,您可能需要su为root、bin或其他某个用户。用于此目的的常见习语是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,您可以使用例如python3或python3.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 推出的 PyCharm。Thonny 是一款流行的初学者 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 替代 .py 和 python.exe。w 变体运行 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 之旅”、CKAN和Google的指导)。
词法结构
编程语言的词法结构是指规定如何在该语言中编写程序的基本规则集。它是语言的最低级语法,指定诸如变量名的外观和如何表示注释等事物。每个 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_integer_ratio | 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 且signed为True时,to_bytes 返回i的二进制补码表示。当i < 0 且signed为False时,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_integer_ratio | 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{Copyright Sign}表示一个 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 的唯二值为True和False,它们的字符串表示分别为**'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.name。obj 是任意表达式,name 是标识符,称为对象的 属性名。将值分配给属性引用会请求对象 obj 绑定其名为 'name' 的属性。
一个索引
具有obj[expr]的语法。obj和expr都是任意表达式。对索引进行赋值会要求容器obj绑定其由expr的值指示的项,也称为容器中项的索引或键(索引是应用于容器的一个索引)。
切片操作
具有obj[start:stop]或obj[start:stop:stride]的语法。obj、start、stop和stride都是任意表达式。start、stop和stride都是可选的(即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 | 指数运算(x 的 y 次方) | 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, x <= y, 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 |
在本表中,expr,key,f,index,x 和 y 表示任意表达式,而 attr,arg 和 ident 表示任何标识符。记号 , ... 表示逗号连接零个或多个重复项;在这种情况下,尾随逗号是可选且无害的。
比较链
你可以链式比较,隐含逻辑and。例如:
*`a`* < *`b`* <= *`c`* < *`d`*
其中 a,b,c 和 d 是任意表达式,在没有副作用的情况下,其值与以下表达式相同:
*`a`* < *`b`* `and` *`b`* <= *`c`* `and` *`c`* < *`d`*
这种链式形式更易读,并且每个子表达式最多评估一次。
短路运算符
and 和 or 操作符会 短路 其操作数的评估:仅当需要其值以获取整个 and 或 or 操作的真值时,才会评估右操作数。
换句话说,x and y 首先评估 x。当 x 为假时,结果为 x;否则,结果为 y。类似地,x or y 首先评估 x。当 x 为真时,结果为 x;否则,结果为 y。
and 和 or 不强制其结果为 True 或 False,而是返回它们的操作数之一。这让你可以更广泛地使用这些操作符,不仅限于布尔上下文。由于其短路语义,and 和 or 与其他操作符不同,其他操作符在执行操作之前会完全评估所有操作数。and 和 or 让左操作数作为右操作数的 守卫。
条件操作符
另一个短路运算符是条件操作符 if/else:
*when_true* if *condition* else *when_false*
when_true,when_false 和 condition 都是任意表达式。首先评估 condition。当 condition 为真时,结果为 when_true;否则,结果为 when_false。只有 when_true 和 when_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)。
比较
所有对象,包括数字,都可以进行相等性(==)和不等性(!=)比较。需要顺序比较(<、<=、>、>=)的比较可以用于任何两个数字,除非其中一个操作数是复数,在这种情况下会在运行时引发异常。所有这些操作符都返回布尔值(True 或 False)。但是,在比较浮点数是否相等时要小心,如 第十六章 和 关于浮点数算术的在线教程 中所讨论的。
整数的位操作
整数可以被解释为比特字符串,并与 表格 3-4 中显示的位运算符一起使用。位运算符的优先级低于算术运算符。正整数在概念上通过左侧无限长度的比特字符串扩展,每个比特为 0。负整数则采用二进制补码表示,概念上通过左侧无限长度的比特字符串扩展,每个比特为 1。
序列操作
Python 支持多种操作,适用于所有序列,包括字符串、列表和元组。一些序列操作适用于所有容器(包括非序列的集合和字典);一些适用于所有可迭代对象(即“任何你可以循环遍历的对象”——所有容器,无论它们是否是序列,都是可迭代的,包括一些不是容器的对象,如文件,在 “The io Module” 中介绍,以及生成器,在 “Generators” 中介绍)。在接下来的内容中,我们非常精确地使用术语 sequence、container 和 iterable,以准确指示每个类别适用的操作。
一般序列
序列是有序容器,其项可通过索引和切片访问。
内置的 len 函数接受任何容器作为参数,并返回容器中的项目数。
内置的 min 和 max 函数接受一个参数,该参数是可比较的项的可迭代对象,并分别返回最小和最大的项。您还可以使用多个参数调用 min 和 max 函数,此时它们将分别返回最小和最大的参数。
min 和 max 还接受两个仅限关键字的可选参数:key,一个应用于每个项目的可调用函数(然后比较将根据可调用函数的结果而不是项目本身进行);以及 default,当可迭代对象为空时返回的值(当可迭代对象为空且您未提供默认参数时,函数会引发 ValueError 异常)。例如,max('who', 'why', 'what', key=len) 返回 'what'。
内置的 sum 函数接受一个参数,该参数是数字的可迭代对象,并返回数字的总和。
序列转换
不同序列类型之间没有隐式转换。您可以使用内置的 tuple 和 list 函数并传入单个参数(任何可迭代对象),以获取与参数中相同顺序的相同项目的新实例。
连接和重复
您可以使用+运算符连接相同类型的序列。您可以使用整数n将序列S乘以运算符。S**n 是S的n个副本的连接。当n <= 0 时,S * n 是与S相同类型的空序列。
成员测试
x in S 运算符用于检查对象x是否等于序列(或其他类型的容器或可迭代对象)S中的任何项。当匹配时返回True,否则返回False。x 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]。如果S有L个项,则索引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],其中 i 和 j 是整数。S[i:j] 是从第 i 个项(包括)到第 j 个项(不包括)的 S 的子序列(在 Python 中,范围始终包括下限且不包括上限)。当 j 小于或等于 i 或 i 大于或等于 L(S 的长度)时,切片是空子序列。当 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):] = s或L += 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为空或i是L中的无效索引时,引发异常。 |
| 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,则必须是一个可调用的函数,该函数以任何列表项作为其唯一参数进行调用。在这种情况下,为了比较任意两个项目x和y,Python 比较 key(x)和 key(y)而不是x和y本身(在内部,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,不包含时返回 False。k 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) 如果 s 和 s1 的交集为空集(它们没有共同项),则返回 True,否则返回 False |
| issubset | s.issubset(s1) 当 s 的所有项也都在 s1 中时返回 True,否则返回 False;可以写成 s <= s1 |
| issuperset | s.issuperset(s1) 当 s 包含 s1 的所有项时返回 True,否则返回 False(类似于 s1.issubset(s));可以写成 s >= s1 |
| symmetric_difference | s.symmetric_difference(s1) 返回所有在 s 或 s1 中的项,但不同时在 s 和 s1 中;可以写成 s ^ s1 |
| union | s.union(s1) 返回所有在 s、s1 或两者中的项的集合;可以写成 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 - S2、S & S2、S ^ S2 和 S | S2),而变异方法可以通过增强赋值语法访问(分别为 S -= S2、S &= S2、S ^= S2 和 S |= S2)。此外,集合和不可变集合还支持比较运算符:==(集合具有相同的项;即它们是“相等”的集合)、!=(与 == 相反)、>=(issuperset)、<=(issubset)、<(issubset 且不相等)、>(issuperset 且不相等)。
当您使用运算符或增强赋值语法时,两个操作数必须是集合或不可变集合;然而,当您调用命名方法时,参数 S1 可以是任何具有可哈希项的可迭代对象,并且它的工作方式就像您传递的参数是 set(S1) 一样。
字典操作
Python 提供了许多适用于字典的操作。由于字典是容器,内置的 len 函数可以接受字典作为参数,并返回字典中条目(键/值对)的数量。字典是可迭代的,因此您可以将其传递给任何接受可迭代参数的函数。在这种情况下,迭代按插入顺序仅生成字典的键。例如,对于任何字典 D,min(D) 返回字典中最小的键(迭代中的键顺序在此处无关紧要)。
字典成员资格
k in D 运算符检查对象 k 是否是字典 D 的键。如果键存在,则返回 True,否则返回 False。k 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 所示。非变动方法返回结果而不更改其应用对象,而变动方法可能会更改其应用对象。在此表中,d 和 d1 表示任意字典对象,k 表示任何可哈希对象,x 表示任何对象。
表 3-7. 字典对象方法
| 非变动 | |
|---|---|
| copy | *d.*copy() 返回字典的浅拷贝(一个与 D 相同对象的拷贝,而不是它们的副本,就像 dict(d) 一样) |
| get | *d.*get(k[, x]) 当 k 是 d 的键时,返回 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]) 当 k 是 d 的键时,移除并返回 d[k];否则返回 x(或者当您不传递 x 时,引发 KeyError 异常) |
| popitem | d.popitem() 按后进先出顺序从 d 中移除并返回项目。 |
| setdefault | d.setdefault(k, x) 当 k 是 d 的一个键时返回 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 方法返回相同的结果,但当 k 是 D 中的一个键时,pop 方法还具有移除 D[k] 的副作用(当未指定 x 且 k 不是 D 的键时,get 返回 None,但 pop 会引发异常)。d.pop(key, None) 是从字典中删除键的一个有用快捷方式,无需先检查键是否存在,类似于 s.discard(x)(与 s.remove(x) 相对应,s 是一个集合)。
3.9+ 更新方法可以通过增强赋值语法进行访问:其中 D2 是一个字典,D |= D2 与 D.update(D2) 相同。操作符语法,D | D2,不会改变任何字典:相反,它返回一个新的字典结果,使得 D3 = D | D2 等同于 D3 = D.copy(); D3.update(D2)。
更新方法(但不包括 | 和 |= 操作符)还可以接受键/值对的可迭代对象作为替代参数,而不是映射,并且可以接受命名参数而不是或者除了它的位置参数;当调用内置的 dict 类型时,传递这些参数的语义与“字典”中涵盖的相同。
控制流程语句
程序的控制流调节了程序代码执行的顺序。Python 程序的控制流主要取决于条件语句、循环和函数调用。(本节涵盖了 if 和 match 条件语句,以及 for 和 while 循环;我们在接下来的章节中介绍了异常的引发和处理方式(通过 try 和 with 语句);异常在 第六章中介绍。
if 语句
经常情况下,您需要仅当某些条件成立时执行某些语句,或者根据互斥条件选择要执行的语句。由 if、elif 和 else 子句组成的复合语句可让您有条件地执行语句块。 if 语句的语法如下:
`if` *`expression``:`*
*`statement``(``s``)`*
`elif` *`expression``:`*
*`statement``(``s``)`*
`elif` *`expression``:`*
*`statement``(``s``)`*
...
`else`:
*`statement``(``s``)`*
elif 和 else 子句是可选的。在引入我们接下来将要查看的 match 构造之前,使用 if、elif 和 else 是所有条件处理的最常见方法(尽管有时使用具有可调用值的字典可能会提供一个很好的替代方法)。
这是一个典型的 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 规定的风格)。
您可以在 if 或 elif 子句中使用任何 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 匹配。如果区分很重要,请考虑使用类匹配(参见“类模式”)而不是字面匹配。True、False和None作为单例对象,每个都与自身匹配。
通配符模式
在模式语法中,下划线(_)扮演通配符表达式的角色。作为最简单的通配符模式,_可以匹配任何值:
>>> `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 模式
当P1和P2是模式时,表达式P1 | P2是一个OR 模式,匹配与P1或P2中任一模式匹配的任何内容,如下例所示。可以使用任意数量的替代模式,匹配从左到右尝试:
>>> `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子句以及break和continue语句,我们在讨论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子句以及break和continue语句;我们将很快讨论所有这些内容,从“循环语句上的 else 子句”开始。如前所述,iterable可以是任何可迭代的 Python 表达式。特别地,任何序列都是可迭代的。解释器隐式调用可迭代对象的内置 iter 函数,生成一个iterator(在下一小节中讨论),然后迭代它。
target通常是一个标识符,用于命名循环的控制变量;for语句按顺序将该变量逐个重新绑定为迭代器的每个项。组成循环体的语句或语句为iterable中的每个项执行一次(除非由于异常、break或return语句而结束循环)。由于循环体可能在迭代器耗尽之前终止,因此这是可以使用无界可迭代对象的一种情况——即本身永远不会停止产生项的对象。
你也可以使用带有多个标识符的目标,例如解包赋值。在这种情况下,迭代器的 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 将无限继续,除非循环体包含适当的 break 或 return 语句,或引发或传播异常。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 从 x 到 y 递减。
当 x >= y 且 stride >0,或者当 x <= y 且 stride <0 时,range 生成一个空的迭代器。当 stride ==0 时,range 抛出一个异常。
range 返回一个专用对象,仅用于像前面所示的 for 语句中的迭代。注意,range 返回一个可迭代对象,而不是迭代器;如果需要,您可以通过调用 iter(range(...)) 轻松获取这样的迭代器。range 返回的专用对象消耗的内存比相应的列表对象少(对于宽范围,内存少得多)。如果您真的需要一个是整数等差级数的列表,请调用 list(range(...))。您会发现,事实上,您并不需要完全构建在内存中的这样一个完整列表。
列表解析
for 循环的一个常见用法是检查可迭代对象中的每个项目,并通过附加在某些或所有项目上计算的表达式的结果来构建新列表。称为 列表解析 或 listcomp 的表达式形式让您可以简洁直接地编码这种常见习语。由于列表解析是一个表达式(而不是一组语句块),您可以在需要表达式的任何地方使用它(例如,在函数调用中作为参数,在 return 语句中,或者作为某些其他表达式的子表达式)。
列表解析具有以下语法:
[ *`expression`* `for` *`target`* `in` *`iterable` `lc``-``clauses`* ]
列表解析中的每个 for 子句中的 target 和 iterable 与常规 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
正如这些示例所示,列表推导式中for和if的顺序与等价循环中的顺序相同,但是,在列表推导式中,嵌套保持隐式。如果您记住“按照嵌套循环中的顺序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子句
while 和 for 语句可以选择带有尾部的else子句。该子句下的语句或块在循环自然终止时执行(在for迭代器结束时或while循环条件变为 false 时),但不会在循环过早终止时执行(通过break、return或异常)。当循环包含一个或多个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)
空的def或class语句:使用文档字符串而非pass
您还可以使用文档字符串,在其他情况下空的def或class语句的主体。这样做时,您不需要额外添加pass语句(如果愿意可以添加,但这不是最佳 Python 风格)。
try 和 raise 语句
Python 通过try语句支持异常处理,其中包括try、except、finally和else子句。您的代码还可以使用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}`*
没有命名参数收集器,未匹配的命名参数将引发 TypeError 异常。
函数对象的属性
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 可以是任何可迭代对象。 **dct 将 dct 的项作为命名参数传递给函数,其中 dct 必须是其所有键都为字符串的映射。每个项的键是参数名,项的值是参数的值。
当参数使用类似形式时,您可能希望传递一个seq或dct的参数,如“参数”中所讨论的。例如,在该部分定义的 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*。即使在调用一个签名中没有使用对应形式的函数时,也可以传递seq或dct*。在这种情况下,您必须确保可迭代对象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})`*
将参数与参数匹配
函数调用必须为所有位置参数提供一个参数,并且可以为非仅关键字参数提供参数。
匹配过程如下所示:
-
形式为**expression的参数在内部被替换为一系列通过expression*迭代获得的位置参数。
-
形式为**expression的参数在内部被替换为一系列关键字参数,其名称和值通过迭代expression的 items()获得。
-
假设函数有N个位置参数,并且调用有M个位置参数:
-
当M≤N时,将所有位置参数绑定到前M个位置参数名称;如果有剩余的位置参数,必须通过命名参数匹配。
-
当M>N时,将剩余的位置参数按照它们在签名中出现的顺序绑定到命名参数。此过程以以下三种方式之一终止:
-
所有位置参数都已绑定。
-
签名中的下一个项是一个*参数收集器:解释器会引发 TypeError 异常。
-
签名中的下一个项是一个**name参数收集器:剩余的位置参数将被收集到一个元组中,然后绑定到函数调用命名空间中的name*。
-
-
-
则按照参数在调用中出现的顺序,按名称与参数(包括位置和命名参数)匹配。尝试重新绑定已绑定的参数名称会引发 TypeError 异常。
-
如果在此阶段有未匹配的命名参数:
-
当函数签名包含一个**name收集器时,解释器会创建一个字典,其中包含键/值对(argument's_name, argument's_value),并将其绑定到函数调用命名空间中的name。
-
在没有这种参数收集器的情况下,Python 会引发 TypeError 异常。
-
-
任何剩余的未匹配命名参数都将绑定到其默认值。
此时,函数调用命名空间已经完全填充,解释器使用该“调用命名空间”作为函数的本地命名空间执行函数体。
参数传递的语义
传统上,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 开发人员意识到许多内置函数的参数在解释器看来实际上没有有效的名称时,他们引入了位置参数。
²⁸ “可选参数”是指函数签名提供默认值的参数之一。