Python-精要-全-
Python 精要(全)
原文:
zh.annas-archive.org/md5/7d0510d3d677346f22a6248a5ef40dda译者:飞龙
前言
Python 编程应该是表达性和优雅的。为了使这一点成为现实,语言本身必须易于学习和使用。任何实用语言及其相关库都可以提供令人敬畏的信息量。为了帮助某人学习 Python,我们已经识别并描述了那些看似基本的功能。
学习一门语言可能是一场漫长的航行。在旅途中,我们将经过无数的岛屿、群岛、海湾和河口。我们的目标是指出在这次旅程的初期阶段将要经过的关键特征。
数据结构和算法的概念在编程中始终是重要的考虑因素。我们的整体方法是首先介绍各种 Python 数据结构。作为处理给定类对象的一部分,语言语句随后被引入。Python 相对于其他语言的一个重要优势是丰富的内置数据类型。选择合适的数据表示可以导致优雅、高性能的应用程序。
Python 的一个基本方面是其整体的简单性。操作符和不同类型的语句非常少。我们编写的很大一部分代码可以在底层数据类型方面是通用的。这使我们能够轻松地交换不同的数据结构实现,作为在存储、性能、准确性和其他考虑因素之间进行权衡的一部分。
一些主题领域可能会使我们远远超出基础知识。Python 的面向对象编程特性足够丰富,可以轻松填满几本大书。如果我们也对函数式编程特性感兴趣,我们可以在其他地方进行更深入的研究。我们只会简要地涉及这些主题。
本书涵盖的内容
第一章, 入门,涉及安装或升级 Python。我们探索 Python 的读取-评估-打印循环(REPL)作为与语言交互的方式。我们将使用这种交互式 Python 模式来探索大多数语言功能。
第二章, 简单数据类型,介绍了一些关于数字和一些简单集合的功能。我们将查看 Python 的 Unicode 字符串以及字节字符串,包括一些字符串和数字之间的转换。
第三章, 表达式和输出,提供了更多关于 Python 表达式语法以及各种数值类型之间关系的详细信息。我们将查看强制转换规则和数值塔。我们还将查看print()函数,这是查看输出的常用工具。
第四章, 变量、赋值和作用域规则,展示了如何为对象分配名称。我们查看 Python 中可用的各种赋值语句。我们还探讨了input()函数,它与print()函数类似。
第五章,逻辑、比较和条件,展示了 Python 使用的逻辑运算符和字面量。我们将探讨比较运算符及其用法。我们将仔细研究if语句。
第六章,更复杂的数据类型,展示了list、set和dict内置类型的核心特性。我们使用for语句来处理这些集合。我们还使用了sum()、map()和filter()等函数。
第七章,基本函数定义,介绍了def语句的语法以及return语句。Python 提供了多种方式来为函数提供参数值;我们展示了其中的一些替代方案。
第八章,更高级的函数,扩展了基本函数定义,包括yield语句。这使我们能够编写迭代一系列数据值的生成器函数。我们还将探讨通过内置函数以及Python 标准库中的模块可用的几个函数式编程特性。
第九章,异常,展示了我们如何处理和引发异常。这使得我们能够编写更加灵活的程序。简单的“快乐路径”可以处理大部分的处理工作,而异常子句可以处理罕见或意外的替代路径。
第十章,文件、数据库、网络和上下文,将介绍与持久存储相关的多个特性。我们将探讨 Python 对文件和类似文件对象的用法。我们还将扩展持久化的概念,包括Python 标准库中可用的某些数据库特性。本章还将包括对上下文管理中with语句的回顾。
第十一章,类定义,展示了class语句和面向对象编程的基本要素。我们探讨了继承的基本知识以及如何定义类级别(静态)方法。
第十二章,脚本、模块、包、库和应用,展示了我们可以创建 Python 代码文件的不同方式。我们将探讨脚本、模块和包的正式结构。我们还将探讨非正式概念,如应用、库和框架。
第十三章,元编程和装饰器,介绍了两个可以帮助我们编写操作 Python 代码的 Python 代码的概念。Python 使得元编程相对简单;我们可以利用这一点来简化某些类型的编程,其中常见的方面不适合整齐地纳入类层次结构或函数库中。
第十四章,完善 - 单元测试、打包和文档,超越了 Python 语言,进入了创建一个完整、精炼产品的理念。任何编写良好的程序都应该包括测试用例和文档。我们将展示确保这些工作正确完成的一些常见方法。
第十五章,下一步行动,将演示四种简单的应用程序。我们将查看命令行界面(CLI)、图形用户界面(GUI)、简单的 Web 框架以及 MapReduce 应用程序。
你需要这本书什么
我们将专注于 Python 3,仅限 Python 3。许多计算机已经安装了 Python 2,这意味着需要升级。有些计算机根本未安装 Python,这意味着需要全新安装 Python 3。这些细节是第一章,入门的主题。
需要注意的是,Python 2 不能轻松地运行所有示例。Python 2 可能适用于许多示例,但这不是我们的重点。
为了安装软件,你通常需要在打算使用的计算机上拥有管理员权限。对于家用计算机,这通常是正确的。对于通过工作或学校提供的计算机,可能需要管理员密码。
你可能还需要一个合适的程序员文本编辑器。默认的文本编辑应用程序,如 Windows 记事本或 Mac OS X TextEdit,可以用来,但不是理想的。有许多免费的文本编辑器可供选择:请随意下载几个,找到最适合你的一个。
这本书面向谁
这本书是为想要快速学习 Python 的程序员而写的。它展示了 Python 的关键特性,假设读者有编程背景。重点是基本特性:方法广泛但相对浅显。我们将提供指向和方向,以便进行进一步的学习和研究,假设读者愿意并能够遵循这些指向。
在许多数据密集型行业中,大量的大数据分析使用 Python 和 Apache Hadoop 等工具集进行。在这种情况下,Python 的用户将是统计学家、数据科学家或分析师。他们的兴趣不在于 Python 本身,而在于使用 Python 处理数据集合。这本书旨在为数据科学家提供语言基础。
这本书可供学习 Python 的学生使用。由于这本书没有涵盖编程的计算机科学基础,额外的文本会有所帮助。
会议
在这本书中,你会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“我们使用所有默认参数构建了一个ArgumentParser方法。”
代码块设置如下:
def prod(sequence):
p= 1
for item in sequence:
p *= item
return p
当我们希望将你的注意力引向代码块的一个特定部分时,相关的行或项目将以粗体显示:
def prod(sequence):
p= 1
for item in sequence:
p *= item
return
任何命令行输入或输出都写作如下:
MacBookPro-SLott:Code slott$ python3 -m test_all
新术语和重要词汇以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:“点击继续将进入阅读我、许可、目标选择和安装类型窗口。”
注意
警告或重要注意事项以如下框中显示。
小贴士
小贴士和技巧看起来像这样。
读者反馈
我们欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出你真正能从中获得最大价值的标题。
要发送一般反馈,只需发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及书籍的标题。
如果你在某个领域有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南:www.packtpub.com/authors。
客户支持
现在你已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助你从购买中获得最大收益。
下载示例代码
你可以从你的账户下载示例代码文件:www.packtpub.com,适用于你购买的所有 Packt Publishing 书籍。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support,并注册以将文件直接通过电子邮件发送给你。
错误清单
尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这个问题,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。
盗版
互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的我们作品的非法副本,请立即向我们提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过 <copyright@packtpub.com> 与我们联系,并提供疑似盗版材料的链接。
我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。
询问
如果您在这本书的任何方面遇到问题,您可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。
第一章. 入门
Python 在某些计算机上作为操作系统的一部分提供。在其他计算机上,我们需要添加 Python 程序和相关工具。安装相当简单,但我们仍将回顾细节,以确保每个人都有一个共同的基础。
一旦我们有了 Python,我们需要确认 Python 确实存在。在某些情况下,我们将有多个 Python 版本可用。我们需要确保我们使用的是 Python 3.4 或更新的版本。为了确认 Python 可用,我们将在 Python 的>>>提示符下进行一些交互。
为了扩展剩余章节的基础,我们将查看一些 Python 语法的必要规则。这并不是完整的,但它将帮助我们编写脚本和学习语言。在我们有更多机会处理简单和复合语句之后,详细的语法规则将变得有意义。
我们还将查看 Python“生态系统”,从内置的标准库开始。本书中我们将强调标准库,有两个原因。首先,它非常庞大——我们需要的很多东西已经在我们的电脑上了。其次,更重要的是,研究这个库是学习 Python 编程细节的最佳方式。
除了内置库之外,我们将查看Python 包索引(PyPI)。如果我们无法在标准库中找到合适的模块,第二个地方寻找扩展的地方是 PyPI—pypi.python.org。
安装或升级
要在 Windows 上使用 Python,我们必须安装 Python。对于 Mac OS X 和 Linux,Python 版本已经存在;我们通常会想要添加一个更新的版本到预装的 Python 中。
可用的 Python 有两个显著不同的版本:
-
Python 2.x
-
Python 3.x
本书关于 Python 3.4。我们不会涉及 Python 2.x。有几个明显的差异。重要的是 Python 2.x 在底层有点混乱。Python 3 反映了某些基本改进。这些改进是以牺牲两个版本的语言在几个领域必须不兼容为代价的。
Python 社区正在继续保留 Python 2.x。这样做对那些被旧软件困住的人来说是一种帮助。大多数开发者正在向前推进使用 Python 3,因为它是一个明显的改进。
在我们开始之前,了解 Python 是否已经安装是很重要的。检查 Python 是否已经安装的一般测试是获取一个操作系统命令提示符。对于 Windows,使用命令提示符;对于 Mac OS X 或 Linux,使用终端工具。我们将展示 Mac OS X 终端的提示。它看起来像这样:
MacBookPro-SLott:~ slott$ python3
Python 3.3.4 (v3.3.4:7ff62415e426, Feb 9 2014, 00:29:34)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
我们已经展示了 OS 提示符MacBookPro-SLott:~ slott$。我们输入了python3命令,这在 Linux 和 Mac OS X 中很常见。在 Windows 中,我们通常会输入python。响应是三行介绍,然后是>>>提示符。输入exit并按回车键以获取有关如何退出 Python 的一些有用建议。这个例子展示了 Python 3.3,这有点过时了。不需要升级。
来自操作系统的某种“命令未找到”错误意味着我们没有 Python,因此我们需要进行安装。
如果我们收到一条以类似“Python 2.7.6”开头的 Python 消息,我们就需要进行升级。
下一个部分将介绍 Windows 安装。之后,我们将查看 Mac OS X,然后我们将看到 Linux 升级。在某些情况下,我们可能在 Windows 桌面上开发软件,但最终目标是大型集中式 Linux 服务器。这两个环境中的 Python 文件可以相同,因此 Python 在多个平台上的使用不会非常复杂或令人困惑。
在 Windows 上安装 Python
Python 可以在许多 Windows 版本上运行。有一些较旧的、使用较少的 Windows 版本没有活跃支持的 Python 版本。例如,Windows 2000 不受支持。
安装 Python 的一般步骤相当简单。我们将下载一个安装程序并做一些准备工作。然后我们将启动安装程序。一旦完成,我们就可以开始运行了。
要找到安装程序,从这里开始:
网络服务器应该检测到您的操作系统,并提供一个带有“下载 Python 3.4.x”等变体的按钮。点击此按钮开始下载。
要查看可用的选项,www.python.org/downloads/windows/路径提供了所有活跃支持的 Python 版本。这将显示一个较旧版本的列表。有两个安装程序可用:
-
Windows x86 MSI 安装程序
-
Windows x86-64 MSI 安装程序
如果我们有一台非常旧的电脑,我们可能需要 32 位版本。大多数现代电脑都将有 64 位 CPU。如果有疑问,64 位是默认选择。
双击.msi文件以启动安装程序。这将从询问是为您自己安装 Python 还是为所有用户安装 Python 的问题开始。如果您有适当的权限,所有用户选项是合适的。在共享电脑上,如果没有适当的权限,您只能为自己安装。

第二页将要求输入安装目录。请小心选择安装路径,并避免在文件名中使用空格。
提示
不要将 Python 安装到名称中包含空格的目录中。避免使用“程序文件”和“我的文档”等名称。空格可能会引起难以诊断的问题。
将 Python 安装到具有简短、无空格名称的简单目录中,如C:\python34。

文件名中的空格不是一般问题,但在开始时可能会有些尴尬。有许多方法可以处理文件名中的空格。然而,在学习一门新的编程语言时,减少这些尴尬的问题很重要,这样我们就可以专注于重要的话题。
下一页还将显示一个可以安装的组件菜单;请求所有内容是最简单的。没有充分的理由关闭任何可选组件。我们将查看需要Tcl/Tk包的 IDLE 开发工具,因此确保它是安装的一部分很重要。
在许多情况下,此列表中的最后一个选项会将系统环境变量更新为包括PATH变量中的 Python。这默认是禁用的,但如果您打算在 Windows 中编写 BAT 文件,这可能很有帮助。

除了基本的 Python 解释器外,Windows 帮助安装程序非常有用。这是一个单独的下载,需要快速安装。安装完成后,我们可以使用F1键调出所有的 Python 文档。
Python 安装完成后,使用读取-评估-打印循环(REPL)部分将展示如何开始与 Python 交互。
考虑一些替代方案
我们将关注 Python 的一个特定实现,称为CPython。我们在这里所做的区分是,Python(抽象语言)可以被各种具体的 Python 运行时或实现处理。CPython 实现是用可移植的 C 编写的,并且可以重新编译用于许多操作系统。这种实现通常非常快。
对于 Windows 开发者来说,还有一个名为Iron Python的替代实现。它与 Windows .NET 开发环境紧密集成。它具有与 Visual Studio 一起工作的优势。它的缺点是基于 Python 2.7 语言。
Windows 用户还有另一个选择,即使用Python Tools for Visual Studio(PTVS)。这将允许你在 Visual Studio 中使用 Python 3.4。对于习惯于 Visual Studio 的开发者来说,这可能很有帮助。
其他 Python 实现包括 Jython、Stackless Python 和 PyPy。这些替代方案适用于所有操作系统,因此我们将在查看其他 Python 解释器部分稍后讨论这些。
在 Mac OS X 上升级到 Python 3.4
Python 在所有版本的 Mac OS X 上运行。结果是 Mac OS X 依赖于 Python。然而,它依赖于 Python 2.7,因此我们需要添加 Python 3.4。
在 Mac OS X 上安装 Python 的一般步骤相当简单。我们将下载一个磁盘镜像(.dmg)安装程序并进行一些准备。然后我们将启动磁盘镜像中的安装程序。一旦完成,我们就可以开始使用了。
要查找安装程序,请从这里开始:
网络服务器应检测您的操作系统,并提供一个带有“下载 Python 3.4.x”变体的按钮。点击此按钮并下载 .dmg 文件。
要查看可用的选项,www.python.org/downloads/mac-osx/ 路径提供了 Mac OS X 所支持的所有活跃版本的 Python。这将显示 Python 旧版本的替代方案。
下载完成后,.dmg 设备可用后,双击 .mpkg 安装程序文件以启动安装程序。

点击 继续 将会依次进入 阅读我、许可协议、目标选择 和 安装类型 窗口。有一个 自定义 按钮允许我们打开或关闭选项。我们不需要这样做——默认安装是理想的。
我们需要提供有权管理此计算机的用户的用户名和密码。这不会删除 Mac OS X 使用的现有 Python。它将添加另一个 Python 版本。这意味着我们将至少有两个 Python 版本。我们将专注于使用 Python 3,忽略内置的 Python,即 Python 2。
要使用 Python 3,我们必须在终端窗口的操作系统提示符中输入 python3。如果我们有 两个 Python 3.3 和 Python 3.4,我们可以在命令提示符中输入更具体的 python3.4 来指定我们正在使用 Python 3 的哪个版本。通常,python3 命令将是 Python 3 的最新版本。不带版本号的 python 命令将是 Mac OS X 所需的 Python 2.x。
添加 Tkinter 包
Python 依赖于名为 Tkinter 的库来提供编写具有 GUI 的程序的支持。此包依赖于 Tcl/Tk。详细信息请见以下链接:
www.python.org/download/mac/tcltk/
总结来说,我们需要安装版本 8.5.17 或更高版本。请参阅 www.python.org/download/mac/tcltk/#activetcl-8-5-17-0。这将提供 Python 将使用的图形环境。我们必须安装 Tcl/Tk,以便 tkinter 包能够工作。
下载 .dmg 文件并打开 .pkg 文件后,我们将看到此窗口:

我们将查看需要 tkinter 的 IDLE 开发工具,因此这个额外的安装是必不可少的。
如果我们避免使用 tkinter,就可以避免这个额外的下载。一些开发者更喜欢使用 Active State Komodo 编辑器作为他们的开发工具;这不需要 Tcl/Tk。此外,还有许多不需要 tkinter 的附加 GUI 框架。
在 Linux 中升级到 Python 3.4
对于 Linux,最新的 Python 可能已经安装。当我们输入python3时,我们可能会看到已经有一个有用的版本可用。在这种情况下,我们已经准备好开始。在某些情况下,操作系统可能只安装了较旧的 Python(可能比 2.7 旧)。在这种情况下,我们需要升级。
对于 Linux 发行版,升级 Python 有两条路径:
-
安装预构建的包:许多发行版已经提供了适当的包。我们可以使用包管理器(如
yum或RPM)来定位和安装必要的 Python 包。在某些情况下,可能会有额外的依赖项,导致一系列的下载和安装。由于 Python 3.4 相对较新,可能没有很多针对您特定 Linux 分发的预构建包。详细信息请参阅docs.python.org/3/using/unix.html#on-linux。 -
从源代码构建:大多数 Linux 发行版都包含了 GNU C 编译器。我们可以下载 Python 源代码,配置构建脚本,并使用
make和make install来构建 Python。这可能需要升级一些 Linux 库,以确保您的 Linux 安装具有对 Python 3.4 所需的支持。安装步骤总结为./configure、make和sudo make altinstall。详细信息请参阅docs.python.org/3/using/unix.html#building-python。
当我们使用altinstall时,最终会安装两个 Python 版本。我们将有一个较旧的 Python 版本,可以使用python命令来运行。通常,python3命令会链接到 Python 3 的最新版本。如果我们需要明确指定,可以使用python3.4命令来选择特定的版本。
与 Mac OS X 安装类似,添加 Python tkinter包很重要。有时,这可能是基本包之外的。这可能会导致升级 Tcl/Tk,进而导致更多的下载和安装。在其他时候,Linux 发行版可能已经有一个最新的 Tcl/Tk 环境,无需做更多操作。
如果我们避免使用tkinter,就可以避免额外的 Tcl/Tk 下载。如前所述,许多开发者更喜欢使用 Active State Komodo 编辑器作为他们的开发工具;这不需要tkinter。此外,还有许多不以tkinter为基础的 GUI 框架。
使用读取-评估-打印循环(REPL)
一旦安装了 Python 3,我们可以通过一些基本的 Python 交互来确保一切正常工作。从长远来看,我们将使用其他工具来创建 Python 程序。一开始,我们将直接在命令行上进行交互。
Python 的读取-评估-打印循环(REPL)是 Python 编程的基础。更复杂的事情——比如编写应用程序脚本或网络服务器——本质上与与 REPL 的交互相同:Python 程序从我们的应用程序脚本文件或网络服务器脚本文件中读取语句并评估这些语句。
这条基本规则是 Python 非常吸引人的特性之一。我们可以编写复杂的脚本,或者我们可以通过交互式解释器(REPL)与语言交互;语言是相同的。
确认一切正常
为了确认一切正常,我们将从命令行提示符启动 Python 解释器。它可能看起来像这样:
MacBookPro-SLott:~ slott$ python3
Python 3.3.4 (v3.3.4:7ff62415e426, Feb 9 2014, 00:29:34)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
获取命令提示符的细节因操作系统而异。在这个示例中,我们展示了 Mac OS X 终端工具。我们输入了 python3 命令,以确保我们运行的是新的 Python 版本,而不是内置的 Python 2。
介绍信息列出了四个被纳入交互式 Python 环境的特殊用途对象。还有两个,quit 和 exit,也是可用的。这些只在 REPL 交互式环境中存在;它们不能在程序中使用。
我们将在稍后的单独部分查看如何获取帮助,即 与帮助子系统交互。然而,其他对象会产生有用的信息片段,并且是确保一切正常工作的理想方式。在 >>> 提示符下输入 copyright、credits 或 license 以确认 Python 正在运行。
执行简单的算术运算
REPL 循环打印每个语句的结果,允许我们以交互方式使用 Python。为了清楚地了解这意味着什么,我们应该定义构成语言中的语句的内容。我们将避免 Python 语言定义的严格形式,并提供一个快速的非正式定义相关语句类型。
Python 语言有大约 20 种语句。一个表达式——本身就是一个语句。除非表达式的值为 None,否则 REPL 会显示表达式的值。我们经常使用表达式语句来评估执行输入和输出的函数。
这个简单的表达式语句允许我们在 Python >>> 提示符下执行以下操作:
>>> 355/113
3.1415929203539825
我们可以输入任何算术表达式。Python 会评估该表达式,如果结果不是 None,我们就会看到结果。在这个例子中,我们展示了真正的除法运算符 /。
我们将在第二章简单数据类型中查看各种数据类型和运算符。目前,我们将识别 Python 的几个特性。Python 有各种类型的数字,包括整数、浮点数和复数。大多数值都会被正确地强制转换为具有更高精度的值。看看这些例子:
>>> 2 * 3.14 * 8j
50.24j
>>> _ **2
(-2524.0576+0j)
第一个表达式计算了一个包含整数 2、浮点数 3.14 和复数 8j 的值。我们使用了 * 运算符进行乘法。结果是复数,50.24j。
第二个表达式使用了 _ 变量。这是一个独特的功能,仅限于 REPL。每个表达式的结果都隐式地分配给这个变量。我们可以在表达式中使用 _ 来引用上一个表达式的结果。这仅在 REPL 中有效;它永远不会是脚本的一部分。
当我们计算 _ **2 时,我们平方了 50.24j。这是 -2524.0576。由于源值是一个复数,结果也是一个复数,即使该复数的虚部为零。这在 Python 中很典型——操作数值的数据类型通常决定了运算符的结果的数据类型。当存在不同类型的数字时,值会根据我们在第二章中将要看到的规则进行强制转换,简单数据类型。
有一个值得注意的例外是操作数的类型与结果的类型相匹配的规则。真正的除法运算符 / 从整数操作数产生浮点结果。另一方面,整除运算符 // 反映了操作数的类型。例如:
>>> 355 / 113
3.1415929203539825
>>> 355 // 113
3
我们有这两个除法运算符,这样我们就可以明确指定我们想要执行哪种除法。这使我们免去了编写额外代码来显式强制转换结果的需要。
将结果分配给变量
简单的赋值语句不会产生可见的输出:
>>> v = 23
这将创建变量 v 并将其值设为 23。我们可以通过使用一个非常小的表达式语句来检查这一点。该表达式只是变量名:
>>> v
23
当我们评估一个非常简单的表达式,例如 v,我们看到变量的值。
Python 的交互式解释器(REPL)有着深远的影响。也许最重要的后果是,几乎所有 Python 编程的例子都像是我们在 >>> 提示符下输入代码一样提供的。对于非常复杂和高级的包的文档,会写成我们打算交互式使用该包的样子。在大多数情况下,我们将编写应用程序;我们实际上在 >>> 提示符下不会做很多。但通过简化复杂性以到达可以交互式执行的内容的想法在 Python 社区中无处不在。
使用导入添加功能
Python 的重要部分之一是存在一个庞大的附加功能库。使用外部库意味着核心语言可以保持相当简单。我们可以导入我们需要的任何附加功能,避免未使用功能的杂乱和复杂性。
import语句用于将额外的函数、类和对象合并到程序或交互式环境中。这个语句有多种变体。例如,我们可能想使用一些更复杂的数学函数。我们可以在 Python 文档中搜索并发现这些函数定义在math库中。我们可以像这样包含并使用它们:
>>> import math
>>> math.pi
3.141592653589793
>>> math.sin( math.pi/6 )
0.49999999999999994
在这个例子中,我们导入了math库。我们评估了math.pi来查看这个库中定义的一个常量。我们评估了
。结果是几乎(但不完全)是 1/2。
这也向我们展示了一个关于浮点数的重要事实——它们只是近似值。这与 Python 本身无关——这是数字计算的一般特性。强调这一点关于浮点数的事实非常重要。
小贴士
浮点数只是近似值。它们不是精确的。它们不是具有无限精度的无理数的抽象数学理想。
我们将在第二章“简单数据类型”中回到浮点数的话题。现在,我们想专注于外部库。
Python 中有一个重要的库模块,名为this。要查看this模块,请在>>>提示符下输入import this,如下所示:
>>> import this
另一个同样重要的模块是antigravity。
>>> import antigravity
我们将把这些模块的探索留给读者作为练习。我们不想破坏乐趣!更多的手舞足蹈的解释不如亲身体验有帮助。有关此主题的更多信息,请参阅xkcd.com/413/。
我们总结一下,指出“Python”这个名字与蒙提·派森(Monty Python)有很大关系,与蛇无关。
与帮助子系统交互
Python 的交互式帮助工具提供了大量关于模块、类、函数和对象的有用信息。帮助系统是一个与 Python 的 REPL(交互式解释器)不同的环境;它提供独特的提示来明确这一点。
有三种帮助模式,每种模式都有其独特的提示:
-
我们将看到 Python 帮助环境的
help>提示符。当我们不带参数值评估help()函数时,我们将进入 Python 的帮助环境。我们可以输入不同的主题,了解各种 Python 特性。当我们输入quit作为主题时,我们将返回到 REPL。 -
在 Windows 环境下,我们会看到
-- More --提示:当我们在一个 Windows 环境中评估类似help(int)的内容时,输出将使用 MS-DOS 的more命令来显示。要获取更多信息,请输入?以获取关于如何翻页help()输出的帮助。在 Windows 命令行中,输入more /?将提供关于more命令如何帮助您翻页长文件的其他信息。 -
在 Mac OS X 和 Linux 中,我们会看到
:提示符。当我们使用特定的参数值评估help()函数——例如,help(float)——在 Mac OS X 或 Linux 中,我们会得到使用 less 程序显示的输出。有关更多信息,请在查看help()输出时输入h以获取帮助。在命令提示符中,输入less -?以获取有关 less 程序如何工作的更多信息。
Python 模块还有其他方法可以查看文档。例如,在 IDLE 中,有一个类浏览器和路径浏览器,可以显示有关模块和文件的文档。这是基于内置的 help() 函数,但它是在一个单独的窗口中显示的。
使用 pydoc 程序
Python 包含了 pydoc 应用程序,我们用它来查看文档。这个应用程序是我们从操作系统命令提示符中运行的。我们不会从 Python >>> 提示符中使用它;我们是从操作系统提示符中使用它。在开发过程中,我们可能希望只保留一个终端窗口来显示模块文档。
pydoc 程序有两种操作模式:
-
它可以显示有关特定包或模块的一些文档。这将使用适当的程序(在 Windows 上是 more,在其他情况下是 less)来显示给定对象的文档。以下是如何显示
math模块的文档:MacBookPro-SLott:~ slott$ python3 -m pydoc math -
它可以启动一个文档网络服务器。这将启动一个服务器(并启动一个浏览器)来查看 Python 模块文档。当我们使用它时,我们将有一个看起来像这样的会话:
MacBookPro-SLott:~ slott$ python3 -m pydoc -b Server ready at http://localhost:50177/ Server commands: [b]rowser, [q]uit server> q Server stopped
第二个示例将启动一个网络服务器以及一个浏览器。浏览器将显示 pydoc 生成的文档。这是从模块和包结构以及嵌入在 Python 代码中的文档字符串中派生出来的。当我们完成阅读文档后,我们输入 q 来退出网络服务器。
当我们编写 Python 包、模块、类和函数时,我们可以(并且应该)为 pydoc/help() 文档提供内容。这些文档字符串是我们编程的一部分,与拥有正确工作的程序一样重要。我们将在 第十四章 完善——单元测试、打包和文档 中查看这些嵌入的文档。
创建简单的脚本文件
虽然我们可以从 REPL 使用所有 Python,但这并不是生成最终应用程序的好方法。我们用 Python 做的大部分工作将通过脚本文件完成。我们将在 第十二章 脚本、模块、包、库和应用 中详细查看脚本文件。现在,我们将查看一些功能。
脚本文件必须遵循一些规则:
-
内容必须是纯文本。虽然有些人更喜欢 ASCII 编码,但 Python 3 可以轻松处理 UTF-8 以及大多数操作系统特定的变体,如 Mac OS Roman 或 Windows CP-1252。强烈建议使用可移植的编码 UTF-8。
-
Python 可以处理 Mac OS X、Linux 换行符 (
\n),以及 Windows CR-LF (\r\n)。只有少数 Windows 工具,如记事本,坚持使用 CR-LF 行结束符;大多数其他编程编辑器可以灵活地识别行结束符。除非你真的必须使用记事本,否则通常最好使用 Unix 风格的换行符行结束符。 -
文件名应该是合法的 Python 标识符。这不是一个要求,但如果我们遵循这个建议,它将给我们带来相当大的灵活性。语言参考手册的第 2.3 节提供了构成标识符的详细规则。这些规则的总结是,标识符必须以字母(或规范化为字母的 Unicode 字符)或
_开头。接着是字母、数字和_字符。重要的是我们应该避免在文件名中使用 Python 操作符或分隔符。特别是,我们应该避免使用连字符(-),这在某些 Python 环境中可能会成为问题。操作系统文件名比 Python 标识符有更灵活的规则,操作系统有方法来转义与操作系统相关的标点符号;当我们限制文件名为有效的 Python 标识符——字母、数字和_时,我们会感到最满意。 -
文件扩展名应该是
.py。同样,这也不是必需的,但遵循这个规则非常有帮助。
例如,我们将尝试关注像 test_1_2.py 这样的名称。我们不太容易使用名为 test-1.2.py 的文件;基本名称不是一个有效的标识符——这个名字看起来像是一个 Python 表达式。虽然第二个名称对于顶层脚本来说是可接受的,但它不能作为一个模块或包使用。
在下一节中,我们将探讨一些 Python 语法规则。现在,我们可以创建一个名为 ex_1.py 的简单脚本文件,它只有一行:
print("π≈", 355/113)
我们也可以使用 "\u03c0\u2248" 来代替 "π≈"。字符串 "\N{GREEK SMALL LETTER PI}\N{ALMOST EQUAL TO}" 也会起作用。
一旦我们有了这个文件,我们就可以按照以下方式让 Python 执行该文件:
MacBookPro-SLott:Chapter_1 slott$ python3 ex_1.py
π≈ 3.1415929203539825
我们已经提供了一个文件名,ex_1.py,作为 python3 程序的位置参数。Python 读取文件并执行每一行。我们看到的输出是 print() 函数打印到控制台的文字。
Python 会使用普通的操作系统规则来定位文件,从当前工作目录开始查找文件。这适用于任何类型的文件名。
如果我们遵循了文件的命名规则——文件名是一个标识符,扩展名是 .py——我们也可以使用以下命令来执行一个 Python 模块:
MacBookPro-SLott:Chapter_1 slott$ python3 -m ex_1
π≈ 3.1415929203539825
-m ex_1 选项强制 Python 搜索名为 ex_1 的模块。与此模块关联的文件名为 ex_1.py。Python 有一个用于查找请求模块的搜索路径。除非做出特殊安排,Python 将首先搜索本地目录,然后搜索库目录。这允许我们使用简单、统一的语法运行我们的脚本和 Python 的内置应用。它还允许我们通过修改 PYTHONPATH 环境变量来添加我们自己的应用和模块。
我们将在 第十二章 脚本、模块、包、库和应用 中查看搜索路径。搜索路径的详细文档是 site 包的一部分。
简化的语法规则
Python 的语法规则在 Python 语言参考 手册的第二部分中定义。我们将在 第三章 表达式和输出 中详细讨论这些规则。
Python 有大约 20 种语句。以下是对规则的快速总结:
-
几乎所有语句都以 Python 关键字开头,如
pass、if和def。表达式语句和赋值语句是例外。 -
Python 有两种语句类型——单行 简单 语句和多行 复合 语句。
-
简单语句必须在单行内完成。赋值语句是简单语句。它以一个或多个用户提供的标识符开始,包括
=赋值符号或类似+=的增强变体。表达式语句也是简单的。 -
复合语句使用缩进来表示嵌入在整体语句中的语句序列。标准缩进是四个空格。大多数开发者将他们的编辑器设置为将制表符替换为四个空格。不一致地使用空格和制表符会导致难以看到的语法错误,因为默认情况下制表符和空格都是不可见的。避免使用制表符通常更容易调试问题。
-
复合语句包括类和函数定义——定义体的内容是缩进的。if 语句和 for 以及 while 循环是包含条件或重复执行缩进语句序列的复合语句的例子。
-
括号
(和)必须匹配。一个逻辑行上的单个语句可以跨越多个物理行,直到括号(和)匹配。
实际上,Python 程序由一行一条语句组成。行的末尾是语句的终止符。我们有几种扩展语句的技术。最常见的技术是基于 Python 的要求,即括号 ( 和 ) 必须匹配。
例如,我们可以编写如下代码:
print(
"Hello world",
"π≈",
355/113
)
小贴士
下载示例代码
您可以从www.packtpub.com下载您购买的所有 Packt 出版物的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
我们使用(和)将单个逻辑行扩展到四行物理行。一个后果是,我们在 REPL 中输入的简单语句不得缩进。前导空格会导致问题,因为前导空格用于显示哪些语句在复合语句内部。
这的另一个后果是间接的。Python 从开始到结束逐行执行脚本文件。这意味着复杂的 Python 程序将首先有一系列定义,而“主要”的处理部分通常在最后。
Python 的注释以#开头,并在行尾结束。这遵循了各种 Linux 壳的相同规则。由于 Python 文档字符串是通过pydoc和help()处理的,因此大多数文档实际上是在包、模块、类或函数定义的开始处以单独的字符串字面量呈现。我们将在第十四章完善 - 单元测试、打包和文档中查看这些文档字符串。#注释使用得很少。
Python 生态系统
Python 编程环境有两个广泛的主题领域:
-
语言本身
-
扩展包。我们可以进一步将扩展包细分为:
-
标准库包
-
更多的扩展包的 Python 生态系统
-
当我们安装 Python 时,我们安装了语言以及标准库中的数百个扩展包。我们将在第十二章脚本、模块、包、库和应用中回到标准库。Python 生态系统是无限的。好消息是 PyPI 使得查找包相对容易。
通过附加组件进行扩展的想法
Python 的设计包括一个小核心语言,可以通过导入额外的功能来扩展。语言参考手册描述了 20 个语句;只有 19 个运算符。我们的想法是,我们可以对一个小语言被正确实现、完整和一致有很大的信心。
标准库文档包含 37 章,并描述了数百个扩展包。有许多功能可以帮助我们解决独特的问题。通常可以看到导入大量标准库包的 Python 程序。
我们将看到import语句的两种常见变体:
-
import math -
from math import sqrt, sin
第一个版本导入了整个 math 模块,并在全局命名空间中创建了一个模块对象。该模块中的各种类和函数名称必须使用正确的命名空间进行限定。限定名称看起来类似于 math.sqrt() 或 math.sin()。
虽然第二个版本也导入了 math 模块,但它只将给定的名称引入到全局命名空间中。这些名称不需要限定符。我们可以像使用内置函数一样使用 sqrt() 和 sin()。然而,math 模块对象不可用,因为它没有被引入到全局命名空间中。
导入只发生一次。Python 跟踪导入的模块,并且不会再次导入一个模块。这允许我们自由地按需导入模块,而不用担心模块之间的顺序或其他隐晦的依赖关系。
为了确认这个一次性导入规则,尝试以下操作:
>>> import this
>>> import this
第二次的行为不同,因为模块已经被导入了一次。
使用 Python 包索引 – PyPI
许多 Python 模块的开发者会将他们的作品注册到 PyPI 上。这个网站位于 pypi.python.org/。这是寻找可能帮助解决特定问题的模块的第二个地方。
首先要查找的地方总是标准库。
PyPI 网页提供了一个方便的搜索表单,以及一个浏览器,可以显示按照九个不同的元数据变量组织的软件包。在许多情况下,一本书或博客文章可能会提供一个直接路径,例如:pypi.python.org/pypi/Sphinx/1.3b2。这确保了可以下载和安装正确的版本。
从 PyPI 下载和安装软件有三种常见方式:
-
使用
pip -
使用
easy_install -
手动
通常,我们会使用 pip 或 easy_install 等工具来安装几乎所有软件。然而,偶尔我们也可能需要手动安装。
一些模块可能涉及 Python 的二进制扩展。这些通常是 C 语言源代码,因此必须编译才能使用。对于 Windows(C 编译器很少见),通常需要找到一个包含预构建二进制的 .msi 安装程序。对于 Mac OS X 和 Linux,C 源代码可以作为安装过程的一部分进行编译。
在大型、复杂的数值和科学软件包的情况下——特别是 numpy 和 scipy——构建过程可能变得相当复杂:通常比 pip 或 easy_install 能够处理的更复杂。这些软件包有许多额外的性能库;构建包括 FORTRAN 和 C 的模块。在这种情况下,使用预构建的特定于操作系统的分发;pip 不是这个过程的一部分。
安装额外的包将需要管理员权限。因此,我们将展示 sudo 命令作为提醒,说明在 Mac OS X 和 Linux 上这是必需的。Windows 用户可以简单地忽略 sudo 命令的存在。
使用 pip 收集模块
pip 程序是 Python 3.4 的一部分。它是 Python3 的附加组件。要使用 pip 安装一个包,我们通常使用以下命令之一:
prompt$ sudo pip3.4 install some-package
对于 Mac OS X 或 Linux,我们需要使用 sudo 命令以便拥有管理员权限。Windows 用户可以省略这一步。
pip 程序将在 PyPI 上搜索名为 some-package 的包。将使用已安装的 Python 版本和操作系统信息来定位适合该平台的最新和最佳版本。文件将被下载,并自动运行包中包含的 Python setup.py 文件来安装它。
对于 Mac OS X 和 Linux 用户,值得注意的是,操作系统所需的 Python 版本通常没有配置 pip。拥有内置 Python 2.7 和 Python 3.4 的 Mac OS X 用户通常可以使用默认的 pip 命令而没有任何问题,因为不会为 Python 2 配置 pip。
如果有人同时安装了 Python 3.3 和 Python 3.4,并且为 Python 3.3 安装了 pip,他们必须选择他们想要使用的版本。使用 pip3.3 或 pip3.4 命令将使用为给定 Python 版本配置的 pip 命令之一。默认的 pip 命令可能链接到最后安装的版本——这是我们不应该猜测的。
pip 程序具有许多其他功能,可以卸载包并跟踪哪些包被添加到初始 Python 安装中。pip 程序还可以创建您新创建的可安装包。
使用 easy_install 添加模块
easy_install 包也是 Python 3.4 的一部分。它是 setuptools 包的一部分。我们使用 easy_install 如下安装一个包:
prompt$ sudo easy_install-3.3 some_package
对于 Mac OS X 或 Linux,我们需要使用 sudo 命令以便拥有管理员权限。Windows 用户可以省略这一步。
easy_install 程序与 pip 类似——它将在 PyPI 上搜索名为 some-package 的包。将使用已安装的 Python 版本和操作系统信息来定位适合该平台的版本。文件将被下载。其中之一是 setup.py 脚本;这将自动运行以完成安装。
手动安装模块
在罕见的情况下,我们可能有一个不在 PyPI 上的包,无法通过 pip 或 easy_install 定位。在这种情况下,我们通常有两个或三个步骤的安装过程:
-
下载:我们需要安全地下载该软件包。在许多情况下,我们可以使用
https或ftps以确保使用安全的套接字。如果我们无法确保连接的安全性,我们可能需要检查文件的 md5 签名,以确保我们的下载完整且未被篡改。 -
解压:如果 Python 软件包被压缩成单个 ZIP 或 TAR 文件,我们需要将下载的文件解压或解包到临时目录中。
-
设置:许多为手动安装设计的 Python 包都包含一个
setup.py文件,它将执行最终的安装。我们需要运行类似以下的命令:sudo python3 setup.py install
这个包括最终命令在内的步骤序列是由 pip 和 easy_install 自动化的。我们已经展示了在 Mac OS X 和 Linux 上使用 sudo 命令来确保管理员权限可用。Windows 用户只需简单地去掉这个命令即可。
setup.py 脚本使用 Python 的 distutils 包来定义必须安装到 Python 库目录结构中的内容。install 选项说明了我们对下载的软件包要做什么。大多数时候,我们会安装,所以这是最常见的选项之一。
在罕见的情况下,一个包可能只包含一个模块文件。可能没有 setup.py 文件。在这种情况下,我们将手动将文件复制到我们自己的 site-packages 目录中。
查看其他 Python 解释器
本书将专注于一种称为 CPython 的特定 Python 实现。这意味着 Python(抽象语言)可以被各种具体的 Python 运行时或实现处理。CPython 实现是用可移植的 C 编写的,并且可以重新编译以适应许多操作系统。
Python 可以嵌入到应用程序中。这意味着一个复杂的应用程序可以包含整个 Python 语言,作为编写脚本来定制给定应用程序的一种方式。一个例子是 Ganglia 监控系统 (ganglia.sourceforge.net)。Python 是系统的一部分;我们可以使用与 Ganglia 组件交互的 Python 脚本来定制其行为。在这本书中,我们不会深入探讨这类应用程序;我们将专注于独立的 Python 实现。
有几种替代的 Python 实现。在本章的 在 Windows 上安装 Python 部分中,我们提到 IronPython (ironpython.net) 和 PTVS (pytools.codeplex.com) 可用。这些提供了与 .NET 框架更紧密的集成。
我们可能还会遇到更多的实现:
-
Jython:这是用 Java 编写的 Python 解释器版本,它在 Java 虚拟机 (JVM) 上运行。请参阅
www.jython.org。该项目专注于 Python 2.7。 -
PyPy:这是一个用 Python 编写的 Python 解释器版本。请参阅
pypy.org。通过 RPython 转换工具链打破了“Python 写在 Python 中”的循环,它创建了一个非常复杂的 Python 程序实现。这可以为各种长时间运行的应用程序,如 Web 服务器,提供显著的性能提升。 -
无栈:这个版本的 Python 与 CPython 的线程模型不同。请参阅
www.stackless.com。这个版本可以为多线程服务器提供显著的性能提升。
由于 Python 源代码易于获取,寻找优化机会相当容易。这种语言相对简单,允许实验以查看实现中的变化可能产生的影响。
摘要
我们已经探讨了安装或升级 Python,以便我们可以使用 3.3 或 3.4 版本,并且我们简要地探讨了 Windows、Mac OS X 和 Linux 之间的细微差别。操作系统变体之间的主要差异是 Windows 缺乏 Python,而 Mac OS X 和 Linux 通常已经预装了 Python 版本。操作系统之间几乎没有其他差异。
我们已经探讨了使用 REPL 进行的一些基本交互。我们查看了一些简单的表达式和内置的 help() 子系统。
我们已经探讨了 import 语句如何扩展我们的 Python 运行时环境的基本功能,并且我们也介绍了更大的 Python 生态系统。我们可以使用 pip(和 easy_install)工具来扩展我们的 Python 库。PyPI 是大多数 Python 扩展模块的中心仓库。
在下一章中,我们将详细探讨 Python 的数值类型。Python 的数字构成了一种遵循数学中整数、有理数、实数和复数概念的“塔”。我们将探讨数学运算符以及一些用于处理数字的标准库。
我们还将探讨一些更复杂的数据类型,包括特定的元组、字符串和冻结集合。这些相对简单,因为它们是不可变的。正如普通数字的情况一样,这些更复杂对象的值也不会改变。
第二章:简单数据类型
现在,我们将探讨一些既是内置类型又是 Python 标准库一部分的数据类型。我们将从 Python 的数值类型开始。这些包括三个内置类型:int,float和complex,以及标准库类型Fraction和Decimal。
我们还将探讨字符串,str,以及简单的集合,tuple。这些比数字更复杂,因为它们包含多个项目。由于它们的行为比我们在后续章节中将要看到的对象类型要简单,因此它们可以作为 Python 中序列一般概念的良好介绍。
注意Fraction和Decimal名称的字母大小写。内置类型名称以小写字母开头。我们必须导入的类型有一个以小写字母开头的模块名称,但类型名称以大写字母开头。这种约定很普遍,但并非普遍适用。
本章我们将探讨的所有类型都具有不可变性的共同特征。这个概念适用于我们将要查看的两个集合:一旦构建,字符串或元组就不能更改。我们不会更改它,而是创建一个新的对象。在第六章,更复杂的数据类型中,我们将查看可以更新而不需要创建新对象的集合。
在本章中,我们将探讨内置的将字符串表示转换为其他表示以及从其他表示转换为字符串表示的函数。这有助于我们在显示输出或将字符串输入转换为有用的 Python 对象时。
注意,我们仍在轻松地处理正式的 Python 语法。我们将推迟对语法规则的详细审查,直到第三章,表达式和输出。现在,我们关注的简单表达式语句必须限制在单行内。
介绍内置运算符
在查看各种可用的数字类型之前,我们将介绍 Python 运算符。运算符分为三大类:
| 组 | 运算符 |
|---|---|
| 算术 | +, -, *, **, /, //, % |
| 位运算 | <<, >>, &, |, ^, ~ |
| 比较 | <, >, <=, >=, ==, != |
这些组之间的差异部分是主观的。比较运算符的工作方式在技术上只有细微差别。大多数运算符是二元的,只有一个(~)是一元的,而少数(+, -, *, **)可以在两种上下文中使用。
+, -, *, / 和 % 运算符的含义与其他编程语言中使用的类似。– 和 + 有算术含义。Python 在将一个数字提升到幂时使用 ** 运算符。** 运算符的优先级高于一元形式 -;这意味着 -2**4 的结果是 -16。
位运算符仅适用于整数。它们也适用于集合。这些绝对不是逻辑运算符。实际的逻辑运算符在 第五章,逻辑、比较和条件 中描述。
进行比较
比较运算符(<, >, ==, !=, <=, >=)的含义与其他编程语言中使用的类似。强制类型转换规则适用于数字之间的比较。如果对象是混合类型,其中一个对象将被强制“向上”转换到数值塔中的浮点数,或者从浮点数到复数。比较的结果是一个布尔值(True 或 False),无论两个操作数的类型如何。
各种强制类型转换规则不适用于字符串或其他对象。字符串不会隐式转换为数字。2 != '2' 是真的,因为整数 2 不是一个字符串 '2'。
一些流行的语言(例如 Java、C++)有原始类型,如 int 或 long,它们不是正确的对象——它们不是类的实例——并且适用于对象的规则不适用于它们。Java 允许对 int 对象使用 == 比较操作,但使用相同的比较运算符与字符串对象比较并不比较两个字符串的字符,它只比较引用。这与 Python 完全不同。所有 Python 对象都是类的正确实例:Python 字符串中的 == 比较是逐字符比较两个字符串。
我们将在 第五章,逻辑、比较和条件 中更详细地研究比较。
使用整数
Python 的整数是 int 类的对象。这些对象具有最多的运算符,包括所有算术、位运算和比较运算符。
整数值受可用内存限制。这意味着它们可以非常大。我们可以轻松地计算 1,000!,一个超过 2,500 位的数。我们将细节留到 第八章,更高级的功能。具有类似巨大规模的数有:
>>> 2**8530
610749...581824
这是一个非常大的数。我们省略了其中大部分。它很容易在 Python 中表示。
通常,我们以十进制(基数 10)提供整数字面量。我们也可以用三种其他基数编写字面量:十六进制、八进制和二进制。
0x 前缀是十六进制值的标志:0x10 是 16。我们可以使用字母 a-f,这在许多其他编程语言中很典型;0xdeadbeef 是有效的。前缀 0o(零和字母 o)用于八进制;尽量避免使用恶意混淆的 0O(零和大写 O)作为八进制值,例如,0o33653337357。我们可以使用 0b 前缀来编写二进制字面量值:0b10 是 2。非十进制数最常见的用途是提供字节数组的十六进制值,这相对较少。
使用位运算符
位运算符是为整数定义的。它们不为复数或浮点对象定义。
<<和>>运算符执行位移。例如,1 << 8是 256。我们已经将值 1 向左移动了 8 位位置。
&、|和^运算符计算两个整数值的按位“与”、“按位或”和按位“异或”。以下是一些示例:
>>> 9 & 5
1
>>> 9 | 5
13
>>> 9 ^ 3
10
为了可视化这些运算符,我们可以使用bin()函数来查看涉及的二进制值。
>>> bin(9)
'0b1001'
>>> bin(5)
'0b101'
使用bin()函数可以阐明9|5的位如何组合以创建13的位。~运算符是整数值的按位二进制补码。例如,~14是-15。这些绝对不是逻辑运算符。逻辑运算符在第五章中描述,逻辑、比较和条件。
小贴士
不要混淆a & b与a and b:
-
a & b计算整数a和b中位的按位“与”。 -
a and b基于a和b的真值计算布尔“与”。
使用有理数
有理数是由两个整数值组成的分数。Python 没有内置的有理数类型。我们必须使用以下方式导入Fraction类:
>>> from fractions import Fraction
这将引入Fraction类的定义到我们的全局环境中。一旦我们有了这个,我们就可以创建Fraction类的对象,如下所示:
>>> Fraction(355,113)
Fraction(355, 113)
算术和比较运算符适用于分数。在进行混合类型表达式时,分数适合于整数之上的数值塔和浮点值之下。以下是将整数强制转换为分数的示例:
>>> Fraction(4,2)*3
Fraction(6, 1)
执行涉及Fraction值和int值的操作需要将int对象强制转换为Fraction类。
我们可以使用它们的属性名称提取分数的分子和分母。以下是一个示例:
>>> a= Fraction(355,113)*5
>>> a.numerator
1775
>>> a.denominator
113
我们从一个涉及Fraction对象和整数的表达式中创建了一个Fraction对象a。然后我们提取了变量a的numerator(分子)和denominator(分母)属性。
使用十进制数
对于货币计算,我们通常使用Decimal数。Python 没有内置的十进制数类型。我们使用以下方式导入Decimal类:
>>> from decimal import Decimal
这将引入Decimal类的定义到我们的全局环境中。我们现在可以创建Decimal对象。重要的是要避免意外地将Decimal和float值混合,因为float值只是近似值。为了确保Decimal值是精确的,我们必须只使用整数或字符串。
>>> Decimal("2.72")
Decimal('2.72')
我们从一个字符串创建了一个Decimal值。生成的Decimal对象将精确地表示这个值,仔细保留适当的十进制位数,并根据需要向上或向下舍入。对于常见的金融计算,Decimal是必需的。以下是一个示例:
>>> (Decimal('512.97')+Decimal('5.97'))*Decimal('0.075')
Decimal('38.92050')
我们添加了两个价格,$512.97 和$5.97,并计算了 7.5%的销售税。税额精确为$38.92050。这通常四舍五入到$38.92。
如果我们尝试使用浮点数进行此类财务计算,我们会遇到一些问题:
>>> (512.97+5.97)*0.075
38.920500000000004
浮点近似值不会产生精确答案。
Python 的强制转换规则与Decimal和int值配合良好。我们可以计算Decimal('3.99')*3并得到Decimal('11.97')作为答案。
强制转换规则不是由Decimal和float类实现的。对于Decimal值强制转换为float值可能有些道理。另一方面,这可能在混合精确货币值和浮点近似值时表明一个深刻的编程错误。由于这是模糊的,并且有争议的,Python 遵循的一般方法可以用 Tim Peters 的Python 之禅中的一句话来总结:
面对歧义,拒绝猜测的诱惑。
因此,混合Decimal和float会导致TypeError异常,而不是遵循强制转换到数值塔的规则,并从精确值转换为近似值。我们必须显式地将Decimal转换为float来进行混合类型表达式。
使用浮点数
浮点值是float类的一个实例。这些对象使用算术和比较运算符。它们不参与位操作符。
Python 浮点实现的细节可能有所不同。CPython 依赖于标准 C 库,这些库应该在广泛的硬件和操作系统平台上提供合理一致的结果。C 库通常使用 IEEE 754 浮点值;Python 的float类型是 C 语言的double。这意味着浮点数将是一个 64 位值,具有(实际上)53 位的分数和 11 位的指数。指数范围从
到
。
我们可以用两种方式写浮点数:作为带小数点的数字,以及用“科学”表示法:
>>> 6335.437
6335.437
>>> 6.335437E3
6335.437
E表示法表示 10 的幂。这意味着 6.335437E3 是
。
非常重要的是要注意,浮点值是一个近似值。我们无法强调它们不是精确的,并且不应该用于货币计算。以下是一个使用浮点近似值工作时的例子:
>>> (5**6)**(1/6)
4.999999999999999
这在任何方面都不应该令人惊讶。从数学上讲,
。由于像 1/6 这样的值没有精确的二进制表示,这种表达式揭示了使用近似值工作的后果。
浮点数使用二进制表示的事实导致了一些有趣的复杂情况。例如,1/6 这样的数没有精确的十进制表示;我们可以用 .1666... 来表示小数位无限重复。然而,例如 1/5 这样的数有一个精确的十进制表示,0.2。这两个数都没有精确的二进制表示。由于我们必须使用有限数量的位,我们会在理想化的值和数字计算机产生的有限值之间注意到细微的差异。
注意,虽然允许进行浮点数的精确相等比较,但这通常不是一个好主意。在 第五章,逻辑、比较和条件 中,我们将讨论如何使用一个狭窄的范围而不是精确相等。我们不需要 a == b,而需要关注 abs(a-b) < ε。
使用复数
Python 的数值塔顶是 complex 类型。它可以被视为由一对浮点数构成的表达式:一个是实数,另一个是虚数。虚数乘以
。我们用 (2+3j) 来表示
。
当处理复数时,我们通常导入 cmath 库而不是 math 库。math.sqrt() 函数仅限于与 float 值一起工作,并且会引发异常而不是提供一个虚数值。如果需要,cmath.sqrt() 函数将提供一个适当的虚数值。
这个库向我们展示了
是本质上正确的:
>>> cmath.e**(cmath.pi*1j)+1
1.2246467991473532e-16j
注意,我们使用了 1j 来表示
。如果我们尝试使用标识符 j(前面没有数字),它会被视为一个简单的变量。1j 的值是一个复数字面量,因为它以数字开头并以 j 结尾。
由于浮点值大约有 53 位,大约是 16 个十进制数字,我们可以预期 float 对无理数如 π 和 e 的近似会有大约
的误差。
数值塔
我们已经看到了 Python 的三种内置数值类型:int、float、complex,以及从标准库中导入的两种更多类型——Fraction 和 Decimal。标准库中的 numbers 模块提供了四种数值类型的基类定义。我们很少需要显式地使用这个模块;当我们需要实现自己的数值类型时,这是一个惯例。
数值类型形成了一种类似于传统数学中看到的各种数字的“塔”。塔的底部是整数。有理数在整数之上。浮点值更上一层楼,而复数位于塔顶。
一个常见的期望是,一种语言将自动强制转换数值,以允许表达式如2*2.718正常工作并产生有用的结果。当乘以一个浮点值时,我们期望整数被强制转换为浮点值。
为了使这成为可能,有两个一般规则应用于二进制算术运算的结果:
-
如果两个操作数是同一类型,结果也将具有该类型。例如,
2 ** 1024不会产生浮点结果。它会产生一个巨大的整数。 -
如果操作数是混合的,其中一个将会被强制“向上”转换到数值塔,从整数 → 有理数 → 浮点数 → 复数。
上述规则有一个显著的例外。/和//运算符定义了两种不同的除法。/运算符提供真正的除法:即使是整数操作数也会产生浮点结果。例如:
>>> 355/113
3.1415929203539825
//运算符提供向下取整除法:结果将被截断,就像它是一个仅包含整数的除法。结果类型不会被强制转换,但答案将被截断。例如:
>>> 355./113.
3.1415929203539825
>>> 355.//113.
3.0
//运算符的存在意味着一个以整数设计的表达式也将正确地与浮点值一起工作。同样,我们可能编写一个带有非正式浮点值期望的表达式;通过使用/,它也将与整数一起工作。
注意,这些数值类型的强制转换规则不适用于字符串或其他对象。字符串不会隐式转换为数字。表达式'2'+2会导致一个TypeError异常。我们将在使用内置转换函数部分稍后查看显式转换。
桥楼隐喻提供了一个方便的记忆强制转换规则的方法。给定来自不同层级的两个值,较低层级的值将被强制转换到塔的较高层级。
数学库
Python 库有六个与数学工作相关的模块。这些在Python 标准库文档的第九章,数值和数学模块中进行了描述。除此之外,我们还有外部库,如 NumPy (www.numpy.org) 和 SciPy (www.scipy.org)。这些库包括大量的复杂算法。对于更复杂的工具集,Anaconda 项目 (store.continuum.io/cshop/anaconda/) 结合了 NumPy、SciPy 和另外 18 个包。
这些是相关的内置数值包:
-
numbers:此模块定义了基本的数值抽象。除非我们要发明一种全新的数字类型,否则我们很少需要它。 -
math:这个模块包含大量函数。它包括基本的sqrt(),各种三角函数(正弦、余弦等)以及各种与对数相关的函数。它有处理浮点数内部结构的函数。它还包括伽玛函数和误差函数。 -
cmath:这个模块是math库的复数版本。我们使用cmath库,以便可以在float和complex值之间无缝切换。 -
decimal:从这个模块导入Decimal类以准确处理货币值。 -
fractions:导入Fraction类以处理精确的有理分数值。 -
random:这个模块包含基本的随机数生成器。它有许多其他函数可以产生各种范围或具有各种约束的随机值。例如random.gauss()产生高斯、正态分布的浮点数值。
从这些库中导入的主要方式如下:
-
import random:当我们想明确指出代码中某个名称的来源时,我们会使用这个。我们将编写类似于random.gauss()和random.randint()的代码,使用模块名称作为明确的限定符。 -
from random import gauss, randint:这将从random模块中引入两个选定的名称到全局命名空间。我们可以使用gauss()和randint()而不需要限定模块名称。 -
from random import *:这将把random模块中所有可用的名称引入我们的应用程序的全局命名空间。这在探索和实验>>>提示符时很有帮助。这可能不适合在更大的程序中使用,因为它可能会引入大量无关的名称。
一个不太常用的功能允许我们重命名通过import语句引入的对象。我们可能想使用from cmath import sqrt as csqrt来将cmath.sqrt()函数重命名为csqrt()。在使用这个import-as重命名功能时,我们必须小心避免歧义和混淆。
使用位和布尔值
如前所述,位运算符&、|、^和~与 Python 的实际布尔运算符and、or、not和if-else没有任何关系。我们将在第五章中探讨布尔值、逻辑运算符和相关编程,逻辑、比较和条件。
如果我们用位运算符&或|代替逻辑运算符and或or,事情可能会显得非常奇怪:
>>> 5 > 6 & 3 > 1
True
>>> (5 > 6) & (3 > 1)
False
第一个例子显然是错误的。为什么?这是因为&运算符的优先级相对较高。它不是一个逻辑连接符,更像是一个算术运算符。&运算符首先执行:6&3的结果是 2。因此,结果表达式5 > 2 > 1是True。
当我们将比较分组以首先执行时,我们将得到5>6的False和3>1的True。当我们应用&运算符时,结果将是False,这正是我们所期望的。如果我们使用括号确保位运算符最后执行,那么不适当地将位运算符用作逻辑连接词可能会工作。然而,这是一个非常糟糕的想法。
使用第五章中展示的正确布尔运算符更容易、更清晰,总体上更好。
与序列一起工作
在本章中,我们将介绍 Python 序列集合。我们将以字符串和元组作为此类的前两个示例。Python 提供了一系列其他序列集合;我们将在第六章中探讨它们,更复杂的数据类型。所有这些序列都有共同的特征。
Python 序列通过位置标识单个元素。位置数字从零开始。以下是一个包含五个元素的tuple集合:
>>> t=("hello", 3.14, 23, None, True)
>>> t[0]
'hello'
>>> t[4]
True
除了预期的升序数字外,Python 还提供了反向编号。位置-1是序列的末尾:
>>> t[-1]
True
>>> t[-2]
>>> t[-5]
'hello'
注意,位置 3(或-2)的值为None。REPL 不会显示None对象,所以t[-2]的值看起来是缺失的。为了更明显地证明这个值是None,请使用以下方法:
>>> t[3] is None
True
序列使用一个额外的比较运算符in。我们可以询问给定值是否出现在集合中:
>>> "hello" in t
True
>>> 2.718 in t
False
切片和切块序列
我们可以使用更复杂的下标表达式从序列中提取一个子序列,称为切片。以下是一个较长字符串的子字符串:
>>> "multifaceted"[5:10]
'facet'
[5:10]表达式是一个从位置 5 开始并延伸到位置 10 之前的切片。Python 通常依赖于“半开”区间。切片的起始位置包括在内,而停止位置不包括在内。
我们可以从切片中省略起始位置,写作[:pos]。如果省略切片的起始值,则默认为 0。我们也可以省略结束,写作[pos:]。如果省略切片的停止值,则默认为序列的长度,由len()函数给出。
Python 使用这些半开区间的方式意味着我们可以用非常整洁的语法对字符串进行分区:
>>> "multifaceted"[:5]
'multi'
>>> "multifaceted"[5:]
'faceted'
在这个例子中,我们在第一个切片中取了前五个字符。我们在第二个切片中取了第一个五个字符之后的所有内容。由于这两个数字都是五,我们可以完全确信整个字符串都被考虑在内。
是的,我们可以从切片中省略两个值:"word"[:]将创建整个字符串的副本。这是一个奇特但有时有用的结构,用于复制对象。
切片有一个第三个参数。我们通常称这些位置为起始位置、结束位置和步长。默认步长为 1。我们可以使用类似"abcdefg"[::2]的形式来提供一个显式的步长,并选择位置为 0、2、4 和 6 的字符。形式"abcdefg"[1::2]将选择奇数位置:1、3 和 5。
步长也可以是负数。这将按相反的顺序枚举索引值。"word"[::-1]的值是'drow'。
使用字符串和字节值
Python 字符串值在某些方面与简单的数值类型相似。有一些类似算术的运算符可用,所有的比较操作都定义了。字符串是不可变的:我们无法更改字符串。然而,我们可以轻松地从现有字符串构建新的字符串,使得字符串对象的可变性问题和数字对象的可变性问题一样无关紧要。Python 有两种字符串值:
-
Unicode:这些字符串使用整个 Unicode 字符集。这是 Python 默认使用的字符串。输入输出库都支持广泛的 Unicode 编码和解码。这种类型的名称是
str。它是一个内置类型,因此以小写字母开头。 -
字节:许多文件格式和网络协议是基于字节定义的,而不是基于 Unicode 字符。Python 使用 ASCII 编码来处理字节。处理字节时必须做出特殊安排。内部类型名称是
bytes。
我们可以轻松地将 Unicode 编码成一系列字节。我们同样可以轻松地将一系列字节解码以查看 Unicode 字符。我们将在查看字面量和运算符之后,在在 Unicode 和字节之间转换部分展示这两种方法。
编写字符串字面量
字符串字面量是由字符串分隔符包围的字符。Python 提供了各种字符串分隔符来解决各种问题。最常见的字面量创建 Unicode 字符串:
-
短字符串:使用
"或'来包围字符串。例如:"Don't Touch"包含一个嵌入的撇号。'Speak "friend" and enter'包含嵌入的引号。在罕见的情况下,如果我们两者都有,我们可以使用\来避免引号:'"Don\'t touch," he said.'使用撇号作为分隔符,并在字符串内部使用转义撇号。虽然字符串字面量必须在单行上完整,但'\n'在内部会扩展成一个正确的换行符。 -
长字符串:使用
"""或'''来包围多行字符串。字符串可以跨越必要的行数。长字符串可以包含任何字符,除了终止的三重引号或三重撇号。
Python 有适量的\转义序列,允许我们输入无法从键盘输入的字符。如果我们使用普通的str字面量,Python 会将所有转义序列替换为正确的 Unicode 字符。在一个普通的bytes字面量中,每个转义序列都变成一个单字节 ASCII 字符。
许多 Python 程序以纯 ASCII 文本保存,但这不是必需的。当以 ASCII 保存文件时,需要转义非 ASCII Unicode 字符。当以 Unicode 保存文件时,则需要相对较少的转义,因为键盘上可用的任何 Unicode 字符都可以直接输入。以下是相同字符串的两个示例:
>>> "String with π×r²"
>>> "String with \u03c0\u00d7r\N{superscript two}"
第一字符串使用 Unicode 字符;为了使此功能正常工作,文件必须以适当的编码保存,例如 UTF-8。第二个字符串使用转义序列来描述 Unicode 字符。\u序列后面跟着一个四位十六进制值。\N{...}转义允许字符的名称。\U转义(在示例中没有显示)需要一个八位十六进制值。第二个示例可以保存为任何编码,包括 ASCII。
最常用的转义序列是\"、\'、\n、\t和\\,用于在引号字符串内创建引号,在单引号分隔的字符串内创建撇号,换行,制表符和一个\字符。还有一些其他的,但它们的含义如此晦涩,通常数字代码更有意义。例如,\v可能应该写成\x0b或\u000b;\v背后的原始含义在很大程度上已经失传。
注意,'\u000b'被替换为实际的 Unicode 字符。我们还有'\u240b',这是一个 Unicode 符号,'
',表示垂直制表符字符。大多数非打印 ASCII 控制字符也有这些符号表示。
使用原始字符串字面量
有时,我们需要提供\字符不是转义字符的字符串。例如,在准备正则表达式时,我们宁愿不被迫写\\来表示单个\字符。同样,当与 Windows 文件名一起工作时,我们不希望"C:\temp"中的 ASCII 水平制表符字符('\u0008')替换字符串字面量中间的'\t'序列。我们可以写成"C:\\temp",但这似乎容易出错。
为了避免这种转义处理,Python 提供了原始字符串。我们可以将前四种分隔符中的任何一种前缀加上字母r或R。例如,r'\b[a-zA-Z_]\w+\b'是一个原始字符串。Python 将保留\字符:\b序列不会被转换为\u0008字符。
如果我们不使用r"字符作为原始字符串的分隔符来做这件事,我们将创建一个与以下等效的字符串字面量:'\x08[a-zA-Z_]\\w+\x08'。这显示了在非原始字符串中\b字符是如何转换为\x08的。省略开头的r'会导致一个不表示我们意图的正则表达式的字符串。
使用字节字符串字面量
我们可能需要在我们的程序中包含字节字符串以及 Unicode 字符串。为了做到这一点,我们在字符串分隔符前使用b或B前缀。字节字符串限于 ASCII 字符和产生单个字节 ASCII 字符的转义序列。
通常,字节字符串关注十六进制转义,\xhh,对于字节字符串使用两个十六进制数字。我们也可以使用八进制转义,\odd,使用八进制数字。
我们还可以使用r或R与b或B结合作为字符串前缀的任何组合来准备原始字节字符串。以下是一个 ASCII 字节中的正则表达式:
>>> rb"\\x[0-9a-fA-F]+"
b'\\\\x[0-9a-fA-F]+'
输出使用 Python 的规范表示法,使用长转义序列表示'\\'正则表达式模式。
为了严谨,我们还可以使用u"前缀来明确表示给定的字符串是 Unicode。这相对较少见,因为它重申了默认假设。在以字节字符串为主体的程序中,使用u"some string"可以使 Unicode 字面量从众多的b"bytes"字面量中脱颖而出。
使用字符串运算符
两个算术运算符+和*在字符串对象的两类str和bytes中都有定义。我们可以使用+运算符来连接两个字符串对象,创建一个更长的字符串。有趣的是,我们可以使用*运算符将字符串与整数相乘以创建一个更长的字符串:"="*3是'==='。
此外,相邻的字符串字面量在代码解析期间合并为一个更大的字符串。以下是一个示例:
>>> "adjacent " 'literals'
'adjacent literals'
由于这发生在解析时间,它仅适用于字符串字面量。对于变量或其他表达式,必须使用正确的+运算符。
所有比较运算符都适用于字符串。比较运算符逐字符比较两个字符串。我们将在第五章中详细探讨这一点,逻辑、比较和条件。
我们不能使用混合类型的操作数与字符串运算符。使用"hello" + b"world"将引发TypeError异常。我们必须将 Unicode str编码为bytes,或者将bytes解码为 Unicode str对象。
字符串是序列集合。我们可以从中提取字符和切片。字符串还与in运算符一起工作。我们可以询问特定的字符或子字符串是否出现在字符串中,如下所示:
>>> "i" in "bankrupted"
False
>>> "bank" in "bankrupted"
True
第一个例子显示了in运算符的典型用法:检查给定项是否在集合中。这种in的用法适用于许多其他类型的集合。第二个例子显示了字符串特有的功能:我们在较长的字符串中寻找给定的子字符串。
在 Unicode 和字节之间转换
大多数 Python I/O 库都了解操作系统文件编码。当处理文本文件时,我们很少需要显式提供编码。我们将在第十章文件、数据库、网络和上下文中检查 Python 输入输出能力的细节。
当我们需要将 Unicode 字符编码为字节字符串时,我们使用字符串的encode()方法。以下是一个示例:
>>> 'String with π×r²'.encode("utf-8")
b'String with \xcf\x80\xc3\x97r\xc2\xb2'
我们提供了一个字面 Unicode 字符串,并将其编码为 UTF-8 字节。Python 有众多的编码方案,所有这些都在codecs模块中定义。
为了解码由字节字符串表示的 Unicode 字符串,我们使用字节的decode()方法。以下是一个示例:
>>> b'very \xe2\x98\xba\xef\xb8\x8e'.decode('utf-8')
'very ☺︎'
我们提供了一个包含十一个单独十六进制编码字节的字节字符串。我们将这些解码以包含六个 Unicode 字符。
注意,对于支持的编码有多个别名。我们使用了"utf-8"和"UTF-8"。在Python 标准库的codecs章节中还有更多解释。
ASCII编解码器是这些中最常用的。除了ASCII之外,许多字符串和文本文件都编码为UTF-8。在从互联网下载数据时,通常有一个标题或其他指示符提供编码,在极少数情况下,它不是UTF-8。
在某些情况下,我们有一个以字节形式编写的文档,使用传统的 ASCII。为了处理 ASCII 文件,我们将从 ASCII 编码的字节转换为 Unicode 字符。同样,我们可以使用 ASCII 编码而不是 UTF-8 来编码 Unicode 字符的子集。
可能存在这样的情况,即给定的字节序列未能正确编码 Unicode 字符。这可能是因为使用了错误的编码来解码字节。或者可能是字节本身不正确。decode()方法有额外的参数来定义当字节无法解码时应该做什么。错误参数的值是字符串:
-
"strict"表示将引发异常。这是默认值。 -
"ignore"表示将跳过无效的字节。 -
"replace"表示将插入默认字符。这在codecs模块中定义。'\ufffd'字符是默认替换字符。
错误处理的选项非常特定于应用程序。
使用字符串方法
字符串对象有大量的方法函数。其中大部分既适用于str对象也适用于bytes对象。这些可以分成四组:
-
转换器:从旧字符串创建新字符串
-
创建器:从非字符串对象(或多个对象)创建字符串
-
访问器:访问字符串并返回有关该字符串的事实
-
解析器:检查字符串并将其分解,或从字符串创建新的数据对象
方法函数的转换器组包括 capitalize()、center()、expandtabs()、ljust()、lower()、rjust()、swapcase()、title()、upper() 和 zfill()。这些方法都对字符串中的字符进行一般性更改以创建转换后的结果。例如,lower() 和 upper() 方法经常用于比较时规范化大小写:
>>> "WoRd".lower()
'word'
使用这种技术可以让我们编写对字符字符串中的小错误更加宽容的程序。
其他转换器包括 strip()、rstrip()、lstrip() 和 replace() 等函数。strip 家族的函数用于删除空白字符。通常在输入行上使用 rstrip() 来删除任何尾随空格和可能存在的尾随换行符。
replace() 函数用于将任何子字符串替换为另一个子字符串。如果我们想进行多次独立的替换,我们可以这样做。
>>> "$12,345.00".replace("$","").replace(",","")
'12345.00'
这将创建一个中间字符串,其中已移除 "$"。它将从这个中间字符串中创建第二个中间字符串,其中已移除 , 字符。这种处理对于清理原始数据很有用。
访问字符串的详细信息
我们使用访问器方法来确定有关字符串的事实;结果可能是布尔值或整数值。例如,count() 方法返回在对象字符串中找到的参数子字符串或字符的次数。
一些常用的方法包括 find()、rfind()、index() 和 rindex() 方法,这些方法将在对象字符串中查找子字符串的位置。如果找不到子字符串,find() 方法返回特殊值 -1。如果找不到子字符串,index() 方法会引发 ValueError 异常。带有 "r" 的版本查找目标子字符串的最右侧出现。所有这些方法都适用于 str 和 bytes 对象。
endswith() 和 startswith() 方法是布尔函数;它们检查字符串的开始或结束。以下是一些示例:
>>> "pleonastic".endswith("tic")
True
>>> "rediscount".find("disc")
2
>>> "postlaunch".find("not")
-1
第一个示例显示了如何使用 endswith() 方法检查字符串的结尾。第二个示例显示了 find() 方法如何在较长的字符串中定位给定子字符串的偏移量。第三个示例显示了如果找不到子字符串,find() 方法返回信号值 -1。
此外,还有七个布尔模式匹配函数。这些是 isalnum()、isalpha()、isdigit()、islower()、isspace()、istitle() 和 isupper()。如果函数与给定的模式匹配,则返回 True。例如,"13210".isdigit() 返回 True。
将字符串解析为子字符串
有一些方法函数我们可以用来将字符串分解为子字符串。我们将在第三章表达式和输出中详细查看 split()、join() 和 partition()。
作为快速概述,我们将指出 split() 方法根据定位一个可能重复的分隔符子字符串将字符串分割成一系列字符串。我们可能使用这样的表达式 '01.03.05.15'.split('.') 来从较长的字符串中创建序列 ['01', '03', '05', '15'],通过在 '.' 字符上分割。join() 方法是 split() 的逆操作。这意味着 "-".join(['01', '03', '05', '15']) 将从单个字符串和分隔符创建一个新的字符串;结果是 '01-03-05-15'。分区可以看作是一个单元素分割,用于将字符串的头部与尾部分开。
Python 的赋值语句处理返回多个值的这种方法非常优雅。在 第四章,变量、赋值和作用域规则,我们将更详细地研究多重赋值。
不应使用 split() 方法解析文件名,也不应使用 join() 方法构建文件名。有一个单独的模块 os.path,它通过应用 OS 特定的规则正确地处理这个问题。
使用 tuple 集合
tuple 是 Python 中可用的最简单的集合之一。它是 Python 序列的多种类型之一。tuple 有一个固定数量的项。例如,我们可能处理 (x, y) 坐标或 (r, g, b) 颜色。在这些情况下,每个 tuple 中的元素数量由问题域固定。我们不希望有一个长度可变的集合的灵活性。
通常,我们会在 tuple 的周围加上 () 来将其与周围的语法区分开来。这并不总是必需的;Python 在某些常见上下文中会隐式地创建 tuple 对象。然而,这始终是一个好主意。如果我们编写一个这样的赋值语句:
a = 2, 3
这个语句将隐式地创建一个二元组 (2, 3),并将对象赋值给变量 a。
tuple 类是 Python 的 Sequence 类族的一部分;我们可以使用它们的索引位置提取 tuple 的项。str 和 byte 类也是序列的例子。除了简单的索引值外,我们还可以使用切片符号从 tuple 中选择项。
值 () 是一个零长度的 tuple。要创建单元素 tuple,我们必须使用 () 并包含一个逗号字符:这意味着 (12,) 是一个单元素 tuple。如果我们省略逗号字符,我们写的是一个表达式,而不是单元素 tuple。
单元素 tuple 必须有一个尾随的逗号。在 tuple 的末尾多出的逗号在其它地方会被静默忽略:(1, 1, 2) 等于 (1, 1, 2,)。
tuple 类仅提供两种方法函数:count() 和 index()。我们可以计算一个给定项在 tuple 中出现的次数,并且可以定位一个项在 tuple 中的位置。
None 对象
Python 中非常简单的一种对象类型是None对象。它有几个方法,并且只有一个此类对象的实例可用。这是一个方便的方式来标识某些内容缺失或不适用。它通常用作函数可选参数的默认值。
None对象是一个单例;只能有一个。这个对象是不可变的:我们无法以任何方式更改它。
在 Python 的交互式使用中,REPL 不会打印None对象。例如,当我们评估print()函数时,此函数的正确结果始终是None。此函数的副作用是在我们的控制台上打印内容。展望第三章,我们将给出一个返回None的函数的快速示例:
>>> a = print("hello world")
hello world
>>> a
>>> a is None
True
我们已经评估了print()函数,并将打印函数的结果保存在a变量中。打印的可见副作用是在控制台上看到字符串值。结果是None对象,它不会被打印。然而,我们可以使用is比较运算符来查看a的值确实是None对象。
不可变性的后果
Python 有两种广泛的对象类型:可变和不可变。可变对象有一个内部状态,可以通过使用运算符或方法函数来更新。不可变对象的状态不能被更改。
不可变对象的典范例子是数字。数字2必须始终在 1 和 3 之间有一个单一的、不可变的价值。我们不能改变2的状态使其变为3,否则就是对数学真理概念的嘲讽。
在第六章中,我们将探讨许多可变数据结构。最重要的三个可变集合是set、list和dict。这些对象可以添加和删除项目;我们可以改变对象的状态。
除了数字是不可变的之外,还有三种其他常见的数据结构也是不可变的:str、bytes和tuple。因为字符串和字节是不可变的,所以字符串操作方法总是会从一个或多个现有的字符串对象创建一个新的字符串对象。
这意味着我们无法在较长的字符串中修改字符或子字符串。我们可能会认为需要尝试类似以下操作:
>>> word="vokalizers"
>>> word[2]= "c"
但这行不通,因为字符串对象是不可变的。我们总是从旧字符串的部分构建新的字符串。我们这样做:
>>> word= word[:2]+"c"+word[3:]
这通过提取原始字符串的片段并包含新旧字符的混合来实现。
使用内置的转换函数
在本章中,我们看到了各种数据类型中的许多转换函数。每个内置的数值类型都有一个合适的构造函数。与许多 Python 函数一样,这些函数可以处理多种不同类型的参数:
-
int(): 从各种其他对象创建int-
int(3.718)用于另一个数字 -
int('48879')用于十进制基数的字符串 -
int('beef', 16)用于给定基数的字符串——在这个例子中是 16 -
int()函数可以忽略用 Python 字面量语法编写的数字上的额外前缀字符:int('0b1010',2),int('0xbeef',16),和int('0o123',8)
-
-
float():从其他对象创建float-
float(7331)用于另一个数字 -
float('4.8879e5')用于十进制字符串
-
-
complex():从各种对象创建complex值-
complex(23)创建(23+0j) -
complex(23, 3)创建(23+3j) -
complex('23+2j')创建(23+2j)
-
我们可以将单个数字、数字对,甚至某些字符串转换为Fraction对象:
-
Fraction(2,3):这是创建Fraction对象最常见的方式。 -
Fraction(2.718):这创建了一个值Fraction(765048986699563, 281474976710656)。这表明浮点值实际上是近似值。如果我们想要一个更精确的值,我们应该自己进行有意义的转换,使用Fraction(2718,1000),这将避免许多浮点值中存在的错误位。 -
Fraction("3/4"):这也非常好地创建了一个合适的Fraction对象。
当我们将浮点值转换为Fraction时,结果看起来很奇怪。然而,考虑到浮点值是近似值,Fraction值揭示了近似值的本质。
我们还可以将整数、字符串和浮点数转换为Decimal对象:
-
Decimal(2):有趣的是,这会产生Decimal('2')作为结果。这表明Decimal值的首选格式是字符串。 -
Decimal('2.718'):这将产生预期的值。这通常是我们创建Decimal对象的方式。 -
Decimal(2.718):这将产生一个反映浮点近似值的值:Decimal('2.717999999999999971578290569595992565155029296875')。正因为如此,我们通常避免从float对象创建Decimal对象。
我们从数字到各种类型的字符串有几种额外的转换:bin(),oct(),hex()和str()分别产生 2,8,16 和 10 的基字符串。我们还可以使用数字的多种格式化功能,使用"{0:b}".format(x)进行二进制,"{0:o}".format(x)进行八进制,和"{0:x}".format(x)进行十六进制。如果我们包括格式字符串中的"#"修饰符,我们在产生的字符串中就有相当大的灵活性。例如:
>>> "{0:x}".format(12)
'c'
>>> "{0:#x}".format(12)
'0xc'
这些函数展示了从字符串创建数字以及从数字创建格式化字符串的许多不同方式。
摘要
我们已经查看了一些 Python 中可用的核心数据类型。我们查看了几种不同类型的数字,包括整数、浮点数、复数、Fraction和Decimal。每个都填补了不同的领域。其中三个是内置的,另外两个必须从标准库中导入。
我们还探讨了三种不同类型的集合。tuple是一个相对简单且方法较少的项序列。str是一个 Unicode 字符串,它有几种方法可以创建新的字符串,作为现有字符串的转换。bytes是一个字节字符串,它也有各种方法。我们可以将字节解码为 Unicode 字符串。我们也可以将 Unicode 字符串编码为字节。
我们已经讨论了如何使用import语句来引入新的类型和模块。这将添加标准库中的功能。
我们还研究了多个函数,用于转换各种数值类型。许多这些函数也将字符串转换为数字。我们将大量使用int()和float()函数将字符串转换为数字。然而,使用str()函数进行反向转换——将数字转换为字符串——可以做得更好。然而,在下一章中,我们将探讨的格式化工具可以做得更好。
在第三章中,我们将在此基础上构建基本概念。我们将更深入地研究 Python 语言的语法。我们还将研究用于创建格式化输出的函数。这将使我们能够编写简单的程序。在第四章第四章中,我们将添加更多基本语言特性,以便我们可以编写更复杂的程序。
第三章。表达式和输出
表达式是 Python 编程的核心。正如第一章,入门中所述,Python 拥有丰富的运算符和内置函数集合。在本章中,我们将总结数据类型及其支持的运算符之间的关系。
可能最基础的程序就是执行计算并显示输出的程序。为了演示这一点,我们将在本章中探讨print()函数。我们将通过探讨多种生成格式化文本输出的方式来扩展基础知识。
我们需要详细研究 Python 的语法规则。这对于编写包含更复杂语句序列的脚本至关重要。这将为在第五章,逻辑、比较和条件中探讨复合语句奠定基础。
本章还将演示一些额外的字符串处理技术。我们将总结一些专注于字符串处理的标准库模块。我们将仔细研究re模块;我们使用这个模块来构建正则表达式,帮助解析字符串输入。在str类的内置方法和re模块之间,我们可以处理各种文本输入转换。
表达式、运算符和数据类型
Python 表达式由运算符和操作数构成。在第二章,简单数据类型中,我们介绍了数值和字符串操作数的一些基础知识,并探讨了各种运算符。我们在这里总结这些细节,以便我们可以讨论一些额外的运算符特性。
我们的数值操作数构成一个“塔”,包括以下类型:
| 类型 | 卡氏数 | 运算符数量 |
|---|---|---|
complex |
理想情况下,由一对无理数构成的最独特值,∞×∞。实际上(float × float)或大约 的值。 |
最少的运算符;只有算术运算、一些内置函数和cmath模块。 |
float |
理想情况下这是有理数与无理数的并集(∞+∞)。实际上更接近 不同的值。 |
算术运算符、比较。许多额外的math模块和内置函数。 |
fractions.Fraction |
理想情况下,这些是有理数(∞×∞)。实际上仅受可用内存限制,以表示两个整数。 | 算术运算符、比较、内置函数。 |
decimal.Decimal |
理想情况下,有理数。实际上仅受内存限制。 | 算术运算符、比较、内置函数。 |
int |
理想情况下,自然数,∞。实际上仅受内存限制。 | 算术运算符、比较、位操作运算符、库和内置函数。 |
必须导入Fraction和Decimal类的定义,其他三个类是内置的。我们通常使用如下语句:from fractions import Fraction。
塔背后的想法是许多算术运算符会将操作数从整数强制转换为浮点数,再到复数。大多数时候,这符合我们隐含的数学期望。如果我们不得不编写显式转换来计算2.333*3,我们会感到不高兴。Python 的算术规则确保我们会得到预期的浮点数结果。
Decimal类与隐式转换规则不太匹配:在尝试在float和Decimal之间进行算术运算的罕见情况下,不清楚如何进行。尝试从一个float值创建一个Decimal值会暴露出微小的错误,因为float值是近似值。尝试从一个Decimal值创建一个float值会违背Decimal的目标,即产生精确结果。面对这种歧义,将引发异常。这意味着我们需要编写显式转换。
字符串对象不会隐式地转换为数值。我们必须显式地将字符串转换为数字。int()、float()、complex()、Fraction()和Decimal()函数将字符串转换为适当类的数字对象。
我们可以将运算符分组到几个类别中。
-
算术:
+、-、*、**、/、//、% -
位运算:
<<、>>、&、|、^、~ -
比较:
<、>、<=、>=、==、!=
位运算符由int类的操作数支持。其他数字类没有这些运算符的有用实现。位运算符也定义在集合上,我们将在第六章中探讨,更复杂的数据类型。
在非数值数据上使用运算符
我们可以将一些算术运算符应用于字符串、字节和元组。结果主要集中在从较小的部分创建更大的字符串或更大的元组。以下是一些示例:
>>> "Hello " + "world"
'Hello world'
>>> "<+>"*4
'<+><+><+><+>'
>>> "<+>"*-2
''
在第一个例子中,我们将+应用于两个字符串。在第二个例子中,我们在str和int之间应用了*。有趣的是,Python 通过连接几个副本的原始字符串对象来产生一个字符串结果。乘以任何负数会创建一个零长度的字符串。
print()函数
当使用 Python 的交互式解释器(REPL)时,我们可以输入一个表达式,Python 会打印出结果。在其他上下文中,我们必须使用print()函数来查看结果。print()函数隐式地写入sys.stdout,因此结果将出现在我们运行 Python 脚本的控制台上。
我们可以向print()函数提供任意数量的表达式。每个值都会使用repr()函数转换为字符串。这些字符串会使用默认的分隔符' '组合,并以默认的行结束符'\n'打印。我们可以更改分隔符和行结束字符。以下是一些示例:
>>> print("value", 355/113)
value 3.1415929203539825
>>> print("value", 355/113, sep='=')
value=3.1415929203539825
>>> print("value", 355/113, sep='=', end='!\n')
value=3.1415929203539825!
我们打印了一个字符串和一个表达式的浮点结果。在第二个示例中,我们将分隔符字符串从空格更改为'='。在第三个示例中,我们将分隔符字符串更改为'=',并将行结束字符串更改为'!\n'。
注意,必须按名称提供sep和end参数;这些被称为关键字参数。Python 语法规则要求关键字参数值在所有位置参数之后提供。我们将在第七章中详细检查这些规则,基本函数定义。
我们可以使用,作为分隔符来创建简单的逗号分隔值(CSV)文件。我们也可以使用\t来创建一种以制表符作为列分隔符的 CSV 文件。csv库模块在 CSV 格式化方面做得更加完善,特别是对于包含分隔符字符的数据项,它提供了适当的转义或引号。
要写入标准错误文件,我们需要导入sys模块,其中定义了该对象。例如:
import sys
print("Error Message", file=sys.stderr)
我们已经导入了sys模块。这个模块包含了sys.stderr和sys.stdout的定义,用于标准输出文件。通过使用file=关键字参数,我们可以将特定的输出行定向到stderr文件,而不是默认的stdout。
这在脚本文件中可以很好地工作。在 REPL 提示符中使用标准错误文件看起来并不有趣,因为默认情况下,标准输出和标准错误都输出到控制台。一些 IDE 会为标准错误输出着色。我们将在第十章中查看许多打开和写入其他文件的方法,文件、数据库、网络和上下文。
检查语法规则
在Python 语言参考的 2.1 节中有九条基本的语法规则。我们在这里总结这些规则:
-
有两种语句类型:简单和复合。简单语句必须在单个逻辑行中完整。复合语句以单个逻辑行开始,必须包含缩进的语句。复合语句的初始子句以冒号
:字符结束。通过使用规则 5 和 6,可以将多个物理行合并为一个逻辑行。-
这里是一个典型的简单语句,完整地位于一个逻辑行中:
from decimal import Decimal -
这里是一个典型的复合语句,包含嵌套的简单语句,跨越两个逻辑行:
if a > b: print(a, "is larger")
-
-
物理行以
\n结尾。在 Windows 中,\r\n也被接受。 -
注释以
#开头,并持续到物理行的末尾。它将结束逻辑行。-
这里是一个注释的示例:
from fractions import Fraction # We'll use this to improve accuracy
-
-
可以使用特殊的注释来注释文件编码。这通常不是必需的,因为大多数 IDE 和文本编辑器都会礼貌地处理文件编码。我们应该通常以 UTF-8 编码保存 Python 文件。较旧的文件可能保存为 ASCII。
-
物理行可以通过在物理行结束字符前的
\作为转义字符显式地合并为一个逻辑行。这很少使用,并且通常不推荐使用。 -
物理行可以使用
()、[]或{}隐式地合并为一个逻辑行;这些必须正确配对,逻辑行才能完整。以(开头的表达式可以跨越多个物理行,直到出现匹配的)。这被频繁使用,并且被强烈推荐。-
这里是一个依赖于
()将四行物理行合并为一条逻辑行的语句示例:print ( "big number", 2 ** 2048 )
-
-
空行只包含空格、制表符和换行符。交互式 REPL 使用空行来结束复合语句;REPL 是唯一有意义的空白行上下文。
-
在复合语句的子句内部正确地分组语句需要前置空白。可以使用空格或制表符进行缩进。一致性是必要的。四个空格的缩进被广泛使用,并且强烈推荐。
-
除了行首——它决定了复合语句的嵌套——可以在标记之间自由使用空白。注意,关于在语句中精确使用空格有一些偏好;Python 增强提案(PEP)编号 8 提供了一些建议。参见
www.python.org/dev/peps/pep-0008/以获取无休止争论的素材。
可能最重要的两条规则是规则 6 和规则 8。规则 6 意味着非常常见地使用()、[]和{}来强制将多个物理行合并为一条逻辑行。
规则 8 要求我们的缩进保持一致:缩进和缩出必须匹配。虽然可以使用制表符、空格以及任何一致但随意的制表符和空格的混合,但四个空格被高度推荐。制表符不被推荐,因为它们难以与空格区分。大多数编辑器都可以设置为将制表符键替换为四个空格。一个好的文本编辑器可以识别 Python 语法的基础知识,并且可以优雅地处理缩进和缩出。
小贴士
使用()允许一个语句跨越多个物理行;避免在行尾使用\。
使用四个空格的缩进。
还要注意,Python 在解析源代码时会合并相邻的字符串。我们可以有如下代码:
>>> message = ("Hello"
... "world")
>>> message
'Helloworld'
这个赋值语句使用了一个不必要的()对,允许逻辑行跨越多个物理行。表达式仅仅是两个相邻的字符串,"Hello"和"world"。当 Python 解析源文本时,这两个相邻的字符串会被合并;在评估语句时只使用一个字符串。
此外,请注意,REPL 提示符已从 >>> 更改为 …,因为 REPL 识别第一行物理文本为部分语句。这是一个方便的提醒,说明我们的语句尚未完整。当解析到最后一个 ) 时,语句才算完整,提示符才切换回 >>>。
分割、分区和连接字符串
在 第二章 中,我们探讨了字符串对象的不同的处理方法。我们可以将字符串转换成新的字符串,从非字符串数据创建字符串,访问字符串以确定字符串中的属性或位置,以及解析字符串以分解它。
在许多情况下,我们需要提取字符串的元素。split() 方法用于在字符串中定位重复的类似列表的结构。partition() 方法用于分离字符串的头和尾。
例如,给定一个形式为 "numerator=355,denominator=115" 的字符串,我们可以使用这两种方法来定位各种名称和值。以下是我们将这个复杂字符串分解成片段的方法:
>>> text="numerator=355,denominator=115"
>>> text.split(",")
['numerator=355', 'denominator=115']
>>> items= _
>>> items[0].partition("=")
('numerator', '=', '355')
>>> items[1].partition("=")
('denominator', '=', '115')
我们使用了 split(",") 方法在每个 , 字符处将较长的字符串分割,创建了一个包含两个子字符串的列表对象。REPL 自动将所有表达式结果分配给一个名为 _ 的变量。我们将对象分配给 items 变量,因为 _ 的值会被每个表达式语句覆盖。
我们在 items 变量的每个项目上使用了 partition("=") 方法,将赋值分解为名称、= 和值。更复杂的应用可能需要对名称和值进行更复杂的处理。
join() 方法是 split() 方法的逆操作。它使用字符串对象的序列来创建一个由许多较小的字符串组成的单个长字符串。以下是一个使用字符串元组创建长字符串的示例:
>>> options = ("x", "y", "z")
>>> "|".join(options)
'x|y|z'
我们创建了一个包含三个字符串的序列,并将其分配给一个名为 options 的变量。然后我们使用字符串 "|" 将 options 序列中的项目连接起来。结果是包含项目并通过给定字符串分隔的更长的字符串。
split() 和 join() 方法与单例很好地配合工作。如果我们尝试分割一个没有标点的单例项,我们会得到一个只有一个元素的序列。如果我们连接一个单例项,分隔符将不会被使用。
Python 的字符串方法为我们提供了处理各种字符串解析和分解的工具。对于更通用的解决方案,我们可能需要求助于更强大的工具。我们将在稍后查看正则表达式模块 re。
如果我们想要创建复杂的字符串,我们使用 format() 方法。我们将在下一节查看这个方法。
使用 format() 方法生成更易读的输出
可以使用 format() 方法进行复杂的字符串创建。我们创建一个模板字符串和可以插入模板的值。以下是如何工作的示例:
>>> c=42
>>> "{0:d}°C is {1:.1f}°F".format(c, 32+9*c/5)
'42°C is 107.6°F'
我们创建了一个变量c,其值为 42。我们使用模板"{0:d}°C is {1:.1f}°F"来格式化两个值。索引为 0 的参数值是c,索引为 1 的参数值是表达式32+9*c/5的值。
模板字符串包括字面字符,以及替换字段。每个替换字段由{}包围。替换字段有两个组成部分,其语法为{index:specification}。索引部分标识从format()方法的参数中取出的哪个项目。指定部分显示如何格式化选定的对象。
示例给出了两个规范。一个是字符d,它是十进制整数转换。另一个是稍微复杂一些的.1f,它是一个小数点右侧有一位数字的浮点转换。
格式规范中有相当多的复杂性。格式规范有八个字段。语法概述如下:
[[fill]align][sign][#][0][width][,][.precision][type]
我们用[]将每个字段围起来,以便在视觉上分组名称。请注意,所有字段实际上都是可选的,并且有默认值。
我们将按从右到左的顺序,按照重要性的顺序总结字段。
-
类型:这指定了转换的整体类型。根据 Python 对象的类型,有多个类型代码可用:
-
对于字符串值,使用类型代码
s。 -
对于整数值,可以使用
d、n、b、o、x或X类型代码。这些提供十进制、区域感知的数字、二进制、八进制或十六进制输出。 -
对于浮点值,类型代码可以是
e、E、f、F、g、G、n或%。e格式提供显式的指数。f代码显示没有指数的float值。g值被称为通用,根据数字的大小选择e或f。n代码是区域感知的,使用区域设置进行浮点表示。%乘以 100 并包含%符号。
-
-
精度:
.precision值仅用于浮点格式。它是小数点右侧的位置数。 -
逗号分隔符:如果使用逗号字符,则包括美式逗号作为 1,000 的分隔符。这不是区域感知的,因此不能由操作系统和 Python 区域模块覆盖。
-
宽度:如果省略,数字将按必要宽度格式化。如果提供,数字将填充到这个宽度。默认情况下,填充使用前导空格,但可以通过提供
fill和align字段的值来更改。 -
0:这强制用前导零填充到所需的宽度。这与填充和定位
0=相同。 -
#:与
b、o和x格式化一起使用,在数字前面包含前缀0b、0o或0x。 -
符号:默认情况下,正数没有符号,负数有前导
-。提供+的 符号 字段表示所有符号都明确显示。提供-的 符号 字段表示为正数包含额外空格,确保正负数在打印时使用固定宽度字体时在列中对齐。 -
填充和对齐:这会将空间填充到 宽度 字段的值。如果我们提供 对齐 而没有特定的 填充 字符,则默认字符是空格。我们虽然不能单独提供 填充 字符,但我们可以使用以下四种代码:
-
<或 fill<将数据推向左边,填充将在右边。 -
>或 fill>将数据推向右边,填充字符将用于左边。 -
^或 fill^将数据居中,填充左右两边。 -
=或 fill=将符号放在前面,填充字符将跟在符号后面。这将使符号在数字列中更加突出。
-
这里有一个使用相当复杂的格式规范的例子:
>>> amount=Decimal("234.56")
>>> "Pay: ${0:*>10n} dollars".format(amount)
'Pay: $****234.56 dollars'
我们创建了一个具有 Decimal 值的对象 amount。然后我们在这个数字上使用了格式规范 *>10n。这使用了前导 * 字符来填充数字至 10 个字符。
标准字符串库总结
Python 的标准库提供了一些具有额外字符串处理功能的模块。
-
string:string模块包含将 ASCII 字符分解为字母、数字、空白等常量。它包含了str.format()方法使用的格式化器的完整定义。我们将在下一节中探讨这一点。它还包含Template类,该类定义了一个字符串模板,可以将值插入其中。 -
re:正则表达式库允许我们定义一个模式,该模式可以用于解析输入字符串。我们将在下一节中探讨这一点。 -
difflib:difflib模块用于比较字符串序列,通常来自文本文件。此模块中提供了多种比较算法。 -
textwrap:我们可以使用textwrap模块来格式化大块文本。 -
unicodedata:unicodedata模块提供了确定 Unicode 字符类型的函数。Unicode 标准附件 44 定义了一组适用于 Unicode 字符的属性。一个常用的函数是字符的一般类别;这包括简单的拉丁规则,如 "Lu" 表示大写字母或 "Nd" 表示十进制数字。一般类别代码还包括 "Sk",用于非字母符号,如修饰符号。 -
stringprep:这是 RFC 3454 的实现,它准备 Unicode 文本字符串以支持合理的字符串比较。
使用 re 模块解析字符串
正则表达式给我们提供了一个简单的方法,通过描述它们共有的模式来指定一组相关的字符串。正则表达式是集合论的一个元素,理论上可以定义所有可能的相关的字符串集合。理论上的匹配过程将是一个快速检查,以查看这个所有可能的字符串集合中的给定字符串是否是由该表达式生成的。由于从模式生成的所有可能的字符串集合可能是无限的,所以在实践中并不是这样工作的。
当我们使用 re 模块时,我们通常做三件事。首先,我们指定模式字符串。其次,我们将模式编译成一个对象,该对象可以有效地确定给定的字符串是否以及在哪里与模式匹配。最后,我们反复使用 pattern 对象来有效地匹配、搜索或解析给定的输入字符串。
作为具体的一个例子,我们需要处理包含如下行的输入:Birth Date: 3/8/1987 或 Birth Date: 1/18/59。注意,每个日期中的数字数量以及允许的空格量可能会有所不同。
我们可以执行以下三种常见的处理方式中的任何一种:
-
一个匹配正则表达式可能是
Birth Date:\s+\d+/\d+/\d+。\s+子表达式意味着一个或多个空格。这个\d+子表达式的意思是一个或多个数字。匹配模式通常被设计为匹配整个字符串。 -
一个搜索正则表达式可能是
\d+/\d+/\d+。这个搜索模式包括一个或多个数字,\d+,以及字面符号,/。这个表达式描述了一个可以在给定字符串中找到的子串。 -
一个解析模式将各个数字组与周围上下文分开。这是对前面示例的一个轻微修改,包括
(),指定要捕获的内容。我们可能使用(\d+)/(\d+)/(\d+)来表示数字组应该被提取以进行进一步处理。
我们可以使用 Python 中的 re 模块来完成这些匹配、搜索和解析操作。
使用正则表达式
在 Python 程序中使用正则表达式的一般步骤有三个基本步骤。当然,我们必须使用 import re 来包含所需的模块。这三个步骤是:
-
定义模式字符串。这几乎总是原始字符串,以
r"开头,因为正则表达式字符串将充满我们不想被 Python 作为转义字符处理的\字符。因为\是 Python 语言转义符的开始,如果我们想在非原始字符串中写一个独立的\字符,我们必须将它们加倍。使用原始字符串来编写r"\d+/\d+/\d+"比使用\\d+/\\d+/\\d+更好。 -
评估
re.compile()函数以创建一个pattern对象。这个生成的对象将执行匹配给定目标字符串与正则表达式pattern对象的实际工作。我们可以将模式和编译合并到一个语句中,如下所示:
>>> date_pattern = re.compile(r"Birth Date:\s+(.*)") -
使用编译后的
pattern对象来匹配或搜索候选字符串。成功匹配或搜索的结果将是一个Match对象。然后我们可以使用这个匹配对象,在需要的时候提取字段。例如:>>> match = date_pattern.match("Should Not Match") >>> match >>> match = date_pattern.match("Birth Date: 3/8/87") >>> match <_sre.SRE_Match object at 0X82e60>在第一个示例中,
date_pattern.match()表达式返回None,因为给定的字符串没有匹配正则表达式。在第二个示例中,给定的字符串与正则表达式模式匹配,并创建了一个Match对象。如果我们使用正则表达式进行解析,我们将查询Match对象以获取各种子字符串。
当我们有一个Match对象时,它可能包含匹配整体模式一部分的捕获子字符串。我们通常会使用各种group()方法来获取子字符串。以下是一些示例:
>>> match.group()
'Birth Date: 3/8/87'
>>> match.group(1)
'3/8/87'
>>> match.groups()
('3/8/87',)
在第一个示例中,我们看到了所有匹配的内容。在第二个示例中,我们看到了组号一的价值,即正则表达式中的第一个部分,用()括起来。在最后的示例中,我们看到了正则表达式中的所有()括起来的组。由于只有一个这样的组,groups()的值是一个包含匹配文本的单项元组。
创建正则表达式字符串
创建正则表达式模式有许多规则,我们在这里将探讨其中的一些。完整的列表可以在re模块的Python 标准库文档的 6.2.1 节中找到。有关此主题的更多信息,请参阅 Packt Books 的精通 Python 正则表达式。请参阅www.packtpub.com/application-development/mastering-python-regular-expressions。
我们首先将查看“原子”正则表达式。然后我们将查看将正则表达式组合成更大正则表达式的规则。以下是一些简单的、原子的正则表达式:
-
任何单个字符。除了一些例外,这意味着几乎任何可打印字符。这些例外是正则表达式语言中有特殊意义的字符,包括
.,*,?,(,),[,],|等。 -
.匹配任何字符。为了匹配点,使用转义字符\:\.匹配一个点。 -
一些转义序列匹配整个字符类。
-
\d匹配任何数字。\D匹配任何非数字字符。 -
\s匹配任何空白字符。\S匹配任何非空白字符。 -
\w匹配任何单词字符。\W匹配任何非单词字符。默认情况下,这些遵循 Unicode 规则。我们可以覆盖它以遵循一个相当简单的仅 ASCII 规则集。
-
我们可以在正则表达式后面添加一些后缀。
-
*后缀表示前面的表达式可以匹配零次或多次。这相当于使前面的正则表达式模式既可选又可重复。 -
+后缀表示前面的表达式可以匹配一次或多次。这意味着前面的模式是必需的,也可以重复。 -
?后缀表示前面的表达式是可选的;它可以匹配零次或一次。 -
要实际匹配后缀字符,请使用
\转义。例如,\*匹配一个星号。
我们可以将单个表达式组合成更大的模式。以下是一些常见的组合技术:
-
正则表达式的序列是一个正则表达式。我们只需将表达式一个接一个地放在模式字符串中。当我们编写
Birth这样的表达式时,它是一个由五个原子表达式组成的序列,每个表达式匹配单个字符。 -
[]中的字符序列匹配给定的任意一个字符。这通常用于单字符表达式;我们经常看到像[a-zA-Z0-9_]这样的结构来匹配任何字母、数字或_。为了匹配多字符字符串,我们在[]后使用后缀。我们可以使用r"[0-9a-fA-F]+"来匹配一个或多个十六进制数字。为了使-成为可选字符之一,它必须是列表中字符的第一个或最后一个。 -
由
|分隔的两个正则表达式是一个正则表达式。任一都可以匹配。我们可能正在查看true|false这样的模式。我们必须匹配这两个正则表达式中的一个:true或false。为了匹配管道字符|,它必须像这样转义\|。 -
括号
()包围的正则表达式是一个正则表达式。它也被保留为一个组,这样我们可以在解析时使用匹配的字符。为了匹配括号,它们必须被转义,\(匹配一个(。通过()捕获的子串可以通过匹配对象的group()方法访问。
这些规则帮助我们检查特定模式的细节。以下是我们可能用于解析一些输入的模式:
r"(\w+)\s*[=:]\s*(.*)"
这是一个由 5 个正则表达式组成的正则表达式序列。
-
字符
(\w+)创建了一个带有+后缀的括号内的正则表达式\w。这匹配任何由一个或多个单词字符组成的序列。 -
\s*是一个正则表达式。它是一个简单的表达式\s,带有*后缀。它匹配零个或多个空白字符。这意味着在初始单词之后的空间是可选的。如果存在空格,可以使用任意数量的空格。 -
[=:]是由两个单字符表达式=和:构成的正则表达式。它匹配这两个字符中的任意一个。 -
\s*用于第二次出现,允许在=或:和值之间有任意数量的空白字符。 -
最终的正则表达式是
(.*),它匹配任何字符序列。
当我们使用这个正则表达式时,如果创建了一个 Match 对象,它将有两个组。然后我们可以提取由这个正则表达式中的模式匹配的名称和值。
处理 Unicode、ASCII 和字节
re模块可以与字节以及 Unicode 字符串一起工作。我们必须根据我们正在处理的字符串类型提供适当的模式字面量。对于 Unicode,我们使用带有r前缀的模式字面量:r"\w+"。对于字节,我们使用rb前缀,rb"\w+";这里的rb表示原始字节而不是原始 Unicode 字符。
字符类别的规则当然不同。匹配"\w+"模式的 Unicode 字符串可以包含任何广泛的 Unicode“单词”字符。使用"\w+"模式的字节对象将匹配来自集合a-z、A-Z、0-9和_的 ASCII 字符。
小贴士
在解析、搜索或与字节匹配时,我们必须明确使用字节为模式字面量。
我们可以在re.compile()中使用一个选项来强制 Unicode 模式遵循简化的 ASCII 规则。如果我们写re.compile(r"\w+", re.ASCII),我们就用 ASCII 规则替换了\w的默认 Unicode 假设,即使我们正在进行 Unicode 字符串匹配。
使用区域模块进行个性化
当查看str.format()方法时,我们看到n格式类型根据用户的区域设置产生了一个格式化的数字。这意味着格式根据操作系统区域设置而变化。不同国家的用户将看到他们的个人区域设置被正确使用。
下面是一个使用locale模块获取特定区域设置格式化的示例:
>>> import locale
>>> locale.setlocale(locale.LC_ALL,'')
'en_US.UTF-8'
>>> "{0:n}".format(23.456)
'23.456'
>>> locale.setlocale(locale.LC_ALL,'sv_SE')
'sv_SE'
>>> "{0:n}".format(23.456)
'23,456'
此脚本使用了locale模块来设置 Python 区域设置以匹配当前的操作系统区域设置。区域设置报告为美国使用的英语(en_US),首选的 Unicode 编码显示为 UTF-8。
23.456的格式化值显示为美国英语的小数点。这符合美国用户的预期。
然后,我们将区域设置切换到瑞典。语言报告为sv_SE,这意味着在瑞典使用的瑞典语。格式化的值切换到23,456,使用小数逗号,这对于瑞典用户来说是合适的。
让我们继续这个例子,并使用locale.currency()格式化函数:
>>> locale.currency(23.54)
'23,54 kr'
金额使用,作为小数分隔符,以kr作为瑞典的本地货币。区域模块包括货币名称。
注意,我们提供了数值23.54,使用 Python 语法,这不会因区域设置而变化。Python 浮点字面量始终使用小数点。只有currency()函数的输出字符串使用,字符作为小数点分隔符。
摘要
在本章中,我们回顾了 Python 的基本数字类型和可用的运算符。我们查看了一些涉及字符串和数字数据混合的表达式。
为了查看我们脚本的输出,我们查看print()函数。这被广泛用于产生输出。print()函数是调试特别复杂函数或类的一个非常方便的工具。
此外,我们探讨了如何使用str.format()方法来生成格式化的数据。这为我们提供了将 Python 对象转换为可显示字符串的广泛技术。我们还探讨了使用诸如split()和partition()之类的字符串方法函数来解析字符串的一些方法。
在字符串处理的基本知识之外,我们探讨了如何使用re模块来匹配、搜索和解析字符串。这个模块功能强大,具有大量从输入字符串中提取有用信息的特性。
在第四章变量、赋值和作用域规则中,我们将通过使用变量来存储中间结果来扩展我们的脚本编写。我们还将探讨如何创建和删除对象。这些规则将有助于理解在复杂程序的不同部分中哪些变量是可见的。
第四章:变量、赋值和作用域规则
表达式创建对象;我们可以将对象赋值给变量以保留它们供将来使用。Python 在赋值主题上提供了一系列变体。除了简单地赋值给单个变量外,我们还可以将元组中的项赋值给多个变量。我们还可以将运算符与赋值结合,以更新可变对象。
在本章中,我们还将探讨input()函数作为将新对象引入运行脚本的一种方式。这有限制——它无法与合适的图形用户界面(GUI)相提并论。然而,它将帮助我们学习更多 Python 编程技术,在我们介绍如何在第十章中从文件和文件系统中读取数据之前。
我们还将探讨一些重要的 Python 语言概念。我们将探讨 Python 程序总是以通用方式编写的的方式,而不绑定到特定数据类型或类。我们还将探讨命名空间的一般概念,以及它在各种 Python 语言构造中的应用。它定义了标识符可见的作用域;随着我们的程序变得更加复杂,这将成为越来越重要的事情。
简单的赋值和变量
我们在前面章节中已经看到了几个 Python 基本赋值语句的例子。该语句包括一个变量、=和一个表达式。由于单个对象是一个表达式,我们可以这样写:
>>> pi = 3.14
这将创建浮点字面量3.14并将其赋值给名为pi的变量。
变量名必须遵循《Python 语言参考》中第 2.3 节“标识符和关键字”的规则。参考手册使用unicodedata模块提供的 Unicode 字符类定义。
关于编程语言标识符问题的有趣背景信息可在 Unicode 标准附录 31《Unicode 标识符和模式语法》中找到。这展示了 Python 中“标识符是什么?”的问题如何融入其他编程语言和世界各地使用的各种自然语言的更广泛背景中。
在 Python 中,标识符有一组小的起始字符;这些字符的选择是为了允许词法扫描器确定可以跟随的字符类型。如果标识符以数字开头,那么区分标识符和数字将会相当复杂。因此,标识符必须以字母或_开头。在初始字符之后,Python 允许标识符继续使用可能来自更大字符集的字符:字母、数字和_。
我们所说的“字母”或“数字”究竟是什么意思?在 Python 的早期版本中,这些术语由基于拉丁字母的 ASCII 字母表定义。使用 Unicode 意味着这些术语现在有更包容的定义。
Python 定义的标识符起始字符属于以下 Unicode 类别:大写字母(Lu),小写字母(Ll),标题字母(Lt),修饰字母(Lm),其他字母(Lo),和字母数字(Nl)。Python 还包括Other_ID_Start类别中的小字符集。这些类别定义的字符集很大。例如,a-z和A-Z范围内的拉丁字母属于这个集合。当编写更多数学导向的程序时,希腊字母α-ω和A-Ω也可以用作标识符起始字符。我们可以这样写:
>>> π = 355/113
这将表达式的结果赋值给变量,π。一些程序员发现他们的操作系统键盘界面使得使用非单一国家字母的字母难以使用;因此,他们建议专注于拉丁字母进行编程。
标识符可以继续使用前一段中定义的任何字母,下划线字符,以及以下类别的字符:非间距标记(Mn),间距组合标记(Mc),十进制数字(Nd),和连接标点(Pc)。这允许我们包括普通的十进制数字以及其他“组合”标记,这些标记会修改前面的字符。例如:
>>> =p_2+0.5*p_1
这显示了字符希腊小写字母 PI后面跟着组合重音符号来创建一个“pi-hat”变量,
。对于一些开发者来说,这可能难以输入,但它也可能很好地与使用这种符号组合的群体基因组公式相匹配。例如,遗传下降估计器使用
。之前显示的表达式涉及两个其他变量,p_2和p_1,它们使用更常见的拉丁字母_和数字。
注意,以__(两个下划线)开头和结尾的变量名被 Python 保留用于特殊目的。例如,我们有全局变量如__name__、__debug__和__file__,这些变量在脚本开始运行时被设置。
我们的程序没有必要创建以__开头和结尾的新名称。我们没有被禁止创建这样的变量,但任何我们可能采用的名称都可能被 Python 的一些内部特性使用。
小贴士
最好假设所有以__(双下划线)开头和结尾的名称都被 Python 保留并执行特殊操作。即使名称在当前版本中没有使用,这并不意味着在未来的版本中不会使用。
多重赋值
我们在第二章简单数据类型中探讨了元组。使用元组的一个重要原因是有固定数量的项目。由于元组是一种序列,我们可以使用数字索引来引用元组内的项目。
考虑以下 RGB 三元组:
>>> brick_red = (203, 65, 84)
我们可以使用brick_red[0]来获取这个三元组的红色元素。
我们也可以这样做:
>>> r, g, b = brick_red
>>> r
203
我们使用多重赋值将 RGB 三元组分解为三个单独的变量。
当左侧 = 的变量数量与右侧集合中的项目数量匹配时,这会起作用。当处理固定大小的元组时,这是一个容易保证的条件。
当处理如 list、set 或 dict 这样的可变集合时,这种赋值可能不会很好用。如果我们不能保证可变集合中元素的数量,我们可能会遇到 ValueError 异常,因为我们的集合与变量的数量不匹配。
注意,Python 的语法灵活性意味着我们也可以这样做:
>>> n, d = 355, 113
并非绝对必要将元组用 () 括起来。通常,在元组周围使用 () 是一种最佳实践。然而,在少数情况下,没有额外的括号,语句也是完全清晰的。
使用重复赋值
Python 允许我们编写这样的语句:a = b = 0。这必须谨慎使用,因为现在两个变量共享一个对象。当处理像数字、字符串和元组这样的不可变对象时,多个变量共享对公共对象的引用。
当我们在第六章中查看可变对象时,更复杂的数据类型,我们会看到这种重复赋值可能成为混淆的来源。虽然这种赋值是合法的,但它必须仅用于像数字、字符串或元组这样的不可变对象。
使用头部,*尾部赋值
当处理序列时,有一些算法通过将序列的头部与序列的其余部分分离来工作。我们可以通过赋值语句的变体来做这件事。我们喜欢称这种赋值语句为 head, *tail = 赋值语句。
假设我们有一个包含值的输入字符串,类似于这样:
>>> line = "255 73 108 Radical Red"
>>> line.split()
['255', '73', '108', 'Radical', 'Red']
我们使用 line.split() 将字符串分割成空格分隔的单词。在这种情况下,列表的头部是红色、绿色和蓝色元素的第一个三个字段。尾部是所有剩余的字段,即解析成单独单词的名字。
我们可以使用 head, *tail = 赋值来从剩余的文件中分离出前三个字段。
它看起来像这样:
>>> r, g, b, *name = line.split()
>>> g
'73'
>>> name
['Radical', 'Red']
我们已经将前三个项目分配给了三个单独的变量,r、g 和 b。* 表示所有剩余的项目都将收集到一个单独的变量 name 中。
我们可以使用 join() 方法,以空格作为分隔字符串来重建原始名称:
>>> " ".join(name)
'Radical Red'
我们使用空格将名为 name 的序列的元素连接起来。这将重建原始颜色名称作为一个单独的字符串,而不是一个单词列表。
增量赋值
增量赋值语句结合了一个运算符和赋值。一个常见的例子是:
a += 1
这等价于
a = a + 1
当与不可变对象(数字、字符串和元组)一起工作时,扩展赋值的概念是语法糖。它允许我们只更新一次变量。语句a += 1始终创建一个新的数字对象,并用新的数字对象替换a的值。
任何运算符都可以与赋值操作结合使用。这意味着+=、-=、*=、/=、//=、%=、**=、>>=、<<=、&=、^=和|=都是赋值运算符。我们可以看到使用+=进行求和和使用*=进行乘积之间的明显平行关系。
在可变对象的情况下,这种扩展赋值可以具有特殊的意义。当我们查看第六章更复杂的数据类型中的列表对象时,我们将看到我们如何向列表对象追加一个项目。以下是一个前瞻性的例子:
>>> some_list = [1, 1, 2, 3]
这将一个列表对象,一个由项目组成的可变长度序列,赋值给变量some_list。
我们可以使用扩展赋值语句来更新这个列表对象:
>>> some_list += [5]
>>> some_list
[1, 1, 2, 3, 5]
在这种情况下,我们实际上是在修改单个列表对象,通过从另一个列表实例中扩展它来改变其内部状态。现有的对象被更新;这不会创建一个新的对象。它相当于使用extend()方法:
>>> some_list.extend( [8] )
>>> some_list
[1, 1, 2, 3, 5, 8]
我们已经第二次修改了列表对象,通过从另一个单元素列表对象中添加项目来扩展它。
这个对列表对象的优化是我们将在第六章更复杂的数据类型中探讨的内容。
input()函数
对于简单的应用程序,可以使用input()函数从用户那里收集输入。该函数写入提示并接受输入。返回的值是一个字符串。我们可以在脚本文件中使用如下方式:
c= float(input("Temperature, C: "))
print("f =", 32+9*c/5)
这将在控制台上写入一个简单的提示,并接受一个字符串作为输入。如果可能,字符串值将被转换为浮点数。如果字符串不是一个有效的数字,float()函数将引发异常。这将然后打印一行输出。
这是我们运行它时的样子:
MacBookPro-SLott:Code slott$ python3 Chapter_4/ex_1.py
Temperature, C: 11
f = 51.8
我们已经突出显示了命令,这是在操作系统 shell 提示符之后输入的。脚本文件中的语句,作为命令的一部分命名,将按顺序执行。
我们输入 Python 的11也被突出显示,以展示input()函数如何支持简单的交互。
input()函数只返回一个 Unicode 字符串。我们的脚本负责任何进一步的解析、验证或转换。
当处理简单的控制台应用程序时,有一些额外的库可能很有帮助。有一个getpass模块,它通过抑制控制台输入的默认字符回显来帮助获取密码。这作为参数文件中的明文密码或命令行上提供的密码的替代方案是非常推荐的。
我们可以包含readline模块,以提供全面的输入历史记录,这使得交互式用户更容易恢复以前的输入。此外,rlcompleter模块可以用来提供自动完成功能,这样用户只需输入部分命令。
此外,Python 可以包含 Linux curses库的实现,用于构建丰富的字符用户界面(CUI)应用程序。这有时用于在控制台上提供彩色输出,这可以使复杂的日志更容易阅读。
Python 被用于各种应用程序环境中。例如,在构建 Web 服务器时,控制台或命令行输入的想法是完全不合适的。同样,input()函数也不会是 GUI 应用程序的一部分。
Python 语言概念
在查看后续章节中的更复杂示例之前,我们将介绍 Python 语言的一些核心概念。这些核心概念中的第一个是 Python 中的一切都是对象。几种流行的语言有原始类型,这些类型逃避了语言面向对象的本性。Python 没有这个特性。即使是简单的整数也是对象,具有定义良好的方法。
由于一切都是对象,我们确保了没有特殊情况的一致行为。在某些语言中,==运算符对原始类型和对象的工作方式不同。Python 缺乏这种分歧行为。所有内置类都一致地实现了==运算符;除非我们做出特定的(并且病态的)实现选择,我们的类也将保持一致的行为。
当与字符串一起工作时,这种一致性尤其令人愉快。在 Python 中,我们总是使用类似txt.lower() = "hours"的方式来比较字符串的相等性。这将使得txt.lower()的值与字面量"hours"之间的预期逐字符比较。
不太常见的是,我们可以使用is比较运算符来检查两个变量是否引用了同一个底层对象。这通常用于将变量与None对象进行比较。我们使用is None是因为None对象是一个正确的单例;只能有一个None实例。我们将在第五章中再次探讨这一点,逻辑、比较和条件。
对象类型与变量声明
在 Python 中,我们根据类型对处理进行通用指定。我们可以编写一系列语句,隐含地理解应该使用浮点值。我们可以通过显式的float()转换函数将此形式化到一定程度。
在某些语言中,每个变量都有一个静态定义的类型。只有具有指定类型的对象才能分配给该变量。
与静态定义变量的语言相比,Python 变量可以理解为附加到对象上的名称。我们可以将名称附加到任何类的任何对象上。我们不会为变量静态声明一个狭窄的允许类型范围。
Python 允许我们通过将对象分配给多个变量来为同一个对象分配多个名称。例如,当我们评估一个函数时,函数参数变量名会被分配给参数对象。(我们将在第七章中更深入地探讨这一点,基本函数定义。)这意味着每个对象可能有两个变量指向它:一个在函数内部的参数变量,另一个在函数外部的变量。
我们可以使用内部的 id() 函数来查看两个变量是否指向相同的底层对象:
>>> a = "string"
>>> b = a
>>> id(a)
4301974472
>>> id(b)
4301974472
从这个例子中,我们可以看出 Python 变量 a 和 b 指向的是底层对象,而不是对象的副本。
在对象复制必要时,我们必须显式地进行。具体细节根据类的通用类型而异。例如,序列可以通过创建包含整个序列的切片来简单地克隆。一些类提供了 copy() 方法。对象也可以通过 copy 库中的函数进行克隆。
变量缺乏固定的类型声明有几种后果:
-
引入变量以分解复杂表达式是微不足道的。这里有一个复杂的表达式:
a = some_function( some_complex_function( another_function( b ) ) ) -
我们可以通过提取子表达式并将它们分配给变量来简单地重写这一点:
af = another_function(b) scf = some_complex_function(af) a = some_function(scf)我们已经提取了每个子表达式并将它们分配给单独的变量。我们永远不需要知道中间结果类型是什么。
-
所有算法都是通用的。当我们运行脚本时,我们将通用的 Python 代码应用于具体对象。我们这个绑定的典范例子基于数字塔。我们可以将相同的表达式
32+9*c/5应用到complex、float、int、Decimal和Fraction等类的对象上。所有这些类都提供了必要的运算符实现。然而,字符串对象不会实现所有所需的算术运算,并且无法工作。同样,我们可以对包括list、str、bytes和tuple在内的广泛序列类执行类似head, *tail = sequence的语句。然而,如果我们将一个数值赋给名为sequence的变量,该语句将无法工作。
避免声明具有静态类型的变量是一种极大的简化。我们可以按需引入变量。我们可以编写清晰、简单的通用软件,并将其留给 Python 运行时处理来确定运行时对象是否具有运算符和方法所需的实现。
避免在命名变量时产生混淆
没有变量声明,如果我们使用模糊、通用的变量,可能会创建出令人困惑的程序。一个像list_of_items这样的模糊名称的变量可能在较长的语句序列中被多次使用。当然,更糟糕的是像t或temp这样的变量名称。
小贴士
尽可能具体地命名变量。避免模糊、通用的名称。
变量名过度使用的一个方面是“较长的”语句序列的概念。如果一个函数的主体如此之长,以至于通用命名的变量可能会意外地被重复使用,那么函数的大小就变成了一个问题。没有任何一段 Python 代码应该如此之长,以至于其中的变量会让人困惑。
小贴士
保持代码序列简短且专注。避免可能错误地重复使用变量的长代码序列。
变量的命名要简单明了。在 Python 中,使用匈牙利命名法来装饰变量名以添加类型信息被认为是可鄙的。匈牙利命名法的原始概念是在变量名前放置几个字符作为前缀,以指示类型。在 Python 中,我们不使用前缀来命名变量lst_str_names,以表明该变量指的是字符串值的列表。
由于 Python 代码是通用编写的,一个编写良好的函数可以应用于许多不同的数据类型。如果我们试图在变量名中编码数据类型信息,我们实际上可能是在制造混乱:算法可能适用于变量名中未明确声明的类型。
在某些情况下,我们需要区分一组项目和单个项目。我们可能有一个name_list和一个单独的name。或者,当我们使用生成器函数时,我们可能有一个name_iter和一个单独的name。这样的小型、清晰的命名约定比详尽的误导性匈牙利命名法要好。
小贴士
避免在变量名中使用复杂的匈牙利命名法。
在更复杂的程序中,我们可能有一个将整数键映射到与这些键关联的集合的字典;每个集合可能包含一系列单独的字符串。用匈牙利前缀或后缀来总结这一点是困难的。我们是否想要尝试将其称为map_int_set_str_something?
展望到第七章,基本函数定义和第十一章,类定义,我们经常在函数、类和模块中使用docstring注释来捕获适合函数的结构细节。我们甚至可以在docstring注释中包含测试用例;测试用例可能是描述数据的最清晰和最精确的方式。
小贴士
在允许的每个上下文中编写docstring注释:函数、类、模块和包。
Python 使用变量的一个后果是我们依赖于单元测试用例来确保结果既符合预期的类型,又正确无误。在静态类型变量语言中工作的程序员非常清楚,单元测试用例对于正确性至关重要,即使编译器对所有变量声明进行类型检查。在 Python 中,测试用例与具有静态类型检查的语言中的测试用例一样重要。如果需要明确函数或类的意图,我们可以在测试用例中包含类型检查。
小贴士
编写单元测试;使用unittest模块、doctest模块或两者都使用。
通过引用计数进行垃圾回收
我们已经看到表达式如何创建新的对象。即使是像2**2024这样简单的东西也会创建一个新的整数对象。这些对象会发生什么?什么时候我们会耗尽内存?
当我们进行类似这样的操作时,Python 使用引用计数来确定对象被使用的次数:
>>> 2**2024
192624...497216
结果对象是一个非常大的整数;它被自动赋值给变量_。显示为192624...497216的对象只有一个引用;这使它在内存中保持活跃。
当我们这样做时,接下来:
>>> 2**2025
385248...994432
我们得到一个新的对象,并将其赋值给变量_。之前赋值给_的大整数值不再有任何引用。由于它不再被使用,它是垃圾,它占用的内存可以被重用。
每次我们将对象赋值给变量时,引用计数增加一个。每次变量的值被重新赋值时,不再使用的先前对象的引用计数减少一个。
当一个变量不再需要时,该变量将被移除,并且该变量所引用的对象的引用计数也会减少一个。
变量属于命名空间。我们的大部分早期示例使用了全局命名空间。在第七章,基本函数定义中,我们将看到局部命名空间。总结一下:当删除命名空间时,该命名空间中的所有变量都将被删除,并且所有对象引用的计数都减少一个。
小贴士
当对象的引用数达到零时,该对象不再需要。该对象占用的内存可以被回收。
我们可以轻松地创建两个相互引用的复杂对象。在这些类型的循环引用存在的情况下,当然,计数永远不会达到零。对象可能永远不会从内存中移除。我们可以使用gc模块来了解更多信息。
在我们必须有相互引用的对象的情况下,我们需要利用weakref模块。此模块提供对象之间的引用,这些引用不会干扰引用计数,允许当不再使用时,多个对象的大型数据结构优雅地从内存中消失。
很少使用的del语句
我们可以使用del语句手动删除变量。以下是一个示例:
>>> a = 2**2024
>>> del a
我们已经创建了一个整数对象,并将其赋值给变量 a。当我们移除这个变量时,这将减少整数对象的引用计数。现在,这个大整数所占用的内存可以被视为可回收。
这种事情很少做。Python 的普通引用计数几乎可以做我们需要的所有事情。通常最好不要浪费脑力试图微观管理内存分配。
Python 命名空间概念
我们已经看到了 Python 命名空间的两个应用。当我们使用 >>> 提示符赋值变量时,我们是在将变量引入全局命名空间。当我们导入一个模块时,该模块在全局命名空间内创建自己的命名空间。
正因如此,我们才能使用像 math.sqrt() 这样的合格名称来引用模块命名空间内的对象。
当我们查看函数和类定义时,我们会看到命名空间有额外的用途。特别是,当评估一个函数或类方法时,会创建一个局部命名空间,所有变量都是该局部命名空间的一部分。当函数评估完成(由于显式的 return 语句或缩进块的末尾)时,局部命名空间会被丢弃,移除所有局部变量,并减少分配给这些局部变量的所有对象的引用计数。
此外,types 模块还包括 SimpleNamespace 类。这个类的实例允许我们在不进行正式类定义的情况下构建复杂对象。以下是一个例子:
>>> from types import SimpleNamespace
>>> red_violet= SimpleNamespace(red=192, green=68, blue=143)
>>> red_violet
namespace(blue=143, green=68, red=192)
>>> red_violet.blue
143
我们已经导入了 SimpleNamespace 类。我们创建了该类的实例,并分配了三个局部变量,red、green 和 blue,它们都是新 SimpleNamespace 对象的一部分。当我们整体检查这个对象时,我们会看到它有三个内部变量。
我们可以使用像 red_violet.blue 这样的语法来查看 red_violet 命名空间内的 blue 变量。
argparse 模块被命令行程序用于解析命令行参数。此模块还包含一个 Namespace 类定义。Namespace 的一个实例用于收集从命令行解析的各种参数。应用程序可以在 Namespace 对象中设置额外的变量来处理特别复杂的解析和配置问题。
全局变量和局部变量
当我们在表达式中使用一个变量名时,Python 会搜索两个命名空间来解析名称并定位它所引用的对象。首先,它会检查局部命名空间。如果找不到该名称,它将检查全局命名空间。这个两步搜索将确保在函数或类方法内部使用的局部变量在使用具有相同名称的全局变量之前被使用。
当使用 REPL 的 >>> 提示符工作时,我们只能创建和使用全局变量。更多示例将不得不等到 第七章,基本函数定义。
当我们在 >>> 提示符下使用 locals() 和 globals() 函数时,我们可以看到它们具有相同的结果。在 >>> 提示符下,以及在脚本文件的最高级别,局部命名空间是全局命名空间。然而,在评估函数时,函数是在一个独立的、局部命名空间中工作的。
摘要
我们已经探讨了如何将对象赋值给变量。我们探讨了简单的赋值语句,以及多重赋值和增强赋值。使用增强赋值,我们可以通过应用运算符和操作数来更新变量。这是一个方便的语法快捷方式。
我们还讨论了 input() 函数,这是一种基于用户输入创建新对象的方法。对于简单的命令行脚本来说,它非常方便。当然,更复杂的 GUI 将会有更复杂的输入机制。
命名空间的概念以及变量如何通过命名空间进行跟踪,这是 Python 的核心。当一个命名空间不再需要时,它会被丢弃,移除所有变量。这也会减少所有由变量引用的对象的引用计数。一旦一个对象的引用计数减少到零,该对象就可以从内存中移除。这是一种整洁且简单的方式来处理变量。
在第五章中,我们将探讨另一个基本的数据类型:布尔值。我们将探讨 Python 对布尔值的处理方法以及逻辑运算符 and、or、not 和 if-else。我们还将探讨各种比较运算符。
我们将探讨几种 Python 语句,包括 if-elif-else 语句、pass 语句和 assert 语句。这将使我们能够编写更复杂的脚本。
第五章:逻辑、比较和条件
我们对 Python 语言的探索始于表达式语句和赋值语句。我们可以使用print()函数将输出视为一个简单语句。我们可以使用input()函数在赋值语句中收集输入。为了有条件地处理数据,我们需要if语句。
为了查看if语句,我们需要查看布尔数据和布尔运算符。and、or、not和if-else布尔运算符具有“短路”行为:如果结果仅由左操作数定义,则不会评估右侧。这是这些逻辑运算符的一个重要特性。(if-else运算符正式称为布尔表达式,但它的行为像布尔运算符。)
我们还将查看比较运算符。比较是创建在if语句中用于选择语句组的布尔值的一种常见方式。
我们在这里介绍pass语句。这个语句什么都不做。它是一个占位符,当我们只需要一个空的语句组时使用。
assert语句可以用来证明在程序执行过程中的某个点上特定的逻辑条件是正确的。这可以澄清一个可能令人困惑的算法。它还可以作为一个方便的调试工具,当出现问题时使程序崩溃。
布尔数据和bool()函数
所有对象都可以映射到布尔值域:True和False。所有内置类都有这种映射定义。当我们定义自己的类时,我们需要考虑这个布尔映射作为一个设计特性。
内置类基于一个简单的原则:如果没有明显的数据,对象应该映射到False。否则,它应该映射到True。以下是一些详细的示例:
-
None对象映射到False。 -
对于所有各种类型的数字,零值映射到
False。所有非零值都是True。 -
对于所有集合(包括
str、bytes、tuple、list、dict、set等),空集合是False。非空集合是True。
我们可以使用bool()函数来查看对象和布尔值之间的映射:
>>> red_violet= (192, 68, 143)
>>> bool(red_violet)
True
>>> empty = ()
>>> type(empty)
<class 'tuple'>
>>> bool(empty)
False
我们创建了一个简单的序列,一个包含三个值的tuple,并将其赋值给red_violet变量。由于这是非空的,它映射到True。另一方面,赋值给empty变量的空tuple映射到False。
这个内置映射的一个重要后果是,任何对象都可以用在布尔构造中。展望未来,我们经常会看到具有这种习惯性模式的程序:
for input from some_file:
if not input.strip(): continue
这个例子的一些细节将不得不等到 第十章 文件、数据库、网络和上下文 中再讨论。这个例子的重要之处在于我们可以从文件中读取一行,使用 strip() 方法去除空白,并使用简单的布尔表达式来检查结果是否为空字符串。如果是空字符串,我们可以通过使用 continue 语句轻松忽略它。
这个结构之所以有效,是因为字符串映射到布尔值。空字符串映射到 False,这使得我们可以用非常简单而优雅的表达式来检查内容是否存在。
比较运算符
在 第二章 简单数据类型 中,我们探讨了六个基本比较运算符:<、>、==、!=、<= 和 >=。== 和 != 的最小值对于所有类都是默认定义的,这样我们就可以始终比较对象进行简单相等性比较。对于数值类型,排序运算符也是定义好的。此外,Python 的类型强制规则由数值类型实现,因此表达式 2 < 3.0 将会将 int 强制转换为 float。
对于序列,包括 str、bytes、tuple 和 list,两个操作数是逐项比较的。这通常会将字符串按字母顺序排序。这对于单词来说效果很好。它通常也会将元组按预期顺序排序。然而,对于类似数字的字符串,排序可能看起来有点奇怪。以下是一个例子:
>>> "11" < "2"
True
字符串 "11" 和 "2" 不是数字。它们只是字符。将这两个值想象成数字并希望 "11" 在 "2" 之后是一种常见的误解。如果这是期望的行为,我们需要使用 int() 函数将这些类似数字的字符串转换为正确的数字。
对于 set 对象,比较运算符映射到超集和子集关系。Python 的 < 运算符实现为真子集关系。<= 运算符实现为子集关系。我们将在 第六章 更复杂的数据类型 中详细探讨这一点。
对于其他类型,比较变得不那么有意义。映射之间的排序不是一个简单概念。我们如何对两个映射进行排序:是只比较键,只比较值,还是两者的组合?如果我们尝试同时比较键和值,缺失键的规则是什么?由于没有简单的答案,Python 没有为映射定义排序运算符。
对于数值塔外的类型,没有强制规则。相等性比较只是比较对象的 ID,以查看两个操作数是否引用了同一个对象。
通常,排序运算符不是默认实现的,并且会引发 TypeError 异常。这对于许多类来说是一个常见的期望。
如果我们尝试比较两个文件对象,我们应该比较哪个属性?大小?创建日期?为了避免混淆,比较运算符对于许多类并没有实现。
组合比较以简化逻辑
在某些情况下,我们可能需要检查一个值是否在给定的范围内。一种方便的语法简化是将排序比较组合成一个简化的表达式。我们可以有意义地写出这样的表达式:
5 > a >= 0
在这种类型的表达式中,Python 解释器将组合运算符解释为 5 > a 和 a >= 0。我们不必重复中间表达式 a 来将排序测试分解为两个二进制比较。
测试浮点值
浮点值的一个重要特性是它们只是近似值。我们可以轻松编写看似数学上精确的计算,但会产生看起来很奇怪的结果。具体例子可能因实现而异。以下是一个例子:
>>> a=1
>>> b=(a/105)*3*5*7
>>> a == b
False
>>> abs(a-b)
2.220446049250313e-16
在抽象数学意义上,(a/105)*3*5*7 必须等于 a 变量的原始值。然而,我们可以看到,由真正的除法运算符创建的浮点近似值有一个小的误差。在这种情况下,误差值大约是 2.22e-16,即 2**-52:在这次浮点操作链之后,52 位值的最不重要位是不正确的。
由于存在这些小的误差项,我们应该避免对浮点值进行简单的 == 测试。当两个值相差很小时,简单的相等测试往往会出现错误。
通常,我们应该使用 abs(a-b) < ε 而不是 a == b。我们可以将 ε 值设置得足够小,以便检测相等性。例如,如果我们打算显示一个有三位小数的值,就没有必要计算超过第 5 位小数。在这种情况下,ε=10e-5 可以用来定义浮点相等性的可接受公差。
小贴士
避免使用 float == float 比较;使用 abs(float-float) < ε 代替。
使用 is 运算符比较对象 ID
要确定两个变量是否实际上引用的是同一个对象,我们有一个特殊的比较运算符:is。这与稍微复杂一些的相等性测试不同。is 运算符是一个非常简单的测试,比较两个对象的内部标识符。
如果 a is b,那么 a == b 也必须为真,因为这两个变量引用的是同一个底层对象。然而,如果 a == b,那么 a is b 可能不一定为真。两个不同的对象可以具有相同的值。以下是一个使用浮点值的例子:
>>> a = 3.14
>>> b = 3.14
>>> a == b
True
>>> a is b
False
这个例子对于浮点对象来说工作得很好。我们可以看到,两个看似相等的对象实际上是不同的实例,它们代表相同的数值。
然而,这样的例子对于小整数值不起作用。对于狭窄范围的整数值,Python 倾向于重用一小批内部对象。这避免了普遍值的副本的激增。如果我们尝试设置a=1和b=1,我们会看到a is b:Python 重用了相同的对象。
通过一点实验,我们可以看到,-5 和 256 之间的数字确实重用了小整数。实现细节可能会有所不同。重要的是,一些不可变对象是从池中隐式分配的。
使用id()函数可以揭示对象身份。这显示了唯一的、内部的对象标识符。例如:
>>> id(a)
4298491200
>>> id(b)
4298491224
我们可以看到,这两个对象在值上恰好相等,但却是两个不同的对象。
相等性和对象哈希值
在 Python 中,相等性比较的一个重要部分是哈希值比较。哈希是一个总结更大、更复杂值的小整数。哈希值不应改变;可变对象不应提供哈希值。
我们打算收集到集合中或用作映射键的任何对象都必须提供哈希值和适当的相等性比较。我们之前看到的所有内置不可变类型——数字、tuple、str和bytes——都提供了这些方法的必要实现。我们将在第六章“更复杂的数据类型”中查看的内置可变类型,如list、set和dict,不提供哈希值,不能用作映射的键。
哈希函数将复杂值简化为小数。在 Python 中,哈希值通常使用 61 位。对于复杂对象,哈希值总结了整个对象。它可能是所有单个字节的和,计算
。它也可能是其他内部对象哈希值的和。比较哈希值比比较复杂对象中的每个单独项目要少得多。
对于不可变对象,哈希值只计算一次,将与对象本身一样不可变。对于可变对象,可以计算哈希值。但是,如果哈希值发生变化,则对象在集合中的行为或作为映射的键将不会很好。对于可变对象,变化的哈希值不是一个很好的主意。
当将项目放入集合中时,例如,Python 会使用哈希值进行快速相等性检查。如果哈希值不同,则底层对象必须不同,无需进行更多比较。然而,如果哈希值匹配,则必须使用详细的相等性测试来确定对象是否确实相等或只是偶然具有相同的哈希值。
在 Python 的一些实现中,你可以使用这种测试来查看两个不同的数字是否恰好具有相同的哈希值:
>>> hash(12)
12
>>> hash(12*2**61)
12
注意
实现可能会有所不同;这是 Mac OS X,v3.3.4:7ff62415e426,你的结果可能会有所不同。
如果我们尝试将这些两个值放入一个集合中,Python 将执行哈希检查以查看它们是否可能相等,然后进行详细比较以查看它们是否不相等。
逻辑运算符 - and、or、not、if-else
Python 为我们提供了四个逻辑运算符:and、or、not 和 if-else。这些运算符与布尔值一起使用以创建布尔结果。它们与我们在第二章中讨论的位运算符 &、|、^ 和 ~ 完全不同,简单数据类型。
and、or 和 not 运算符在所有编程语言中都很常见。它们符合布尔代数中广泛使用的定义。
if-else 布尔表达式有三个操作数。在中间,它使用布尔条件,但其他两个操作数可以是任何类型的对象。以下是一个示例:
selection = "yankee" if wind < 15 else "stays'l"
if-else 运算符在中间有一个布尔条件。在这个例子中,它是比较 wind < 15。如果条件为 True,则最左侧的表达式是结果,字符串 "yankee"。如果条件为 False,则最右侧的表达式是结果;这里,它是 "stays'l"。
逻辑运算符隐式地将 bool() 函数应用于其操作数。这意味着我们可以做如下事情:
valid= line and line[0] != "#"
and 表达式涉及两个布尔操作数。当 Python 隐式评估 bool(line) 时,非空行将是 True;空行将是 False。对于空行,valid 变量将是 False;如果 line[0] 不是 "#" 字符,则对于非空行,它也将是 False。
这种隐式使用 bool() 也意味着这是正确的:
>>> not 12
False
not 12 的值被评估为 not bool(12)。非零数值的 bool() 值是 True;因此,这个表达式的最终结果是 False。
短路(或非严格)评估
考虑以下内容:
>>> total= 0
>>> count= 0
>>> average = total != 0 and total/count
>>> average
False
发生了什么?或者,更准确地说,没有发生什么?为什么这不会引发 ZeroDivisionError 异常?前两个赋值语句并不令人惊讶;它们将零赋值给两个变量,total 和 count。然而,逻辑表达式有几个有趣的特点。首先,Python 按从左到右的顺序评估表达式。这意味着 total != 0 子表达式首先被评估。这个比较的结果是 False。
其次,也许更重要的是,and 运算符打破了严格的评估规则。如果左侧值等同于 False,则整体结果为 False。右侧根本不会进行评估。如果左侧值等同于 True,则结果简单地是右侧值。
这有时被称为短路评估规则。如果从左侧已知结果,就没有必要评估右侧。
结果不一定是布尔值;它只是 and 运算符给出的操作数之一。以下是一些示例:
>>> 0 and 12
0
>>> () and "non-false"
()
>>> 12 and ()
()
在第一个例子中,0 等同于 False,并且该对象是 and 操作符的整个结果。在第二个例子中,空元组 () 等同于 False;它是操作符的结果。
在第三个例子中,左侧,12,是非零的,因此,等同于 True。这意味着右侧必须被评估。右侧是 and 操作符的结果;在这种情况下,它是一个空元组,()。
or 操作符类似;如果左侧等同于 True,就没有理由评估右侧。我们可以使用这个特性来应用默认值。
我们可以编写如下表达式。
x = parameter or 42
如果 parameter 变量的值是一个 True 值,则 or 操作符的值将是等效于 True 的值。如果 parameter 变量的值不是一个 True 值(例如,它可能是 None),那么结果将是字面值 42。
我们当然也可以使用 if-else 操作符来做这件事。这里有一个例子:
x = 42 if parameter is None else parameter
如果 parameter 变量的值是 None 对象,则左侧操作数——字面值 42——是结果。如果 parameter 变量的值不是 None 对象,那么右侧操作符——parameter 变量的值——是结果。
if-elif-else 语句
我们进行条件处理的中心工具是 if 语句。这是一个由多个子句组成的复合语句。初始子句以 if 关键字开始。可以使用任意数量的 elif(代表“else if”)子句。每个这些子句都有一个条件表达式和一个缩进的语句序列。我们还可以在末尾添加一个单一的通配符 else 子句;它没有条件,但有一个语句序列。
最小的 if 语句,只有一个子句,可能看起来像这样:
if abs(a-b) < ε:
print("{a} \N{ALMOST EQUAL TO} {b}".format(a=a, b=b))
if 语句包含一个单独的表达式。如果表达式为 True,则执行语句序列。在这种情况下,语句序列是一个单独的表达式语句,使用 print() 函数。
else 子句可以用于简单的 if 语句。
if count == 0:
print("Insufficient Data")
else:
print("Mean = {0:.2f}".format(total/count))
在这种情况下,我们有两个条件。我们正式声明了 count == 0 条件用于一个 print() 函数。对于另一个 print() 函数,我们有一个未声明的条件。在这个简单的情况下,推断出隐含的条件相对容易。
添加 elif 子句
在某些情况下,我们可以将复杂的情况分解成一系列的情况。例如,我们可能有一些这样的条件:
if y % 400 == 0:
leap = True
elif y % 100 == 0:
leap = False
elif y % 4 == 0:
leap = True
else:
leap = False
我们在这里写了一个相当复杂的逻辑链。我们指定了四个不同的条件:
-
y是 400 的倍数,在这种情况下,leap变量将被设置为True。例如,公元 2000 年是闰年。 -
y是 100 的倍数(但不是 400 的倍数),在这种情况下,leap变量将被设置为False。公元 2100 年将不是闰年。 -
如果
y是 4 的倍数(并且不是 100 或 400 的倍数),则将leap变量设置为True。2016 年将是闰年。 -
如果
y不是 4、100 或 400 的倍数,则将leap变量设置为False。2015 年不是闰年。
由于 Python 以严格的顺序评估子句,每个elif子句都有一个隐含的“并且不是之前的任何子句”。这意味着每个elif中的条件可以非常简洁地编写,但它们也需要之前的子句作为其上下文的一部分。
随着 elif 子句数量的增加,引入微妙逻辑错误的可能性也增加。这可能导致else子句的隐含条件很难正确推断。因此,一些程序包含类似以下逻辑:
if y % 400 == 0:
leap = True
elif y % 400 != 0 and y % 100 == 0:
leap = False
elif y % 400 != 0 and y % 100 != 0 and y % 4 == 0:
leap = True
elif y % 400 != 0 and y % 100 != 0 and y % 4 != 0:
leap = False
else:
raise Exception("Logic Error")
此示例显示了每个隐含条件都完全写出。它还显示了使用else子句在不太可能的情况下引发异常,即条件被忽略或表述错误。一些开发者认为这纯粹是浪费时间。其他人认为,任何仅仅是暗示的东西都可能是错误的一个来源,并更喜欢明确地陈述条件。
对于简单的条件集,这可能是一种不必要的过度设计。在其他情况下,这种冗长的变化可能更可靠,因为它消除了所有假设和隐含条件。
将pass语句用作占位符
在某些算法中,else子句可能比if子句更重要。这种情况发生在算法默认设计为处理一组特定的条件——即“快乐路径”时。所有其他非“快乐路径”的条件都需要一些特殊处理。
当默认条件相对清晰且易于编写,但不需要对该条件进行处理时,Python 中就存在一个语法问题。有趣的处理属于else子句,但我们没有为初始的if子句编写真正的代码。以下是一个典型的模式,展示了无效的语法:
if happy_path(x):
# nothing special required
else:
some_special_processing(x)
# Processing Continues
happy_path()条件确认默认处理将正常工作。当这是真的时,实际上不需要执行任何处理。由于我们不希望做任何事情,我们在if子句中写什么?
上述代码是无效的 Python 代码。我们无法在if子句中有一个空代码块。由于我们无法编写上述代码,我们必须找到可以工作的替代语法。
一个明显的选择是取反happy_path()条件的逻辑。我们可以简单地使用not运算符。
if not happy_path(x):
some_special_processing(x)
这会产生预期的效果。然而,not运算符可能难以察觉。当happy_path()条件是一个复杂的逻辑表达式时,额外的not可能会令人困惑。
这就是 Python 的pass语句可能比not运算符更清晰的地方。它看起来会是这样:
if happy_path(x):
pass # nothing special required
else:
some_special_processing(x)
# Processing Continues
我们在if子句中的语法空缺处填充了一个“什么也不做”的语句。我们使用pass在if子句中创建了一个合适的代码块。我们保留了注释,因为这种信息可能是有帮助的。
pass语句还有一些其他用途。我们将在第十一章类定义中探讨它们。
断言语句
assert语句是if语句的一种高度专业化的形式。这个语句确认一个给定的条件是真实的。如果条件不是真实的,assert语句将抛出一个异常。在最简单的情况下,脚本停止运行,因为异常没有被我们的编程处理。
它看起来是这样的:
assert a > b >= 0
我们已经使用assert语句来提供在 Python 脚本、函数或方法中某个特定点必须为真的变量之间关系的文档。如果条件a > b >= 0为假,则抛出AssertionError异常。
我们可以通过向assert语句提供第二个参数来自定义抛出的异常:
assert a > b >= 0, "a={0} and b={1}".format(a, b)
我们提供了一个包含关于断言信息的字符串。这个字符串将是创建的异常对象的参数。
异常有两个有趣的特点。首先,它是一个带有我们可以设置的参数的对象。其次,更重要的是,它中断了语句的正常顺序执行。可以编写try/except语句来处理异常:执行在try子句中停止,并在匹配异常的except子句中开始。如果没有匹配异常的try语句,抛出异常将停止程序。我们将在第九章异常中详细探讨异常。
注意,assert语句可以被禁用。当我们使用带有-O,优化命令行选项运行 Python3 时,assert语句不会包含在内置的 Python 字节码中。
None对象逻辑
在第二章简单数据类型中,我们介绍了None对象。它是一个独特的不变对象,通常用于表示一个参数应该有一个默认值或输入不可用。一些语言有一个特殊的空对象或空指针,其语义与 Python 的None对象类似。
None对象没有定义算术运算符。它等同于False。==和!=运算符通常为None定义。然而,这些运算符并不总是合适的,因为其他对象可能表现出类似的行为。
通常,当我们试图确定一个变量是否设置为None时,我们会使用is比较。==测试可以被实现__eq__特殊方法的类重新定义;is测试不能被覆盖。
小贴士
因为==可以被重新实现,所以始终使用is None而不是== None。
由于 bool(None) == False,我们可以在 if 条件中使用可能为 None 的变量。尽管如此,我们通常应该使用 is None 或 is not None 来更清晰地表达。
这里有一个例子:
if not a:
print("a could be None")
这依赖于 Python 隐式评估 bool(a) 的方式,以查看变量 a 的值是否等同于 True。通常最好是非常明确的:
if a is None:
print("a is None")
这表明我们正在将变量 a 的值与 None 对象进行匹配。
概述
我们已经仔细研究了 Python 的布尔数据类型,它只有两个值(True 和 False)和四个运算符:and、or、not 和 if-else。布尔运算符和 if 语句都会隐式地将值转换为布尔值。这意味着非空字符串的行为与 True 值相同。
我们已经研究了比较运算符。这些运算符与其他对象一起工作,并创建布尔结果。
在数值比较的情况下,数值转换规则被用来允许我们比较 float 与 int 值,而无需编写显式的转换。对于字符串或元组值,我们已经看到项目是按顺序比较的。
我们还看到了逻辑运算符 or 和 and 在评估它们的操作数方面并不严格。如果 and 的左侧是 False,则不会评估右侧。同样,如果 or 的左侧是 True,则不会评估右侧。
我们研究了多种 Python 语句,包括 if-elif-else 语句、pass 语句和 assert 语句。这些语句允许我们编写更复杂的脚本。
在 第六章,更复杂的数据类型 中,我们将探讨 list、set 和 dict 集合。我们将看到如何使用 for 语句处理给定集合中的所有项目。这将使我们能够编写相当复杂的脚本。
第六章。更复杂的数据类型
我们将探讨许多内置和标准库集合类型。这些集合提供了比简单元组集合更多的功能。我们将探讨for和while语句,这些语句允许我们处理集合的各个项。
我们将探讨一些我们可以用来处理数据集合的函数;这些包括map()、filter()和functools.reduce()函数。通过使用这些函数,我们不需要编写显式的for语句来处理集合。我们还将探讨更具体的归约类型,如max()、min()、len()和sum()。
我们还将探讨break和continue语句;这些语句修改for或while循环,允许跳过项或在循环处理所有项之前退出。这是集合处理语句语义的一个基本变化。
可变性和不可变性是理解对象行为的一部分。本章中的内置类型都是可变的。这与不可变对象(如字符串和元组)的行为方式大不相同。
可变性和不可变性之间的区别
在第二章中,我们探讨了不可变性问题。这是 Python 对象的一个重要特性。我们需要在第七章中探讨更多关于可变性的方面,基本函数定义。我们将在第十一章中探讨如何创建我们自己的可变类,类定义。
我们已经看到,Python 的各种类包括创建可变对象和不可变对象的类。不可变类包括所有的数字类、字符串、字节和元组。tuple (247, 83, 148)对象不能被更改:我们无法将新值赋给索引为 1 的项。
tuple对象具有Sequence的结构:我们可以根据它们的位位置提取项。然而,我们无法改变tuple对象的内部状态。
list也是Sequence类的子类。然而,我们可以改变list对象的状态,而无需创建新的list实例。
Sequence和MutableSequence的抽象基类定义在collections.abc模块中。该模块的文档显示了各种复杂类型之间的关系。
虽然list和tuple的一些功能相似,但它们针对不同的使用场景。不可变性的好处是简单性、减少存储需求以及某些类型处理的高性能。可变性的好处是单个对象可以经历内部状态的变化。
使用列表集合
Python 的list集合是其内置的可变序列。我们可以通过使用简单的显示来轻松创建列表对象,该显示仅提供用[]括起来的表达式。它看起来是这样的:
fib_list = [1, 1, 3, 5, 8]
与元组一样,项目通过它们在list集合中的位置来识别。位置从左开始编号,从零开始。位置也可以用负数从右编号。列表中的最后一个值在位置-1,倒数第二个值在位置-2。
小贴士
索引值从零开始。索引位置 0 是第一个项目。索引值可以用负数反向进行。列表中的最后一个值在位置-1,倒数第二个值在位置-2。
我们也可以使用list()函数来创建列表。这将把许多种类的集合转换为list对象。不带参数使用list()会创建一个空的list,就像[]一样。由于list()函数在将集合转换为list对象方面非常灵活,我们将在后面的章节中更多地使用它。
我们可以使用append()等方法更新list集合:
fib_list.append(fib_list[-2] + fib_list[-1])
在这个例子中,fib_list[-1]的值是列表中的最后一个元素,而fib_list[-2]是倒数第二个值。这个表达式创建了一个新数字,可以追加到fib_list对象中。
我们可以使用索引来操作列表中的单个元素,例如前一个示例中所示。[]中的值必须是一个整数,用于标识列表中的项。它看起来是这样的:
>>> fib_list[2]
3
位置二的项(列表中的第三项)的值为 3。
我们可以使用切片符号提取子列表。切片使用[]中的多部分值。切片的结果始终是从原始列表对象构建的列表。有几种指定切片的方法,我们将展示一些示例:
>>> fib_list[2:5]
[3, 5, 8]
>>> fib_list[2:]
[3, 5, 8, 13]
>>> fib_list[:-1]
[1, 1, 3, 5, 8]
第一个切片[2:5]从索引 2 开始,并在索引 5 之前停止。这意味着 2、3 和 4 的索引值被从原始列表中切片出来。由于列表从零开始索引,索引 2 是列表中的第三个位置。将切片视为“半开”区间是至关重要的。
小贴士
大多数 Python 使用“半开”区间。
当我们编写切片表达式[a:b]时,位置a是包含的,而位置b是不包含的。这个切片指定了所有满足
的索引值i。切片中有
个值。
第二个切片[2:]省略了结束位置,这意味着它从索引 2 开始,并包括列表末尾的所有项目。
第三个切片[:-1]省略了起始位置,这意味着它从索引 0 开始。结束位置给出为-1,即列表中的最后一个项目。由于切片在给定的最终位置之前停止,这个切片将省略列表中的最后一个项目。
我们可以使用[:]作为一个退化情况,其中起始和结束都被省略。这在制作整个list对象的浅拷贝时非常有效。
切片可以扩展到包括第三个参数。这允许我们指定一个起始、停止和一个步长值。我们可以这样做:
>>> fib_list[::2]
[1, 3, 8]
>>> fib_list[1::2]
[1, 5, 13]
在第一个例子中,省略了起始和停止,所以我们将使用整个列表。步长值是 2,所以我们将使用偶数索引提取一个新的列表:0、2、4、……等等。
在第二个例子中,我们提供了一个起始和步长值。这将从索引 1 开始,每次增加 2。它将提取由奇数索引组成的列表:1、3、5、……等等。
我们可以使用负步长值以相反的顺序遍历列表。这可能会令人困惑,但它工作得非常好。
列表对象有几个运算符,包括+和*。我们还将查看我们可以使用的各种列表赋值语句,这些语句涉及赋值语句左侧的切片表达式。这些可以通过改变一些值来改变列表。
使用列表运算符
我们可以使用+运算符连接两个列表对象,例如[1, 1] + [2, 3, 5]。如果我们想扩展列表,我们可以使用这个扩展赋值语句:
>>> fib_list += [ fib_list[-2] + fib_list[-1] ]
注意,我们不得不创建一个单例list集合,这样+运算符才能将新的list连接到现有的list上。
由于list对象是可变的,这个+=赋值将更新list对象;它通过添加新的list集合来扩展。这与tuple形成对比,在tuple中必须从两个原始tuple创建一个新的tuple,并将其赋给变量。
在第五章中,我们注意到像list和tuple这样的序列是逐项比较的。这意味着[1, 1, 2] < [1, 2]将会是True。
列表和其他序列也支持in运算符。我们可以询问特定值是否在list集合中。我们还可以确认给定值是否不在list集合中。这些是简单的布尔表达式,看起来像这样:
>>> 13 in fib_list
True
>>> 12 not in fib_list
True
我们使用了in运算符来确认值 13 在fib_list变量中,而值 12 不在那个list对象中。
使用下标修改列表
我们可以使用赋值语句左侧的订阅或切片来更改list集合中的项目。订阅使用[]和单个整数值来标识列表中的项目。我们可以这样替换一个项目:
fib_list[0]= 1
我们将用值 1 替换索引 0(第一个项目)。如果我们提到一个不在列表中的索引值,将会引发IndexError。
我们可以用不同的列表替换列表的任何简单切片。替换列表不必与原列表大小相同。实际上,它可以是空列表,这将有效地从列表中删除项目。以下是一个通过提供较短的替换来修改长切片的例子:
fib_list[2:5]= [3]
我们指定了一个包含三个项目——索引值 2、3 和 4——的切片,并用只有一个项目的列表替换了这些项目。结果列表将看起来像这样:
[1, 1, 3, 13]
位置 0 和 1 保持不变。同样,位置从 5 到原始列表的末尾也保持不变。
我们可以替换一个扩展切片——包括一个步长值——但替换的大小必须相同。如果我们没有提供正确的替换值数量,我们将得到一个ValueError异常。
使用方法函数修改列表
我们可以使用大量方法函数中的任何一个来修改list对象。列表的修改方法几乎总是返回None值。除了pop()方法外,修改方法不返回有意义的值。
还有提供有关列表信息的方法函数;这些函数必须返回一个值。我们将查看只读方法函数。
列表的修改方法包括append()、clear()、extend()、insert()、pop()、remove()、reverse()和sort()。以下是一些示例:
>>> fib_list
[1, 1, 3, 5, 8, 13]
>>> fib_list.extend( [21, 34] )
>>> fib_list
[1, 1, 3, 5, 8, 13, 21, 34]
>>> fib_list.insert(0, 0)
>>> fib_list
[0, 1, 1, 3, 5, 8, 13, 21, 34]
>>> fib_list.remove(34)
>>> fib_list
[0, 1, 1, 3, 5, 8, 13, 21]
>>> fib_list.pop()
21
>>> fib_list.pop(0)
0
我们展示了包含六个项的初始列表。我们使用一个包含两个额外项的第二个列表扩展了列表,即[21, 34]。结果是两个原始列表组成的单个列表。
insert()方法有一个值和一个位置。在这个例子中,两者都是零。当我们使用help(list.insert)时,我们看到索引位置是第一个参数值。要插入该位置之前的位置的值作为第二个参数值提供。
当我们从列表中移除一个项时,我们提供要移除的项值。对于非常大的列表,这可能涉及大量的时间来搜索所需的项。
pop()方法做两件事。它通过位置移除一个项,并将该项作为结果值返回。默认位置是最后一个项,-1。我们也可以使用索引位置 0 从列表的开始移除项。
我们也可以使用del语句从列表中移除项。语句del fib_list[0]将从列表中移除第一个项。
我们还没有展示reverse()和sort()方法,这些方法会改变列表中项的顺序。sort()方法可能比这些方法更复杂。我们将在第八章更高级的功能中探讨排序。
我们没有给出clear()方法的示例。这个方法会从列表中移除所有项。
注意,除了pop()之外,我们必须明确请求显示fib_list对象,才能看到 Python 的 REPL 的任何输出。这些修改方法只返回None值。看到a = a.append(x)是一个常见的错误;这个语句总是将变量a设置为None。
访问列表
如前所述,我们可以使用索引或切片来访问列表。索引给我们一个单独的项。另一方面,切片会创建原始列表中项的浅拷贝。
访问列表的方法函数包括count()、index()和copy()。以下是一些示例,以展示这些函数的工作方式:
>>> fib_list.count(1)
2
>>> fib_list.index(5)
3
count()方法计算所有与给定值相等的项的数量。在这种情况下,列表中有两个值等于 1。如果给定值在列表中找不到,计数将为零。
index()方法定位给定的项值,并返回该值在列表中的索引位置。如果该值不存在,将引发ValueError异常。
列表对象的copy()方法与空切片做同样的事情。表达式fib_list[:]和fib_list.copy()都是原始列表的副本。
使用集合函数
Python 提供了一系列与任何类型的集合一起工作的函数。这些包括sorted()、max()、min()和sum()。我们还有一些高阶函数,如map()、filter()以及整个itertools模块。我们将在第八章中讨论更多的高阶函数,更高级的函数。
sorted()函数从集合中返回一个排序后的列表。在排序过程中,它将给定的集合转换为list集合。如果集合没有定义适当的迭代方法,则无法使用此函数轻松排序。
max()和min()函数将集合简化为单个值:集合中的最大值或最小值。这种简化假设项目可以有意义地比较。考虑一个包含混合值的tuple:
((255, 73, 108), 'Radical Red')
我们无法有意义地评估这种混合值集合的max()或min()。函数将被迫比较一个数字元组与一个字符串。这将引发TypeError异常。
sum()函数将数字集合简化为单个值。它可以用于几乎任何实现了+操作符的对象;我们可以合并一个列表的列表来创建一个非常长的列表。以下是一个使用这些集合函数与简单的set对象一起使用的示例:
>>> some_set = {7, 2, 3, 5}
>>> sorted(some_set)
[2, 3, 5, 7]
>>> max(some_set)
7
>>> min(some_set)
2
>>> sum(some_set)
17
我们创建了一个包含四个整数的集合。当我们评估sorted()函数时,我们得到一个包含按升序排序的项的list对象。当我们评估max()或min()函数时,我们得到集合中的最大值或最小值。sum()函数将集合中的值相加。
使用集合
我们之前查看的所有集合都已经是序列:str、bytes、tuple和list都有可以通过其在集合中的位置访问的项。set集合是一个无序集合,其中项是存在或不存在。
set集合中的元素必须是不可变的;它们必须提供适当的哈希值以及等价性测试。这意味着我们可以创建包含数字、字符串和元组的集合。我们无法轻松地创建包含列表或集合的集合。
set显示的语法是一系列用{}括起来的表达式。
这里是一个使用数字构建的示例set:
>>> fib_set = {1, 1, 3, 5, 8}
>>> fib_set
{8, 1, 3, 5}
我们通过将值包围在 {} 中创建了一个 set 对象。这种语法与创建 list 或 tuple 的语法非常相似。请注意,set 集合中的元素显示的顺序不同。顺序没有保证;不同的实现可能显示不同的顺序。
重要的是要注意,我们试图在 set 集合中包含两个整数 1 的实例。由于一个项目要么存在于 set 集合中,要么不存在,因此项目不能被包含第二次。重复的项目会被静默忽略。
我们也可以通过将 set() 函数应用于值集合来创建一个 set 集合。我们可以从一个 list 或 tuple 集合中创建一个 set 集合。我们还可以从一个简单的字符串中创建一个 set 集合:每个单独的字符将成为结果集合中的一个项目。我们可以使用 set([1, 1, 3, 5, 8]) 来将 set() 函数应用于一个字面列表对象。
有趣的是,语法 {} 并不创建一个空的 set。这实际上创建了一个空的 dict 类。要创建一个空的 set,我们必须使用 set() 函数。
我们为集合对象提供了相当多的运算符。除了运算符之外,我们还有大量的方法函数。这些可以按以下方式分类:
-
修改器:这些修改
set对象 -
访问器:这些访问列表并返回关于该
set对象的事实。
set 集合的修改方法几乎总是返回 None 的值。除了 pop() 方法外,修改方法不返回值。提供列表信息的访问器必须返回一个值。我们首先看看运算符。
使用集合运算符
集合有许多运算符,这些运算符与数学运算符非常相似。映射利用了位运算符;它将它们解释为集合成员资格而不是整数值中的位。
我们有以下运算符:|、&、- 和 ^,分别代表并集
、交集
、差集
和对称差集
。
这两个集合的例子如下:
>>> words = set("How I wish".split())
{'How', 'I', 'wish'}
>>> more = set("I could recollect pi".split())
{'recollect', 'pi', 'I', 'could'}
每个集合都是通过将字符串分割成单独的空格分隔的单词来构建的。结果包含正确的元素;然而,顺序可能会变化。以下是每个运算符的示例:
>>> words | more
{'wish', 'could', 'pi', 'I', 'How', 'recollect'}
>>> words & more
{'I'}
>>> words - more
{'How', 'wish'}
>>> words ^ more
{'recollect', 'wish', 'pi', 'How', 'could'}
并集运算符创建了一个新的集合,其中的元素来自两个集合。我们可以说 a | b 的并集创建了一个包含元素的集合,{x},其中每个元素要么是 a 的元素 或者 是 b 的元素。布尔 或 运算符的概念与集合并集之间有一个整洁的平行关系。
交集运算符 a & b 找到既是 a 的元素又是 b 的元素。再次强调,布尔 与 运算符与集合交集之间有一个紧密的平行关系。
集合差集运算符将从左侧集合中移除右侧集合中的项目。我们可以这样说,结果元素是a的元素而不是b的元素。没有常用的布尔运算符与集合差集的定义相对应。
对称差集运算符是两个集合中独有的项目;公共的项目已被移除。这对应于异或布尔运算。我们可以这样说,结果是属于a或属于b但不属于两个集合中的成员。
使用方法函数突变集合
集合有一些突变函数与list集合的突变函数平行。这些方法包括add()、remove()、discard()和clear()。由于这些方法是突变函数,它们不会返回有用的值。add()方法与list.append()平行:它向集合中添加一个单独的项目。
remove()和discard()方法将从集合中移除一个项目;如果项目不在集合中,remove()方法将引发异常,而discard()方法总是成功,即使项目不在集合中。clear()方法将丢弃集合中的所有项目。
例如,我们可以这样更新我们的fib_set变量:
f_n = max(fib_set)
f_n1 = max(fib_set-{f_n})
fib_set.add(f_n+f_n1)
我们已经找到了集合中的最大值,并将其分配给f_n变量。我们使用了集合差集运算符来创建一个不包含最大值的新的集合。当我们对这个新集合使用max()函数时,我们会得到次大的值。最后,我们使用add()方法突变集合以向集合中插入一个值。
集合差集运算符-不会改变集合:像所有算术运算符一样,它从操作数创建一个新的对象。然而,add()方法会改变给定的集合。
注意,斐波那契数并不是set集合的最佳用途。前两个斐波那契数都是 1。
pop()方法独特;它是一个既会突变又会返回值的突变函数。从集合中弹出的值将被任意选择。没有简单的方法可以预测哪个项目将被移除并返回。
每个运算符都有一个与运算符匹配的方法函数。以下运算符:|、&、-和^对应于update()、intersection()、difference()和symmetric_difference()方法。我们可以写a | b,或者我们可以写a.update(b)。两者都有相同的结果。
使用扩展赋值与集合
扩展赋值语句也与集合很好地工作。我们可以使用|=, &=, -=, 和 ^=来根据另一个集合中的元素更新一个集合。例如,考虑这个语句:
words |= more
words集合将被突变以包含more集合中的所有项目。
每个扩展赋值语句都有一个相应的更新方法。这些突变函数的方法名是update()、intersection_update()、difference_update()和symmetric_difference_update()。这些方法是突变函数,与扩展赋值语句相匹配。
使用运算符和方法函数访问集合
有几个算子可以算作集合访问器。也许访问集合最基本的方法是in运算符;这将检查特定元素是否存在于集合中。
>>> 'I' in words
True
集合的比较运算符实现了基本的集合理论操作。当我们使用<、<=、>或>=在两个集合之间时,我们正在进行子集和超集比较。例如:
>>> {'I'} < words
True
>>> {'How', 'I', 'wish'} <= words
True
在第一种情况下,集合{'I'}是words变量中集合的一个真子集。在第二种情况下,不正确的子集比较是True,因为这两个集合实际上是相等的。
我们还有与各种比较运算符匹配的方法函数。我们可以使用isdisjoint()、issubset()和issuperset(),以及!=、<和>运算符。
在item in set和{item} <= set之间几乎没有实际区别。当给定的item在set中时,set–{item} != set也会成立。这些数学等价性很有趣,但通常涉及额外的计算。
映射
Python 有几个映射集合。映射是键和值之间的关联。内置的映射集合是dict类。其他映射在collections库中定义,并且必须导入。
映射中的键必须是不可变的;它们必须提供适当的哈希值以及匹配的相等性测试。映射中的值没有限制;它们可以是可变的或不可变的。键的顺序由dict类不维护。
我们可以使用{}创建一个简单的dict显示;每个键和值都由冒号:字符分隔。
这里是一个简单映射的例子:
sieve = {2: True, 3: True, 4: False, 5: True, 6: None, 7: None}
我们创建了一个简单的映射,其键都是整数,值是布尔值和None值的混合。
我们还可以使用dict()函数创建字典。这个函数可以从各种来源构建字典。我们可以提供一个现有的字典作为参数;dict()函数将制作该源字典的浅拷贝。我们可以提供一个(key, value)二元组的序列。它看起来像这样:
>>> sieve = dict(
... [(2, True), (3, True), (4, False), (5, True), (6, None), (7, None)]
... )
这个例子从一个(key, value)二元组列表中创建了一个字典。创建的字典对象将与前面示例中显示的文本显示相匹配。
我们还可以使用dict()函数创建具有字符串键的字典。当我们提供关键字参数时,它们成为键。
>>> cadaeic= dict( poe=3, e=1, near=4, a=1, raven=5, midnights= 9 )
>>> cadaeic
{'raven': 5, 'e': 1, 'near': 4, 'midnights': 9, 'poe': 3, 'a': 1}
重要的是要重复指出,内置dict对象中键的顺序是没有定义的。
我们也可以从一个键集合中构建一个字典,提供一个默认值。我们可以这样做:
>>> sieve = dict.fromkeys( range(2,10) )
>>> sieve
{2: None, 3: None, 4: None, 5: None, 6: None, 7: None, 8: None, 9: None}
我们已经使用了range()函数来遍历一系列从 2 开始,到 10 之前结束的数字。这些数字随后被用来为字典创建键。与每个键关联的值是None的默认值。
使用字典运算符
所有 Python 映射,包括内置的dict,都使用[]中的键来获取、设置和删除项。语法看起来像这样:
>>> cadaeic['poe']
3
>>> cadaeic['so']= 2
>>> del cadaeic['so']
我们提供了字面字符串来展示如何获取一个项,设置一个项,以及使用del语句删除一个项。
注意,在一般意义上定义字典比较是困难的。不清楚顺序比较应该只比较键,只比较值,还是键和值的组合。因此,只有定义了字典之间的==和!=比较。
使用字典修改器
我们可以在赋值语句的左侧使用dict[key]来修改字典。如果键不存在,这将插入给定的键和值;如果键已经存在,它将更改与该键关联的值。
我们还有许多可以用来修改字典对象的方法。这些方法包括clear()、pop()、popitem()、setdefault()和update()来修改字典对象。
clear()和update()方法不返回有用的值。clear()方法将清空字典。update()方法将额外数据折叠到现有字典中。此方法可以接受与创建字典的dict()函数相同的各种参数。第一个位置参数可以是字典对象或(key, value)二元组的序列。此外,我们可以提供任意数量的关键字参数;关键字将成为更新字典中的键。
这里有两个示例,展示了update()方法可以使用的不同方式:
>>> cadaeic.update( {'so':2, 'dreary':6} )
>>> cadaeic.update( [('tired',5), ('and',3)], weary=5 )
>>> cadaeic
{'a': 1, 'weary': 5, 'near': 4, 'dreary': 6, 'e': 1,
'raven': 5, 'midnights': 9, 'and': 3, 'so': 2, 'poe': 3,
'tired': 5}
我们使用另一个包含两个条目的字典更新了cadaeic字典对象。然后我们使用一系列(key, value)二元组来应用进一步的更新。第二个示例还包括了一个额外的关键字参数,它将键'weary'插入到字典中。
setdefault()方法函数是一个有趣的特殊情况。这是get()访问器的变体。get()方法(以及pop()方法)有一个默认值的条款。setdefault()方法不仅仅在键缺失时返回默认值——类似于get()的行为。setdefault()方法更新字典以确保默认值现在在字典中。所有随后的setdefault()或get()方法都将找到字典中的键。
操作序列可能看起来像这样:
>>> counter = {}
>>> counter.setdefault('a',0)
0
>>> counter['a'] += 1
>>> counter
{'a': 1}
我们创建了一个空字典并将其分配给counter变量。当我们使用counter.setdefault('a',0)时,我们将得到与键'a'关联的值,或者我们将得到默认值零。除了返回之外,默认值还将用于更新字典,确保给定的键有一个关联的值。
我们可以执行一个简单、易于理解的counter['a'] += 1操作,知道键'a'在字典中有一个值。要么键已经存在,setdefault()函数没有做任何事情,要么键不存在,setdefault()函数提供了那个默认值。
由于setdefault()返回一个值,我们可以将其优化为类似以下内容:
>>> counter['b'] = counter.setdefault('b',0) + 1
这个setdefault()过程非常常见,因此在collections中有两个密切相关类。defaultdict类简单地将所有get()操作视为setdefault()。Counter类将隐式地对任何可迭代对象执行count[key]+=1过程,基于defaultdict类。
pop()方法有两种变体。典型的pop()实现将删除指定的键并返回与该键关联的值。除此之外,popitem()方法将从一个字典中删除并返回一个(key, value)对。这对将被任意选择。在这两种情况下,字典都会更新以删除值。
使用访问映射中项的方法
我们有几种方法可以访问映射中的项。首先,我们有一个dict[key]构造,它定位与给定键关联的值。如果键不存在,将引发KeyError异常。
get()方法也会返回字典中与键关联的值。get()方法还可以提供一个默认值。我们可以使用cadaeic.get("word",4)来定位键(在这个例子中是"word")。如果找不到键,则返回默认值4。
copy()方法返回字典的浅拷贝。我们可以通过a=dict(d)或a= d.copy()来创建一个新的字典,它是原始字典的副本。两者是等效的。
有三种方法可以公开映射的重要功能:
-
keys()是从映射中获取的键序列。默认情况下,在将映射转换为另一个集合时使用。如果我们使用set(cadaeic)或list(cadaeic),我们将看到集合或列表对象中仅有的键值。sorted(cadaeic)的值与sorted(cadaeic.keys())相同。 -
values()是从映射中获取的值序列。 -
items()是从映射中获取的(key, value)对序列。这个由两个元组组成的列表可以用来重建字典。如果我们使用tuple(cadaeic.items()),我们就创建了一个两个元组的元组。这个元组是不可变的,可以用作另一个映射的键或集合中的项。这是一种“冻结”字典以创建不可变副本的方法。
使用collections模块的扩展
Python 标准库包括collections模块。此模块为我们提供了对内置集合的多种替代方案。此模块具有以下附加集合:
-
我们可以导入
namedtuple函数并使用它来创建基于基本tuple的变体,该变体包括除了通过位置索引识别的属性之外还有命名属性。 -
deque类定义了一个双端队列,类似于可以快速在两端执行append()和pop()函数的list集合。这个类的一些功能可以创建单端栈(后进先出,LIFO)或队列(先进先出,FIFO)结构。 -
在某些情况下,我们可以使用
ChainMap而不是通过update()合并映射。结果是多个映射的视图,而不是单个更新后的映射。这可以非常快速地构建;搜索比单个映射要长。 -
OrderedDict映射是一个维护键创建顺序的映射。 -
defaultdict类是内置dict的子类,它使用工厂函数为缺失的键提供值。 -
Counter类是一个dict子类,用于计数对象以创建频率表。它也被用作更复杂的数据结构,称为多重集或包。
我们可以使用Counter类非常简单地创建字母频率。Counter会计算序列中项的出现次数。给定一个字符串,它是一个字符的可迭代序列,创建一个Counter可以直接得到一个频率表。以下是一个示例:
>>> from collections import Counter
>>> text = """Poe, E.
... Near a Raven
...
... Midnights so dreary, tired and weary,
... Silently pondering volumes extolling all by-now obsolete lore.
... During my rather long nap - the weirdest tap!
... An ominous vibrating sound disturbing my chamber's antedoor.
... "This", I whispered quietly, "I ignore"."""
>>> freq= Counter(text)
>>> freq.most_common(5)
[(' ', 35), ('e', 23), ('n', 18), ('r', 17), ('i', 17)]
我们已从collections模块中导入了Counter类。我们还设置了一个变量text,它包含迈克·基思的一首诗的一部分。要了解更多关于这首诗的信息,请参阅www.cadaeic.net/naraven.htm。
我们使用字符串作为来源创建了一个Counter对象。Counter对象将遍历序列中的每个项,计算该项的出现次数。当我们使用most_common()方法时,我们将看到集合中最常见的五个项。如果我们简单地打印freq变量的值,我们将看到所有字符的频率。
这些集合中的每一个都提供了独特的功能。如果内置的dict、list或tuple不能满足我们的需求,这些额外的集合中可能有一个更适合我们试图解决的问题。
使用for语句处理集合
for语句是一种非常灵活的方式来处理集合中的每个项。我们通过定义一个目标变量、项的来源和一系列语句来实现这一点。for语句将遍历项的来源,将每个项分配给目标变量,并执行一系列语句。Python 中的所有集合都提供了必要的方法,这意味着我们可以将任何东西用作for语句中项的来源。
这里有一些我们将要处理的示例数据。这是迈克·基思的诗《近乌鸦》的一部分。我们将移除标点符号,以便更容易处理文本:
>>> text = '''Poe, E.
... Near a Raven
...
... Midnights so dreary, tired and weary.'''
>>> text = text.replace(",","").replace(".","").lower()
这将把原始文本(包括大写、小写和标点符号)放入 text 变量中。我们使用了 第二章 中的一些方法函数,简单数据类型,来删除常见的标点符号,并返回一个完全由小写字母组成的整个字符串版本。
当我们使用 text.split() 时,我们得到一系列单独的单词。for 循环可以遍历这个单词序列,以便我们可以逐个处理。语法看起来像这样:
>>> cadaeic= {}
>>> for word in text.split():
... cadaeic[word]= len(word)
我们创建了一个空字典,并将其分配给 cadaeic 变量。for 循环中的表达式 text.split() 将创建一个子字符串序列。这些子字符串中的每一个都将分配给 word 变量。for 循环体——一个单独的赋值语句——将为分配给 word 的每个值执行一次。
结果字典可能看起来像这样(不考虑顺序):
{'raven': 5, 'midnights': 9, 'dreary': 6, 'e': 1,
'weary': 5, 'near': 4, 'a': 1, 'poe': 3, 'and': 3,
'so': 2, 'tired': 5}
映射或集合没有保证的顺序。你的结果可能会有所不同。
除了遍历序列之外,我们还可以遍历字典中的键。
>>> for word in sorted(cadaeic):
... print(word, cadaeic[word])
当我们在 tuple 或 list 上使用 sorted() 时,会创建一个包含排序项的临时列表。当我们对映射应用 sorted() 时,排序应用于映射的键,创建一个排序键的序列。这个循环将按字母顺序打印出这首诗中使用的各种 pilish 单词的列表。
注意
Pilish 是英语的一个子集,其中单词长度很重要:它们被用作记忆辅助工具。
for 语句对应于“对所有”逻辑量词,
。在一个简单的 for 循环结束时,我们可以断言源集合中的所有项目都已处理。为了构建“存在”量词,
,我们可以使用 while 语句,或者在 for 语句体中使用 break 语句。
在 for 语句中使用字面值列表
我们可以将 for 语句应用于字面值序列。表示字面值最常见的方式之一是作为 tuple。它可能看起来像这样:
for scheme in 'http', 'https', 'ftp':
do_something(scheme)
这将为 scheme 变量分配三个不同的值。对于这些值中的每一个,它都会评估 do_something() 函数。
从这个例子中,我们可以看出,严格来说,括号 () 不是必需的,不能限定 tuple 对象。然而,如果值的序列增长,并且我们需要跨越多个物理行,我们就会想要 add (),使 tuple 字面量更加明确。
使用 range() 和 enumerate() 函数
range()对象将提供一个数字序列,通常用于for循环中。range()对象是可迭代的,它本身不是一个序列对象。它是一个生成器,当需要时会产生项。如果我们不在for语句中使用range(),我们需要使用像list(range(x))或tuple(range(a,b))这样的函数来消耗所有生成的值并创建一个新的序列对象。
range()对象有三种常用的形式:
-
range(n)生成包括 0 但不包括n本身的递增数字。这是一个半开区间。我们可以说range(n)生成数字,x,如下所示
。表达式list(range(5))返回[0, 1, 2, 3, 4]。这产生n个值,包括 0 和n - 1。 -
range(a,b)生成从a开始的递增数字,但不包括b。表达式tuple(range(-1,3))将返回(-1, 0, 1, 2)。这会产生b - a个值,包括a和b - 1。 -
range(x,y,z)生成如
所示的递增数字序列。这产生(y-x)/z个值。
我们可以这样使用range()对象:
for n in range(1, 21):
status= str(n)
if n % 5 == 0: status += " fizz"
if n % 7 == 0: status += " buzz"
print(status)
在这个例子中,我们使用range()对象来生成值,n,如下所示
。
我们使用range()对象来生成列表中所有项的索引值:
for n in range(len(some_list)):
print(n, some_list[n])
我们已经使用range()函数来生成名为some_list的序列对象之间的值,从 0 开始到其长度。
for语句允许多个目标变量。多个目标变量的规则与多个变量赋值语句相同:一个序列对象将被分解,并将项分配给每个变量。正因为如此,我们可以利用enumerate()函数遍历一个序列,并同时分配索引值。它看起来像这样:
for n, v in enumerate(some_list):
print(n, v)
enumerate()函数是一个生成器函数,它遍历源序列中的项,并产生一个包含索引和项的两个元组对的序列。由于我们提供了两个变量,两个元组被分解并分配给每个变量。
这个多赋值for循环有无数的使用场景。我们经常有列表-元组数据结构,可以用这个多赋值特性非常整洁地处理。在第八章中,我们将探讨这些设计模式中的许多。
使用 while 语句进行迭代
while语句比for语句更通用。我们将在两种情况下使用while循环。在没有有限集合来对循环迭代施加上限的情况下,我们将使用它;我们可以在while子句本身中建议上限。我们还将使用它来编写“搜索”或“存在”类型的循环;我们不是在处理集合中的所有项目。
例如,一个接受用户输入的桌面应用程序通常会使用while循环。应用程序会一直运行,直到用户决定退出;用户交互的数量没有上限。为此,我们通常使用while True:循环。建议使用无限迭代。
如果我们想编写一个字符模式用户界面,我们可以这样做:
quit_received= False
while not quit_received:
command= input("prompt> ")
quit_received= process(command)
这将一直迭代,直到quit_received变量被设置为True。这将无限期地处理;迭代次数没有上限。
这个process()函数可能使用某种命令处理。这应该包括如下语句:
if command.lower().startswith("quit"): return True
当用户输入"quit"时,process()函数将返回True。这将分配给quit_received变量。while表达式not quit_received将变为False,循环结束。
“存在”循环将遍历一个集合,在遇到满足某些标准的第一个项目时停止。这看起来可能很复杂,因为我们被迫明确地处理循环处理的两个细节。
这里有一个搜索第一个满足条件的值的例子。此示例假设我们有一个函数condition(),该函数最终会对某些数字为True。以下是我们可以如何使用while语句定位此函数为True的最小值:
>>> n = 1
>>> while n != 101 and not condition(n):
... n += 1
>>> assert n == 101 or condition(n)
当n == 101或condition(n)为True时,while语句将终止。如果此表达式为False,我们可以将n变量推进到值序列中的下一个值。由于我们按顺序从最小到最大迭代值,我们知道n将是condition()函数为真的最小值。
在while语句的末尾,我们包含了一个正式的断言,即n是 101 或对于给定的n值,condition()函数为True。编写这样的断言可以帮助设计以及调试,因为它通常会总结循环不变条件。
我们还可以使用for循环中的break语句编写这种类型的循环,我们将在下一节中探讨。
continue和break语句
continue语句对于跳过项目而不编写深层嵌套的if语句非常有用。执行continue语句的效果是跳过循环体中的其余部分。在for循环中,这意味着下一个项目将从源可迭代对象中取出。在while循环中,必须小心使用,以避免无限迭代。
我们可能会看到类似这样的文件处理:
for line in some_file:
clean = line.strip()
if len(clean) == 0:
continue
data, _, _ = clean.partition("#")
data = data.rstrip()
if len(data) == 0:
continue
process(data)
在这个循环中,我们依赖于文件表现得像一系列单独的行的方式。对于文件中的每一行,我们都从输入行中删除了空格,并将结果字符串分配给 clean 变量。如果这个字符串的长度为零,则该行完全由空格组成,我们将继续循环到下一行。continue 语句跳过了循环体内的剩余语句。
我们将行分成三部分:任何 "#" 前的部分,"#"(如果存在),以及任何 "#" 后的部分。我们将 "#" 字符和任何 "#" 后面的文本分配给同一个容易忽略的变量 _,因为我们不需要这两个 partition() 方法的输出结果。然后我们可以从分配给 data 变量的字符串中删除任何尾随空格。如果结果字符串的长度为零,则该行完全由 "#" 和任何尾随注释文本组成。由于没有有用的数据,我们可以继续循环,忽略这一行输入。
如果该行通过了两个 if 条件,我们可以处理结果数据。通过使用 continue 语句,我们避免了看起来复杂、深度嵌套的 if 语句。我们将在 第十章 文件、数据库、网络和上下文 中详细检查文件。
需要注意的是,continue 语句必须始终是 if 语句内部、for 或 while 循环内部的代码块的一部分。那个 if 语句的条件成为一个过滤条件,应用于正在处理的数据集合。continue 总是应用于最内层的循环。
提前退出循环
break 语句是循环语义的一个重大变化。一个普通的 for 语句可以总结为“对所有”。我们可以舒适地说,“对于集合中的所有项目,都处理了语句块。”
当我们使用 break 语句时,循环不再总结为“对所有”。我们需要改变我们的视角到“存在”。break 语句断言集合中至少有一个项目与导致执行 break 语句的条件匹配。
这里是一个 break 语句的简单示例:
for n in range(1, 100):
factors = []
for x in range(1,n):
if n % x == 0: factors.append(x)
if sum(factors) == n:
break
我们编写了一个受
限制的循环。这个循环包含一个 break 语句,因此它不会处理 n 的所有值。相反,它会确定 n 的最小值,对于这个值,n 等于其因子的和。由于循环没有检查所有值,这表明在给定范围内至少存在这样一个数。
我们使用嵌套循环来确定数字 n 的因子。这个嵌套循环为范围
内的所有 x 值创建了一个序列 factors,使得 x 是数字 n 的一个因子。这个内部循环没有 break 语句,所以我们确信它会检查给定范围内的所有值。
这个条件成立的最小值是数字六。
重要的是要注意,break 语句必须始终是 for 或 while 循环内部 if 语句集中的部分。如果 break 不在 if 语句集中,循环将在处理第一个项目时始终终止。该 if 语句的条件成为总结整个循环的“存在条件”。
在循环中使用 else 子句
Python 的 else 子句也可以用在 for 或 while 语句以及 if 语句上。如果循环体中没有执行 break 语句,则 else 子句将执行。为了说明这一点,这里有一个人为的例子:
>>> for item in 1,2,3:
... print(item)
... if item == 2:
... print("Found",item)
... break
... else:
... print("Found Nothing")
这里的 for 语句将遍历一个短列表的文本值。当找到特定的目标值时,会打印一条消息。然后,break 语句将结束循环,避免执行 else 子句。
当我们运行这段代码时,我们会看到三行输出,如下所示:
1
2
Found 2
三的值没有显示,也没有在 else 子句中显示的“未找到任何内容”消息。
如果我们将 if 语句中的目标值从二更改为不会出现的值(例如,零或四),那么输出将改变。如果 break 语句没有执行,那么 else 子句将被执行。
这里的想法是允许我们编写包含 break 和非 break 语句的对比语句集。包含 break 语句的 if 语句集可以在 break 语句结束循环之前在语句集中进行一些处理。else 子句允许在循环结束时进行一些处理,当没有执行任何与 break 相关的语句集时。
概述
我们已经探讨了三种可变集合:列表、集合和字典。Python 中内置的字典类只是众多映射中的一种,其他映射定义在标准库的 collections 模块中。列表允许我们收集通过列表中位置标识的项。集合允许我们收集一组唯一的项,其中每个项仅通过自身来标识。映射允许我们通过键来标识项。
对于集合,每个项必须是不可变的。对于映射,用作键的对象必须是不可变的。这意味着数字、字符串和元组通常用作映射键。
我们已经了解了for语句,这是我们处理集合中各个项目的主要方式。一个简单的for语句确保我们的处理已经针对集合中的所有项目完成。我们还探讨了通用的while循环。
在第七章,基本函数定义中,我们将探讨如何定义我们自己的函数。我们还将了解在 Python 中评估函数的多种方法。
第七章. 基本函数定义
从数学的角度来看,一个函数是将定义域中的值映射到值域中的映射。像正弦或余弦这样的函数将角度域的值映射到-1 和+1 之间的实数值域。映射的详细信息总结在函数名、定义域和值域中。我们将使用这个函数概念来将我们的 Python 编程打包成可以使用名称总结实现细节的东西。
我们将探讨如何定义和评估 Python 函数。在本章中,我们将关注仅返回 Python 对象作为值域的 Python 函数。在第八章,更高级的函数中,我们将探讨生成器函数;这些是迭代器,它们与for循环一起使用以产生值序列。
Python 函数还提供可选参数以及位置参数和关键字参数的混合。这使得我们可以定义一个具有多个变体签名的单个函数,从而在函数的使用上提供了相当大的灵活性。
查看五种可调用类型
Python 在函数主题上提供了五种变体。每一种都是一种可调用对象:我们可以用参数值调用对象,它返回一个结果。以下是我们将如何组织我们的探索:
-
使用
def语句创建的基本函数是本章的主题。 -
Lambda 形式是将函数定义简化为参数和表达式;这也是本章的一个主题。
-
生成器函数和
yield语句是我们将在第八章,更高级的函数中探讨的内容。这些函数是迭代器,可以提供多个结果。 -
函数包装器用于类方法,我们将在第十一章,类定义中探讨。这些是利用类特性的内置函数。像
len()这样的函数是由集合的__len__()方法实现的。 -
可调用对象也是第十一章,类定义的一部分。这些类包括
__call__()方法,使得类的实例表现得像使用def语句创建的基本函数。
所有这些都是基于一个共同主题的变体。它们是将某些功能打包成具有名称、输入参数和结果的方式。这使得我们可以将大型、复杂的程序分解成更小、更容易理解的函数。
使用位置参数定义函数
Python 函数定义的基本结构是用def语句构建的。我们提供一个名称、参数的名称,以及函数体的缩进语句序列。return语句提供值域。
语法看起来像这样:
def prod(sequence):
p= 1
for item in sequence:p *= item
return p
我们定义了一个名为prod的名称,并提供了一个只有一个参数sequence的列表。函数体包括三个语句:赋值、for和return。return语句中的表达式提供了结果值。
这合理地符合函数的数学概念。值域是任何数值序列,值域将是反映序列数据类型的类型值。
我们通过在表达式中使用函数名和特定参数值来评估函数:
>>> prod([1,2,3,4])
24
>>> prod(range(1,6))
120
在第一个例子中,我们提供了一个简单的列表显示[1, 2, 3, 4]作为参数。这被分配给函数的参数sequence。函数的评估返回了这些序列项的乘积。
在第二个例子中,我们将一个range()对象作为prod()函数的参数。这个参数值被分配给函数的参数。当与for循环一起使用时,范围对象表现得像一个序列集合,计算并返回一个乘积。
定义多个参数
Python 为我们提供了多种方式来为参数赋值。在最简单的情况下,参数值根据位置分配给参数。这里有一个具有两个位置参数的函数:
def main_sail_area(boom, mast):
return (boom*mast)/1.8
我们定义了一个函数,该函数需要帆杆的长度,通常称为“E”维度,以及帆索的长度,通常称为“P”维度。给定这两个数字以及关于帆曲率的假设,我们返回帆的大致面积。
我们可以评估这个函数,提供两个位置参数,即帆杆长度和桅杆高度。
>>> main_sail_area(15, 45)
375.0
我们可以定义具有任何数量的参数的函数。具有大量参数的函数可能会使可理解性达到极限。一个好的函数应该有一个整洁的摘要,使得在不详细研究太多细节的情况下也能理解函数的目的。
使用return语句
return语句有两个目的:它结束函数的执行,并且可以可选地提供函数的结果值。return语句是可选的。这导致三种用法:
-
没有使用
return语句:函数在语句块结束时结束。返回值是None。 -
没有表达式的
return语句:当执行return语句时,函数结束,结果是None。 -
带有表达式的
return语句:当执行return语句时,函数结束,表达式的值是结果。带有表达式列表的return语句创建一个tuple,适合多重赋值。
这里是一个没有return语句的函数:
def boat_summary(name, rig, sails):
print( "Boat {0}, {1} rig, {2:.0f} sq. ft.".format(
name, rig, sum(sails))
)
这个函数由一个使用print()函数的单个表达式语句组成。没有显式的return,所以默认返回值将是None。
当满足异常条件时,通常使用 return 语句提前结束,否则执行函数定义中的语句序列的其余部分。它看起来像这样:
def mean_diff(data_sequence):
s0, s1 = 0, 0
for item in data_sequence:
s0 += 1
s1 += item
if s0 < 2:
return
m= s1/s0
for item in data_sequence:
print(item, abs(item-m))
此函数期望一个数据集合。它将从该集合中计算两个值:s0 和 s1。s0 值将是项目数量的计数,s1 值将是项目的总和。如果计数太小,函数将直接返回。如果计数足够大,则进行额外的处理:打印值及其与平均值的绝对差异。
在语句序列的末尾没有 return 语句,因为这不是必需的。在函数中间使用 return 语句允许我们避免深层嵌套的 if 语句。
注意,变量 s0、s1 和 m 是在局部命名空间中创建的,该命名空间仅在函数被评估时存在。一旦函数完成,局部命名空间将被移除,引用计数将减少,临时对象将被清理。我们将在本章后面的 使用命名空间 部分查看更多细节。
内置函数 divmod() 返回两个结果。我们经常使用这样的多重赋值:q, r = divmod(n, 16);它将两个结果赋值给两个变量,q 和 r。我们可以在 return 语句中包含多个表达式来编写返回多个值的函数。
在 可变和不可变参数值 部分,我们将展示一个具有多个返回值的函数。
使用位置或关键字参数评估函数
Python 允许我们通过显式参数名称提供参数值。当我们提供名称时,它被称为关键字参数。例如,上一节中的 boat_summary() 函数可以用多种方式使用。
我们可以按位置提供参数值,如下所示:
>>> sails = [358.3, 192.5, 379.75, 200.0]
>>> boat_summary("Red Ranger", "ketch", sails)
参数根据其位置分配给 name、rig 和 sails 的参数变量。
我们可以,作为替代,这样做:
>>> boat_summary(sails=sails, rig="ketch", name="Red Ranger" )
此示例提供了所有三个参数的关键字。请注意,当提供关键字参数时,位置并不重要。关键字参数必须在任何位置参数之后提供,但关键字参数之间的顺序并不重要,因为它们是根据名称分配给参数的。
我们可以使用位置和关键字参数的混合。为了使这起作用,Python 使用两条规则将参数值映射到函数的参数:
-
从左到右匹配所有位置参数到参数。
-
通过名称匹配所有关键字参数。
有一些额外的规则来处理重复项和默认值——包括可选参数——将在稍后的 通过默认值定义可选参数 部分描述。
为了使这些规则能够正确工作,我们必须首先提供所有位置参数,然后我们可以在位置参数之后提供任何关键字参数。我们不能通过位置和关键字为同一个参数提供两个值。同样,也不能为关键字提供两次。
这里有一个好的例子和一个不好的例子:
>>> boat_summary("Red Ranger", sails=sails, rig="ketch")
>>> boat_summary("Red Ranger", sails=sails, rig="ketch", name="Red Ranger")
在第一个例子中,name 参数是按位置匹配的。sails 和 rig 参数是通过关键字匹配的。
在第二个例子中,name 变量既有位置值也有关键字值。这将引发一个 TypeError 异常。
由于这个原因,选择参数变量名非常重要。一个好的参数名可以使关键字参数函数评估非常清晰。
编写函数的文档字符串
为了节省空间,我们没有提供很多带有文档字符串的函数示例。我们将在第十四章第十四章。完善 – 单元测试、打包和文档中详细讨论文档字符串。现在,我们需要意识到每个函数至少应该有一个摘要。摘要包含在一个三引号字符串中,它必须是函数语句块中的第一个表达式。
带有文档字符串的函数看起来像这样:
def jib(foot, height):
"""
jib(foot,height) -> area of given jib sail.
>>> jib(12,40)
240.0
"""
return (foot*height)/2
这个特定的三引号字符串有两个作用。首先,它总结了函数的功能。当我们查看源文件时可以阅读它。我们也可以在调用 help(jib) 时看到它。
这个文档字符串的第二个目的是提供一个具体的例子,说明如何使用该函数。这些例子看起来就像是从一个交互式解释器会话中复制并粘贴到文档字符串注释中一样。
这些交互式解释器格式的例子是通过使用 doctest 工具定位的。在定位示例后,这个工具可以运行代码以确认它按预期工作。本书中的所有示例都是使用 doctest 测试的。虽然测试的细节是第十四章第十四章。完善 – 单元测试、打包和文档的一部分,但考虑在每个函数中编写文档字符串是很重要的。
可变和不可变参数值
在某些编程语言中,存在多种函数评估策略,包括按值调用和按引用调用。在按值调用的语义中,将参数值的副本分配给函数中的参数变量。在按引用调用的语义中,在函数中使用变量的引用。这意味着函数内部的赋值语句可以替换函数外部的变量值。这两种语义类型都不适用于 Python。
Python 使用名为“按共享调用”或“按对象调用”的机制。函数被赋予原始对象的引用。如果该对象是可变的,函数可以修改该对象。然而,函数不能通过参数变量将变量赋值给函数外的变量。函数共享对象,而不是对象所分配的变量。
最重要的影响之一是,函数体可以给参数变量赋新值,而不会对传递给函数的原始参数有任何影响。参数变量严格局限于函数内部。
这是一个给参数变量赋新值的函数:
def get_data(input_string):
input_string= input_string.strip()
input_string, _, _ = input_string.partition("#")
input_string= input_string.rstrip()
name, _, value = input_string.partition('=')
return name, value
这个函数评估input_string变量的strip()方法,并将结果字符串赋给参数变量。它将partition()方法应用于新的input_string变量值,并将三个结果字符串之一赋给参数变量。然后它返回这个字符串对象,再次将其赋给参数变量。
对input_string参数变量的任何赋值语句都不会影响函数外的任何变量。当函数被评估时,会使用一个独立的命名空间来处理参数和其他局部变量。
Python 工作方式的一个后果是,当我们提供可变对象作为参数时,这些对象可以被函数内部评估的方法更新。函数的参数变量将是原始可变对象的引用,我们可以评估像remove()或pop()这样的方法,这些方法会改变引用的对象。
这是一个通过移除选定值来更新list参数的函数:
def remove_mod(some_list, modulus):
for item in some_list[:]:
if item % modulus == 0:
some_list.remove(item)
这个函数期望一个可变对象,如列表,命名为some_list,以及一个值,命名为modulus。函数使用some_list[:]创建参数值的临时副本。对于这个副本中是modulus值的倍数的每个值,我们将从原始的some_list对象中移除该副本。这将修改原始对象。
当我们评估这个函数时,它看起来是这样的:
>>> data= list(range(10))
>>> remove_mod(data, 5)
>>> remove_mod(data, 7)
>>> data
[1, 2, 3, 4, 6, 8, 9]
我们创建了一个简单的列表并将其赋值给data变量。这个由data变量引用的对象被remove_mod()函数修改。序列中的所有五和七的倍数都被丢弃。
在这个函数中,在开始移除值之前,我们需要创建输入list对象的临时副本。如果我们试图在同时从该list中移除项目的同时迭代该list,我们会得到看起来不正确的结果。将原始值与正在修改的list分开是有帮助的。
函数可以通过global和nonlocal语句(在使用命名空间部分中展示)进行特殊安排,在全局命名空间和其他非局部命名空间中创建变量。
通过默认值定义可选参数
Python 允许我们为参数提供一个默认值。具有默认值的参数是可选的。标准库中充满了具有可选参数的函数。一个例子是int()函数。我们可以使用int("48897")将字符串转换为整数,假设该字符串表示的是十进制数。我们可以使用int("48897", 16)明确指出字符串应被视为十六进制值。base参数的默认值是 10。
记住我们可以为函数使用关键字参数。这意味着我们可能想要写一些像这样的事情:int("48897", base=16),以便清楚地表明int()函数的第二个参数是用来做什么的。
之前,我们列出了两个将参数值与参数匹配的规则。当我们引入默认值时,我们增加了两个额外的规则。
-
从左到右将所有位置参数与参数匹配。
-
匹配所有关键字参数。如果已经分配了位置参数,将引发
TypeError异常。 -
为任何缺失的参数设置默认值。
-
如果有参数没有值,将引发
TypeError异常。注意
注意:这不是最终的规则集;还有一些其他特性需要介绍。
这些规则的一个重要后果是,必须首先定义所需的参数——即没有默认值的参数。具有默认值的参数必须最后定义。"先定义必需参数,后定义可选参数"的规则确保我们位置匹配过程能够正常工作。
我们在函数定义中提供默认值。以下是一个示例:
import random
def dice(n=2, sides=6):
return [random.randint(1,sides) for i in range(n)]
我们导入了random模块,以便可以使用random.randint()函数。我们的dice()函数有两个参数,它们都有默认值。如果未提供n参数,它的值将为 2。如果省略了sides参数,它的值将为 6。
这个函数的主体是一个列表推导式:它使用生成器表达式来构建一个包含单个值的列表。我们将在第八章中详细探讨生成器表达式,更高级的函数。现在,我们可以观察到它使用random.randint(1,sides)函数来生成介于 1 和sides参数值之间的数字。推导式包括一个for子句,它遍历n个值。
我们可以用多种方式使用这个函数。以下是一些示例:
>>> dice()
[6, 6]
>>> dice(6)
[3, 6, 2, 2, 1, 5]
>>> dice(4, sides=4)
[3, 3, 4, 3]
第一个示例依赖于默认值来模拟在像 Craps 这样的赌场游戏中常用的两枚骰子。第二个示例使用六枚骰子,这在像 10,000(有时称为 Zilch 或 Crap Out)这样的游戏中很典型。第三个示例使用四枚四面骰子,这在使用各种多面骰子的游戏中很常见。
注意
关于测试的说明:为了为涉及random模块的函数提供可重复的单元测试,我们已使用random.seed("test")设置了一个特定的种子值。
关于可变默认值的警告
这里有一个病态的例子。这显示了非常糟糕的编程实践;这是许多 Python 程序员在开始使用默认值时犯的一个错误。
这是一个非常糟糕的想法:
def more_dice(n, collection=[]):
for i in range(n):
collection.append(random.randint(1,6))
return collection
我们定义了一个只有一个参数变量n和collection的简单函数。collection的默认值是一个空列表。(剧透:这将会是一个错误。)该函数将模拟六面骰子的数量添加到给定的集合中。
函数返回一个值以及修改一个参数。这意味着当我们使用这个函数在 REPL 中时,我们会看到return值被打印出来。
我们可以使用这个来玩像 Yacht(也称为 Generala 或 Poker Dice)这样的游戏。玩家有一副骰子,我们将从中移除骰子并附加新的骰子滚动。
一个用例是创建一个list对象,并将其用作more_dice()函数的参数。这个list对象将得到很好的更新。下面是如何工作的示例:
>>> hand1= []
>>> more_dice(5, hand1)
[6, 6, 3, 6, 2]
>>> hand1
[6, 6, 3, 6, 2]
我们创建了一个空的list并将其分配给hand变量。我们向more_dice()函数提供了这个序列对象,以便将五个值附加到hand对象上。这给了我们一个初始的三个六、一个三和一个二的骰子。我们可以从hand1对象中移除两个和三个;我们可以使用more_dice(2, hand1)重新使用它,将两个骰子放入手中。
我们可以使用另一个空序列作为参数来发第二手牌。除了结果外,它与其他示例相同:
>>> hand2= []
>>> more_dice(5, hand2)
[5, 4, 2, 2, 5]
>>> hand2
[5, 4, 2, 2, 5]
所有的东西似乎都工作得很好。这是因为我们为收集参数提供了一个明确的论证。每个手对象都是一个独特的、空的list。让我们尝试为collection参数使用默认值。
在这个第三个示例中,我们不会提供参数,而是依赖于more_dice()函数返回的默认序列:
>>> hand1= more_dice(5)
>>> hand1
[6, 6, 3, 6, 2]
>>> hand2= more_dice(5)
>>> hand2
[6, 6, 6, 2, 1, 5, 4, 2, 2, 5]
等等。刚才发生了什么?这是怎么可能的?
作为提示,我们需要在代码中搜索一个具有隐藏的共享状态的对象。之前,我们提到默认的list对象会是一个问题。这个隐藏的list对象正在被重复使用。
发生的事情是这样的:
-
当
def语句执行时,定义参数默认值的表达式将被评估。这意味着创建了一个单个可变list对象作为collection参数的默认对象。 -
当
more_dice()函数在没有collection参数的论证下评估时,唯一可变的list对象被用作默认对象。重要的是,单个可变对象正在被重复使用。如果在任何时刻我们更新了这个对象,那么这种变更将应用于该对象的所有共享使用。由于它是由函数返回的,这个单一的list可以被分配给几个变量。 -
当
more_dice()函数在没有collection参数的论证下第二次评估时,变更后的list对象被重新用作默认对象。
从这个例子中,我们可以看出可变对象是一个糟糕的默认值选择。
通常,我们不得不做类似这样的事情:
def more_dice_good(n, collection=None):
if collection is None:
collection = []
for i in range(n):
collection.append(random.randint(1,6))
return collection
此函数使用一个不可变且易于识别的默认值 None。如果没有为 collection 变量提供参数值,它将被设置为 None。我们可以在函数评估时创建一个新的空可变对象来替换 None 值。然后我们可以更新这个新的列表对象,确信我们没有破坏任何正在被重复使用的可变默认对象。
小贴士
不要将可变对象用作参数的默认值。
避免使用 list、dict、set 以及任何其他可变类型作为默认参数值。使用 None 作为默认值;将 None 替换为一个新的空可变对象。
你已经被警告了。
这可能会导致错误。这是函数定义方式和按共享语义调用的一种后果。
有意利用这一点是可能的:我们可以使用可变默认值作为缓存来保留值,创建具有滞后效应的函数。可调用对象可能是实现具有内部缓存或缓冲区功能的函数的更好方式。有关更多信息,请参阅第十一章,类定义。
使用 * 和 ** 的 "其余所有内容" 表示法
Python 在如何定义函数的位置参数和关键字参数方面提供了更多的灵活性。我们看到的例子都限于固定和有限的参数值集合。Python 允许我们编写具有无限数量位置参数以及关键字参数值的函数。
Python 将创建一个包含所有未匹配位置参数的 tuple。它还将创建一个包含所有未匹配关键字参数的字典。这允许我们编写如下使用的函数:
>>> prod2(1, 2, 3, 4)
24
此函数接受任意数量的位置参数。与前面展示的 prod() 函数进行比较。我们之前的例子需要一个单一的序列对象,我们必须按照以下方式使用该函数:
>>> prod([1, 2, 3, 4])
24
prod2() 函数将创建所有参数值的乘积。由于 prod2() 函数可以与无限数量的位置参数一起工作,这导致该函数的语法稍微简单一些。
为了编写一个具有无限数量位置参数的函数,我们必须提供一个带有 * 前缀的参数。它看起来是这样的:
def prod2(*args):
p= 1
for item in args:
p *= item
return p
prod2() 的定义将所有位置参数分配给带有 * 前缀的参数 *args。args 参数的值是一个包含参数值的元组。
这是一个使用位置和关键字参数混合的函数:
def boat_summary2(name, rig, **sails):
print("Boat {0}, {1} rig, {2:.0f} sq. ft.".format(
name, rig, sum(sails.values())))
此函数将接受两个参数,name 和 rig。这些可以通过位置或关键字提供。除了 name 和 rig 之外的所有额外关键字参数被收集到一个字典中,并分配给 sails 参数。sails.values() 表达式仅从 sails 字典中提取值;这些值被加在一起以写入最终的总结行。
这是我们可以使用此函数的许多方法之一:
>>> boat_summary2("Red Ranger", rig="ketch",
... main=358.3, mizzen=192.5, yankee=379.75, staysl=200 )
我们通过位置提供了第一个参数值;这将分配给第一个位置参数,name。我们使用关键字参数 rig 提供了一个定义好的参数。剩余的关键字参数被收集到一个字典中,并分配给名为 sails 的参数。
sails 字典将被分配一个类似这样的值:
{'main': 358.3, 'mizzen': 192.5, 'yankee': 379.75, 'staysl': 200}
由于这是一个正确的 dict 对象,我们可以使用任何字典处理来处理这个映射。
之前,我们提供了匹配参数值与参数的四条规则。以下是匹配参数值与函数参数的更完整的规则集:
-
从左到右匹配所有位置参数到参数。
-
如果位置参数多于参数名:
-
如果有一个带有
*前缀的参数名,将剩余值的tuple分配给带前缀的参数。 -
如果没有带有
*前缀的参数,则引发TypeError异常。
-
-
匹配所有关键字参数。如果已经分配了位置参数,则引发
TypeError异常。 -
如果关键字参数多于参数名:
-
如果有一个带有
**前缀的参数名,将剩余的关键字和值dict分配给带前缀的参数。 -
如果没有带有
**前缀的参数,则引发TypeError异常。
-
-
将默认值应用于缺失的参数。
-
如果参数仍然没有值,则引发
TypeError异常。
这些规则的一个后果是,最多只有一个参数可以有 * 前缀;同样,最多只有一个参数可以有 ** 前缀。这些特殊情况必须在其他所有参数之后给出。如果没有剩余的位置参数,* 前缀变量将被分配一个空元组。如果没有剩余的关键字参数,** 前缀变量将被分配一个空字典。
在调用函数时,我们必须首先提供位置参数值。我们可以按任何顺序提供关键字参数值。
使用序列和字典填充 *args 和 *kw
之前展示的 prod2() 函数期望收集到单个 *args 元组中的单个值。如果我们用 prod2(1, 2, 3, 4, 5) 调用该函数,则从五个位置参数构建的元组被分配给 args 参数。
如果我们想向 prod2() 函数提供一个列表,我们如何有效地写出 prod2(some_list[0], some_list[1], some_list[2], … )?
当我们使用prod2(*some_sequence)调用函数时,给定参数序列的值将与位置参数匹配。参数序列中的第一个项成为第一个位置参数。序列中的第二个项成为第二个参数,依此类推。每个项都会被分配,直到它们都被使用完。如果有额外的参数值,并且函数定义中使用了带有*前缀的参数,则额外的参数值将被分配给*前缀参数。
因此,我们可以轻松地使用prod2(*range(1, 10))。这实际上等同于prod2(1, 2, 3, 4, 5, …, 9)。由于所有的位置参数值都被分配给*前缀的args变量,我们可以使用这个函数与单个值一起使用,例如:prod2(1, 2, 3, 4)。我们也可以提供值序列,例如:prod2(*sequence)。
我们有一个类似的技术,可以将关键字参数的字典提供给函数。我们可以这样做:
>>> rr_args = dict(
... name="Red Ranger", rig="ketch",
... main=358.3, mizzen=192.5, yankee=379.75, staysl=200
... )
>>> boat_summary2(**rr_args)
Boat Red Ranger, ketch rig, 1131 sq. ft.
我们创建了一个包含所有通过关键字定义的参数的字典。这是dict()函数的一个方便特性,其中所有关键字参数都用于构建字典对象。我们将该字典分配给rr_args变量。当我们调用boat_summary2()函数时,我们使用**rr_args参数来强制将rr_args字典中的每个键和值与函数的参数匹配。这意味着与字典中name和rig键关联的值将与name和rig参数匹配。字典中的所有其他键都将分配给sails参数。
这些技术使我们能够动态地构建函数参数。这为我们定义和使用 Python 函数提供了极大的灵活性。
嵌套函数定义
我们可以在函数定义中包含任何内容,甚至另一个函数定义。当我们查看第十三章中的装饰器时,元编程与装饰器,我们会看到包含嵌套函数定义的函数的例子。
我们可以在函数定义中包含import语句。一个import语句实际上只执行一次。但是,导入的模块有一个全局集合。然而,名称会被本地化到执行导入的函数。
一般建议在 Tim Peters 的《Python 之禅》诗中给出:
扁平优于嵌套。
我们通常会努力使函数定义在一个相对简单、扁平的序列中。除非真正需要,否则我们会避免嵌套,例如在创建装饰器时。
与命名空间一起工作
当一个函数被评估时,Python 会创建一个局部命名空间。当参数值(或默认值)被分配时,参数变量在这个局部命名空间中创建。函数体中的语句块中创建的任何变量也会在这个局部命名空间中创建。
正如我们在第四章中提到的,变量、赋值和作用域规则,每个对象都有一个引用计数器。作为函数参数提供的对象,在函数的语句套件执行期间,其引用计数会增加。
当函数完成时——无论是由于显式的return语句还是套件末尾的隐式返回——命名空间被移除。这将减少对参数对象的引用数。
当我们评估像more_dice_good(2, hand)这样的表达式时,字面整数2将被分配给n参数变量。在函数执行期间,它的引用计数为1。分配给hand变量的对象将被分配给collection参数。在函数执行期间,这个对象将有一个引用计数为2。
当函数退出时,命名空间被移除,这将移除两个参数变量。分配给n变量的2对象将结束时的引用计数为零,这个int对象可以从内存中移除。分配给collection变量的对象将它的引用计数从两个减少到一;它不会被从内存中移除。这个对象仍然被分配给hand变量,并且可以在其他地方继续使用。
使用局部命名空间的好处是,我们可以自由地将对象分配给参数,而不用担心对象会被覆盖或从内存中移除。这还允许我们在函数体内自由创建中间变量,因为我们知道变量不会覆盖脚本中其他地方使用的某个变量。
当我们引用一个变量时,Python 会在两个地方查找该变量。它首先在局部命名空间中查找。如果找不到变量,Python 然后搜索全局命名空间。
当我们导入一个模块,比如random,我们通常在脚本的开头写import,这样模块就被导入到全局命名空间。这意味着使用random.randint()的函数首先会在局部命名空间中查找random;如果没有找到,它将检查全局命名空间并找到导入的模块。
这种回退到全局命名空间的做法允许我们在脚本文件中自由地重用导入的模块、函数定义和类定义。我们还可以在一定程度上共享全局变量。默认行为是我们可以读取全局变量的值,但不容易更新它们。
如果我们在函数中写入global_variable = global_variable + 1,我们可以获取名为global_variable的全局变量的值。然而,这个赋值操作将在局部命名空间中创建一个新的名为global_variable的变量。实际的全球变量将保持不变。
赋值全局变量
如果我们想要为一个没有作为参数提供的变量赋值怎么办?我们可以编写一个函数来更新全局变量。这可能导致程序令人困惑,因为几个函数可能通过全局变量共享公共状态。
要在全局命名空间而不是局部命名空间中创建名称,我们使用 global 语句。这标识了必须在全局命名空间而不是局部命名空间中找到的变量。以下是一个更新全局变量的函数示例:
import random
def roll_dice_count_7():
global sevens
d= random.randint(1,6), random.randint(1,6)
if d[0] + d[1] == 7:
sevens += 1
return d
我们定义了一个函数,并使用 global 语句来声明名为 sevens 的变量将在全局命名空间中找到。我们创建了两个随机数,并将这对数赋给一个局部变量 d。这个变量将在局部命名空间中创建,并且不会与其他命名空间中定义的其他变量冲突。
每当两个骰子的总和为七时,全局变量就会被更新。这是一个可能令人困惑的副作用。它必须明确记录,并且需要一些仔细的单元测试。
两个内置函数 globals() 和 locals() 可以帮助澄清在函数被评估时可以使用的变量。如果我们直接在 return 语句之前添加一个 print() 函数,我们会看到如下结果(一些细节被省略):
globals={'__cached__': None,
'__loader__': <_frozen_importlib.SourceFileLoader object at 0x100623750>,
'sevens': 20,
'__name__': '__main__',
'__file__': '…',
… etc.
'roll_dice_count_7': <function roll_dice_count_7 at 0x10216e710>,
'random': <module 'random' from '...'>}
locals={'d': (2, 1)}
globals 函数包括像 sevens 这样的变量,它包括随机模块和 roll_dice_count_7 函数。它还包括一些系统变量:like __cached__、__loader__、__name__ 和 __file__。
locals 函数包括局部变量 d 以及没有其他内容。
赋值非局部变量
当一个函数在另一个函数内部定义时,外部函数可以包含既不是内部函数的局部变量也不是全局的变量。我们称这些为非局部变量。在某些情况下,我们可能想要设置一个属于封装函数的变量。
嵌套函数定义最常用于定义装饰器。我们将在第十三章《元编程和装饰器》中探讨这一点。
这里有一个关于嵌套函数和非局部共享变量的虚构示例:
def roll_nl(n=2, d=6):
def dice():
nonlocal total
points= tuple(random.randint(1,d) for _ in range(n))
total = sum(points)
return points
total= 0
dice()
return total
我们定义了一个名为 roll_nl() 的函数,该函数将模拟掷骰子。函数的主体包括一个嵌套函数定义 dice()。其余的主体创建了变量 total,评估内部 dice() 函数,并返回 total 变量的值。
total 变量是如何被设置为非零值的?在 roll_nl() 函数的主体中并没有更新它。
在嵌套的dice()函数中,有一个对名为total的变量的非局部引用。这个变量必须存在于外部命名空间中,但不一定是全局命名空间。dice()函数创建一个包含n个骰子值的tuple对象。这个表达式从一个生成器函数的结果构建一个tuple。它更新非局部的total变量,即points元组的总和。nonlocal语句确保total变量是dice()函数容器的一部分。dice()函数的返回值是骰子元组,这个值实际上并没有被真正使用。
定义 lambda 表达式
lambda 形式是一种退化的函数。lambda 甚至没有名字:它只有参数和一个单一的表达式。我们通过提供参数名称和表达式来创建 lambda。它看起来像这样:
lambda x: x[0]+x[1]
这种方法在 Python 的高阶函数上下文中很有帮助。我们经常与max()、min()、sorted()、map()、filter()或list.sort()一起使用 lambda。这里有一个简单的例子:
>>> colors = [
... (255,160,137),
... (143, 80,157),
... (255,255,255),
... (162,173,208),
... (255, 67,164),
... ]
>>> sorted(colors)
[(143, 80, 157), (162, 173, 208), (255, 67, 164),
(255, 160, 137), (255, 255, 255)]
>>> sorted(colors,
... key= lambda rgb: (rgb[0]+rgb[1]+rgb[2])/3)
[(143, 80, 157), (255, 67, 164), (162, 173, 208),
(255, 160, 137), (255, 255, 255)]
我们创建了一个简单的列表对象,其中包含四个 RGB 颜色值。如果我们对这个列表使用sorted()函数,颜色将按照红色分量值排序。如果红色分量相等,则使用绿色分量。在红色和绿色分量都相等的情况下,则使用蓝色分量。
如果我们想要按亮度排序颜色,我们不能简单地按红色、绿色和蓝色排序。对亮度的感知是微妙的,有多个公式可以近似这种现象。我们只选择了一个,那就是平均 RGB 值。这个公式没有考虑到我们的眼睛对绿色更敏感的事实。
sorted()函数接受第二个参数key,我们在第二个示例中提供了关键字参数。我们不是写一个完整的函数定义,这个定义只会包含一个表达式,而是将表达式(rgb[0]+rgb[1]+rgb[2])/3打包成一个 lambda。
语法 lambda rgb: (rgb[0]+rgb[1]+rgb[2])/3 等同于以下函数定义。
def brightness(rgb):
return (rgb[0]+rgb[1]+rgb[2])/3
lambda 表达式更紧凑。如果我们只需要在某个地方使用这个表达式,那么一个可重用的函数可能就不合适了。lambda 是一个提供简单表达式且开销最小的简单方法。如果我们认为我们需要编写复杂的 lambda 表达式——不仅仅是简单表达式,或者我们需要重用 lambda,那么我们应该考虑使用合适的函数。
编写附加函数注释
Python 增强提案(PEP)编号 3107 指定了可以应用于函数定义的附加注释。此外,PEP 482、483 和 484 涵盖了相关的一些想法。
这之所以重要,仅仅是因为 Python 有一些可选的语法,我们可能会看到。在 Python 3.5 中,可能会有提供此类信息类型的一些额外工具。注释的代码可以看起来像这样:
def roller( n: int, d: int = 6 ) -> tuple:
return tuple(random.randint(1,d) for _ in range(n))
此函数在每个参数之后包含额外的: 表达式注解。它还包括一个-> 表达式注解来显示函数的返回类型。本例中的所有注解表达式都是内置类型的名称。
为了描述更复杂的结构,一个额外的类型模块可以提供定义更精确的Tuple[int, …]作为此函数返回类型的工具。这是一个令人兴奋的发展,可能会避免某些类型的错误。
这些注解是合法的 Python3 语法。它们没有正式定义的语义,这意味着它们是可选的。有一些增强项目正在努力利用这些可选注解,并创建可以使用那里提供的信息的工具。虽然很少使用,但完全合法。
摘要
我们探讨了 Python 中用于函数定义的许多功能。我们探讨了如何定义函数的名称和参数,提供默认值以使参数可选。我们还探讨了如何向函数提供参数:我们可以按位置提供参数,或者使用参数变量名作为关键字。我们可以通过位置将值映射到参数上,通过评估function(*args)。我们也可以通过评估function(**kw)将字典中的值按名称映射到参数上。当然,我们也可以结合这两种技术。
我们已经探讨了函数如何通过return语句返回值。我们还探讨了不返回值的函数。技术上,它们返回一个None值,Python 编程的其他部分会忽略这个值。
我们探讨了尝试在函数定义中将可变对象作为默认值使用的重要问题。大多数情况下,将可变对象作为默认值将会引起问题。
除了函数定义的基本知识之外,我们还探讨了如何将局部变量分配给临时命名空间。我们还探讨了如何使用global语句在全局命名空间中创建变量。我们还探讨了如何使用嵌套函数定义来操作嵌套函数非局部但不是容器函数全局的变量。
在第八章中,我们将探讨生成器表达式和函数。这些是可迭代的函数,与for循环协同工作,以处理集合和数据序列。
第八章。更高级的函数
在 第七章 中,我们探讨了定义返回单个结果的函数的核心特性。即使函数在语句序列的末尾有一个隐式的 return 语句,或者一个没有表达式的 return 语句,也会返回一个结果:None 对象是默认的返回值。在本章中,我们将探讨返回多个结果的函数。生成器函数定义了一个可迭代对象:它可以与 for 语句一起使用。这意味着生成器不会产生一个包含所有结果项的单个对象;相反,它会单独产生每个结果项。
Python 提供了生成器表达式和推导式,这些与生成器函数的概念相辅相成。我们可以编写简单的表达式,表示一个一次生成一个值的值序列。我们可以使用生成器表达式通过推导式创建 list、set 或 dict 对象。
我们将回顾 for 语句及其与可迭代数据的关系。这将帮助我们理解生成器函数是如何工作的。我们还将查看一些既适用于集合对象也适用于生成器函数的函数。这包括内置的归约函数,如 max()、min() 和 sum(),以及高阶函数,如 map()、filter()、functools.reduce() 和 itertools 模块中的函数。
本章将简要介绍一些函数式编程的概念。关于 Python 中的函数式编程,可以写一本书。有关更多信息,请参阅www.packtpub.com/application-development/functional-python-programming。我们将关注核心内容。
使用 for 语句与可迭代集合
Python 允许我们使用 for 语句与任何类型的集合一起使用。我们可以编写类似 for x in coll 的语句来处理 list、set 或 dict 的键。这是因为所有 Python 集合都有在 collections.abc 模块中定义的共同抽象基类。
这是通过基类 Sequence、Set 和 Mapping 的一个共同特性实现的。类中的 Iterable 混合是每个类定义的一部分。这个抽象实现的保证是所有内置的集合都将与 for 语句协同工作。
让我们打开内部结构,看看它是如何工作的。我们将使用这个复合 for 语句作为一个具体的例子:
for x in coll:
print(x)
从概念上讲,这个复合语句开始于一个非常类似于这个赋值的东西:coll_i=iter(coll)。这将获取 coll 集合的迭代器对象。这个 iter() 函数将利用特殊方法 __iter__() 来产生迭代器对象。我们可以用一条简单的规则来总结这个工作原理:如果变量 coll 不引用一个合适的集合,将引发 TypeError 异常。
给定结果迭代器对象 coll_i,for 语句可以评估 x=next(coll_i) 来从迭代器中获取每个项目。这将利用特殊方法 coll_i.__next__() 从原始集合中产生一个项目。
如果 next(coll_i) 的评估返回一个项目,这个项目将被分配给 x,并且语句序列将使用这个值绑定到 x 变量来执行。我们将看到 x 的值被打印出来。
如果 next(coll_i) 引发 StopIteration 异常,则底层集合已无项目,循环将正常结束。在引发任何其他异常的情况下,这简单地根据标准异常规则传播。(我们将在第九章异常中查看异常。)
迭代器和可迭代集合
当集合实现了 __iter__() 特殊方法时,它是可迭代的。几乎在所有情况下,这意味着它将是 collections.abc 模块中定义的 Iterable 类的子类。这个特殊方法的存在意味着在集合对象上评估 iter() 将返回一个迭代器对象。
集合的迭代器必须实现 __next__() 和 __iter__() 特殊方法。通常,一个迭代器对象通过返回自身作为结果来实现 __iter__() 方法。这种自洽的冗余意味着我们不仅可以创建一个显式的迭代器,还可以将其提供给 for 语句而不引发异常;for 语句的处理可以评估 iter(object) 而无需检查该对象是否已经是迭代器。
如果我们有一系列项目,其中包含我们想要忽略的标题,这种情况通常发生在源数据文件包含必须单独处理的标题行时。我们可以利用显式的迭代器对象来丢弃顺序集合中的项目。
我们可能会写一些像这样的事情:
source_iter= iter(source)
heading= next(source_iter)
for data in source_iter:
print(data)
在这个例子中,我们创建了一个基于源集合或生成器的迭代器,命名为 source_iter,这个名字缺乏想象力,叫作 source。当我们评估 next(source_iter) 时,我们从集合中消耗了第一个项目,然后将其分配给 heading 变量。然后我们可以使用迭代器对象在 for 语句中消耗该迭代器中的其余项目。
实际上,前面的例子几乎与这个相同:
heading, *rest = source
for data in rest:
print(data)
第二个示例实际上是对源集合进行了浅拷贝,并将这个拷贝赋值给 rest 变量。我们几乎加倍了使用的内存量。对于小列表来说,这无关紧要。对于更大的集合来说,这可能会成为一个问题。
如果源是一个打开的文件或基于打开文件的生成器,将 rest 集合中的数据实体化可能是不可行的。文件太大,无法放入内存,是它们自己独特问题的一部分,有时被称为“大数据”。显式使用 iter() 函数允许我们避免创建可能不适合内存的大型集合的风险尝试。
后果和下一步
有三个重要的后果是 for 语句使用 coll_i= iter(x) 和 x=next(coll_i) 的方式:
-
我们可以编写隐式具有所需接口的生成器表达式,以便作为
Iterable类工作 -
Python 给我们提供了编写作为
Iterable类工作的生成器函数的方法 -
我们可以创建自己的类,这些类实现了实现
Iterable抽象基类所需的特殊方法名称
我们将开始编写生成器表达式。我们可以使用这些表达式来创建 list、set 和映射“推导式”。推导式是一个定义集合内容的表达式。
我们将探讨编写生成器函数。yield 语句改变了函数的语义,从“简单”的(或“普通”)变为生成器。
虽然 第十一章,类定义 是关于类定义的主题,但我们不会深入探讨如何创建我们自己的独特集合。Python 已经提供了如此多的集合,定义自己的并不是真的必要。
使用生成器表达式和推导式
我们可以将简单的生成器表达式视为一个有三个操作数的运算符。这三个操作数的语法与 for 语句平行:
(expression for target in source)
我们指定一个 表达式,该表达式为从 源 分配给 目标 变量的每个值进行评估。还有更复杂的生成器,我们将在后面探讨。
生成器表达式在 Python 中可以自由使用。它们可以在任何有意义的序列或集合中使用。
重要的是要注意,生成器表达式是惰性的,或者说“非严格的”。它实际上不会计算任何东西,直到某个消耗操作要求它提供值。为了看到这一点,我们可以在 REPL 中尝试评估一个生成器表达式:
>>> (2*x+1 for x in range(5))
<generator object <genexpr> at 0x1023981e0>
Python 只告诉我们我们创建了一个生成器对象。由于我们没有编写一个表达式来消耗这些值,我们看到的就是这个对象,被动地等待被评估。
探索生成器表达式的最佳方式是应用一个函数,例如 list() 或 tuple(),它将消耗生成器的值并从它们中构建一个集合对象。以下是一个示例:
>>> tuple(2*x+1 for x in range(5))
(1, 3, 5, 7, 9)
在这个例子中,tuple() 函数从生成器对象中消耗值,并从这些值创建了一个 tuple 对象。而不是显示生成器对象,REPL 显示了我们从生成值创建的 tuple。
我们可以使用生成器表达式进行各种处理。itertools 模块中有几种模式。
生成器表达式的局限性
生成器表达式有一些局限性。最明显的局限性是,某些语言特性仅作为 Python 语句可用。如果我们需要执行异常处理、上下文管理或通过 break 语句提前退出循环,我们无法编写生成器表达式。我们必须求助于编写完整的生成器函数。
另一个不那么明显的局限性是,生成器表达式表现得非常像序列。但它只能这样做一次。在生成器第一次终止后,每次引用它时都表现得像空序列。这里有一个具体的例子:
>>> x= (2*x+1 for x in range(20))
>>> sum(x)
400
>>> sum(x)
0
在这个例子中,我们将生成器表达式赋值给变量 x。当我们计算 sum(x) 时,sum() 函数消耗了生成器表达式产生的所有值:在这个例子中总和是 400。一旦我们使用了生成器,它仍然有效,但它不再生成值。所有后续的 sum(x) 评估都将产生 0。
没有特殊的异常来警告我们正在重用已经耗尽的迭代器。在某些情况下,程序可能看起来是损坏的,因为我们使用了生成器表达式而不是 list 或 tuple 序列。修复方法几乎总是将生成器转换为 tuple 对象,以便它可以被两次使用。我们可以将 x= tuple(2*x+1 for x in range(20)) 改变以查看差异。
当与生成器函数或表达式一起工作时,iter(some_function) 将返回生成器对象,因为它是一个迭代器。在集合对象的情况下,iter(some_collection) 将创建一个具有集合引用的迭代器对象。结果将是一个不同的对象。一个函数可以使用 iter(param) is iter(param) 来检测生成器函数和具体集合之间的差异。
在某些情况下,我们可能会包含语句 assert iter(param) is not iter(param), "Collection object required" 以在将生成器函数作为参数传递给遍历集合超过一次的函数时引发异常。
使用多个循环和条件
生成器的主体可以包含多个 for 子句。这允许我们遍历多个维度。我们可以编写这样的表达式:
>>> deck= list((r,s) for s in '♣♦♥♠' for r in range(1,14))
>>> deck # doctest: +ELLIPSIS
[(1, '♣'), (2, '♣'), (3, '♣'), ... (11, '♠'), (12, '♠'), (13, '♠')]
>>> len(deck)
52
生成器表达式有两个for子句:for s in '♣♦♥♠'和for r in range(1,14)。从结果中可以看出,右侧的for子句执行得最频繁。这遵循了如果我们将其重写为嵌套for语句时我们会看到的嵌套规则。右侧的for子句就像一个最内层的for语句。
此外,生成器的主体可以包含if子句。这些可以用来过滤由for子句创建的值。以下是一个生成器表达式中条件处理的例子:
>>> list(x for x in range(36) if x%5 == 0 or x%7 == 0)
[0, 5, 7, 10, 14, 15, 20, 21, 25, 28, 30, 35]
在这个例子中,表达式仅仅是目标变量x。来源是range(36),包括零和 35 的数字。我们包含了一个if子句,它只会传递那些是五或七的倍数的值。所有其他值将被拒绝。为了看到结果,我们将生成器的值收集到一个list对象中。
编写推导式
我们可以利用生成器表达式的变体来创建list、set或dict对象。这些被称为推导式,它们代表从惰性生成器构建的实体对象。
这里有一些简单的例子:
[2*x+1 for x in range(5)]
{x for x in range(36) if x%5 == 0 or x%7 == 0}
{n: 2*n**2-3*n-14 for n in range(-5,6)}
第一个例子使用[]创建一个list推导式。这将创建一个从一到九的奇数值列表。第二个例子使用{}创建一个set推导式。这将基于五或七的倍数创建一个集合。
第三个例子创建一个dict推导式。{}用于括号表达式。使用:字符来分隔键和值,将dict推导式与set推导式区分开来。这个字典提供了从n的映射。
这个最后的例子可以用作对深度嵌套表达式的优化。在映射中查找值比反复重新计算要快。使用@lru_cache装饰器可以提供类似性能的好处。
使用yield语句定义生成器函数
生成器函数具有与生成器表达式相似的属性。生成器函数不是一个单独的表达式,而是一个完整的 Python 函数。它具有第七章中描述的函数的所有特性,基本函数定义。它还有一个额外的特性,即它是一个迭代器,能够生成一系列项目。
当我们使用yield语句时,函数的语义会发生变化。没有yield,函数将返回单个值。有yield语句时,函数将表现得像迭代器,向消费者提供多个值。
以下是一个生成器函数的例子,它将一系列值应用于模型以计算结果域。我们将对一系列输入值应用模型以计算每个输入的结果:
def model_iter(until):
for n in range(0, until):
yield n*(n+1)//2
这个 model_iter() 函数接受一个参数,until,它表示该函数生成的值的数量。函数体包含一个 for 语句,该语句将 n 变量设置为 range() 对象定义的值。
该函数的基本特性是 yield 语句。由 yield 语句创建的每个值都将成为此语句产生的项目序列的一部分。
这是我们使用此函数的一种方法:
>>> list(model_iter(6))
[0, 1, 3, 6, 10, 15]
在这个例子中,我们将结果收集到一个单独的 list 对象中。创建一个 list 对象只是我们可以做的许多事情之一。我们同样可以计算给定范围的平均值,将模型的结果相加。
>>> mean = sum(model_iter(6))/6
>>> round(mean, 4)
5.8333
在这个例子中,我们将 model_iter() 生成器的结果传递给 sum() 函数。这样可以避免构建大量结果集合。sum() 函数将消费生成器函数产生的所有值。我们可以使用这种结构处理成千上百万的值,因为不会在内存中生成一个大的 list 或 set。只有单个项目被处理。
使用高阶函数
接受函数作为参数或返回函数作为结果的函数被称为高阶函数。Python 有许多高阶函数。这些函数中最常用的是 map()、filter() 和 sorted()。itertools 模块包含许多其他高阶函数。
map() 和 filter() 函数是生成器;它们的输出必须被消费。它们都将一个函数应用到值集合上。在 map() 的情况下,函数的结果被产生。在 filter() 的情况下,如果函数的结果为真,原始值被产生。
这是我们如何将一个非常简单的函数——简单到我们将其编码为 lambda——应用到一系列值:
>>> mapping= map( lambda x: 2*x**2-2, range(5) )
>>> list(mapping)
[-2, 0, 6, 16, 30]
该函数只是一个表达式,2*x**2-2。我们已经将此函数应用于 range() 对象提供的值。结果是生成器,我们需要消费这些值。我们使用了 list() 来创建一个可以打印的集合。这些值是应用给定函数到源集合中每个值的计算结果。
这是我们如何使用 filter() 对值序列应用简单逻辑测试:
>>> fb= filter( lambda n: n%5==0 or n%7==0, range(16) )
>>> [n for n in fb]
[0, 5, 7, 10, 14, 15]
我们定义了一个简单的函数作为 lambda;函数 n%5==0 or n%7==0 对于五的倍数或七的倍数是真实的。我们将这个过滤器应用于 range() 对象生成的值。结果只包括给定函数为 True 的值。所有其他值都被拒绝。
我们使用列表推导式将值收集到一个 list 对象中。这个列表推导式没有进行计算和过滤,因此它与 list(fb) 等效。
我们可以使用生成器表达式实现 map() 和 filter() 的简单版本:
-
map(function, iterable)与(function(x) for x in iterable)相同 -
filter(function, iterable)与(x for x in iterable if function(x))相同
map() 函数可以处理额外的可迭代对象,比生成器表达式提供更多的复杂性。
sorted() 函数类似于 map() 和 filter()。sorted() 函数对其参数遵循不同的设计模式。map() 和 filter() 函数首先接受一个函数,然后是一个要处理的项。sorted() 函数首先接受一个要排序的项,以及一个可选的函数,该函数定义了排序的键,以及一个可选的布尔值,用于反转键比较的意义。我们将在后面的 三种排序序列的方法 部分详细探讨排序。
itertools 模块包含大量可以组合以创建复杂处理的生成器函数。有关此模块如何工作的更多信息,书籍 Functional Python Programming,作者 Steven Lott,由 Packt Publishing 出版,为此主题奉献了两章(www.packtpub.com/application-development/functional-python-programming)。
编写我们自己的高级函数
可能最简单的高级函数类型是基于生成器表达式。由于生成器表达式是惰性的,其行为更像是一个函数而不是包含相关数据的对象。返回生成器的函数依赖于其他编程片段来实际消费生成器产生的数据。
常见的文件输入需求是去除尾随标点符号并忽略空白行。我们将假设遵循 Python 注释规则的编程语言。
这里是一个返回生成器的函数的例子:
def text_cleaner( source ):
stripped = (line.strip() for line in source)
partitioned = (line.partition("#") for line in stripped)
decommented = (data.rstrip() for data, sharp, comment in partitioned)
non_empty = (line for line in decommented if line)
return non_empty
我们将处理分解为四个独立的生成器函数。函数的结果是这四个生成器中的第四个,但这取决于其他生成器来产生结果。由于生成器是惰性的,直到函数或语句消费生成器产生的数据之前,不会发生任何处理。我们必须使用 for 语句或 list() 或 tuple() 函数的结果来消费数据。
当消费过程迭代此函数的结果时,它将接收到来自 non_empty 生成器表达式的单个文本行。non_empty 生成器过滤由 decommented 生成器表达式创建的行。decommented 生成器反过来依赖于 partitioned 和 stripped 生成器表达式来移除注释和空白。
重要的是,处理流程的管道是 text_cleaner() 函数的返回值。这个函数不处理任何数据。这个函数返回一个生成器表达式,它将处理一些数据。
这些生成器也可以重写为使用 map() 或 filter()。我们将把这个作为读者的练习。
我们可以这样使用 text_cleaner() 函数:
>>> text = '''
... # options
... db=name # database
... task=delete # task
... '''.splitlines()
>>> for line in text_cleaner(text):
... print(line)
db=name
task=delete
我们创建了一些带有注释和数据的文本。数据的格式看起来是 name=value 设置。text_cleaner() 函数对数据的格式不敏感,只对注释和空白敏感。我们应用了 splitlines() 函数,使文本块表现得像文件。
text_cleaner() 函数的结果是一个函数,它移除了注释、前导和尾随空格,只留下了文件的有意义内容。在这个例子中,我们使用了一个 for 语句来消费生成器函数产生的数据。
这可能是更复杂过程的一部分,该过程使用这些 name=value 行作为配置参数。
生成器函数的重要之处在于它们是完全懒加载的。它们不会在内存中创建巨大的数据结构。它们只处理满足消费者请求的最小数据量。这减少了开销。此外,每个生成器可以相对简单,从而可以从简单的部分构建出表达性的组合。
使用内置归约函数 – max、min 和 reduce
我们还有两个内置的高阶函数可以接受函数作为参数。这些可以描述为归约:它们将一组值归约为一个值。还有一个内置的归约函数 sum,但它不是一个正确的高阶函数:我们无法通过插入函数来定制其操作。
max() 和 min() 归约遵循 sorted() 函数的设计模式:它们首先接受一个可迭代对象,并且可以通过可选的键函数进行自定义。我们将首先展示默认行为,然后展示如何使用键函数进行自定义:
>>> data = ["21", "3", "35", "4"]
>>> min(data)
'21'
>>> min(data, key=int)
'3'
在第一个例子中,字符串对象是通过字符串比较来比较的。这导致了一个异常,即看到 "21" 看起来小于 "3"。实际上,以 "2" 开头的字符串会排在以 "3" 开头的字符串之前,但这可能不是程序需要显示的输出。
在第二个例子中,我们为 min 函数提供了 int() 函数,用于比较项。这意味着字符串被作为整数比较,而不是作为字符串。这选择了具有最小整数值的字符串 "3"。
注意,我们没有写 min(data, key=int())。我们不是评估 int 函数。我们提供 int 函数作为对象,min() 函数将使用它。
此外,还有一个通用的 functools.reduce() 函数,可以用来构建新的归约类型。这个函数接受一个二元函数、一个可迭代对象和一个初始值。它可以计算各种归约。
排序序列的三种方法
Python 为我们提供了三种处理复杂项 list 排序的通用方法。
-
我们可以使用
sorted()生成器函数进行排序。这会将对象作为排序的一部分进行复制。 -
我们可以使用列表的
sort()方法和键函数对列表进行排序。这将使列表按请求的顺序进行修改。 -
我们可以创建一个中间序列的对象,这些对象可以很容易地进行排序。这有时被称为包装-排序-解包设计模式。
为了详细查看这些,我们需要一个可以排序的复杂对象集合。我们将使用基于NIST 工程统计手册案例研究的简单数据集,第 7.1.6 节。更多信息请见www.itl.nist.gov/div898/handbook。
我们已经整理和清理了一些度量数据,看起来是这样的:
>>> data
[['2013-09-10', '289'], ['2013-09-11', '616'],
. . . ,
['2013-12-07', '752'], ['2013-12-08', '739']]
我们有一个包含 90 对的对列表结构。由于日期字符串格式良好,为yyyy-mm-dd,我们可以很容易地使用sorted(data)函数或data.sort()方法将其按日期顺序排序。请注意,sorted(data)将创建data对象的副本。data.sort()方法将就地修改data对象。
我们如何按计数对数据进行排序呢?我们可以将键函数应用于sorted()函数或sort()方法。我们首先看看这些。作为替代,我们可以使用包装-排序-解包设计模式。
通过键函数进行排序
按计数对度量数据进行排序需要我们使用一个函数来改变项目比较的方式。在这种情况下,我们需要一个更复杂的键函数来完成两件事。它必须选择每个两个数据点的第二个项目,并且必须将第二个项目转换为适当的整数值。
我们可以使用这两个示例中的任何一个按计数进行排序:
>>> data.sort(key=lambda x: int(x[1]))
>>> by_count= sorted(data, key=lambda x: int(x[1]))
两个示例都使用了一个 lambda 表达式,该表达式对每个两元素列表中的第二个项目执行整数转换。第一个示例更新了数据对象。第二个示例创建了一个新的对象,它是数据对象的克隆,并已排序。
通过包装和解包进行排序
使用一对生成器表达式可以实现包装-排序-解包设计模式。第一个将创建从每个原始数据块中生成的两元组。每个新两元组中的第一个项目是适当的排序键。第二个生成器将选择这些两元组中的第二个项目以恢复原始对象。
整个序列看起来是这样的:
>>> wrapped = [(int(x[1]), x) for x in data]
>>> wrapped.sort()
>>> by_count = [x[1] for x in wrapped]
在第一步中,我们将每条原始数据转换成了一个(sort key, original item)的两元组。我们使用列表推导式创建了一个新的对象,我们可以对其进行排序,而原始对象保持不变。一旦我们这样做,默认的排序操作就可以正确工作。一旦数据排序完成,我们就可以轻松地恢复原始项目。在这种情况下,我们使用列表推导式创建了另一个列表对象。
在这两种情况下,我们可以稍微调整一下,使用map()函数而不是生成器表达式。例如,我们可以使用map(lambda item: (int(item[1]), item), data)来包装项目。
注意,map()函数是一个生成器:它是惰性的。列表理解消耗数据并创建一个有形的对象。我们无法通过简单的复制粘贴从列表切换到生成器。我们需要从 map 生成器创建一个列表对象,或者使用sorted(),它从一个生成器创建列表。
当包装函数相当复杂时,通常会使用包装-排序-解包的方法。我们可能有一个执行数据库查询、文件合并或作为排序一部分的极其复杂的计算的生成器。在这些情况下,编写一个简单的 lambda 函数可能很困难。
函数式编程设计模式
Python 中高阶函数的存在使我们能够利用许多函数式编程设计模式。要了解更多关于这些设计模式的信息,一个好的起点是itertools模块。该模块中的函数提供了许多示例,说明了我们如何编写简单的函数来进行复杂的处理。
此外,我们还可以使用functools模块的一些功能。它包含通用的reduce()函数。它还包含一些可以帮助我们编写装饰器的函数。正如我们在第十三章 Metaprogramming and Decorators 中将要看到的,装饰器是一种高级函数:它修改原始函数的定义。这是函数式编程的另一个方面。
最重要的是,我们有两种方法来处理算法:
-
我们可以在大量数据集合中处理项目,创建副本、子集或转换的额外集合。
-
我们可以通过迭代大量数据集合,就像我们正在创建额外的集合一样来处理项目。实际上,我们不需要创建副本、子集或转换,我们可以使用迭代器、过滤函数和映射函数。
当我们有替代方案时,我们可以选择简洁且表达力强的变体。
摘要
在本章中,我们看到了函数的许多高级特性。我们研究了基本的生成器表达式以及它是如何作为理解的一部分使用的。列表理解从生成的值中组装列表。同样,集合理解创建集合。字典理解从生成器表达式中的键和值创建字典结构。
我们研究了使用yield语句创建生成器函数。这允许我们在创建生成器时使用所有各种 Python 语句功能。由于生成器是可迭代的,它可以与for循环一起使用,这样我们就可以编写一个简单的循环来处理由迭代器创建的多个值。
我们还研究了高阶函数。这些函数接受函数作为参数或产生函数作为结果。使用高阶函数,我们可以将我们的算法重构为可以组合以创建所需行为的函数。
在第九章,异常中,我们将探讨 Python 如何引发异常,我们如何捕获这些异常,以及我们需要编写什么样的异常处理。
第九章。异常
Python 处理意外情况的一般方法是通过引发异常。想法是操作要么正常完全工作,要么引发异常。在某些语言中,使用复杂的数值状态码来指示成功。在 Python 中,假设成功;如果有问题,会引发异常来指示操作未成功。
Python 程序的所有方面都可以引发异常。所有内置类都涉及各种意外情况的异常。许多库包定义了自己的独特异常,这些异常扩展了内置异常层次结构。
我们首先看看异常背后的基本概念。Python 有一些我们将使用的语句。raise语句创建一个异常对象。try语句允许我们处理异常。
try语句中的except子句用于匹配引发的异常类。在一些编程语言中,我们严格匹配特定的异常类。在其他情况下,我们使用更不具体的异常类或异常类列表,以统一的方式处理各种异常。
核心异常概念
异常背后的核心概念可以总结为:“当不确定时,引发异常”。在典型情况下,每个 Python 函数或方法都会返回一个值或有一些文档化的副作用。对于所有不在“成功路径”上的情况,Python 的方法是引发异常。
尽管大多数异常描述了错误情况,但异常并不一定是错误。它只是给定函数无法处理的异常情况。例如,当迭代器无法再产生结果项时,会引发StopIteration异常。这是迭代器对象生命周期中仅发生一次的异常情况。
当处理数字时,作为第二个例子,除以零是异常的。如果我们除以任何其他值,成功的路径将引导我们得到结果。虽然可以通过构造非数字(NaN)值作为除以零的结果,但让除法运算符引发ZeroDivisionError异常更简单——也更通用。除以零不是正常或预期的设计。几乎普遍来说,除以零表示以下情况之一:
-
设计问题:零是一个可能的情况,但设计没有处理这种情况。
ZeroDivisionError异常是意外的。设计问题的根本原因可能是对需求理解不足:可能是匆忙整理的故事,也可能是对问题域理解的其他问题。 -
实现问题:零的出现是因为一个错误。
ZeroDivisionError异常同样意外。根本原因可能包括单元测试不足。 -
应用误用:用户提供了导致除以零的输入。整体应用可以提供一个有用的错误消息并等待不同的输入。或者,整体应用可能可以使用更适合输入值的另一种计算方法。
异常在其含义上可以是深刻的或浅显的。
当处理字符串时,例如,有许多情况下会引发异常。也有一些情况下,返回状态码而不是抛出异常。我们可以比较str.find()和str.index()在方法上的两个不同之处:
>>> "abc".index("x")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: substring not found
>>> "abc".find("x")
-1.
第一个例子显示了index()方法,当子串找不到时引发异常。第二个例子显示了find()方法,当子串找不到时返回一个特殊数字。
异常被广泛使用。在 Python 中,状态码很少被使用。
检查异常对象
当异常被抛出时,它涉及处理方式的改变以及一些关于条件的数据。异常是更通用类的一个实例。我们将一般地讨论EOFError异常,而不强调给定的异常是EOFError异常类的实例。
与异常关联的数据可以包括根本原因异常和一系列额外的参数。有时这些额外的参数仅仅是一个字符串消息。一些异常可能有更复杂的参数集合。
还有一个包含调用栈的 traceback 对象。这确定了引发异常的函数,调用该函数的函数,以及等等,直到最初启动一切的函数。这个 traceback 信息在一个特别命名的属性__traceback__中。
我们可以通过几种不同的方式创建异常:
-
我们可以创建异常对象,稍后抛出它们以表示问题:
obj = Exception("some message") raise obj -
我们可以一次性创建并抛出异常:
raise Exception("Some Argument", "additional details") -
我们可以创建一个封装根本原因异常的异常:
raise MyError("problem") from some_exception
在最后一种情况中,当一个异常封装了根本原因时,根本原因信息在一个名为__cause__的属性中。
使用 try 和 except 语句
当异常被抛出时,普通的顺序语句执行停止。下一个顺序语句不会执行。相反,会检查异常处理程序以找到与给定异常类匹配的except子句。这个搜索从当前函数开始,沿着调用栈向下到调用它的函数。如果找到一个与异常匹配的except子句,那么在except子句中继续普通的顺序执行。当except子句完成后,try语句也结束了。从那里开始,正常的顺序语句执行在try语句之后继续。
如果没有except子句与给定的异常匹配,则会打印异常和跟踪信息。处理停止,Python 退出。通常,退出状态是非零的,表示 Python 程序异常结束。
函数内部的try语句看起来像这样:
def clean_number(text):
try:
value= float(text)
except ValueError:
value= None
return value
我们定义了一个将文本转换为数字的函数。我们将静默ValueError异常,并返回None对象而不是引发异常。我们可能会在清理 CSV 文件时使用它,以便将没有正确数值的单元格替换为None对象。
当我们将它应用于数字时,我们可以看到它的运行情况,如下所示。
>>> row = ['heading', '23', '2.718']
>>> list(map(clean_number, row))
[None, 23.0, 2.718]
>>> clean_number("1,956")
在这个例子中,我们将clean_number()函数应用于 CSV 读取器的数据行。示例数据行显示了快乐路径和异常路径。在快乐路径上,两个数字从字符串转换为正确的浮点值。在异常路径上,不正确的文本被转换为None。
我们还包含了一个处理不好的测试用例。这个类似数字的字符串"1,956"变成了None。我们可能希望即使有嵌入的逗号,也能将其转换为正确的数字。我们可以看到,简单的except子句并没有真正完成我们希望它完成的全部工作。
注意,一些以财务为导向的电子表格值应该转换为Decimal值而不是float值。我们可以创建一个高阶函数,该函数将使用float()函数或Decimal()函数(或任何其他转换函数)来创建所需类型的值。
这里有一个包含两个try语句的修订版本:
from decimal import Decimal, InvalidOperation
def clean_number3(text, num_type=Decimal):
try:
value= num_type(text)
except (ValueError, InvalidOperation):
text= text.replace(",","").replace("$","")
try:
value= num_type(text)
except (ValueError, InvalidOperation):
value= None
return value
在我们这个数字清理函数的版本中,我们有一个额外的参数num_type,它有一个要应用的转换函数。我们提供了一个默认值Decimal,因此它是可选的。函数的主体与上一个版本相同。我们已经更新了第一个except子句,以进行更复杂的回退处理。这种更复杂的处理包括创建一个新的字符串,其中不包含常见的污染数值数据的","或"$"字符。
如果将这个第二个字符串转换为数字,我们将返回一个有用的数值结果。如果这个修订后的字符串不是数字,我们将陷入困境,被迫返回None对象。
注意
作为练习,读者可以创建一个算法将单词转换为数字作为回退。将"twenty one"转换为 21。英语等语言复杂度使得这是一个有趣的挑战。
使用嵌套try语句
clean_number3()函数展示了我们可以嵌套try语句的两种方式之一。在这种情况下,try语句嵌套在单个函数内部。如果在内部try语句中引发异常,则首先检查内部try语句的except子句以匹配异常。然后检查外部try语句的except子句。如果没有这些匹配,则检查调用此函数的函数。
考虑以下例子:
>>> from fractions import Fraction
>>> clean_number3(',2/0,', Fraction)
这将生成一个跟踪输出,显示嵌套try块的行为:
Traceback (most recent call last):
...
ValueError: Invalid literal for Fraction: ',2/0,'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
...
ZeroDivisionError: Fraction(2, 0)
我们省略了一些行号细节,以关注消息的相关部分。第一个异常是在第一次尝试应用Fraction(',2/0,')时引发的。这引发了一个ValueError异常,使我们偏离了快乐路径。Python 在except子句中继续顺序执行。这创建了一个新的字符串,其中移除了","字符。第二次转换尝试不会引发ValueError异常,而是引发ZeroDivisionError异常。
内部try语句没有except子句来匹配此异常。然后 Python 必须搜索外部try语句的except子句以查找匹配的异常。由于外部try语句不匹配异常,整个脚本以未处理的异常结束。
更常见的情况是在单独的函数中包含try语句。嵌套是通过函数调用栈发生的,而不是单个语句块的结构。以下是一个调用我们的clean_number3()函数以创建一排Fraction对象的函数。
def fraction_row(row):
try:
return [clean_number3(item,Fraction) for item in row]
except (TypeError, ZeroDivisionError):
return [None for item in row]
此函数包含另一个try语句。当此函数调用clean_number3()时,调用栈将包括fraction_row()和clean_number3()。如果clean_number3()函数引发未处理的异常,Python 将向下移动调用栈,并在此try语句中搜索匹配的except子句。
在except子句中匹配异常类
在前面的例子中,我们展示了两种except子句的类型:
-
except SomeException: -
except (OneException, AnotherException):
第一个例子匹配单个特定异常。第二个例子匹配列表中的任何特定异常。
在许多情况下,异常的详细信息并不重要。另一方面,有些情况下我们想在异常对象的参数上执行一些处理。我们可以使用此语法将异常对象分配给变量:
except SomeException as exc:
这将把异常实例分配给exc变量。然后我们可以将其写入日志,或检查参数,或修改要打印的跟踪信息。
匹配更一般的异常
Python 异常形成一个类层次结构。通常,我们会具体匹配异常。在少数情况下,我们会使用异常超类而不是具体类。一些最常见的超类是 OSError 和 ArithmeticError 异常。有一些 OSError 的子类提供了关于异常的更详细的信息;在许多情况下,我们对 OSError 超类的细微差别不太感兴趣。同样,OverflowError 和 ZeroDivisionError 之间的区别可能没有帮助。
我们可以这样使用超类异常:
import os
def names(path="."):
try:
return [name
for name in os.listdir(path)
if not name.startswith('.')]
except OSError as exc:
print( exc.__class__.__name__, exc )
raise
我们已经使用超类 OSError 来匹配所有各种 OSError 子类。虽然最可能的异常是 FileNotFoundError 和 NotADirectoryError,但我们也可能得到 OSError 的其他子类之一。在这种情况下,我们不在乎具体问题,所以我们可以使用超类错误。
此示例还使用了不带参数的 raise 语句。在 except 子句中,这将在进行一些初步处理后重新抛出异常。现在,异常将随着 Python 搜索处理程序而沿着调用堆栈传播。
空的 except 子句
Python 允许没有指定异常的 except 子句。这是最通用的异常匹配器:它匹配所有异常类。
由于它与 SystemExit 和 KeyboardInterrupt 异常匹配,随意使用可能会引起问题。当我们处理这个异常时,我们可能会发现我们不能再优雅地退出我们的程序,而必须求助于 SIGKILL 信号来停止程序。
未装饰的 except 子句应持怀疑态度。
创建我们自己的异常
异常的层次结构有一个名为 Exception 的错误相关异常的父类。所有反映基本错误条件的异常都是 Exception 类的子类。所有异常的基类是 BaseException 类;一些非错误相关异常是 BaseException 类的直接子类。
我们可以这样总结层次结构:
-
BaseException-
SystemExit -
KeyboardInterrupt -
GeneratorExit -
Exception- 所有其他异常
-
所有错误相关异常的父类 Exception 非常广泛。我们可以在像这样长期运行的服务器中使用它:
def server():
try:
while True:
try:
one_request()
except Exception as e:
print(e.__class__.__name__, e)
except Shutdown_Request:
print("Shutting Down")
此示例依赖于一个函数,one_request(),它处理单个请求。while 循环永远运行,评估 one_request() 函数。如果抛出了任何与错误相关的 Exception 子类,错误将被记录,但请求处理将继续。
当抛出 Shutdown_Request 异常时,内部的 try 语句不会匹配这个异常。异常将传播出循环到外部的 try 语句。我们可以记录关闭请求,执行任何其他所需的清理,并退出 server() 函数。
类层次结构确保两个非错误异常——KeyboardInterrupt和SystemExit——不会在内部try语句中被错误处理。这些异常是Exception类的同等级别,这就是为什么它们不会匹配。这意味着一个SIGINT信号(在键盘上按Ctrl + C的效果)将干净地终止服务器。此外,如果请求处理的一部分评估sys.exit(),服务器将优雅地关闭。
使用finally子句
我们可以在try语句上包含一个finally子句。这包含一个在try语句结束时始终执行的语句集。这意味着无论是成功路径还是异常路径,都会始终执行finally子句。以下是它的总结:
try:
# Something that might fail.
except SomeException:
# Fallback plan to handle failure.
finally:
# Always executed.
当我们有清理或必须始终执行的结束语句集时,我们会使用这个。这种用法最常见的情况是关闭文件或网络连接,即使异常已被引发并正确处理。
在许多情况下,我们可以使用上下文管理器来正确关闭文件或网络连接。我们可以使用contextlib.closing()来包装具有close()方法但不是正确上下文管理器的对象。我们将在第十章文件、数据库、网络和上下文中查看上下文管理器。
异常的使用场景
异常的使用场景非常广泛。我们将确定几个在 Python 中使用异常的显著领域。
一些异常完全是良性的。StopIteration异常是由一个耗尽值的可迭代对象引发的。for语句从可迭代对象中消耗项目,直到这个异常被引发以表示没有更多数据。同样,当生成器在产生所有数据之前被关闭时,会使用GeneratorExit。这并不是错误;这是一个信号,表示不会请求更多数据。
完全在程序之外的条件下可能被视为异常。意外的操作系统条件或错误通过是OSError异常子类的异常来表示。一些操作系统条件可以忽略;其他可能表明环境或应用程序中存在严重问题。有十几个此类错误的子类,以提供对操作系统条件的更详细描述。此外,内部操作系统错误号也作为参数提供给这些异常,以帮助区分问题的细节。
一些异常是程序内部普通事物引起的。当我们使用str.index()方法时,可能会引发ValueError异常而不是返回一个数值。我们可以捕获并利用这个异常信息作为程序正常操作的一部分。
我们通常会通过异常检测程序的使用不当。可能涉及不良数据,或者尝试了不受支持的运算。在这些情况下,程序可能会使用异常来表示由用户输入问题引起的问题。一个常见的设计模式是在足够高的级别进行异常处理,以捕获、记录并以有意义的方式向用户显示这些问题。一个长期运行的服务器可能只是记录然后处理下一个请求。一个网页可能将输入表单验证包裹在异常处理中,以便用户的响应是带有错误信息的表单页面。
一些异常反映了设计或实现问题。ValueError 异常的意外发生可能表明设计问题或实现问题。它可能表明测试用例不足。在这种情况下,最好是整个程序崩溃,以便可以使用回溯信息来定位和纠正问题。
未预期的异常通常表明程序已损坏。程序将停止;异常的输出可以提供有价值的调试信息。我们可以通过编写不必要的广泛异常处理程序来干扰这种正常行为,但隐藏未预期的异常通常是一个坏主意,因为会丢失有价值的调试信息。
在蒂姆·彼得斯的《Python 之禅》中,有一些诗意的建议:
错误绝不应该默默无闻。
除非明确静默。
这里的想法是,Python 中的未预期异常将以一个大的、嘈杂的错误回溯停止程序。如果我们需要静默异常,我们可以使用广泛的通用 except 语句来捕获和静默它们。
发出警告而不是异常
Python 的 warnings 模块处理异常的特殊子类。我们可以使用 warnings 模块来识别应用程序中的潜在问题。警告模块用于内部跟踪多个内部考虑因素。
警告概念介于完全正常的操作和错误条件之间。我们的程序可能表现不佳,但也不是完全损坏。
在运行单元测试时,我们可能会遇到三种显著的警告类别。由于单元测试框架显示所有警告,我们可能会在测试环境中看到一些在软件的正常操作使用中看不到的警告。
-
DeprecationWarning:此警告由已弃用的模块、函数或类引发。它提醒我们需要修复代码以停止使用此功能。 -
PendingDeprecationWarning:对于已宣布弃用的函数、模块或类,可能会引发此警告。这是一个提示,我们需要在它成为弃用功能之前停止使用此功能。 -
ImportWarning:由于一些模块是可选的或平台特定的,一些导入语句被包裹在try块中;这个警告会引发而不是异常。我们可以暴露这些警告以确保导入被正确处理。
我们可以利用 warnings 模块来暴露那些通常被静默的警告。我们可以使用 warnings.simplefilter("always") 来查看所有警告。
我们可以像这样引发通用的 UserWarning:
>>> import warnings
>>> warnings.warn("oopsie")
__main__:1: UserWarning: oopsie
使用 warnings.warn() 允许我们在应用程序中包含警告信息,而几乎不需要任何开销。我们可以将其用作调试辅助工具,以跟踪可疑或可能令人困惑的罕见情况。
授权与宽恕——Pythonic 方法
一条常见的 Python 知识点是来自 RADM 格蕾丝·穆雷·霍珀的以下建议:
"求得宽恕比求得许可更容易"
在 Python 社区中,这有时被总结为 EAFP(先做后检查)编程。这与 LBYL(先检查后执行)编程形成对比。
Python 异常处理速度快。更重要的是,所有潜在问题的必要先决条件检查已经包含在语言本身中。我们永远不需要用额外的 if 语句括起来处理过程,以查看输入是否可能引发异常。
通常认为编写如下形式的 LBYL(先检查后执行)代码是不良实践:
if text.isdigit():
num= int(text)
else:
num= None
这里展示的糟糕想法是仔细检查以防止抛出异常。由于多种原因,这种方法是无效的。
-
isdigit()测试无法正确处理负数。对于float()转换,这种测试会错过大量有效的语法变体。 -
检查字符和语法的有效性开销已经包含在
int()函数中。提前检查会重复已经存在的检查。
更 Pythonic 的方法是对内置异常进行处理。例如:
try:
num= int(text)
except ValueError:
num= None
这段代码的行数与之前相同。它正确地转换了所有可能的 Python 整数字符串。它不包括任何冗余的有效性检查。
摘要
在本章中,我们看到了如何使用 Python 异常编写能够正确处理意外情况的程序。各种类型的异常反映了外部条件以及可能改变程序行为的内部条件。我们可以使用异常子句来实现回退处理,以便程序能够优雅地处理这些异常情况。
我们还看到了一些被 discourage 的事情。空白的 except 子句——它匹配太多种异常类——是合法的,但不应该使用。
“三思而后行”(LBYL)编程的思想也通常不被鼓励。Python 的方法可以概括为“求原谅比求许可更容易”(EAFP)。一般的方法是将操作封装在try语句中,并为有意义的异常编写适当的异常处理器。
一些异常,如RuntimeError或SyntaxError,不应该由普通的应用程序编程来处理。这些异常通常表明问题非常严重,程序确实应该崩溃。
其他异常,如IndexError或KeyError,可能是设计的一部分。当这些异常是意外的,我们就发现了设计问题。这也可能表明我们缺乏足够的单元测试。
在第十章文件、数据库、网络和上下文中,我们将探讨 Python 在处理持久数据文件和网络数据传输方面的多种方法。这类处理通常需要异常处理。
第十章:文件、数据库、网络和上下文
文件和文件系统是现代操作系统工作方式的核心。许多操作系统资源都作为文件系统的一部分可见。例如,Linux 的/dev/mem是处理器内存的视图,作为文件系统中可见的设备实现。Python 提供了映射到这些操作系统特性的文件对象。
在基本层面上,操作系统文件仅仅是字节集合。在实践中,我们经常处理的是由 Unicode 字符组成的文件集合。Python 提供了这两种文件视图。对于某些文件格式,我们需要处理字节。对于文本文件,我们期望 Python 能够正确地将 Unicode 字符从字节中解码出来。
Python 文件对象通常与操作系统资源纠缠在一起。为了确保应用程序不会泄露操作系统资源,我们经常使用上下文管理器。这允许我们确保在 Python 文件关闭时释放操作系统资源。with语句提供了一种整洁的方式来使用上下文管理器分配和释放资源。
除了普通文件外,我们还将探讨 TCP/IP 套接字。urllib模块允许我们打开一个远程主机的套接字。套接字被用作文件来从远程主机读取数据。
文件有一个物理格式;除了最简单的格式外,所有格式都需要一个library模块来正确地读写内容。此外,在物理格式的约束下,数据逻辑布局可能会有所不同。例如,逗号分隔值(CSV)文件可能使用文件的第一行中的字段名来描述列的逻辑布局。
SQLite 数据库或shelve数据库依赖于一个(或多个)文件来使数据持久化。我们将简要地探讨依赖于文件的高级结构。
文件的基本概念
现代操作系统依赖于文件和设备驱动程序来提供各种服务和功能。磁盘驱动器上的字节只是文件的一种类型。
注意
由于许多存储设备使用或包含固态硬盘(SSD),从技术上来说,“磁盘”这个术语是一个误称;我们将使用过时的术语。
网络适配器是另一种类型的文件;在这种文件中,字节是连续可用的,而不是静止出现。除了磁盘和网络文件外,Linux 文件系统还包括/dev目录,它描述了给定计算机上的所有设备。这些设备包括串行端口、内存引用,甚至一个累积熵池以提供随机字节的设备。
Python 文件对象封装了一个操作系统文件。open()函数将 Python 文件对象绑定到操作系统文件。除了名称外,该函数还期望一个用于访问的模式字符串。模式字符串结合了两个功能:
-
字符与字节:默认情况下,文件以文本模式打开;我们可以通过使用
t来明确这一点。在读取时,操作系统字节被解码以创建 Unicode 字符。在写入时,Unicode 字符被编码成字节。要使用字节而不是文本,我们在模式中包含b;不会进行编码或解码。 -
允许的操作:默认情况下,文件以
r模式打开,仅允许读取。我们可以以w模式打开文件,这将删除任何先前内容并仅允许写入。我们可以以a模式打开文件,这将搜索到先前内容的末尾,以便可以附加新内容。+修饰符允许读写;这意味着w+将删除任何先前内容并允许读写;r+将保留先前内容并允许读写。
当我们打开一个文本文件时,我们提供明确的编码。在某些情况下,需要明确编码,因为操作系统期望的编码不在文件中。
在某些情况下,我们可能还需要指定如何处理换行符。在输入时,我们很少需要指定行结束符:Python 通过将 Windows \r\n转换为\n来优雅地处理它们。然而,在输出时,我们可能需要明确提供行结束符。如果我们设置newline="",则不会执行转换;我们需要这样做,以便可以创建具有\r\n行结束符的 CSV 文件。如果我们打开文件时设置newline=None,那么程序输出的\n将转换为os.linesep变量中的平台特定值。这是默认行为。newline的任何其他值都将替换输出中的\n字符。
我们可以指定缓冲区。我们还可以指定如何处理 Unicode 解码错误。有七个选项用于 Unicode 错误,包括strict、ignore、replace、xmlcharrefreplace、backslashreplace和surrogateescape。strict错误处理会引发异常。ignore错误处理会静默地丢弃非法字符。其他选项提供不同的替换策略。
打开文本文件
对于处理文本文件,以下是使用open()函数创建文件对象的方法:
>>> my_file = open("Chapter_10/10letterwords.txt")
>>> text= my_file.read().splitlines()
>>> text[:5]
['consultive', 'syncopated', 'forestland', 'postmarked', 'configures']
我们已经使用所有默认设置打开了文件。模式将是只读。文件必须使用系统的默认编码(例如 Mac-Roman)。我们将依赖默认的缓冲区和默认的 Unicode 错误处理,即strict。
在这个例子中,我们将整个文件读入一个巨大的字符串,然后将该单个字符串拆分为一系列单独的行。我们将字符串列表分配给text变量。我们只显示了列表中的前五项。默认情况下,split()字符串方法不保留拆分字符。
过滤文本行
我们将在以下示例中查看两个关键概念。我们将首先打开一个使用"utf-8"编码的文件:
>>> code_file = open("Chapter_1/ch01_ex1.py", "rt", encoding="utf-8", errors="replace")
>>> code_lines = list(code_file)
>>> code_lines[:5]
['#!/usr/bin/env python3\n', '"""Python Essentials\n', '\n',
'Chapter 1, Example Set 1\n', '\n']
我们以 "rt" 模式打开了一个文件,这意味着只读和文本模式。这是默认设置,所以可以省略。我们明确提供了 "utf-8" 编码,这并非操作系统默认编码。
我们使用 list() 函数将文件对象转换为行序列。当我们将文件对象用作可迭代对象时,我们会看到文件按行迭代。如果我们不更改文件的换行设置,则使用“通用换行”规则:\n、\r 或 \r\n 结束一行;它们被规范化为 \n。当我们按行处理文件时,行结束字符被保留。
我们通常希望从每行的末尾删除换行符。这是一种从原始行到去除尾随空白的行的映射。我们可以使用生成器表达式或 map() 函数以及 str.rstrip() 方法来实现。
在某些情况下,空行没有意义,可以删除。这也可以通过具有 if 子句以拒绝空行的生成器表达式来完成。我们还可以使用 filter() 函数来完成。如果我们将这些映射和过滤操作写成两行,会更简单,如下所示:
>>> txt_stripped = (line.rstrip() for line in code_file)
>>> txt_non_empty= (line for line in txt_stripped if line)
>>> code_lines= list(txt_non_empty)
我们将输入清理分解为两个生成器表达式。第一个生成器表达式 txt_stripped 将原始行映射到去除尾随空白的行。第二个生成器表达式 txt_non_empty 是一个过滤器,它会拒绝空行。我们很容易在 if 子句中添加其他过滤条件。由于生成器表达式是惰性的,直到最终的 list() 函数消耗了所有生成器中的行,实际上并没有做什么。
以这种方式,我们可以设计相当复杂的文件解析,作为生成器表达式集合。我们可以应用一系列映射和过滤操作,使得主语句块中只有干净的数据。
处理原始字节
这是我们打开文件并查看原始字节的方法:
>>> raw_bytes = open("Chapter_10/favicon.ico", "rb" )
>>> data = raw_bytes.read()
>>> len(data)
894
>>> data[:22]
b'\x00\x00\x01\x00\x01\x00\x10\x10\x00\x00\x00\x00\x18\x00h\x03\x00\x00\x16\x00\x00\x00'
我们以二进制模式打开了此文件。我们得到的输入将是 bytes 而不是 str。由于 bytes 对象具有许多与 str 对象相似的功能,我们可以对这些字节进行大量的字符串处理。我们已经从文件中导出了前 22 个字节。字节以十六进制值和 ASCII 字符的混合形式显示。
我们需要查看 ICO 文件格式的描述,以了解字节的意义。有关背景信息,请参阅en.wikipedia.org/wiki/ICO_(file_format)。
解码此字节块的最简单方法是使用 struct 模块。我们可以执行以下操作来解析文件头和文件第一个图像的头。
>>> import struct
>>> struct.unpack( "<hhhbbbbhhii", data[:22] )
(0, 1, 1, 16, 16, 0, 0, 0, 24, 872, 22)
unpack() 函数需要一个格式,该格式指定了对字节流执行的不同类型的转换。在这种情况下,格式包含三个用于字节组的代码:h 表示双字节半字,b 表示单字节,而 i 表示四字节整数。字节被组装成数值,结果结构是一个包含适当 Python int 值的元组。格式中的前导 < 指定整数转换使用 小端字节序。
使用文件类似对象
由于 Python 中对象的工作方式,任何提供类似 file 类接口的对象都可以用来代替文件。这导致了“文件类似对象”这个术语。我们可以使用文件对象,或者任何其他设计为像文件一样工作的对象。例如,io 模块有 StringIO 类,它允许我们像处理文件内容一样处理字符串。
我们经常使用它来创建测试数据。请注意,io.StringIO 对象与一个打开的文件非常相似。当我们考虑为可测试性进行设计——即 第十四章 的主题,“完善——单元测试、打包和文档”——我们需要设计函数以与文件对象一起工作,而不是与文件名一起工作。
这是一个将简单模式匹配应用于文件行以产生从复杂文本行中提取的数值的函数。有关正则表达式的更多信息,请参阅 第三章,“表达式和输出”。
此函数使用一个模式来过滤文件或文件类似对象的行:
import re
def tests_run(log_file):
data_pat = re.compile(r"\s*([\w ]+):\s+(\d+\.?\d*)\s*")
for line in log_file:
match= data_pat.findall(line)
if match:
yield match
我们定义了一个生成器函数,该函数将日志文件减少到与给定模式匹配的几行。我们使用了 re 模块来定义一个模式 data_pat,该模式查找一个单词字符串 ([\w ]+),一个冒号字符,以及一个可能是整数或浮点数的数字 (\d+\.?\d*)。data_pat.findall(line) 表达式将在给定行中定位所有这些 单词:数字 对。对于每行匹配项,将生成一个匹配结果列表。
匹配结果是字符串。我们需要应用额外的函数来将结果中的数字组从字符串转换为正确的数字。
在定义我们的函数时使用文件名很重要;函数不会打开文件。打开文件的函数稍微难以测试。相反,我们定义了 tests_run() 函数以使用任何文件类似对象。这允许我们编写如下单元测试:
>>> import io
>>> data = io.StringIO(
... '''
... Tests run: 1, Failures: 2, Errors: 0, Skipped: 1, Time elapsed: 0.547 sec
... Other data
... Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.018 sec
... ''')
>>> list( tests_run(data) )
[[('Tests run', '1'), ('Failures', '2'), ('Errors', '0'), ('Skipped', '1'), ('Time elapsed', '0.547')],
[('Tests run', '1'), ('Failures', '0'), ('Errors', '0'), ('Skipped', '0'), ('Time elapsed', '0.018')]]
我们已经导入了io模块,以便我们可以创建一个包含模拟输入的io.StringIO对象。我们可以将这个类似文件的对象提供给tests_run()函数。由于StringIO的行为类似于文件,我们可以用它来代替实际文件来测试我们的函数,以确保它正确地定位了Tests run行并忽略了其他行。我们将在第十四章中查看单元测试,完善 - 单元测试、打包和文档。
通过with语句使用上下文管理器
Python 的文件对象通常与操作系统资源纠缠在一起。当我们完成文件的使用时,我们需要确保文件被正确关闭,以便操作系统资源可以被释放。对于小型命令行应用程序,这个考虑并不是那么重要:当我们从 Python 退出时,并且所有对象的引用计数都减少到零,文件将在对象删除处理过程中关闭。
然而,对于大型、长时间运行的服务器,未正确关闭的文件将积累操作系统资源。由于操作系统资源的池是有限的,文件句柄泄露最终将导致问题。
作为一般实践,我们可以使用上下文管理器来确保在完成使用文件后文件被关闭。想法是将一个打开的文件约束在上下文管理器内的语句集中。一旦这个语句集完成,上下文管理器将确保文件被关闭。
我们使用with语句来指定上下文。文件对象是一个上下文管理器;with语句使用文件作为管理器。在with语句结束时,上下文管理器将退出,文件将被关闭。一些更复杂的文件结构也是上下文管理器。例如,zipfile模块中定义的ZipFile对象是一个合适的上下文管理器;当在with语句中使用时,文件将被整洁地关闭。
应该将所有文件输入输出处理都包裹在with语句中,以确保文件被正确关闭,这是一个最佳实践。以下是我们如何使用(前面展示的)tests_run()函数(作为一个上下文管理器的例子)的示例:
file_in= "Chapter_10/log_example.txt"
file_out= "Chapter_10/summary.txt"
with open(file_in) as source, open(file_out, "w") as target:
for stats in tests_run(source):
print(stats, file=target)
我们已经打开了两个文件作为上下文管理器。用于读取的文件"Chapter_10/log_example.txt"被分配给source变量。用于写入的文件"Chapter_10/summary.txt"被分配给target变量。然后我们可以处理这些文件,知道它们将正确关闭。
如果发生异常,文件将被关闭。这非常重要。如果with语句内部的语句组中发生异常,每个上下文管理器都会收到通知。在这种情况下,这两个管理器都是文件对象。每个对象都会看到异常并关闭文件——释放所有操作系统资源——并允许异常处理继续。我们的应用程序将因异常而崩溃,但文件也将正确关闭。
小贴士
总是使用with语句包装文件处理。
使用 contextlib 关闭文件类似对象
在某些情况下,我们想确保我们的应用程序关闭一个没有实现上下文管理器方法的文件类似对象。例如,http.client模块将创建一个HTTPConnection对象,该对象可能与网络资源纠缠在一起。我们希望确保在完成使用连接对象后释放任何网络资源。然而,由于此对象不是合适的上下文管理器,当在with语句中使用时,它不会自动关闭。
事实上,尝试在with语句中使用HTTPConnection对象作为上下文管理器将引发AttributeError异常。这个错误将显示HTTPConnection对象没有实现作为上下文管理器所需的方法。
我们可以利用contextlib模块中的通用上下文管理器。contextlib.closing()函数将包装任何具有close()方法的对象,并添加所需的特殊方法,使包装的对象成为一个上下文管理器。
一个 RESTful 网络服务请求可能看起来像这样:
import contextlib
import http.client
with contextlib.closing(
http.client.HTTPConnection("www.example.com")) as host:
host.request("GET", "/path/to/resources/12345/")
response= host.getresponse()
print(response.read())
我们对向网络服务发送 GET 请求感兴趣。http.client.HTTPConnection对象不是上下文管理器;如果没有异常发生,无法保证它会被关闭。通过使用contextlib.closing()函数包装它,我们使其成为一个合适的上下文管理器。我们可以发送请求并处理响应,同时知道HTTPConnection对象将正确调用其close()方法。
将 shelve 模块用作数据库
文件为我们提供了持久存储。文件简单使用的局限性在于数据必须按顺序访问。我们如何以任意顺序访问项目?
我们将使用“数据库”一词来指代一个文件(一组文件),我们将在其中以任意顺序对数据元素执行创建、检索、更新和删除(CRUD)操作。如果我们创建大小一致的对象,我们可以在r+模式下打开一个普通文本文件,并使用seek()方法定位到任何特定记录的开始。然而,这相当复杂,我们可以做得更好。
核心数据库概念——可读和可写存储——可以通过看似无穷无尽的辅助功能进行扩展。现在,我们将忽略锁定、日志记录、审计、日志记录、分布式事务管理以及许多其他功能,以专注于持久化的核心功能。
shelve模块为我们提供了一个非常灵活的数据库。书架对象的行为类似于一个普通的 Python 映射,并且具有内容持久化的附加功能。一个额外的约束是,用于书架的键必须是字符串。
通常,我们使用多部分字符串作为书架键,这样我们就可以包含一些类信息以及类的实例的唯一标识符。我们可以使用简单的class:id格式来包含类名和对象的标识值,作为书架的复合键。
下面是一个创建将键映射到值列表的书架的示例。在这个例子中,输入文件有一系列单词,还有一些空白行和一个我们想要忽略的尾随行。书架的键是单词的首字母。与每个键关联的值是与该共同首字母共享的单词列表。
下面是整个函数:
import contextlib
import shelve
def populate():
with contextlib.closing(
shelve.open("Chapter_10/shelf","n")) as shelf:
with open("Chapter_10/10letterwords.txt") as source:
txt_stripped= (l.strip() for l in source)
txt_non_empty= (l for l in txt_stripped
if l and not l.startswith("Tool") )
for word in txt_non_empty:
key = "word_list:{0}".format(word[0])
try:
word_list= shelf[key]
except KeyError:
word_list= []
word_list.append(word)
shelf[key]= word_list
我们使用shelve.open()打开了书架对象。"n"模式会在每次应用程序运行时创建一个新的、空的书架文件。由于书架不是一个合适的上下文管理器,我们需要用contextlib.closing()函数将其包装。
shelve模块依赖于特定平台的数据库模块。这可能会导致需要一个或多个底层文件来支持书架。我们提供了一个基本文件名为"Chapter_10/shelf"。根据我们使用的操作系统,可能会创建一个.dat或.db文件。
for循环遍历由txt_non_empty表达式生成的输入单词序列。套件首先构建一个两部分的键。第一部分是字符串word_list;这显然不是 Python 数据类,但它作为数据含义的总结。在冒号之后,我们放上了单词的第一个字符。
我们获取与该键关联的当前单词列表。如果没有这样的键在书架中,我们通过创建一个新的、空列表来处理KeyError异常。一旦我们有一个列表——无论是新的还是从书架中检索的——我们就可以通过追加我们的新单词来更新列表。然后我们将单词列表保存在书架中。
要查询以特定首字母开头的单词,我们可以使用shelf["word_list:"+letter]。我们需要创建一个完整的键字符串,包括一个分类器,这样我们就有了一个包含多个集合的书架。
为了检索和总结数据,我们使用基于这个生成器表达式的简单循环:
sorted(k for k in shelf.keys() if k.startswith("word_list:"))
这将只选择来自书架数据库中的word_list集合的键。在一个更复杂的数据库中,可能有其他带有其他键前缀的集合。
使用 sqlite 数据库
sqlite模块为我们提供了一个基于 SQL 的数据库。利用 SQL 的应用程序在原则上是可以移植的。我们应该能够在不大幅修改我们的 Python 应用程序的情况下,使用 MySQL 或 PostgreSQL 作为我们的数据库而不是 SQLite。
虽然有几种适用于 SQL 的标准,但每个实现似乎都存在自己特有的问题。因此,基于 SQL 的应用程序很少能够在数据库平台之间完美移植。
SQL 数据库需要一个正式的模式定义。这意味着 SQL 应用程序必须始终包含创建或确认模式的一些规定。就像前面的例子一样,我们将与一个只有一个表且有两个列的数据库一起工作:一个非唯一键,它是单词的首字母,以及具有该首字母的单词。
这是 SQL 中的表定义:
CREATE TABLE IF NOT EXISTS word(
letter VARCHAR(1),
word VARCHAR(10),
PRIMARY KEY (letter))
这定义了一个有两个列的表,letter和word。要找到所有具有共同首字母的单词,我们需要从该表中检索多行。这是一种常见的 SQL 设计。它并不完全符合 Python 的面向对象设计,这是使用 SQL 时的一个常见限制。
我们需要执行 SQL CREATE TABLE语句来在 SQLite 数据库中创建(或确认)表。以下是一个将建立(或确认)模式的函数:
def schema():
with SQL.connect("Chapter_10/sqlite.sdb") as db:
db.execute( """CREATE TABLE IF NOT EXISTS word(
letter VARCHAR(1),
word VARCHAR(10),
PRIMARY KEY (letter))
""")
重要的语句是 SQLite 连接对象的execute()方法。我们提供了一个三引号字符串的 SQL。如果出现问题,将引发异常。
这是一个函数,它将从文本文件加载数据到这个表中:
def populate():
with SQL.connect("Chapter_10/sqlite.sdb") as db:
db.execute( """DELETE FROM word""" )
with open("Chapter_10/10letterwords.txt") as source:
txt_stripped= (l.strip() for l in source)
txt_non_empty= (l for l in txt_stripped
if l and not l.startswith("Tool") )
for word in txt_non_empty:
db.execute( """INSERT INTO WORD(letter, word)
VALUES (:1, :2)""", (word[0], word) )
注意,我们首先从word表中删除所有行。这类似于我们之前的例子,通过创建一个全新的空shelve数据库来工作。创建空 SQL 数据库可能会有很高的开销;此示例期望有一个已建立的数据库,其中已经定义了表,并从定义的表中删除行。
与前面的例子一样,我们使用了两个生成器表达式来从输入文件中过滤掉这些垃圾行。循环遍历由no_summary表达式生成的单词。该代码块执行一个 SQL INSERT语句,为表中的letter和word列绑定两个值。这个语句在我们的数据库中为单词表创建了一个新行。
要查看以给定字母开头的单词计数,我们可以使用 SQL 聚合。我们将执行以下SELECT语句。
SELECT letter, COUNT(*) FROM word GROUP BY letter
当我们执行这个操作时,我们得到一个 SQL 迭代器(称为“游标”),它根据SELECT子句产生一系列二元组。每个元组将包含字母和共享该字母的单词数量。我们可以使用这个来显示具有给定首字母的单词计数的摘要。
使用对象关系映射
许多流行的 SQL 数据库提供了 Python 驱动程序。有些比其他的有更好的支持级别。当与 SQL 数据库一起工作时,有时很难找到有效且可移植的 SQL 语法。一个数据库上的特性可能在另一个数据库上成为问题。
更重要的是,然而,SQL 表的完全扁平的列-行结构与面向对象语言如 Python 中更复杂的类定义的要求之间存在不匹配。这种阻抗不匹配通常通过对象关系映射(ORM)包来解决。两个流行的包是 SQLAlchemy 或 SQLObject。
这些包帮助将复杂对象映射到简单的 SQL 表。它还通过将特定 SQL 数据库的细节与应用程序编程分离来提供帮助。
不使用 SQL 的数据库,如shelve、MongoDB、CouchDB 和其他 NoSQL 数据库,没有 SQL 数据库所具有的相同的对象关系阻抗不匹配问题。我们在持久化技术方面有很多选择;Python 可以与各种数据库一起使用。
互联网服务和互联网协议
正如我们之前提到的,许多 TCP/IP 协议,如 HTTP,依赖于套接字抽象。套接字被设计成类似文件:我们可以使用普通的文件操作来读取或写入套接字。在非常低级的情况下,我们可以使用 Python 的socket模块。我们可以创建、读取和写入套接字来连接客户端和服务器程序。
然而,我们不会直接与套接字工作,而是会使用更高级的模块,例如urllib和http.client。这些模块为我们提供了 HTTP 协议的客户端操作,使我们能够连接到 Web 服务器,发出请求,并获取回复。我们在之前的使用 contextlib 关闭文件类似对象部分简要介绍了http.client模块。
要实现服务器,我们可以使用http.server。然而,在实践中,我们通常会利用前端应用程序,如 Apache HTTPD 或 NGINX,来提供网站的静态内容。对于动态内容,我们通常会使用 WSGI 网关将前端传递到 Python 框架的 Web 请求。有几个 Python 网络服务器框架,每个框架都有各种功能、优势和劣势。
物理格式考虑
Python 库为我们提供了许多模块来帮助处理常见的物理文件格式。《Python 标准库》的第十三章文件格式描述了文件压缩和归档;这包括处理使用 zip 或 BZip2 压缩的文件的模块。第十四章加密服务描述了处理 CSV、配置文件和 PLIST 文件等文件格式的模块。第十九章结构化标记处理工具描述了互联网数据处理,其中包括 JSON 文件格式。第二十章互联网协议和支持描述了处理 HTML 和 XML 等标记语言的模块。对于不是标准库一部分的模块,Python 包索引(PyPI)可能有处理文件格式的包。请参阅pypi.python.org。
我们将快速查看 CSV 模块,因为它在处理“大数据”问题时经常被使用。例如,Apache Hadoop 软件库——一个允许分布式处理大数据集的框架——利用简单的编程模型。我们可以使用 Python 与 Hadoop 流处理。
Hadoop 文件通常是 CSV 格式的文件。在某些情况下,它将使用 "|" 而不是逗号,并且不会使用引号或转义符。在其他情况下,可以使用 \x01(ASCII SOH)字符作为分隔符。这可以通过 Python CSV 模块相对简单地处理。
当我们从电子表格创建 CSV 文件时,第一行可能包含标题信息。这可能非常有帮助。csv.DictReader() 类使用 CSV 文件的第一个行作为标题。每一行剩余的内容将被转换为一个 dict。这个 dict 中的键将是第一行中的列名。
当与其他 CSV 文件一起工作时,可能不存在标题行。这意味着我们需要一个单独的模式定义来确定每个列的含义。在大多数情况下,我们可以简单地用一个列名列表或元组来表示模式。
我们可能有一行这样的内容来提供缺失的列名:
TEST_LOG_SUMMARY = (
"module", "datetime", "tests_run", "failures",
"errors", "skipped", "time_elapsed",
)
这为我们提供了一个简单的元组,其中包含友好的 Python 列名。我们在元组项的末尾添加了一个多余的逗号,以便更容易添加新列而不会出现语法错误。一般来说,我们可以简单地将这个定义放入文件中并导入这个模式定义。
假设我们有一个名为 log_parser() 的函数,它可以解析一个复杂的日志文件以提取之前显示的字段。这个函数将使用正则表达式来定位日志中包含测试结果、模块名称和时间戳的行。日志数据将被用来构建一个简单的字典,其键由 TEST_LOG_SUMMARY 全局变量定义。解析器将返回一系列 dict 对象,其外观如下:
{'module': 'com.mycompany.app.AppTest', 'errors': '0', 'time_elapsed': '0', 'failures': '0', 'datetime': 'Thu Oct 06 08:12:17 MDT 2005', 'tests_run': '1'}
我们可以使用这个 log_parser() 函数从日志中写入一个 CSV 摘要文件。我们将把这个函数称为 mapper(),因为它将文件名序列映射到数据行序列,同时保留相关细节:
def mapper(name_iter, result):
writer= csv.DictWriter(result, fieldnames=TEST_LOG_SUMMARY, delimiter='|')
for name in name_iter:
with open(name) as source:
writer.writerow( log_parser(source) )
此函数期望两个参数:一个生成日志文件名的迭代器,以及一个打开的文件,结果将被写入其中。此函数将使用输出文件创建一个 CSV DictWriter 对象,包括每个将被写入的字典中的字段名集合,最后是一个分隔符。
对于每个名称,日志将被打开并解析。解析的结果 dict 将被写入 CSV 文件以总结处理过程。我们可能在类似这样的脚本中使用这个函数:
mapper(glob.glob("Chapter_10/log_*.txt"), sys.stdout)
我们已经将输出写入到操作系统的标准输出。这允许我们将这些结果管道传输到另一个程序,该程序对日志摘要进行统计分析。我们可能会将统计摘要称为 reducer,因为它将大量值减少到单个结果。reducer 会共享TEST_LOG_SUMMARY变量,以确保两个程序对它们之间传递的文件内容达成一致。
摘要
在本章中,我们看到了如何使用 Python 异常来编写与各种文件类型一起工作的程序。我们专注于文本文件,因为它们易于处理。我们还研究了解析二进制文件,这通常需要struct模块的支持。
文件也是一个上下文管理器。最佳实践是使用with语句来使用文件,以确保文件被正确关闭,并且所有操作系统资源都被释放。在命令行程序中,这可能不是那么重要;在长时间运行的服务器中,确保资源不会因文件关闭不当而泄漏是绝对必要的。
我们还研究了更复杂的持久化机制,包括shelve模块和 SQLite 数据库。这些为我们提供了在文件中对数据对象执行 CRUD 操作的方法。SQLite 数据库要求我们使用 SQL 语言来描述数据访问:这可以使我们的程序更容易移植到其他数据库。同时使用 SQL 和 Python 可能会让人感到困惑。我们可以通过使用如 SQLAlchemy 这样的库来克服这个小问题,这样我们就可以完全在 Python 中工作,让 SQLAlchemy 来创建适合我们数据库的 SQL 语句。
标准库有多个包来处理不同的物理文件格式。其中之一可以帮助创建和检索 CSV 格式的数据。逗号分隔符的角色可以是任何字符序列,扩展了这个概念,使得许多类型的分隔文件都可以由这个模块读取或写入。
在第十一章,类定义中,我们将探讨如何在 Python 中定义我们自己的定制类。类定义是面向对象编程的核心。我们将简要介绍在 Python 编程中常见的几种类设计模式。
第十一章。类定义
Python 对象是类的实例。类通过方法函数定义了对象的行为。在本章中,我们将查看创建我们自己的类和对象。我们将从查看创建类和对象的基本知识开始。一旦我们看到了基本工具,我们就可以总结一些我们可以使用类定义来创建对象的方法,以及对象应该如何交互以实现我们期望的行为。
我们将查看更复杂类定义的一些元素。高级主题将包括类方法和静态方法的概念。关于 Python 的高级面向对象编程可以写成一整本书,所以我们将采取广泛但浅显的方法来查看类定义。
我们还将查看内置的抽象基类。我们可以使用这些基类来简化我们自己的类定义。在许多情况下,我们有类似于容器的类,可以利用基类,这样我们可以节省一些编程工作,并确保与其他 Python 特性的无缝配合。
创建一个类
面向对象程序的核心是类定义。class语句创建了一个对象,用于创建类的实例。当我们创建一个新的类SomeClass时,我们可以使用那个SomeClass()函数来创建具有类共同定义的对象。这是内置类的工作方式;例如,int()函数创建了一个int类的实例。
在 Python 中,class语句包括描述每个实例行为的函数方法。除了普通方法外,还有一些与 Python 操作紧密相关的“特殊”方法。
我们没有义务以任何正式的方式为类提供特定的属性(也称为实例变量)。对象的实例变量是灵活的,并且不是预先定义的。
class语句的初始子句提供了类名。它还可以命名任何超类,从这些超类继承特性。类主体的主要内容包含方法定义,这些定义是通过缩进的def语句创建的。
在某些情况下,我们不需要提供语句块。我们经常创建像这样的定制异常类
class MyAppError(Exception):
pass
在这个例子中,我们提供了一个新的类名MyAppError,并指定它继承自Exception类的特性。我们不需要对该基定义进行任何修改;由于我们必须提供一个缩进的语句块,我们使用pass语句来完成class语句的语法。
由于这个类的工作方式与任何其他异常类似,我们可以使用类似raise MyAppError("Some Message")的语句来引发这个新异常类的一个实例。
在类中编写语句块
class 语句内部的语句集通常是一系列方法定义。每个方法都是一个绑定到类的函数。语句集还可以包括赋值语句;这些将创建作为整个类定义一部分的变量。
这是一个简单的用于 (x, y) 坐标对的类:
class Point:
"""
Point on a plane.
"""
def __init__(self, x, y):
self.x= x
self.y= y
def __repr__(self):
return "{cls}({x:.0f}, {y:.0f})".format(
cls=self.__class__.__name__, x=self.x, y=self.y)
我们提供了一个类名 Point。我们没有明确提供超类;默认情况下,我们的新类将是 object 的子类。按照惯例,大多数内置类的名称,如 object,以小写字母开头。我们将定义的所有其他类的名称都应该以大写字母开头;因此,我们的名称为 Point。我们还为这个类提供了一个简短的文档字符串。在 第十四章,完善 - 单元测试、打包和文档 中,我们将探讨扩展这个文档字符串。
我们在类中定义了两种方法。第一种方法有一个特殊的名称 __init__()。在类中定义的任何方法的第一参数都必须包括实例变量。这个变量,通常称为 self,将是相关对象的引用。当我们给变量 self.x 赋值时,这将设置 Point 类特定实例的 x 属性。当方法被调用时,实例变量会隐式提供。
Python 不会对允许的实例变量进行任何正式的定义,而是依赖于 __init__() 特殊方法来初始化适当的实例变量。默认情况下,一个对象可以在任何时候添加额外的属性。
第二种方法有一个特殊的名称 __repr__()。为了成为一个正确的方法,第一个参数必须是实例变量 self。此方法必须返回一个表示我们的坐标对的字符串。如果我们不重写这个特殊方法,我们将得到一个默认的字符串表示,看起来像这样:<__main__.Point object at 0x100623e10>。我们的实现使用 self.__class__.__name__ 来利用对象的类,以便任何子类都将正确的类名插入到输出结果中。
特殊方法名在 Python 中无处不在。使用它们允许我们的类与内置的 Python 功能无缝集成。有大量的特殊方法名——太多以至于无法在本书中全部审查。所有这些名称都以 __(两个下划线)开始和结束。避免与这种命名约定冲突很容易。没有好的理由在我们的应用程序编程中使用 __ 命名,我们选择的任何这种形式的名称都可能成为 Python 的隐藏特性。
注意,我们没有在两个方法函数中包含占位符文档字符串。我们省略了它们以使示例简短,并专注于类定义。一般来说,类的每个方法都将有一个文档字符串,以提供对该方法的简洁、有用的总结。
在第四章中,我们介绍了命名空间的概念,它是一个用于存储变量的容器。self变量是对象,它是一个我们可以插入属性变量的命名空间。
我们可以像这样创建类的实例:
>>> p_1 = Point(22, 7)
>>> p_1.x
22
>>> p_1.y
7
我们像函数一样使用了类名Point。首先创建了一个空对象。然后,将参数值提供给__init__()特殊方法以初始化该空对象。请注意,我们没有显式地为实例变量self提供值。
要执行__repr__()特殊方法,我们可以这样做:
>>> p_1
Point(22, 7)
当打印对象时,将应用内置的repr()函数以获取对象的字符串表示。此内置函数依赖于对象的__repr__()特殊方法来为对象提供字符串表示。在评估__repr__()方法时,对象p_1被隐式地分配给实例变量self。
我们实现的__repr__()特殊方法产生了一个包含x和y坐标值的字符串。我们使用了.0f作为格式说明符,为self实例变量的x和y属性提供了小数点右边的零位。
使用实例变量和方法
上一节中的Point类定义只包含两个特殊方法。我们现在将添加第三个非特殊方法。这是该类的第三个方法:
def dist(self, point):
return math.hypot(self.x-point.x, self.y-point.y)
此方法函数接受一个名为point的单个参数。此方法函数的主体使用math.hypot()计算同一平面上的两点之间的直接距离。
这是我们如何使用此函数的方法:
>>> p_1 = Point(22, 7)
>>> p_2 = Point(20, 5)
>>> round(p_1.dist(p_2),4)
2.8284
我们创建了两个Point对象。当评估p_1.dist(p_2)表达式时,分配给p_1变量的对象将被分配给self变量。这是执行相关处理的Point实例。分配给dist()方法的参数,分配给p_2变量,将被分配给point参数变量。
小贴士
当我们评估obj.method()时,obj对象将是self实例变量。
默认情况下,我们创建的对象是可变的。这是Point对象的另一个方法——这会改变内部状态:
def offset(self, d_x, d_y):
self.x += d_x
self.y += d_y
此方法需要两个值,这些值用于偏移Point对象的坐标。该方法将新值分配给对象的x和y属性。
当我们使用此方法时,会发生以下情况:
>>> p_1.offset(-3, 3)
>>> p_1.x
19
>>> p_1.y
10
我们已经评估了与对象p_1关联的偏移方法。如前所述,self实例变量将与p_1引用的相同对象。当我们为self.x赋值时,这将改变p_1引用的对象,设置p_1.x。
Pythonic 面向对象编程
我们已经看到了 Python 面向对象方法的一些重要特性。也许最重要的是,Python 缺乏变量名和类型之间的静态绑定;任何类型的对象都可以赋给任何变量。名称不是由编译器静态解析的。Python 的动态名称解析意味着我们可以将我们的程序视为在类方面完全通用的。
当我们评估obj.attribute或obj.method()时,有两个步骤。首先,必须解析名称attribute或method。其次,评估引用的属性或方法。
对于名称解析步骤,有几个命名空间被搜索以确定名称的含义。
-
搜索
obj实例的局部命名空间以解析名称。对象的命名空间作为obj.__dict__可用。属性名称(和值)通常在对象的自身命名空间中找到。另一方面,方法通常不是对象实例的一部分。 -
如果名称不是对象的局部名称,则搜索对象类的局部命名空间。类的命名空间作为
obj.__class__.__dict__可用。方法名称通常在类的命名空间中找到。类的属性也可能在这里找到。 -
如果名称不在类中,将搜索超类以找到名称。整个超类格状结构被组装到
obj.__class__.__mro__值中。这定义了方法解析顺序(MRO);此序列中的每个类都会搜索该名称。
一旦找到名称,Python 必须确定其值。对于不指向可调用方法的名称,即属性,所引用的对象是该属性的值。指向可调用方法的名称将绑定参数值,并将其作为函数评估。该函数的结果是值。
之前描述的“搜索”依赖于内置的dict类。这使用散列来快速确定名称的存在或不存在。从 Python 中可用的复杂和灵活的类行为中几乎没有性能成本。
如果在运行时提供了一个不适当的类型对象,那么在对象中找不到方法名或属性名,将引发AttributeError异常。在我们之前的例子中,我们可以尝试评估p_1.copy()。copy这个名字既没有定义在我们的类中,也没有定义在任何超类中,所以会引发AttributeError异常。
尝试进行类型转换
虽然 Python 变量仅仅是附加到对象上的名称,但底层对象具有非常严格的类型。无法为新值分配__class__名称,该名称定义了对象的类。
类型转换在一些静态编译的语言中是必需的,以便能够创建泛型数据结构。在这些语言中,我们可以将一个类型的引用转换为另一个类型的引用。由于方法解析的动态性,Python 中不需要这种类型的转换。
所有 Python 集合都可以包含混合类型的对象。我们可以轻松地评估这一点:
>>> map(lambda x:x+1, [1, 2.3, (4+5j)])
lambda 表达式 x+1 可以应用于 int、float 或 complex 类型,而无需进行任何类型的类型转换操作。这是因为每个类都提供了适当的特殊方法函数来实现整数的加法。
设计封装和隐私
关于 Python 类定义的一个常见问题是,如果所有属性和成员名称都是公开的,我们如何实现封装。一些程序员对此表示担忧:
>>> p_2 = Point(20, 5)
>>> p_2.y = 6
>>> p_2
(20, 6)
我们创建了一个对象 p_2。然后我们修改了该对象的一个属性值,而没有使用该对象的方法函数。这并不是没有使用封装设计原则的失败:该类有一个适当的封装设计。该类没有可以被编译器静态检查的实现。
Python 原则可以用以下观察结果来概括:
我们都是成年人。
没有充分的理由去创建私有、公开和受保护的方法和属性这种复杂性,因为 Python 代码以源代码的形式分发,任何人都可以检查源代码,以了解弯曲或破坏封装可能带来的后果。首选的方法是为类和方法编写清晰的文档字符串,并提供单元测试来证明属性和方法被正确使用。
我们可以使用单个下划线 _ 作为前缀来表示方法或属性不是类公共接口的一部分。Python 文档工具会礼貌地忽略这些名称,以便可以自由地更改这些实现细节。以下划线开头的名称被认为是可能会在没有通知的情况下更改的;依赖于这些名称可能会导致程序以意想不到的方式崩溃。
在某些语言中,需要“获取器和设置器”方法来公开类的属性。在 Python 中,我们可以直接使用对象的 __dict__,简化了内省。我们还可以使用内置函数 getattr()、setattr() 和 delattr() 来以字符串的形式处理属性名称。例如:
>>> p_2.__dict__.keys()
dict_keys(['y', 'x'])
>>> getattr(p_2, "x")
20
这显示了我们可以如何动态地获取属性的名字和值。在第一个例子中,我们查看对象的内部命名空间 __dict__ 来获取属性。在第二个例子中,我们使用了内置的 getattr() 函数来获取属性的值。
使用属性
Python 允许我们创建方法,它们可以用作属性。这为我们提供了从对象获取派生值时非常愉悦的语法。看起来像属性的方法定义为属性。我们将使用两个额外的方法定义我们的 Point 类:
@property
def r(self):
return math.sqrt(self.x**2 + self.y**2)
@property
def θ(self):
return math.atan2(self.y, self.x)
我们使用 @property 装饰器定义了两个函数。这个装饰器可以与只有一个参数 self 的实例变量的函数一起使用。
下面是如何使用这些属性的例子:
>>> p = Point(12, 5)
>>> round(p.r, 1)
13.0
>>> round(math.degrees(p.θ), 1)
22.6
我们像访问对象的简单属性一样访问了这些方法,使用 p.r 和 p.θ 比在复杂公式中写 p.r() 和 p.θ() 更愉快。前面的属性是显式只读的。如果我们尝试给 p.r 或 p.θ 赋值,我们会得到一个异常。
我们将在第十三章《元编程和装饰器》中回到 @property 装饰器的话题。
使用继承来简化类定义
我们可以使用继承——在子类中重用超类的代码,这可以简化子类的定义。在先前的例子中,我们创建了一个 MyAppError 类作为 Exception 的子类。这意味着 Exception 的所有特性都将可用给 MyAppError。这是因为名称搜索的三个步骤:如果方法名在对象类中找不到,那么会搜索所有超类以查找该名称。
这里是一个子类的例子,它只覆盖了父类的一个方法:
class Manhattan_Point(Point):
def dist(self, point):
return abs(self.x-point.x)+abs(self.y-point.y)
我们定义了一个名为 Manhattan_Point 的 Point 子类。这个类具有 Point 的所有特性。它对父类进行了一个单一的改变。它为 dist() 方法提供了一个定义,该定义将覆盖 Point 超类中的定义。
下面是一个示例,展示了方法解析的工作原理:
>>> p_1 = Point(22, 7)
>>> p_2 = Manhattan_Point(20, 5)
>>> round(p_1.dist(p_2),4)
2.8284
>>> round(p_2.dist(p_1),4)
4
我们创建了两个对象:p_1 是 Point 的一个实例,而 p_2 是 Manhattan_Point 的一个实例。我们没有编写 Manhattan_Point 的 __init__() 方法;它是从 Point 继承的。当我们评估 p_1.dist() 时,我们使用的是 p_1 的类 Point 中的 dist() 方法。另一方面,当我们评估 p_2.dist() 时,我们使用的是 p_2 的方法,即 Manhattan_Point 的方法。
通过继承重用是一种保证几个类具有相同行为的方法。这是一个重要的面向对象设计原则,有时被称为Liskov 替换原则(LSP)。Manhattan_Point 的一个实例可以在任何需要 Point 的实例的地方使用。
使用多重继承和混入设计模式
继承有时被可视化为一个简单的相关类层次结构。如果每个子类最多只有一个父类,那么任何给定的子类与 object 超类之间将存在一条关系链。这种单继承模型并不总是合适的。在某些情况下,一个类将包含一些不适合线性血统观念的不同特性。
collections 抽象基类模块,collections.abc,包含了许多多重继承的例子。这里的设计模式是有一个中心类层次结构,它定义了 List、Set 或 Mapping 集合的基本特性。其他特性通过可重用的 混合 类包含在内。
例如,Set 类是 Container 的子类。在这个定义中混合了 Sized 和 Iterable 类定义的特性。Sized 混合类包含了 __len__() 特殊方法。Iterable 混合类包含了 __iter__() 特殊方法。
这导致最终类成为可重用超类的组装。我们可以利用这一点来创建包含不同特性混合的自己的类。
Python 通过依赖于 class 语句中类命名的顺序来管理多重继承。这构建了用于在继承图中搜索名称的 __mro__ 值。以下是一个例子:
>>> from collections.abc import Mapping
>>> Mapping.__mro__
(<class 'collections.abc.Mapping'>, <class 'collections.abc.Sized'>,
<class 'collections.abc.Iterable'>, <class 'collections.abc.Container'>,
<class 'object'>)
我们导入了一个抽象基类。当我们查看 MRO 时,我们看到 Python 将按顺序在 Mapping、Sized、Iterable、Container 和 object 中搜索名称。
当使用此类混合类进行设计时,我们通常在各个类之间分配责任,以避免在用于组装最终类定义的各种超类之间发生名称冲突。
使用类方法和属性
通常,我们期望对象是有状态的,而类是无状态的。虽然典型,但无状态类不是必需的。我们可以创建具有属性和方法类对象的类。在罕见的情况下,类也可以有可变属性。
类变量的一种用途是创建适用于类所有实例的参数。当一个名称没有被对象实例解析时,接下来将搜索类。以下是一个依赖于类级属性的类的小层次结构:
class Units(float):
units= None
def __repr__(self):
text = super().__repr__()
return "{0} {1}".format(text, self.units)
class Height(Units):
units= "inches"
Units 类定义扩展了 float 类。它引入了一个名为 units 的类级属性。它覆盖了 float 的 __repr__() 特殊方法。此方法使用超类的 __repr__() 方法来获取值的本质文本表示。然后包括 units 属性的值。
当我们评估 self.units 时,将进行对名称的三步搜索。Height 的实例不会提供 units 属性。然而,Height 类将提供 units 属性;其值将是 inches。
当我们创建一个 Height 对象的实例时,我们将看到单位:
>>> Height(61.5)
61.5 inches
当我们打印 Height 的实例时,print() 函数将使用内置的 repr() 函数来获取字符串表示。repr() 函数使用对象的 __repr__() 特殊方法。我们已经重写了 __repr__() 特殊方法,以包含 units 属性中的文本。
由于所有属性都是公开可用的,我们可以编写类似 Height.units= "furlongs" 这样的代码,这将导致所有后续使用 Height 类对象的操作都会显示不同的单位。更改类级别的属性通常不是一个好主意,但并没有任何正式的禁止方式。
回想一下政策:我们都是成年人。
使用可变类变量
一些应用程序可能需要一个作为整体类一部分的适当可变变量。在寻找名称的三个步骤中,会找到一个类级别的属性名称:首先查找对象,然后是类,最后是超类。这意味着即使名称不在对象实例中,而是在类或父超类中定义,我们也可以成功评估 self.class_level_name。
然而,如果我们尝试使用类似 self.class_level_name 的名称来分配类级别的变量,我们将在实例中创建一个新的属性。由于实例名称现在会被首先找到,类级别的名称将不再可见。
如果我们想要更新一个类级别的变量,我们必须显式地使用类名,避免使用 self 实例变量。以下是一个为每个创建的实例分配序列号的类:
class Sample:
counter= 0
def __init__(self, measure):
Sample.counter += 1
self.sequence = Sample.counter
self.measure = measure
我们创建了一个类级别的变量 counter,当类被创建时初始化为零。__init__() 方法将增加类级别的 counter 属性。为了避免在实例中创建变量,我们使用类名 Sample 而不是 self。除了更新 Sample.counter,此方法还设置实例的两个属性:将 Sample.counter 的当前值分配给序列属性,并将给定的测量值也保存起来。
重要的是要注意,在方法函数内部,我们可以使用 self.counter 和 Sample.counter 来访问同一个对象。当没有名为 counter 的实例变量时,这将是正确的。然而,为了在类中分配变量,我们只能使用 Sample.counter。
编写静态方法
在某些情况下,我们会在类中包含一个实际上不依赖于任何实例变量的方法。在许多语言中,这种方法被称为 静态。使用 静态 一词来指代类级别的特性来自 C++ 和 Java;它也被用于 Python。
对于类级别的属性,我们没有任何语法上的复杂性。正如我们在之前的例子中所看到的,任何不属于实例的属性都会在类中搜索;实例变量和类变量之间的区别不需要任何额外的语法。
然而,类级别的方无法将实例变量作为第一个定义的参数。这是一个重要的语法变化。我们使用@staticmethod装饰器来注释没有实例变量的方法。
我们将扩展前面展示的Sample类,以包括一个有效性检查。检查有效性不是一个合适的实例方法:我们不应该使用无效值创建一个实例。我们将把这个方法添加到类中:
@staticmethod
def validate(measure):
m= float(measure)
if 0 <= m < 12:
pass
else:
raise ValueError("Out of range")
我们已经用@staticmethod装饰器标记了这个方法。这个方法没有self变量,因为它不适用于类的实例。这个方法只能通过Sample.validate(some_value)来调用。这个方法将确认measure参数的值是否有效,或者它会引发一个异常,详细说明为什么该值无效。
我们可能使用这个方法来创建和使用Sample对象的实例:
try:
Sample.validate(some_data)
s= Sample(some_data)
*… etc. …*
except Exception as ex:
print(ex)
我们将简单地评估Sample.validate()方法来开始try语句。如果这个方法没有引发异常,给定的值是有效的。如果这个方法引发了异常,我们将写入一个错误消息并继续处理。通常,我们会在文件输入循环中有这种处理:我们会处理好的数据,并将关于坏数据的消息写入日志。
Python 还提供了一个@classmethod装饰器。这是一个更专业的工具。它将类作为参数而不是实例提供。它允许我们编写可以与各种类一起工作的方法。这可能在元类中使用。
我们将在第十三章元编程和装饰器中回到装饰器的话题。
使用__slots__来节省存储
object超类的默认行为是为对象的属性创建一个dict。这提供了快速的名称解析。这意味着对象可以非常自由地添加和更改属性。由于使用哈希来通过名称定位属性,内部的dict对象可能会消耗相当多的内存。
我们可以通过在创建类时提供一个特定的属性名称列表来修改object超类的行为。当我们将这些名称分配给特别命名的__slots__变量时,这些将成为唯一可用的属性。不会创建dict,从而大大减少了内存使用。
如果我们正在处理非常大的数据集,我们可能需要使用一个类似这样的类:
class SmallSample:
counter= 0
__slots__ = ["sequence", "measure"]
def __init__(self, measure):
SmallSample.counter += 1
self.sequence = SmallSample.counter
self.measure = measure
这个类使用__slots__属性来定义实例可以使用的唯一两个属性。这避免了使用dict来表示这个类的实例属性。
抽象基类的 ABC(ABC 代表 Abstract Base Classes)
在 第六章 中,我们探讨了 collections 模块,该模块在映射主题上提供了一些变体。这些不同类型的集合建立在 collections.abc 模块中定义的抽象基类的基础上。查看这个模块可以揭示集合的共同特征以及它们之间的差异。
我们可以看到 Sequence 是内置元组 class 的基础,而 MutableSequence 是内置 list 的基础。Set 抽象基类是内置 frozenset 类的基础,而 MutableSet 是内置 set 类的基础。Mapping 类没有具体的实现,但 dict 类是 MutableMapping 类的内置实现。
如果我们需要实现一种独特的集合类型,这种类型不是由 collection 模块提供的,我们鼓励使用 collections.abc 模块作为起点。如果我们利用这些常见的基类,我们可以确保我们的新集合能够与其他 Python 特性无缝结合。
编写可调用类
抽象基类 Callable 定义在 collections.abc 模块中。这个类似乎与集合没有太多关系。尽管如此,它是一个有用的抽象。
从 Callable 派生的类必须定义 __call__() 特殊方法。从这个类创建的对象是可调用的,可以用作函数。这允许我们基于类定义创建相当复杂的函数。
这是一个计算第 n 个斐波那契数的函数。计算这个值有三个相关的规则:

前两个斐波那契数定义为零和一。其他斐波那契数是前两个数的和。如果我们使用一个简单的算法,计算一个大的斐波那契数是非常昂贵的。然而,我们可以定义一个使用内部缓存的 Callable,以将工作量降低到可管理的水平。这种技术称为 记忆化。
Callable 类的定义如下:
from collections.abc import Callable
class Fibonacci(Callable):
def __init__(self):
self.cache= {0: 0, 1: 1}
def __call__(self, n):
if n not in self.cache:
self.cache[n]= self.__call__(n-1) + self.__call__(n-2)
return self.cache[n]
我们定义了一个类,Fibonacci,它扩展了 Callable 抽象基类。__init__() 方法初始化一个缓存,其中包含斐波那契数的两个定义值。__call__() 方法仅在数字不在缓存中时计算斐波那契数 n。它是通过递归调用来计算斐波那契数 n-1 和 n-2 的。一旦结果在缓存中,就可以返回。
当我们创建这个类的实例时,我们已经创建了一个可调用的函数。给定这个函数,我们可以计算斐波那契数。以下是一个示例:
>>> fib= Fibonacci()
>>> fib(7)
13
我们创建了一个 Fibonacci 类的实例,并将其分配给变量 fib。fib 对象是可调用的;当我们用六作为参数值评估它时,我们得到第七个斐波那契数。
概述
在本章中,我们看到了定义类和使用该类对象的基本方法。我们探讨了如何创建定义类行为的函数。类的内部状态是各种方法的结果:在 Python 中,我们并不正式声明实例变量。我们通常依赖于 __init__() 方法来提供对象的初始或默认值。
我们探讨了 Python 通过搜索对象、类以及超类来解决属性和方法名称的方式。方法解析顺序基于类在初始 class 语句中呈现的顺序。
@properties 装饰器可以用来创建与属性具有相同语法的函数。这有助于阐明其他情况下可能复杂的算法。我们还探讨了 @staticmethod 装饰器,它用于创建属于整个类的方法,且与类的任何特定实例无关。
为了节省一些内存,我们可以使用 __slots__ 变量。这将构建一个不基于 dict 存储属性的对象。这个对象要小得多,但也有一些限制。
我们还探讨了如何创建一个可调用的对象。这是一个可以像函数一样使用的对象,但具有对象的所有强大功能。
在第十二章 脚本、模块、包、库和应用 中,我们将探讨如何将我们的函数和类打包成模块。我们将了解模块是如何分组成包的。Python 标准库 是我们使用 Python 安装的一系列包。我们将探讨模块和脚本文件之间的微小区别,以及我们如何创建更完整的 Python 应用程序。
第十二章:脚本、模块、包、库和应用程序
虽然在与 Python 的 读取-评估-打印循环(REPL)>>> 提示符一起工作时很容易,但我们的真正目标是创建 Python 应用程序文件。一个 Python 文件可能是一个脚本,这意味着当它被 Python 程序执行时应该能够做一些有用的工作。一个文件可能是一个模块,这意味着它被设计为导入以提供有用的定义。Python 模块的目录是一个 包。这些都是由语言实现的正式定义。
更通用的术语如 库、应用程序或 框架 并没有由语言正式化。我们有在 Python 中实现这些常见概念的方法。我们可以将模块或包的集合视为库。例如,Python 标准库是一个包含大量模块和包的大集合。一个“应用程序”至少是一个脚本。更复杂的应用程序可能涉及脚本以及几个额外的模块和包。框架将是一个 Python 应用程序,我们将向其中注入定制的模块或包。许多框架还将包括非 Python 文件:一个网络框架可能包括大量的 HTML 和 CSS;一个 GUI 框架可能包括图像文件和字体。
我们将探讨创建和运行脚本文件。我们还将探讨创建模块和模块包。最后,我们将探讨一个非常巧妙的 Python 功能,它允许我们编写既可以作为脚本使用也可以作为模块使用的脚本。这种设计模式允许我们构建基于其他应用程序的复合应用程序。
脚本文件规则
Python 脚本文件必须遵循一条简单的规则:它必须是纯文本格式。在某些情况下,一个不恰当的文件名可能会导致问题,因此我们将提供两条经常有帮助的建议:
-
内容必须是纯文本;理想情况下使用 UTF-8 编码,尽管 ASCII 也很流行。
-
文件名应遵循 Python 标识符规则。它应以字母开头,并仅使用字母、数字和下划线
_字符。以__(两个下划线)开头和结尾的文件名是保留的,并且对 Python 有特殊含义。 -
扩展名应为
.py。
这两条额外的建议对于编写模块和包是必不可少的,但编写简单的脚本并不需要。
脚本简单地说是一系列语句;它与我们在 REPL 提示符 >>> 中可能执行的操作相同,只有一个区别:脚本没有隐式的打印输出。我们必须在脚本中使用 print() 函数来查看任何结果。在更大的应用程序中,我们经常使用 logging 模块来生成更复杂的输出。在某些情况下,随着应用程序的成熟,我们将仔细替换我们放入早期技术激增中的所有 print() 函数,用 logging.debug() 函数替换。
要运行脚本,我们需要将其作为输入提供给 Python 程序。我们将探讨三种常见的方法来实现这一点。
通过文件名运行脚本
运行脚本的常见方式是将文件名提供给 Python 命令。假设我们在名为Chapter_12的目录中有一个名为ch12_script1.py的文件。
在 Linux 和 Mac OS X 中,完整名称将是Chapter_12/ch12_script1.py。在 Windows 中,完整文件名将是Chapter_12\ch12_script1.py。在剩余的示例中,我们将坚持使用 Linux 标准的文件名。
这是我们通过提供文件名来运行脚本的方式:
MacBookPro-SLott:Code slott$ python3 Chapter_12/ch12_script1.py
Temperature °C: 8
C=8°, F=46°
这个输出显示了操作系统提示符。我们输入的python3命令被突出显示。提示符和脚本的输出也被显示出来。这个例子对于使用 Python 2 作为内部语言的操作系统来说是典型的;我们必须区分我们新的 Python 3 和操作系统的内部python命令。
应用程序提示我们,我们输入了 8 摄氏度。输出显示 8°C 大约是 46°F。我们需要穿上外套。
脚本文件ch12_script1.py看起来像这样:
c= float(input("Temperature °C: "))
f = 32+9*c/5
print("C={c:.0f}°, F={f:.0f}°".format(c=c,f=f))
该脚本使用input()函数在控制台提示交互式用户。输出通过简单的print()函数显示。
我们保持脚本较小,以强调脚本可以运行的方式。与此相关的用户体验(UX)问题有很多,但这不是本节的重点。
通过模块名称运行脚本
在大多数情况下,我们的脚本可以安装在 Python 库中的site-packages目录内,或者我们可以使用PYTHONPATH环境变量扩展 Python 路径,包括脚本的位置。这两种方法中的任何一种都可以使脚本文件在 Python 的搜索路径上可见。
要在site-packages中安装脚本,我们可以依赖 Python 的distutils包。我们将创建一个setup.py文件,该文件描述了我们想要安装的模块。然后我们可以运行python3 setup.py install,将我们的模块放置到site-packages目录中。像pip和easy-install这样的安装程序需要按照这个标准模式使用distutils。
我们还可以定位site-packages目录,并将我们的模块手动复制到该目录。这个位置因操作系统而异。这个目录是sys.path变量中的最后一个项目。
设置PYTHONPATH环境变量是另一种选择。我们可以使用 Linux 的export命令来更改环境变量。我们经常将其放在~/.bash_profile文件中。对于 Windows,我们必须更改设置环境变量的高级系统设置。我们可以通过PYTHONPATH变量轻松创建包含许多模块的私有库,使其可见。
一旦我们的模块在 Python 的搜索路径上可见,我们可以像这样执行模块:
MacBookPro-SLott:Code slott$ python3 -m Chapter_12.ch12_script1
Temperature °C: 8
C=8°, F=46°
当我们提供-m选项时,我们正在命名一个要执行的模块。在这个例子中,我们使用了一个限定名称:Chapter_12是一个包,ch12_script1是这个包内的模块。我们将在后面的部分中查看包;包基本上是模块文件可以找到的目录。
使用操作系统 shell 规则运行脚本
我们运行脚本的第三种方式是通过使脚本文件可执行,并在脚本文件和 Python3 程序之间建立操作系统关联。
在 Linux 和 Mac OS X 中,文件关联是通过文件的第一行设置的。我们经常将类似以下内容作为文件的第一行,以关联给定的.py文件和 Python3 程序:
#!/usr/bin/env python3
这将使用操作系统的env程序来定位并启动python3环境。shell 将整个文件作为输入提供给名为#!行的程序。这意味着env程序将以脚本文件作为输入启动。env程序将准备环境,然后将文件交给 Python3 程序。
要在 Linux 和 Mac OS X 中将文件标记为可执行,我们使用chmod +x命令。我们可以这样做来标记我们的脚本为可执行:
MacBookPro-SLott:Code slott$ chmod +x Chapter_12/ch12_script1.py
此命令将执行x选项添加到文件的模式中。当我们执行ls -l时,我们会在文件详情中看到这一点。
在 Windows 中,所有文件都被认为是可执行的。文件扩展名与程序的关联是通过 Windows 控制面板完成的。这个设置是在你安装 Python 时设置的。
一旦文件被标记为可执行,我们只需提供名称就可以运行它:
MacBookPro-SLott:Code slott$ Chapter_12/ch12_script1.py
在 Windows 中,.py文件扩展名绑定到 Python 程序,Windows 将启动 Python,并将此文件名作为输入。文件名与脚本的绑定在应用程序之外。
在 Linux 和 Mac OS X 中,处理基于文件的神奇第一行。Linux shell 检查文件的模式,以确认它是可执行的。然后它读取文件的前几个字节。在这种情况下,前几个字节是#!,这标志着文件是一个脚本。脚本的第一行完整地包含了必须用于处理此脚本命令。在这种情况下,命令是/usr/bin/env python3。shell 使用这个程序作为输入启动这个程序。
选择好的脚本名称
脚本名称应保持简短且具有意义。与文件名一样,通常最好避免复杂的前缀和后缀。Linux 或 Windows DOS 命令提供了一些关于什么使脚本名称好(和不好)的指导。最好的例子之一是git命令,它有众多的子命令。而不是发明几十个看起来复杂的名称,git使用一个简单的命令名作为前缀。
用于解析命令行参数的argparse模块很好地支持了这一点。我们可以定义一些适用于所有子命令的常见参数。我们还可以定义仅适用于每个子命令的独特参数。
为了保持这本书的代码按照出版流程组织,脚本名称很长。这些名称中的冗余(Chapter_12/ch12_...)不是最佳实践,应尽可能避免。与变量名和函数名一样,脚本名称应保持合理短且有意义。
创建可重用模块
在 Python 中,模块是软件重用的单元。当我们有一个必须出现在多个脚本中的特性时,我们会把这个特性放入一个模块中,并将该模块导入到每个共享该特性的脚本中。
需要注意“重用”这个词的两个略有不同的含义如下:
-
我们可以通过定义类层次结构在应用程序内实现局部重用。继承是共享相关对象代码的一种优雅方式。我们通常会在单个模块文件中定义所有这些相关类。
-
我们可以定义一个模块以实现跨应用程序的较少局部重用。
要创建一个可导入的模块,我们只需确保 Python 文件可见于 Python 搜索路径的一部分目录中。由于本地目录总是可见的,我们只需在当前工作目录中创建一个文件即可创建一个模块。
设计用于导入的模块应主要由import、class和def语句组成。我们也可以使用赋值语句来创建模块全局变量,但我们需要谨慎处理多少工作。通过赋值、class、def或import创建的任何名称都将位于该模块的命名空间中。
模块只导入一次。import实现会检查全局已加载模块的缓存,即sys.modules,以查看模块是否已知。正因为如此,实际上执行某种处理的模块只会这样做一次。之后,导入就会被忽略。这种行为使得在导入模块内部创建全局单例对象变得容易。
在import时进行大量处理的模块示例有this和antigravity。当我们执行import this或import antigravity时,这些模块将立即执行一些有趣的处理。一旦被导入一次,它们就不会再这样做。虽然在一些特定情况下很有用,但这不是一个通用的模式。
小贴士
我们通常期望一个import语句提供类、函数和模块全局变量的定义。
我们通常不期望import语句执行有用的处理。
模块可以定义一个独特的异常。我们可能想在模块中创建一个名为Error的通用异常类。它可能看起来像这样:
class Error(Exception): pass
因为当模块被导入时,这个名称将由模块名称限定,所以我们能够通过some_module.Error来引用这个异常。它可能看起来像这样:
import some_module
try:
some_module.some_function()
except some_module.Error as e:
logger.exception("some_function broke: {0}".format(e))
模块名some_module作为Error类定义来源的一个很好的限定符。我们不需要给Error类一个更复杂、全局唯一的名称。
创建混合库/应用程序模块
脚本可以导入模块,也许定义一些函数或类,但它总是会执行相关的处理。我们的第一个示例脚本只有三条相关的处理语句:两个赋值语句和一个打印结果的函数声明。这展示了 Python 的理想,即没有任何样板代码的程序;我们试图避免只是开销的语法。
完美清洁脚本方法的可能缺点是创建单元测试困难。每个单元测试都必须作为子进程调用脚本;这可能涉及相当多的操作系统开销。单元测试的目标是隔离每个单元——每个函数、类、模块、包或脚本——以便可以单独测试。让操作系统启动脚本文件似乎并没有得到适当的隔离。
此外,随着应用程序的成熟,一个好的脚本可能成为更大、更全面的应用程序中的一个组件。从脚本文件中创建复合应用程序可能会变得困难。从函数或类中创建复合过程则容易得多。
这导致了对脚本以下建议的结构:
def c_to_f():
c= float(input("Temperature °C: "))
f = 32+9*c/5
print("C={c:.0f}°, F={f:.0f}°".format(c=c,f=f))
if __name__ == "__main__":
c_to_f()
我们已经将脚本用 def 语句包装起来,使其成为一个函数。然后我们编写了一个 if 语句,通过检查 __name__ 变量来区分主脚本和导入的模块。if 语句做出以下条件:
-
当模块被导入时,Python 将全局变量
__name__设置为实际的模块名称 -
当作为主脚本运行时,Python 将全局变量
__name__设置为__main__
这种模式可以用来编写运行其自身单元测试的库模块。我们可以在一个永远不会作为主脚本的库模块中包含以下内容:
if __name__ == "__main__":
import doctest
doctest.testmod( verbose=1 )
这将运行嵌入在文档字符串中的所有单元测试。我们将在第十四章第十四章。完善 – 单元测试、打包和文档中更详细地讨论测试,完善 – 单元测试、打包和文档。
创建包
包是一个包含模块文件和一个附加文件的目录。每个包都必须有一个 __init__.py 文件。此文件必须存在,通常是空的。
Tim Peters 的诗歌《Python 之禅》提供了以下建议:
扁平优于嵌套。
理想是尽可能地将 Python 应用程序组织成模块的扁平集合。深度嵌套、复杂的包层次结构并不被认为是有帮助的。
我们可以使用两种方式使用包。我们可以导入包的一部分模块。例如,标准库有一个包含几个 XML 解析模块的 XML 包。我们可以使用 import xml.etree 从 XML 包中导入 etree 模块。在这种情况下,__init__.py 文件有一个注释和子包列表。
在其他情况下,我们可以将包作为一个模块整体导入。例如,当我们编写 import collections 时,实际上我们导入的是模块 collections/__init__.py。
__init__.py 文件是整个包的最高级模块。它可以是一个空文件,在这种情况下,我们只能从包内部选择特定的模块。或者 __init__.py 文件可能包含内容,允许我们将整个包导入为一个单一的复杂结构。
设计替代实现
我们可以轻松地提供给定功能的替代实现。如果我们想要更高的速度、更高的精度或更少的内存使用,我们应该能够导入给定库的替代定义。
我们可以通过比较 math 和 cmath 模块来具体说明这个原则。以下是他们之间差异的一个例子:
>>> import math
>>> import cmath
>>> math.sqrt(-1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: math domain error
>>> cmath.sqrt(-1)
1j
math 模块包含一个平方根函数,我们使用 math.sqrt() 来调用它。这个函数只产生实数值结果,并且当遇到非实数值的表达式时必须抛出异常。
cmath 模块也包含一个平方根函数。cmath.sqrt() 函数可以返回复数值而不是抛出异常。由于这两个包非常相似,我们可以以多种方式相互替换。
这两个模块提供了一组类似的函数定义。模块内的组件具有相同的名称。作为命名空间的模块有不同的名称,以区分定义的来源。
这种技术通常用于支持不同的平台。我们可以在包内部创建具有平台特定模块的包。包的最高级 __init__.py 可以选择导入哪个模块并提供平台特定的定义。我们也可以使用这种方法来编写必须在不同环境中运行的商业软件:开发、质量保证和最终生产。一个包可以包含不同的配置模块。标准库中的 os 包展示了这个概念。
查看包搜索路径
我们可以通过导入 sys 包来查看 Python 的搜索路径 sys.path:
>>> import sys
>>> sys.path
['', '/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/site-packages/setuptools-2.0.2-py3.3.egg',
*…, etc.*
'/Library/Frameworks/Python.framework/Versions/3.3/lib/python33.zip',
'/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3',
'/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/plat-darwin',
'/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/lib-dynload',
'/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/site-packages']
我们已经省略了输出中的许多行,以展示标准库如何融入我们开发 Python 代码的方式。这个搜索模块的位置列表是由 sites 包在 Python 启动时构建的。
零长度目录名 '' 是第一个。这意味着当前工作目录是首选的模块定位位置。这允许我们从本地目录导入我们自己的模块。在我们的本地目录之后,会搜索多个位置,最后以 .../site-packages 目录结束。
接下来的名称组,从 setuptools-2.0.2-py3.3.egg 开始,是所有以下载的 .egg 文件形式添加到这个安装中的外部包的列表。确切列表会因安装而异。这些名称是由 pip 和 easy_install 程序创建的。
当我们设置PYTHONPATH环境变量时,这些名称会被拼接到各种已安装包的路径之后。以python33.zip开始的最后一组名称是 Python 附带的一些常见模块列表。最后一项列出的是库的通用 site-packages 部分。如果你下载了一个包并运行该包的setup.py脚本,它将被复制到这个目录中,Python 将会找到它。
sys.path对象是一个正确的可变列表。我们可以在脚本文件中动态地更改路径。这可能会使得确定脚本所依赖的所有模块变得困难。几乎总是更清晰的是明确地依赖于正确安装的模块或设置PYTHONPATH环境变量。
摘要
在本章中,我们探讨了组织软件的高级方法。一个函数包含许多语句,一个类包含许多方法函数,一个模块可以包含许多类和函数。一个包可以包含许多模块。
我们已经探讨了执行 Python 脚本的各种方法。由于我们需要在许多不同的上下文中执行软件,所以我们有很大的灵活性。通常,我们会关注通过模块名而不是文件名来执行 Python 程序。这种区别很小。由于一个模块必须在搜索路径上,我们可以创建一个包含脚本以及任何支持模块和库的目录,并确保这个目录被命名为PYTHONPATH。
我们已经探讨了如何创建包含定义的库模块,这些模块将被导入到其他脚本中。这是我们主要的重用方法。我们还探讨了如何创建一个作为库模块可重用的脚本。这支持单元测试以及我们软件的成熟。
在第十三章中,我们将探讨一些更高级的编程技术。这些技术将使我们能够创建更复杂的类和函数定义。我们可以使用这些设计模式来编写更灵活和可重用的软件。
第十三章。元编程和装饰器
我们所涵盖的大部分内容都是编程——编写 Python 语句来处理数据。我们也可以使用 Python 来处理 Python,而不是处理数据。我们将这称为元编程。我们将探讨两个方面:装饰器和元类。
装饰器是一个接受函数作为参数并返回函数的函数。我们可以使用它来向函数添加功能,而无需在多个不同的函数定义中重复该功能。装饰器防止了复制粘贴编程。我们经常用于日志记录、审计或安全目的;这些都是将跨越多个类或函数定义的事情。
元类定义将扩展当我们创建类的实例时发生的本质对象创建。隐式地,使用特殊的__new__()方法名称来创建一个裸对象,随后由类的__init__()方法进行初始化。元类允许我们改变对象创建的一些基本特性。
使用装饰器的简单元编程
Python 有一些内置的装饰器可以修改函数或类的成员方法。例如,在第十一章类定义中,我们看到了@staticmethod和@property,它们用于改变类中方法的行怍。@staticmethod装饰器将函数改为在类上工作,而不是在类的实例上工作。@property装饰器使得评估无参数方法可以通过与属性相同的语法进行。
functools模块中可用的函数装饰器是@lru_cache。这修改了一个函数以添加备忘录。缓存结果可以显著提高速度。它看起来是这样的:
from functools import lru_cache
from glob import glob
import os
@lru_cache(100)
def find_source(directory):
return glob(os.path.join(directory,"*.py"))
在这个例子中,我们导入了@lru_cache装饰器。我们还导入了glob.glob()函数和os模块,这样我们就可以使用os.path.join()来创建文件名,而不管操作系统特定的标点符号。
我们向@lru_cache()装饰器提供了一个大小参数。参数化装饰器通过添加一个将保存 100 个先前结果的缓存来修改find_source()函数。这可以加快大量使用本地文件系统的程序。最近最少使用(LRU)算法确保保留最近的请求,并悄悄地忘记较旧的请求,以限制缓存的大小。
@lru_cache装饰器体现了一种可重用的优化,可以应用于各种函数。我们将函数实现的备忘录方面与其他方面分离。
Python 标准库定义了一些装饰器。有关装饰器元编程的更多示例,请参阅 Python 装饰器库页面,wiki.python.org/moin/PythonDecoratorLibrary。
定义我们自己的装饰器
在某些情况下,我们可以从多个函数中提取一个共同方面。像安全、审计或日志记录这样的关注点是我们在许多函数或类中一致实现的一些常见示例。
让我们看看一种支持增强调试的方法。我们的目标是有一个简单的注解,我们可以用它从几个无关的函数中提供一致、详细的输出。我们希望创建一个具有如下定义的模块:
@debug_log
def some_function(ksloc):
return 2.4*ksloc**1.05
@debug_log
def another_function(ksloc, a=3.6, b=1.20):
return a*ksloc**b
我们定义了两个简单的函数,这些函数将被装饰器包装以提供一致的调试输出。
装饰器是一个接受函数作为参数并返回函数作为结果的函数。前面代码块中展示的内容评估如下:
>>> def some_function(ksloc):
... return 2.4*ksloc**1.05
>>> some_function = debug_log(debug_log)
当我们将装饰器应用于一个函数时,我们隐式地用原始函数作为参数评估装饰器函数。这将创建一个装饰后的函数作为结果。使用装饰器创建的结果与原始函数具有相同的名称——装饰后的版本替换了原始版本。
为了使这生效,我们需要编写一个装饰器来创建调试日志条目。这必须是通用的,以便它适用于任何函数。正如我们在第七章中提到的,基本函数定义,我们可以使用*和**修饰符将“所有其他”位置参数和所有其他关键字参数收集到一个单一的序列或一个单一的字典中。这允许我们编写完全通用的装饰器。
这里是@debug_log装饰器函数:
import logging
from functools import wraps
def debug_log(func):
log= logging.getLogger(func.__name__)
@wraps(func)
def decorated(*args, **kw):
log.debug(">>> call(*{0}, **{1})".format(args, kw))
try:
result= func(*args, **kw)
log.debug("<<< return {}".format(result))
return result
except Exception as ex:
log.exception( "*** {}".format(ex) )
raise
return decorated
装饰器定义的主体做了三件事。首先,它根据原始函数的名称func.__name__创建了一个日志记录器。其次,它定义了一个全新的函数,命名为decorated(),这个函数基于原始函数。最后,它返回这个新函数。
注意,我们使用了functools库中的一个装饰器@wraps来显示新装饰器函数包装了原始函数。这将确保名称和文档字符串正确地从原始函数复制到装饰后的函数。装饰后的版本将无法与原始版本区分。
我们可以像使用普通函数一样使用这些函数:
>>> round(some_function(25),3)
70.477
装饰对函数的值没有影响。它有轻微的性能影响。
如果我们启用了日志记录,并将日志级别设置为DEBUG,我们将在日志中看到额外的输出。前面的示例会导致以下内容出现在日志记录器的输出中:
DEBUG:some_function:>>> call(*(25,), **{})
DEBUG:some_function:<<< return 70.47713658528114
这显示了由这个装饰器产生的调试细节。日志显示了参数值和结果值。如果有异常,我们还会看到参数值以及异常信息,这比只显示异常信息更有用。
启用日志记录的一个简单方法是在应用程序中包含以下内容:
import sys
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
这将把日志输出定向到标准错误流。它还将包括所有严重级别高于调试级别的消息。我们可以将此级别设置更改为类似 logging.INFO 的值,以静默调试消息,同时保留信息性消息。
一个也接受参数值的装饰器——类似于 @lru_cache 装饰器——更复杂。首先将参数值应用于创建装饰器。然后使用这个初始绑定产生的装饰器来构建装饰函数。
使用元类的更复杂元编程
在某些情况下,类中内置的默认功能可能不适合我们的特定应用。我们可以看到一些常见的情况,我们可能想要扩展对象构造的默认行为。
-
我们可以使用元类来保留定义类的原始源代码的一部分。默认情况下,每个类对象使用
dict来存储各种方法和类级属性。我们可能想使用有序字典来保留类级属性的原始源代码顺序。一个例子可以在 Python 标准库 的 3.3.3.5 节中找到。 -
抽象基类(ABC)依赖于一个
metaclass __new__()方法来确认当我们尝试创建类的实例时,具体的子类是否完整。如果我们未能在一个 ABC 的子类中提供所有必需的方法,我们就无法创建该子类的实例。 -
元类可用于简化对象序列化。元类可以包含用于 XML 或 JSON 表示实例所需的信息。
-
我们可以使用元类向对象注入额外的属性。因为元类提供了创建空对象的
__new__()方法的实现,它能够在__init__()方法评估之前注入属性。对于一些不可变类,如元组,没有__init__()方法,元组的子类必须使用__new__()方法来设置值。
默认元类是 type。这是由应用程序类在调用 __init__() 方法之前创建新裸对象时使用的。内置的 type.__new__() 方法需要四个参数值——元类、应用程序类的名称、应用程序类的基础类,以及系统定义的初始值命名空间。
当我们创建元类时,我们将覆盖 __new__() 方法。我们仍然会使用 type.__new__() 方法来创建裸对象。然后我们可以在返回对象之前扩展或修改这个裸对象。
这里有一个在 __init__() 之前插入日志记录器的元类:
import logging
class Logged(type):
def __new__(cls, name, bases, namespace, **kwds):
result = type.__new__(cls, name, bases, dict(namespace))
result.logger= logging.getLogger(name)
return result
我们定义了一个扩展内置 type 类的类。我们定义了一个重写的特殊方法,__new__()。这个特殊方法使用超类 type.__new__() 方法来创建裸对象,并将其分配给 result 变量。
一旦我们有了裸对象,我们就可以创建一个记录器并将这个记录器注入到裸对象中。这个self.logger属性将在使用这个元类创建的每个类的__init__()方法的第 一行就可用。
我们可以创建利用这个元类的应用程序类,如下所示:
class Machine(metaclass=Logged):
def __init__(self, machine_id, base, cost_each):
self.logger.info("creating {0} at {1}+{2}".format(
machine_id, base, cost_each))
self.machine_id= machine_id
self.base= base
self.cost_each= cost_each
def application(self, units):
total= self.base + self.cost_each*units
self.logger.debug("Applied {units} ==> {total}".format(
total=total, units=units, **self.__dict__))
return total
我们定义了一个显式依赖于Logged元类的类。如果我们不包括metaclass关键字参数,将使用默认的type元类。在这个类中,logger属性是在调用__init__()方法之前创建的。这允许我们在__init__()方法中使用记录器而不需要任何额外的开销。
摘要
在本章中,我们探讨了两种常见的元编程技术。第一种是编写装饰器函数——这些可以用来转换原始函数以添加新特性。第二种是使用元类来扩展类定义的默认行为。
我们可以使用这些技术来开发跨越许多功能和类的应用程序特性。一次编写一个特性并将其应用于多个类,可以确保一致性,并在调试、升级或重构期间提供帮助。
在第十四章“完善 - 单元测试、打包和文档”中,我们将探讨一系列特征,这些特征定义了一个完整的 Python 项目。我们不会处理技术语言特性,而是会探讨我们可以如何使用 Python 特性来创建精致、完整的解决方案。
第十四章。完善与测试 - 单元测试、打包和文档
除了 Python 语言及其库之外,Python 编程还有其他几个方面。我们将首先仔细研究文档字符串,它们应被视为每个包、模块、类和函数定义的基本组成部分。它们有几个目的,其中之一是阐明对象的功能。
在本章中,我们还将探讨单元测试的不同方法。doctest和unittest模块提供了一套全面的工具。外部工具如 Nose 也被广泛使用。
我们还将探讨如何利用logging模块作为完整应用程序的一部分。Python 的记录器也非常复杂,因此我们将关注一些基本功能。
我们将检查一些用于从嵌入的文档字符串注释构建 Python 文档的工具。使用工具提取文档使我们能够专注于编写正确的代码,并从代码中派生出参考文档。为了创建完整的文档——而不仅仅是 API 参考——许多开发者使用 Sphinx 工具。
我们还将讨论在大型 Python 项目中文件的组织结构。由于 Python 被用于许多不同的环境和框架,使用 Flask 构建的 Web 应用程序的布局将与使用 Django 构建的 Web 应用程序大相径庭。然而,我们可以遵循一些基本原则,以保持 Python 程序整洁和井然有序。
编写文档字符串
在第七章,基本函数定义中,我们指出所有函数都应该有一个描述函数的文档字符串。在第十一章,类定义和第十二章,脚本、模块、包、库和应用程序中,我们提供了类似的建议,但没有提供很多细节。
def语句和class语句应该普遍地后面跟着一个三引号字符串,描述函数、方法或类。这不是语言的要求——这是所有试图阅读、理解、扩展、改进或修复我们代码的人的要求。
我们将回顾第十一章中的一个示例,类定义,以展示省略的文档字符串类型。以下是我们可能创建的更完整的类定义示例:
class Point:
"""
Point on a plane.
Distances are calculated using hypotenuse.
This is the "as a crow flies" straight line distance.
Point on a plane.
Distances are calculated using hypotenuse.
This is the "as a crow flies" straight line distance.
>>> p_1 = Point(22, 7)
>>> p_1.x
22
>>> p_1.y
7
>>> p_1
Point(22, 7)
"""
def __init__(self, x, y):
"""Create a new point
:param x: X coördinate
:param y: Y coördinate
"""
self.x= x
self.y= y
def __repr__(self):
"""Returns string representation of this Point."""
return "{cls}({x:.0f}, {y:.0f})".format(
cls=self.__class__.__name__, x=self.x, y=self.y)
def dist(self, point):
"""Distance to another point measured on a plane.
>>> p_1 = Point(22, 7)
>>> p_2 = Point(20, 5)
>>> round(p_1.dist(p_2),4)
2.8284
:param point: Another instance of Point.
:returns: float distance.
"""
return math.hypot(self.x-point.x, self.y-point.y)
在这个类定义中,我们提供了四个独立的文档字符串。对于整个类,我们提供了一个概述,说明了类的作用,以及一个展示类行为的示例。这显示了从 Python 交互式解释器(REPL)复制粘贴的内容,输入前带有>>>提示符。
对于每个方法函数,我们提供了一个文档字符串,显示了方法函数的作用。在 dist() 方法的例子中,我们在文档字符串中包含了另一个交互示例,以展示该方法预期行为的示例。
参数和返回值的文档使用 ReStructuredText (RST) 标记语言。这因其工具如 docutils 和 Sphinx 而被广泛使用,这些工具可以将 RST 格式化为漂亮的 HTML 或 LaTeX。我们将在本章后面的 使用 RST 标记编写文档 部分查看 RST。
现在,我们可以关注 :param name: 和 :returns: 作为帮助工具理解这些构造语义的标记语法。然后,工具可以给予它们特殊的格式化以反映其含义。
使用 doctest 编写单元测试
在文档字符串中提供类和函数的具体示例是一种广泛采用的实践。正如前面示例所示,我们可以在文档字符串中提供以下类型的示例文本:
>>> p_1 = Point(22, 7)
>>> p_2 = Point(20, 5)
>>> round(p_1.dist(p_2),4)
2.8284
具体示例有许多好处。Python 代码的目标是美观和可读。如果代码示例晦涩或令人困惑,这是一个真正应该解决的问题的设计问题。在注释中写更多文字来尝试解释糟糕的代码是更深层次问题的症状。具体示例应该像代码本身一样清晰和富有表现力。
具体示例的另一个好处是它们是测试用例。doctest 模块可以扫描每个文档字符串以定位这些示例,构建并执行测试用例。这将确认示例中的输出与实际输出相匹配。
使用 doctest 的一个常见方法是在 library 模块中包含以下内容:
if __name__ == "__main__":
import doctest
doctest.testmod(verbose=1)
如果模块作为主脚本执行而不是被导入,那么它将导入 doctest,扫描模块以查找文档字符串,并执行这些文档字符串中的所有测试。我们已将详细级别设置为一级,这将产生显示测试的详细输出。如果我们保留详细级别为其默认值零,则成功将是静默的;甚至不会显示 Ok。
我们也可以将 doctest 作为命令行应用程序运行。以下是一个示例:
MacBookPro-SLott:Code slott$ python3 -m doctest Chapter_1/ch01_ex1.py -v
Trying:
355/113
Expecting:
3.1415929203539825
ok
...
1 items had no tests:
ch01_ex1
9 items passed all tests:
2 tests in __main__.__test__.assignment
4 tests in __main__.__test__.cmath
2 tests in __main__.__test__.division
1 tests in __main__.__test__.expression
3 tests in __main__.__test__.import 1
1 tests in __main__.__test__.import 2
2 tests in __main__.__test__.import 3
2 tests in __main__.__test__.mixed_math
2 tests in __main__.__test__.print
19 tests in 10 items.
19 passed and 0 failed.
Test passed.
我们已将 doctest 模块作为应用程序运行,向其提供要检查以定位文档字符串中测试示例的文件名。输出从找到的第一个示例开始。该示例是:
>>> 355/113
3.1415929203539825
详细输出显示了表达式和预期结果。ok 的输出表示测试通过。
那么那个没有测试的项目呢?这是模块本身的文档字符串。这表明我们的测试用例覆盖率不完整。我们应该考虑在模块文档字符串中添加一个测试。
总结显示有 9 个项目有 19 个测试。这些项目用字符串如 ch01_ex1.__test__.assignment 来标识。特殊名称 __test__ 既不是函数也不是类;它是一个全局变量。如果存在名为 __test__ 的变量,它必须是一个字典。__test__ 字典中的键是文档,值是必须包含 doctest 示例的字符串。
__test__ 变量可能看起来像这样:
__test__ = {
'expression': """
>>> 355/113
3.1415929203539825
""",
*etc.*
}
每个键标识一个测试。每个值是一个包含预期结果的 REPL 交互片段的三引号字符串。
作为一项实际措施,这个特定的测试受到了 doctest 示例潜在局限性之一的影响。
正如我们在 第五章 中提到的,逻辑、比较和条件,我们不应该在浮点值之间使用精确相等测试。编写这种测试的正确方式是使用 round(355/113, 6) 来截断尾数;最终数字可能会因硬件或底层浮点库而略有不同。编写与实现细节无关的测试会更好。
doctest 示例存在一些潜在的局限性。字典键没有定义的顺序。因此,当键以与测试中预期输出不同的顺序显示时,doctest 可能会失败。同样,集合项也没有定义的顺序。此外,错误跟踪消息可能不会精确匹配,因为它将包含类似 File "<stdin>", line 1, in <module> 的行,这可能会根据测试运行的上下文而变化。
对于这些潜在的局限性,doctest 提供了可以用来注释测试的指令。这些指令以特殊的注释形式出现,例如:# doctest: +ELLIPSIS。这将启用灵活的模式匹配,以应对显示输出的变化。对于其他局限性,我们需要正确构建我们的测试用例。我们可以使用 sorted(some_dict.values()) 将字典结果转换为有序的元组列表,其中顺序是有保证的。
Docstrings 是良好 Python 编程的一个基本特性。示例是良好文档的一个基本特性。给定一个可以验证示例正确性的工具,这种测试应该被认为是强制性的。
使用 unittest 库进行测试
对于更复杂的测试,doctest 示例可能不足以提供深度或灵活性。包含大量案例的文档字符串可能太长,无法作为有效的文档。包含复杂测试设置、拆卸或模拟对象的文档字符串也可能不适用于文档。
对于这些情况,我们将使用 unittest 模块来定义测试用例和测试套件。当使用 unittest 时,我们通常会创建单独的模块。这些测试模块将包含包含测试方法的 TestCase 类。
下面是一个典型的测试用例类定义的快速概述:
import unittest
from Chapter_7.ch07_ex1 import FtoC
class Test_FtoC(unittest.TestCase):
def setUp(self):
self.temps= [50, 60, 72]
def test_single(self):
self.assertAlmostEqual(0.0, FtoC(32))
self.assertAlmostEqual(100.0, FtoC(212))
def test_map(self):
temps_c = list(map(FtoC, self.temps))
self.assertEqual(3, len(temps_c))
rounded = [round(t,3) for t in temps_c]
self.assertEqual([10.0, 15.556, 22.222], rounded)
我们已经展示了 setUp() 方法以及两个测试方法。默认的 runTest() 方法将搜索所有以 test 开头名称的方法;然后运行在各个 test... 方法之前执行的 setUp() 方法。
我们可以使用 Python 的 assert 语句来比较实际和预期结果。因为有很多常见的比较,TestCase 类提供了方便的方法来比较不同类型的预期结果与实际结果。我们已经展示了 assertEqual() 和 assertAlmostEqual()。这些方法中的每一个都与 assert 语句平行——它们无声地成功。如果有问题,它们将引发 AssertionError 异常。
使用 unittest 模块允许我们编写大量的测试用例。doctest 字符串在表达几个有用的具体示例时最有用。单元测试是包含许多边缘和角落情况的更好方式。
unittest 模块也便于测试涉及与文件系统交互的示例。我们可能有一个包含多个示例的 .csv 格式文件。我们可以编写一个 runTest() 方法来读取这个文件,并将每一行作为测试用例处理。
在追求验收测试驱动开发(ATDD)时,测试用例本身可能变得相当复杂。测试用例设置可能涉及在执行大型应用程序功能之前用样本数据填充数据库,然后检查结果数据库的内容。ATDD 测试的基本结构符合 unittest 模块提供的单元测试设计模式。被测试的“单元”不是一个孤立的类;相反,我们正在测试一个完整的 Web API 或命令行应用程序。
结合 doctest 和 unittest
我们可以将 doctest 测试用例纳入 unittest 测试套件中。这确保了在使用 unittest 测试用例时不会遗漏 doctest 示例。我们将通过使用可以包含其他 TestCase 类以及 TestSuite 类的 TestSuite 类来实现这一点。
一个 doctest.DocTestSuite 对象将从给定模块中嵌入的 doctest 字符串创建一个合适的 unittest.TestSuite 方法。我们可以使用以下类似的功能来定位大量包和模块中的所有测试用例:
def doctest_suite():
files = glob.glob("Chapter*/ch*_ex*.py")
by_chxx= lambda name: name.partition(".")[2].partition("_")[0]
modules = sorted(
(".".join(f.replace(".py","").split(os.sep)) for f in files),
key=by_chxx)
suites= [doctest.DocTestSuite(m) for m in modules]
return unittest.TestSuite(suites)
这个函数将返回一个由其他 TestSuite 对象构建的 TestSuite 对象。这个函数有五个步骤:
-
它使用
glob.glob()来获取包中所有匹配的模块名称列表。这个特定的模式将定位到本书的所有示例代码。我们可能需要更改这个模式以通过或拒绝其他可能存在的名称。 -
它定义了一个 lambda 对象,该对象从模块中提取章节号,忽略包。该表达式使用
name.partition(".")将完整的模块名称拆分为包、点字符和模块名称。序列中的第 2 项是模块名称。这是在"_"上分割的,包括章节前缀、下划线和示例后缀。我们使用序列中的第 0 项,即章节前缀,作为模块的排序顺序。 -
sorted()函数的输入是一个重新结构化为模块名称的文件名序列。这种转换涉及替换".py"文件名后缀,然后在操作系统路径分隔符(在大多数操作系统中是 "/",但在 Windows 中是 "") 上分割文件名,得到单独的子字符串。当我们使用 "." 连接这些子字符串时,我们得到一个模块名称,我们可以用它来进行排序和测试用例发现。 -
我们构建了一个列表推导式,该推导式可以构建每个模块中的 doctest 示例可以构建的测试套件。这包括从本书的示例中提取的超过 100 个单独的测试。
-
我们从测试套件列表中组装一个单独的测试套件。然后可以执行它以确认所有示例都产生预期的结果。
我们可以将这个 doctest TestSuite 对象与从基于 unittest.TestCase 定义的测试构建的 TestSuite 对象合并。然后,这个完整的测试套件可以执行以证明代码按预期工作。
我们经常使用以下类似的方法:
if __name__ == "__main__":
runner= unittest.TextTestRunner( verbosity=1 )
all_tests = unittest.TestSuite( suite() )
runner.run( all_tests )
这将创建一个测试运行器,它可以生成测试和测试失败的摘要。suite() 函数(未显示)返回一个由 doctest_suite() 函数和一个扫描文件以查找 unittest.TestCase 类的函数构建的 TestSuite() 方法。
输出总结了运行的测试和失败情况。当我们构建这样的综合测试套件时,我们包括 unittest 和 doctest 测试用例。这允许我们自由地混合复杂的测试套件和简单的文档字符串示例。
使用其他附加测试库
doctest 和 unittest 模块允许我们方便地编写单元测试。在许多情况下,我们希望有更多的复杂性。更受欢迎的附加功能之一是测试发现。nose 包为我们提供了一种轻松检查模块和包以查找测试的方法。有关更多信息,请参阅 nose.readthedocs.org/en/latest/。
使用 nose 作为 unittest 的扩展有几个好处。nose 模块可以从 unittest.TestCase 子类、简单的测试函数以及不是 unittest.TestCase 子类的测试类中收集测试。我们还可以使用 nose 来编写计时测试——这在 unittest 中可能有点尴尬。
由于nose特别擅长自动收集测试,因此无需手动将测试用例收集到测试套件中;我们不需要之前展示的一些示例。此外,nose支持在包、模块和类级别上使用测试固定装置,因此可以尽可能少地执行昂贵的初始化。这使得我们可以为多个相关测试模块填充测试数据库——这是unittest难以轻松做到的。
记录事件和条件
一个表现良好的应用程序可以生成各种处理摘要。对于命令行应用程序,摘要可能是一条简单的“一切正常”的消息。对于图形用户界面应用程序,这种摘要正好相反——沉默意味着一切正常,而带有错误消息的对话框则表明事情没有按预期进行。
在某些命令行处理上下文中,摘要可能包括一些关于处理的对象数量的额外细节。在金融应用程序中,一些计数和不同对象的总额必须正确平衡,以显示所有接收到的输入对象都变成了适当的输出。
当我们需要比简单的“工作或失败”摘要更多的详细信息时,我们可以利用print()函数。输出可以重定向到sys.stderr文件以生成一个方便的日志。虽然这在小型程序中很有效,但logging模块提供了许多期望的特性。
使用logging模块的第一步是创建记录器对象并使用记录器生成有用的输出。每个记录器都有一个名称,该名称使用.字符分隔,并适合到一个树结构中。记录器名称与模块名称的标准平行;我们可以使用以下方法:
import logging
logger = logging.getLogger(__name__)
这将创建一个与模块名称匹配的模块级logger对象。根记录器具有名称"";即一个空字符串。
我们还可以创建类级别的记录器以及对象特定的记录器。例如,我们可以在对象创建的__init__()方法部分创建一个记录器。我们可能会使用对象类的__qualname__属性为记录器提供有资格的类名。要为类的特定实例创建记录器,我们可以在类名后缀一个.字符和一些唯一的实例标识符。
我们使用记录器创建具有从DEBUGGING(最不严重)到FATAL或CRITICAL(最严重级别的同义词)严重级别的消息。我们通过反映严重级别的名称来执行此操作。使用如下方法创建消息:
logger.debug("Finished with {0} using {2}".format(message, details))
logger.error("Error due to {0}".format(data))
logging模块有一个默认配置,不执行任何操作。这意味着我们可以在应用程序中包含日志请求而不需要进一步考虑。只要我们正确创建Logger实例并使用记录器实例的方法,我们就不需要做任何事情。
要查看输出,我们需要创建一个处理器,将消息写入特定的流或文件。这通常作为日志系统整体配置的一部分来完成。
配置日志系统
我们有几种配置日志系统的方式。对于小型应用程序,我们可能会使用logging.basicConfig()函数来提供日志设置。这在第十三章 元编程和装饰器 中已经展示过。简单的初始化会将输出发送到标准错误流,并显式设置一个级别来过滤显示的消息。这使用了stream和level关键字参数。
一个稍微复杂一些的配置可能看起来像这样:
logging.basicConfig(filename='app.log', filemode='a', level=logging.INFO)
我们已经打开了一个命名的文件,将其模式设置为a以追加,并将级别设置为显示严重性等于或大于INFO的消息。
由于每个单独的日志记录器都有名称,我们可以调整特定日志记录器的详细程度。我们可以包含以下类似行来在特定日志记录器上启用调试:
logging.getLogger('Demonstration').setLevel(logging.DEBUG)
这允许我们查看特定类或模块的详细信息。这在调试时通常非常有帮助。
logging.handlers模块提供了大量用于路由、打印或保存日志消息序列的处理程序。前面的示例显示了文件处理器。流处理器用于写入标准错误流。在某些情况下,我们需要有多个处理器。我们可以对每个处理器应用过滤器,这样处理器将反映不同类型的细节。
日志配置通常过于复杂,无法使用basicConfig()函数。logging.config模块提供了几个可用于配置应用程序日志的函数。一种通用方法是用logging.config.dictConfig()函数。我们可以在 Python 中直接创建 Python dict对象,或者读取dict对象的某些序列化版本。标准库文档使用 YAML 标记语言编写的示例,因为它简单且灵活。
我们可能像这样创建一个配置对象:
config = {
'version': 1,
'handlers': {
'console': {
'class' : 'logging.StreamHandler',
'stream': 'ext://sys.stderr',
}
},
'root': {
'level': 'DEBUG',
'handler': ['console'],
},
}
此对象具有所需的version属性,用于指定配置的结构。定义了一个单个处理器;它被命名为console,并使用logging.StreamHandler写入标准错误流。根日志记录器配置为使用console处理器。严重级别被定义为包括任何DEBUG级别或以上的消息。
只有在配置文件中,根日志记录器才被命名为'root'。在应用程序代码中,根日志记录器使用空字符串命名。
较大且更复杂的应用程序将依赖于外部配置文件中的日志配置。这允许灵活且复杂的日志配置。
使用 RST 标记编写文档
虽然 Python 代码应该是美观且富有信息的,但它并不容易提供背景或上下文来展示为什么选择特定的算法或数据结构。我们经常需要提供这些额外的细节来帮助人们维护、扩展并有效地使用我们的软件。虽然我们可以在模块文档字符串中包含大量信息,但似乎最好将文档字符串集中在实现细节上,并单独提供额外的材料。
我们可以用各种格式编写额外的文档。我们可以使用具有复杂文件格式的复杂编辑器,或者我们可以使用简单的文本编辑器和纯文本格式。我们甚至可以完全用 HTML 编写我们的文档。Python 还提供了一种混合方法——我们可以使用带有简化ReStructuredText(RST)标记的文本编辑器来编写,并使用docutils工具从该标记创建漂亮的 HTML 页面或适合出版的 LaTeX 文件。
RST 标记语言被广泛用于创建 Python 文档。这种标记允许我们编写纯文本,同时遵守一些格式规则。在下一节中,我们将探讨使用docutils工具解析 RST 并创建输出文档。
RST 标记的规则很简单。存在段落级别的标记,适用于大块文本。段落必须由空白行分隔。当一行被字符序列“下划”时,它被视为标题。当一个段落以一个独立的标点符号开头时,它是一个项目符号。当一个段落以字母或数字开头,后面跟着一个标点符号时,这表示数字而不是项目符号。docutils的rst2html.py工具将输入的每个段落转换为适当的 HTML 结构。
有许多段落级别的“指令”可以用来插入图像、表格、方程式或大块代码。这些指令以前缀..开头,以::结尾。我们可能使用指令.. contents::来将目录添加到我们的文档中。
我们可以在段落的主体内编写内联标记。内联标记包括一些简单的结构。如果我们用*字符包围一个单词,如*this*,我们将在最终文档中看到以斜体风格的字体显示的单词;我们可以使用**bold**来表示粗体字符。如果我们想在不混淆工具的情况下写入*字符,我们可以用\字符来转义它。然而,在许多情况下,我们需要使用更复杂的语义标记,如下所示::code:`code sample`。这包括文本角色:code:作为前缀,显示如何分类标记的字符;内容被`字符包围。:code:``和:math:`的文本角色被广泛使用。
当我们编写文档字符串时,我们通常会使用额外的 RST 标记。当我们定义函数或类方法的参数时,我们会使用:param name:。我们使用:returns:来注释函数的返回值。当我们提供这些额外的标记时,我们可以确保各种格式化工具能够从我们的文档字符串中生成优雅的文档。
下面是一个 RST 文件可能包含的示例:
Writing RST Documentation
==========================
For more information, see http://docutils.sourceforge.net/docs/user/rst/quickref.html
1\. Separate paragraphs with blank lines.
2\. Underline headings.
#. Prefix with one character for an unordered list. Otherwise it may be
interpreted as an ordered list.
#. Indent freely to show structure.
#. Inline markup.
- Use ``*word*`` for *italics*, and ``**word**`` for **bold**.
- Use ``:code:\`word\```以获取更复杂的语义标记。
```py
We've shown a heading, underlined with a sequence of `=` characters. We've provided a URL; in the final HTML output, this will become a proper link using the `<a>` tag. We've shown numbered paragraphs. When we omit the leading number and use `#`, the `docutils` tools will assign increasing numbers. We've also shown indented bullet point within the last numbered paragraph.
While this example shows numbering and simple hyphen bullets, we can use lettering or Roman numerals as well. The `docutils` tools are generally able to parse a wide variety of formatting conventions.
## Creating HTML documentation from an RST source
To create HTML or LaTeX (or any of the other supported formats), we'll use one of the `docutils` frontend tools. There are many individual conversion tools that are part of the `docutils` package.
The `docutils` tools are not part of Python. See [`docutils.sourceforge.net`](http://docutils.sourceforge.net) for the download.
All of the tools have a similar command-line interface. We might use the following command to create an HTML page from some RST input:
MacBookPro-SLott:Chapter_14 slott$ rst2html.py ch14_doc.rst ch14_doc.rst.html
我们提供了`rst2html.py`命令。我们已命名了输入文件和输出文件。这将使用默认的样式表值,并为生成的文档提供其他可选功能。我们可以通过命令行或提供配置文件来配置输出,以确保所有生成的 HTML 文件具有统一的样式。
要创建 LaTeX,我们可以使用`rst2latex.py`或`rst2xetex.py`工具,然后使用 LaTeX 格式化器。TeX Live 发行版非常适合从 LaTeX 创建 PDF 文件。请参阅[`www.tug.org/texlive/`](https://www.tug.org/texlive/)。
对于大型且复杂的文档,创建单个 RST 文件并不是最佳选择。虽然我们可以使用`.. include::`指令从单独的文件中插入内容,但文档必须作为一个整体来构建,这需要大量的内存;在文档进行小幅度修改后重新构建可能需要不成比例的处理量。
对于多页网站,我们必须使用像 Make、Ant 或 SCons 这样的工具来在源 RST 文件更新后重新构建相关的 HTML 页面。这种开销呼唤一个工具来自动化和简化大型或复杂文档的生产。
## 使用 Sphinx 工具
Sphinx 工具使我们能够轻松构建多页网站或复杂文档。有关更多信息,请参阅[`sphinx-doc.org`](http://sphinx-doc.org)。当我们使用`pip`或`easy_install`安装 Sphinx 时,安装程序也会为我们包括`docutils`。
要创建复杂的文档,我们将从`sphinx-quickstart`脚本开始。此应用程序将构建模板文件结构、配置文件以及一个 Makefile,我们可以使用它来高效地重新构建我们的文档。
Sphinx 在 RST 的基本指令和文本角色中添加了大量指令。这些额外的角色和指令使得编写关于代码的内容变得更加容易,可以正确地引用模块、类和函数。Sphinx 简化了文档间的引用——我们可以拥有多个文档,它们对目标位置的引用保持一致;我们可以移动目标,所有引用都将自动更新。
使用`sphinx-build`命令从 RST 源文件构建目标文件。Sphinx 可以构建十几种不同类型的目标文档,使其成为一个多功能的工具。
Python 文档是用 Sphinx 构建的。这意味着我们的项目可以包含看起来像 Python 文档一样光鲜和优雅的文档。
# 组织 Python 代码
Python 程序应该是美丽的。为此,语言有很少的语法开销;我们应该能够编写简短的脚本而不需要不愉快的模板代码。这个原则有时被阐述为“简单的事情应该简单”。“Hello World”脚本实际上是一行代码,它使用了`print()`函数。
一个更复杂的文件通常有几个主要部分:
+ 一行`!#`,通常是`#!/usr/bin/env python3`。
+ 一个注释文档字符串,解释模块的功能。
+ 函数或类的定义。我们通常将多个函数和类组合成一个模块。在 Python 中,模块是重用的适当单元。
+ 如果模块可以作为主脚本运行,我们将包括一个`if __name__ == "__main__":`部分,该部分定义了文件作为主脚本运行时的行为。
许多应用程序对于单个文件来说过于复杂。在设计较大的应用程序时,Python 的理想是尽可能保持结果结构尽可能扁平。虽然语言支持嵌套包,但深度嵌套并不被视为理想。在第十二章中,我们探讨了定义模块和包的细节。
# 摘要
在本章中,我们探讨了几个光鲜和完整的 Python 项目的特性。工作代码最重要的特性是一套单元测试,它证明了代码是有效的。没有测试用例的代码根本无法信任。为了使用任何软件,我们必须有显示软件是可信的测试。
我们已经探讨了在文档字符串中包含测试。`doctest`工具可以定位这些测试并执行它们。我们已经探讨了创建`unittest.TestCase`类。我们可以将两者结合到一个脚本中,该脚本将定位所有`doctest`和`unittest`测试用例到一个单独的主测试套件中。
软件的一个其他特点是关于如何安装和使用软件的解释。这可能只是一个提供基本信息的`README`文件。然而,通常我们需要一个更复杂的文档,它提供了各种附加信息。我们可能希望提供背景、设计背景或太大而无法打包到模块或类文档字符串中的示例。我们通常会使用超出 Python 基本组件的工具来编写文档。
在第十五章,“下一步”,我们将探讨 Python 探索的下一步。一旦我们掌握了基础知识,我们需要加深与我们需要解决的问题相关的领域的深度。我们可能想要研究大数据应用、Web 应用或游戏开发。这些更专业化的领域将涉及额外的 Python 概念、工具和框架。
# 第十五章。下一步
在学习 Python 基础之后,接下来是什么?每个开发者的旅程将因他们将要构建的应用程序的一般架构而异。在本章中,我们将探讨四种类型的 Python 应用程序。我们将深入探讨 **命令行界面**(**CLI**)应用程序。我们也会简要地看看 **图形用户界面**(**GUI**)应用程序。我们可以使用许多图形库和框架来完成这项工作;很难涵盖所有替代方案。
网络服务器应用程序通常涉及一个复杂的网络框架,该框架处理标准化的开销。我们的 Python 代码将连接到这个框架。与 GUI 应用程序一样,有几个常用的框架。我们将快速查看网络框架的一些常见功能。我们还将探讨以 Hadoop 服务器流式接口为代表的大数据环境。
这并不是要全面或具有代表性。Python 被以许多不同的方式使用。
# 利用标准库
在实现 Python 解决方案时,扫描标准库以查找相关模块是有帮助的。这个库很大,一开始可能会让人感到有些畏惧。然而,我们可以集中我们的搜索。
我们可以将 *Python 标准库* 文档分为三个部分。前五章是所有 Python 程序员都需要了解的一般参考材料。接下来的 20 章以及第二十八章和第三十二章描述了我们可能将其纳入各种应用程序的模块。剩下的章节不太有用;它们更多地关注 Python 的内部结构和扩展语言本身的方法。
库目录表中的模块名称和摘要可能不足以展示模块可能被使用的所有方式。例如,`bisect` 模块可以被扩展来创建一个快速字典,该字典保留其键的既定顺序。如果不仔细阅读模块的描述,这一点并不明显。
一些库模块具有相对较小、易于理解的实现。对于较大的模块和包,通常有一些可以从上下文中提取出来并广泛重用的部分。例如,考虑一个使用 `http.client` 来进行 REST 网络服务请求的应用程序。我们经常需要 `urllib.parse` 模块中的函数来编码查询字符串或正确引用 URL 的部分。在 Python 应用程序的前端通常可以看到一个长长的导入列表。
# 利用 PyPI – Python 包索引
在扫描库之后,寻找更多 Python 包的下一个地方是位于 [`pypi.python.org/pypi`](https://pypi.python.org/pypi) 的 **Python 包索引**(**PyPI**)。这里列出了数千个包,它们的支持和质量各不相同。
如我们在第一章中所述,*入门*,Python 3.4 还安装了两个脚本来帮助我们添加包,`pip`和`easy_install`。这些工具在 PyPI 上搜索请求的包。大多数包可以通过使用它们的名称找到;工具定位适合平台和 Python 版本的适当版本。
我们在其他章节中提到了一些外部库:
+ 使用`nose`编写测试,请参阅[`pypi.python.org/pypi/nose/1.3.6`](https://pypi.python.org/pypi/nose/1.3.6)
+ 使用`docutils`编写文档,请参阅[`pypi.python.org/pypi/docutils/0.12`](https://pypi.python.org/pypi/docutils/0.12)
+ 使用`Sphinx`编写复杂文档,请参阅[`pypi.python.org/pypi/Sphinx/1.3.1`](https://pypi.python.org/pypi/Sphinx/1.3.1)
此外,还有许多包集合可用:我们可能会安装 Anaconda、NumPy 或 SciPy,每个都包含一个整洁的分发中的多个其他包。请参阅[`continuum.io/downloads`](http://continuum.io/downloads)、[`www.numpy.org`](http://www.numpy.org)或[`www.scipy.org`](http://www.scipy.org)。
在某些情况下,我们可能有一些相互不兼容的 Python 配置。例如,我们可能需要在两个环境中工作,一个使用较旧的 Beautiful Soup 3,另一个使用较新的版本 4。请参阅[`pypi.python.org/pypi/beautifulsoup4/4.3.2`](https://pypi.python.org/pypi/beautifulsoup4/4.3.2)。为了简化这个切换,我们可以使用`virtualenv`工具创建具有自己复杂依赖模块树的隔离 Python 环境。请参阅[`virtualenv.pypa.io/en/latest/`](https://virtualenv.pypa.io/en/latest/)。
Python 生态系统庞大而复杂。没有好的理由在真空中发明解决方案。通常最好找到适当的组件或部分解决方案,然后下载并扩展它们。
# 应用程序类型
我们将探讨四种类型的 Python 应用程序。这些既不是最常见的也不是最受欢迎的 Python 应用程序类型;它们是根据作者的有限经验随机选择的。Python 被广泛使用,任何试图总结 Python 被使用的各种地方的努力都有误导而不是提供信息的风险。
我们将探讨 CLI 应用程序的两个原因。首先,它们可能相对简单,比其他类型的应用程序依赖更少的额外包或框架。其次,更复杂的应用程序通常从 CLI 主脚本启动。出于这些原因,CLI 功能似乎对 Python 的大多数使用都是基本的。
我们将探讨 GUI 应用程序,因为它们在桌面电脑上很受欢迎。这里的困难在于,Python 软件开发中有许多可用的 GUI 框架。以下是一个列表:[`wiki.python.org/moin/GuiProgramming`](https://wiki.python.org/moin/GuiProgramming)。我们将重点关注`turtle`包,因为它简单且内置。
我们将探讨 Web 应用程序,因为 Python 与 Django 或 Flask(以及其他许多框架)一起用于构建高流量网站。以下是一个 Python Web 框架列表:[`wiki.python.org/moin/WebFrameworks`](https://wiki.python.org/moin/WebFrameworks)。我们将重点关注 Flask,因为它相对简单。
我们还将探讨如何使用 Python 与 Hadoop 流进行数据分析。我们不会下载和安装 Apache Hadoop,而是简要介绍如何在我们的桌面上构建和测试管道映射-归约处理。
# 构建命令行应用程序
从第一章的初始脚本示例“入门”中,我们的重点是使用 CLI 脚本学习 Python 基础知识。CLI 应用程序具有许多共同特性:
+ 它们通常从标准输入文件读取,写入标准输出文件,并在标准错误文件中产生日志或错误。操作系统保证这些文件始终可用。Python 通过`sys.stdin`、`sys.stdout`和`sys.stderr`提供它们。此外,`input()`和`print()`等函数默认使用这些文件。
+ 它们通常使用环境变量进行配置。这些值通过`os.environ`可用。
+ 它们也可能依赖于 shell 功能,如将`~`扩展为用户的家目录,这是由`os.path.expanduser()`完成的。
+ 它们通常解析命令行参数。虽然变量`sys.argv`包含参数字符串,但直接使用它们很麻烦。我们将使用`argparse`模块来定义参数模式,解析字符串,并创建一个包含相关参数值的对象。
这些基本功能涵盖了多种编程选择。例如,一个网络服务器可以被视为一个永远运行的 CLI 程序,它从特定的端口号接收请求。一个 GUI 应用程序可能从命令行开始,但随后打开窗口以允许用户交互。
## 使用 argparse 获取命令行参数
我们将使用`argparse`模块创建一个解析器来使用命令行参数。一旦配置完成,我们可以使用这个解析器创建一个小的命名空间对象,该对象包含在命令行上提供的所有参数值,或者包含默认值。我们的应用程序可以使用这个对象来控制其行为。
通常,我们希望将命令行处理与我们的应用程序的其他部分隔离开。以下是一个处理解析的函数,然后使用解析的选项调用另一个函数来完成实际工作:
```py
logger= logging.getLogger(__name__)
def main():
parser= argparse.ArgumentParser()
parser.add_argument("-v", "--verbose",
action="store_const", const=logging.DEBUG, default=logging.INFO)
parser.add_argument("c", type=float)
options= parser.parse_args()
logging.getLogger().setLevel(options.verbose)
logger.debug("Converting '{0!r}'".format(options.c))
convert(options.c)
我们使用所有默认参数构建了一个ArgumentParser方法。我们本可以识别程序名称,提供使用说明,或者当有人使用-h选项获取帮助时显示任何其他内容。我们省略了这些额外的文档,以保持示例简洁。
我们为这个应用程序定义了两个参数:一个可选参数和一个位置参数。可选参数-v或--verbose在结果选项集合中存储一个常量值。这个属性的名称是参数的长名称,即verbose。提供的常量是logging.DEBUG;如果选项不存在,则默认值为logging.INFO。
位置参数c在解析完所有选项之后接受一个命令行参数。nargs的值可以省略;它可以设置为'*'以收集所有参数。我们提供了一个要求,即输入值通过float()函数转换,这意味着在参数解析期间将拒绝非数值值并显示错误。这将设置为结果对象的c属性。
当我们评估parse_args()方法时,定义的参数用于解析sys.argv中的命令行值。options对象将具有结果值或默认值。
在main()函数的第二部分,我们使用options对象通过verbose参数值设置根日志记录器的日志级别。然后我们使用全局logger对象将单个位置参数值输出,该值将被分配给options对象的c属性。
最后,我们使用输入参数值评估我们的应用程序函数;解析器将此分配给options.c变量。执行实际工作的函数设计为完全独立于用于调用它的命令行界面。该函数接受一个浮点值并将结果打印到标准输出。它可以利用模块全局logger对象。
我们设计命令行应用程序的目标是将有用的工作与所有界面考虑完全分离。这允许我们导入执行实际工作的函数,并从单个组件构建更大或更复杂的应用程序。这通常意味着命令行参数被转换为普通函数参数或类构造函数参数。
使用 cmd 模块进行交互式应用程序
一些命令行应用程序需要用户交互。例如,sftp命令可以从命令行使用,以与服务器交换文件。我们可以使用 Python 的cmd模块创建类似交互式应用程序。
要构建更复杂的交互式应用程序,我们可以创建一个扩展cmd.Cmd类的类。这个类中任何以do_开头命名的函数定义了一个交互式命令。例如,如果我们定义了一个方法do_get(),这意味着我们的应用程序现在有一个交互式的get命令。
用户输入get命令后的任何后续文本都将作为参数提供给do_get()方法。然后do_get()函数负责对命令之后的文本进行进一步解析和处理。
我们可以创建此类的一个实例,并调用继承的 cmdloop() 方法来拥有一个工作着的交互式应用程序。这使我们能够非常快速和简单地部署一个工作着的交互式应用程序。虽然我们受限于字符模式、命令行界面,但我们可以轻松地添加功能而无需做太多额外的工作。
构建 GUI 应用程序
我们可以区分仅与图形工作以及深度交互的应用程序。在前一种情况下,我们可能有一个命令行应用程序,它创建或修改图像文件。在第二种情况下,我们将定义一个响应输入事件的程序。这些交互式应用程序创建一个事件循环,它接受鼠标点击、屏幕手势、键盘字符和其他事件,并对这些事件做出响应。在某种程度上,GUI 程序的唯一独特特性是它响应事件的广泛多样性。
tkinter 模块是 Python 和 Tk 用户界面小部件工具包之间的接口。此模块帮助我们构建丰富的交互式应用程序。当我们使用 Python 内置的 IDLE 编辑器时,我们正在使用一个用 tkinter 构建的应用程序。tkinter 模块文档包括关于 Tk 小部件的背景信息。
turtle 模块还依赖于底层的 Tk 图形。此模块还允许我们构建简单的交互式应用程序。turtle 的想法来自 Logo 编程语言,其中图形命令用于使一个turtle在绘图空间中移动。turtle 模型为某些类型的图形提供了一种非常方便的规范。例如,绘制一个旋转的矩形可能涉及到一个相当复杂的计算,包括正弦和余弦来确定四个角最终的位置。或者,我们可以指导 turtle 使用诸如 forward(w)、forward(l) 和 right(90) 等命令,从任何起始位置和任何初始旋转绘制大小为 w × l 的矩形。
为了让学习 Python 更容易,turtle 模块提供了一些基本的类,这些类实现了 Screen 和 Turtle。该模块还包括一个丰富的函数集合,这些函数隐式地与单例 Turtle 和 Screen 对象一起工作,消除了设置图形环境的需求。对于初学者来说,这个仅提供函数的环境是一种简单的动词语言,可以用来学习编程的基础。
简单的程序看起来像这样:
from turtle import *
def on_screen():
x, y = pos()
w, h = screensize()
return -w <= x < w and -h <= y < h
def spiral(angle, incr, size=10):
while on_screen():
right(angle)
forward(size)
size *= incr
我们使用 from turtle import * 来引入所有单个函数。这是初学者的常见设置。
我们定义了一个函数,on_screen(),它将 pos() 函数给出的 turtle 位置与 screensize() 函数给出的屏幕整体大小进行比较。我们的函数使用一个简单的逻辑表达式来确定当前 turtle 位置是否仍然在显示边界内。
对于学习编程的人来说,pos()和screensize()函数的实现细节可能并不那么有帮助。更高级的程序员可能想知道pos()函数使用单例全局Turtle实例的Turtle.pos()方法。同样,screensize()函数使用单例全局Screen实例的Screen.screensize()方法。
spiral()函数将使用定义螺旋线段的三参数来绘制螺旋形状。这个函数依赖于turtle包中的right()和forward()函数来设置海龟的方向并绘制线段。虽然forward()函数绘制的线段端点的计算可能涉及一点三角学,但新程序员能够学习迭代的基本知识,而无需与正弦或余弦函数纠缠。
这是我们如何使用这个函数的方法:
if __name__ == "__main__":
speed(10)
spiral(size=10, incr=1.05, angle = 67)
done()
作为初始化的一部分,我们将海龟的速度设置为 10,这相当快。对于在循环或条件语句上遇到困难的人来说,较慢的速度可以帮助他们在观察海龟时更好地跟随代码。我们已经使用一组参数值评估了spiral()函数。
done()函数将启动一个 GUI 事件处理循环,等待用户交互。我们在绘制有趣的部分之后启动了循环,因为唯一预期的事件是图形窗口的关闭。当用户关闭窗口时,done()函数也会结束。然后我们的脚本可以正常结束。
如果我们要构建更复杂的交互式应用程序,有一个合适的mainloop()函数可以使用。这个函数可以捕获事件,使我们的程序能够对这些事件做出响应。
Logo 语言及其相关的turtle包允许初学者在不必须一次掌握太多细节的情况下学习编程的基本知识。turtle包并不是为了产生与matplotlib或Pillow等包相同类型的复杂技术图形。
使用更复杂的包
我们可以使用 Pillow 库创建复杂的图像处理应用程序。这个包允许我们创建大图像的缩略图,转换图像格式,并验证文件实际上是否包含编码的图像数据。我们还可以使用这个包创建简单的科学图形,显示数据点的二维图。这个包并不是为了构建完整的 GUI,因为它不会为我们处理输入事件。更多信息,请参阅pypi.python.org/pypi/Pillow/2.8.1。
对于数学、科学和统计工作,matplotlib 包被广泛使用。这个包包括创建二维和三维基本数据图的非常复杂工具。这个包与 SciPy 和 Anaconda 捆绑在一起。更多信息,请参阅matplotlib.org。
有几个更通用的图形框架。其中一个常用于学习 Python 的是Pygame框架。它包含大量组件,包括图形、声音和图像处理工具。Pygame 包包括多个图形驱动程序,能够以平滑的方式处理大量移动对象。请参阅www.pygame.org/news.html。
构建 Web 应用程序
Web 应用程序涉及大量的处理,这最好描述为样板代码。例如,HTTP 协议的基本处理通常是标准化的,有库可以优雅地处理它。解析请求头和将 URL 路径映射到特定资源的细节不需要重新发明。
然而,简单地处理 HTTP 协议和将 URL 映射到特定应用程序资源之间存在深刻的区别。这两个层次推动了Web 服务网关接口(WSGI)设计和wsgi模块在标准库中的定义。有关更多信息,请参阅Python 增强提案(PEP)3333,www.python.org/dev/peps/pep-3333/。
WSGI 背后的想法是所有 Web 服务都应该遵守处理 HTTP 请求和响应细节的单个、最小标准。这个标准允许复杂的 Web 服务器包含各种 Python 工具和框架,这些工具和框架通过 WSGI 组合在一起,以确保组件正确互联。URL 到资源的映射必须在标准上下文中处理。
可以将mod_wsgi模块插入到 Apache HTTPD 服务器中。此模块将在 Apache 前端和后端 Python 实例之间传递请求和响应。通过一点规划,我们可以确保静态内容(如图形、样式表、JavaScript 库等)由前端 Web 服务器处理。动态内容(如 HTML 页面、XML 或 JSON 文档)则由我们的 Python 应用程序处理。
有关mod_wsgi的更多信息,请参阅www.modwsgi.org/。
使用 Web 框架
在这个背景下,Web 应用程序通常使用一个解析 URL 并调用 Python 函数以返回由 URL 定位的资源框架来构建。虽然这显然是创建 Web 服务器所需的最小要求,但通常还有大量我们希望拥有的附加功能。
例如,身份验证和授权是我们经常需要且希望不需要实现的功能。与一个允许我们添加 OAuth 客户端代码的框架一起工作会更好。使用 cookie 的网站也将从具有无缝集成的会话管理功能中受益。
许多网站提供 RESTful 网络服务。有时这些服务是数据库访问的薄包装。当数据库是关系型时,我们通常需要一个 对象关系映射器(ORM)层,它允许我们通过 RESTful 服务暴露更完整的对象。这也是一个良好的网络服务器框架选项。
在 Python 中提供网络服务有两种主要方法:套件和组件。套件方法以 Django 等包为代表,这些包提供了一个统一集合中的几乎所有可能需要的模块和包。请参阅 www.djangoproject.com。
组件方法可以在 Flask 等项目中看到。这被称为 微框架,因为它相对较少。Flask 服务器专注于 URL 路由,使其非常适合构建 RESTful 服务。它可能包括会话管理,使其可用于 HTML 网站。它与 Jinja2、WTForms、SQLAlchemy、OAuth 认证模块和其他许多模块很好地协作。有关更多信息,请参阅 flask.pocoo.org/docs/0.10/。
使用 Flask 构建 RESTful 网络服务
我们将演示一个非常简单的网络服务。我们将使用之前在 turtle 示例中展示的算法,进行一些小的修改,以创建动态图形下载。为了更容易创建可下载的文件,我们将放弃简单的 turtle 图形包,并使用 Pillow 包来创建图像文件。许多网站使用 Pillow 来验证上传的图像并创建缩略图。它是任何使用图像的网站的必要组成部分。
关于 Pillow 的更多信息,请参阅 pypi.python.org/pypi/Pillow/2.8.1。
网络服务必须对 HTTP 请求提供资源。一个简单的 Flask 网站将有一个整体的应用程序对象和多个路由,这些路由将 URL(以及可能的方法名称)映射到函数。
这里有一个简单的例子:
from flask import Flask, request
from PIL import Image, ImageDraw, ImageColor
import tempfile
spiral_app = Flask(__name__)
@spiral_app.route('/image/<spec>', methods=('GET',))
def image(spec):
spec_uq= urllib.parse.unquote_plus(spec)
spec_dict = urllib.parse.parse_qs(spec_uq)
spiral_app.logger.info( 'image spec {0!r}'.format(spec_dict) )
try:
angle= float(spec_dict['angle'][0])
incr= float(spec_dict['incr'][0])
size= int(spec_dict['size'][0])
except Exception as e:
return make_response('URL {0} is invalid'.format(spec), 403)
# Working dir should be under Apache Home.
_, temp_name = tempfile.mkstemp('.png')
im = Image.new('RGB', (400, 300), color=ImageColor.getrgb('white'))
pen= Pen(im)
spiral(pen, angle=angle, incr=incr, size=size)
im.save(temp_name, format='png')
# Should redirect so that Apache serves the image.
spiral_app.logger.debug( 'image file {0!r}'.format(temp_name) )
with open(temp_name, 'rb' ) as image_file:
data = image_file.read()
return (data, 200, {'Content-Type':'image/png'})
此示例展示了 Flask 应用程序的核心三个特性。此脚本定义了一个 Flask 实例。我们基于文件名定义了该实例,对于主脚本,它将是 "__main__",而对于导入的脚本,它将是模块名。我们将该 Flask 容器分配给一个变量 spiral_app,以便在整个模块文件中使用。
一个更复杂的 Flask 应用程序可能在一个子模块包中包含多个单独的视图函数。这些中的每一个都将依赖于全局 Flask 应用程序。
我们通过image()函数创建图像资源。为此函数提供了一个route装饰器,它显示了 URL 路径以及与此资源一起工作的方法。为 HTTP 协议定义了大量的方法。许多 RESTful 网络服务专注于 POST、GET、PUT 和 DELETE,因为这些与常用的创建、检索、更新和删除(CRUD)规则相匹配,这些规则通常用于总结数据库操作。
我们将image()函数分解为四个独立的部分。首先,我们需要解析 URL。route包括一个占位符<spec>,Flask 会解析并提供给函数作为参数。这将是一个用于描述螺旋的 URL 编码参数。它可能看起来像这样:
http://127.0.0.1:5000/image/size=10&angle=65.0&incr=1.05
一旦我们解码了规范,我们将有一个特殊的多元值字典。这看起来像是来自 HTML 表单的输入。结构将是表单字段名称到每个字段值的列表的映射。对象看起来像这样:
{'size': ['10'], 'angle': ['65.0'], 'incr': ['1.05']}
image()函数只使用每个项目中的一个值;每个输入都必须转换为数值。我们将所有潜在的异常收集到一个单独的except子句中,从而掩盖了任何错误输入的细节。我们使用 Flask 的make_response()函数构建一个包含错误消息和状态码 403(“禁止”)的响应。一个更复杂的函数会使用Accept头根据客户端声明的偏好将响应格式化为 JSON 或 XML。我们将其保留为默认的 MIME 类型 text/plain。
图像被保存到一个临时文件中,该文件是用tempfile.mkstemp()函数创建的。在这种情况下,我们将从 Flask 应用程序中保存那个临时文件。对于低流量网站,这是可以接受的。对于高流量网站,Python 应用程序永远不应该处理下载。文件应该创建在 Apache HTTPD 服务器可以下载图像的目录中,而不是 Python 应用程序。
图像构建使用了一些 Pillow 定义的对象来定义绘图空间。一个定制的类定义了一个Pen实例,它与turtle.Turtle类平行。一旦图像构建完成,它就会使用给定的文件名保存。请注意,Pillow 包可以以多种格式保存文件;在这个例子中我们使用了.png格式。
最后一个部分下载文件。注释指出,高流量网站会重定向到一个 URL,Apache 会从该 URL 下载图像文件。这使 Flask 服务器能够处理另一个请求。
注意,在这个函数的本地命名空间中会有两个图像副本。im变量将保存整个详细的图像。data变量将保存图像文档的压缩文件系统版本。我们可以使用del im来删除图像对象;然而,通常将此分解为两个函数更好,这样命名空间会为我们处理对象删除。
我们可以使用以下脚本运行此服务的演示版本:
if __name__ == '__main__':
spiral_app.run(debug=True)
这允许我们在桌面上使用一个运行中的 Web 服务器。然后我们可以尝试不同的实现方案。
这个例子的重要之处在于我们可以——非常快速地——在我们的桌面环境中运行一个服务。然后我们可以轻松地探索和实验用户体验。例如,由于图像将嵌入到 HTML 页面中,我们希望为该页面设计和调试 HTML、CSS 和 JavaScript。当我们有一个简单、易于调整的 Web 服务器时,整个开发过程会变得更加容易。
连接到 MapReduce 框架
关于 Apache Hadoop 服务器的背景信息,请参阅hadoop.apache.org。以下是摘要:
Apache Hadoop 软件库是一个框架,它允许使用简单的编程模型在计算机集群上分布式处理大型数据集。它旨在从单个服务器扩展到数千台机器,每台机器都提供本地计算和存储。
Hadoop 分布式处理的一部分是 MapReduce 模块。此模块允许我们将数据分析分解为两个互补的操作:映射和缩减。这些操作在 Hadoop 集群中分布,以并发运行。映射操作处理集群中分散的数据集的所有行。然后,映射操作的输出被输送到缩减操作以进行汇总。
Python 程序员可以使用 Hadoop 流接口。这涉及到一个 Hadoop“包装器”,它将数据作为标准输入文件呈现给 Python 映射程序。映射程序的标准输出必须是制表符分隔的键值对。这些被发送到缩减程序,再次作为标准输入。有关帮助 Python 程序员使用 Hadoop 的包的更多信息,请参阅blog.cloudera.com/blog/2013/01/a-guide-to-python-frameworks-for-hadoop/。
MapReduce 操作的一个常见示例是创建书中找到的单词的索引。映射操作将巨型文本文件转换为文本文件中找到的单词序列。缩减操作将计算每个单词的出现次数,从而得出单词及其流行度的最终汇总。(有关其重要性的更多信息,请访问 NLTK 网站:www.nltk.org。)
实际问题可能涉及多个映射和多个缩减。在许多情况下,映射似乎很简单:它们会从源数据中的每一行提取一个键和一个值。我们不会过多地研究 Hadoop,而是展示如何在我们的桌面上编写和测试映射器和缩减器。
我们的目标是拥有两个程序,map.py和reduce.py,它们可以组合成如下流:
cat some_file.dat | python3 map.py | sort | python3 reduce.py
这种方法将通过向我们的map.py程序和reduce.py程序提供数据来模拟 Hadoop 流。这将成为我们映射和减少处理的一个简单集成测试。对于 Windows,我们将使用type命令而不是 Linux 的cat程序。
让我们看看美国国家海洋和大气管理局国家气候数据中心的一些原始气候数据。有关在线气候数据,请参阅www.ncdc.noaa.gov/cdo-web/。我们可以请求包含特定时间段降雪等详细信息的文件。
我们的问题是“哪几个月在弗吉尼亚州里士满机场有降雪?”降雪数据属性名为TSNW。它的单位是 1/10 英寸,因此我们的映射器需要将其转换为Decimal英寸,以便更实用。
我们可以编写一个看起来像这样的映射脚本:
import csv
import sys
import datetime
from decimal import Decimal
if __name__ == "__main__":
rdr = csv.DictReader(sys.stdin)
wtr = csv.writer(sys.stdout, delimiter='\t', lineterminator='\n')
for row in rdr:
date = datetime.datetime.strptime(row['DATE'], "%Y%m%d").date()
if row['TSNW'] in ('0', '-9999', '9999'):
continue # Zero or equipment error: reject
wtr.writerow( [date.month, Decimal(row['TSNW'])/10] )
由于我们的输入在大约标准 CSV 表示法中——带有标题——我们可以使用csv.DictReader对象来解析输入。每一行数据都是一个dict对象,其键由 CSV 文件的第一行定义。输出更加专业化:在 Hadoop 中,它必须是一个制表符分隔的关键字和值,以换行符结束。
对于每个输入字典对象,我们将日期从文本转换为适当的 Python 日期,以便我们可以可靠地提取月份。我们可以通过使用row['DATE'][4:6]来完成此操作,但这似乎很模糊。映射器包括一个过滤器,以拒绝没有降雪的月份,或者有特定领域的空值(9999 或-9999)而不是测量值。
输出是一个键和一个值。我们的键是报告的月份;值是将十分之一英寸的降雪转换为英寸测量值。我们使用了Decimal类来避免引入浮点近似。
减少操作使用Counter对象来总结映射器产生的结果。对于这个例子,减少操作看起来像这样:
import csv
import sys
from collections import Counter
from decimal import Decimal
if __name__ == "__main__":
rdr= csv.DictReader(
sys.stdin, fieldnames=("month","snowfall"),
delimiter='\t', lineterminator='\n')
counts = Counter()
for line in rdr:
counts[line['month']] += Decimal(line['snowfall'])
print( counts )
减少读取器与映射器的写入器相匹配:它们都使用制表符作为分隔符,使用换行符作为行终止符。这遵循了 Hadoop 对从映射器到减少器的数据的要求。我们还创建了一个Counter对象来存储我们的降雪数据。
对于每一行输入,我们提取降雪英寸数,并将这些数累加到以月份数字为键的Counter对象中。最终结果将显示大里士满都会区每个月的降雪英寸数。
我们可以很容易地在我们的桌面上测试和实验这个。我们可以使用 shell 脚本或可能是一个像这样的小包装程序来执行映射器、排序和减少器的管道:
import subprocess
dataset = "526212.csv"
command = """cat {dataset} | python3 -m Chapter_15.map | sort |
python3 -m Chapter_15.reduce"""
command = command.format_map(locals())
result= subprocess.check_output(command, shell=True)
for line in result.splitlines():
print( line.decode("ASCII") )
我们创建了一个在 Mac OS X 或 Linux 上工作的命令,并将文件名替换到该命令中。对于 Windows,我们可以使用type而不是cat;Python 程序可能被命名为python而不是python3。否则,shell 管道应该在 Windows 上正常工作。
我们使用了 subprocess.check_output() 函数来运行这个 shell 命令并收集输出。这是一种快速实验我们的 Hadoop 程序的方法,同时避免了使用繁忙的 Hadoop 集群相关的延迟。
只要我们坚持使用在 Hadoop 环境中正确安装的库元素,这种方法就能很好地工作。在某些情况下,我们的集群可能已经安装了 Anaconda,这使我们能够访问各种包。当我们想使用自己的包——一个在整个集群中未安装的包时,我们需要将额外的模块提供给 Hadoop 流命令,以确保我们的额外模块被下载到集群中的每个节点,包括我们的映射器和归约器。
摘要
在本章中,我们探讨了多种 Python 应用程序。虽然 Python 被广泛使用,但我们选择了一些重点关注的领域。我们研究了能够处理大量数据的 CLI 应用程序。命令行界面也存在于其他类型的应用程序中,这使得它成为任何程序的基本部分。
我们还研究了使用内置的 turtle 模块进行 GUI 程序。广泛使用的 GUI 框架涉及下载、安装和更复杂的编程,我们无法在一个章节中展示。有几个流行的选择;对于 GUI 应用程序来说,没有关于“最佳”包的共识。做出选择是困难的。
我们还研究了使用 Flask 模块的网络应用程序。这也是一个单独的下载。在许多情况下,有许多相关的下载将成为网络应用程序的一部分。我们可能包括 Jinja2、WTForms、OAuth、SQLAlchemy 和 Pillow,以扩展网络服务器的库。
我们还研究了如何利用桌面 Python 来开发 Hadoop 应用程序。我们不必下载和安装 Hadoop,可以创建一个遵循 Hadoop 方法的处理管道。我们可以仅使用桌面工具编写映射器和归约器,这样我们就可以创建可靠的单元测试。这使我们有了信心,当我们使用完整的数据集在 Hadoop 集群上运行应用程序时,我们会得到预期的结果。
当然,这还不是全部。Python 可以作为自动化其他应用程序的语言在另一个应用程序中使用。一个程序可以嵌入一个 Python 解释器,该解释器与整体应用程序交互。有关更多信息,请参阅 docs.python.org/2/extending/embedding.html。
我们可以将 Python 应用程序的世界想象成一个充满岛屿、群岛、湾口和河口的大水体。美国东海岸的切萨皮克湾就是一个例子。我们试图展示这个湾的主要特征:海角、尖端、浅滩和海岸线。我们避开了洋流、天气和潮汐的影响,以便我们可以专注于湾的必要特征。沿着特定路线进行实用导航需要更深入地研究感兴趣的区域:详细的航海图、飞行员指南以及来自其他船员的当地知识。
考虑 Python 世界的广度是很重要的。到达目的地的距离可能会显得令人畏惧。我们的目标一直是展示一些主要航标,这些航标可以帮助将漫长的航行分解成更短的航程。如果我们孤立出漫长旅程的各个部分,我们就可以分别解决它们,并从这些部分构建出一个更大的解决方案。


的值。
不同的值。
。表达式
所示的递增数字序列。这产生(y-x)/z个值。
浙公网安备 33010602011771号