Python-简明之书第三版-全-
Python 简明之书第三版(全)
原文:The Quick Python Book 3e
译者:飞龙
第一部分. 入门
这前三章会向你简要介绍 Python,它的优点和缺点,以及为什么你应该考虑学习 Python 3。在第二章中,你可以了解到如何在 Windows、macOS 和 Linux 平台上安装 Python,以及如何编写一个简单的程序。第三章是对 Python 的语法和功能的一个快速、高级的概述。
如果你正在寻找对 Python 的最快入门介绍,从第三章开始。
第一章. 关于 Python
本章涵盖
-
为什么使用 Python?
-
Python 擅长什么
-
Python 不擅长什么
-
为什么学习 Python 3?
如果你想知道 Python 与其他语言相比如何,以及它在整个体系结构中的位置,请阅读本章。如果你想立即开始学习 Python,请跳过——直接跳到第三章。本章的信息是本书的有效部分——但它当然不是用 Python 编程所必需的。
1.1. 为什么我应该使用 Python?
今天有数百种编程语言可供选择,从成熟的 C 和 C++语言,到较新的 Ruby、C#和 Lua 语言,再到企业巨头 Java。选择一种语言来学习是困难的。虽然没有一种语言适合所有可能的情况,但我认为 Python 是解决大量编程问题的好选择,如果你正在学习编程,它也是一个不错的选择。全世界有成千上万的程序员使用 Python,而且这个数字每年都在增长。
Python 由于各种原因继续吸引新用户。它是一种真正的跨平台语言,在 Windows、Linux/UNIX、Macintosh 平台以及其他平台上运行得同样好,从超级计算机到手机。它可以用来开发小型应用程序和快速原型,但它具有良好的扩展性,可以开发大型程序。它附带了一个强大且易于使用的图形用户界面(GUI)工具包、网络编程库等。而且它是免费的。
1.2. Python 擅长什么
Python 是一种由 Guido van Rossum 在 20 世纪 90 年代(并以一个著名的喜剧团体命名)开发的现代编程语言。尽管 Python 不是每个应用的完美选择,但其优势使其在许多情况下成为不错的选择。
1.2.1. Python 易于使用
熟悉传统语言的程序员会发现学习 Python 很容易。所有熟悉的构造——循环、条件语句、数组等等——都包括在内,但许多在 Python 中更容易使用。以下是几个原因:
-
类型与对象相关联,而不是变量。 一个变量可以分配任何类型的值,一个列表可以包含多种类型的对象。这也意味着通常不需要类型转换,并且你的代码不会被预先声明的类型的紧身衣所束缚。
-
Python 通常在更高的抽象级别上运行。 这部分是语言构建方式的结果,部分是 Python 发行版附带的大量标准代码库的结果。一个下载网页的程序可以用两到三行代码完成!
-
语法规则非常简单。 虽然成为一名专家 Pythonista 需要时间和努力,但即使是初学者也能快速吸收足够的 Python 语法来编写有用的代码。
Python 非常适合快速应用开发。用 Python 编写应用程序通常只需要 C 或 Java 的五分之一的时间,并且可能只需要相当于 C 程序五分之一的行数。这当然取决于特定的应用程序;对于一个在for循环中主要执行整数运算的数值算法,生产率提升可能不会很大。对于普通应用程序,生产率提升可能是显著的。
1.2.2. Python 具有表达性
Python 是一种非常具有表达性的语言。在这个上下文中,“表达性”意味着 Python 代码的一行可以完成大多数其他语言一行代码的工作。更具有表达性的语言的优点是显而易见的:你写的代码行数越少,你完成项目就越快。代码行数越少,程序就越容易维护和调试。
要了解 Python 的表达能力如何简化代码,可以考虑交换两个变量var1和var2的值。在像 Java 这样的语言中,这需要三行代码和一个额外的变量:
int temp = var1;
var1 = var2;
var2 = temp;
变量temp用于在将var2放入其中时保存var1的值,然后那个保存的值被放入var2。这个过程并不特别复杂,但阅读这三行并理解发生了交换需要一定的开销,即使是经验丰富的程序员也是如此。
相比之下,Python 允许你在一行内完成相同的交换,并且以一种使值交换发生明显的方式:
var2, var1 = var1, var2
当然,这是一个非常简单的例子,但你可以在整个语言中找到相同的优势。
1.2.3. Python 易于阅读
Python 的另一个优点是它易于阅读。你可能认为编程语言只需要被计算机阅读,但人类也需要阅读你的代码:无论是谁调试你的代码(很可能是你),无论是谁维护你的代码(可能是你再次),还是谁可能希望在将来修改你的代码。在所有这些情况下,代码越容易阅读和理解,就越好。
代码越容易理解,就越容易调试、维护和修改。Python 在这一方面的主要优势在于其使用缩进。与大多数语言不同,Python坚持代码块需要缩进。尽管这可能会让一些人觉得奇怪,但它有一个好处,那就是你的代码总是以非常易于阅读的格式呈现。
下面是两个简短的程序,一个用 Perl 编写,一个用 Python 编写。它们都接受两个大小相等的数字列表,并返回这些列表的成对和。我认为 Python 代码比 Perl 代码更易读;它看起来更整洁,包含的难以理解的符号更少:
# Perl version.
sub pairwise_sum {
my($arg1, $arg2) = @_;
my @result;
for(0 .. $#$arg1) {
push(@result, $arg1->[$_] + $arg2->[$_]);
}
return(\@result);
}
# Python version.
def pairwise_sum(list1, list2):
result = []
for i in range(len(list1)):
result.append(list1[i] + list2[i])
return result
这两段代码做的是同一件事,但在可读性方面 Python 代码胜出。(当然,在 Perl 中还有其他方法来做这件事,其中一些比展示的更简洁——但在我看来更难阅读。)
1.2.4. Python 是完整的——“内置电池”
Python 的另一个优点是其关于库的“内置电池”哲学。其理念是,当你安装 Python 时,你应该拥有完成实际工作所需的一切,而无需安装额外的库。这就是为什么 Python 标准库包含了处理电子邮件、网页、数据库、操作系统调用、GUI 开发等模块。
例如,使用 Python,你只需两行代码就可以编写一个网络服务器来共享目录中的文件:
import http.server
http.server.test(HandlerClass=http.server.SimpleHTTPRequestHandler)
没有必要安装库来处理网络连接和 HTTP;它已经包含在 Python 中,直接从盒子里出来。
1.2.5. Python 是跨平台的
Python 也是一个出色的跨平台语言。Python 可以在许多平台上运行:Windows、Mac、Linux、UNIX 等等。因为它被解释,相同的代码可以在任何有 Python 解释器的平台上运行,而几乎所有当前的平台都有。甚至还有运行在 Java(Jython)和.NET(IronPython)上的 Python 版本,这为你提供了更多可能的运行 Python 的平台。
1.2.6. Python 是免费的
Python 也是免费的。Python 最初,并且继续在开源模型下开发,并且是免费可用的。你可以下载并安装几乎任何版本的 Python,并使用它来开发商业或个人应用程序,而且你不需要支付一分钱。
尽管态度正在改变,但有些人仍然对免费软件持谨慎态度,因为他们担心缺乏支持,害怕自己没有付费客户的分量。然而,Python 被许多知名公司作为其业务的关键部分使用;谷歌、Rackspace、工业光魔和霍尼韦尔只是其中的一些例子。这些公司和许多其他公司都清楚 Python 的价值:这是一个非常稳定、可靠且得到良好支持的软件产品,拥有活跃且知识渊博的用户社区。你会在 Python 互联网新闻组上比在大多数技术支持电话线更快地得到甚至最复杂的 Python 问题的答案,而且 Python 的答案将是免费且正确的。
Python 与开源软件
不仅 Python 免费,而且其源代码也是免费可用的,如果你愿意,你可以自由地修改、改进和扩展它。因为源代码是免费可用的,你有能力亲自去修改它(或者雇佣某人帮你修改)。在专有软件中,你很少以合理的成本获得这种选择。
如果这是您第一次进入开源软件的世界,您应该了解,您不仅可以使用和修改 Python,而且能够(并被鼓励)为其做出贡献并改进它。根据您的环境、兴趣和技能,这些贡献可能是财务上的,例如向 Python 软件基金会(PSF)捐款,或者参与一个特别兴趣小组(SIG),测试并反馈 Python 核心或辅助模块的发布,或者将您或您的公司开发的一些内容贡献回社区。当然,贡献的水平(如果有)取决于您;但如果您能够回馈,请务必考虑这样做。这里正在创造一些有价值的东西,您有机会为其增添价值。
Python 有很多优点:表达性、可读性、丰富的内置库和跨平台能力。此外,它是开源的。有什么问题吗?
1.3. Python 不擅长的方面
虽然 Python 有很多优点,但没有一种语言能做所有事情,所以 Python 并不是满足所有需求的完美解决方案。要决定 Python 是否适合您的环境,您还需要考虑 Python 不擅长的领域。
1.3.1. Python 不是最快的语言
Python 的一个可能的缺点是其执行速度。它不是一个完全编译的语言。相反,它首先被编译成内部字节码形式,然后由 Python 解释器执行。对于一些任务,例如使用正则表达式进行字符串解析,Python 有高效的实现,并且与您可能编写的任何 C 程序一样快,甚至更快。然而,大多数时候,使用 Python 的结果是比 C 语言等语言慢的程序。但您应该从大局出发。现代计算机的计算能力如此强大,对于绝大多数应用程序,程序的速度并不像开发速度那样重要,Python 程序通常可以编写得更快。此外,使用用 C 或 C++编写的模块很容易扩展 Python,这些模块可以用来运行程序中 CPU 密集的部分。
1.3.2. Python 没有最多的库
虽然 Python 自带了优秀的库集合,并且还有更多可供选择,但 Python 在这个领域并不占据领先地位。像 C、Java 和 Perl 这样的语言甚至有更大的库集合可供选择,在某些情况下,它们提供了解决方案,而 Python 则没有或者 Python 可能只有一个选项。然而,这些情况往往相当专业,Python 本身或通过 C 和其他语言中现有的库很容易扩展。对于几乎所有常见的计算问题,Python 的库支持都是出色的。
1.3.3. Python 在编译时不检查变量类型
与某些语言不同,Python 的变量不像容器那样工作;相反,它们更像是指向各种对象的标签:整数、字符串、类实例,等等。这意味着尽管这些对象本身有类型,但指向它们的变量并不绑定到特定的类型。使用变量 x 在一行中指向一个字符串,在另一行中指向一个整数是可能的(如果不是必需的):
>>> x = "2"
>>> x
'2' *1*
>>> x = int(x)
>>> x
2 *2*
-
1 x 是字符串“2”
-
2 x 现在是整数 2
Python 将类型与对象关联,而不是与变量关联的事实意味着解释器不会帮助您捕获变量类型不匹配。如果您打算让变量 count 保存一个整数,Python 不会对您将字符串 "two" 分配给它而抱怨。传统的程序员将此视为一个缺点,因为您失去了对代码的额外免费检查。但这类错误通常不难找到和修复,Python 的测试功能使得避免类型错误变得可行。大多数 Python 程序员认为动态类型带来的灵活性远远超过了其成本。
1.3.4. Python 在移动设备上的支持不多
在过去十年中,移动设备的数量和类型激增,智能手机、平板电脑、大屏手机、Chromebook 等设备无处不在,运行着各种操作系统。Python 在这个领域并不是一个强大的参与者。虽然存在一些选择,但在移动设备上运行 Python 并非总是容易,而且使用 Python 编写和分发商业应用也存在问题。
1.3.5. Python 不擅长使用多处理器
多核处理器现在无处不在,在许多情况下都显著提高了性能。然而,Python 的标准实现并没有设计用来使用多核,这要归因于一个名为全局解释器锁(GIL)的特性。更多信息,可以查找 David Beazley、Larry Hastings 等人关于 GIL 相关的演讲和帖子,或者访问 Python 维基上的 GIL 页面 wiki.python.org/moin/GlobalInterpreterLock。虽然可以通过使用 Python 来运行并发进程,但如果您需要开箱即用的并发性,Python 可能不适合您。
1.4. 为什么学习 Python 3?
Python 已经存在了多年,并随着时间的推移而发展。本书的第一版基于 Python 1.5.2,Python 2.x 几年一直是主导版本。本书基于 Python 3.6,但也已测试过 Python 3.7 的 alpha 版本。
Python 3,最初被戏称为 Python 3000,是 Python 语言历史上第一个不向后兼容的版本。这意味着为 Python 早期版本编写的代码可能需要一些修改才能在 Python 3 上运行。例如,在 Python 的早期版本中,print 语句不需要在参数周围使用括号:
print "hello"
在 Python 3 中,print是一个函数,需要使用括号:
print("hello")
你可能会想,“为什么要改变这样的细节,如果它会破坏旧代码?”因为这种改变对任何语言来说都是一大步,Python 的核心开发者仔细考虑了这个问题。尽管 Python 3 中的变化破坏了与旧代码的兼容性,但这些变化相当小,并且是朝着更好的方向发展的;它们使语言更加一致、可读性更强、歧义更少。Python 3 并不是对语言的彻底重写;它是一个经过深思熟虑的演变。核心开发者还注意提供策略和工具,以安全有效地将旧代码迁移到 Python 3,这将在后面的章节中讨论,同时 Six 和 Future 库也可用于使过渡更加容易。
为什么学习 Python 3?因为它是目前最好的 Python。此外,随着项目转向利用其改进,它将成为未来多年的主导 Python 版本。自 Python 3 引入以来,库的迁移一直稳步进行,现在许多最受欢迎的库都支持 Python 3。实际上,根据 Python Readiness 页面(py3readiness.org),360 个最受欢迎的库中有 319 个已经迁移到 Python 3。如果您需要尚未转换的库,或者您正在使用 Python 2 的现有代码库中工作,请务必坚持使用 Python 2.x。但如果你开始学习 Python 或开始一个项目,请选择 Python 3;它不仅更好,而且也是未来的选择。
摘要
-
Python 是一种现代、高级的语言,具有动态类型、简单一致的语言语法和语义。
-
Python 是跨平台的,高度模块化,适用于快速开发和大规模编程。
-
它运行速度相当快,并且可以通过 C 或 C++模块轻松扩展以提高速度。
-
Python 具有内置的高级功能,如持久化对象存储、高级散列表、可扩展的类语法和通用的比较函数。
-
Python 包含广泛的库,如数值处理、图像处理、用户界面和 Web 脚本。
-
它得到了一个动态的 Python 社区的支撑。
第二章. 入门
本章涵盖
-
安装 Python
-
使用 IDLE 和基本交互模式
-
编写一个简单的程序
-
使用 IDLE 的 Python 命令行窗口
本章将指导你下载、安装和启动 Python 和 IDLE,这是 Python 的集成开发环境。在撰写本文时,Python 3.6 是最新版本,3.7 正在开发中。经过多年的改进,Python 3 是第一个与早期版本不完全向后兼容的语言版本,因此请确保获取 Python 3 的版本。在几年内不太可能发生这样的重大变化,并且未来的任何增强都将考虑到避免影响已经相当庞大的现有代码库。因此,本章之后的内容不太可能很快过时。
2.1. 安装 Python
安装 Python 是一件简单的事情,无论你使用的是哪个平台。第一步是获取适合你机器的最新版本;最新版本总是可以在 www.python.org 找到。本书基于 Python 3.6。如果你有 Python 3.5 或甚至 3.7,那也行。实际上,你应该在使用 Python 3 的任何版本时遇到很少的麻烦。
拥有多个 Python 版本
你可能已经在你的机器上安装了 Python 的早期版本。许多 Linux 发行版和 macOS 都将 Python 2.x 作为操作系统的一部分提供。由于 Python 3 与 Python 2 不完全兼容,因此合理地怀疑在同一个计算机上安装两个版本是否会导致冲突。
没有必要担心;你可以在同一台计算机上安装多个版本的 Python。在基于 UNIX 的系统(如 OS X 和 Linux)的情况下,Python 3 与旧版本一起安装,不会替换它。当你的系统寻找“python”时,它仍然会找到它期望的那个,当你想要访问 Python 3 时,你可以运行 python3 或 idle。在 Windows 上,不同的版本安装在不同的位置,并且有独立的菜单条目。
下文给出了一些关于 Python 安装的针对特定平台的基本描述。具体细节可能会根据你的平台有很大差异,所以请确保阅读下载页面上的说明以及各种版本。你很可能熟悉在你的特定机器上安装软件,所以我会将这些描述保持简短:
-
Microsoft Windows— Python 可以通过 Python 安装程序在大多数 Windows 版本中安装,目前称为 python-3.6.1.exe。下载它,执行它,并按照安装程序的提示操作。你可能需要以管理员身份登录才能运行安装。如果你在网络上,并且没有管理员密码,请要求你的系统管理员为你安装。
-
Macintosh— 您需要获取与您的 OS X 版本和处理器匹配的 Python 3 版本。确定正确的版本后,下载磁盘映像文件,双击以挂载它,然后在其中运行安装程序。OS X 安装程序会自动设置一切,Python 3 将位于应用程序文件夹内的一个子文件夹中,并标有版本号。macOS 随系统附带各种版本的 Python,但您无需担心这一点;Python 3 将作为系统版本之外的版本安装。如果您已安装 brew,您也可以使用命令
brew install python3来安装 Python。您可以通过访问 Python 主页上的链接来获取有关在 OS X 上使用 Python 的更多信息。 -
Linux/UNIX— 大多数 Linux 发行版都预装了 Python。但 Python 的版本各不相同,安装的 Python 版本可能不是 3;对于本书,您需要确保已安装 Python 3 软件包。也可能 IDLE 默认未安装,您可能需要单独安装该软件包。虽然您也可以从 www.python.org 网站上提供的源代码构建 Python 3,但还需要额外的库,而且这个过程不适合初学者。如果您的 Linux 发行版有预编译的 Python 版本,我建议您使用它。使用您发行版的软件管理系统来定位和安装正确的 Python 3 和 IDLE 软件包。还有许多其他操作系统下运行 Python 的版本。有关当前支持的平台和安装细节,请参阅 www.python.org。
Anaconda:Python 的替代发行版
除了您可以直接从 Python.org 获取的 Python 发行版之外,还有一种名为 Anaconda 的发行版正在获得越来越多的关注,尤其是在科学和数据科学用户中。Anaconda 是一个以 Python 为核心的开源数据科学平台。当您安装 Anaconda 时,您不仅会获得 Python,还会获得 R 语言和丰富的预安装数据科学包,您还可以通过包含的 conda 软件包管理器添加更多包。您还可以安装 miniconda,它仅包含 Python 和 conda,然后添加您需要的包。
您可以从 www.anaconda.com/download/ 获取 Anaconda 或 miniconda。下载与您的操作系统匹配的 Python 3 版本的安装程序,并按照说明运行它。完成之后,您的机器上就会安装上完整的 Python 版本。
尤其是如果您的主要兴趣在于数据科学,您可能会发现 Anaconda 是一种更快、更简单的方式来快速开始使用 Python。
2.2. 基本交互模式和 IDLE
您有两种内置选项来获取对 Python 解释器的交互式访问:原始的基本(命令行)模式和 IDLE。IDLE 在许多平台上都可用,包括 Windows、Mac 和 Linux,但在其他平台上可能不可用。您可能需要做更多工作并安装额外的软件包来运行 IDLE,但这样做是值得的,因为 IDLE 提供的体验比基本交互模式更为流畅。另一方面,即使您通常使用 IDLE,有时您可能仍然想要启动基本模式。您应该足够熟悉,可以启动和使用任一模式。
2.2.1. 基本交互模式
基本交互模式是一个相当原始的环境,但本书中的交互式示例通常都很小。本书后面,您将学习如何轻松地将您放置在文件中的代码带入会话(通过使用模块机制)。以下是启动 Windows、macOS 和 UNIX 上的基本会话的方法:
-
在 Windows 上启动基本会话— 对于 Python 3.x 版本,您需要在开始菜单的“程序”文件夹中,导航到“Python 3.6 (32-bit)”子菜单下的 Python 3.6 条目,并点击它。或者,您可以直接找到 Python.exe 可执行文件(例如,在 C:\Users\myuser\AppData\Local\Programs\Python \Python35-32),并双击它。这样做会弹出图 2.1 中显示的窗口。
图 2.1. Windows 10 上的基本交互模式
![图片 2.1]()
-
在 macOS 上启动基本会话— 打开一个终端窗口,并输入
python3。如果您收到“命令未找到”错误,请运行位于应用程序文件夹中 Python3 子文件夹中的“更新 Shell 配置文件”命令脚本。 -
在 UNIX 上启动基本会话— 在命令提示符下输入
python3。一个类似于图 2.1 中显示的版本消息,随后是 Python 提示符>>>出现在当前窗口中。
退出交互式外壳
要退出基本会话,请按 Ctrl-Z(如果您在 Windows 上)或 Ctrl-D(如果您在 Linux 或 UNIX 上),或在命令提示符下输入 exit()。
大多数平台都有命令行编辑和命令历史记录机制。您可以使用上箭头和下箭头,以及 Home、End、Page Up 和 Page Down 键来滚动过去的项目,并通过按 Enter 键重复它们。这就是您在学习 Python 时需要通过本书的所有内容所需的一切。另一个选项是使用适用于 Emacs 的优秀 Python 模式,它提供了通过集成外壳缓冲区访问 Python 交互式模式的功能。
2.2.2. IDLE 集成开发环境
IDLE 是 Python 的内置开发环境。其名称基于 集成开发环境(IDE)的缩写(尽管当然,它也可能受到了某个英国电视节目特定演员姓氏的影响)。IDLE 将交互式解释器与代码编辑和调试工具相结合,为您提供创建 Python 代码的一站式服务。IDLE 的各种工具使其成为学习 Python 的一个有吸引力的起点。这是您在 Windows、macOS 和 Linux 上运行 IDLE 的方法:
-
在 Windows 上启动 IDLE—对于 Python 3.6 版本,您需要导航到 Windows 菜单中所有应用文件夹的 Python 3.6 子菜单下的 IDLE (Python GUI) 条目,并点击它。这样做将弹出一个类似于图 2.2 中所示的窗口。
图 2.2. Windows 上的 IDLE
![图片]()
-
在 macOS 上启动 IDLE—导航到应用程序文件夹中的 Python 3.x 子文件夹,并从那里运行 IDLE。
-
在 Linux 或 UNIX 上启动 IDLE—在命令提示符中输入
idle3。这将弹出一个类似于图 2.2 中所示的窗口。如果您通过您的发行版的包管理器安装了 IDLE,编程子菜单或类似的地方也应该有一个 IDLE 的菜单项。
2.2.3. 在基本交互模式和 IDLE 之间进行选择
您应该使用哪个:IDLE 还是基本命令行窗口?首先,您可以使用 IDLE 或 Python 命令行窗口。两者都提供了您完成本书中的代码示例所需的一切,直到您达到第十章。从那里开始,我将介绍编写您自己的模块,IDLE 将是创建和编辑文件的一个方便方式。但如果您对另一个编辑器有强烈的偏好,您可能会发现基本命令行窗口和您喜欢的编辑器也能为您提供同样的服务。如果您没有强烈的编辑器偏好,我建议您从一开始就使用 IDLE。
2.3. 使用 IDLE 的 Python 命令行窗口
当您启动 IDLE 时,Python 命令行窗口(图 2.3)会打开。它提供自动缩进,并根据您输入的代码的 Python 语法类型为您着色。
图 2.3. 在 IDLE 中使用 Python 命令行。代码在输入时自动着色(基于 Python 语法)。将光标放在任何之前的命令上并按 Enter 键将命令和光标移动到底部,在那里您可以编辑命令然后按 Enter 键将其发送到解释器。将光标放在底部,您可以通过按 Alt-P 和 Alt-N 键在之前命令的历史记录中上下切换。当您找到想要的命令时,按需编辑它并按 Enter 键,它将被发送到解释器。

您可以使用鼠标、箭头键、Page Up 和 Page Down 键以及一些标准的 Emacs 键绑定在缓冲区中移动。有关详细信息,请检查帮助菜单。
您会话中的所有内容都是缓冲的。您可以向上滚动或搜索,将光标放在任何一行上,然后按 Enter(创建硬回车),该行将被复制到屏幕底部,在那里您可以编辑它,然后通过再次按 Enter 键将其发送到解释器。或者,将光标留在底部,您可以通过按 Alt-P 和 Alt-N 在之前输入的命令之间切换上下,这会依次将行的副本带到底部。当您找到想要的行时,您可以再次编辑它,然后通过按 Enter 键将其发送到解释器。通过按 Tab 键,您可以查看可能的完成列表,包括 Python 关键字或用户定义的值。
如果您发现自己处于似乎卡住且无法获取新提示符的情况,解释器可能处于等待您输入特定内容的状态。按 Ctrl-C 发送中断并应将您带回到提示符。它也可以用来中断任何正在运行的命令。要退出 IDLE,请从文件菜单中选择退出。
最初,您可能会最频繁地使用编辑菜单。与其他菜单一样,您可以通过双击顶部的虚线将其撕下,并将其留在窗口旁边。
2.4. Hello, world
无论您如何访问 Python 的交互模式,您都应该看到一个由三个尖括号组成的提示符:>>>。这个提示符是 Python 的命令提示符,它表示您可以输入要执行的命令或要评估的表达式。从必做的“Hello, World”程序开始,这是 Python 中的一个单行程序(输入的每一行都以硬回车结束):
>>> print("Hello, World")
Hello, World
在这里,我在命令提示符中输入了 print 函数,结果出现在屏幕上。执行 print 函数会导致其参数被打印到标准输出——通常是屏幕。如果命令是在 Python 从文件运行 Python 程序时执行的,会发生完全相同的事情:“Hello, World”将被打印到屏幕上。
恭喜!您刚刚编写了您的第一个 Python 程序,而我甚至还没有开始谈论这门语言。
2.5. 使用交互提示符探索 Python
无论您是在 IDLE 中还是在标准交互提示符中,一些实用的工具可以帮助您探索 Python。第一个是 help() 函数,它有两种模式。您可以在提示符中输入 help() 来进入帮助系统,在那里您可以获取有关模块、关键字或主题的帮助。当您处于帮助系统时,您会看到一个 help> 提示符,您可以输入一个模块名称,例如 math 或其他主题,以浏览 Python 关于该主题的文档。
通常,使用 help() 的方式更方便一些。将类型或变量名称作为 help() 的参数输入会立即显示该类型的文档:
>>> x = 2
>>> help(x)
Help on int object:
class int(object)
| int(x=0) -> integer
| int(x, base=10) -> integer
|
| Convert a number or string to an integer, or return 0 if no arguments
| are given. If x is a number, return x.__int__(). For floating point
| numbers, this truncates towards zero.
|
| If x is not a number or if base is given, then x must be a string,
| bytes, or bytearray instance representing an integer literal in the...
(continues with the documentation for an int)
使用 help() 函数以这种方式检查方法的确切语法或对象的行为非常方便。
help() 函数是 pydoc 库的一部分,该库提供了多种访问 Python 库内嵌文档的选项。由于每个 Python 安装都附带完整的文档,即使您不在线,也可以轻松访问所有官方文档。有关访问 Python 文档的更多信息,请参阅附录 A。
另一个有用的函数是 dir(),它列出了特定命名空间中的对象。不使用参数时,它列出当前的全局变量,但它也可以列出模块或类型的对象:
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__', 'x']
>>> dir(int)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__',
'__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__',
'__float__', '__floor__', '__floordiv__', '__format__', '__ge__',
'__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__',
'__init__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__',
'__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__',
'__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__',
'__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__',
'__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__',
'__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__',
'__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__',
'__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length',
'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real',
'to_bytes']
>>>
dir() 函数对于查找定义了哪些方法和数据非常有用,对于快速提醒自己属于对象或模块的所有成员也很有用,并且对于调试很有用,因为您可以看到在哪里定义了什么。
与 dir 不同,globals 和 locals 都显示了与对象关联的值。在当前情况下,这两个函数返回相同的结果,所以我们只展示了 globals() 的输出:
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__':
<class '_frozen_importlib.BuiltinImporter'>, '__spec__': None,
'__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>,
'x': 2}
与 dir 不同,globals 和 locals 都显示了与对象关联的值。您可以在第十章中了解更多关于这两个函数的信息;目前,只需知道您有几种选项可以检查 Python 会话中的情况即可。
摘要
-
在 Windows 系统上安装 Python 3 与从 www.python.org 下载最新安装程序并运行一样简单。在 Linux、UNIX 和 Mac 系统上的安装可能会有所不同。
-
请参考 Python 网站的安装说明,并在可能的情况下使用您系统上的软件包安装程序。
-
另一种安装选项是从
www.anaconda.com/download/安装 Anaconda(或 miniconda)发行版。 -
安装 Python 后,您可以使用基本的交互式 shell(稍后,您喜欢的编辑器)或 IDLE 集成开发环境。
第三章. 快速 Python 概览
本章涵盖
-
概览 Python
-
使用内置数据类型
-
控制程序流程
-
创建模块
-
使用面向对象编程
本章的目的是让你对 Python 语言的语法、语义、功能和哲学有一个基本的认识。它被设计成为你提供一个初步的视角或概念框架,随着你在本书的其余部分遇到它们时,你将能够添加细节。
在初次阅读时,你不需要担心深入理解和处理代码段的具体细节。如果你对正在做的事情有一个大致的了解,你就能做得很好。后续章节将引导你了解这些特性的具体细节,并且不假设你有先前的知识。你可以在阅读了后续章节之后,回到本章并在适当的部分复习示例。
3.1. Python 概述
Python 有几个内置数据类型,如整数、浮点数、复数、字符串、列表、元组、字典和文件对象。这些数据类型可以通过语言运算符、内置函数、库函数或数据类型自己的方法进行操作。
程序员也可以定义自己的类并实例化自己的类实例.^([1]) 这些类实例可以通过程序员定义的方法进行操作,以及程序员为它们定义了适当特殊方法属性的编程语言运算符和内置函数。
¹
Python 文档和本书使用术语“对象”来指代任何 Python 数据类型的实例,而不仅仅是许多其他语言所说的 类实例。这是因为所有 Python 对象都是某个类的实例。
Python 通过 if-elif-else 构造以及 while 和 for 循环提供条件性和迭代性控制流。它允许以灵活的参数传递选项定义函数。可以通过使用 raise 语句引发异常(错误),并且可以使用 try-except-else-finally 构造来捕获和处理它们。
变量(或标识符)不必声明,可以引用任何内置数据类型、用户定义对象、函数或模块。
3.2. 内置数据类型
Python 有几个内置数据类型,从标量如数字和布尔值到更复杂的数据结构如列表、字典和文件。
3.2.1. 数字
Python 的四种数字类型是整数、浮点数、复数和布尔值:
-
整数—1, –3, 42, 355, 888888888888888, –7777777777(整数的大小除了受可用内存限制外没有限制)
-
浮点数—3.0, 31e12, –6e-4
-
复数—3 + 2j, –4- 2j, 4.2 + 6.3j
-
布尔值—True, False
你可以通过使用算术运算符来操作它们:+(加法)、–(减法)、*(乘法)、/(除法)、**(指数)和 %(取模)。
以下示例使用整数:
>>> x = 5 + 2 - 3 * 2
>>> x
1
>>> 5 / 2
2.5 *1*
>>> 5 // 2
2 *2*
>>> 5 % 2
1
>>> 2 ** 8
256
>>> 1000000001 ** 3
1000000003000000003000000001 *3*
整数除法使用/ 1 进行,结果为浮点数(Python 3.x 中的新特性),而整数除法使用// 2 进行截断。请注意,整数的大小是无限的 3;它们会根据需要增长,仅受可用内存的限制。
这些示例使用浮点数,这些浮点数基于 C 中的双精度数:
>>> x = 4.3 ** 2.4
>>> x
33.13784737771648
>>> 3.5e30 * 2.77e45
9.695e+75
>>> 1000000001.0 ** 3
1.000000003e+27
这些示例使用复数:
>>> (3+2j) ** (2+3j)
(0.6817665190890336-2.1207457766159625j)
>>> x = (3+2j) * (4+9j)
>>> x *1*
(-6+35j)
>>> x.real
-6.0
>>> x.imag
35.0
复数由一个实数元素和一个虚数元素组成,后缀为j。在前面代码中,变量x被赋值为一个复数 1。您可以使用属性符号x.real获取其实部,使用x.imag获取其虚部。
几个内置函数可以操作数字。还有库模块cmath(包含复数函数)和库模块math(包含其他三种类型的函数):
>>> round(3.49) *1*
3
>>> import math
>>> math.ceil(3.49) *2*
4
内置函数始终可用,并且通过使用标准函数调用语法来调用。在前面代码中,round函数使用浮点数作为其输入参数 1 被调用。
库模块中的函数通过import语句提供。在2处,导入了math库模块,并使用属性符号调用其ceil函数:module.function(arguments)。
以下示例使用布尔值:
>>> x = False
>>> x
False
>>> not x
True
>>> y = True * 2 *1*
>>> y
2
除了表示为True和False之外,布尔值的行为类似于数字 1(True)和 0(False) 1。
3.2.2. 列表
Python 有一个强大的内置列表类型:
[]
[1]
[1, 2, 3, 4, 5, 6, 7, 8, 12]
[1, "two", 3, 4.0, ["a", "b"], (5,6)] *1*
列表可以包含其他类型的元素,包括字符串、元组、列表、字典、函数、文件对象以及任何类型的数字 1。
列表可以从其前端或后端进行索引。您还可以通过使用切片符号来引用列表的子段,或切片:
>>> x = ["first", "second", "third", "fourth"]
>>> x[0] *1*
'first' *1*
>>> x[2] *1*
'third'
>>> x[-1] *2*
'fourth' *2*
>>> x[-2] *2*
'third' *2*
>>> x[1:-1] *2*
['second', 'third'] *3*
>>> x[0:3] *3*
['first', 'second', 'third'] *3*
>>> x[-2:-1] *3*
['third'] *3*
>>> x[:3] *3*
['first', 'second', 'third'] *4*
>>> x[-2:] *4*
['third', 'fourth'] *4*
使用正索引从前端 1 索引(从 0 开始作为第一个元素)。使用负索引从后端 2 索引(从-1 开始作为最后一个元素)。使用[m:n] 3 获取切片,其中m是包含的起始点,n是不包含的结束点(见表 3.1)。[:n]切片 4 从其开始处开始,而[m:]切片延伸到列表的末尾。
表 3.1. 列表索引
| x= | [ | "first" , | "second" , | "third" , | "fourth" | ] |
|---|---|---|---|---|---|---|
| 正索引 | 0 | 1 | 2 | 3 | ||
| 负索引 | –4 | –3 | –2 | –1 |
您可以使用此符号在列表中添加、删除和替换元素,或者从中获取元素或新的列表切片:
>>> x = [1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> x[1] = "two"
>>> x[8:9] = []
>>> x
[1, 'two', 3, 4, 5, 6, 7, 8]
>>> x[5:7] = [6.0, 6.5, 7.0] *1*
>>> x
[1, 'two', 3, 4, 5, 6.0, 6.5, 7.0, 8]
>>> x[5:]
[6.0, 6.5, 7.0, 8]
如果新切片比替换的切片大或小,则列表的大小会增加或减少 1。
一些内置函数(len、max和min)、一些运算符(in、+和*)、del语句以及列表方法(append、count、extend、index、insert、pop、remove、reverse和sort)作用于列表:
>>> x = [1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> len(x)
9
>>> [-1, 0] + x *1*
[-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> x.reverse() *2*
>>> x
[9, 8, 7, 6, 5, 4, 3, 2, 1]
运算符 + 和 * 各自创建一个新的列表,而不会改变原始列表 1。通过在列表本身上使用属性表示法来调用列表的方法:x.method (arguments) 2。
这些操作中的一些与切片表示法可以执行的功能重复,但它们提高了代码的可读性。
3.2.3. 元组
元组 与列表类似,但它们是不可变的——也就是说,一旦创建后就不能修改。运算符(in、+ 和 *)和内置函数(len、max 和 min)对它们的作用方式与对列表的作用方式相同,因为它们都不会修改原始数据。索引和切片表示法以相同的方式用于获取元素或切片,但不能用于添加、删除或替换元素。此外,元组只有两种方法:count 和 index。元组的一个重要用途是作为字典的键。当不需要可修改性时,它们的使用效率更高。
()
(1,) *1*
(1, 2, 3, 4, 5, 6, 7, 8, 12)
(1, "two", 3L, 4.0, ["a", "b"], (5, 6)) *2*
一个单元素元组 1 需要一个逗号。与列表一样,元组可以包含其他类型的混合元素,包括字符串、元组、列表、字典、函数、文件对象以及任何类型的数字 2。
可以使用内置函数 tuple 将列表转换为元组:
>>> x = [1, 2, 3, 4]
>>> tuple(x)
(1, 2, 3, 4)
相反,可以使用内置函数 list 将元组转换为列表:
>>> x = (1, 2, 3, 4)
>>> list(x)
[1, 2, 3, 4]
3.2.4. 字符串
字符串处理是 Python 的一个优势。有许多选项可以用来分隔字符串:
"A string in double quotes can contain 'single quote' characters."
'A string in single quotes can contain "double quote" characters.'
'''\tA string which starts with a tab; ends with a newline character.\n'''
"""This is a triple double quoted string, the only kind that can
contain real newlines."""
字符串可以用单引号(' ')、双引号(" ")、三单引号(''' ''')或三双引号(""" """)分隔,并且可以包含制表符(\t)和换行符(\n)字符。
字符串也是不可变的。与它们一起工作的运算符和函数返回由原始字符串派生的新字符串。运算符(in、+ 和 *)和内置函数(len、max 和 min)对字符串的作用方式与对列表和元组的作用方式相同。索引和切片表示法以相同的方式用于获取元素或切片,但不能用于添加、删除或替换元素。
字符串有几个用于处理其内容的方法,re 库模块也包含用于处理字符串的函数:
>>> x = "live and let \t \tlive"
>>> x.split()
['live', 'and', 'let', 'live']
>>> x.replace(" let \t \tlive", "enjoy life")
'live and enjoy life'
>>> import re *1*
>>> regexpr = re.compile(r"[\t ]+")
>>> regexpr.sub(" ", x)
'live and let live'
re 模块 1 提供正则表达式功能。它提供的模式提取和替换功能比 string 模块更复杂。
print 函数输出字符串。其他 Python 数据类型可以轻松转换为字符串并格式化:
>>> e = 2.718
>>> x = [1, "two", 3, 4.0, ["a", "b"], (5, 6)]
>>> print("The constant e is:", e, "and the list x is:", x) *1*
The constant e is: 2.718 and the list x is: [1, 'two', 3, 4.0,
['a', 'b'], (5, 6)]
>>> print("the value of %s is: %.2f" % ("e", e)) *2*
the value of e is: 2.72
对象在打印时会自动转换为字符串表示 1。% 运算符 2 提供了类似于 C 的 sprintf 的格式化功能。
3.2.5. 字典
Python 的内置字典数据类型通过使用哈希表实现了关联数组功能。内置的len函数返回字典中键值对的数量。del语句可以用来删除键值对。与列表类似,字典有几种方法(clear、copy、get、items、keys、update和values)可用。
>>> x = {1: "one", 2: "two"}
>>> x["first"] = "one" *1*
>>> x[("Delorme", "Ryan", 1995)] = (1, 2, 3) *2*
>>> list(x.keys())
['first', 2, 1, ('Delorme', 'Ryan', 1995)]
>>> x[1]
'one'
>>> x.get(1, "not available")
'one'
>>> x.get(4, "not available") *3*
'not available'
- 1 设置新键“first”的值为“one”
键必须是不可变类型 2,包括数字、字符串和元组。值可以是任何类型的对象,包括列表和字典这样的可变类型。如果您尝试访问字典中不存在的键的值,将引发KeyError异常。为了避免这种错误,字典方法get 3 在键不在字典中时可选地返回一个用户定义的值。
3.2.6. 集合
Python 中的集合是无序的对象集合,用于需要了解对象在集合中的成员资格和唯一性的情况。集合的行为类似于没有关联值的字典键集合:
>>> x = set([1, 2, 3, 1, 3, 5]) *1*
>>> x
{1, 2, 3, 5} *2*
>>> 1 in x *3*
True
>>> 4 in x *3*
False
>>>
您可以通过在序列上使用set来创建一个集合,例如列表 1。当将序列转换为集合时,会移除重复项 2。使用in关键字 3 来检查对象是否在集合中。
3.2.7. 文件对象
通过 Python 文件对象访问文件:
>>> f = open("myfile", "w") *1*
>>> f.write("First line with necessary newline character\n")
44
>>> f.write("Second line to write to the file\n")
33
>>> f.close()
>>> f = open("myfile", "r") *2*
>>> line1 = f.readline()
>>> line2 = f.readline()
>>> f.close()
>>> print(line1, line2)
First line with necessary newline character
Second line to write to the file
>>> import os *3*
>>> print(os.getcwd())
c:\My Documents\test
>>> os.chdir(os.path.join("c:\\", "My Documents", "images")) *4*
>>> filename = os.path.join("c:\\", "My Documents",
"test", "myfile") *5*
>>> print(filename)
c:\My Documents\test\myfile
>>> f = open(filename, "r")
>>> print(f.readline())
First line with necessary newline character
>>> f.close()
open语句 1 创建一个文件对象。在这里,当前工作目录中的文件myfile以写入("w")模式打开。写入两行后关闭它 2,然后再次以读取("r")模式打开相同的文件。os模块 3 提供了用于在文件系统中移动和操作文件和目录路径名的多个函数。在这里,您移动到另一个目录 4。但是,通过使用绝对路径名 5 来引用文件,您仍然可以访问它。
还有一些其他输入/输出功能可用。您可以使用内置的input函数提示并从用户那里获取字符串。sys库模块允许访问stdin、stdout和stderr。struct库模块提供了对由 C 程序生成或将要使用的文件的支持。Pickle 库模块通过能够轻松地将 Python 数据类型读写到文件中,实现了数据持久性。
3.3. 控制流结构
Python 提供了一系列结构来控制代码执行和程序流程,包括常见的分支和循环结构。
3.3.1. 布尔值和表达式
Python 有几种表示布尔值的方法;布尔常量False、0、Python 的空值None以及空值(例如,空列表[]或空字符串"")都被视为False。布尔常量True和所有其他内容都被认为是True。
您可以通过使用比较运算符(<, <=, ==, >, >=, !=, is, is not, in, not in)和逻辑运算符(and, not, or)来创建比较表达式,这些运算符都返回True或False。
3.3.2. if-elif-else 语句
在第一个True条件(if或elif)之后的代码块被执行。如果没有条件为True,则执行else之后的代码块:
x = 5
if x < 5:
y = -1
z = 5
elif x > 5: *1*
y = 1 *1*
z = 11 *1*
else:
y = 0 *2*
z = 10 *2*
print(x, y, z)
elif和else子句是可选的 1,可以有任意数量的elif子句。Python 使用缩进来界定代码块 2。不需要显式的界定符,如括号或花括号。每个代码块由一个或多个通过换行符分隔的语句组成。所有这些语句必须在相同的缩进级别。示例中的输出将是5 0 10.。
3.3.3. 循环语句
当条件(这里为x > y)为True时,执行while循环:
u, v, x, y = 0, 0, 100, 30 *1*
while x > y: *2*
u = u + y *2*
x = x - y *2*
if x < y + 2: *2*
v = v + x *2*
x = 0 *2*
else: *2*
v = v + y + 2 *2*
x = x - y - 2 *2*
print(u, v)
这是一个简写符号。在这里,u和v被赋予 0 的值,x被设置为 100,y获得 30 的值 1。这是循环块 2。循环中可以包含break(结束循环)和continue语句(终止当前循环迭代)。输出将是60 40.。
3.3.4. for 循环
for循环简单但强大,因为它可以遍历任何可迭代类型,如列表或元组。与许多语言不同,Python 的for循环遍历序列中的每个项目(例如,列表或元组),使其更像是一个foreach循环。以下循环查找第一个能被 7 整除的整数:
item_list = [3, "string1", 23, 14.0, "string2", 49, 64, 70]
for x in item_list: *1*
if not isinstance(x, int):
continue *2*
if not x % 7:
print("found an integer divisible by seven: %d" % x)
break *3*
x按顺序被分配列表中的每个值 1。如果x不是整数,则通过continue语句终止这次迭代的其余部分 2。控制流继续,x设置为列表的下一个项目。在找到第一个合适的整数后,通过break语句结束循环 3。输出将是
found an integer divisible by seven: 49
3.3.5. 函数定义
Python 提供了灵活的机制来传递参数给函数:
>>> def funct1(x, y, z): *1*
... value = x + 2*y + z**2
... if value > 0:
... return x + 2*y + z**2 *2*
... else:
... return 0
...
>>> u, v = 3, 4
>>> funct1(u, v, 2)
15
>>> funct1(u, z=v, y=2) *3*
23
>>> def funct2(x, y=1, z=1): *4*
... return x + 2 * y + z ** 2
...
>>> funct2(3, z=4)
21
>>> def funct3(x, y=1, z=1, *tup): *5*
... print((x, y, z) + tup)
...
>>> funct3(2)
(2, 1, 1)
>>> funct3(1, 2, 3, 4, 5, 6, 7, 8, 9)
(1, 2, 3, 4, 5, 6, 7, 8, 9)
>>> def funct4(x, y=1, z=1, **kwargs): *6*
... print(x, y, z, kwargs)
>>> funct4(1, 2, m=5, n=9, z=3)
1 2 3 {'n': 9, 'm': 5}
函数通过使用def语句定义 1。return语句 2 是函数用来返回值的方式。这个值可以是任何类型。如果没有遇到return语句,Python 将返回None值。函数参数可以通过位置或名称(关键字)输入。在这里,z和y是通过名称 3 输入的。函数参数可以定义默认值,如果函数调用省略了它们,则使用这些默认值 4。可以定义一个特殊参数,它将函数调用中所有额外的位置参数收集到一个元组中 5。同样,可以定义一个特殊参数,它将函数调用中所有额外的关键字参数收集到一个字典中 6。
3.3.6. 异常
可以使用 try-except-else-finally 复合语句捕获和处理异常(错误)。此语句还可以捕获和处理你自己定义和引发的异常。任何未被捕获的异常都会导致程序退出。此列表展示了基本的异常处理。
列表 3.1. 文件 exception.py
class EmptyFileError(Exception): *1*
pass
filenames = ["myfile1", "nonExistent", "emptyFile", "myfile2"]
for file in filenames:
try: *2*
f = open(file, 'r')
line = f.readline() *3*
if line == "":
f.close()
raise EmptyFileError("%s: is empty" % file) *4*
except IOError as error:
print("%s: could not be opened: %s" % (file, error.strerror)
except EmptyFileError as error:
print(error)
else: *5*
print("%s: %s" % (file, f.readline()))
finally:
print("Done processing", file) *6*
在这里,你定义自己的异常类型,继承自基类 Exception 类型 1。如果在 try 块中的语句执行期间发生 IOError 或 EmptyFileError,则执行相关的 except 块 2。这就是可能引发 IOError 的地方 3。在这里,你引发 EmptyFileError 4。else 子句是可选的 5;如果没有在 try 块中发生异常,则执行它。(注意,在这个例子中,except 块中可以使用 continue 语句代替。)finally 子句是可选的 6;无论是否引发异常,它都在块的末尾执行。
3.3.7. 使用 with 关键字进行上下文处理
使用 with 关键字和上下文管理器封装 try-except-finally 模式的一种更简洁的方法。Python 为文件访问等事物定义了上下文管理器,并且开发者可以定义自定义上下文管理器。上下文管理器的一个好处是它们可能(并且通常确实)定义了默认的清理操作,无论是否发生异常,这些操作都会执行。
此列表展示了使用 with 和上下文管理器打开和读取文件。
列表 3.2. 文件 with.py
filename = "myfile.txt"
with open(filename, "r") as f:
for line in f:
print(f)
在这里,with 建立了一个上下文管理器,它包装了 open 函数和随后的代码块。在这种情况下,上下文管理器的预定义清理操作会关闭文件,即使发生异常也是如此,因此只要第一行的表达式执行时没有引发异常,文件总是会关闭。这段代码等同于以下代码:
filename = "myfile.txt"
try:
f = open(filename, "r")
for line in f:
print(f)
except Exception as e:
raise e
finally:
f.close()
3.4. 模块创建
创建自己的模块很容易,这些模块可以像 Python 的内置库模块一样导入和使用。此列表中的示例是一个简单的模块,其中包含一个函数,提示用户输入文件名,并确定该文件中单词出现的次数。
列表 3.3. 文件 wo.py
"""wo module. Contains function: words_occur()""" *1*
# interface functions *2*
def words_occur():
"""words_occur() - count the occurrences of words in a file."""
# Prompt user for the name of the file to use.
file_name = input("Enter the name of the file: ")
# Open the file, read it and store its words in a list.
f = open(file_name, 'r')
word_list = f.read().split() *3*
f.close()
# Count the number of occurrences of each word in the file.
occurs_dict = {}
for word in word_list:
# increment the occurrences count for this word
occurs_dict[word] = occurs_dict.get(word, 0) + 1
# Print out the results.
print("File %s has %d words (%d are unique)" \ *4*
% (file_name, len(word_list), len(occurs_dict)))
print(occurs_dict)
if __name__ == '__main__': *5*
words_occur()
文档字符串,或称为 docstrings,是记录模块、函数、方法和类的标准方式 1。注释是以 # 字符开头的任何内容 2。read 返回一个包含文件中所有字符的字符串 3,而 split 返回一个列表,其中包含基于空白分隔的字符串中的单词。你可以使用 \ 在多行中拆分长语句 4。此 if 语句允许程序作为脚本运行,通过在命令行中键入 python wo.py 来执行 5。
如果将文件放置在模块搜索路径上的某个目录中,该路径可以在 sys.path 中找到,它可以通过使用 import 语句像任何内置库模块一样导入:
>>> import wo
>>> wo.words_occur() *1*
这个函数是通过使用与库模块函数相同的属性语法调用的 1。
注意,如果你更改磁盘上的 wo.py 文件,import 不会将你的更改引入相同的交互会话。在这种情况下,你使用 imp 库中的 reload 函数:
>>> import imp
>>> imp.reload(wo)
<module 'wo'>
对于更大的项目,有一个模块概念的泛化称为 包,它允许你轻松地将模块分组在目录或目录子树中,然后使用 package.subpackage.module 语法导入和分层引用它们。这仅仅意味着为每个包或子包创建一个可能为空的初始化文件。
3.5. 面向对象编程
Python 完全支持面向对象编程(OOP)。列表 3.4 是一个示例,可能是绘图程序中简单形状模块的起点。它主要用作参考,如果你已经熟悉 OOP。调用注释将 Python 的语法和语义与在其他语言中找到的标准功能联系起来。
[列表 3.4. 文件 sh.py]
"""sh module. Contains classes Shape, Square and Circle"""
class Shape: *1*
"""Shape class: has method move"""
def __init__(self, x, y): *2*
self.x = x *3*
self.y = y *3*
def move(self, deltaX, deltaY): *4*
self.x = self.x + deltaX
self.y = self.y + deltaY
class Square(Shape):
"""Square Class:inherits from Shape"""
def __init__(self, side=1, x=0, y=0):
Shape.__init__(self, x, y)
self.side = side
class Circle(Shape): *5*
"""Circle Class: inherits from Shape and has method area"""
pi = 3.14159 *6*
def __init__(self, r=1, x=0, y=0):
Shape.__init__(self, x, y) *7*
self.radius = r
def area(self):
"""Circle area method: returns the area of the circle."""
return self.radius * self.radius * self.pi
def __str__(self): *8*
return "Circle of radius %s at coordinates (%d, %d)"\
% (self.radius, self.x, self.y)
类是通过使用 class 关键字定义的 1。类的实例初始化方法(构造函数)始终称为 __init__ 2。在这里创建了实例变量 x 和 y 并进行了初始化 3。与函数一样,方法是通过使用 def 关键字定义的 4。任何方法的第一个参数按照惯例称为 self。当方法被调用时,self 被设置为调用该方法的实例。类 Circle 从类 Shape 继承 5,与标准类变量相似,但并不完全相同。一个类必须在它的初始化器中显式地调用其基类的初始化器 7。__str__ 方法被 print 函数使用 8。其他特殊方法属性允许操作符重载,或被内置方法(如长度函数 len)使用。
导入此文件使这些类可用:
>>> import sh
>>> c1 = sh.Circle() *1*
>>> c2 = sh.Circle(5, 15, 20)
>>> print(c1)
Circle of radius 1 at coordinates (0, 0)
>>> print(c2) *2*
Circle of radius 5 at coordinates (15, 20)
>>> c2.area()
78.539749999999998
>>> c2.move(5,6) *3*
>>> print(c2)
Circle of radius 5 at coordinates (20, 26)
初始化器被隐式调用,并创建了一个圆实例 1。print 函数隐式地使用了特殊的 __str__ 方法 2。在这里,你可以看到 Circle 的父类 Shape 的 move 方法是可用的 3。通过在对象实例上使用属性语法调用方法:object.method()。任何方法的第一(self)参数都是隐式设置的。
摘要
-
本章对 Python 进行了快速且非常高级的概述;接下来的章节将提供更多细节。本章结束了本书对 Python 的概述。
-
你在阅读了后续章节中介绍的功能后,可能会发现回顾本章并适当练习示例是有价值的。
-
如果本章对你来说主要是复习,或者如果你想只了解几个功能,请随意跳转,使用索引或目录。
-
在跳转到 第四部分 之前,你应该对本章中介绍的 Python 功能有一个扎实的理解。
第二部分. 基础知识
在接下来的章节中,我将向您展示 Python 的基础知识。我从 Python 程序的基本要素开始,逐步介绍 Python 的内置数据类型和控制结构,以及定义函数和使用模块。
本部分的最后一章将向您展示如何编写独立的 Python 程序,操作文件,处理错误以及使用类。
第四章:绝对基础知识
本章涵盖
-
缩进和块结构
-
区分注释
-
变量赋值
-
表达式求值
-
使用常见的数据类型
-
获取用户输入
-
使用正确的 Python 风格
本章介绍了 Python 的绝对基础知识:如何使用赋值和表达式,如何输入数字或字符串,如何在代码中指示注释,等等。它从讨论 Python 如何组织代码块开始,这与其他所有主要语言都不同。
4.1. 缩进和块结构
Python 与大多数其他编程语言不同,因为它使用空白和缩进来确定块结构(即确定循环的主体、条件语句的 else 子句等)。大多数语言使用某种类型的括号来做这件事。以下是一个计算 9 的阶乘并将结果存储在变量 r 中的 C 代码示例:
/* This is C code */
int n, r;
n = 9;
r = 1;
while (n > 0) {
r *= n;
n--;
}
括号定义了 while 循环的主体,即每次循环重复时执行的代码。代码通常按照所示进行缩进,以清楚地表明正在发生什么,但它也可以写成这样:
/* And this is C code with arbitrary indentation */
int n, r;
n = 9;
r = 1;
while (n > 0) {
r *= n;
n--;
}
即使代码的可读性相当差,代码仍然可以正确执行。
这里是 Python 的等效代码:
# This is Python code. (Yea!)
n = 9
r = 1
while n > 0:
r = r * n *1*
n = n - 1 *2*
-
*1 Python 也支持 C 风格的 r = n
-
2 Python 也支持 n -= 1
Python 不使用花括号来表示代码结构;相反,缩进本身就被用来表示。上一段代码的最后两行是 while 循环的主体,因为它们紧随 while 语句之后,并且比 while 语句多缩进一个级别。如果那些行没有缩进,它们就不会是 while 循环的主体部分。
使用缩进来结构化代码而不是使用括号可能需要一段时间来适应,但好处是显著的:
-
不可能缺少或多余的括号。你永远不需要在代码中寻找与顶部几行匹配的括号。
-
代码的视觉结构反映了其实际结构,这使得仅通过查看代码就能轻松地掌握代码的骨架。
-
Python 的编码风格大多是统一的。换句话说,你不太可能因为处理某人认为美观的代码而变得疯狂。每个人的代码看起来都会很相似。
你可能已经在代码中使用了一致的缩进,所以这对你来说不会是一个很大的步骤。如果你使用的是 IDLE,它将自动缩进行。你只需要在需要时使用退格键退出缩进级别。大多数编程编辑器和 IDE(例如 Emacs、VIM 和 Eclipse)也提供此功能。一旦你习惯了,可能会让你困惑一次或两次的事情是,Python 解释器如果命令前有空格(或多个空格),会返回错误信息。
4.2. 区分注释
在 Python 文件中,大部分跟随#符号的内容都是注释,并被语言忽略。明显的例外是字符串中的#,它只是该字符串的一个字符:
# Assign 5 to x
x = 5
x = 3 # Now x is 3
x = "# This is not a comment"
你会在 Python 代码中频繁地添加注释。
4.3. 变量和赋值
Python 中最常用的命令是赋值,其外观与其他语言中可能使用过的类似。创建一个名为x的变量并将值 5 赋给该变量的 Python 代码如下:
x = 5
在 Python 中,与许多其他计算机语言不同,不需要变量类型声明或行结束分隔符。行以行尾结束。变量在首次赋值时自动创建。
Python 中的变量:桶还是标签?
在 Python 中,变量这个名字有些误导;名称或标签可能更准确。然而,似乎几乎每个人在某个时候都会把变量称为变量。无论你称它们为什么,你应该知道它们在 Python 中是如何真正工作的。
一种常见但并不准确的解释是,变量是一个存储值的容器,有点像桶。这对于许多编程语言(例如 C 语言)来说是合理的。
然而,在 Python 中,变量不是桶。相反,它们是标签或标记,指向 Python 解释器命名空间中的对象。任意数量的标签(或变量)可以指向同一个对象,当该对象发生变化时,所有这些变量所引用的值也会发生变化。
要了解这意味着什么,请看以下简单的代码:
>>> a = [1, 2, 3]
>>> b = a
>>> c = b
>>> b[1] = 5
>>> print(a, b, c)
[1, 5, 3] [1, 5, 3] [1, 5, 3]
如果你认为变量是容器,这个结果就没有意义。如何改变一个容器的内容同时改变另外两个?然而,如果变量只是标签,指向对象,那么改变所有三个标签指向的对象在所有地方都有所反映是有意义的。
如果变量引用的是常量或不可变值,这种区别就不那么明显了:
>>> a = 1
>>> b = a
>>> c = b
>>> b = 5
>>> print(a, b, c)
1 5 1
由于它们所引用的对象不能改变,这种情况下变量的行为与任何一种解释都一致。实际上,在这种情况下,第三行之后,a、b 和 c 都指向同一个不可变的整数对象,其值为 1。下一行b = 5使 b 指向整数对象 5,但不会改变 a 或 c 的引用。
Python 变量可以设置为任何对象,而 C 语言和许多其他语言中的变量只能存储它们声明为的类型。以下是完全合法的 Python 代码:
>>> x = "Hello"
>>> print(x)
Hello
>>> x = 5
>>> print(x)
5
x最初指向字符串对象"Hello",然后指向整数对象5。当然,这个特性可能会被滥用,因为任意地将相同的变量名连续赋值给不同的数据类型可能会使代码难以理解。
新的赋值会覆盖任何之前的赋值。del 语句会删除变量。在删除变量后尝试打印其内容会导致错误,就像变量从一开始就没有被创建过一样:
>>> x = 5
>>> print(x)
5
>>> del x
>>> print(x)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined
>>>
这里,你第一次看到了一个 traceback,当检测到错误,即所谓的 exception 时,会打印出来。最后一行告诉你检测到了什么异常,在这个例子中是 x 上的 NameError 异常。在其删除后,x 就不再是有效的变量名。在这个例子中,由于只有单行被发送到交互模式,所以 trace 只返回 line 1, in <module>。一般来说,在错误发生时,会返回当时现有函数的完整动态调用结构。如果你使用 IDLE,你将获得相同的信息,但有一些小的差异。代码可能看起来像这样:
Traceback (most recent call last):
File "<pyshell#3>", line 1, in <module>
print(x)
NameError: name 'x' is not defined
第十四章更详细地描述了这一机制。可能的异常及其原因的完整列表可以在 Python 标准库文档中找到。使用索引查找你收到的任何特定异常(如 NameError)。
变量名是区分大小写的,可以包括任何字母数字字符以及下划线,但必须以字母或下划线开头。有关创建变量名的 Python 风格的更多指导,请参阅第 4.10 节。
4.4. 表达式
Python 支持算术和类似的表达式;这些表达式对大多数读者来说都很熟悉。以下代码计算了 3 和 5 的平均值,并将结果存储在变量 z 中:
x = 3
y = 5
z = (x + y) / 2
注意,仅涉及整数的算术运算符并不总是返回整数。尽管所有值都是整数,但除法(从 Python 3 开始)返回一个浮点数,因此小数部分不会被截断。如果你想要传统的整数除法返回截断的整数,可以使用 // 代替。
标准的算术优先级规则适用。如果你在上面的最后一行省略了括号,代码将被计算为 x + (y / 2)。
表达式不必仅涉及数值;字符串、布尔值和许多其他类型的对象可以用各种方式用在表达式中。我将更详细地讨论这些对象,当它们被使用时。
尝试以下内容:变量和表达式
在 Python 命令行中创建一些变量。当你尝试在变量名中放入空格、连字符或其他非字母数字字符时会发生什么?尝试一些复杂的表达式,例如 x = 2 + 4 * 5 – 6 / 3。使用括号以不同的方式分组数字,并查看结果与原始未分组表达式的变化。
4.5. 字符串
你已经看到,Python,像大多数其他编程语言一样,使用双引号来表示字符串。这一行将字符串 "Hello, World" 存储在变量 x 中:
x = "Hello, World"
反斜杠可以用作转义字符,赋予它们特殊含义。\n表示换行符,\t表示制表符,\\表示一个普通的单个反斜杠字符,而\"是一个普通的双引号字符。它不会结束字符串:
x = "\tThis string starts with a \"tab\"."
x = "This string contains a single backslash(\\)."
你可以用单引号代替双引号。以下两行做的是同一件事:
x = "Hello, World"
x = 'Hello, World'
唯一的区别是,你不需要在单引号字符串中使用反斜杠"字符,或者在双引号字符串中使用反斜杠'字符:
x = "Don't need a backslash"
x = 'Can\'t get by without a backslash'
x = "Backslash your \" character!"
x = 'You can leave the " alone'
你不能将普通字符串拆分到多行。以下代码不会工作:
# This Python code will cause an ERROR -- you can't split the string
across two lines.
x = "This is a misguided attempt to
put a newline into a string without using backslash-n"
但 Python 提供了三引号字符串,这使得你可以这样做,并且包括单引号和双引号而不需要反斜杠:
x = """Starting and ending a string with triple " characters
permits embedded newlines, and the use of " and ' without
backslashes"""
现在x是"""分隔符之间的整个句子。(你也可以使用三单引号——'''——来达到同样的效果。)
Python 提供了足够的字符串相关功能,以至于第六章专门讨论了该主题。
4.6. 数字
由于你很可能熟悉来自其他语言的常规数值操作,这本书没有单独的章节来描述 Python 的数值能力。本节描述了 Python 数字的独特特性,而 Python 文档列出了可用的函数。
Python 提供了四种类型的数字:整数、浮点数、复数和布尔值。一个整数常量写作一个整数——0、–11、+33、123456,并且具有无限的范围,仅受限于你机器的资源。浮点数可以用小数点或科学记数法来写:3.14、–2E-8、2.718281828。这些值的精度由底层机器控制,但通常等于 C 中的双精度(64 位)类型。复数可能不太有趣,将在本节稍后单独讨论。布尔值要么是True要么是False,除了它们的字符串表示外,它们的行为与 1 和 0 相同。
算术运算与 C 语言中的类似。涉及两个整数的运算会产生一个整数,除了除法(/),它会产生一个浮点数。如果使用//除法符号,结果是一个整数,并且会截断。涉及浮点数的运算总是产生一个浮点数。以下是一些示例:
>>> 5 + 2 - 3 * 2
1
>>> 5 / 2 # floating-point result with normal division
2.5
>>> 5 / 2.0 # also a floating-point result
2.5
>>> 5 // 2 # integer result with truncation when divided using '//'
2
>>> 30000000000 # This would be too large to be an int in many languages
30000000000
>>> 30000000000 * 3
90000000000
>>> 30000000000 * 3.0
90000000000.0
>>> 2.0e-8 # Scientific notation gives back a float
2e-08
>>> 3000000 * 3000000
9000000000000
>>> int(200.2) *1*
200
>>> int(2e2) *1*
200
>>> float(200) *1*
200.0
这些是类型之间的显式转换 1。int截断浮点数值。
Python 中的数字比 C 或 Java 有两个优势:整数可以任意大,两个整数的除法结果是一个浮点数。
4.6.1. 内置数值函数
Python 作为其核心部分提供了以下与数字相关的函数:
abs, divmod, float, hex, int, max, min, oct,
pow, round
请参阅文档以获取详细信息。
4.6.2. 高级数值函数
更高级的数值函数,如三角函数和双曲三角函数,以及一些有用的常量,并没有内置到 Python 中,而是在一个名为 math 的标准模块中提供。我将在后面详细解释模块。现在,只需知道你必须通过在启动 Python 程序或交互会话时使用以下语句来使本节中的数学函数可用:
from math import *
math 模块提供了以下函数和常量:
acos, asin, atan, atan2, ceil, cos, cosh, e, exp, fabs, floor, fmod,
frexp, hypot, ldexp, log, log10, mod, pi, pow, sin, sinh, sqrt, tan,
tanh
请参阅文档以获取详细信息。
4.6.3. 数值计算
由于速度限制,Python 的核心安装并不适合进行密集的数值计算。但强大的 Python 扩展 NumPy 提供了许多高级数值操作的高效实现。重点是数组操作,包括多维矩阵以及更高级的函数,如快速傅里叶变换。你应该能在 www.scipy.org 找到 NumPy(或其链接)。
4.6.4. 复数
每当遇到形式为 nj 的表达式时,复数就会自动创建,其中 n 的形式与 Python 整数或浮点数相同。j 当然是表示等于 –1 的平方根的虚数的标准表示法,例如:
>>> (3+2j)
(3+2j)
注意,Python 以括号的形式表示结果复数,以此表明打印到屏幕上的值代表单个对象的价值:
>>> 3 + 2j - (4+4j)
(-1-2j)
>>> (1+2j) * (3+4j)
(-5+10j)
>>> 1j * 1j
(-1+0j)
计算 j * j 会得到预期的答案 –1,但结果仍然是一个 Python 复数对象。复数永远不会自动转换为等效的实数或整数对象。但你可以通过 real 和 imag 容易地访问它们的实部和虚部:
>>> z = (3+5j)
>>> z.real
3.0
>>> z.imag
5.0
注意,复数的实部和虚部始终以浮点数的形式返回。
4.6.5. 高级复数函数
math 模块中的函数不适用于复数;其理由是大多数用户希望 –1 的平方根产生一个错误,而不是一个答案!相反,cmath 模块提供了可以操作复数的类似函数:
acos, acosh, asin, asinh, atan, atanh, cos, cosh, e, exp, log, log10,
pi, sin, sinh, sqrt, tan, tanh.
为了在代码中明确指出这些函数是特殊用途的复数函数,并避免与更常见的等效函数的名称冲突,最好通过以下方式导入 cmath 模块:
import cmath
然后在使用函数时明确引用 cmath 包:
>>> import cmath
>>> cmath.sqrt(-1)
1j
**最小化从
这是一个很好的例子,说明了为什么最好最小化使用 from <module> import * 形式的 import 语句。如果你首先导入了 math 模块,然后导入了 cmath 模块,cmath 中的常用函数将覆盖 math 中的函数。对于阅读你代码的人来说,确定你使用的特定函数的来源也是一项额外的工作。一些模块明确设计为使用这种导入形式。
有关如何使用模块和模块名称的更多详细信息,请参阅第十章。
需要记住的重要一点是,通过导入 cmath 模块,你可以做几乎所有其他数字能做的事情。
尝试这个:操作字符串和数字
在 Python 命令行中,创建一些字符串和数字变量(整数、浮点数,以及复数)。尝试一下当你对这些变量进行操作时会发生什么,包括跨类型操作。例如,你能将一个字符串乘以一个整数吗?或者你能将它乘以一个浮点数或复数吗?然后加载 math 模块并尝试几个函数;然后加载 cmath 模块并做同样的操作。如果你在加载 cmath 模块后尝试使用这些函数之一对整数或浮点数进行操作会发生什么?你如何获取 math 模块函数?
4.7. None 值
除了字符串和数字等标准类型之外,Python 还有一个特殊的基本数据类型,它定义了一个称为 None 的单个特殊数据对象。正如其名所示,None 用于表示空值。它在 Python 的各个地方都有所体现。例如,Python 中的过程只是一个不显式返回值的函数,这意味着它默认返回 None。
在日常 Python 编程中,None 经常很有用,作为一个占位符来指示数据结构中的一个点,最终将找到有意义的数据,即使这些数据尚未计算。你可以轻松地测试 None 的存在,因为整个 Python 系统中只有一个 None 实例(所有对 None 的引用都指向同一个对象),并且 None 只等同于自身。
4.8. 从用户获取输入
你也可以使用 input() 函数从用户获取输入。将你想显示给用户的提示字符串作为 input 的参数:
>>> name = input("Name? ")
Name? Jane
>>> print(name)
Jane
>>> age = int(input("Age? ")) *1*
Age? 28
>>> print(age)
28
>>>
- 1 将输入从字符串转换为 int
这是一种获取用户输入的相当简单的方法。唯一的缺点是输入以字符串的形式出现,所以如果你想将其用作数字,你必须使用 int() 或 float() 函数将其转换。
尝试这个:获取输入
尝试使用 input() 函数获取字符串和整数输入。使用与之前代码类似的代码,不使用 int() 来调用 input() 对整数输入有什么影响?你能修改这段代码以接受浮点数——比如,28.5 吗?如果你故意输入错误类型的值会发生什么?例如,一个期望整数但输入了浮点数的情况,以及一个期望数字但输入了字符串的情况——反之亦然。
4.9. 内置运算符
Python 提供了各种内置运算符,从标准的(+, * 等)到更神秘的,例如执行位移位、位逻辑函数等运算符。这些运算符中的大多数并不比其他语言更独特,因此,我将在正文中不对其进行解释。你可以在文档中找到 Python 内置运算符的完整列表。
4.10. 基本 Python 风格
Python 在编码风格上相对较少的限制,除了要求使用缩进来组织代码块之外。即使在那种情况下,缩进量以及缩进类型(制表符与空格)也没有强制规定。然而,Python 有一些首选的样式约定,这些约定包含在 Python 增强提案(PEP)8 中,该提案在附录 A(kindle_split_039.html#app02)中总结,并在 www.python.org/dev/peps/pep-0008/ 上提供。Python 风格约定的选择在 表 4.1 中提供,但要完全吸收 Python 风格,定期重读 PEP 8。
表 4.1. Python 风格编码约定
| 情况 | 建议 | 示例 |
|---|---|---|
| 模块/包名 | 简短,全部小写,如果需要则使用下划线 | imp, sys |
| 函数名 | 全部小写,使用下划线以提高可读性 | foo(), my_func() |
| 变量名 | 全部小写,使用下划线以提高可读性 | my_var |
| 类名 | 每个单词首字母大写 | MyClass |
| 常量名 | 全大写,使用下划线分隔 | PI, TAX_RATE |
| 缩进 | 每级四个空格,不使用制表符 | |
| 比较 | 不要显式地与 True 或 False 进行比较 | if my_var: if not my_var: |
我强烈建议你遵循 PEP 8 的约定。这些约定是经过深思熟虑且经过时间考验的,它们会使你的代码对你和其他 Python 程序员来说更容易理解。
快速检查:Python 风格
你认为以下哪些变量和函数名不符合 Python 风格?为什么?
bar(), varName, VERYLONGVARNAME, foobar, longvarname,
foo_bar(), really_very_long_var_name
摘要
-
上述基本语法足以开始编写 Python 代码。
-
Python 语法是可预测且一致的。
-
由于语法提供的惊喜不多,许多程序员可以出人意料地快速开始编写代码。
第五章。列表、元组和集合
本章涵盖
-
操作列表和列表索引
-
修改列表
-
排序
-
使用常见的列表操作
-
处理嵌套列表和深拷贝
-
使用元组
-
创建和使用集合
在本章中,我讨论了 Python 的两种主要序列类型:列表和元组。一开始,列表可能会让你想起许多其他语言中的数组,但不要被误导:列表比普通的数组要灵活和强大得多。
元组就像不能修改的列表;你可以把它们看作是列表的一种受限类型或是一种基本记录类型。我在本章后面讨论了这种受限数据类型的需求。本章还讨论了一种较新的 Python 集合类型:集合。当集合中对象的成员资格(而不是其位置)很重要时,集合很有用。
本章的大部分内容都致力于列表,因为如果你理解了列表,你基本上也就理解了元组。本章的最后部分讨论了列表和元组在功能和设计方面的区别。
5.1. 列表就像数组
Python 中的列表与 Java 或 C 或其他语言中的数组几乎相同;它是有序对象集合。你通过将逗号分隔的元素列表括在方括号中来创建列表,如下所示:
# This assigns a three-element list to x
x = [1, 2, 3]
注意,你不必担心提前声明列表或固定其大小。这一行创建了列表并为其赋值,列表会根据需要自动增长或缩小。
Python 中的数组
Python 中可用的类型化array模块基于 C 数据类型提供数组。有关其使用信息,请参阅Python 库参考。我建议你只有在真正需要性能提升时才去查看它。如果需要数值计算,你应该考虑使用在第四章中提到的NumPy,它可在www.scipy.org找到。
与许多其他语言中的列表不同,Python 列表可以包含不同类型的元素;列表元素可以是任何 Python 对象。以下是一个包含各种元素的列表:
# First element is a number, second is a string, third is another list.
x = [2, "two", [1, 2, 3]]
可能最基础的内置列表函数是len函数,它返回列表中的元素数量:
>>> x = [2, "two", [1, 2, 3]]
>>> len(x)
3
注意,len函数不会计算嵌套列表中的项目。
快速检查:len()
对于以下每个len()会返回什么:[0]; []; [[1, 3, [4, 5], 6], 7]?
5.2. 列表索引
理解列表索引的工作方式将使 Python 对你更有用。请阅读整个章节!
可以使用类似于 C 的数组索引的表示法从 Python 列表中提取元素。像 C 和许多其他语言一样,Python 从 0 开始计数;请求元素 0 返回列表的第一个元素,请求元素 1 返回第二个元素,依此类推。以下是一些示例:
>>> x = ["first", "second", "third", "fourth"]
>>> x[0]
'first'
>>> x[2]
'third'
但 Python 的索引比 C 语言的索引更灵活。如果索引是负数,它们表示从列表末尾开始计数的位置,其中-1 是列表中的最后一个位置,-2 是倒数第二个位置,依此类推。继续使用相同的列表 x,你可以执行以下操作:
>>> a = x[-1]
>>> a
'fourth'
>>> x[-2]
'third'
对于涉及单个列表索引的操作,通常可以将索引视为指向列表中的特定元素。对于更高级的操作,更正确的方法是将列表索引视为指示元素之间的位置。在列表["first", "second", "third", "fourth"]中,你可以将索引视为如下所示:
| x =[ | "first", | "second", | "third", | "fourth" | ] | |||||
|---|---|---|---|---|---|---|---|---|---|---|
| 正数索引 | 0 | 1 | 2 | 3 | ||||||
| 负数索引 | –4 | –3 | –2 | –1 |
当你提取单个元素时,这并不相关,但 Python 可以一次性提取或分配整个子列表——这种操作称为切片。要提取index之后的项,而不是输入list[index],而是输入list[index1:index2],以将包括index1和直到(但不包括)index2的所有项提取到一个新列表中。以下是一些示例:
>>> x = ["first", "second", "third", "fourth"]
>>> x[1:-1]
['second', 'third']
>>> x[0:3]
['first', 'second', 'third']
>>> x[-2:-1]
['third']
如果第二个索引指示在第一个索引之前的列表中的位置,这似乎是合理的,这段代码应该返回这些索引之间的元素,但事实并非如此。相反,这段代码返回一个空列表:
>>> x[-1:2]
[]
当切片列表时,也可以省略index1或index2。省略index1意味着“从列表的开始处开始”,省略index2意味着“到列表的末尾”:
>>> x[:3]
['first', 'second', 'third']
>>> x[2:]
['third', 'fourth']
省略两个索引将创建一个从列表开始到结束的新列表——即复制列表。当你想创建一个可以修改而不影响原始列表的副本时,这种技术很有用:
>>> y = x[:]
>>> y[0] = '1 st'
>>> y
['1 st', 'second', 'third', 'fourth']
>>> x
['first', 'second', 'third', 'fourth']
尝试这个:列表切片和索引
使用你对len()函数和列表切片的了解,你如何将两者结合起来在不知道列表大小的情况下获取列表的后半部分?在 Python shell 中实验以确认你的解决方案有效。
5.3. 修改列表
你可以使用列表索引符号来修改列表,以及从中提取元素。将索引放在赋值运算符的左侧:
>>> x = [1, 2, 3, 4]
>>> x[1] = "two"
>>> x
[1, 'two', 3, 4]
切片符号也可以在这里使用。例如,lista[index1:index2] = listb会导致lista在index1和index2之间的所有元素被listb中的元素替换。listb可以比从lista中移除的元素多或少,在这种情况下,lista的长度会改变。你可以使用切片赋值来完成几个操作,如下所示:
>>> x = [1, 2, 3, 4]
>>> x[len(x):] = [5, 6, 7] *1*
>>> x
[1, 2, 3, 4, 5, 6, 7]
>>> x[:0] = [-1, 0] *2*
>>> x
[-1, 0, 1, 2, 3, 4, 5, 6, 7]
>>> x[1:-1] = [] *3*
>>> x
[-1, 7]
-
1 将列表追加到列表末尾
-
2 将列表追加到列表前面
-
3 从列表中删除元素
向列表中追加单个元素是一个如此常见的操作,以至于有一个特殊的 append 方法用于它:
>>> x = [1, 2, 3]
>>> x.append("four")
>>> x
[1, 2, 3, 'four']
如果你尝试将一个列表追加到另一个列表中,可能会出现一个问题。列表被追加为主列表的单个元素:
>>> x = [1, 2, 3, 4]
>>> y = [5, 6, 7]
>>> x.append(y)
>>> x
[1, 2, 3, 4, [5, 6, 7]]
extend 方法类似于 append 方法,但它允许你将一个列表添加到另一个列表中:
>>> x = [1, 2, 3, 4]
>>> y = [5, 6, 7]
>>> x.extend(y)
>>> x
[1, 2, 3, 4, 5, 6, 7]
此外,还有一个特殊的 insert 方法,可以在两个现有元素之间或列表开头插入新的列表元素。insert 是列表的一个方法,并接受两个额外的参数。第一个额外参数是列表中应插入新元素的位置索引,第二个是新元素本身:
>>> x = [1, 2, 3]
>>> x.insert(2, "hello")
>>> print(x)
[1, 2, 'hello', 3]
>>> x.insert(0, "start")
>>> print(x)
['start', 1, 2, 'hello', 3]
insert 理解列表索引,如 5.2 节 中所述,但对于大多数用途,将 list.insert(n, elem) 理解为在列表的第 n 个元素之前插入 elem 更容易。insert 只是一个便利方法。任何可以用 insert 完成的操作也可以用切片赋值完成。也就是说,当 n 为非负数时,list.insert(n, elem) 与 list[n:n] = [elem] 是相同的。使用 insert 可以使代码更易于阅读,并且 insert 甚至可以处理负索引:
>>> x = [1, 2, 3]
>>> x.insert(-1, "hello")
>>> print(x)
[1, 2, 'hello', 3]
del 语句是删除列表项或切片的首选方法。它执行的操作不能通过切片赋值完成,但它通常更容易记住且更容易阅读:
>>> x = ['a', 2, 'c', 7, 9, 11]
>>> del x[1]
>>> x
['a', 'c', 7, 9, 11]
>>> del x[:2]
>>> x
[7, 9, 11]
通常,del list[n] 与 list[n:n+1] = [] 做的是同样的事情,而 del list[m:n] 与 list[m:n] = [] 做的是同样的事情。
remove 方法不是 insert 的逆操作。与 insert 在指定位置插入元素不同,remove 在列表中查找给定值的第一个实例,并将其从列表中删除:
>>> x = [1, 2, 3, 4, 3, 5]
>>> x.remove(3)
>>> x
[1, 2, 4, 3, 5]
>>> x.remove(3)
>>> x
[1, 2, 4, 5]
>>> x.remove(3)
Traceback (innermost last):
File "<stdin>", line 1, in ?
ValueError: list.remove(x): x not in list
如果 remove 找不到可以删除的内容,它将引发错误。你可以通过使用 Python 的异常处理能力来捕获此错误,或者通过在尝试删除之前使用 in 来检查列表中是否存在某个内容来避免这个问题。
reverse 方法是一种更专业的列表修改方法。它有效地原地反转列表:
>>> x = [1, 3, 5, 6, 7]
>>> x.reverse()
>>> x
[7, 6, 5, 3, 1]
尝试这个:修改列表
假设你有一个包含 10 个元素的列表。你如何将列表末尾的最后三个元素移动到列表开头,同时保持它们的顺序?
5.4. 列表排序
列表可以通过使用内置的 Python sort 方法进行排序:
>>> x = [3, 8, 4, 0, 2, 1]
>>> x.sort()
>>> x
[0, 1, 2, 3, 4, 8]
此方法执行原地排序——即改变正在排序的列表。要排序列表而不改变原始列表,你有两种选择。你可以使用在 5.4.2 节 中讨论的 sorted() 内置函数,或者你可以复制列表并排序副本:
>>> x = [2, 4, 1, 3]
>>> y = x[:]
>>> y.sort()
>>> y
[1, 2, 3, 4]
>>> x
[2, 4, 1, 3]
排序也可以用于字符串:
>>> x = ["Life", "Is", "Enchanting"]
>>> x.sort()
>>> x
['Enchanting', 'Is', 'Life']
sort 方法可以排序几乎任何东西,因为 Python 几乎可以比较任何东西。但在排序时有一个注意事项:sort 方法默认使用的键方法要求列表中的所有项都必须是可比较的类型。这意味着在包含数字和字符串的列表上使用 sort 方法会引发异常:
>>> x = [1, 2, 'hello', 3]
>>> x.sort()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'str' and 'int'
相反,你可以对列表的列表进行排序:
>>> x = [[3, 5], [2, 9], [2, 3], [4, 1], [3, 2]]
>>> x.sort()
>>> x
[[2, 3], [2, 9], [3, 2], [3, 5], [4, 1]]
根据内置的 Python 比较复杂对象的规则,子列表首先按升序的第一个元素排序,然后按升序的第二个元素排序。
sort 更加灵活;它有一个可选的 reverse 参数,当 reverse=True 时,排序将按逆序进行,并且可以使用你自己的键函数来确定列表元素的排序方式。
5.4.1. 自定义排序
要使用自定义排序,你需要能够定义函数——这是我还没有讨论过的。在本节中,我还讨论了 len(string) 返回字符串中字符数的事实。字符串操作在第六章中进行了更全面的讨论。
默认情况下,sort 使用内置的 Python 比较函数来确定排序顺序,这对于大多数目的来说都是令人满意的。然而,有时你想要以不对应默认排序的方式对列表进行排序。假设你想要按每个单词中的字符数对单词列表进行排序,而不是 Python 通常执行的字典序排序。
要做到这一点,编写一个函数,返回你想要排序的值或键,然后与 sort 方法一起使用。在这个 sort 的上下文中,这个函数是一个接受一个参数并返回 sort 函数将要使用的键或值的函数。
对于按字符数排序,一个合适的键函数可以是
def compare_num_of_chars(string1):
return len(string1)
这个键函数很简单。它将每个字符串的长度传递回 sort 方法,而不是字符串本身。
在定义了键函数之后,使用它只需通过使用 key 关键字将其传递给 sort 方法。因为函数是 Python 对象,所以它们可以像任何其他 Python 对象一样传递。以下是一个小型程序,说明了默认排序和自定义排序之间的区别:
>>> def compare_num_of_chars(string1):
... return len(string1)
>>> word_list = ['Python', 'is', 'better', 'than', 'C']
>>> word_list.sort()
>>> print(word_list)
['C', 'Python', 'better', 'is', 'than']
>>> word_list = ['Python', 'is', 'better', 'than', 'C']
>>> word_list.sort(key=compare_num_of_chars)
>>> print(word_list)
['C', 'is', 'than', 'Python', 'better']
第一个列表是按字典序排序的(大写字母在前面),第二个列表是按字符数升序排序的。
自定义排序非常有用,但如果性能至关重要,它可能比默认排序慢。通常,这种影响很小,但如果键函数特别复杂,影响可能会超过预期,尤其是在涉及数十万或数百万个元素的排序中。
有一个特定的地方应该避免自定义排序,那就是当你想要按降序而不是升序对列表进行排序时。在这种情况下,使用 sort 方法的 reverse 参数设置为 True。如果出于某种原因你不想这样做,仍然最好正常排序列表,然后使用 reverse 方法来反转结果列表的顺序。这两个操作——标准的排序和反转——仍然会比自定义排序快得多。
5.4.2. sorted() 函数
列表有一个内置的方法可以自行排序,但 Python 中的其他可迭代对象,如字典的键,没有 sort 方法。Python 还有一个内置函数 sorted(),它可以从任何可迭代对象返回一个排序后的列表。sorted() 使用与排序方法相同的 key 和 reverse 参数:
>>> x = (4, 3, 1, 2)
>>> y = sorted(x)
>>> y
[1, 2, 3, 4]
>>> z = sorted(x, reverse=True)
>>> z
[4, 3, 2, 1]
尝试这个:排序列表
假设你有一个列表,其中每个元素本身也是一个列表:[[1, 2, 3], [2, 1, 3], [4, 0, 1]]。如果你想要根据每个列表中的第二个元素对这个列表进行排序,使得结果为 [[4, 0, 1], [2, 1, 3], [1, 2, 3]],你应该编写什么函数作为 sort() 方法的 key 值?
5.5. 其他常见的列表操作
其他一些列表方法经常很有用,但它们不属于任何特定的类别。
5.5.1. 使用 in 运算符进行列表成员资格
使用 in 运算符很容易测试一个值是否在列表中,它返回一个布尔值。你还可以使用其逆运算符,即 not in 运算符:
>>> 3 in [1, 3, 4, 5]
True
>>> 3 not in [1, 3, 4, 5]
False
>>> 3 in ["one", "two", "three"]
False
>>> 3 not in ["one", "two", "three"]
True
5.5.2. 使用 + 运算符进行列表连接
要通过连接两个现有的列表来创建一个列表,请使用 +(列表连接)运算符,该运算符不会改变参数列表:
>>> z = [1, 2, 3] + [4, 5]
>>> z
[1, 2, 3, 4, 5]
5.5.3. 使用 * 运算符进行列表初始化
使用 * 运算符生成一个给定大小的列表,该列表初始化为给定值。对于大小在事先已知的大型列表,这是一个常见的操作。虽然你可以使用 append 添加元素并自动扩展列表,但使用 * 在程序开始时正确地设置列表大小可以获得更高的效率。一个大小不变的列表不会产生任何内存重新分配开销:
>>> z = [None] * 4
>>> z
[None, None, None, None]
当以这种方式与列表一起使用时,*(在这个上下文中称为 列表乘法运算符)会根据指示的次数复制给定的列表,并将所有副本连接起来形成一个新的列表。这是在事先定义给定大小的列表的标准 Python 方法。在列表乘法中,通常使用包含单个实例的 None 的列表,但列表可以是任何内容:
>>> z = [3, 1] * 2
>>> z
[3, 1, 3, 1]
5.5.4. 使用 min 和 max 进行列表的最小值或最大值
你可以使用min和max来找到列表中的最小和最大元素。你可能会主要在数值列表中使用min和max,但也可以在包含任何类型元素的列表中使用它们。尝试在类型不同的对象集中找到最大或最小对象会导致错误,如果比较这些类型没有意义的话:
>>> min([3, 7, 0, -2, 11])
-2
>>> max([4, "Hello", [1, 2]])
Traceback (most recent call last):
File "<pyshell#58>", line 1, in <module>
max([4, "Hello",[1, 2]])
TypeError: '>' not supported between instances of 'str' and 'int'
5.5.5. 使用索引进行列表搜索
如果你想要找到值在列表中的位置(而不仅仅是知道值是否在列表中),请使用index方法。此方法遍历列表,寻找与给定值相等的列表元素,并返回该列表元素的位置:
>>> x = [1, 3, "five", 7, -2]
>>> x.index(7)
3
>>> x.index(5)
Traceback (innermost last):
File "<stdin>", line 1, in ?
ValueError: 5 is not in list
尝试查找列表中不存在的元素的位置会引发错误,如下所示。这个错误可以像处理remove方法可能发生的类似错误一样进行处理(即在调用index之前用in测试列表)。
5.5.6. 使用 count 进行列表匹配
count也会遍历列表,寻找给定的值,但它返回值在列表中出现的次数,而不是位置信息:
>>> x = [1, 2, 2, 3, 5, 2, 5]
>>> x.count(2)
3
>>> x.count(5)
2
>>> x.count(4)
0
5.5.7. 列表操作总结
你可以看到列表是非常强大的数据结构,其功能远超普通数组。列表操作在 Python 编程中非常重要,因此值得列出以便于参考,如表 5.1 所示。
表 5.1. 列表操作
| 列表操作 | 说明 | 示例 |
|---|---|---|
| [] | 创建一个空列表 | x = [] |
| len | 返回列表的长度 | len(x) |
| append | 将单个元素添加到列表的末尾 | x.append('y') |
| extend | 将另一个列表添加到列表的末尾 | x.extend(['a', 'b']) |
| insert | 在列表的指定位置插入新元素 | x.insert(0, 'y') |
| del | 删除列表元素或切片 | del(x0) |
| remove | 在列表中搜索并删除指定的值 | x.remove('y') |
| reverse | 在原地对列表进行反转 | x.reverse() |
| sort | 在原地对列表进行排序 | x.sort() |
| + | 将两个列表相加 | x1 + x2 |
| * | 复制列表 | x = ['y'] * 3 |
| min | 返回列表中的最小元素 | min(x) |
| max | 返回列表中的最大元素 | max(x) |
| index | 返回值在列表中的位置 | x.index['y'] |
| count | 计算列表中值出现的次数 | x.count('y') |
| sum | 对项目进行求和(如果可以求和) | sum(x) |
| in | 返回一个项目是否在列表中 | 'y' in x |
熟悉这些列表操作将使你的 Python 编程生活更加轻松。
快速检查:列表操作
len([[1,2]] * 3)的结果会是什么?
使用in运算符和列表的index()方法之间有什么两个区别?
以下哪个会引发异常?:min(["a", "b", "c"]); max([1, 2, "three"]); [1, 2, 3].count("one")
尝试这个:列表操作
如果你有一个列表 x,编写代码以安全地删除一个项目,仅当该值在列表中时。
修改此代码,仅当项目在列表中多次出现时才删除该元素。
5.6. 嵌套列表和深拷贝
本节涵盖了一个你可能想要跳过的更高级的话题,如果你只是刚开始学习这门语言。
列表可以嵌套。嵌套的一个应用是表示二维矩阵。这些矩阵的成员可以通过使用二维索引来引用。这些矩阵的索引工作如下:
>>> m = [[0, 1, 2], [10, 11, 12], [20, 21, 22]]
>>> m[0]
[0, 1, 2]
>>> m[0][1]
1
>>> m[2]
[20, 21, 22]
>>> m[2][2]
22
此机制可以扩展到更高维度,正如你所期望的那样。
大多数时候,你只需要关注这些。但你可能会遇到嵌套列表的问题;特别是变量引用对象的方式以及某些对象(如列表)可以被修改(是可变的)。以下是一个示例:
>>> nested = [0]
>>> original = [nested, 1]
>>> original
[[0], 1]
图 5.1 展示了此示例的外观。
图 5.1. 一个列表,其第一个元素引用一个嵌套列表

现在可以通过嵌套变量或原始变量来更改嵌套列表中的值:
>>> nested[0] = 'zero'
>>> original
[['zero'], 1]
>>> original[0][0] = 0
>>> nested
[0]
>>> original
[[0], 1]
但如果 nested 设置为另一个列表,它们之间的连接就会断开:
>>> nested = [2]
>>> original
[[0], 1]
图 5.2 阐述了此条件。
图 5.2. 原始列表的第一个元素仍然是一个嵌套列表,但嵌套变量引用的是不同的列表。

你已经看到,你可以通过取完整切片(即 x[:])来获取列表的副本。你还可以通过使用 + 或 * 运算符(例如,x + [] 或 x * 1)来获取列表的副本。这些技术比切片方法稍微低效一些。所有三种方法都创建了一个称为 浅拷贝 的列表副本,这可能是你大多数时候想要的结果。但如果你的列表中嵌套了其他列表,你可能想要创建一个 深拷贝。你可以使用 copy 模块的 deepcopy 函数来完成此操作:
>>> original = [[0], 1]
>>> shallow = original[:]
>>> import copy
>>> deep = copy.deepcopy(original)
见 图 5.3 以了解说明。
图 5.3. 浅拷贝不会复制嵌套列表。

原始或浅拷贝变量指向的列表是相连的。通过其中任何一个修改嵌套列表中的值都会影响另一个:
>>> shallow[1] = 2
>>> shallow
[[0], 2]
>>> original
[[0], 1]
>>> shallow[0][0] = 'zero'
>>> original
[['zero'], 1]
深拷贝与原始对象独立,对其的任何更改都不会影响原始列表:
>>> deep[0][0] = 5
>>> deep
[[5], 1]
>>> original
[['zero'], 1]
这种行为适用于任何其他可修改的嵌套对象(如字典)列表。
现在你已经看到了列表能做什么,是时候看看元组了。
尝试这个:列表副本
假设你有一个以下列表:x = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] 你可以使用什么代码来获取该列表的副本 y,其中你可以更改元素,而不会影响 x 的内容?
5.7. 元组
元组是类似于列表的数据结构,但它们不能被修改;它们只能被创建。元组和列表如此相似,你可能想知道为什么 Python 要费力包含它们。原因在于元组有重要的角色,这些角色不能由列表有效地填充,例如作为字典的键。
5.7.1. 元组基础
创建元组与创建列表类似:将一系列值分配给变量。列表是一个由[和]包围的序列;元组是一个由(和)包围的序列:
>>> x = ('a', 'b', 'c')
这行代码创建了一个包含三个元素的元组。
元组创建后,使用它就像使用列表一样,很容易忘记元组和列表是不同的数据类型:
>>> x[2]
'c'
>>> x[1:]
('b', 'c')
>>> len(x)
3
>>> max(x)
'c'
>>> min(x)
'a'
>>> 5 in x
False
>>> 5 not in x
True
元组和列表之间的主要区别是元组是不可变的。尝试修改元组会导致一个令人困惑的错误信息,这是 Python 表示它不知道如何在元组中设置项的方式:
>>> x[2] = 'd'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
你可以通过使用+和*运算符从现有的元组中创建新的元组:
>>> x + x
('a', 'b', 'c', 'a', 'b', 'c')
>>> 2 * x
('a', 'b', 'c', 'a', 'b', 'c')
元组的副本可以通过与列表相同的方式创建:
>>> x[:]
('a', 'b', 'c')
>>> x * 1
('a', 'b', 'c')
>>> x + ()
('a', 'b', 'c')
如果你没有阅读第 5.6 节,你可以跳过本段落的其余部分。元组本身不能被修改。但如果它们包含任何可变对象(例如,列表或字典),这些对象如果仍然分配给它们自己的变量,可能会被更改。包含可变对象的元组不允许作为字典的键。
5.7.2. 单元素元组需要逗号
使用元组有一个小的语法点。因为用于包围列表的方括号在 Python 的其他地方没有使用,所以[]表示一个空列表,而[1]表示一个只有一个元素的列表。对于用于包围元组的括号来说,情况并非如此。括号也可以用于在表达式中分组项,以强制执行特定的评估顺序。如果你在 Python 程序中写下(x + y),你是想表示x和y应该相加然后放入一个只有一个元素的元组中,还是你想使用括号来强制x和y在任一侧的表达式发挥作用之前先相加?
这种情况只对只有一个元素的元组有问题,因为包含多个元素的元组总是包含逗号来分隔元素,而逗号告诉 Python 括号表示一个元组,而不是分组。对于只有一个元素的元组,Python 要求元组中的元素后面跟着一个逗号,以消除歧义。对于零元素(空)元组,没有问题。一个空括号集合必须是一个元组,因为否则它没有意义:
>>> x = 3
>>> y = 4
>>> (x + y) # This line adds x and y.
7
>>> (x + y,) # Including a comma indicates that the parentheses denote a tuple.
(7,)
>>> () # To create an empty tuple, use an empty pair of parentheses.
()
5.7.3. 元组的打包和解包
为了方便起见,Python 允许元组出现在赋值运算符的左侧,在这种情况下,元组中的变量会从赋值运算符右侧的元组中接收相应的值。这里有一个简单的例子:
>>> (one, two, three, four) = (1, 2, 3, 4)
>>> one
1
>>> two
2
这个例子可以写得更加简单,因为 Python 在赋值上下文中甚至没有括号也能识别元组。右侧的值被打包到一个元组中,然后解包到左侧的变量中:
one, two, three, four = 1, 2, 3, 4
一行代码替换了以下四行代码:
one = 1
two = 2
three = 3
four = 4
这种技术是交换变量之间值的一种方便方式。而不是说
temp = var1
var1 = var2
var2 = temp
简单地说
var1, var2 = var2, var1
为了使事情更加方便,Python 3 有一个扩展的解包功能,允许用*标记的元素吸收任何数量的不匹配其他元素的元素。再次,一些例子可以使这个特性更清晰:
>>> x = (1, 2, 3, 4)
>>> a, b, *c = x
>>> a, b, c
(1, 2, [3, 4])
>>> a, *b, c = x
>>> a, b, c
(1, [2, 3], 4)
>>> *a, b, c = x
>>> a, b, c
([1, 2], 3, 4)
>>> a, b, c, d, *e = x
>>> a, b, c, d, e
(1, 2, 3, 4, [])
注意,带星号的元素接收所有剩余的项目作为一个列表,如果没有剩余元素,带星号的元素接收一个空列表。
打包和解包也可以通过使用列表分隔符来完成:
>>> [a, b] = [1, 2]
>>> [c, d] = 3, 4
>>> [e, f] = (5, 6)
>>> (g, h) = 7, 8
>>> i, j = [9, 10]
>>> k, l = (11, 12)
>>> a
1
>>> [b, c, d]
[2, 3, 4]
>>> (e, f, g)
(5, 6, 7)
>>> h, i, j, k, l
(8, 9, 10, 11, 12)
5.7.4. 在列表和元组之间转换
元组可以很容易地通过list函数转换为列表,该函数接受任何序列作为参数,并产生一个与原始序列具有相同元素的新列表。同样,列表可以通过tuple函数转换为元组,该函数执行相同的事情,但产生一个新的元组而不是新的列表:
>>> list((1, 2, 3, 4))
[1, 2, 3, 4]
>>> tuple([1, 2, 3, 4])
(1, 2, 3, 4)
作为有趣的一笔,list是分割字符串为字符的方便方式:
>>> list("Hello")
['H', 'e', 'l', 'l', 'o']
这种技术之所以有效,是因为list(以及tuple)适用于任何 Python 序列,而字符串只是字符序列的集合。(字符串将在第六章中详细讨论。)
快速检查:元组
解释为什么以下操作对于元组x = (1, 2, 3, 4)是不合法的:
x.append(1)
x[1] = "hello"
del x[2]
如果您有一个元组x = (3, 1, 4, 2),您如何将其排序?
5.8. 集合
Python 中的集合是一个无序的对象集合,当您需要了解该对象在集合中的成员资格和唯一性时使用。像字典键(在第七章中讨论过)一样,集合中的项目必须是不可变的且可哈希的。这意味着整数、浮点数、字符串和元组可以是集合的成员,但列表、字典和集合本身不能。
5.8.1. 集合操作
除了适用于集合的一般操作,如in、len和for循环中的迭代外,集合还有几个特定的集合操作:
>>> x = set([1, 2, 3, 1, 3, 5]) *1*
>>> x
{1, 2, 3, 5} *2*
>>> x.add(6) *3*
>>> x
{1, 2, 3, 5, 6}
>>> x.remove(5) *4*
>>> x
{1, 2, 3, 6}
>>> 1 in x *5*
True
>>> 4 in x *5*
False
>>> y = set([1, 7, 8, 9])
>>> x | y *6*
{1, 2, 3, 6, 7, 8, 9}
>>> x & y *7*
{1}
>>> x ^ y *8*
{2, 3, 6, 7, 8, 9}
>>>
您可以使用set在序列上创建一个集合,例如一个列表 1。当将序列转换为集合时,会删除重复项 2。使用set函数创建集合后,您可以使用add 3 和remove 4 来更改集合中的元素。in关键字用于检查对象在集合中的成员资格 5。您还可以使用| 6 来获取两个集合的并集,或组合,使用& 7 来获取它们的交集,使用^ 8 来找到它们的对称差——即在一个集合中或在另一个集合中但不在两个集合中的元素。
这些示例并不是集合操作的完整列表,但足以让你对集合的工作方式有一个良好的了解。更多信息,请参考官方 Python 文档。
5.8.2. Frozensets
因为集合不是不可变的且不可哈希的,所以它们不能属于其他集合。为了解决这个问题,Python 有另一种集合类型,frozenset,它就像一个集合,但在创建后不能被更改。因为 frozensets 是不可变的且可哈希的,所以它们可以是其他集合的成员:
>>> x = set([1, 2, 3, 1, 3, 5])
>>> z = frozenset(x)
>>> z
frozenset({1, 2, 3, 5})
>>> z.add(6)
Traceback (most recent call last):
File "<pyshell#79>", line 1, in <module>
z.add(6)
AttributeError: 'frozenset' object has no attribute 'add'
>>> x.add(z)
>>> x
{1, 2, 3, 5, frozenset({1, 2, 3, 5})}
快速检查:集合
如果你从以下列表中构建一个集合,这个集合将有多少个元素?:[1, 2, 5, 1, 0, 2, 3, 1, 1, (1, 2, 3)]
Lab 5: 检查列表
在这个实验中,任务是读取一组温度数据(1948 年至 2016 年希思罗机场的月最高温度)从文件中,然后找到一些基本信息:最高和最低温度,平均(平均)温度,以及中位温度(如果所有温度都已排序,则位于中间的温度)。
温度数据位于本章源代码目录中的lab_05.txt文件中。因为我还没有讨论读取文件,所以这里提供了将文件读入列表的代码:
temperatures = []
with open('lab_05.txt') as infile:
for row in infile:
temperatures.append(int(row.strip())
你应该找到最高和最低温度,平均值,以及中位数。你可能需要使用min()、max()、sum()、len()和sort()函数/方法。
奖励
确定列表中有多少个独特的温度。
摘要
-
列表和元组是体现元素序列概念的构造,就像字符串一样。
-
列表在其他语言中类似于数组,但具有自动调整大小、切片表示法和许多便利函数。
-
元组就像列表,但不能修改,所以它们使用更少的内存,并且可以作为字典键(参见第七章 chapter 7)。
-
集合是可迭代的集合,但它们是无序的,不能有重复的元素。
第六章. 字符串
本章涵盖
-
将字符串视为字符序列
-
使用基本字符串操作
-
插入特殊字符和转义序列
-
从对象转换为字符串
-
字符串格式化
-
使用字节类型
处理文本——从用户输入到文件名,再到要处理的文本块——是编程中的常见任务。Python 提供了强大的工具来处理和格式化文本。本章讨论了 Python 中的标准字符串和字符串相关操作。
6.1. 字符串作为字符序列
为了提取字符和子字符串,可以将字符串视为字符序列,这意味着你可以使用索引或切片记号:
>>> x = "Hello"
>>> x[0]
'H'
>>> x[-1]
'o'
>>> x[1:]
'ello'
使用字符串切片记号的一个用途是去除字符串末尾的换行符(通常,这是从文件中读取的一行):
>>> x = "Goodbye\n"
>>> x = x[:-1]
>>> x
'Goodbye'
这段代码只是一个示例。你应该知道 Python 字符串有其他更好的方法来去除不需要的字符,但这个例子说明了切片的有用性。
你也可以通过使用 len 函数来确定字符串中的字符数量,就像找出列表中的元素数量一样:
>>> len("Goodbye")
7
但字符串不是字符列表。字符串和列表之间最明显的区别是,与列表不同,字符串不能被修改。尝试执行类似 string.append('c') 或 string[0] = 'H' 的操作会导致错误。你会在前面的例子中注意到,我是通过创建一个前一个字符串的切片来去除字符串中的换行符,而不是直接修改前一个字符串。这是 Python 的一个基本限制,出于效率考虑而实施。
6.2. 基本字符串操作
将 Python 字符串组合的最简单(也可能是最常见)的方法是使用字符串连接运算符 +:
>>> x = "Hello " + "World"
>>> x
'Hello World'
Python 还有一个类似的有用字符串乘法运算符,但并不常用:
>>> 8 * "x"
'xxxxxxxx'
6.3. 特殊字符和转义序列
你已经看到了一些 Python 在字符串中使用时视为特殊的字符序列:\n 表示换行符,\t 表示制表符。以反斜杠开头并用于表示其他字符的字符序列称为 转义序列。转义序列通常用于表示 特殊字符——即没有标准单字符可打印表示的字符(如制表符和换行符)。本节将更详细地介绍转义序列、特殊字符和相关主题。
6.3.1. 基本转义序列
Python 提供了一组简短的两位转义序列,用于在字符串中使用(见表 6.1)。相同的序列也适用于字节对象,将在本章末尾介绍。
表 6.1. 字符串和字节字面量的转义序列
| 转义序列 | 表示的字符 |
|---|---|
| ' | 单引号字符 |
| " | 双引号字符 |
| \ | 反斜杠字符 |
| \a | 铃声字符 |
| \b | 退格字符 |
| \f | 换页字符 |
| \n | 换行字符 |
| \r | 回车字符(与\n 不同) |
| \t | 制表符 |
| \v | 垂直制表符字符 |
ASCII 字符集,即 Python 和几乎所有计算机上使用的标准字符集,定义了相当多的特殊字符。这些字符通过下一节中描述的数字转义序列访问。
6.3.2. 数字(八进制和十六进制)和 Unicode 转义序列
您可以通过使用与该字符对应的八进制(基数为 8)或十六进制(基数为 16)转义序列来在字符串中包含任何 ASCII 字符。八进制转义序列是一个反斜杠后跟三个数字,定义了一个八进制数;与这个八进制数对应的 ASCII 字符将替换八进制转义序列。十六进制转义序列以\x而不是\开头,可以包含任意数量的十六进制数字。转义序列在找到一个不是十六进制数字的字符时终止。例如,在 ASCII 字符表中,字符m的十进制值恰好是 109。这个值是八进制值 155 和十六进制值 6D,所以
>>> 'm'
'm'
>>> '\155'
'm'
>>> '\x6D'
'm'
所有这三个表达式都评估为包含单个字符m的字符串。但这些形式也可以用来表示没有可打印表示的字符。例如,换行符\n的八进制值为 012,十六进制值为 0A:
>>> '\n'
'\n'
>>> '\012'
'\n'
>>> '\x0A'
'\n'
因为 Python 3 中的所有字符串都是 Unicode 字符串,所以它们也可以包含几乎来自每种语言的每个字符。尽管 Unicode 系统的讨论远远超出了本书的范围,但以下示例说明您也可以通过数字(如之前所示)或 Unicode 名称转义任何 Unicode 字符:
>>> unicode_a ='\N{LATIN SMALL LETTER A}' *1*
>>> unicode_a
'a' *2*
>>> unicode_a_with_acute = '\N{LATIN SMALL LETTER A WITH ACUTE}'
>>> unicode_a_with_acute
'á'
>>> "\u00E1" *3*
'á'
>>>
-
1 使用 Unicode 名称进行转义
-
3 使用数字进行转义,使用 \u
Unicode 字符集包括常见的 ASCII 字符2。
6.3.3. 打印与评估包含特殊字符的字符串
我之前提到过,在交互式 Python 会话中评估 Python 表达式和使用print函数打印相同表达式的结果之间的区别。尽管涉及的是相同的字符串,但这两个操作可以产生看起来不同的屏幕输出。在交互式 Python 会话的最高级别评估的字符串会显示其所有特殊字符作为八进制转义序列,这使得字符串中的内容清晰可见。同时,print函数直接将字符串传递给终端程序,该程序可能会以特殊方式解释特殊字符。以下是一个由一个a字符后跟一个换行符、一个制表符和一个b字符组成的字符串的示例:
>>> 'a\n\tb'
'a\n\tb'
>>> print('a\n\tb')
a
b
在第一种情况下,换行符和制表符在字符串中明确显示;在第二种情况下,它们用作换行符和制表符。
正常的 print 函数也会在字符串的末尾添加一个换行符。有时(即当你有以换行符结束的文件行时),你可能不希望这种行为。给 print 函数一个 end 参数为 "" 将导致 print 函数不添加换行符:
>>> print("abc\n")
abc
>>> print("abc\n", end="")
abc
>>>
6.4. 字符串方法
大多数 Python 字符串方法都内置在标准的 Python 字符串类中,因此所有字符串对象都自动拥有它们。标准的 string 模块也包含一些有用的常量。模块将在第十章中详细讨论。第十章。
对于本节的内容,你只需要记住,大多数字符串方法都是通过点(.)附加到它们操作的字符串对象上的,例如 x.upper()。也就是说,它们在字符串对象前加上点。因为字符串是不可变的,所以字符串方法仅用于获取它们的返回值,并且不会以任何方式修改它们附加的字符串对象。
我从最有用和最常用的字符串操作开始;然后讨论一些不太常用但仍然有用的操作。在本节的最后,我将讨论一些与字符串相关的杂项问题。并非所有的字符串方法都在这里进行说明。请参阅文档以获取字符串方法的全列表。
6.4.1. 分割和连接字符串方法
几乎任何处理字符串的人都会发现 split 和 join 方法非常有价值。它们是彼此的逆操作:split 返回字符串中的子字符串列表,而 join 接受一个字符串列表并将它们组合成一个单独的字符串,每个元素之间都有原始字符串。通常,split 使用空白字符作为分隔符来分割字符串,但你可以通过可选参数来改变这种行为。
使用 + 进行字符串连接很有用,但不是将大量字符串连接成一个字符串的高效方法,因为每次应用 + 时都会创建一个新的字符串对象。之前的“Hello World”示例产生了两个字符串对象,其中一个被立即丢弃。更好的选择是使用 join 函数:
>>> " ".join(["join", "puts", "spaces", "between", "elements"])
'join puts spaces between elements'
通过改变用于 join 的字符串,你可以在连接的字符串之间放置任何你想要的内容:
>>> "::".join(["Separated", "with", "colons"])
'Separated::with::colons'
你甚至可以使用空字符串 "" 来连接列表中的元素:
>>> "".join(["Separated", "by", "nothing"])
'Separatedbynothing'
split 最常见的用途可能是作为字符串分隔记录的简单解析机制,这些记录存储在文本文件中。默认情况下,split 在任何空白字符上分割,而不仅仅是单个空格字符,但你也可以通过传递一个可选参数来告诉它在一个特定的序列上分割:
>>> x = "You\t\t can have tabs\t\n \t and newlines \n\n " \
"mixed in"
>>> x.split()
['You', 'can', 'have', 'tabs', 'and', 'newlines', 'mixed', 'in']
>>> x = "Mississippi"
>>> x.split("ss")
['Mi', 'i', 'ippi']
有时,允许连接的字符串中的最后一个字段包含任意文本是有用的,可能包括在读取该数据时split可能分割的子字符串。你可以通过指定split在生成结果时应执行多少次分割来做到这一点,通过一个可选的第二个参数。如果你指定n次分割,split将沿着输入字符串进行,直到它执行了n次分割(生成一个包含n+1 个子字符串的列表)或直到它耗尽字符串。以下是一些示例:
>>> x = 'a b c d'
>>> x.split(' ', 1)
['a', 'b c d']
>>> x.split(' ', 2)
['a', 'b', 'c d']
>>> x.split(' ', 9)
['a', 'b', 'c', 'd']
当使用带有可选第二个参数的split时,你必须提供一个第一个参数。为了在使用第二个参数时按空白字符分割,请使用None作为第一个参数。
我广泛使用split和join,通常是在处理由其他程序生成的文本文件时。如果你想要从你的程序中创建更标准的输出文件,Python 标准库中的csv和json模块是不错的选择。
快速检查:分割和连接
你如何使用split和join将字符串 x 中的所有空白字符替换为破折号,例如将"this is a test"更改为"this-is-a-test"?
6.4.2. 将字符串转换为数字
你可以使用int和float函数将字符串转换为整数或浮点数。如果它们传递的字符串不能被解释为给定类型的数字,这些函数将引发ValueError异常。异常在第十四章中解释。
此外,你可以向int传递一个可选的第二个参数,指定解释输入字符串时使用的数字基数:
>>> float('123.456')
123.456
>>> float('xxyy')
Traceback (innermost last):
File "<stdin>", line 1, in ?
ValueError: could not convert string to float: 'xxyy'
>>> int('3333')
3333
>>> int('123.456') *1*
Traceback (innermost last):
File "<stdin>", line 1, in ?
ValueError: invalid literal for int() with base 10: '123.456'
>>> int('10000', 8) *2*
4096
>>> int('101', 2)
5
>>> int('ff', 16)
255
>>> int('123456', 6) *3*
Traceback (innermost last):
File "<stdin>", line 1, in ?
ValueError: invalid literal for int() with base 6: '123456'
-
1 整数中不能有十进制点。
-
2 将 10000 解释为八进制数
-
3 不能将 123456 解释为六进制数。
你是否发现了最后一个错误的理由?我要求将字符串解释为六进制数,但数字 6 永远不会出现在六进制数中。狡猾!
快速检查:字符串转换为数字
以下哪个不会转换为数字,为什么?
int('a1')
int('12G', 16)
float("12345678901234567890")
int("12*2")
6.4.3. 移除额外空白
三个非常实用的简单方法分别是strip、lstrip和rstrip函数。strip返回一个新的字符串,它与原始字符串相同,除了字符串的开头或结尾的任何空白都被移除。lstrip和rstrip的工作方式类似,但它们只分别移除原始字符串的左端或右端的空白:
>>> x = " Hello, World\t\t "
>>> x.strip()
'Hello, World'
>>> x.lstrip()
'Hello, World\t\t '
>>> x.rstrip()
' Hello, World'
在这个例子中,制表符字符被视为空白。确切的意义可能因操作系统而异,但你总是可以通过访问string.whitespace常量来找出 Python 认为什么是空白。在我的 Windows 系统上,Python 返回以下内容:
>>> import string
>>> string.whitespace
' \t\n\r\x0b\x0c'
>>> " \t\n\r\v\f"
' \t\n\r\x0b\x0c'
以反斜杠十六进制 (\xnn) 格式给出的字符代表制表符和换页字符。空格字符就是它本身。你可能想改变这个变量的值,试图影响 strip 等函数的工作方式,但不要这样做。这样的操作并不能保证你得到你想要的结果。
但你可以通过传递一个包含要移除的字符的字符串作为额外参数来改变 strip、rstrip 和 lstrip 移除的字符:
>>> x = "www.python.org"
>>> x.strip("w") *1*
'.python.org'
>>> x.strip("gor") *2*
'www.python.'
>>> x.strip(".gorw") *3*
'python'
-
1 移除所有 w 字符
-
2 移除所有 g、o 和 r 字符**
-
3 移除所有点、g、o、r 和 w 字符**
注意,strip 会移除任何在额外参数字符串中出现的字符,无论它们的顺序如何 2。
这些函数最常见的使用方式是作为一种快速清理刚读取的字符串的方法。当你从文件中读取行时(在第十三章 [kindle_split_024.html#ch13] 中讨论),这种技术尤其有用,因为 Python 总是读取整个行,包括尾随换行符(如果有的话)。当你开始处理读取的行时,你通常不希望有尾随换行符。rstrip 是一种方便的方法来去除它。
快速检查:strip
如果字符串 x 等于 "(name, date),\n",以下哪个会返回包含 "name, date" 的字符串?
x.rstrip("),")
x.strip("),\n")
x.strip("\n)(,")
6.4.4. 字符串搜索
字符串对象提供了几种方法来执行简单的字符串搜索。在我描述它们之前,我将谈谈 Python 中的另一个模块:re。(这个模块在 第十六章 中进行了深入讨论。)
另一种搜索字符串的方法:re 模块
re 模块也执行字符串搜索,但方式更加灵活,使用 正则表达式。而不是搜索单个指定的子字符串,re 搜索可以查找字符串模式。例如,你可以查找完全由数字组成的子字符串。
为什么在后面会详细讨论 re 模块的时候,我还要提到这个?在我的经验中,许多基本字符串搜索的使用是不恰当的。你会从更强大的搜索机制中受益,但你可能不知道它的存在,所以你甚至没有寻找更好的东西。也许你有一个紧急的项目涉及到字符串,你没有时间阅读这本书的整个内容。如果基本的字符串搜索对你来说足够用,那很好。但请注意,你有一个更强大的替代方案。
四种基本字符串搜索方法类似:find、rfind、index 和 rindex。一个相关的方法 count,用于计算一个子字符串在另一个字符串中可以出现的次数。我将详细描述 find,然后检查其他方法与它的区别。
find 函数接受一个必需的参数:要搜索的子字符串。find 函数返回 string 对象中第一个 substring 实例的第一个字符的位置,如果 substring 在字符串中不存在,则返回 -1:
>>> x = "Mississippi"
>>> x.find("ss")
2
>>> x.find("zz")
-1
find 方法也可以接受一个或两个额外的可选参数。这些参数中的第一个,如果存在,是一个整数 start;它会导致 find 在搜索子字符串时忽略 string 中 start 位置之前的所有字符。第二个可选参数,如果存在,是一个整数 end;它会导致 find 忽略 string 中 end 位置及之后的字符:
>>> x = "Mississippi"
>>> x.find("ss", 3)
5
>>> x.find("ss", 0, 3)
-1
rfind 几乎与 find 相同,除了它从 string 的末尾开始搜索,因此返回子字符串在 string 中最后出现的第一字符的位置:
>>> x = "Mississippi"
>>> x.rfind("ss")
5
rfind 也可以接受一个或两个可选参数,其含义与 find 的相同。
index 和 rindex 与 find 和 rfind 相同,唯一的区别是:如果 index 或 rindex 在 string 中找不到 substring 的出现,它不会返回 -1,而是引发一个 ValueError 异常。具体是什么意思,在你阅读了 第十四章 之后就会清楚。
count 的用法与前面四个函数相同,但返回给定子字符串在给定字符串中非重叠出现的次数:
>>> x = "Mississippi"
>>> x.count("ss")
2
你可以使用另外两种字符串方法来搜索字符串:startswith 和 endswith。这些方法根据它们所使用的字符串是否以参数中给出的字符串之一开始或结束,返回 True 或 False 结果:
>>> x = "Mississippi"
>>> x.startswith("Miss")
True
>>> x.startswith("Mist")
False
>>> x.endswith("pi")
True
>>> x.endswith("p")
False
startswith 和 endswith 都可以同时查找多个字符串。如果参数是字符串的元组,这两个方法都会检查元组中的所有字符串,如果其中任何一个被找到,则返回 True:
>>> x.endswith(("i", "u"))
True
startswith 和 endswith 对于简单的搜索很有用,其中你确信你要检查的是行的开始或结束。
快速检查:字符串搜索
如果你想检查一行是否以字符串 "rejected" 结尾,你会使用什么字符串方法?还有没有其他方法可以得到相同的结果?
6.4.5. 修改字符串
字符串是不可变的,但字符串对象有几种方法可以操作该字符串,并返回一个新的字符串,它是原始字符串的修改版本。这在大多数情况下提供了与直接修改相同的效果。你可以在文档中找到这些方法的更完整描述。
你可以使用 replace 方法将字符串中的 substring(其第一个参数)替换为 newstring(其第二个参数)。此方法还接受一个可选的第三个参数(有关详细信息,请参阅文档):
>>> x = "Mississippi"
>>> x.replace("ss", "+++")
'Mi+++i+++ippi'
与字符串搜索函数一样,re 模块是替换子字符串的更强大方法。
函数 string.maketrans 和 string.translate 可以一起使用,将字符串中的字符转换成不同的字符。尽管很少使用,但在需要时这些函数可以简化你的生活。
假设你正在编写一个程序,将字符串表达式从一种计算机语言翻译成另一种语言。第一种语言中用 ~ 表示逻辑非,而第二种语言中用 !;第一种语言中用 ^ 表示逻辑与,第二种语言中用 &;第一种语言中使用 ( 和 ),而第二种语言中使用 [ 和 ]。在给定的字符串表达式中,你需要将所有 ~ 替换为 !,所有 ^ 替换为 &,所有 ( 替换为 [,以及所有 ) 替换为 ]。你可以通过多次调用 replace 来完成这个任务,但一个更简单、更有效的方法是
>>> x = "~x ^ (y % z)"
>>> table = x.maketrans("~^()", "!&[]")
>>> x.translate(table)
'!x & [y % z]'
第二行使用 maketrans 从其两个字符串参数创建一个翻译表。这两个参数必须包含相同数量的字符,并且创建的表使得在表中查找第一个参数的第 n 个字符会返回第二个参数的第 n 个字符。
接下来,maketrans 生成的表传递给 translate。然后 translate 遍历其 string 对象中的每个字符,并检查它们是否可以在提供的第二个参数的表中找到。如果一个字符可以在翻译表中找到,translate 就会将其替换为表中查找的相应字符,以生成翻译后的字符串。
你可以向 translate 提供一个可选参数来指定应从字符串中删除的字符。有关详细信息,请参阅文档。
string 模块中的其他函数执行更专业的任务。string.lower 将字符串中的所有字母字符转换为小写,而 upper 则相反。capitalize 将字符串的第一个字符转换为大写,而 title 将字符串中的所有单词转换为大写。swapcase 在同一字符串中将小写字符转换为大写,将大写字符转换为小写。expandtabs 通过将每个制表符替换为指定数量的空格来删除字符串中的制表符。ljust、rjust 和 center 使用空格填充字符串以在特定字段宽度内对齐。zfill 在数字字符串的左侧填充零。有关这些方法的详细信息,请参阅文档。
6.4.6. 使用列表操作修改字符串
因为字符串是不可变对象,你不能像操作列表那样直接操作它们。虽然产生新字符串(不改变原始字符串)的操作对许多事情都很有用,但有时你希望能够像操作字符列表一样操作字符串。在这种情况下,将字符串转换为字符列表,做你想做的任何操作,然后将结果列表转换回字符串:
>>> text = "Hello, World"
>>> wordList = list(text)
>>> wordList[6:] = [] *1*
>>> wordList.reverse()
>>> text = "".join(wordList)
>>> print(text) *2*
,olleH
-
1 删除逗号之后的所有内容
-
2 无空格连接
你还可以使用内置的 tuple 函数将字符串转换为字符元组。要将列表转换回字符串,使用 "".join()。
您不应该过度使用此方法,因为它会导致新的 string 对象的创建和销毁,这相对昂贵。以这种方式处理数百或数千个字符串可能不会对您的程序产生太大影响;处理数百万个字符串可能会。
快速检查:修改字符串
有什么快速的方法可以将字符串中的所有标点符号更改为空格?
6.4.7. 有用的方法和常量
string 对象还具有几个有用的方法来报告字符串的各种特征,例如它是否由数字或字母字符组成,或者是否全部为大写或小写:
>>> x = "123"
>>> x.isdigit()
True
>>> x.isalpha()
False
>>> x = "M"
>>> x.islower()
False
>>> x.isupper()
True
有关所有可能的字符串方法的列表,请参阅官方 Python 文档中的字符串部分。
最后,string 模块定义了一些有用的常量。您已经看到了 string.whitespace,它是由 Python 系统认为的空白字符组成的字符串。string.digits 是字符串 '0123456789'。string.hexdigits 包含 string.digits 中的所有字符,以及 'abcdefABCDEF',这是十六进制数中使用的额外字符。string.octdigits 包含 '01234567'——仅包含八进制数中使用的数字。string.lowercase 包含所有小写字母字符;string.uppercase 包含所有大写字母字符;string.letters 包含 string.lowercase 和 string.uppercase 中的所有字符。您可能会想尝试将这些常量赋值以更改语言的行为。Python 会让您逃避这种行为,但这可能不是一个好主意。
记住,字符串是字符序列,因此您可以使用方便的 Python in 操作符来测试字符是否属于这些字符串中的任何一个,尽管通常现有的字符串方法更简单、更容易使用。最常见的字符串操作如表 6.2 所示。
表 6.2. 常见字符串操作
| 字符串操作 | 说明 | 示例 |
|---|---|---|
| + | 将两个字符串连接在一起 | x = "hello " + "world" |
| * | 复制字符串 | x = " " * 20 |
| upper | 将字符串转换为大写 | x.upper() |
| lower | 将字符串转换为小写 | x.lower() |
| title | 将字符串中每个单词的首字母大写 | x.title() |
| find, index | 在字符串中搜索目标 | x.find(y) x.index(y) |
| rfind, rindex | 在字符串中从末尾开始搜索目标 | x.rfind(y) x.rindex(y) |
| startswith, endswith | 检查字符串的开始或结束是否与指定字符串匹配 | x.startswith(y) x.endswith(y) |
| replace | 将目标字符串替换为新字符串 | x.replace(y, z) |
| strip, rstrip, lstrip | 从字符串的末尾删除空白或其他字符 | x.strip() |
| encode | 将 Unicode 字符串转换为 bytes 对象 | x.encode("utf_8") |
注意,这些方法不会改变字符串本身;它们返回字符串中的一个位置或一个新的字符串。
尝试这个:字符串操作
假设你有一个字符串列表,其中一些(但不一定是所有)字符串以双引号字符开始和结束:
x = ['"abc"', 'def', '"ghi"', '"klm"', 'nop']
你会使用什么代码来处理每个元素以仅删除双引号?
你可以使用什么代码来找到 Mississippi 中最后一个 p 的位置?当你找到这个位置后,你会使用什么代码来仅删除那个字母?
6.5. 从对象转换为字符串
在 Python 中,几乎任何东西都可以通过使用内置的 repr 函数转换为某种字符串表示。列表是你迄今为止熟悉的唯一复杂 Python 数据类型,所以在这里,我将一些列表转换为它们的表示:
>>> repr([1, 2, 3])
'[1, 2, 3]'
>>> x = [1]
>>> x.append(2)
>>> x.append([3, 4])
>>> 'the list x is ' + repr(x)
'the list x is [1, 2, [3, 4]]'
示例使用 repr 将列表 x 转换为字符串表示,然后将该字符串与其他字符串连接起来形成最终的字符串。如果没有使用 repr,此代码将无法工作。在表达式 "string" + [1, 2] + 3 中,你是想添加字符串、添加列表还是仅仅添加数字?Python 在这种情况下不知道你想要什么,所以它做了安全的事情(引发错误)而不是做出任何假设。在前面的例子中,所有元素都必须转换为字符串表示,然后才能进行字符串连接。
到目前为止,我描述的唯一复杂 Python 对象是列表,但 repr 可以用于为几乎任何 Python 对象获取某种字符串表示。为了看到这一点,尝试在某个内置的复杂对象周围使用 repr,这是一个实际的 Python 函数:
>>> repr(len)
'<built-in function len>'
Python 没有生成包含实现 len 函数的代码的字符串,但它至少返回了一个字符串——<内置函数 len>——描述了该函数的功能。如果你记住 repr 函数并尝试将其应用于书中提到的每种 Python 数据类型(字典、元组、类等),你会发现无论你有什么类型的 Python 对象,你都可以得到一个描述该对象的字符串。
这对于调试程序非常有用。如果你对你的程序中某个特定点的变量所包含的内容有疑问,请使用 repr 并打印出该变量的内容。
我已经介绍了 Python 如何将任何对象转换为描述该对象的字符串。事实上,Python 可以以两种方式之一完成此操作。repr 函数始终返回可以松散地称为 Python 对象的 正式字符串表示 的内容。更具体地说,repr 返回一个可以重建原始对象的字符串表示。对于大型、复杂对象,这可能不是你希望在调试输出或状态报告中看到的内容。
Python 还提供了内置的 str 函数。与 repr 相比,str 的目的是生成 可打印 的字符串表示,它可以应用于任何 Python 对象。str 返回可能被称为对象的 非正式字符串表示。str 返回的字符串不需要完全定义一个对象,其目的是供人类阅读,而不是供 Python 代码阅读。
当你开始使用它们时,你不会注意到 repr 和 str 之间的任何区别,因为直到你开始使用 Python 的面向对象特性,它们之间没有区别。将 str 应用到任何内置 Python 对象时,总是调用 repr 来计算其结果。只有当你开始定义自己的类时,str 和 repr 之间的区别才变得重要,如第十五章 中所述。
那么为什么现在要谈论这个呢?我想让你意识到,在 repr 的背后还有更多的事情在进行,而不仅仅是能够轻松地编写 print 函数进行调试。作为一个良好的风格习惯,你可能想要养成在创建用于显示信息的字符串时使用 str 而不是 repr 的习惯。
6.6. 使用格式方法
在 Python 3 中,你可以用两种方式格式化字符串。较新的方法是使用字符串类的 format 方法。format 方法将包含用 { } 标记的替换字段的格式字符串与从 format 命令提供的参数中取出的替换值结合起来。如果你需要在字符串中包含字面量 { 或 },你可以将其加倍为 {{ 或 }}。format 命令是一个强大的字符串格式化迷你语言,提供了几乎无限的可能性来操作字符串格式化。相反,对于最常见的用例来说,它使用起来相当简单,所以我在本节中查看了一些基本模式。然后,如果你需要使用更高级的选项,你可以参考标准库文档中的字符串格式化部分。
6.6.1. 格式方法和位置参数
使用字符串 format 方法的简单方法是与传递给 format 函数的参数相对应的编号替换字段:
>>> "{0} is the {1} of {2}".format("Ambrosia", "food", "the gods") *1*
'Ambrosia is the food of the gods'
>>> "{{Ambrosia}} is the {0} of {1}".format("food", "the gods") *2*
'{Ambrosia} is the food of the gods'
注意,format 方法应用于格式字符串,该字符串也可以是一个字符串变量 1。将 { } 字符加倍可以将其转义,这样它们就不会标记替换字段 2。
这个例子有三个替换字段,{0}、{1} 和 {2},它们依次由第一个、第二个和第三个参数填充。无论你在格式字符串中的哪个位置放置 {0},它总是被第一个参数替换,依此类推。
你也可以使用命名参数。
6.6.2. 格式方法和命名参数
format 方法也识别命名参数和替换字段:
>>> "{food} is the food of {user}".format(food="Ambrosia",
... user="the gods")
'Ambrosia is the food of the gods'
在这种情况下,替换参数是通过将替换字段的名字与传递给 format 命令的参数的名字相匹配来选择的。
你也可以使用位置参数和命名参数,甚至可以访问这些参数内的属性和元素:
>>> "{0} is the food of {user[1]}".format("Ambrosia",
... user=["men", "the gods", "others"])
'Ambrosia is the food of the gods'
在这种情况下,第一个参数是位置参数,第二个参数 user[1] 指的是命名参数 user 的第二个元素。
6.6.3. 格式说明符
格式说明符允许你使用比旧式字符串格式化的格式化序列更多的力量和控制来指定格式化的结果。格式说明符允许你控制填充字符、对齐、符号、宽度、精度和数据类型,当它替换替换字段时。如前所述,格式说明符的语法是其自身的迷你语言,过于复杂,无法在此完全介绍,但以下示例可以给你一个其有用性的概念:
>>> "{0:10} is the food of gods".format("Ambrosia") *1*
'Ambrosia is the food of gods'
>>> "{0:{1}} is the food of gods".format("Ambrosia", 10) *2*
'Ambrosia is the food of gods'
>>> "{food:{width}} is the food of gods".format(food="Ambrosia", width=10)
'Ambrosia is the food of gods'
>>> "{0:>10} is the food of gods".format("Ambrosia") *3*
' Ambrosia is the food of gods'
>>> "{0:&>10} is the food of gods".format("Ambrosia") *4*
'&&Ambrosia is the food of gods'
:10 是一个格式说明符,使字段宽度为 10 个空格,并用空格填充 1。 :{1} 从第二个参数 2 中获取宽度。 :>10 强制字段右对齐,并用空格填充 3。 :&>10 强制右对齐,并用 & 而不是空格填充 4。
快速检查:format() 方法
当以下代码片段执行时,x 将包含什么?:
x = "{1:{0}}".format(3, 4)
x = "{0:$>5}".format(3)
x = "{a:{b}}".format(a=1, b=5)
x = "{a:{b}}:{0:$>5}".format(3, 4, a=1, b=5, c=10)
6.7. 使用 % 格式化字符串
本节介绍了使用 字符串模运算符 (%) 格式化字符串。此运算符用于将 Python 值组合成格式化字符串以供打印或其他用途。C 语言用户会注意到它与 printf 函数家族有奇怪的相似性。使用 % 进行字符串格式化是旧式字符串格式化的方式,我在这里介绍它是因为它是 Python 早期版本的标准,你可能会在从早期版本迁移的代码中看到它,或者是由熟悉这些版本的程序员编写的代码中看到它。然而,这种格式化方式不应该用于新代码,因为它计划在未来被弃用并从语言中移除。
这里有一个例子:
>>> "%s is the %s of %s" % ("Ambrosia", "food", "the gods")
'Ambrosia is the food of the gods'
字符串模运算符(中间的粗体 %,而不是示例中它前面的三个 %s 实例)包含两部分:左边,是一个字符串;右边,是一个元组。字符串模运算符会扫描左边的字符串以查找特殊的格式化序列,并通过用右边的值按顺序替换这些格式化序列来生成一个新的字符串。在这个例子中,左边唯一的格式化序列是三个 %s 实例,代表“在这里插入一个字符串”。
在右边传递不同的值会产生不同的字符串:
>>> "%s is the %s of %s" % ("Nectar", "drink", "gods")
'Nectar is the drink of gods'
>>> "%s is the %s of the %s" % ("Brussels Sprouts", "food",
... "foolish")
'Brussels Sprouts is the food of the foolish'
右边元组的成员会被 %s 自动应用 str,因此它们不必已经是字符串:
>>> x = [1, 2, "three"]
>>> "The %s contains: %s" % ("list", x)
"The list contains: [1, 2, 'three']"
6.7.1. 使用格式化序列
所有格式化序列都是位于中心 % 左侧字符串中的子字符串。每个格式化序列以一个百分号开始,后面跟一个或多个字符,这些字符指定了要替换格式化序列的内容以及如何进行替换。之前使用的 %s 格式化序列是最简单的格式化序列;它表示应该用位于中心 % 右侧元组的对应字符串替换 %s。
其他格式化序列可能更复杂。以下序列指定了打印数字的字段宽度(总字符数)为六,指定小数点后的字符数为两位,并将数字左对齐在其字段中。我将这个格式化序列放在尖括号中,这样你可以看到在格式化字符串中插入额外空格的位置:
>>> "Pi is <%-6.2f>" % 3.14159 # use of the formatting sequence: %–6.2f
'Pi is <3.14 >'
格式化序列中允许使用的字符选项都在文档中给出。选项有很多,但没有任何一个特别难用。记住,你总是可以在 Python 中交互式地尝试一个格式化序列,看看它是否按预期工作。
6.7.2. 命名参数和格式化序列
最后,% 操作符的一个额外功能在特定情况下可能很有用。不幸的是,为了描述它,我必须使用一个我还没有详细讨论过的 Python 功能:字典,在其他语言中通常称为 散列表 或 关联数组。你可以跳到第七章去了解字典;现在跳过这一节,稍后再回来;或者直接阅读,相信示例会使事情变得清晰。
格式化序列可以指定通过名称而不是位置来替换它们的内容。当你这样做时,每个格式化序列在其格式化序列的初始 % 后面都有一个括号内的名称,如下所示:
"%(pi).2f" *1*
- 1 注意括号中的名称。
此外,% 操作符右侧的参数不再是作为单个值或值的元组来打印,而是作为要打印的值的字典。在字典中,每个命名的格式化序列都有一个相应命名的键。使用之前的格式化序列和字符串模数操作符,你可能产生如下代码:
>>> num_dict = {'e': 2.718, 'pi': 3.14159}
>>> print("%(pi).2f - %(pi).4f - %(e).2f" % num_dict)
3.14 - 3.1416 - 2.72
当你使用执行大量替换的格式字符串时,这段代码特别有用,因为你不再需要跟踪右侧元素元组与格式字符串中格式化序列的位置对应关系。在 dict 参数中定义元素的顺序无关紧要,模板字符串可以使用 dict 中的值多次(就像 'pi' 条目那样)。
使用 print 函数控制输出
Python 的内置 print 函数还有一些选项可以使处理简单的字符串输出更加容易。当使用一个参数时,print 打印值和换行符,因此一系列对 print 的调用将在单独的一行上打印每个值:
>>> print("a")
a
>>> print("b")
b
但 print 函数的功能不止于此。你还可以给 print 函数传递多个参数,这些参数将在同一行上打印出来,由空格分隔,并以换行符结束:
>>> print("a", "b", "c")
a b c
如果这还不够,你可以给 print 函数提供额外的参数来控制每个项目之间的分隔符以及行结束符:
>>> print("a", "b", "c", sep="|")
a|b|c
>>> print("a", "b", "c", end="\n\n")
a b c
>>>
最后,print 函数还可以用于打印到文件以及控制台输出。
>>> print("a", "b", "c", file=open("testfile.txt", "w")
使用 print 函数的选项为你提供了足够控制简单文本输出的能力,但对于更复杂的情况,最好使用 format 方法。
快速检查:使用 % 格式化字符串
在以下代码片段执行后,变量 x 中会有什么?
x = "%.2f" % 1.1111
x = "%(a).2f" % {'a':1.1111}
x = "%(a).08f" % {'a':1.1111}
6.8. 字符串插值
从 Python 3.6 开始,有一种创建包含任意值字符串常量的方法,这被称为 字符串插值。字符串插值是一种在字面字符串中包含 Python 表达式值的方法。这些 f-字符串,因为它们以 f 开头而通常被称为 f-字符串,使用与格式化方法类似的语法,但开销更小。以下示例应该能给你一个关于 f-字符串如何工作的基本概念:
>>> value = 42
>>> message = f"The answer is {value}"
>>> print(message)
The answer is 42
就像格式化方法一样,可以添加格式说明符:
>>> pi = 3.1415
>>> print(f"pi is {pi:{10}.{2}}")
pi is 3.1
由于字符串插值是一个新特性,目前尚不清楚它将如何被使用。有关 f-字符串和格式说明符的完整文档,请参阅在线 Python 文档中的 PEP-498。
6.9. 字节
一个 bytes 对象与 string 对象类似,但有一个重要的区别:string 是一个不可变的 Unicode 字符序列,而 bytes 对象是一个值从 0 到 256 的整数序列。当你处理二进制数据,如从二进制数据文件读取时,字节可能是必要的。
需要记住的关键点是,bytes 对象可能看起来像字符串,但它们不能像字符串那样使用,也不能与字符串组合:
>>> unicode_a_with_acute = '\N{LATIN SMALL LETTER A WITH ACUTE}'
>>> unicode_a_with_acute
'á'
>>> xb = unicode_a_with_acute.encode() *1*
>>> xb
b'\xc3\xa1' *2*
>>> xb += 'A' *3*
Traceback (most recent call last):
File "<pyshell#35>", line 1, in <module>
xb += 'A'
TypeError: can't concat str to bytes
>>> xb.decode() *4*
'á'
你首先可以看到的是,要将常规(Unicode)字符串转换为 bytes,你需要调用字符串的 encode 方法 1。在编码为 bytes 对象后,字符是 2 个字节,并且不再以字符串的方式打印 2。此外,如果你尝试将 bytes 对象和字符串对象相加,你会得到一个类型错误,因为这两种类型是不兼容的 3。最后,要将 bytes 对象转换回字符串,你需要调用该对象的 decode 方法 4。
大多数情况下,你根本不需要考虑 Unicode 或字节。但是,当你需要处理国际字符集(一个越来越普遍的问题)时,你必须理解常规字符串和 bytes 之间的区别。
快速检查:字节
对于以下哪种类型的数据,你会想要使用字符串?对于哪种可以使用字节?
-
1 存储二进制数据的文件
-
2 带有重音字符的语言中的文本
-
3 只有大小写罗马字符的文本
-
4 一系列不超过 255 的整数
实验室 6:文本预处理
在处理原始文本时,通常在执行其他任何操作之前都需要清理和规范化文本。例如,如果你想找到文本中单词的频率,你可以在开始计数之前确保所有内容都是小写(或如果你更喜欢,大写)并且所有标点符号都已删除。你还可以通过将文本分解成一系列单词来简化工作。在这个实验中,任务是读取《白鲸记》第一部分的第一章(位于书籍的源代码中),确保所有内容都是统一的大小写,删除所有标点符号,并将单词逐行写入第二个文件。因为我还没有介绍读取和写入文件,所以这里提供了这些操作的代码:
with open("moby_01.txt") as infile, open("moby_01_clean.txt", "w") as outfile:
for line in infile:
# make all one case
# remove punctuation
# split into words
# write all words for line
outfile.write(cleaned_words)
摘要
-
Python 字符串具有强大的文本处理功能,包括搜索和替换、修剪字符和更改大小写。
-
字符串是不可变的;它们不能在原地更改。
-
操作看起来像是在更改字符串,实际上返回的是带有更改的副本。
-
re(正则表达式)模块具有更强大的字符串功能,这些功能将在第十六章中讨论。链接。
第七章. 字典
本章涵盖
-
定义字典
-
使用字典操作
-
确定什么可以作为键
-
创建稀疏矩阵
-
使用字典作为缓存
-
信任字典的效率
本章讨论了字典,这是 Python 对关联数组或映射的称呼,它通过使用哈希表来实现。字典非常实用,即使在简单的程序中也是如此。
由于字典对于许多程序员来说不如列表和字符串等基本数据结构熟悉,因此说明字典使用的某些示例可能比其他内置数据结构的示例稍微复杂一些。可能需要阅读第八章的部分内容,才能完全理解本章中的一些示例。
7.1. 什么是字典?
如果你从未在其他语言中使用关联数组或哈希表,理解字典使用的一个好方法是将其与列表进行比较:
-
列表中的值通过称为索引的整数来访问,这些索引指示给定值在列表中的位置。
-
字典通过整数、字符串或其他称为键的 Python 对象来访问值,这些键指示给定值在字典中的位置。换句话说,列表和字典都提供了对任意值的索引访问,但可以用作字典索引的项集比可用作列表索引的项集大得多。此外,字典用于提供索引访问的机制与列表使用的机制相当不同。
-
列表和字典都可以存储任何类型的对象。
-
列表中存储的值会根据它们在列表中的位置隐式地有序排列,因为访问这些值的索引是连续的整数。你可能不在乎这种排序,但如果需要,你可以使用它。字典中存储的值相对于彼此不是隐式有序的,因为字典键不仅仅是数字。注意,如果你使用字典但同时也关心项的顺序(即它们被添加的顺序),你可以使用一个有序字典,这是一个可以从
collections模块导入的字典子类。你还可以通过使用另一个数据结构(通常是列表)来显式存储这种排序,这样就不会改变基本字典没有隐式(内置)排序的事实。
尽管它们之间有差异,但字典和列表的使用通常看起来是相同的。作为一个起点,创建一个空字典的方式与创建一个空列表相似,但使用花括号而不是方括号:
>>> x = []
>>> y = {}
这里,第一行创建了一个新的空列表并将其赋值给x。第二行创建了一个新的空字典并将其赋值给y。
在你创建字典后,你可以像对待列表一样在其中存储值:
>>> y[0] = 'Hello'
>>> y[1] = 'Goodbye'
即使在这些赋值中,字典和列表的使用之间也已经存在显著的差异。尝试用列表做同样的事情会导致错误,因为在 Python 中,向一个不存在的列表位置赋值是非法的。例如,如果你尝试向列表x的0个元素赋值,你会收到一个错误:
>>> x[0] = 'Hello'
Traceback (innermost last):
File "<stdin>", line 1, in ?
IndexError: list assignment index out of range
这并不是字典的问题;字典中会根据需要创建新的位置。
在字典中存储了一些值之后,现在你可以访问和使用它们:
>>> print(y[0])
Hello
>>> y[1] + ", Friend."
'Goodbye, Friend.'
总的来说,这使字典看起来几乎就像一个列表。现在来看看最大的不同。在键不是整数的情况下存储(和使用)一些值:
>>> y["two"] = 2
>>> y["pi"] = 3.14
>>> y["two"] * y["pi"]
6.28
这绝对是一些列表无法做到的事情!与列表索引必须是整数不同,字典键的限制要少得多;它们可以是数字、字符串或一系列广泛的 Python 对象之一。这使得字典非常适合列表无法完成的任务。例如,使用字典而不是列表来实现电话簿应用程序更有意义,因为可以通过人的姓氏来存储和索引该人的电话号码。
字典是从一组任意对象映射到与之相关但同样任意的另一组对象的一种方式。实际的字典、同义词词典或翻译书籍在现实世界中是很好的类比。为了看到这种对应关系是多么自然,这里有一个从英语到法语的颜色翻译器的开头:
>>> english_to_french = {} *1*
>>> english_to_french['red'] = 'rouge' *2*
>>> english_to_french['blue'] = 'bleu'
>>> english_to_french['green'] = 'vert'
>>> print("red is", english_to_french['red']) *3*
red is rouge
-
1 创建空字典
-
2 在其中存储三个单词
-
3 获取‘red’的值
尝试这样做:创建一个字典
编写代码以询问用户输入三个名字和三个年龄。在输入名字和年龄后,询问用户其中一个名字,并打印正确的年龄。
7.2. 其他字典操作
除了基本的元素赋值和访问外,字典还支持几种操作。你可以通过逗号分隔的键值对显式地定义一个字典:
>>> english_to_french = {'red': 'rouge', 'blue': 'bleu', 'green': 'vert'}
len返回字典中的条目数:
>>> len(english_to_french)
3
你可以使用keys方法获取字典中的所有键。这个方法通常用于使用 Python 的for循环遍历字典的内容,这在第八章中有所描述 chapter 8:
>>> list(english_to_french.keys())
['green', 'blue', 'red']
在 Python 3.5 及更早版本中,keys返回的列表中键的顺序没有意义;键不一定排序,也不一定按创建顺序出现。你的 Python 代码打印出的键的顺序可能与我的 Python 代码不同。如果你需要排序的键,你可以将它们存储在一个列表变量中,然后对那个列表进行排序。然而,从 Python 3.6 开始,字典保留了键创建的顺序,并按该顺序返回键。
也可以通过使用values来获取存储在字典中的所有值:
>>> list(english_to_french.values())
['vert', 'bleu', 'rouge']
这种方法并不像keys那样经常被使用。
你可以使用 items 方法来返回所有键及其相关值的元组序列:
>>> list(english_to_french.items())
[('green', 'vert'), ('blue', 'bleu'), ('red', 'rouge')]
与 keys 方法类似,此方法通常与 for 循环结合使用,以遍历字典的内容。
可以使用 del 语句从字典中删除条目(键值对):
>>> list(english_to_french.items())
[('green', 'vert'), ('blue', 'bleu'), ('red', 'rouge')]
>>> del english_to_french['green']
>>> list(english_to_french.items())
[('blue', 'bleu'), ('red', 'rouge')]
字典视图对象
keys、values 和 items 方法返回的不是列表,而是类似于序列的视图,但每当字典发生变化时都会动态更新。这就是为什么在这些示例中你需要使用 list 函数来使它们看起来像列表的原因。否则,它们的行为类似于序列,允许代码在 for 循环中使用 in 来检查它们中的成员资格,等等。
keys 返回的视图(以及在某些情况下 items 返回的视图)也像集合一样行为,具有并集、差集和交集操作。
尝试访问字典中不存在的键在 Python 中是一个错误。为了处理此错误,你可以使用 in 关键字测试字典中是否存在键,如果字典在给定键下存储了值,则返回 True,否则返回 False:
>>> 'red' in english_to_french
True
>>> 'orange' in english_to_french
False
或者,你可以使用 get 函数。如果字典包含该键,此函数返回与该键关联的值,但如果字典不包含该键,则返回其第二个参数:
>>> print(english_to_french.get('blue', 'No translation'))
bleu
>>> print(english_to_french.get('chartreuse', 'No translation'))
No translation
第二个参数是可选的。如果该参数未包含,get 方法在字典不包含该键时返回 None。
类似地,如果你想安全地获取键的值并且确保它在字典中设置为默认值,你可以使用 setdefault 方法:
>>> print(english_to_french.setdefault('chartreuse', 'No translation'))
No translation
get 和 setdefault 之间的区别在于,在 setdefault 调用之后,字典中有一个 'chartreuse' 键,其值为 'No translation'。
你可以通过使用 copy 方法来获取字典的副本:
>>> x = {0: 'zero', 1: 'one'}
>>> y = x.copy()
>>> y
{0: 'zero', 1: 'one'}
此方法创建字典的浅拷贝,这在大多数情况下可能就足够了。对于包含任何可修改对象(例如列表或其他字典)作为值的字典,你可能想通过使用 copy.deepcopy 函数来创建深拷贝。参见第五章以了解浅拷贝和深拷贝的概念介绍。
update 方法使用第二个字典中的所有键值对更新第一个字典。对于两个字典共有的键,第二个字典的值将覆盖第一个字典的值:
>>> z = {1: 'One', 2: 'Two'}
>>> x = {0: 'zero', 1: 'one'}
>>> x.update(z)
>>> x
{0: 'zero', 1: 'One', 2: 'Two'}
字典方法为你提供了一套完整的工具来操作和使用字典。为了快速参考,表 7.1 列出了一些主要的字典函数。
表 7.1. 字典操作
| 字典操作 | 说明 | 示例 |
|---|---|---|
| {} | 创建一个空字典 | x = {} |
| len | 返回字典中的条目数 | len(x) |
| keys | 返回字典中所有键的视图 | x.keys() |
| values | 返回字典中所有值的视图 | x.values() |
| items | 返回字典中所有项的视图 | x.items() |
| del | 从字典中删除条目 | del(x[key]) |
| in | 测试键是否存在于字典中 | 'y' in x |
| get | 返回键的值或可配置的默认值 | x.get('y', None) |
| setdefault | 如果键在字典中,则返回值;否则,将键的值设置为默认值并返回值 | x.setdefault('y', None) |
| copy | 创建字典的浅拷贝 | y = x.copy() |
| update | 合并两个字典的条目 | x.update(z) |
这个表并不是所有字典操作的完整列表。要获取完整列表,请参阅 Python 标准库文档。
快速检查:字典操作
假设你有一个字典x = {'a':1, 'b':2, 'c':3, 'd':4}和一个字典y = {'a':6, 'e':5, 'f':6}。以下代码片段执行后,x 的内容会是什么?:
del x['d']
z = x.setdefault('g', 7)
x.update(y)
7.3. 单词计数
假设你有一个包含单词列表的文件,每行一个单词。你想知道每个单词在文件中出现的次数。你可以使用字典轻松完成这个任务:
>>> sample_string = "To be or not to be"
>>> occurrences = {}
>>> for word in sample_string.split():
... occurrences[word] = occurrences.get(word, 0) + 1 *1*
...
>>> for word in occurrences:
... print("The word", word, "occurs", occurrences[word], \
... "times in the string")
...
The word To occurs 1 times in the string
The word be occurs 2 times in the string
The word or occurs 1 times in the string
The word not occurs 1 times in the string
The word to occurs 1 times in the string
将每个单词的occurrences计数增加1。这是一个字典强大功能的例子。代码很简单,但由于 Python 中对字典操作的高度优化,它也相当快。这个模式如此方便,实际上已经被标准化为标准库中collections模块的Counter类。
7.4. 可以用作键的内容?
之前的例子使用字符串作为键,但 Python 允许使用不仅仅是字符串的方式。任何不可变且可哈希的 Python 对象都可以用作字典的键。
在 Python 中,如前所述,任何可以修改的对象都称为可变。列表是可变的,因为列表元素可以添加、更改或删除。字典也是可变的,原因相同。数字是不可变的。如果变量x指向数字 3,并将 4 赋值给x,那么你让x指向了不同的数字(4),但你并没有改变数字 3 本身;3 仍然是 3。字符串也是不可变的。list[n]返回list的n个元素,string[n]返回string的n个字符,而list[n] = value会更改list的n个元素,但在 Python 中string[n] = character是非法的,会导致错误,因为 Python 中的字符串是不可变的。
不幸的是,键必须是不可变且可哈希的要求意味着列表不能用作字典键,但在许多情况下,有一个类似列表的键会非常方便。例如,在由人的名字和姓氏组成的键下存储有关一个人的信息会非常方便,如果你可以使用一个包含两个元素的列表作为键,你就可以轻松做到这一点。
Python 通过提供元组来解决这个难题,元组基本上是不可变列表;它们的创建和使用方式与列表相似,但一旦创建,就不能修改。还有一个进一步的限制:键也必须是可哈希的,这比仅仅是不可变更进一步。为了可哈希,一个值必须有一个哈希值(由__hash__方法提供),在整个值的生命周期中都不会改变。这意味着包含可变值的元组是不可哈希的,尽管元组本身在技术上是不变的。只有不包含嵌套任何可变对象的元组才是可哈希的,并且可以作为字典的键使用。表 7.2 说明了 Python 的哪些内置类型是不可变的、可哈希的,并且可以作为字典键使用。
表 7.2. 可以用作字典键的 Python 值
| Python 类型 | 不可变? | 可哈希? | 字典键? |
|---|---|---|---|
| int | 是 | 是 | 是 |
| float | 是 | 是 | 是 |
| boolean | 是 | 是 | 是 |
| complex | 是 | 是 | 是 |
| str | 是 | 是 | 是 |
| bytes | 是 | 是 | 是 |
| bytearray | 否 | 否 | 否 |
| list | 否 | 否 | 否 |
| tuple | 是 | 有时 | 有时 |
| set | 否 | 否 | 否 |
| frozenset | 是 | 是 | 是 |
| dictionary | 否 | 否 | 否 |
下面的几节将给出示例,说明元组和字典如何协同工作。
快速检查:什么可以作为键?
判断以下哪个表达式可以是字典键:1;'bob';('tom', [1, 2, 3]);["filename"];"filename";("filename", "extension")`
7.5. 稀疏矩阵
从数学的角度来看,矩阵是一个二维数字网格,通常在教科书中写成带有每边方括号的网格,如下所示。
表示这样的矩阵的一种相当标准的方式是通过列表的列表。在 Python 中,矩阵是这样表示的:
matrix = [[3, 0, -2, 11], [0, 9, 0, 0], [0, 7, 0, 0], [0, 0, 0, -5]]

可以通过行号和列号访问矩阵中的元素:
element = matrix[rownum][colnum]
但在某些应用中,例如天气预报,矩阵可能非常大——每边有数千个元素,总共数百万个元素。这样的矩阵通常包含许多零元素。在某些应用中,除了小部分元素外,矩阵的所有元素都可能被设置为 0。为了节省内存,这样的矩阵通常以只存储非零元素的形式存储。这种表示称为稀疏矩阵。
通过使用具有元组索引的字典,可以简单地实现稀疏矩阵。例如,前面的稀疏矩阵可以表示如下:
matrix = {(0, 0): 3, (0, 2): -2, (0, 3): 11,
(1, 1): 9, (2, 1): 7, (3, 3): -5}
现在你可以通过以下代码片段访问给定行和列编号的矩阵元素:
if (rownum, colnum) in matrix:
element = matrix[(rownum, colnum)]
else:
element = 0
一种稍微不那么清晰但更有效的方法是使用字典的get方法,你可以告诉它如果字典中找不到键,则返回0,否则返回与该键关联的值,从而避免一次字典查找:
element = matrix.get((rownum, colnum), 0)
如果你考虑进行大量的矩阵工作,你可能想了解一下NumPy,这是一个数值计算包。
7.6. 字典作为缓存
这一部分展示了如何使用字典作为缓存,这是一种存储结果以避免重复计算这些结果的数据结构。假设你需要一个名为sole的函数,它接受三个整数作为参数并返回一个结果。这个函数可能看起来像这样:
def sole(m, n, t):
# . . . do some time-consuming calculations . . .
return(result)
但如果这个函数非常耗时,并且被调用数十万次,程序可能会运行得太慢。
现在假设在程序运行期间sole被调用约 200 种不同的参数组合。也就是说,你可能在程序执行期间调用sole(12, 20, 6) 50 次或更多,以及其他许多参数组合。通过消除对相同参数的sole重新计算,你可以节省大量时间。你可以使用具有元组键的字典,如下所示:
sole_cache = {}
def sole(m, n, t):
if (m, n, t) in sole_cache:
return sole_cache[(m, n, t)]
else:
# . . . do some time-consuming calculations . . .
sole_cache[(m, n, t)] = result
return result
重写的sole函数使用全局变量来存储之前的结果。这个全局变量是一个字典,字典的键是之前给sole提供的参数组合的元组。然后,每当sole传递一个已经计算过结果的参数组合时,它会返回存储的结果而不是重新计算。
尝试这个:使用字典
假设你正在编写一个像电子表格一样的程序。你如何使用字典来存储表格的内容?请编写一些示例代码来存储和检索特定单元格的值。这种方法的潜在缺点可能有哪些?
7.7. 字典的效率
如果你来自传统的编译语言背景,你可能会犹豫使用字典,担心它们不如列表(数组)高效。事实是,Python 字典的实现相当快。许多内部语言特性都依赖于字典,并且已经投入了大量工作来提高它们的效率。由于 Python 的所有数据结构都经过了高度优化,你不应该花太多时间担心哪个更快或更高效。如果使用字典比使用列表更容易和更干净地解决问题,那就这样做,只有在明确字典会导致无法接受的性能下降时才考虑其他方案。
实验 7:单词计数
在上一个实验中,你取了《白鲸记》第一章的文本,标准化了大小写,移除了标点符号,并将分离的单词写入文件。在这个实验中,你读取那个文件,使用字典来计算每个单词出现的次数,然后报告最常见的和最不常见的单词。
摘要
-
字典是强大的数据结构,在 Python 本身内部也被用于许多目的。
-
字典键必须是不可变的,但任何不可变对象都可以作为字典键。
-
使用键意味着比许多其他解决方案更直接地访问数据集合,并且代码更少。
第八章. 控制流
本章涵盖
-
使用
while循环重复代码 -
做出决定:
if-elif-else语句 -
使用
for循环遍历列表 -
使用列表和字典推导式
-
使用缩进来界定语句和代码块
-
评估布尔值和表达式
Python 提供了一套完整的控制流元素,包括循环和条件判断。本章将详细检查每个元素。
8.1. while 循环
你已经多次遇到基本的while循环。完整的while循环看起来像这样:
while condition:
body
else:
post-code
condition是一个布尔表达式——也就是说,它评估为True或False值。只要它是True,body就会重复执行。当condition评估为False时,while循环执行post-code部分,然后终止。如果condition一开始就是False,则body根本不会执行——只执行post-code部分。body和post-code各自是一系列由换行符分隔的 Python 语句,并且处于相同的缩进级别。Python 解释器使用这个级别来界定它们。不需要其他界定符,如花括号或方括号。
注意,while循环的else部分是可选的,并且不常使用。这是因为只要body中没有break,这个循环
while condition:
body
else:
post-code
以及这个循环
while condition:
body
post-code
做同样的事情——第二个更容易理解。我可能不会提到else子句,除非你不知道它,你可能会在别人的代码中遇到这种语法时感到困惑。此外,在某些情况下它很有用。
在while循环的body中可以使用两个特殊语句break和continue。如果执行break,它将立即终止while循环,并且甚至不会执行post-code(如果有else子句)。如果执行continue,它将跳过body的剩余部分;重新评估condition,然后循环按正常方式继续。
8.2. if-elif-else 语句
Python 中 if-then-else 构造的最一般形式是
if condition1:
body1
elif condition2:
body2
elif condition3:
body3
.
.
.
elif condition(n-1):
body(n-1)
else:
body(n)
它说:如果condition1为True,则执行body1;否则,如果condition2为True,则执行body2;否则……以此类推,直到找到评估为True的条件或遇到else子句,在这种情况下,它将执行body(n)。与while循环一样,body部分又是包含一个或多个 Python 语句的序列,这些语句通过换行符分隔,并且处于相同的缩进级别。
当然,你不需要在每个条件判断中都使用所有这些。你可以省略elif部分、else部分,或者两者都省略。如果一个条件判断找不到要执行的body(没有条件评估为True,且没有else部分),它将不执行任何操作。
if 语句之后的 body 是必需的。但在这里可以使用 pass 语句(就像你可以在 Python 中需要语句的任何地方一样)。pass 语句在需要语句的地方充当占位符,但它不执行任何操作:
if x < 5:
pass
else:
x = 5
Python 中没有 case(或 switch)语句。
Python 中哪里有 case 语句?
正如刚才提到的,Python 中没有 case 语句。在大多数情况下,在其他语言中会使用 case 或 switch 语句的地方,Python 只需通过一系列的 if... elif... elif... else 就可以很好地处理。在极少数情况下,这会变得繁琐,通常可以使用函数字典来解决这个问题,就像这个例子中所示:
def do_a_stuff():
#process a
def do_b_stuff():
#process b
def do_c_stiff():
#process c
func_dict = {'a' : do_a_stuff,
'b' : do_b_stuff,
'c' : do_c_stuff }
x = 'a'
func_dict[x]() *1*
- 1 从字典中运行函数
实际上,已经有人提出了(参见 PEP 275 和 PEP 3103)在 Python 中添加 case 语句的建议,但总体上共识是它不是必需的,也不值得麻烦。
8.3. 循环语句
Python 中的 for 循环与其他语言的 for 循环不同。传统的模式是在每次迭代中递增并测试一个变量,这正是 C 语言 for 循环通常所做的。在 Python 中,for 循环遍历任何可迭代对象返回的值——也就是说,任何可以产生值序列的对象。例如,for 循环可以遍历列表、元组或字符串中的每个元素。但可迭代对象也可以是一个特殊函数,称为 range 或一种特殊类型的函数,称为 生成器 或生成器表达式,这可以非常强大。其一般形式是
for item in sequence:
body
else:
post-code
body 对 sequence 的每个元素执行一次。item 被设置为 sequence 的第一个元素,然后执行 body;然后 item 被设置为 sequence 的第二个元素,然后执行 body,依此类推,直到 sequence 的每个剩余元素。
else 部分是可选的。就像 while 循环的 else 部分一样,它很少被使用。在 for 循环中,break 和 continue 与在 while 循环中做的是相同的事情。
这个小循环会打印出 x 中每个数的倒数:
x = [1.0, 2.0, 3.0]
for n in x:
print(1 / n)
8.3.1. range 函数
有时候,你需要使用显式索引进行循环(例如,列表中值出现的位置)。你可以使用 range 命令与列表上的 len 命令一起生成一个用于 for 循环的索引序列。以下代码会打印出列表中所有出现负数的位置:
x = [1, 3, -7, 4, 9, -5, 4]
for i in range(len(x)):
if x[i] < 0:
print("Found a negative number at index ", i)
给定一个数字n,range(n)返回一个序列 0, 1, 2, ..., n – 2, n – 1。所以传递列表的长度(使用len找到)会产生该列表元素的索引序列。range函数不会构建一个包含 100 万个元素的 Python 列表;它只是看起来如此。相反,它创建了一个 range 对象,按需生成整数。这在您使用显式循环遍历非常大的列表时很有用。例如,您不需要构建一个包含 1000 万个元素的列表,这将占用相当多的内存,您可以使用range(10000000),它只占用很少的内存,并且根据for循环的需要生成从 0 到(但不包括)10000000 的整数序列。
8.3.2. 使用起始值和步长值控制范围
您可以使用range函数的两个变体来获得对其产生的序列的更多控制。如果您使用两个数字参数的range,第一个参数是结果序列的起始数字,第二个数字是结果序列将到达的数字(但不包括)。以下是一些示例:
>>> list(range(3, 7)) *1*
[3, 4, 5, 6]
>>> list(range(2, 10)) *1*
[2, 3, 4, 5, 6, 7, 8, 9]
>>> list(range(5, 3))
[]
list()仅用于强制range生成的项以列表的形式出现。它通常不在实际代码中使用1。
这仍然不允许您反向计数,这就是为什么list(range(5, 3))的值是一个空列表的原因。要反向计数或按任何不是 1 的量计数,您需要使用range的第三个可选参数,它提供了一个步长值,计数就按这个步长值进行:
>>> list(range(0, 10, 2))
[0, 2, 4, 6, 8]
>>> list(range(5, 0, -1))
[5, 4, 3, 2, 1]
range返回的序列始终包含作为range参数给出的起始值,并且从不包含作为参数给出的结束值。
8.3.3. 在 for 循环中使用 break 和 continue
两个特殊语句break和continue也可以用在for循环体中。如果执行break,它将立即终止for循环,并且即使有else子句,也不会执行post-code。如果在for循环中执行continue,它将导致跳过body的剩余部分,并且循环将正常继续到下一个项目。
8.3.4. 循环和元组解包
您可以使用元组解包来使一些for循环更简洁。以下代码接受一个包含两个元素元组的列表,并计算每个元组中两个数字乘积之和的值(在某些领域中这是一种相当常见的数学运算):
somelist = [(1, 2), (3, 7), (9, 5)]
result = 0
for t in somelist:
result = result + (t[0] * t[1])
这里是同样的内容,但更简洁:
somelist = [(1, 2), (3, 7), (9, 5)]
result = 0
for x, y in somelist:
result = result + (x * y)
此代码在for关键字之后立即使用一个元组x, y,而不是通常的单个变量。在for循环的每次迭代中,x包含来自list的当前元组的第0个元素,而y包含来自list的当前元组的第1个元素。以这种方式使用元组是 Python 的一个便利之处,这样做表示 Python 期望列表中的每个元素都是一个适当大小的元组,可以解包到元组后面提到的变量名中。
8.3.5. enumerate 函数
你可以将元组解包与enumerate函数结合使用,以同时遍历项目和它们的索引。这与使用range类似,但它的优点是代码更清晰、更容易理解。像上一个示例一样,以下代码打印出列表中所有找到负数的位置:
x = [1, 3, -7, 4, 9, -5, 4]
for i, n in enumerate(x): *1*
if n < 0: *2*
print("Found a negative number at index ", i) *3*
enumerate函数返回包含(index, item)的元组1。你可以访问项目而不需要索引2。索引也是可用的3。
8.3.6. zip 函数
有时,在遍历它们之前将两个或多个可迭代对象组合起来是有用的。zip函数从一个或多个可迭代对象中取出相应的元素,并将它们组合成元组,直到达到最短可迭代对象的末尾:
>>> x = [1, 2, 3, 4]
>>> y = ['a', 'b', 'c'] *1*
>>> z = zip(x, y)
>>> list(z)
[(1, 'a'), (2, 'b'), (3, 'c')] *2*
-
1 y 有 3 个元素;x 有 4 个元素。
-
2 z 只有 3 个元素。
尝试这个:循环和 if 语句
假设你有一个列表x = [1, 3, 5, 0, -1, 3, -2],你需要从该列表中移除所有负数。请编写执行此操作的代码。
你会如何计算列表y = [[1, -1, 0], [2, 5, -9], [-2, -3, 0]]中负数的总数?
如果x的值小于-5,你会使用什么代码来打印very low,如果它在-5 到 0 之间,你会打印low,如果它等于 0,你会打印neutral,如果它大于 0 且小于等于 5,你会打印high,如果它大于 5,你会打印very high?
8.4. 列表和字典推导式
使用for循环遍历列表、修改或选择单个元素,并创建新的列表或字典的模式非常常见。这样的循环通常看起来像以下这样:
>>> x = [1, 2, 3, 4]
>>> x_squared = []
>>> for item in x:
... x_squared.append(item * item)
...
>>> x_squared
[1, 4, 9, 16]
这种情况如此常见,以至于 Python 为这种操作提供了一个特殊的快捷方式,称为列表推导式。你可以将列表或字典推导式视为一个单行for循环,它从一个序列中创建新的列表或字典。列表推导式的模式如下:
new_list = [expression1 for variable in old_list if expression2]
字典推导式看起来像这样:
new_dict = {expression1:expression2 for variable in list if expression3}
在这两种情况下,表达式的核心类似于for循环的开始——for variable in list——使用一些使用该变量的表达式来创建新的键或值,以及一个可选的条件表达式,使用变量的值来选择是否将其包含在新的列表或字典中。以下代码与之前的代码完全相同,但是一个列表推导式:
>>> x = [1, 2, 3, 4]
>>> x_squared = [item * item for item in x]
>>> x_squared
[1, 4, 9, 16]
你甚至可以使用if语句从列表中选择项目:
>>> x = [1, 2, 3, 4]
>>> x_squared = [item * item for item in x if item > 2]
>>> x_squared
[9, 16]
字典推导式类似,但你需要提供键和值。如果你想做一些类似于上一个示例的操作,但将数字作为键,将数字的平方作为字典中的值,你可以使用字典推导式,如下所示:
>>> x = [1, 2, 3, 4]
>>> x_squared_dict = {item: item * item for item in x}
>>> x_squared_dict
{1: 1, 2: 4, 3: 9, 4: 16}
列表和字典推导式非常灵活且强大,当你习惯使用它们时,它们会使列表处理操作变得更加简单。我建议你尝试使用它们,并在你发现自己正在编写用于处理项目列表的for循环时尝试使用它们。
8.4.1. 生成器表达式
生成器表达式与列表推导式类似。生成器表达式看起来很像列表推导式,除了它使用圆括号而不是方括号。以下示例是前面讨论的列表推导式的生成器表达式版本:
>>> x = [1, 2, 3, 4]
>>> x_squared = (item * item for item in x)
>>> x_squared
<generator object <genexpr> at 0x102176708>
>>> for square in x_squared:
... print(square,)
...
1 4 9 16
除了从方括号到圆括号的变化外,请注意,这个表达式不返回列表。相反,它返回一个生成器对象,可以用作for循环中的迭代器,如下所示,这与range()函数非常相似。使用生成器表达式的优点是,整个列表不需要在内存中生成,因此可以以很小的内存开销生成任意大的序列。
尝试这个:列表推导式
你会用什么列表推导式来处理列表x,以便移除所有负值?
创建一个生成器,只返回从 1 到 100 的奇数。(提示:如果一个数除以 2 有余数,那么这个数就是奇数;使用% 2来获取除以 2 的余数。)
编写代码以创建从 11 到 15 的数字及其立方体的字典。
8.5. 语句、块和缩进
因为在本章中遇到的控制流构造是第一个使用块和缩进的,现在是回顾这个主题的好时机。
Python 使用语句的缩进来确定不同控制流构造的不同块(或主体)的界限。一个块由一个或多个语句组成,通常由换行符分隔。Python 语句的例子包括赋值语句、函数调用、print函数、占位符pass语句和del语句。控制流构造(if-elif-else、while和for循环)是复合语句:
compound statement clause:
block
compound statement clause:
block
复合语句包含一个或多个子句,每个子句后面都跟着缩进的块。复合语句可以像任何其他语句一样出现在块中。当它们这样做时,会创建嵌套的块。
你还可能遇到一些特殊情况。如果多个语句由分号分隔,它们可以放在同一行上。一个包含单行的块可以放在复合语句子句的分号之后同一行上:
>>> x = 1; y = 0; z = 0
>>> if x > 0: y = 1; z = 10
... else: y = -1
...
>>> print(x, y, z)
1 1 10
不正确的缩进会导致抛出异常。你可能遇到两种形式的这种异常。第一种是
>>>
>>> x = 1
File "<stdin>", line 1
x = 1
^
IndentationError: unexpected indent
>>>
这段代码缩进了不应该缩进的行。在基本模式下,符号^表示问题发生的位置。在 IDLE Python shell 中(见图 8.1),无效的缩进会被突出显示。如果代码在必要的地方没有缩进(即复合语句子句之后的第一个行),也会出现相同的消息。
图 8.1. 缩进错误

这种情况可能让人困惑。如果你使用的是以四个空格为单位的制表符显示编辑器(或者 Windows 交互模式,其中第一个制表符从提示符开始缩进四个空格),并且用四个空格缩进一行,然后用制表符缩进下一行,这两行可能看起来处于相同的缩进级别。但你会收到这个异常,因为 Python 将制表符映射为八个空格。避免这种问题的最好方法是只使用空格在 Python 代码中。如果你必须使用制表符进行缩进,或者如果你正在处理使用制表符的代码,确保永远不要将它们与空格混合。
在基本交互模式和 IDLE Python 壳中,你可能已经注意到,在最外层缩进级别之后需要额外的空行:
>>> x = 1
>>> if x == 1:
... y = 2
... if v > 0:
... z = 2
... v = 0
...
>>> x = 2
在 z = 2 这一行后面不需要空行,但在 v = 0 这一行后面需要。如果你将代码放在文件中的模块里,这一行是不必要的。
如果在代码块中缩进一个语句少于合法的数量,就会发生第二种形式的异常:
>>> x = 1
>>> if x == 1:
y = 2
z = 2
File "<stdin>", line 3
z = 2
^
IndentationError: unindent does not match any outer indentation level
在这个例子中,包含 z = 2 的行没有正确地与包含 y = 2 的行对齐。这种形式很少见,但我再次提到它,因为在类似的情况下,它可能会让人困惑。
Python 允许你缩进任意数量,只要你在单个代码块内保持一致性,它就不会抱怨缩进的变化。请不要滥用这种灵活性。建议的标准是每级缩进使用四个空格。
在结束缩进之前,我将介绍如何在多行中拆分语句,当然,随着缩进级别的增加,这种情况更加常见。你可以通过使用反斜杠字符显式地拆分一行。你还可以在 ()、 {} 或 [] 分隔符内隐式地拆分任何语句(即,在输入列表、元组或字典中的值集;函数调用中的参数集;或括号内的任何表达式)。你可以根据需要将语句的续行缩进到任何级别:
>>> print('string1', 'string2', 'string3' \
... , 'string4', 'string5')
string1 string2 string3 string4 string5
>>> x = 100 + 200 + 300 \
... + 400 + 500
>>> x
1500
>>> v = [100, 300, 500, 700, 900,
... 1100, 1300]
>>> v
[100, 300, 500, 700, 900, 1100, 1300]
>>> max(1000, 300, 500,
... 800, 1200)
1200
>>> x = (100 + 200 + 300
... + 400 + 500)
>>> x
1500
你也可以使用反斜杠 \ 来中断字符串。但任何缩进的制表符或空格都成为字符串的一部分,并且该行必须以反斜杠结束。为了避免这种情况,请记住,任何由空白分隔的字符串字面量都会被 Python 解释器自动连接:
>>> "strings separated by whitespace " \
... """are automatically""" ' concatenated'
'strings separated by whitespace are automatically concatenated'
>>> x = 1
>>> if x > 0:
... string1 = "this string broken by a backslash will end up \
... with the indentation tabs in it"
...
>>> string1
'this string broken by a backslash will end up \t\t\twith
the indentation tabs in it'
>>> if x > 0:
... string1 = "this can be easily avoided by splitting the " \
... "string in this way"
...
>>> string1
'this can be easily avoided by splitting the string in this way'
8.6. 布尔值和表达式
之前关于控制流的例子以相当明显的方式使用了条件测试,但从未真正解释在 Python 中什么是真或假,或者需要条件测试时可以使用哪些表达式。本节描述了 Python 的这些方面。
Python 有一个布尔对象类型,可以设置为 True 或 False。任何带有布尔操作的运算表达式都会返回 True 或 False。
8.6.1. 大多数 Python 对象都可以用作布尔值
此外,Python 在布尔值方面与 C 类似,即 C 使用整数 0 表示假,而任何其他整数表示真。Python 将这一概念推广:0 或空值是 False,而任何其他值都是 True。从实际意义上讲,这意味着以下内容:
-
数字
0、0.0和0+0j都是False;任何其他数字都是True。 -
空字符串
""是False;任何其他字符串都是True。 -
空列表
[]是False;任何其他列表都是True。 -
空字典
{}是False;任何其他字典都是True。 -
空集合
set()是False;任何其他集合都是True。 -
特殊的 Python 值
None总是False。
我们还没有查看一些 Python 数据结构,但通常,相同的规则适用。如果数据结构为空或 0,则在布尔上下文中被认为是假;否则,被认为是真。某些对象,如文件对象和代码对象,没有合理的 0 或空元素的定义,这些对象不应在布尔上下文中使用。
8.6.2. 比较和布尔运算符
您可以使用常规运算符比较对象:<, <=, >, >= 等等。== 是相等测试运算符,而 != 是“不等于”测试。还有 in 和 not in 运算符用于测试序列(列表、元组、字符串和字典)中的成员资格,以及 is 和 is not 运算符用于测试两个对象是否相同。
返回布尔值的表达式可以使用 and、or 和 not 运算符组合成更复杂的表达式。以下代码片段检查一个变量是否在某个范围内:
if 0 < x and x < 10:
...
Python 为这种特定类型的复合语句提供了一种简洁的缩写。您可以像在数学论文中那样编写它:
if 0 < x < 10:
...
优先级规则适用;如有疑问,您可以使用括号确保 Python 以您期望的方式解释表达式。使用括号可能是复杂表达式的良好做法,无论是否必要,因为它可以使未来的代码维护者清楚地了解正在发生的事情。有关优先级的更多详细信息,请参阅 Python 文档。
本节剩余部分提供更高级的信息。如果您在学习语言的同时阅读这本书,您可能现在想跳过这部分内容。
and 和 or 运算符返回对象。and 运算符返回第一个为假的对象(表达式评估到的)或最后一个对象。同样,or 运算符返回第一个为真的对象或最后一个对象。这可能会有些令人困惑,但它工作得正确;如果 and 运算符的表达式中有任何一个是假的,那么这个元素就会使整个表达式评估为 False,并返回这个 False 值。如果所有元素都是 True,则表达式为 True,并返回最后一个值,这个值也必须是 True。对于 or 运算符,情况相反;只有一个 True 元素会使语句在逻辑上为 True,并返回找到的第一个 True 值。如果没有找到 True 值,则返回最后一个(False)值。换句话说,就像许多其他语言一样,对于 or 运算符,一旦找到真表达式,评估就会停止;对于 and 运算符,一旦找到假表达式,评估就会停止:
>>> [2] and [3, 4]
[3, 4]
>>> [] and 5
[]
>>> [2] or [3, 4]
[2]
>>> [] or 5
5
>>>
== 和 != 运算符用于测试它们的操作数是否包含相同的值。== 和 != 在大多数情况下使用,而不是 is 和 is not 运算符,后者用于测试操作数是否是同一个对象:
>>> x = [0]
>>> y = [x, 1]
>>> x is y[0] *1*
True
>>> x = [0] *2*
>>> x is y[0]
False
>>> x == y[0]
True
-
1 它们引用的是同一个对象。
-
2 x 被分配给了不同的对象。
如果这个例子对你来说不清楚,请回顾 第 5.6 节,“嵌套列表和深拷贝。”
快速检查:布尔值和真值
判断以下语句是否为真:1, 0, -1, [0], 1 and 0, 1 > 0 or [].
8.7. 编写一个简单的程序来分析文本文件
为了更好地了解 Python 程序的工作方式,本节将查看一个小样本,该样本大致复制了 UNIX wc 工具,并报告文件中的行数、单词数和字符数。这个列表中的样本故意写成对 Python 新手程序员来说尽可能清晰,并且尽可能简单。
列表 8.1. word_count.py
#!/usr/bin/env python3
""" Reads a file and returns the number of lines, words,
and characters - similar to the UNIX wc utility
"""
infile = open('word_count.tst') *1*
lines = infile.read().split("\n") *2*
line_count = len(lines) *3*
word_count = 0 *4*
char_count = 0 *4*
for line in lines: *5*
words = line.split() *6*
word_count += len(words)
char_count += len(line) *7*
print("File has {0} lines, {1} words, {2} characters".format *8*
(line_count, word_count, char_count)) *8*
-
1 打开文件
-
2 读取文件;分割成行
-
3 使用 len() 获取行数
-
4 初始化其他计数
-
5 遍历行
-
6 分割成单词
-
7 返回字符数
-
8 打印答案
为了测试,你可以运行这个示例,针对包含本章总结第一段内容的样本文件,如下所示。
列表 8.2. word_count.tst
Python provides a complete set of control flow elements,
including while and for loops, and conditionals.
Python uses the level of indentation to group blocks
of code with control elements.
运行 word_count.py 后,你会得到以下输出:
naomi@mac:~/quickpythonbook/code $ python3.1 word_count.py
File has 4 lines, 30 words, 189 characters
这段代码可以给你一个 Python 程序的思路。代码并不多,大部分工作都在 for 循环的三个代码行中完成。实际上,这个程序可以变得更短、更符合 Python 习惯。大多数 Python 程序员都认为这种简洁性是 Python 的一个伟大优势。
实验 8:重构 word_count
将第 8.7 节(section 8.7)中的单词计数程序重写以使其更短。你可能需要查看已经讨论过的字符串和列表操作,以及考虑不同的代码组织方式。你可能还想使程序更智能,以便只有字母字符串(不是符号或标点)被计算为单词。
摘要
-
Python 使用缩进来分组代码块。
-
Python 使用
while和for循环,以及if-elif-else条件语句。 -
Python 有布尔值
True和False,可以通过变量来引用。 -
Python 还将任何 0 或空值视为
False,任何非零或非空值视为True。
第九章 函数
本章涵盖
-
定义函数
-
使用函数参数
-
将可变对象作为参数传递
-
理解局部和全局变量
-
创建和使用生成器函数
-
创建和使用 lambda 表达式
-
使用装饰器
本章假设您至少熟悉一种其他计算机语言中的函数定义以及与函数定义、参数等相对应的概念。
9.1. 基本函数定义
Python 函数定义的基本语法是
def name(parameter1, parameter2, . . .):
body
就像控制结构一样,Python 使用缩进来限定函数定义的主体。以下简单示例将之前章节中的阶乘代码放入函数体中,这样您就可以通过调用 fact 函数来获取一个数的阶乘:
>>> def fact(n):
... """Return the factorial of the given number.""" *1*
... r = 1
... while n > 0:
... r = r * n
... n = n - 1
... return r *2*
...
第二行 1 是可选的 文档字符串,或 docstring。您可以通过打印 fact.__doc__ 来获取其值。文档字符串的目的是描述函数的外部行为及其参数,而注释应记录有关代码内部工作方式的信息。文档字符串是紧跟在函数定义第一行之后的字符串,通常使用三引号来允许多行描述。有浏览工具可以提取文档字符串的第一行。对于多行文档字符串,标准做法是在第一行给出函数的概述,接着是一个空行,然后是其余信息。这一行表明返回值之后将返回给调用函数的代码 2。
过程或函数?
在某些语言中,不返回值的函数被称为 过程。尽管您可以(并将会)编写没有 return 语句的函数,但它们并不是真正的过程。所有 Python 过程都是函数;如果在过程体中没有执行显式的 return,则返回特殊的 Python 值 None,如果执行了 return arg,则立即返回值 arg。在执行 return 之后,函数体中的其他内容不再执行。因为 Python 没有真正的过程,所以我会将这两种类型都称为 函数。
尽管所有 Python 函数都返回值,但您可以选择是否使用函数的返回值:
>>> fact(4) *1*
24 *2*
>>> x = fact(4) *3*
>>> x
24
>>>
返回值与变量无关 1。fact 函数的值仅在解释器中打印 2。返回值与变量 x 相关联 3。
9.2. 函数参数选项
大多数函数需要参数,每种语言都有自己的函数参数定义规范。Python 非常灵活,提供了三种定义函数参数的选项。这些选项在本节中概述。
9.2.1. 位置参数
在 Python 中向函数传递参数的最简单方法是按位置传递。在函数的第一行中,您为每个参数指定变量名;当函数被调用时,调用代码中使用的参数根据它们的顺序与函数的参数变量匹配。以下函数计算 x 的 y 次幂:
>>> def power(x, y):
... r = 1
... while y > 0:
... r = r * x
... y = y - 1
... return r
...
>>> power(3, 3)
27
此方法要求调用代码使用的参数数量与函数定义中的参数数量完全匹配;否则,将引发 TypeError 异常:
>>> power(3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: power() missing 1 required positional argument: 'y'
>>>
默认值
函数参数可以具有默认值,您可以通过在函数定义的第一行分配默认值来声明,如下所示:
def fun(arg1, arg2=default2, arg3=default3, . . .)
可以为任意数量的参数指定默认值。具有默认值的参数必须在参数列表的末尾定义,因为 Python,像大多数语言一样,根据位置将参数与参数配对。必须向函数提供足够的参数,使得该函数参数列表中最后一个没有默认值的参数获得一个参数。有关更灵活的机制,请参阅第 9.2.2 节,“通过参数名传递参数。”
以下函数也计算 x 的 y 次幂。但如果在函数调用中没有给出 y,则使用默认值 2,该函数只是平方函数:
>>> def power(x, y=2):
... r = 1
... while y > 0:
... r = r * x
... y = y - 1
... return r
...
您可以在以下交互会话中看到默认参数的效果:
>>> power(3, 3)
27
>>> power(3)
9
9.2.2. 通过参数名传递参数
您还可以通过使用相应的函数参数的名称而不是其位置来将参数传递给函数。继续使用之前的交互示例,您可以输入
>>> power(2, 3)
8
>>> power(3, 2)
9
>>> power(y=2, x=3)
9
因为在最终调用 power 的参数是命名的,它们的顺序无关紧要;参数与 power 定义中同名参数相关联,并返回 3²。这种参数传递方式称为 关键字 传递。
关键字传递与 Python 函数的默认参数功能结合使用时,在定义具有大量可能参数的函数时非常有用,其中大多数参数具有常见的默认值。考虑一个旨在生成关于当前目录中文件信息的列表的函数,并使用布尔参数来指示该列表是否应包括有关每个文件的信息,例如文件大小、最后修改日期等。您可以按照以下方式定义此类函数
def list_file_info(size=False, create_date=False, mod_date=False, ...):
...get file names...
if size:
# code to get file sizes goes here
if create_date:
# code to get create dates goes here
# do any other stuff desired
return fileinfostructure
然后可以通过使用关键字参数传递从其他代码中调用它,以指示您只想获取某些信息(在这个例子中,是文件大小和修改日期,但不是创建日期):
fileinfo = list_file_info(size=True, mod_date=True)
这种类型的参数处理特别适合于具有非常复杂行为的函数,这类函数出现在图形用户界面(GUI)中。如果你曾经使用 Tkinter 包在 Python 中构建 GUI,你会发现使用这种可选的、以关键字命名的参数非常有价值。
9.2.3. 可变数量的参数
Python 函数也可以定义为处理可变数量的参数,这可以通过两种方式实现。一种方式处理相对熟悉的案例,即你想要将未知数量的参数收集到参数列表的末尾到一个列表中。另一种方法可以收集任意数量的关键字传递参数,这些参数在函数参数列表中没有相应的命名参数。接下来将讨论这两种机制。
处理通过位置传递的不定数量参数
在函数的最后一个参数名称前加上 * 前缀会导致在函数调用中收集所有多余的、非关键字参数(即未分配给其他参数的位置参数)并作为一个元组分配给指定的参数。以下是一个实现查找数字列表中最大值的函数的简单方法。
首先,实现该函数:
>>> def maximum(*numbers):
... if len(numbers) == 0:
... return None
... else:
... maxnum = numbers[0]
... for n in numbers[1:]:
... if n > maxnum:
... maxnum = n
... return maxnum
...
现在测试函数的行为:
>>> maximum(3, 2, 8)
8
>>> maximum(1, 5, 9, -2, 2)
9
处理通过关键字传递的不定数量参数
可以处理任意数量的关键字参数。如果参数列表中的最后一个参数以 ** 前缀开头,它将所有多余的 关键字传递 参数收集到一个字典中。字典中每个条目的键是多余参数的关键字(参数名称),该条目的值是参数本身。如果传递参数的关键字与函数定义中的参数名称不匹配,则在这种情况下通过关键字传递的参数是多余的。
例如:
>>> def example_fun(x, y, **other):
... print("x: {0}, y: {1}, keys in 'other': {2}".format(x,
... y, list(other.keys())))
... other_total = 0
... for k in other.keys():
... other_total = other_total + other[k]
... print("The total of values in 'other' is {0}".format(other_total))
在交互式会话中尝试此函数显示,它可以处理在关键字 foo 和 bar 下传递的参数,即使 foo 和 bar 不是函数定义中的参数名称:
>>> example_fun(2, y="1", foo=3, bar=4)
x: 2, y: 1, keys in 'other': ['foo', 'bar']
The total of values in 'other' is 7
9.2.4. 混合参数传递技术
可以同时使用 Python 函数的所有参数传递功能,尽管如果不小心使用可能会令人困惑。使用混合参数传递的一般规则是,位置参数先于命名参数,然后是不定位置参数(单个 *),最后是不定关键字参数 **。有关完整详情,请参阅文档。
快速检查:函数和参数
你将如何编写一个函数,它可以接受任意数量的未命名参数,并按相反顺序打印它们的值?
你需要做什么来创建一个过程或无返回值的函数?
如果你捕获了函数的返回值变量会发生什么?
9.3. 可变对象作为参数
参数是通过对象引用传递的。参数成为指向对象的新的引用。对于不可变对象(如元组、字符串和数字),对参数所做的操作在函数外部没有效果。但是,如果你传递一个可变对象(如列表、字典或类实例),对对象所做的任何更改都会改变函数外部参数所引用的内容。重新分配参数不会影响参数,如图 9.1 和 9.2 所示:
>>> def f(n, list1, list2):
... list1.append(3)
... list2 = [4, 5, 6]
... n = n + 1
...
>>> x = 5
>>> y = [1, 2]
>>> z = [4, 5]
>>> f(x, y, z)
>>> x, y, z
(5, [1, 2, 3], [4, 5])
图 9.1 和 9.2 展示了当调用函数 f 时会发生什么。变量 x 没有改变,因为它是不变的。相反,函数参数 n 被设置为引用新的值 6。同样,变量 z 没有改变,因为在函数 f 内部,它对应的参数 list2 被设置为引用一个新的对象 [4, 5, 6]。只有 y 看到了变化,因为指向其实际列表的引用被改变了。
图 9.1。在函数 f() 的开始时,初始变量和函数参数都指向相同的对象。

图 9.2。在函数 f() 结束时,函数内部的 y(list1)已被内部更改,而 n 和 list2 指向不同的对象。

快速检查:可变函数参数
如果改变作为参数值传递给函数的列表或字典,结果会是什么?哪些操作可能会创建在函数外部可见的更改?你可以采取哪些步骤来最小化这种风险?
9.4。局部、非局部和全局变量
这里,你回到本章开头对 fact 的定义:
def fact(n):
"""Return the factorial of the given number."""
r = 1
while n > 0:
r = r * n
n = n - 1
return r
变量 r 和 n 都是对任何特定的阶乘函数调用来说是局部的;在函数执行时对它们的更改不会影响函数外的任何变量。函数参数列表中的任何变量,以及函数内部通过赋值(如 fact 中的 r = 1)创建的任何变量,都是局部于函数的。
你可以通过在变量使用之前声明它来显式地将变量设置为全局变量,使用 global 语句。全局变量可以被函数访问和修改。它们存在于函数外部,也可以被声明为全局的其他函数或不在函数内的代码访问和修改。以下是一个显示局部和全局变量之间差异的示例:
>>> def fun():
... global a
... a = 1
... b = 2
...
此示例定义了一个函数,它将 a 作为全局变量处理,将 b 作为局部变量,并尝试修改这两个变量。
现在测试这个函数:
>>> a = "one"
>>> b = "two"
>>> fun()
>>> a
1
>>> b
'two'
在fun中赋值给a是对存在于fun外部的全局变量a的赋值。因为a在fun中被指定为global,所以赋值会修改该全局变量以保持值1而不是值"one"。对于b来说并不相同;在fun内部称为b的局部变量最初指向与fun外部的变量b相同的值,但赋值导致b指向一个新的值,该值是函数fun的局部值。
与global语句类似的是nonlocal语句,它使一个标识符引用最接近的封闭作用域中之前绑定的变量。我在第十章中更详细地讨论了作用域和命名空间,但重点是global用于顶层变量,而nonlocal可以引用封闭作用域中的任何变量,如列表 9.1 中的示例所示。
列表 9.1. 文件 nonlocal.py
g_var = 0 *1*
nl_var = 0 *1*
print("top level-> g_var: {0} nl_var: {1}".format(g_var, nl_var))
def test():
nl_var = 2 *2*
print("in test-> g_var: {0} nl_var: {1}".format(g_var, nl_var))
def inner_test():
global g_var *3*
nonlocal nl_var *4*
g_var = 1
nl_var = 4
print("in inner_test-> g_var: {0} nl_var: {1}".format(g_var,
nl_var))
inner_test()
print("in test-> g_var: {0} nl_var: {1}".format(g_var, nl_var))
test()
print("top level-> g_var: {0} nl_var: {1}".format(g_var, nl_var))
-
1 内部 _test 中的 g_var 绑定到顶层 g_var。
-
2 内部 _test 中的 nl_var 绑定到 test 中的 nl_var。
-
3 内部 _test 中的 g_var 绑定到顶层 g_var。
-
4 内部 _test 中的 nl_var 绑定到 test 中的 nl_var。
当运行时,此代码将打印以下内容:
top level-> g_var: 0 nl_var: 0
in test-> g_var: 0 nl_var: 2
in inner_test-> g_var: 1 nl_var: 4
in test-> g_var: 1 nl_var: 4
top level-> g_var: 1 nl_var: 0
注意,顶层nl_var的值没有受到影响,如果inner_test包含global nl_var这一行,就会发生这种情况。
核心原则是,如果你想要将变量分配给函数外部存在的变量,你必须明确声明该变量为非局部或全局变量。但是,如果你正在访问函数外部存在的变量,你不需要声明它为非局部或全局。如果 Python 在局部函数作用域中找不到变量名,它会尝试在全局作用域中查找该名称。因此,对全局变量的访问会自动传递到正确的全局变量。我个人不推荐使用这个快捷方式。如果所有全局变量都明确声明为全局,对读者来说会更清晰。此外,你可能希望将函数内部的全局变量使用限制在罕见场合。
尝试这个:全局变量与局部变量
假设x = 5,在funct_1()执行后,x的值将会是什么?在funct_2()执行后?
def funct_1():
x = 3
def funct_2():
global x
x = 2
9.5. 将函数分配给变量
函数可以像其他 Python 对象一样分配给变量,如本例所示:
>>> def f_to_kelvin(degrees_f): *1*
... return 273.15 + (degrees_f - 32) * 5 / 9
...
>>> def c_to_kelvin(degrees_c): *2*
... return 273.15 + degrees_c
...
>>> abs_temperature = f_to_kelvin *3*
>>> abs_temperature(32)
273.15
>>> abs_temperature = c_to_kelvin *3*
>>> abs_temperature(0)
273.15
-
1 定义 f_to_kelvin kelvin 函数
-
2 定义 c_to_kelvin 函数
-
3 将函数分配给变量
你可以将函数放入列表、元组或字典中:
>>> t = {'FtoK': f_to_kelvin, 'CtoK': c_to_kelvin} *1*
>>> t'FtoK' *2*
273.15
>>> t'CtoK' *3*
273.15
-
2 将 f_to_kelvin 函数作为字典中的值访问
-
3 将 c_to_kelvin 函数作为字典中的值访问
一个指向函数的变量可以像函数 1 一样使用。最后一个示例展示了如何使用字典通过用作键的字符串值调用不同的函数。这种模式在需要根据字符串值选择不同函数的情况下很常见,并且在许多情况下,它取代了 C 和 Java 等语言中找到的 switch 结构。
9.6. lambda 表达式
就像您刚才看到的短函数一样,也可以使用以下形式的 lambda 表达式来定义:
lambda parameter1, parameter2, . . .: expression
lambda 表达式是匿名的小函数,您可以在行内快速定义。通常,需要将一个小函数传递给另一个函数,例如列表排序方法中使用的键函数。在这种情况下,通常不需要大函数,并且需要在使用它的地方之外定义函数会显得很尴尬。前一小节中的字典可以在一个地方全部定义:
>>> t2 = {'FtoK': lambda deg_f: 273.15 + (deg_f - 32) * 5 / 9,
... 'CtoK': lambda deg_c: 273.15 + deg_c} *1*
>>> t2'FtoK'
273.15
此示例将 lambda 表达式定义为字典的值 1。请注意,lambda 表达式没有 return 语句,因为表达式的值会自动返回。
9.7. 生成器函数
生成器 函数是一种特殊的函数,您可以使用它来定义自己的迭代器。当您定义生成器函数时,您使用 yield 关键字返回每次迭代的值。当没有更多的迭代或遇到空的 return 语句或函数的末尾时,生成器将停止返回值。与普通函数不同,生成器函数中的局部变量在每次调用之间被保存:
>>> def four():
... x = 0 *1*
... while x < 4:
... print("in generator, x =", x)
... yield x *2*
... x += 1 *3*
...
>>> for i in four():
... print(i)
...
in generator, x = 0
0
in generator, x = 1
1
in generator, x = 2
2
in generator, x = 3
3
-
1 将 x 的初始值设置为 0
-
2 返回 x 的当前值
-
3 增加 x 的值
注意,此生成器函数有一个 while 循环,限制了生成器执行的次数。根据其使用方式,没有停止条件的生成器在调用时可能会导致无限循环。
yield 与 yield from
从 Python 3.3 开始,新的生成器关键字 yield from 与 yield 结合。基本上,yield from 使得将生成器连接起来成为可能。yield from 的行为与 yield 相同,但它将生成器机制委托给子生成器。所以,在简单的情况下,你可以这样做:
>>> def subgen(x):
... for i in range(x):
... yield i
...
>>> def gen(y):
... yield from subgen(y)
...
>>> for q in gen(6):
... print(q)
...
0
1
2
3
4
5
此示例允许将 yield 表达式移出主生成器,使重构更容易。
您还可以使用 in 与生成器函数一起使用,以查看值是否在生成器产生的序列中:
>>> 2 in four()
in generator, x = 0
in generator, x = 1
in generator, x = 2
True
>>> 5 in four()
in generator, x = 0
in generator, x = 1
in generator, x = 2
in generator, x = 3
False
快速检查:生成器函数
你需要修改前面的代码中的哪些部分才能使函数 four() 对任何数字都有效?你需要添加什么才能允许设置起始点?
9.8. 装饰器
由于函数在 Python 中是一等对象,因此它们可以被分配给变量,正如您所看到的。函数还可以作为参数传递给其他函数,并作为其他函数的返回值传递。
例如,可以编写一个 Python 函数,该函数接受另一个函数作为其参数,将其包装在执行相关操作的另一个函数中,然后返回新的函数。这个新的组合可以用作原始函数的替代:
>>> def decorate(func):
... print("in decorate function, decorating", func.__name__)
... def wrapper_func(*args):
... print("Executing", func.__name__)
... return func(*args)
... return wrapper_func
...
>>> def myfunction(parameter):
... print(parameter)
...
>>> myfunction = decorate(myfunction)
in decorate function, decorating myfunction
>>> myfunction("hello")
Executing myfunction
hello
装饰器是这个过程的语法糖,它允许您通过一行代码将一个函数包装在另一个函数中。它仍然提供了与之前代码完全相同的效果,但生成的代码更加简洁且易于阅读。
简单来说,使用装饰器涉及两个部分:定义将要包装或“装饰”其他函数的函数,然后在定义包装函数之前立即使用一个@符号后跟装饰器。装饰器函数应该接受一个函数作为参数并返回一个函数,如下所示:
>>> def decorate(func):
... print("in decorate function, decorating", func.__name__) *1*
... def wrapper_func(*args):
... print("Executing", func.__name__)
... return func(*args)
... return wrapper_func *2*
...
>>> @decorate *3*
... def myfunction(parameter):
... print(parameter)
...
in decorate function, decorating myfunction *4*
>>> myfunction("hello")
Executing myfunction
hello
decorate函数在定义函数时打印它所包装的函数名称 1。当它完成时,装饰器返回包装函数 2。myfunction使用@decorate进行装饰 3。在装饰器函数完成后,调用包装函数 4。
使用装饰器将一个函数包装在另一个函数中,对于多个目的来说可能很有用。在像 Django 这样的 Web 框架中,装饰器用于确保在执行函数之前用户已登录;在图形库中,装饰器可以用来将函数注册到图形框架中。
尝试这个:装饰器
如何修改装饰器函数的代码以删除不需要的消息,并将包装函数的返回值封装在"<html>"和"</html>"中,以便myfunction ("hello")返回"<html>hello<html>"?
实验室 9:有用的函数
回顾第六章和第七章的实验室,将代码重构为用于清理和处理数据的函数。目标应该是将大部分逻辑移动到函数中。根据您的判断选择函数类型和参数,但请记住,函数应该只做一件事,并且它们不应该有任何在函数外部延续的副作用。
摘要
-
可以通过使用
global语句轻松在函数内部访问外部变量。 -
参数可以通过位置或通过参数名称传递。
-
函数参数可以提供默认值。
-
函数可以将参数收集到元组中,这使您能够定义接受不定数量参数的函数。
-
函数可以将参数收集到字典中,这使您能够定义接受通过参数名称传递的不定数量参数的函数。
-
函数在 Python 中是一等对象,这意味着它们可以被分配给变量,通过变量访问,并且可以装饰。
第十章. 模块和作用域规则
本章涵盖:
-
定义模块
-
编写第一个模块
-
使用
import语句 -
修改模块搜索路径
-
在模块中使名称私有
-
导入标准库和第三方模块
-
理解 Python 作用域规则和命名空间
模块用于组织更大的 Python 项目。Python 标准库被分割成模块以使其更易于管理。你不需要将你的代码组织成模块,但如果你在编写超过几页的任何程序或任何你想要重用的代码,你可能需要这样做。
10.1. 什么是模块?
模块 是一个包含代码的文件。它定义了一组 Python 函数或其他对象,模块的名称来自文件的名称。
模块通常包含 Python 源代码,但它们也可以是编译后的 C 或 C++ 对象文件。编译模块和 Python 源模块的使用方式相同。
除了分组相关的 Python 对象外,模块还有助于避免命名冲突问题。你可能为你的程序编写一个名为 mymodule 的模块,该模块定义了一个名为 reverse 的函数。在同一个程序中,你也可能想要使用另一个名为 othermodule 的模块,该模块也定义了一个名为 reverse 的函数,但与你的 reverse 函数执行不同的操作。在没有模块的语言中,使用两个不同名称的 reverse 函数是不可能的。在 Python 中,这个过程非常简单;你通过在主程序中引用函数作为 mymodule.reverse 和 othermodule.reverse 来引用这些函数。
使用模块名称可以保持两个 reverse 函数清晰,因为 Python 使用命名空间。命名空间 实质上是一个包含一个代码块、函数、类、模块等可访问标识符的字典。我在本章末尾对命名空间进行了更多讨论,但请注意,每个模块都有自己的命名空间,这有助于防止命名冲突。
模块还用于使 Python 本身更易于管理。大多数标准 Python 函数并没有内置到语言的核心中,而是通过特定的模块提供,你可以按需加载这些模块。
10.2. 第一个模块
了解模块的最佳方式可能是自己创建一个,所以你将在本节中开始。
创建一个名为 mymath.py 的文本文件,并在该文本文件中输入 列表 10.1 中的 Python 代码。(如果你使用的是 IDLE,请选择文件 > 新窗口并开始输入,如图 图 10.1 所示。)
列表 10.1. 文件 mymath.py
"""mymath - our example math module"""
pi = 3.14159
def area(r):
"""area(r): return the area of a circle with radius r."""
global pi
return(pi * r * r)
图 10.1. IDLE 编辑窗口提供了与 shell 窗口相同的编辑功能,包括自动缩进和着色。

将此代码暂时保存在你的 Python 可执行文件所在的目录中。此代码仅将 pi 赋予一个值并定义一个函数。强烈建议所有 Python 代码文件使用 .py 文件名后缀;它将文件标识为 Python 源代码。与函数一样,你还有选择在模块的第一行放置文档字符串。
现在启动 Python 壳,并输入以下内容:
>>> pi
Traceback (innermost last):
File "<stdin>", line 1, in ?
NameError: name 'pi' is not defined
>>> area(2)
Traceback (innermost last):
File "<stdin>", line 1, in ?
NameError: name 'area' is not defined
换句话说,Python 并没有内置的常量 pi 或函数 area。
现在输入
>>> import mymath
>>> pi
Traceback (innermost last):
File "<stdin>", line 1, in ?
NameError: name 'pi' is not defined
>>> mymath.pi
3.14159
>>> mymath.area(2)
12.56636
>>> mymath.__doc__
'mymath - our example math module'
>>> mymath.area.__doc__
'area(r): return the area of a circle with radius r.'
你已经从 mymath.py 文件中引入了 pi 和 area 的定义,使用了 import 语句(当它搜索名为 mymath 的模块定义文件时会自动添加 .py 后缀)。但是,新的定义不能直接访问;单独输入 pi 会报错,单独输入 area(2) 也会报错。相反,你需要通过在它们前面加上包含它们的模块的名称来访问 pi 和 area,这保证了名称的安全性。另一个模块也可能定义了 pi(也许那个模块的作者认为 pi 是 3.14 或 3.14159265),但那个模块并不重要。即使那个其他模块被导入,它的 pi 版本也将通过 othermodulename.pi 访问,这与 mymath.pi 不同。这种访问方式通常被称为 限定(即变量 pi 正在被模块 mymath 限定)。你也可以将 pi 称为 mymath 的 属性。
模块内的定义可以访问该模块内的其他定义,而无需前置模块名称。mymath.area 函数通过 pi 访问 mymath.pi 常量。
如果你想,你也可以以这种方式特别请求从模块中导入名称,这样你就不需要用模块名称来前置它们。输入
>>> from mymath import pi
>>> pi
3.14159
>>> area(2)
Traceback (innermost last):
File "<stdin>", line 1, in ?
NameError: name 'area' is not defined
由于你通过使用 from mymath import pi 特意请求了它,pi 的名称现在可以直接访问。尽管如此,函数 area 仍然需要以 mymath .area 的方式调用,因为它没有被明确导入。
你可能想使用基本的交互模式或 IDLE 的 Python 壳来逐步测试你创建的模块。但是,如果你在磁盘上更改了你的模块,重新输入 import 命令不会使其再次加载。你需要使用 importlib 模块中的 reload 函数来完成此操作。importlib 模块提供了一个接口,用于导入模块背后的机制:
>>> import mymath, importlib
>>> importlib.reload(mymath)
<module 'mymath' from '/home/doc/quickpythonbook/code/mymath.py'>
当一个模块被重新加载(或首次导入)时,它的所有代码都会被解析。如果发现错误,将引发语法异常。另一方面,如果一切正常,将创建一个包含 Python 字节码的 .pyc 文件(例如,mymath.pyc)。
重新加载模块不会使您回到与您开始新会话并首次导入时完全相同的情况。但通常这些差异不会给您带来任何问题。如果您感兴趣,可以在 Python 语言参考 的 importlib 模块部分中查找 reload,该部分位于本页的 importlib 部分 docs.python.org/3/reference/import.html。
当然,模块不仅可以从交互式 Python 命令行使用。您也可以将它们导入到脚本(或其他模块)中;在程序文件的开头输入合适的 import 语句。在 Python 内部,交互式会话和脚本也被视为模块。
总结如下:
-
模块是一个定义 Python 对象的文件。
-
如果模块文件的名称是
modulename.py,则该模块的 Python 名称是modulename。 -
您可以使用
import modulename语句将名为modulename的模块投入使用。执行此语句后,模块中定义的对象可以通过modulename.objectname访问。 -
您可以使用
from modulename import objectname语句直接将模块中的特定名称引入到您的程序中。此语句使objectname对您的程序可用,而无需在前面加上modulename,这对于引入经常使用的名称非常有用。
10.3. 导入语句
import 语句有三种不同的形式。最基本的是
import modulename
它会搜索给定名称的 Python 模块,解析其内容,并使其可用。导入的代码可以使用模块的内容,但该代码对模块内名称的任何引用都必须在前面加上模块名称。如果找不到指定的模块,将生成错误。我将在第 10.4 节中详细讨论 Python 查找模块的位置。
第二种形式允许从模块中显式导入特定的名称到代码中:
from modulename import name1, name2, name3, . . .
modulename 内的 name1、name2 等名称都可供导入的代码使用;在 import 语句之后,代码可以使用 name1、name2、name3 等等,而无需在前面加上模块名称。
最后,from ... import ... 语句有一个通用形式:
from modulename import *
* 代表 modulename 中所有导出的名称。from modulename import * 导入了 modulename 中的所有公共名称——即不以下划线开头的名称——并使它们对导入的代码可用,而无需在前面加上模块名称。但如果模块(或包的 __init__.py)中存在名为 __all__ 的名称列表,则导入的名称就是这些,无论它们是否以下划线开头。
在使用这种特定的导入形式时,您应该小心。如果两个模块都定义了同一个名称,并且您使用这种导入形式导入这两个模块,您最终会遇到名称冲突,第二个模块的名称将替换第一个模块的名称。这种技术也使得读者更难确定您使用的名称的来源。当您使用两种之前的导入语句形式之一时,您向您的读者提供了关于它们来源的明确信息。
但某些模块(如 tkinter)命名它们的函数,以便清楚地表明它们的来源,并减少名称冲突的可能性。在交互式外壳中使用时,也常用通用导入来节省按键。
10.4. 模块搜索路径
Python 查找模块的确切位置由一个名为 path 的变量定义,您可以通过一个名为 sys 的模块来访问它。输入以下内容:
>>> import sys
>>> sys.path
_list of directories in the search path_
显示在 _ 搜索路径中的目录列表 _ 位置上的值取决于您系统的配置。无论细节如何,该字符串表示 Python 在尝试执行 import 语句时按顺序搜索的目录列表。找到的第一个满足 import 请求的模块被使用。如果在模块搜索路径中没有找到满意的模块,将引发 ImportError 异常。
如果您正在使用 IDLE,您可以通过路径浏览器窗口图形化地查看搜索路径及其上的模块,您可以从 Python 壳窗口的文件菜单启动它。
如果存在,sys.path 变量将从环境(操作系统)变量 PYTHONPATH 的值初始化,或者从依赖于您安装的默认值初始化。此外,每次您运行一个 Python 脚本时,该脚本的 sys.path 变量都会将包含脚本的目录作为其第一个元素插入,这为确定执行中的 Python 程序的位置提供了一个方便的方法。在一个如前所述的交互式会话中,sys.path 的第一个元素被设置为空字符串,Python 将其视为应在当前目录中首先查找模块。
10.4.1. 将您的模块放置在哪里
在本章开始的示例中,mymath 模块对 Python 可用,因为(1)当您以交互方式执行 Python 时,sys.path 的第一个元素是 "",告诉 Python 在当前目录中查找模块;(2)您在包含 mymath.py 文件的目录中执行了 Python。在生产环境中,这两个条件通常都不成立。您不会以交互方式运行 Python,Python 代码文件也不会位于您的当前目录中。为了确保您的程序可以使用您编写的模块,您需要:
-
将您的模块放置在 Python 通常搜索模块的目录之一。
-
将 Python 程序使用的所有模块放置在程序所在的同一目录中。
-
创建一个目录(或多个目录)来存放您的模块,并修改
sys.path变量,使其包括这个新目录(或多个目录)。
在这三个选项中,第一个显然是最简单的,也是您应该永远不选择的一个选项,除非您的 Python 版本在其默认模块搜索路径中包含本地代码目录。这些目录专门用于特定于站点的代码(即特定于您的机器的代码),并且不会因为新的 Python 安装而被覆盖,因为它们不是 Python 安装的一部分。如果您的sys.path引用了这样的目录,您可以将模块放在那里。
第二种选择对于与特定程序关联的模块来说是个不错的选择。只需将它们与程序放在一起即可。
第三种选择是针对将在该站点多个程序中使用的特定于站点的模块的正确选择。您可以通过多种方式修改sys.path。您可以在代码中将其赋值,这很简单,但这样做会将目录位置硬编码到您的程序代码中。您可以通过设置PYTHONPATH环境变量来实现,这相对简单,但它可能不适用于您站点的所有用户;或者您可以使用.pth 文件将其添加到默认搜索路径中。
如何设置PYTHONPATH的示例可以在 Python 文档的 Python 设置和使用部分(在命令行和环境部分下)找到。您设置的目录或目录将添加到sys.path变量之前。如果您使用PYTHONPATH,请确保您没有定义与您正在使用的现有库模块同名的一个模块。如果您这样做,您的模块将在库模块之前被找到。在某些情况下,这可能是您想要的,但可能并不常见。
您可以通过使用.pth 文件来避免这个问题。在这种情况下,您添加的目录或目录将被追加到sys.path。以下机制的最佳说明是一个例子。在 Windows 上,您可以将.pth 文件放置在sys.prefix指向的目录中。假设您的sys.prefix是c:\program files \python,并将文件放置在这个目录中。
列表 10.2. 文件 myModules.pth
mymodules
c:\Users\naomi\My Documents\python\modules
下次启动 Python 解释器时,如果它们存在,sys.path将包含c:\program files \python\mymodules和c:\Users\naomi\My Documents\python\modules,现在您可以将模块放置在这些目录中。请注意,mymodules 目录仍然有被新安装覆盖的风险。模块目录更安全。您还可能需要在升级 Python 时移动或创建一个 mymodules.pth 文件。如果您想了解更多关于使用.pth 文件的信息,请参阅Python 库参考中site模块的描述。
10.5. 模块中的私有名称
我在前面章节中提到,你可以输入 from module import * 来导入一个模块中的几乎所有名称。唯一的例外是模块中以下划线开头的标识符不能通过 from module import * 来导入。人们可以编写旨在通过 from module import * 导入的模块,但仍然保留某些函数或变量不被导入。通过以下划线开头所有内部名称(即不应在模块外部访问的名称),你可以确保 from module import * 只导入用户希望访问的名称。
要看到这个技术在实际中的应用,假设你有一个名为 modtest.py 的文件,其中包含以下代码。
列表 10.3. 文件 modtest.py
"""modtest: our test module"""
def f(x):
return x
def _g(x):
return x
a = 4
_b = 2
现在启动一个交互式会话,并输入以下内容:
>>> from modtest import *
>>> f(3)
3
>>> _g(3)
Traceback (innermost last):
File "<stdin>", line 1, in ?
NameError: name '_g' is not defined
>>> a
4
>>> _b
Traceback (innermost last):
File "<stdin>", line 1, in ?
NameError: name '_b' is not defined
如你所见,名称 f 和 a 被导入,但名称 _g 和 _b 在 modtest 外部保持隐藏。请注意,这种行为仅在 from ... import * 时发生。你可以这样做来访问 _g 或 _b:
>>> import modtest
>>> modtest._b
2
>>> from modtest import _g
>>> _g(5)
5
以下划线开头表示私有名称的约定在 Python 中被广泛使用,而不仅仅是模块中。
10.6. 库和第三方模块
在本章开头,我提到标准的 Python 发行版被分割成模块,以便更好地管理。在你安装 Python 之后,这些库模块中的所有功能都对你可用。你所需要做的只是在使用之前明确导入适当的模块、函数、类等等。
本书通篇讨论了许多最常见和有用的标准模块。但标准的 Python 发行版包含的远不止本书所描述的内容。至少,你应该浏览一下 Python 库参考 的目录。
在 IDLE 中,你可以使用路径浏览器窗口轻松浏览并查看用 Python 编写的模块。你还可以使用“在文件中查找”对话框搜索使用模块的示例代码,你可以从 Python 壳窗口的编辑菜单中打开它。你也可以以这种方式搜索你自己的模块。
可用的第三方模块及其链接在 Python 包索引(pyPI)中标识,我在第十九章中讨论了它。第十九章。你需要下载这些模块并将它们安装在你模块搜索路径中的一个目录中,以便将它们导入到你的程序中。
快速检查:模块
假设你有一个名为 new_math 的模块,其中包含一个名为 new_divide 的函数。你可能会以哪些方式导入并使用这个函数?每种方法的优缺点是什么?
假设 new_math 模块中包含一个名为 _helper_math() 的函数调用。下划线字符会如何影响 _helper_math() 的导入方式?
10.7. Python 作用域规则和命名空间
随着你作为 Python 程序员的经验的增长,Python 的作用域规则和命名空间将变得更加有趣。如果你是 Python 的新手,你可能不需要做任何事情,只需快速阅读文本以获得基本概念。对于更多细节,请在Python 语言参考中查找命名空间。
这里的核心概念是命名空间。Python 中的命名空间是将标识符映射到对象的一种映射——也就是说,Python 是如何跟踪哪些变量和标识符是活动的以及它们指向什么的。因此,一个像x = 1这样的语句会将x添加到命名空间中(假设它还没有在那里),并将其与值1关联起来。当在 Python 中执行代码块时,它有三个命名空间:局部、全局和内置(见图 10.2)。
图 10.2。检查命名空间以定位标识符的顺序

在执行过程中遇到标识符时,Python 首先在局部命名空间中查找它。如果找不到标识符,则查找全局命名空间。如果标识符仍然没有找到,则检查内置命名空间。如果它不存在那里,这种情况被认为是错误,并发生NameError异常。
对于模块、交互会话中执行的命令或从文件运行的脚本,全局和局部命名空间是相同的。创建任何变量或函数或从另一个模块导入任何内容都会导致在这个命名空间中创建新的条目或绑定。
但是,当进行函数调用时,会创建一个局部命名空间,并且为调用的每个参数在该命名空间中创建一个绑定。然后,每当在函数内部创建变量时,都会将新的绑定进入这个局部命名空间。函数的全局命名空间是函数包含块的全局命名空间(模块、脚本文件或交互会话的)。它与调用它的动态上下文无关。
在所有这些情况下,内置命名空间是__builtins__模块的。此模块包含了许多内置函数,例如你遇到过的len、min、max、int、float、list、tuple、range、str和repr,以及 Python 中的其他内置类,例如异常(如NameError)。
有时候,新 Python 程序员可能会遇到的一个问题是,你可以覆盖内置模块中的项。例如,如果你在程序中创建了一个列表并将其放入名为list的变量中,那么你将无法使用内置的list函数。你的列表条目首先被找到。对于函数、模块和其他对象的名称没有区别。给定标识符的绑定中最近的一次出现会被使用。
足够的讨论了——现在是时候探索一些例子了。这些例子使用了两个内置函数:locals 和 globals。这些函数分别返回包含局部和全局命名空间绑定的字典。
开始一个新的交互会话:
>>> locals()
{'__builtins__': <module 'builtins' (built-in)>, '__name__': '__main__',
'__doc__': None, '__package__': None}
>>> globals()
{'__builtins__': <module 'builtins' (built-in)>, '__name__': '__main__',
'__doc__': None, '__package__': None}>>>
这个新交互会话的局部和全局命名空间是相同的。它们有三个初始的键值对,用于内部使用:(1) 一个空的文档字符串 __doc__,(2) 主模块名称 __name__(对于交互式会话和从文件运行的脚本,总是 __main__),以及(3) 用于内置命名空间的模块 __builtins__(模块 __builtins__)。
现在如果你继续通过创建变量和从模块中导入,你会看到创建了几个绑定:
>>> z = 2
>>> import math
>>> from cmath import cos
>>> globals()
{'cos': <built-in function cos>, '__builtins__': <module 'builtins'
(built-in)>, '__package__': None, '__name__': '__main__', 'z': 2,
'__doc__': None, 'math': <module 'math' from
'/usr/local/lib/python3.0/libdynload/math.so'>}
>>> locals()
{'cos': <built-in function cos>, '__builtins__':
<module 'builtins' (built-in)>, '__package__': None, '__name__':
'__main__', 'z': 2, '__doc__': None, 'math': <module 'math' from
'/usr/local/lib/python3.0/libdynload/math.so'>}
>>> math.ceil(3.4)
4
如预期的那样,局部和全局命名空间继续保持等效。为 z 作为数字、math 作为模块以及从 cmath 模块中的 cos 作为函数添加了条目。
你可以使用 del 语句从命名空间中删除这些新绑定(包括使用 import 语句创建的模块绑定):
>>> del z, math, cos
>>> locals()
{'__builtins__': <module 'builtins' (built-in)>, '__package__': None,
'__name__': '__main__', '__doc__': None}
>>> math.ceil(3.4)
Traceback (innermost last):
File "<stdin>", line 1, in <module>
NameError: math is not defined
>>> import math
>>> math.ceil(3.4)
4
结果并不剧烈,因为你能够导入 math 模块并再次使用它。在交互模式下使用 del 的这种方式可能很有用。[¹]
¹
使用
del然后再次import不会拾取对磁盘上模块所做的更改。它不会被从内存中移除并再次从磁盘加载。绑定是从你的命名空间中取出并放回的。如果你想拾取对文件所做的更改,你仍然需要使用importlib.reload。
对于那些喜欢冒险的人来说,是的,你也可以使用 del 来删除 __doc__、__main__ 和 __builtins__ 的条目。但请抵制这样做,因为这不会对你的会话健康有益!
现在看看在交互会话中创建的函数:
>>> def f(x):
... print("global: ", globals())
... print("Entry local: ", locals())
... y = x
... print("Exit local: ", locals())
...
>>> z = 2
>>> globals()
{'f': <function f at 0xb7cbfeac>, '__builtins__': <module 'builtins'
(built-in)>, '__package__': None, '__name__': '__main__', 'z': 2,
'__doc__': None}
>>> f(z)
global: {'f': <function f at 0xb7cbfeac>, '__builtins__': <module
'builtins' (built-in)>, '__package__': None, '__name__': '__main__',
'z': 2, '__doc__': None}
Entry local: {'x': 2}
Exit local: {'y': 2, 'x': 2}
>>>
如果你剖析这个明显的混乱,你会看到,正如预期的那样,在进入时,参数 x 是 f 的局部命名空间中的原始条目,但 y 是后来添加的。全局命名空间与你的交互会话相同,这是 f 被定义的地方。注意它包含 z,这是在 f 之后定义的。
在生产环境中,你通常调用定义在模块中的函数。它们的全局命名空间是定义函数的模块的命名空间。假设你已经创建了本列表中的文件。
列出 10.4. 文件 scopetest.py
"""scopetest: our scope test module"""
v = 6
def f(x):
"""f: scope test function"""
print("global: ", list(globals().keys()))
print("entry local:", locals())
y = x
w = v
print("exit local:", locals().keys())
注意,你将只打印 globals 返回的字典的键(标识符),以减少结果中的混乱。你只打印键,因为模块被优化为将整个 __builtins__ 字典作为 __builtins__ 键的值字段存储:
>>> import scopetest
>>> z = 2
>>> scopetest.f(z)
global: ['__name__', '__doc__', '__package__', '__loader__', '__spec__',
'__file__', '__cached__', '__builtins__', 'v', 'f']
entry local: {'x': 2}
exit local: dict_keys(['x', 'w', 'y'])
现在全局命名空间是 scopetest 模块的命名空间,包括函数 f 和整数 v(但不包括你的交互会话中的 z)。因此,当你创建一个模块时,你可以完全控制其函数的命名空间。
我已经介绍了局部和全局命名空间。接下来,我将转向内建命名空间。这个例子介绍了另一个内建函数 dir,它给定一个模块,返回其中定义的名称列表:
>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException',
'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning',
'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError',
'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning',
'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False',
'FileExistsError', 'FileNotFoundError', 'FloatingPointError',
'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError',
'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError',
'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError',
'MemoryError', 'ModuleNotFoundError', 'NameError', 'None',
'NotADirectoryError', 'NotImplemented', 'NotImplementedError',
'OSError', 'OverflowError', 'PendingDeprecationWarning',
'PermissionError', 'ProcessLookupError', 'RecursionError',
'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning',
'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning',
'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True',
'TypeError', 'UnboundLocalError', 'UnicodeDecodeError',
'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError',
'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning',
'ZeroDivisionError', '__build_class__', '__debug__', '__doc__',
'__import__', '__loader__', '__name__', '__package__', '__spec__',
'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes',
'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright',
'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval',
'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr',
'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int',
'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals',
'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open',
'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed',
'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str',
'sum', 'super', 'tuple', 'type', 'vars', 'zip']
这里有很多条目。以 Error 和 Exit 结尾的条目是 Python 内建异常的名称,我在 第十四章 中讨论了这些异常。
最后一个组(从 abs 到 zip)是 Python 的内建函数。你已经在本书中看到了许多这些函数,并将看到更多,但在这里我不会涵盖所有这些函数。如果你感兴趣,你可以在 Python 库参考 中找到其余部分的详细信息。你也可以通过使用 help() 函数或直接打印文档字符串来轻松地获取它们的文档字符串:
>>> print(max.__doc__)
max(iterable[, key=func]) -> value
max(a, b, c, ...[, key=func]) -> value
With a single iterable argument, return its largest item.
With two or more arguments, return the largest argument.
如我之前提到的,一个新 Python 程序员无意中覆盖内建函数的情况并不少见:
>>> list("Peyto Lake")
['P', 'e', 'y', 't', 'o', ' ', 'L', 'a', 'k', 'e']
>>> list = [1, 3, 5, 7]
>>> list("Peyto Lake")
Traceback (innermost last):
File "<stdin>", line 1, in ?
TypeError: 'list' object is not callable
即使你使用的是内建 list 函数的语法,Python 解释器也不会超出新的 list 绑定来寻找 list 作为 list。
当然,如果你在单个命名空间中尝试使用相同的标识符两次,也会发生相同的事情。无论其类型如何,前一个值都会被覆盖:
>>> import mymath
>>> mymath = mymath.area
>>> mymath.pi
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'function' object has no attribute 'pi'
当你意识到这种情况时,这并不是一个重大问题。即使是为不同类型的对象重用标识符,代码的可读性也不会因此而降低。如果你在交互模式下无意中犯了这些错误,恢复起来也很容易。你可以使用 del 来删除你的绑定,以重新访问被覆盖的内建函数,或者再次导入你的模块以重新访问:
>>> del list
>>> list("Peyto Lake")
['P', 'e', 'y', 't', 'o', ' ', 'L', 'a', 'k', 'e']
>>> import mymath
>>> mymath.pi
3.14159
locals 和 globals 函数可以作为简单的调试工具使用。dir 函数不会给出当前的设置,但如果你不带参数调用它,它将返回本地命名空间中标识符的排序列表。这种做法有助于你捕捉到编译器通常在需要声明的语言中为你捕获的误拼变量错误:
>>> x1 = 6
>>> xl = x1 - 2
>>> x1
6
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__', 'x1', 'xl']
与 IDLE 一起捆绑的调试器具有设置,允许你在执行代码时查看局部和全局变量设置;它显示 locals 和 globals 函数的输出。
快速检查:命名空间和作用域
考虑一个位于 make_window.py 模块中的变量 width。在以下哪个上下文中 width 是作用域内的?
(A) 在模块本身内部
(B) 在模块中的 resize() 函数内部
(C) 在导入 make_window.py 模块的脚本内部
实验室 10:创建一个模块
将 第九章 结尾创建的函数打包成一个独立的模块。虽然你可以包含代码以将模块作为主程序运行,但目标应该是使函数可以从另一个脚本中完全使用。
摘要
-
Python 模块允许你将相关的代码和对象放入一个文件中。
-
使用模块还有助于防止变量名冲突,因为导入的对象通常与其模块相关联命名。
第十一章. Python 程序
本章涵盖
-
创建一个非常基本的程序
-
在 Linux/UNIX 上直接使程序可执行
-
在 macOS 上编写程序
-
在 Windows 中选择执行选项
-
结合程序和模块
-
分发 Python 应用程序
到目前为止,你主要在交互模式下使用 Python 解释器。对于生产使用,你将想要创建 Python 程序或脚本。本章的几个部分专注于命令行程序。如果你来自 Linux/UNIX 背景,你可能熟悉可以从命令行启动的脚本,并可以提供用于传递信息和可能重定向输入输出的参数和选项。如果你来自 Windows 或 Mac 背景,这些可能对你来说是新的,你可能更倾向于质疑它们的价值。
虽然在 GUI 环境中,命令行脚本有时不太方便使用,但 Mac 有 UNIX 命令行 shell 的选项,Windows 也提供了增强的命令行选项。花时间阅读本章的大部分内容将非常值得。你可能会发现这些技术有用,或者你可能会遇到需要理解使用其中一些技术的代码。特别是,当需要处理大量文件时,命令行技术非常有用。
11.1. 创建一个非常基本的程序
任何一组按顺序放置在文件中的 Python 语句都可以用作程序或脚本。但引入额外的结构更为标准和有用。在最基本的形式中,这项任务只是简单地在一个文件中创建一个控制函数并调用该函数。
列表 11.1. 文件 script1.py
def main(): *1*
print("this is our first test script file")
main() *2*
-
1 控制函数 main
-
2 调用 main
在这个脚本中,main是控制函数——也是唯一的函数。首先,它被定义,然后被调用。虽然在小程序中这没有太大区别,但这种结构在创建更大的应用程序时可以给你更多的选项和控制,所以从一开始就养成使用它的习惯是个好主意。
11.1.1. 从命令行启动脚本
如果你使用 Linux/UNIX,请确保 Python 在你的路径上,并且你与你的脚本在同一个目录中。然后,在你的命令行上输入以下内容以启动脚本:
python script1.py
如果你使用的是运行 OS X 的 Macintosh,程序与其它 UNIX 系统相同。你需要打开一个终端程序,它在应用程序文件夹的实用工具文件夹中。你还有其他几种在 OS X 上运行脚本的选择,我将在稍后讨论。
如果你使用的是 Windows,打开命令提示符(这可以在不同的菜单位置找到,取决于 Windows 的版本;在 Windows 10 中,它在 Windows 系统菜单中),或者 PowerShell。这两个都可以打开到你的主目录,如果需要,你可以使用cd命令来更改到子目录。如果 script1.py 保存在你的桌面上,运行它的样子如下:
C:\Users\naomi> cd Desktop *1*
C:\Users\naomi\Desktop> python script1.py *2*
this is our first test script file *3*
C:\Users\naomi\Desktop>
-
1 更改为桌面文件夹
-
2 运行 script1.py
-
3 script1.py 的输出
我将在本章后面部分探讨其他调用脚本的选项,但现在我们坚持使用这个选项。
11.1.2. 命令行参数
存在一种简单的机制来传递命令行参数。
列表 11.2. 文件 script2.py
import sys
def main():
print("this is our second test script file")
print(sys.argv)
main()
如果你用以下行调用它
python script2.py arg1 arg2 3
你会得到
this is our second test script file
['script2.py', 'arg1', 'arg2', '3']
你可以看到,命令行参数已经被存储在 sys.argv 中,作为一个字符串列表。
11.1.3. 重定向脚本的输入和输出
你可以通过使用命令行选项来重定向脚本的输入和/或输出。为了展示这项技术,我使用了这个简短的脚本。
列表 11.3. 文件 replace.py
import sys
def main():
contents = sys.stdin.read() *1*
sys.stdout.write(contents.replace(sys.argv[1], sys.argv[2])) *2*
main()
-
1 从 stdin 读取到内容
-
2 用第二个参数替换第一个参数
此脚本读取其标准输入,并将读取到的内容写入其标准输出,其中所有出现第一个参数的地方都被第二个参数替换。按照以下方式调用脚本,脚本将 infile 的副本放置在 outfile 中,其中所有 zero 的出现都被替换为 0:
python replace.py zero 0 < infile > outfile
注意,此脚本在 UNIX 上工作,但在 Windows 上,如果从命令提示符窗口启动脚本,则输入和/或输出重定向才有效。
通常,该行
python script.py arg1 arg2 arg3 arg4 < infile > outfile
该命令的效果是将任何 input 或 sys.stdin 操作重定向到 infile,并将任何 print 或 sys.stdout 操作重定向到 outfile。效果就像你将 sys.stdin 设置为以 'r'(读取)模式打开的 infile,将 sys.stdout 设置为以 'w'(写入)模式打开的 outfile:
python replace.py a A < infile >> outfile
这行代码导致输出被追加到 outfile 而不是覆盖它,正如前一个示例中发生的那样。
你还可以将一个命令的输出作为另一个命令的输入进行 管道:
python replace.py 0 zero < infile | python replace.py 1 one > outfile
此代码导致 outfile 包含 infile 的内容,其中所有 0 的出现都被更改为 zero,所有 1 的出现都被更改为 one。
11.1.4. argparse 模块
你可以配置脚本以接受命令行选项以及参数。argparse 模块提供了解析不同类型参数的支持,甚至可以生成用法消息。
要使用 argparse 模块,你需要创建一个 ArgumentParser 实例,用参数填充它,然后读取可选和位置参数。以下列表展示了模块的使用。
列表 11.4. 文件 opts.py
from argparse import ArgumentParser
def main():
parser = ArgumentParser()
parser.add_argument("indent", type=int, help="indent for report")
parser.add_argument("input_file", help="read data from this file") *1*
parser.add_argument("-f", "--file", dest="filename", *2*
help="write report to FILE", metavar="FILE")
parser.add_argument("-x", "--xray",
help="specify xray strength factor")
parser.add_argument("-q", "--quiet",
action="store_false", dest="verbose", default=True, *3*
help="don't print status messages to stdout")
args = parser.parse_args()
print("arguments:", args)
main()
此代码创建了一个 ArgumentParser 实例,然后添加了两个位置参数,indent 和 input_file,这些参数是在解析所有可选参数之后输入的。位置参数 是没有前缀字符(通常是 "-")的参数,并且是必需的,在这种情况下,indent 参数也必须可解析为 int 1。
下一行添加了一个可选的文件名参数,可以是'-f'或'--file' 2。最后添加的选项,"quiet"选项,还增加了关闭默认的详细选项(True,action="store_false")的能力。这些选项以前缀字符"-"开头,这告诉解析器它们是可选的。
最后一个参数,“-q”,也有一个默认值(在这种情况下为True),如果未指定该选项,则会被设置。action="store_false"参数指定,如果该参数被指定,则会在目标位置存储False值。3
argparse模块返回一个包含参数作为属性的 Namespace 对象。你可以通过点符号获取参数的值。如果没有为选项指定参数,其值是None。因此,如果你用以下行调用前面的脚本,
python opts.py -x100 -q -f outfile 2 arg2 *1*
- 1 选项跟在脚本名称之后。
以下输出结果:
arguments: Namespace(filename='outfile', indent=2, input_file='arg2',
verbose=False, xray='100')
如果发现无效参数,或者没有提供必需的参数,parse_args会引发错误:
python opts.py -x100 -r
这行代码将产生以下响应:
usage: opts.py [-h] [-f FILE] [-x XRAY] [-q] indent input_file
opts.py: error: the following arguments are required: indent, input_file
11.1.5. 使用 fileinput 模块
fileinput模块有时对脚本很有用。它提供了从一个或多个文件中处理输入行的支持。它自动读取命令行参数(来自sys.argv)并将它们作为其输入文件列表。然后它允许你顺序遍历这些行。本列表中的简单示例脚本(该脚本移除了以##开头的任何行)说明了模块的基本用法。
列表 11.5. 文件 script4.py
import fileinput
def main():
for line in fileinput.input():
if not line.startswith('##'):
print(line, end="")
main()
现在假设你有下一两个列表中显示的数据文件。
列表 11.6. 文件 sole1.tst
## sole1.tst: test data for the sole function
0 0 0
0 100 0
##
0 100 100
列表 11.7. 文件 sole2.tst
## sole2.tst: more test data for the sole function
12 15 0
##
100 100 0
也假设你进行以下调用:
python script4.py sole1.tst sole2.tst
移除注释行并将两个文件中的数据合并后,你将得到以下结果:
0 0 0
0 100 0
0 100 100
12 15 0
100 100 0
如果没有命令行参数,则只读取标准输入。如果其中一个参数是连字符(-),则在该点读取标准输入。
该模块还提供了其他几个函数。这些函数允许你在任何时刻确定已读取的总行数(lineno)、从当前文件中读取的行数(filelineno)、当前文件的名称(filename)、是否是文件的第一个行(isfirstline),以及/或是否正在读取标准输入(isstdin)。你可以在任何时刻跳到下一个文件(nextfile)或关闭整个流(close)。以下列表中的简短脚本(该脚本将其输入文件中的行合并并添加文件起始分隔符)说明了如何使用这些函数。
列表 11.8. 文件 script5.py
import fileinput
def main():
for line in fileinput.input():
if fileinput.isfirstline():
print("<start of file {0}>".format(fileinput.filename()))
print(line, end="")
main()
使用以下调用
python script5.py file1 file2
结果如下(其中虚线表示原始文件中的行):
<start of file file1>
.......................
.......................
<start of file file2>
.......................
.......................
最后,如果你用单个文件名或文件名列表作为参数调用fileinput.input,它们将用作其输入文件,而不是sys.argv中的参数。fileinput.input还有一个inplace选项,它将输出留在与输入相同的文件中,同时可选地保留原始文件作为备份文件。请参阅文档以了解此最后选项的描述。
快速检查:脚本和参数
匹配以下与命令行交互的方式以及每个用例的正确使用:
| 多个参数和选项 | sys.argv |
|---|---|
| 没有参数或只有一个参数 | 使用 file_input 模块 |
| 处理多个文件 | 重定向标准输入和输出 |
| 将脚本用作过滤器 | 使用 argparse 模块 |
11.2. 在 UNIX 上直接使脚本可执行
如果你是在 UNIX 上,你可以轻松地将脚本直接设置为可执行。在其顶部添加以下行,并适当地更改其模式(即,chmod +x replace.py):
#! /usr/bin/env python
注意,如果你使用的不是 Python 3.x 作为默认的 Python 版本,你可能需要将代码片段中的python更改为python3、python3.6或类似的内容,以指定你想要使用 Python 3.x 而不是较早的默认版本。
然后,如果你将你的脚本放在你的路径上的某个位置(例如,在你的 bin 目录中),你可以在任何目录中通过键入其名称和所需的参数来执行它:
replace.py zero 0 < infile > outfile
在 UNIX 上,你将拥有输入和输出重定向,如果你使用的是现代 shell,那么还有命令历史和自动完成功能。
如果你正在 UNIX 上编写管理脚本,有几个库模块可供使用,你可能觉得它们很有用。这些模块包括grp用于访问组数据库,pwd用于访问密码数据库,resource用于访问资源使用信息,syslog用于与 syslog 设施一起工作,以及stat用于处理从os.stat调用获取的文件或目录信息。你可以在Python 库参考中找到有关这些模块的信息。
11.3. macOS 上的脚本
在许多方面,macOS 上的 Python 脚本与 Linux/UNIX 上的行为相同。你可以像在任何 UNIX 盒子上一样,从终端窗口运行 Python 脚本。但在 Mac 上,你也可以通过拖动脚本文件到 Python 启动器应用,或者将 Python 启动器配置为打开你的脚本(或者可选地,所有以.py 扩展名的文件)的默认应用程序来运行 Python 程序。
在 Mac 上使用 Python 有几种选择。所有这些选项的详细信息超出了本书的范围,但你可以通过访问www.python.org网站并查看 Python 文档中“使用 Python”部分的 Mac 部分来获得完整的解释。你还应该查看文档中的第 11.6 节,即“分发 Python 应用程序”(#ch11lev1sec6),以获取有关如何在 Mac 平台上分发 Python 应用程序和库的更多信息。
如果你感兴趣的是编写 macOS 的管理脚本,你应该查看那些在 Apple 的 Open Scripting Architectures (OSA)和 Python 之间架起桥梁的包。两个这样的包是appscript和PyOSA。
11.4. Script execution options in Windows
如果你使用的是 Windows,你有几种启动脚本的选择,这些选择的性能和易用性各不相同。不幸的是,这些选项可能是什么以及它们的配置方式可能因当前使用的各种 Windows 版本而大相径庭。本书专注于从命令提示符或 PowerShell 运行 Windows。有关在您的系统上运行 Python 的其他选项的信息,您应查阅您版本 Python 的在线 Python 文档,并查找“在 Windows 上使用 Python”。
11.4.1. 从命令窗口或 PowerShell 启动脚本
要从命令窗口或 PowerShell 窗口运行脚本,打开命令提示符或 PowerShell 窗口。当你处于命令提示符并已导航到包含你的脚本的文件夹时,你可以使用 Python 以与 UNIX/Linux/MacOS 系统上相同的方式运行你的脚本:
> python replace.py zero 0 < infile > outfile
Python 没有运行?
如果你输入python后 Windows 命令提示符没有运行 Python,这可能意味着 Python 可执行文件的位置不在你的命令路径上。你可能需要手动将 Python 可执行文件添加到系统 PATH 环境变量中,或者重新运行安装程序以完成这项工作。有关在 Windows 上设置 Python 的更多帮助,请参阅在线 Python 文档的 Python 设置和使用部分。在那里,你可以找到一个关于在 Windows 上使用 Python 的部分,包括安装 Python 的说明。
这是 Windows 上运行脚本最灵活的方法,因为它允许你使用输入和输出重定向。
11.4.2. 其他 Windows 选项
其他选项可供探索。如果你熟悉编写批处理文件,你可以将你的命令包装在它们中。Cygwin 工具集包含 GNU BASH shell 的移植版,你可以在www.cygwin.com上了解更多信息,它为 Windows 提供了类似 UNIX 的 shell 功能。
在 Windows 上,你可以编辑环境变量(参见上一节)以将.py 作为魔法扩展名添加,使你的脚本自动可执行:
PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.JS;.PY
尝试这样做:使脚本可执行
在你的平台上执行脚本进行实验。还尝试将输入和输出重定向到脚本中。
11.5. 程序和模块
对于只包含几行代码的小脚本,一个函数就足够好了。但如果脚本的大小超出了这个范围,将控制函数与代码的其他部分分离是一个不错的选择。本节剩余部分将说明这种技术及其一些好处。我先从一个简单的控制函数的例子开始。下一列表中的脚本返回 0 到 99 之间给定数字的英语名称。
列表 11.9. 文件 script6.py
#! /usr/bin/env python3
import sys
# conversion mappings
_1to9dict = {'0': '', '1': 'one', '2': 'two', '3': 'three', '4': 'four',
'5': 'five', '6': 'six', '7': 'seven', '8': 'eight',
'9': 'nine'}
_10to19dict = {'0': 'ten', '1': 'eleven', '2': 'twelve',
'3': 'thirteen', '4': 'fourteen', '5': 'fifteen',
'6': 'sixteen', '7': 'seventeen', '8': 'eighteen',
'9': 'nineteen'}
_20to90dict = {'2': 'twenty', '3': 'thirty', '4': 'forty', '5': 'fifty',
'6': 'sixty', '7': 'seventy', '8': 'eighty', '9': 'ninety'}
def num2words(num_string):
if num_string == '0':
return'zero'
if len(num_string) > 2:
return "Sorry can only handle 1 or 2 digit numbers"
num_string = '0' + num_string *1*
tens, ones = num_string[-2], num_string[-1]
if tens == '0':
return _1to9dict[ones]
elif tens == '1':
return _10to19dict[ones]
else:
return _20to90dict[tens] + ' ' + _1to9dict[ones]
def main():
print(num2words(sys.argv[1]))
*2*
main()
- 1 左侧填充,以防它是单个数字
如果你用
python script6.py 59
你会得到这个结果:
fifty nine
此处的控制函数调用num2words函数并打印结果 2。通常,调用放在底部,但有时你会在文件的顶部看到控制函数的定义。我更喜欢将这个函数放在底部,紧挨着调用,这样我就不必滚动回顶部去找它,在找到它的名字后再回到底部。这种做法也清楚地将脚本管道与文件的其他部分分开,这在将脚本和模块结合使用时很有用。
当人们想要将他们创建在脚本中的函数提供给其他模块或脚本时,他们会将脚本与模块结合使用。此外,模块可以被配置为可以作为一个脚本运行,以便为用户提供快速接口或提供自动化模块测试的钩子。
将脚本和模块结合在一起是一个简单的问题,只需在控制函数周围放置以下条件测试即可:
if __name__ == '__main__':
main()
else:
# module-specific initialization code if any
如果它被当作脚本调用,它将以__main__的名字运行,并且将调用控制函数main。如果测试被导入到交互会话或另一个模块中,它的名字将是它的文件名。
在创建脚本时,我经常从一开始就将其设置为模块。这种做法允许我将它导入到会话中,并在创建函数时交互式地测试和调试它们。只需要调试控制函数。如果脚本变大,或者我发现自己在写可能在其他地方使用的函数,我可以将这些函数分离成它们自己的模块,或者让其他模块导入这个模块。
列表 11.10 中的脚本是对上一个脚本的扩展,但修改为用作模块。功能也得到了扩展,允许输入从 0 到 999999999999999 的数字,而不仅仅是 0 到 99。控制函数(main)检查其参数的有效性,并从中删除任何逗号,允许更易读的输入,如 1,234,567。
列表 11.10. 文件 n2w.py
#! /usr/bin/env python3
"""n2w: number to words conversion module: contains function
num2words. Can also be run as a script
usage as a script: n2w num *1*
(Convert a number to its English word description)
num: whole integer from 0 and 999,999,999,999,999 (commas are
optional)
example: n2w 10,003,103
for 10,003,103 say: ten million three thousand one hundred three
"""
import sys, string, argparse
_1to9dict = {'0': '', '1': 'one', '2': 'two', '3': 'three', '4': 'four', *2*
'5': 'five', '6': 'six', '7': 'seven', '8': 'eight',
'9': 'nine'}
_10to19dict = {'0': 'ten', '1': 'eleven', '2': 'twelve',
'3': 'thirteen', '4': 'fourteen', '5': 'fifteen',
'6': 'sixteen', '7': 'seventeen', '8': 'eighteen',
'9': 'nineteen'}
_20to90dict = {'2': 'twenty', '3': 'thirty', '4': 'forty', '5': 'fifty',
'6': 'sixty', '7': 'seventy', '8': 'eighty', '9': 'ninety'}
_magnitude_list = [(0, ''), (3, ' thousand '), (6, ' million '),
(9, ' billion '), (12, ' trillion '),(15, '')]
def num2words(num_string):
"""num2words(num_string): convert number to English words"""
if num_string == '0': *3*
return 'zero'
num_string = num_string.replace(",", "") *4*
num_length = len(num_string)
max_digits = _magnitude_list[-1][0]
if num_length > max_digits:
return "Sorry, can't handle numbers with more than " \
"{0} digits".format(max_digits)
num_string = '00' + num_string *5* *6*
word_string = '' *7* *6*
for mag, name in _magnitude_list: *6*
if mag >= num_length: *6*
return word_string *6*
else: *6*
hundreds, tens, ones = num_string[-mag-3], \ *6*
num_string[-mag-2], num_string[-mag-1] *6*
if not (hundreds == tens == ones == '0'): *6*
word_string = _handle1to999(hundreds, tens, ones) + \ *6*
name + word_string *6*
def _handle1to999(hundreds, tens, ones):
if hundreds == '0':
return _handle1to99(tens, ones)
else:
return _1to9dict[hundreds] + ' hundred ' + _handle1to99(tens, ones)
def _handle1to99(tens, ones):
if tens == '0':
return _1to9dict[ones]
elif tens == '1':
return _10to19dict[ones]
else:
return _20to90dict[tens] + ' ' + _1to9dict[ones]
def test(): *8*
values = sys.stdin.read().split()
for val in values:
print("{0} = {1}".format(val, num2words(val)))
def main():
parser = argparse.ArgumentParser(usage=__doc__)
parser.add_argument("num", nargs='*') *9*
parser.add_argument("-t", "--test", dest="test",
action='store_true', default=False,
help="Test mode: reads from stdin")
args = parser.parse_args()
if args.test: *10*
test()
else:
try:
result = num2words(args.num[0])
except KeyError: *11*
parser.error('argument contains non-digits')
else:
print("For {0}, say: {1}".format(args.num[0], result))
if __name__ == '__main__':
main() *12*
else:
print("n2w loaded as a module")
-
1 使用说明;包括示例
-
2 转换映射
-
3 处理特殊条件(数字为零或过大)
-
4 从数字中移除逗号
-
5 在数字左侧填充
-
6 创建包含数字的字符串
-
7 为数字初始化字符串
-
8 模块测试模式的函数
-
9 将该参数的所有值收集到一个列表中
-
10 如果设置了测试变量,则以测试模式运行
-
11 捕获由于参数包含非数字而引起的 KeyErrors
如果作为脚本调用,名称将是__main__。如果作为模块导入,名称将是n2w 12。
这个main函数说明了命令行脚本的控制器函数的目的,实际上是为用户创建一个简单的用户界面。它可能处理以下任务:
-
确保命令行参数的数量正确,并且它们的类型正确。如果没有,通知用户并提供使用信息。在这里,函数确保只有一个参数,但它并没有明确测试以确保参数只包含数字。
-
可能处理特殊模式。在这里,
--test参数将你置于测试模式。 -
将命令行参数映射到函数所需的参数,并以适当的方式调用它们。在这里,逗号被移除,并且只调用单个函数
num2words。 -
可能会捕获并打印出更友好的错误信息,以处理可能预期的异常。在这里,捕获了
KeyErrors,这发生在参数包含非数字时.^([1])¹
一种更好的方法是显式检查参数中的非数字,使用稍后将要介绍的正则表达式模块。这将确保我们不会隐藏由于其他原因发生的
KeyErrors。 -
如果需要,可以将输出映射到更用户友好的形式,这里在
print语句中进行了映射。如果这是一个要在 Windows 上运行的脚本,你可能希望允许用户通过双击方法打开它——也就是说,使用input查询参数,而不是将其作为命令行选项,并通过在脚本末尾添加行来保持屏幕显示输出input("Press the Enter key to exit")但你可能仍然希望将测试模式作为命令行选项保留。
下面的测试模式为模块及其num2words函数提供了回归测试功能。在这种情况下,你可以通过将一组数字放置在文件中来使用它。
列表 11.11. 文件 n2w.tst
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 98 99 100
101 102 900 901 999
999,999,999,999,999
1,000,000,000,000,000
然后输入
python n2w.py --test < n2w.tst > n2w.txt
输出文件可以轻松地检查其正确性。此示例在其创建过程中被运行了多次,并且可以在num2words或其调用的任何函数被修改时重新运行。是的,我清楚全面彻底的测试肯定没有发生。我承认,对于这个程序,有超过 999 万亿个有效的输入还没有被检查过!
通常,为模块提供测试模式是脚本的唯一功能。我知道至少有一家公司将创建每个开发的 Python 模块的测试模式作为其开发政策的一部分。Python 内置的数据对象类型和方法通常使这个过程变得简单,而且实践这种技术的似乎都一致认为这是值得的。参见第二十一章了解更多关于测试 Python 代码的信息。
另一个选项是创建一个只包含 main 函数处理参数部分的单独文件,并将 n2w 导入到这个文件中。然后,n2w.py 的 main 函数中只剩下测试模式。
快速检查:程序和模块
使用 if __name__ == "__main__": 的目的是防止什么问题,它是如何做到这一点的?你能想到其他防止这种问题的方法吗?
11.6. 分发 Python 应用程序
你可以通过几种方式分发你的 Python 脚本和应用程序。当然,你可以分享源文件,可能打包在一个 zip 或 tar 文件中。假设应用程序是可移植的,你也可以只发送 .pyc 文件的字节码。然而,这两种选项通常都存在很多不足。
11.6.1. 轮子包
当前打包和分发 Python 模块和应用程序的标准方式是使用名为 wheels 的包。Wheels 设计用于使安装 Python 代码更加可靠,并帮助管理依赖关系。创建 wheels 的详细内容超出了本章的范围,但有关创建 wheels 的要求和过程的完整详细信息可以在 Python 打包用户指南中找到,网址为 packaging.python.org。
11.6.2. zipapp 和 pex
如果你有一个在多个模块中的应用程序,你也可以将其作为可执行 zip 文件分发。这种格式依赖于 Python 的两个事实。
首先,如果一个 zip 文件包含一个名为 __main__.py 的文件,Python 可以使用该文件作为存档的入口点并直接执行 __main__.py 文件。此外,zip 文件的内容会被添加到 sys.path 中,因此它们可以被 __main__.py 导入和执行。
其次,zip 文件允许将任意内容添加到存档的开头。如果你添加一个指向 Python 解释器的 shebang 行,例如 #!/usr/bin/env python3,并给予文件所需的权限,该文件就可以成为一个自包含的可执行文件。
实际上,手动创建可执行的 zipapp 并不难。创建一个包含 __main__.py 的 zip 文件,在开头添加 shebang 行,并设置权限。
从 Python 3.5 开始,zipapp 模块包含在标准库中;它可以从命令行或通过库的 API 创建 zipapp。
一个更强大的工具,pex,不在标准库中,但可以通过 pip 从包索引中获取。pex 执行相同的基本任务,但提供了更多功能和选项,并且对于需要 Python 2.7 的情况,它是可用的。无论如何,zip 文件应用程序是方便打包和分发多文件 Python 应用程序以便运行的好方法。
11.6.3. py2exe 和 py2app
虽然本书的目的不是深入探讨特定平台的工具,但值得一提的是,py2exe可以创建独立的 Windows 程序,而py2app在 macOS 平台上也能做到同样的事情。这里的“独立”是指它们是单个可执行文件,可以在没有安装 Python 的机器上运行。在许多方面,独立可执行文件并不理想,因为它们通常比原生 Python 应用程序更大、更不灵活。但在某些情况下,它们是最好的——有时甚至是唯一的——解决方案。
11.6.4. 使用 freeze 创建可执行程序
你也可以使用freeze工具创建在未安装 Python 的机器上运行的可执行 Python 程序。你可以在 Python 源目录的 Tools 子目录下的 freeze 目录中的 Readme 文件中找到这些说明。如果你打算使用freeze,你可能需要下载 Python 源代码分发版。
在“冻结”Python 程序的过程中,你会创建 C 文件,然后使用 C 编译器编译和链接这些文件,你需要确保你的系统上已经安装了 C 编译器。冻结后的应用程序将仅在 C 编译器提供的可执行文件的平台运行。
几种其他工具试图以某种方式将 Python 解释器/环境与应用程序一起转换和打包为独立应用程序。然而,总的来说,这条路径仍然困难且复杂,除非你有强烈的需求,并且有时间和资源使这个过程可行,否则你可能会想避免它。
实验室 11:创建程序
在第八章中,你创建了一个 UNIX wc实用程序的版本,用于统计文件中的行数、单词数和字符数。现在你有更多的工具可以使用,重构这个程序使其更像原始程序。特别是,程序应该有选项来显示仅行(-l)、仅单词(-w)和仅字符(-c)。如果没有给出这些选项中的任何一个,则显示所有三个统计信息。但如果这些选项中的任何一个存在,则只显示指定的统计信息。
为了增加挑战性,请在 Linux/UNIX 系统上查看wc的man页面,并添加-L来显示最长行长度。你可以自由尝试实现 man 页面中列出的完整行为,并使用你系统上的wc实用程序进行测试。
摘要
-
Python 脚本和模块在最基本的形式下,只是放置在文件中的 Python 语句序列。
-
模块可以被配置为作为脚本运行,脚本也可以被设置以便它们可以作为模块导入。
-
脚本可以在 UNIX、macOS 或 Windows 命令行上设置为可执行。它们可以配置为支持输入和输出的命令行重定向,并且使用
argparse模块,解析复杂的命令行参数组合变得非常容易。 -
在 macOS 上,您可以使用 Python Launcher 运行 Python 程序,无论是单独运行还是作为打开 Python 文件的默认应用程序。
-
在 Windows 上,您可以通过多种方式调用脚本:通过双击打开它们,使用运行窗口,或使用命令提示符窗口。
-
Python 脚本可以作为脚本、字节码或称为 wheels 的特殊包进行分发。
-
py2exe,py2app和freeze工具提供了一种可执行 Python 程序,该程序可以在不包含 Python 解释器的机器上运行。 -
既然您已经了解了创建脚本和应用程序的方法,下一步就是了解 Python 如何与文件系统交互和操作。
第十二章. 使用文件系统
本章涵盖
-
管理路径和路径名
-
获取文件信息
-
执行文件系统操作
-
处理目录子树中的所有文件
与文件一起工作涉及以下两种情况之一:基本的 I/O 操作(在第十三章中描述,“读取和写入文件”)和与文件系统一起工作(例如,命名、创建、移动或引用文件),这有点棘手,因为不同的操作系统有不同的文件系统约定。
如果不学习 Python 提供的简化跨平台文件系统交互的所有功能,就可以轻松地学习如何执行基本的文件 I/O 操作——但我不会推荐这样做。相反,至少阅读本章的第一部分,它提供了你需要的工具,以便以不依赖于你特定操作系统的方式引用文件。然后,当你使用基本的 I/O 操作时,你可以以这种方式打开相关文件。
12.1. os 和 os.path 与 pathlib
在 Python 中,处理文件路径和文件系统操作的传统方式是使用os和os.path模块中包含的函数。这些函数已经足够好了,但通常会导致比必要的更冗长的代码。自 Python 3.5 以来,新增了一个名为pathlib的新库;它提供了一种更面向对象和更统一的方式来执行相同的操作。由于许多代码仍然使用旧风格,我保留了那些示例及其解释。另一方面,pathlib有很多优点,并可能成为新的标准,因此在每个旧方法的示例之后,我都包括了一个示例(以及必要的简要解释),说明如何使用pathlib来完成相同的事情。
12.2. 路径和路径名
所有操作系统都使用字符串来命名文件或目录,以引用文件或目录。以这种方式使用的字符串通常被称为路径名(有时也简称为路径),我将使用这个词来指代它们。路径名是字符串的事实,在处理它们时引入了可能的复杂性。Python 提供了很好的函数来帮助避免这些复杂性;但为了有效地使用这些 Python 函数,你需要了解其背后的问题。本节将讨论这些细节。
跨操作系统的路径名语义非常相似,因为几乎所有的操作系统都将文件系统建模为树结构,其中磁盘是根,文件夹、子文件夹等是分支、子分支等。这意味着大多数操作系统以基本相同的方式引用特定文件:使用路径名指定从文件系统树(磁盘)的根到要查询的文件的路径。(这种将根对应到硬盘的描述是一种过度简化,但它足够接近真相,可以用于本章。)这个路径名由一系列文件夹组成,需要进入这些文件夹才能到达所需的文件。
不同操作系统对路径名的精确语法有不同的约定。在 Linux/UNIX 路径名中用于分隔连续文件或目录名的字符是/,而在 Windows 路径名中用于分隔文件或目录名的字符是\。此外,UNIX 文件系统有一个单一的根(通过在路径名中以/字符作为第一个字符来引用),而 Windows 文件系统为每个驱动器都有一个单独的根,标记为A:\、B:\、C:\等等(其中 C:通常是主驱动器)。由于这些差异,不同操作系统上的文件有不同的路径名表示。在 MS Windows 中名为C:\data\myfile的文件在 UNIX 和 Mac OS 上可能被称为/data/myfile。Python 提供了函数和常量,允许您在不担心这些语法细节的情况下执行常见的路径名操作。只要稍加注意,您就可以以这种方式编写 Python 程序,无论底层文件系统是什么,它们都能正确运行。
12.2.1. 绝对和相对路径
这些操作系统允许两种类型的路径名:
-
绝对路径名指定了文件在文件系统中的确切位置,没有任何歧义;它们通过列出到达该文件的完整路径来实现,从文件系统的根开始。
-
相对路径名指定了文件相对于文件系统中的某个其他点的位置,而那个其他点在相对路径名中并没有指定;相反,相对路径名的绝对起始点是它们被使用的上下文提供的。
例如,以下是两个 Windows 的绝对路径名:
C:\Program Files\Doom
D:\backup\June
以及以下是两个 Linux 的绝对路径名和一个 Mac 的绝对路径名:
/bin/Doom
/floppy/backup/June
/Applications/Utilities
以及以下是两个 Windows 的相对路径名:
mydata\project1\readme.txt
games\tetris
以及这些是 Linux/UNIX/Mac 的相对路径名:
mydata/project1/readme.txt
games/tetris
Utilities/Java
相对路径需要上下文来定位。这种上下文通常以两种方式之一提供。
简单的方法是将相对路径附加到现有的绝对路径上,从而生成一个新的绝对路径。你可能有一个相对的 Windows 路径,Start Menu\Programs\Startup,和一个绝对路径,C:\Users\Administrator。通过将这两个路径连接起来,你得到一个新的绝对路径:C:\Users\Administrator\Start Menu\Programs\Startup,它指向文件系统中的特定位置。通过将相同的相对路径附加到不同的绝对路径(例如,C:\Users\myuser),你得到一个指向不同用户(myuser)的配置文件目录中的启动文件夹的路径。
相对路径获取上下文的第二种方式是通过隐式引用 当前工作目录,这是 Python 程序在其执行过程中认为自身所在的特定目录。当 Python 命令以相对路径作为参数时,它们可能会隐式地使用当前工作目录。例如,如果你使用 os.listdir(path) 命令并带有相对路径参数,那么该相对路径的锚点是当前工作目录,该命令的结果是目录中文件名的列表,其路径是通过将当前工作目录与相对路径参数连接而成的。
12.2.2. 当前工作目录
每当你在一台计算机上编辑文档时,你都有一个关于你在该计算机文件结构中的位置的概念,因为你处于你正在工作的文件的同一目录(文件夹)中。同样,每当 Python 运行时,它都有一个关于在任何时刻它在目录结构中的位置的概念。这个事实很重要,因为程序可能会要求列出当前目录中存储的文件列表。Python 程序所在的目录被称为该程序的 当前工作目录。这个目录可能与程序所在的目录不同。
要查看这一功能如何运作,请启动 Python 并使用 os.getcwd(获取当前工作目录)命令来查找 Python 的初始当前工作目录:
>>> import os
>>> os.getcwd()
注意,os.getcwd 被用作零参数函数调用,以强调它返回的值不是一个常数,而是会随着你发出改变当前工作目录值的命令而改变。(该目录可能要么是 Python 程序本身所在的目录,要么是你启动 Python 时所在的目录。在 Linux 机器上,结果是 /home/myuser,这是主目录。)在 Windows 机器上,你会在路径中看到插入的额外反斜杠,因为 Windows 使用 \ 作为其路径分隔符,而在 Python 字符串(如第 6.3.1 节中讨论的)中,\ 有特殊含义,除非它本身被转义。(section 6.3.1)
现在输入
>>> os.listdir(os.curdir)
常量 os.curdir 返回系统当前使用的相同目录指示符的字符串。在 UNIX 和 Windows 上,当前目录都表示为一个单独的点,但为了保持你的程序的可移植性,你应该始终使用 os.curdir 而不是直接输入点。这个字符串是一个相对路径,意味着 os.listdir 会将其附加到当前工作目录的路径上,得到相同的路径。此命令返回当前工作目录内所有文件或文件夹的列表。选择一个文件夹名称,并输入
>>> os.chdir(folder name) *1*
>>> os.getcwd()
- 1 “更改目录”功能
如你所见,Python 会移动到 os.chdir 函数参数指定的文件夹。再次调用 os.listdir(os.curdir) 将返回 folder 中的文件列表,因为此时 os.curdir 将相对于新的当前工作目录进行解析。许多 Python 文件系统操作都以这种方式使用当前工作目录。
12.2.3. 使用 pathlib 访问目录
要使用 pathlib 获取当前目录,你可以执行以下操作:
>>> import pathlib
>>> cur_path = pathlib.Path()
>>> cur_path.cwd()
PosixPath('/home/naomi')
pathlib 没有办法像 os.chdir() 那样更改当前目录(参见上一节),但你可以通过创建一个新的路径对象来处理一个新的文件夹,正如在第 12.2.5 节“使用 pathlib 操作路径名”中讨论的那样。
12.2.4. 操作路径名
现在你已经了解了理解文件和目录路径名的基础,是时候看看 Python 提供的用于操作这些路径名的工具了。这些工具包括 os.path 子模块中的几个函数和常量,你可以使用它们来操作路径,而无需明确使用任何特定于操作系统的语法。路径仍然以字符串的形式表示,但你无需将它们视为或以这种方式操作它们。
首先,使用 os.path.join 函数在不同的操作系统上构建几个路径名。请注意,导入 os 就足以引入 os.path 子模块;不需要显式地使用 import os.path 语句。
首先,在 Windows 下启动 Python:
>>> import os
>>> print(os.path.join('bin', 'utils', 'disktools'))
bin\utils\disktools
os.path.join 函数将它的参数解释为一系列目录名或文件名,这些名称将被连接起来形成一个字符串,该字符串可以被底层操作系统理解为一个相对路径。在一个 Windows 系统中,这意味着路径组件名称应该用反斜杠连接,这正是产生的结果。
现在尝试在 UNIX 中做同样的事情:
>>> import os
>>> print(os.path.join('bin', 'utils', 'disktools'))
bin/utils/disktools
结果是相同的路径,但使用了 Linux/UNIX 的正斜杠分隔符约定,而不是 Windows 的反斜杠分隔符约定。换句话说,os.path.join 允许你从一系列目录或文件名中构建文件路径,而无需担心底层操作系统的约定。os.path.join 是构建文件路径的基本方式,这种方式不会限制你的程序将在哪些操作系统上运行。
os.path.join 的参数不需要是单个目录或文件名;它们也可以是子路径,然后连接起来形成更长的路径名。以下示例在 Windows 环境中说明了这一点,并且也是您需要在使用字符串中的双反斜杠的情况下找到它的一个例子。请注意,您也可以使用正斜杠 (/) 输入路径名,因为 Python 在访问 Windows 操作系统之前会将它们转换过来:
>>> import os
>>> print(os.path.join('mydir\\bin', 'utils\\disktools\\chkdisk'))
mydir\bin\utils\disktools\chkdisk
当然,如果您始终使用 os.path.join 来构建路径,那么您很少需要担心这种情况。为了以可移植的方式编写此示例,您应该输入
>>> path1 = os.path.join('mydir', 'bin');
>>> path2 = os.path.join('utils', 'disktools', 'chkdisk')
>>> print(os.path.join(path1, path2))
mydir\bin\utils\disktools\chkdisk
os.path.join 命令也了解绝对路径和相对路径。在 Linux/UNIX 中,绝对 路径始终以 / 开头(因为单个斜杠表示整个系统的最高级目录,其中包含所有其他内容,包括可能可用的各种软盘和 CD 驱动器)。UNIX 中的 相对 路径是任何合法路径,且不以斜杠开头。在任何 Windows 操作系统中,情况更为复杂,因为 Windows 处理相对和绝对路径的方式更为混乱。而不是深入所有细节,我只想说,处理这种情况的最佳方式是遵循以下简化的 Windows 路径规则:
-
以驱动器字母开头,后跟冒号和反斜杠,然后是路径的路径名是绝对路径:C:\Program Files\Doom。(注意,仅凭 C: 本身,如果没有尾随的反斜杠,就不能可靠地用来引用 C: 驱动器上的顶级目录。您必须使用 C:\ 来引用 C: 的顶级目录。这一要求是 DOS 习惯的结果,而不是 Python 的设计。)
-
以 neither 驱动器字母 nor 反斜杠开头的路径名是相对路径:
mydirectory\letters\business。 -
以 \ 开头,后跟服务器名称的路径名是网络资源的路径。
-
任何其他情况都可以被认为是无效的路径名.^([1])
¹
Microsoft Windows 允许一些其他结构,但可能最好坚持使用给定的定义。
不论使用哪种操作系统,os.path.join 命令都不会对其构造的路径名进行合理性检查。有可能构造出包含根据您的操作系统认为在路径名中禁止使用的字符的路径名。如果此类检查是必需的,可能最好的解决方案是自行编写一个小的路径有效性检查函数。
os.path.split 命令返回一个包含两个元素的元组,将路径的基本名称(路径末尾的单个文件或目录名称)从路径的其余部分分割开来。您可能会在 Windows 系统上使用此示例:
>>> import os
>>> print(os.path.split(os.path.join('some', 'directory', 'path')))
('some\\directory', 'path')
os.path.basename 函数仅返回路径的基本名称,而 os.path.dirname 函数返回路径直到但不包括最后一个名称的部分,如下例所示:
>>> import os
>>> os.path.basename(os.path.join('some', 'directory', 'path.jpg'))
'path.jpg'
>>> os.path.dirname(os.path.join('some', 'directory', 'path.jpg'))
'some\\directory'
为了处理大多数文件系统(Macintosh 是一个显著的例外)用来表示文件类型的点分扩展名表示法,Python 提供了 os.path.splitext:
>>> os.path.splitext(os.path.join('some', 'directory', 'path.jpg'))
('some/directory/path', '.jpg')
返回的元组的最后一个元素包含指定文件的点分扩展名(如果有)。返回的元组的第一个元素包含从原始参数中除点分扩展名之外的所有内容。
你还可以使用更专业的函数来操作路径名。os.path.commonprefix(path1, path2, ...) 为一组路径找到公共前缀(如果有的话)。如果你想要找到包含一组文件中的每个文件的最低级目录,这个技术很有用。os.path.expanduser 在路径中扩展用户名快捷方式,例如在 UNIX 中。同样,os.path.expandvars 对环境变量执行相同的操作。以下是在 Windows 10 系统上的一个示例:
>>> import os
>>> os.path.expandvars('$HOME\\temp')
'C:\\Users\\administrator\\personal\\temp'
12.2.5. 使用 pathlib 操作路径名
正如你在前面的部分中所做的那样,首先在不同的操作系统上使用路径对象的方法构造几个路径名。
首先,在 Windows 下启动 Python:
>>> from pathlib import Path
>>> cur_path = Path()
>>> print(cur_path.joinpath('bin', 'utils', 'disktools'))
bin\utils\disktools
使用斜杠操作符也可以达到相同的结果:
>>> cur_path / 'bin' / 'utils' / 'disktools'
WindowsPath('bin/utils/disktools')
注意,在路径对象的表示中,始终使用正斜杠,但 Windows 路径对象将正斜杠转换为操作系统所需的反斜杠。所以如果你在 UNIX 中尝试相同的事情:
>>> cur_path = Path()
>>> print(cur_path.joinpath('bin', 'utils', 'disktools'))
bin/utils/disktools
parts 属性返回一个包含路径所有组件的元组。你可以在 Windows 系统上使用这个示例:
>>> a_path = WindowsPath('bin/utils/disktools')
>>> print(a_path.parts)
('bin', 'utils', 'disktools')
name 属性返回路径的基名,parent 属性返回路径直到但不包括最后一个名称的部分,而 suffix 属性处理大多数文件系统用来表示文件类型的点分扩展名表示法(但 Macintosh 是一个显著的例外)。以下是一个示例:
>>> a_path = Path('some', 'directory', 'path.jpg')
>>> a_path.name
'path.jpg'
>>> print(a_path.parent)
some\directory
>>> a_path.suffix
'.jpg'
与 Path 对象相关联的几个其他方法允许灵活地操作路径名和文件本身,因此你应该查看 pathlib 模块的文档。pathlib 模块可能会使你的生活更轻松,并使你的文件处理代码更简洁。
12.2.6. 有用的常量和函数
你可以访问几个有用的路径相关常量和函数,使你的 Python 代码比其他情况下更系统无关。这些常量中最基本的是 os.curdir 和 os.pardir,它们分别定义了操作系统用于目录和父目录路径指示器的符号。在 Windows 以及 Linux/UNIX 和 macOS 中,这些指示器分别是 . 和 ..,它们可以用作正常的路径元素。以下是一个示例
os.path.isdir(os.path.join(path, os.pardir, os.curdir))
询问 path 的父父是否是目录。os.curdir 特别适用于请求当前工作目录上的命令。以下是一个示例
os.listdir(os.curdir)
返回当前工作目录中的文件名列表(因为 os.curdir 是一个相对路径,而 os.listdir 总是接受相对路径作为相对于当前工作目录的路径)。
os.name 常量返回用于处理操作系统特定细节的 Python 模块的名称。以下是在我的 Windows XP 系统上的一个示例:
>>> import os
>>> os.name
'nt'
注意,即使实际的 Windows 版本是 Windows 10,os.name 也会返回 'nt'。除了 Windows CE 之外,大多数 Windows 版本都被标识为 'nt'。
在运行 OS X 的 Mac 和 Linux/UNIX 上,响应是 posix。你可以根据你正在工作的平台执行特殊操作:
import os
if os.name == 'posix':
root_dir = "/"
elif os.name == 'nt':
root_dir = "C:\\"
else:
print("Don't understand this operating system!")
你也可能看到程序使用 sys.platform,它提供了更精确的信息。在 Windows 10 上,sys.platform 被设置为 win32——即使机器运行的是操作系统的 64 位版本。在 Linux 上,你可能看到 linux2,而在 Solaris 上,它可能根据你运行的版本被设置为 sunos5。
所有你的环境变量以及与它们关联的值都存储在一个名为 os.environ 的字典中。在大多数操作系统中,这个目录包括与路径相关的变量——通常是二进制文件的搜索路径等。如果你需要这个目录,现在你知道在哪里可以找到它。
到目前为止,你已经了解了在 Python 中处理路径的主要方面。如果你的直接需求是打开文件进行读取或写入,你可以直接跳到下一章。继续阅读以获取有关路径名、测试它们指向的内容、有用的常量等方面的更多信息。
快速检查:路径操作
你会如何使用 os 模块的函数来获取一个指向名为 test.log 的文件的路径,并在同一目录下创建一个名为 test.log.old 的新文件路径?你会如何使用 pathlib 模块来完成同样的操作?
如果你从 os.pardir 创建一个 pathlib Path 对象,你会得到什么路径?试一试,看看结果。
12.3. 获取文件信息
文件路径应该指示你硬盘上的实际文件和目录。你当然会传递一个路径,因为你想要了解它指向的内容。Python 提供了各种函数来处理这个目的。
最常用的 Python 路径信息函数是 os.path.exists、os.path.isfile 和 os.path.isdir,它们都接受一个路径作为参数。os.path.exists 如果其参数是一个在文件系统中存在的路径,则返回 True。os.path.isfile 仅当其提供的路径指示某种正常的数据文件(可执行文件属于这一类)时返回 True,否则返回 False,包括路径参数在文件系统中没有指示任何内容的情况。os.path.isdir 仅当其路径参数指示一个目录时返回 True,否则返回 False。以下示例在我的系统上是有效的。你可能需要使用你自己的不同路径来调查这些函数的行为:
>>> import os
>>> os.path.exists('C:\\Users\\myuser\\My Documents')
True
>>> os.path.exists('C:\\Users\\myuser\\My Documents\\Letter.doc')
True
>>> os.path.exists('C:\\Users\\myuser\\\My Documents\\ljsljkflkjs')
False
>>> os.path.isdir('C:\\Users\\myuser\\My Documents')
True
>>> os.path.isfile('C:\\Users\\ myuser\\My Documents')
False
>>> os.path.isdir('C:\\Users\\ myuser\\My Documents
\\Letter.doc')
False
>>> os.path.isfile('C:\\Users\\ myuser\\My Documents\\Letter.doc')
True
几个类似的功能提供了更专业的查询。os.path.islink 和 os.path.ismount 在 Linux 和其他提供文件链接和挂载点的 UNIX 操作系统环境中很有用;它们分别返回 True,如果路径指示的是一个链接或挂载点。os.path.islink 在 Windows 快捷方式文件(以 .lnk 结尾的文件)上不返回 True,简单的理由是这类文件不是真正的链接。然而,os.path.islink 在使用 mklink() 命令创建的真正符号链接的 Windows 系统上返回 True。操作系统没有为它们分配特殊状态,程序不能透明地使用它们,就像它们是实际文件一样。os.path.samefile(path1, path2) 仅当两个路径参数指向同一文件时返回 True。os.path.isabs(path) 如果其参数是绝对路径则返回 True;否则返回 False。os.path.getsize(path)、os.path.getmtime(path) 和 os.path.getatime(path) 分别返回路径名的大小、最后修改时间和最后访问时间。
12.3.1. 使用 scandir 获取文件信息
除了列出的 os.path 函数外,你还可以通过使用 os.scandir 获取目录中文件的更完整信息,它返回一个 os.DirEntry 对象的迭代器。os.DirEntry 对象公开了目录条目的文件属性,因此使用 os.scandir 可能比将 os.listdir(在下一节中讨论)与 os.path 操作组合更快、更高效。例如,如果你需要知道条目是否指向文件或目录,os.scandir 能够访问比仅名称更多的目录信息,这将是一个优点。os.DirEntry 对象具有与上一节中提到的 os.path 函数相对应的方法,包括 exists、is_dir、is_file、is_socket 和 is_symlink。
os.scandir 也支持使用 with 的上下文管理器,并建议使用它以确保资源得到适当的处理。以下示例代码遍历目录中的所有条目并打印条目的名称以及它是否是文件:
>>> with os.scandir(".") as my_dir:
... for entry in my_dir:
... print(entry.name, entry.is_file())
...
pip-selfcheck.json True
pyvenv.cfg True
include False
test.py True
lib False
lib64 False
bin False
12.4. 更多的文件系统操作
除了获取文件信息外,Python 允许你通过 os 模块中的一组基本但非常有用的命令直接执行某些文件系统操作。
我只在本节中描述了那些真正的跨平台操作。许多操作系统可以访问更高级的文件系统功能,你需要检查主要的 Python 库文档以获取详细信息。
你已经看到,要获取目录中的文件列表,你使用 os.listdir:
>>> os.chdir(os.path.join('C:', 'my documents', 'tmp'))
>>> os.listdir(os.curdir)
['book1.doc.tmp', 'a.tmp', '1.tmp', '7.tmp', '9.tmp', 'registry.bkp']
注意,与许多其他语言或壳中的列表目录命令不同,Python 不在 os.listdir 返回的列表中包含 os.curdir 和 os.pardir 指示符。
来自 glob 模块的 glob 函数(以旧 UNIX 模式匹配函数命名)扩展了 Linux/UNIX shell 样式的通配符和字符序列,在路径名中返回当前工作目录中匹配的文件。* 匹配任何字符序列。? 匹配任何单个字符。字符序列([h,H] 或 [0-9])匹配该序列中的任何单个字符:
>>> import glob
>>> glob.glob("*")
['book1.doc.tmp', 'a.tmp', '1.tmp', '7.tmp', '9.tmp', 'registry.bkp']
>>> glob.glob("*bkp")
['registry.bkp']
>>> glob.glob("?.tmp")
['a.tmp', '1.tmp', '7.tmp', '9.tmp']
>>> glob.glob("[0-9].tmp")
['1.tmp', '7.tmp', '9.tmp']
要重命名(移动)文件或目录,请使用 os.rename:
>>> os.rename('registry.bkp', 'registry.bkp.old')
>>> os.listdir(os.curdir)
['book1.doc.tmp', 'a.tmp', '1.tmp', '7.tmp', '9.tmp', 'registry.bkp.old']
您可以使用此命令在目录之间以及目录内部移动文件。
使用 os.remove 删除或删除数据文件:
>>> os.remove('book1.doc.tmp')
>>> os.listdir(os.curdir)
['a.tmp', '1.tmp', '7.tmp', '9.tmp', 'registry.bkp.old']
注意,您不能使用 os.remove 删除目录。此限制是一个安全特性,以确保您不会意外删除整个目录子结构。
可以通过写入它们来创建文件,如第十一章所述。第十一章。要创建目录,请使用 os.makedirs 或 os.mkdir。它们之间的区别在于 os.mkdir 不会创建任何必要的中间目录,而 os.makedirs 会:
>>> os.makedirs('mydir')
>>> os.listdir(os.curdir)
['mydir', 'a.tmp', '1.tmp', '7.tmp', '9.tmp', 'registry.bkp.old']
>>> os.path.isdir('mydir')
True
要删除一个目录,请使用 os.rmdir。此函数仅删除空目录。尝试在非空目录上使用它将引发异常:
>>> os.rmdir('mydir')
>>> os.listdir(os.curdir)
['a.tmp', '1.tmp', '7.tmp', '9.tmp', 'registry.bkp.old']
要删除非空目录,请使用 shutil.rmtree 函数。它递归地删除目录树中的所有文件。有关其使用的详细信息,请参阅 Python 标准库文档。
12.4.1. 使用 pathlib 执行更多文件系统操作
路径对象具有前面提到的许多相同的方法。然而,存在一些差异。iterdir 方法类似于 os.path.listdir 函数,但它返回路径的迭代器而不是字符串列表:
>>> new_path = cur_path.joinpath('C:', 'my documents', 'tmp'))
>>> list(new_path.iterdir())
[WindowsPath('book1.doc.tmp'), WindowsPath('a.tmp'), WindowsPath('1.tmp'),
WindowsPath('7.tmp'), WindowsPath('9.tmp'), WindowsPath('registry.bkp')]
注意,在 Windows 环境中,返回的路径是 WindowsPath 对象,而在 Mac OS 或 Linux 中,它们是 PosixPath 对象。
pathlib 路径对象也内置了 glob 方法,它再次返回的不是字符串列表,而是路径对象的迭代器。否则,此函数的行为与上面演示的 glob.glob 函数非常相似:
>>> list(cur_path.glob("*"))
[WindowsPath('book1.doc.tmp'), WindowsPath('a.tmp'), WindowsPath('1.tmp'),
WindowsPath('7.tmp'), WindowsPath('9.tmp'), WindowsPath('registry.bkp')]
>>> list(cur_path.glob("*bkp"))
[WindowsPath('registry.bkp')]
>>> list(cur_path.glob("?.tmp"))
[WindowsPath('a.tmp'), WindowsPath('1.tmp'), WindowsPath('7.tmp'),
WindowsPath('9.tmp')]
>>> list(cur_path.glob("[0-9].tmp"))
[WindowsPath('1.tmp'), WindowsPath('7.tmp'), WindowsPath('9.tmp')]
要重命名(移动)文件或目录,请使用路径对象的 rename 方法:
>>> old_path = Path('registry.bkp')
>>> new_path = Path('registry.bkp.old')
>>> old_path.rename(new_path)
>>> list(cur_path.iterdir())
[WindowsPath('book1.doc.tmp'), WindowsPath('a.tmp'), WindowsPath('1.tmp'),
WindowsPath('7.tmp'), WindowsPath('9.tmp'),
WindowsPath('registry.bkp.old')]
您可以使用此命令在目录之间以及目录内部移动文件。
使用 unlink 删除或删除数据文件:
>>> new_path = Path('book1.doc.tmp')
>>> new_path.unlink()
>>> list(cur_path.iterdir())
[WindowsPath('a.tmp'), WindowsPath('1.tmp'), WindowsPath('7.tmp'),
WindowsPath('9.tmp'), WindowsPath('registry.bkp.old')]
注意,与 os.remove 一样,您不能使用 unlink 方法删除目录。此限制是一个安全特性,以确保您不会意外删除整个目录子结构。
要使用路径对象创建目录,请使用路径对象的 mkdir 方法。如果您给 mkdir 方法传递 parents=True 参数,它将创建任何必要的中间目录;否则,如果中间目录不存在,它将引发 FileNotFoundError:
>>> new_path = Path ('mydir')
>>> new_path.mkdir(parents=True)
>>> list(cur_path.iterdir())
[WindowsPath('mydir'), WindowsPath('a.tmp'), WindowsPath('1.tmp'),
WindowsPath('7.tmp'), WindowsPath('9.tmp'),
WindowsPath('registry.bkp.old')]]
>>> new_path.is_dir('mydir')
True
要删除目录,请使用 rmdir 方法。此方法仅删除空目录。尝试在非空目录上使用它将引发异常:
>>> new_path = Path('mydir')
>>> new_path.rmdir()
>>> list(cur_path.iterdir())
[WindowsPath('a.tmp'), WindowsPath('1.tmp'), WindowsPath('7.tmp'),
WindowsPath('9.tmp'), WindowsPath('registry.bkp.old']
实验室 12:更多文件操作
你如何计算目录中所有以 .txt 结尾且不是符号链接的文件的总大小?如果你的第一个答案是使用 os.path,也试着用 pathlib 来做,反之亦然。
编写一些代码,基于你的解决方案将实验室问题中的相同 .txt 文件移动到同一目录下的一个名为 backup 的新子目录中。
12.5. 处理目录子树中的所有文件
最后,一个用于遍历递归目录结构的非常有用的函数是 os.walk 函数。你可以用它来遍历整个目录树,对于它遍历的每个目录,返回三个东西:该目录的根或路径;其子目录的列表;以及其文件的列表。
os.walk 使用起始或顶级目录的路径调用,并可以有三个可选参数:os.walk(directory, topdown=True, onerror=None, followlinks=False)。directory 是起始目录路径;如果 topdown 为 True 或未指定,则每个目录中的文件在处理其子目录之前进行处理,结果是一个从顶部开始向下走的列表;而如果 topdown 为 False,则首先处理每个目录的子目录,从而实现从下到上的树遍历。onerror 参数可以设置为一个函数来处理由 os.listdir 调用产生的任何错误,默认情况下这些错误会被忽略。默认情况下,os.walk 不会进入符号链接指向的文件夹,除非你提供了 followlinks=True 参数。
当被调用时,os.walk 创建一个迭代器,它递归地应用于 top 参数包含的所有目录。换句话说,对于 names 中的每个子目录 subdir,os.walk 递归地调用自身的形式为 os.walk(subdir, ...)。注意,如果 topdown 为 True 或未指定,在递归的下一级使用之前,子目录列表可能会被修改(使用任何列表修改运算符或方法);你可以使用这个来控制 os.walk 将会进入哪些子目录(如果有的话)。
为了感受 os.walk 的能力,我建议迭代整个树并打印出每个目录返回的值。作为 os.walk 力量的一个例子,列出当前工作目录及其所有子目录,以及每个目录中条目的数量,排除任何 .git 目录:
import os
for root, dirs, files in os.walk(os.curdir):
print("{0} has {1} files".format(root, len(files)))
if ".git" in dirs: *1*
dirs.remove(".git") *2*
-
1 检查名为 .git 的目录
-
2 从目录列表中移除 .git(仅 .git 目录)
这个例子很复杂,如果你想充分利用 os.walk,你可能需要大量地玩弄它来理解正在发生的事情的细节。
shutil模块的copytree函数递归地复制目录及其所有子目录中的所有文件,并保留权限模式和 stat(即访问/修改时间)信息。shutil还具有已提到的rmtree函数,用于删除目录及其所有子目录,以及几个用于复制单个文件的函数。有关详细信息,请参阅标准库文档。
摘要
-
Python 提供了一组函数和常量,以独立于底层操作系统的方式处理文件系统引用(路径名)和文件系统操作。
-
对于更高级和专业的文件系统操作,这些操作通常与特定的操作系统或系统相关联,请查看
os、pathlib和posix模块的主 Python 文档。 -
为了方便起见,本章讨论的函数的总结在表 12.1 和表 12.2 中给出。
表 12.1. 文件系统值和函数的摘要
| 函数 | 文件系统值或操作 |
|---|---|
| os.getcwd(), Path.cwd() | 获取当前目录 |
| os.name | 提供通用的平台识别 |
| sys.platform | 提供特定的平台信息 |
| os.environ | 映射环境 |
| os.listdir(path) | 获取目录中的文件 |
| os.scandir(path) | 获取目录的os.DirEntry对象迭代器 |
| os.chdir(path) | 更改目录 |
| os.path.join(elements), Path.joinpath(elements) | 将元素组合成一个路径 |
| os.path.split(path) | 将路径拆分为基本路径和尾部(路径的最后一个元素) |
| Path.parts | 路径元素的元组 |
| os.path.splitext(path) | 将路径拆分为基本路径和文件扩展名 |
| Path.suffix | 路径的文件扩展名 |
| os.path.basename(path) | 获取路径的基本名 |
| Path.name | 路径的基本名 |
| os.path.commonprefix(list_of_paths) | 获取列表中所有路径的共同前缀 |
| os.path.expanduser(path) | 将 ~ 或 ~user 扩展为完整路径名 |
| os.path.expandvars(path) | 扩展环境变量 |
| os.path.exists(path) | 检查路径是否存在 |
| os.path.isdir(path), Path.is_dir() | 检查路径是否为目录 |
| os.path.isfile(path), Path.is_file() | 检查路径是否为文件 |
| os.path.islink(path), Path.is_link() | 检查路径是否为符号链接(非 Windows 快捷方式) |
| os.path.ismount(path) | 检查路径是否为挂载点 |
| os.path.isabs(path), Path.is_absolute() | 检查路径是否为绝对路径 |
| os.path.samefile(path_1, path_2) | 检查两个路径是否指向同一文件 |
| os.path.getsize(path) | 获取文件的大小 |
| os.path.getmtime(path) | 获取修改时间 |
| os.path.getatime(path) | 获取访问时间 |
| os.rename(old_path, new_path) | 重命名文件 |
| os.mkdir(path) | 创建目录 |
| os.makedirs(path) | 创建目录以及所需的任何父目录 |
| os.rmdir(path) | 删除目录 |
| glob.glob(pattern) | 获取与通配符模式匹配的结果 |
| os.walk(path) | 获取目录树中的所有文件名 |
表 12.2. pathlib 属性和函数的部分列表
| 方法或属性 | 值或操作 |
|---|---|
| Path.cwd() | 获取当前目录 |
| Path.joinpath(elements) 或 Path / element / element | 将元素组合成新的路径 |
| Path.parts | 路径元素的元组 |
| Path.suffix | 路径的文件扩展名 |
| Path.name | 路径的基本名称 |
| Path.exists() | 检查路径是否存在 |
| Path.is_dir() | 检查路径是否为目录 |
| Path.is_file() | 检查路径是否为文件 |
| Path.is_symlink() | 检查路径是否为符号链接(非 Windows 快捷方式) |
| Path.is_absolute() | 检查路径是否为绝对路径 |
| Path.samefile(Path2) | 检查两个路径是否指向同一文件 |
| Path1.rename(Path2) | 重命名文件 |
| Path.mkdir([parents=True]) | 创建目录,如果 parents 为 True,也将创建所需的父目录 |
| Path.rmdir() | 删除目录 |
| Path.glob(pattern) | 获取与通配符模式匹配的结果 |
第十三章. 读取和写入文件
本章涵盖
-
打开文件和
file对象 -
关闭文件
-
以不同模式打开文件
-
读取和写入文本或二进制数据
-
重定向屏幕输入/输出
-
使用
struct模块 -
将对象序列化到文件中
-
存档对象
13.1. 打开文件和文件对象
你可能最想对文件做的单个操作就是打开和读取它们。
在 Python 中,你可以通过使用内置的 open 函数和多种内置的读取操作来打开和读取文件。以下简短的 Python 程序从名为 myfile 的文本文件中读取一行:
with open('myfile', 'r') as file_object:
line = file_object.readline()
open 不会从文件中读取任何内容;相反,它返回一个名为 file 对象的实例,你可以使用它来访问已打开的文件。file 对象跟踪文件以及已读取或写入文件的程度。所有 Python 文件 I/O 都是通过 file 对象而不是文件名来完成的。
第一次调用 readline 返回 file 对象中的第一行,包括第一个换行符或如果没有换行符则整个文件;下一次调用 readline 返回第二行(如果存在),依此类推。
open 函数的第一个参数是路径名。在先前的示例中,你正在打开你期望在当前工作目录中存在的文件。以下是在绝对位置打开文件——c:\My Documents\test\myfile:
import os
file_name = os.path.join("c:", "My Documents", "test", "myfile")
file_object = open(file_name, 'r')
注意,此示例使用了 with 关键字,表示文件将以上下文管理器的方式打开,我将在第十四章第十四章中解释更多。现在,只需注意这种打开文件的方式更好地管理潜在的 I/O 错误,并且通常更受欢迎。
13.2. 关闭文件
在从 file 对象读取或写入所有数据之后,应该关闭它。关闭 file 对象可以释放系统资源,允许其他代码读取或写入底层文件,并且通常使程序更可靠。对于小型脚本,不关闭 file 对象通常没有太大影响;file 对象在脚本或程序终止时自动关闭。对于大型程序,过多的打开 file 对象可能会耗尽系统资源,导致程序终止。
当不再需要 file 对象时,你可以使用 close 方法来关闭它。先前的简短程序变为如下:
file_object = open("myfile", 'r')
line = file_object.readline()
# . . . any further reading on the file_object . . .
file_object.close()
使用上下文管理器和 with 关键字也是自动关闭文件的好方法:
with open("myfile", 'r') as file_object:
line = file_object.readline()
# . . . any further reading on the file_object . . .
13.3. 以写入或其他模式打开文件
open 命令的第二个参数是一个字符串,表示文件应该如何打开。'r' 表示“以读取方式打开文件”,'w' 表示“以写入方式打开文件”(文件中已有的数据将被删除),'a' 表示“以追加方式打开文件”(新数据将被追加到文件中已有的数据末尾)。如果你想以读取方式打开文件,可以省略第二个参数;'r' 是默认值。以下简短的程序将“Hello, World”写入文件:
file_object = open("myfile", 'w')
file_object.write("Hello, World\n")
file_object.close()
根据操作系统,open 也可能访问到额外的文件模式。这些模式对于大多数目的来说并不是必需的。随着你编写更高级的 Python 程序,你可能需要查阅 Python 参考手册以获取详细信息。
open 可以接受一个可选的第三个参数,该参数定义了该文件的读取或写入如何进行缓冲。缓冲 是将数据保留在内存中,直到请求或写入足够的数据以证明进行磁盘访问的时间成本是合理的。open 的其他参数控制文本文件的编码以及文本文件中换行符的处理。再次强调,这些功能通常不是你需要担心的事情,但随着你在 Python 中的使用变得更加高级,你可能想了解它们。
13.4. 读取和写入文本或二进制数据的功能
我已经介绍了最常用的文本文件读取函数 readline。此函数从 file 对象中读取并返回一行,包括行尾的任何换行符。如果没有更多内容可从文件中读取,readline 返回一个空字符串,这使得(例如)计算文件中的行数变得容易:
file_object = open("myfile", 'r')
count = 0
while file_object.readline() != "":
count = count + 1
print(count)
file_object.close()
对于这个特定问题,一个更短的方法来计算所有行数是使用内置的 readlines 方法,该方法读取文件中的所有行,并将它们作为字符串列表返回,每行一个字符串(包括尾随换行符):
file_object = open("myfile", 'r')
print(len(file_object.readlines()))
file_object.close()
如果你碰巧正在计算一个大型文件中的所有行数,当然,这种方法可能会导致你的计算机内存耗尽,因为它一次性将整个文件读入内存。如果你不幸尝试从一个不包含换行符的大型文件中读取一行,使用 readline 也可能导致内存溢出,尽管这种情况非常不可能。为了处理这种情况,readline 和 readlines 都可以接受一个可选参数,该参数影响它们在任何一次读取的数据量。有关详细信息,请参阅 Python 参考文档。
另一种遍历文件中所有行的方法是将 file 对象作为 for 循环中的迭代器处理:
file_object = open("myfile", 'r')
count = 0
for line in file_object:
count = count + 1
print(count)
file_object.close()
这种方法的优势在于,行是按需读入内存的,因此即使对于大文件,内存耗尽也不是一个担忧。这种方法的其他优势在于它更简单,更容易阅读。
read 方法的可能问题可能源于在 Windows 和 Macintosh 机器上,如果你使用 open 命令以文本模式打开,则会发生文本模式转换——也就是说,没有在模式中添加 b。在文本模式下,在 Macintosh 上,任何 \r 都会被转换为 "\n",而在 Windows 上,"\r\n" 对会被转换为 "\n"。你可以通过在打开文件时使用 newline 参数并指定 newline="\n"、"\r" 或 "\r\n" 来指定换行符的处理方式,这将强制只使用该字符串作为换行符:
input_file = open("myfile", newline="\n")
此示例强制只将 "\n" 考虑为换行符。如果文件是以二进制模式打开的,则不需要 newline 参数,因为所有字节都会按文件中的原始字节返回。
与 readline 和 readlines 方法对应的写入方法是 write 和 writelines 方法。请注意,没有 writeline 函数。write 方法写入单个字符串,如果字符串中嵌入换行符,则可以跨越多行,如下例所示:
myfile.write("Hello")
write 在写入其参数后不会写入换行符;如果你想在输出中包含换行符,你必须自己添加。如果你以文本模式打开文件(使用 w),则任何 \n 字符都会映射回平台特定的行结束符(即 Windows 平台上的 '\r\n' 或 Macintosh 平台上的 '\r')。再次强调,使用指定换行符打开文件可以防止这种情况。
writelines 有点名不副实,因为它不一定写入行;它接受一个字符串列表作为参数,并将它们依次写入给定的 file 对象,而不写入换行符。如果列表中的字符串以换行符结尾,则它们作为行写入;否则,它们在文件中实际上被连接起来。但 writelines 是 readlines 的精确逆操作,因为它可以用在 readlines 返回的列表上,以写入与 readlines 读取的文件相同的文件。假设 myfile.txt 存在并且是一个文本文件,以下代码创建了一个名为 myfile2.txt 的 myfile.txt 的精确副本:
input_file = open("myfile.txt", 'r')
lines = input_file.readlines()
input_file.close()
output = open("myfile2.txt", 'w')
output.writelines(lines)
output.close()
13.4.1. 使用二进制模式
在某些情况下,你可能希望将文件中的所有数据读入一个单独的 bytes 对象中,特别是如果数据不是字符串,并且你希望将其全部加载到内存中以便将其作为字节序列处理。或者你可能希望以固定大小的 bytes 对象读取文件中的数据。你可能正在读取没有显式换行符的数据,例如,假设每一行是固定大小的字符序列。为此,请使用 read 方法。如果没有提供任何参数,此方法将从当前位置读取文件的全部内容,并返回一个 bytes 对象。使用单个整数参数时,它将读取指定数量的字节(如果文件中没有足够的数据来满足请求,则读取的字节数可能更少),并返回一个给定大小的 bytes 对象:
input_file = open("myfile", 'rb')
header = input_file.read(4)
data = input_file.read()
input_file.close()
第一行以二进制模式打开文件进行读取,第二行读取前四个字节作为标题字符串,第三行将文件的其余部分作为单个数据读取。
请记住,以二进制模式打开的文件只处理字节,而不是字符串。要使用数据作为字符串,你必须将任何bytes对象解码为string对象。在处理网络协议时,这一点通常很重要,因为数据流通常表现为文件,但需要被解释为字节,而不是字符串。
快速检查
在文件打开模式字符串中添加一个"b"(如open("file", "wb"))有什么意义?
假设你想打开一个名为myfile.txt的文件并在其末尾写入附加数据。你将使用什么命令来打开myfile.txt?你将使用什么命令来重新打开文件以从头读取?
13.5. 使用 pathlib 进行读写
除了在第十二章中讨论的路径操作功能外,Path对象还可以用于读取和写入文本和二进制文件。这种能力可能很方便,因为不需要打开或关闭文件,并且使用不同的方法进行文本和二进制操作。然而,有一个限制,那就是你不能使用Path方法进行追加,因为写入会替换任何现有内容:
>>> from pathlib import Path
>>> p_text = Path('my_text_file')
>>> p_text.write_text('Text file contents')
18
>>> p_text.read_text()
'Text file contents'
>>> p_binary = Path('my_binary_file')
>>> p_binary.write_bytes(b'Binary file contents')
20
>>> p_binary.read_bytes()
b'Binary file contents'
13.6. 屏幕输入/输出和重定向
你可以使用内置的input方法提示并读取输入字符串:
>>> x = input("enter file name to use: ")
enter file name to use: myfile
>>> x
'myfile'
提示行是可选的,输入行末尾的换行符被删除。要使用input读取数字,你需要显式地将input返回的字符串转换为正确的数字类型。以下示例使用int:
>>> x = int(input("enter your number: "))
enter your number: 39
>>> x
39
input将它的提示写入到标准输出并从标准输入读取。可以通过使用sys模块来获得对这些和标准错误的更低级访问,该模块具有sys.stdin、sys.stdout和sys.stderr属性。这些属性可以被视为专门的file对象。
对于sys.stdin,你有read、readline和readlines方法。对于sys.stdout和sys.stderr,你可以使用标准的print函数以及write和writelines方法,它们的作用方式与其他file对象相同:
>>> import sys
>>> print("Write to the standard output.")
Write to the standard output.
>>> sys.stdout.write("Write to the standard output.\n")
Write to the standard output.
30 *1*
>>> s = sys.stdin.readline()
An input line
>>> s
'An input line\n'
- 1 sys.stdout.write 返回写入的字符数。
你可以将标准输入重定向到从文件中读取。同样,可以将标准输出或标准错误设置为写入文件,然后使用sys.__stdin__、sys.__stdout__和sys.__stderr__程序性地恢复它们的原始值:
>>> import sys
>>> f = open("outfile.txt", 'w')
>>> sys.stdout = f
>>> sys.stdout.writelines(["A first line.\n", "A second line.\n"]) *1*
>>> print("A line from the print function")
>>> 3 + 4 *2*
>>> sys.stdout = sys.__stdout__
>>> f.close()
>>> 3 + 4
7
-
1 在此行之后,输出文件
outfile.txt包含两行: 第一行 第二行 -
2 输出文件
outfile.txt现在包含三行: 第一行 第二行 来自打印函数的行
print函数也可以重定向到任何文件,而不会改变标准输出:
>>> import sys
>>> f = open("outfile.txt", 'w')
>>> print("A first line.\n", "A second line.\n", file=f) *1*
>>> 3 + 4
7
>>> f.close()
>>> 3 + 4
7
- 1 输出文件
outfile.txt包含: 第一行 第二行
在重定向标准输出时,你会收到提示和错误跟踪,但没有其他输出。如果你使用 IDLE,这些使用 sys.__ stdout__ 的示例不会按指示工作;你必须直接使用解释器的交互模式。
你通常会在运行脚本或程序时使用这种技术。但是,如果你在 Windows 上使用交互模式,你可能想暂时将标准输出重定向以捕获可能否则会滚动到屏幕上的内容。这里展示的简短模块实现了一组提供这种功能的函数。
列表 13.1. 文件 mio.py
"""mio: module, (contains functions capture_output, restore_output,
print_file, and clear_file )"""
import sys
_file_object = None
def capture_output(file="capture_file.txt"):
"""capture_output(file='capture_file.txt'): redirect the standard
output to 'file'."""
global _file_object
print("output will be sent to file: {0}".format(file))
print("restore to normal by calling 'mio.restore_output()'")
_file_object = open(file, 'w')
sys.stdout = _file_object
def restore_output():
"""restore_output(): restore the standard output back to the
default (also closes the capture file)"""
global _file_object
sys.stdout = sys.__stdout__
_file_object.close()
print("standard output has been restored back to normal")
def print_file(file="capture_file.txt"):
"""print_file(file="capture_file.txt"): print the given file to the
standard output"""
f = open(file, 'r')
print(f.read())
f.close()
def clear_file(file="capture_file.txt"):
"""clear_file(file="capture_file.txt"): clears the contents of the
given file"""
f = open(file, 'w')
f.close()
在这里,capture_output() 将标准输出重定向到默认为 "capture_file.txt" 的文件。函数 restore_output() 将标准输出恢复到默认状态。假设 capture_output 没有被执行,print_file() 将此文件打印到标准输出,而 clear_file() 清除其当前内容。
尝试这个:重定向输入和输出
编写一些代码来使用 列表 13.1 中的 mio.py 模块,将脚本的全部打印输出捕获到名为 myfile.txt 的文件中,将标准输出重置到屏幕,并将该文件打印到屏幕上。
13.7. 使用 struct 模块读取结构化二进制数据
通常来说,当你处理自己的文件时,你可能不希望在 Python 中读取或写入二进制数据。对于非常简单的存储需求,通常最好使用文本或字节输入和输出。对于更复杂的应用,Python 提供了轻松读取或写入任意 Python 对象的能力(称为 pickling,在 第 13.8 节 中描述)。这种能力比直接编写和读取自己的二进制数据要少出错,并且强烈推荐。
但至少有一种情况下,你可能会需要知道如何读取或写入二进制数据:当你处理由其他程序生成或使用的文件时。本节描述了如何使用 struct 模块来完成这项工作。有关更多详细信息,请参阅 Python 参考文档。
如你所见,Python 支持通过使用字节而不是字符串来显式地支持二进制输入或输出,如果你以二进制模式打开文件。但是,由于大多数二进制文件依赖于特定的结构来帮助解析值,因此编写自己的代码来正确地读取和分割它们到变量中通常比它值得的工作要多。相反,你可以使用标准的 struct 模块,允许你将这些字符串视为具有特定意义的格式化字节序列。
假设你想要读取一个名为 data 的二进制文件,该文件包含由 C 程序生成的记录序列。每个记录由一个 C 短整数、一个 C 双精度浮点数和一系列四个字符组成,这些字符应被视为一个四字符字符串。你希望将这些数据读取到 Python 的元组列表中,每个元组包含一个整数、一个浮点数和一个字符串。
首先要做的是定义一个struct模块能理解的格式字符串,它告诉模块你的记录中的数据是如何打包的。格式字符串使用对struct有意义的字符来指示记录中预期的数据类型。例如,字符'h'表示存在单个 C 短整型,而字符'd'表示存在单个 C 双精度浮点数。不出所料,'s'表示存在字符串。这些中的任何一个都可以由一个整数前缀,表示值的数量;在这种情况下,'4s'表示由四个字符组成的字符串。因此,对于你的记录,适当的格式字符串是'hd4s'。struct理解广泛的数字、字符和字符串格式。有关详细信息,请参阅Python 库参考。
在开始从你的文件中读取记录之前,你需要知道每次读取多少字节。幸运的是,struct包含一个calcsize函数,它接受你的格式字符串作为参数,并返回用于包含这种格式的数据的字节数。
要读取每条记录,你使用本章前面描述的read方法。然后struct.unpack函数会根据你的格式字符串解析读取的记录,方便地返回一个值的元组。读取你的二进制数据文件的程序非常简单:
import struct
record_format = 'hd4s'
record_size = struct.calcsize(record_format)
result_list = []
input = open("data", 'rb')
while 1:
record = input.read(record_size) *1*
if record == '': *2*
input.close()
break
result_list.append(struct.unpack(record_format, record)) *3*
-
1 读取单个记录
-
3 将记录解包到元组中;将结果添加到其中
如果记录为空,那么你已到达文件末尾,因此退出循环2。请注意,这里没有检查文件一致性;如果最后一个记录的大小是奇数,struct.unpack函数会引发错误。
如你所猜测,struct还提供了将 Python 值转换为打包字节的序列的能力。这种转换是通过struct.pack函数完成的,它几乎,但不是完全,是struct.unpack的逆操作。几乎是因为struct.unpack返回一个 Python 值的元组,而struct.pack不接收 Python 值的元组;相反,它接收一个格式字符串作为其第一个参数,然后接收足够多的额外参数以满足格式字符串。为了生成与前面示例中使用的格式相同的二进制记录,你可能做如下操作:
>>> import struct
>>> record_format = 'hd4s'
>>> struct.pack(record_format, 7, 3.14, b'gbye')
b'\x07\x00\x00\x00\x00\x00\x00\x00\x1f\x85\xebQ\xb8\x1e\t@gbye'
struct功能更强大;你可以在格式字符串中插入其他特殊字符,以指示数据应以大端、小端或机器本地端格式(默认为机器本地)读取/写入,以及指示像 C 短整型这样的数据应按机器本地(默认)或标准 C 大小来设置大小。如果你需要这些功能,知道它们存在是很好的。有关详细信息,请参阅Python 库参考。
快速检查:struct
你能想到哪些使用场景,其中struct模块对读取或写入二进制数据是有用的?
13.8. 序列化对象文件
Python 可以将任何数据结构写入文件,从文件中读取该数据结构,并使用几个命令重新创建它。这种能力不同寻常,但可能很有用,因为它可以节省你许多页面的代码,这些代码只是将程序状态保存到文件中(并且可以节省同样数量的代码,这些代码只是读取该状态)。
Python 通过 pickle 模块提供这种能力。序列化功能强大但使用简单。假设程序的全部状态都保存在三个变量 a、b 和 c 中。你可以将此状态保存到名为 state 的文件中,如下所示:
import pickle
.
.
.
file = open("state", 'wb')
pickle.dump(a, file)
pickle.dump(b, file)
pickle.dump(c, file)
file.close()
无论存储在 a、b 和 c 中的内容是什么,内容可能只是数字,也可能非常复杂,如包含用户定义类实例的字典列表。pickle.dump 会保存一切。
现在,为了在程序稍后的运行中读取这些数据,只需编写
import pickle
file = open("state", 'rb')
a = pickle.load(file)
b = pickle.load(file)
c = pickle.load(file)
file.close()
pickle.load 会将之前存储在变量 a、b 或 c 中的任何数据恢复到这些变量中。
pickle 模块可以以这种方式存储几乎任何东西。它可以处理列表、元组、数字、字符串、字典以及由这些类型对象组成的几乎所有东西,包括所有类实例。它还可以正确处理共享对象、循环引用和其他复杂的内存结构,只存储共享对象一次,并将它们作为共享对象恢复,而不是作为相同的副本。但是代码对象(Python 用来存储字节编译代码的东西)和系统资源(如文件或套接字)不能被序列化。
更多的时候,你不会想要使用 pickle 来保存整个程序状态。例如,大多数应用程序可以同时打开多个文档。如果你保存了整个程序状态,实际上你就是在同一个文件中保存了所有打开的文档。保存和恢复感兴趣数据的简单有效方法之一是编写一个保存函数,该函数将所有想要保存的数据存储到一个字典中,然后使用 pickle 来保存这个字典。然后你可以使用一个互补的恢复函数来读取这个字典(再次使用 pickle),并将字典中的值分配给适当的程序变量。这种技术还有一个优点,那就是没有可能以错误的顺序读取值——也就是说,与存储值的顺序不同的顺序。使用这种方法与前面的示例相结合,你得到的代码看起来可能像这样:
import pickle
.
.
.
def save_data():
global a, b, c
file = open("state", 'wb')
data = {'a': a, 'b': b, 'c': c}
pickle.dump(data, file)
file.close()
def restore_data():
global a, b, c
file = open("state", 'rb')
data = pickle.load(file)
file.close()
a = data['a']
b = data['b']
c = data['c']
.
.
这个例子有些牵强。你可能不会经常保存交互模式中顶层变量的状态。
一个实际应用是第七章中给出的缓存示例的扩展。第七章。在第七章中,您调用了一个基于其三个参数进行耗时计算的功能。在程序运行过程中,您对那个函数的许多调用最终都使用了相同的参数集。您通过在字典中缓存结果,以产生它们的参数为键,获得了显著的性能提升。但这种情况也是,这个程序在几天、几周和几个月的时间里被多次运行。因此,通过 pickle 缓存,您可以避免每次会话都需要从头开始。以下是您可能用于此目的的模块的简化版本。
列表 13.2. 文件 sole.py
"""sole module: contains functions sole, save, show"""
import pickle
_sole_mem_cache_d = {}
_sole_disk_file_s = "solecache"
file = open(_sole_disk_file_s, 'rb') *1*
_sole_mem_cache_d = pickle.load(file)
file.close()
def sole(m, n, t): *2*
"""sole(m, n, t): perform the sole calculation using the cache."""
global _sole_mem_cache_d
if _sole_mem_cache_d.has_key((m, n, t)):
return _sole_mem_cache_d[(m, n, t)]
else:
# . . . do some time-consuming calculations . . .
_sole_mem_cache_d[(m, n, t)] = result
return result
def save():
"""save(): save the updated cache to disk."""
global _sole_mem_cache_d, _sole_disk_file_s
file = open(_sole_disk_file_s, 'wb')
pickle.dump(_sole_mem_cache_d, file)
file.close()
def show():
"""show(): print the cache"""
global _sole_mem_cache_d
print(_sole_mem_cache_d)
-
1 初始化代码在模块加载时执行。
-
2 公共函数
此代码假定缓存文件已经存在。如果您想尝试使用它,请使用以下代码初始化缓存文件:
>>> import pickle
>>> file = open("solecache",'wb')
>>> pickle.dump({}, file)
>>> file.close()
当然,您还需要将注释# . . . 进行一些耗时的计算替换为实际的计算。请注意,对于生产代码,这种情况可能是您会使用绝对路径名来指定缓存文件的情况。此外,这里没有处理并发。如果两个人同时运行重叠的会话,您最终只会保存最后一个人的添加。如果这种情况是问题,您可以通过在save函数中使用字典更新方法显著限制重叠窗口。
13.8.1. 不应该使用 Pickles 的原因
虽然在前一种情况下使用 pickle 对象可能有些合理,但您也应该意识到 pickle 的缺点:
-
Pickles 作为一种序列化手段,既不是特别快也不是特别节省空间。即使使用 JSON 存储序列化对象,速度也会更快,并且生成的磁盘文件更小。
-
Pickles 不安全,加载包含恶意内容的 pickle 可能会导致在您的机器上执行任意代码。因此,如果 pickle 文件有可能被任何可能修改它的人访问,您应该避免使用 pickle。
快速检查:Pickles
考虑以下用例中 pickle 是否是一个好的解决方案:
-
从一次运行保存一些状态变量到下一次
-
为游戏保存高分列表
-
存储用户名和密码
-
存储大量英语术语的字典
13.9. 暂存对象
这个主题有些高级,但绝对不难。您可以将shelve对象想象成一个将数据存储在磁盘上的文件而不是内存中的字典,这意味着您仍然可以使用键来访问数据,但您不会受到可用 RAM 数量的限制。
本节可能对那些工作涉及在大型文件中存储或访问数据片段的人最有兴趣,因为 Python 的shelve模块正是这样做的:允许在不读取或写入整个文件的情况下读取或写入大型文件中的数据片段。对于执行大量大型文件访问的应用程序(如数据库应用程序),节省的时间可能非常显著。像它所使用的pickle模块一样,shelve模块很简单。
在本节中,您将通过地址簿来探索这个模块。这类东西通常足够小,以至于整个地址簿可以在应用程序启动时一次性读入,并在应用程序完成后写出。如果你是一个非常友好的人,并且你的地址簿对于这个例子来说太大,那么最好使用shelve并且不用担心它。
假设你的地址簿中的每个条目由三个元素的元组组成,分别给出一个人的名字、电话号码和地址。每个条目都按条目所引用的人的姓氏进行索引。这种设置非常简单,以至于你的应用程序将是一个与 Python 壳的交互式会话。
首先,导入shelve模块,并打开地址簿。shelve.open如果不存在,则创建地址簿文件:
>>> import shelve
>>> book = shelve.open("addresses")
现在添加几个条目。注意,你将shelve.open返回的对象当作字典来处理(尽管它是一个只能使用字符串作为键的字典):
>>> book['flintstone'] = ('fred', '555-1234', '1233 Bedrock Place')
>>> book['rubble'] = ('barney', '555-4321', '1235 Bedrock Place')
最后,关闭文件并结束会话:
>>> book.close()
那又如何呢?好吧,在同一个目录下,再次启动 Python,并打开相同的地址簿:
>>> import shelve
>>> book = shelve.open("addresses")
但现在,不是输入新内容,而是看看你之前输入的内容是否还在:
>>> book['flintstone']
('fred', '555-1234', '1233 Bedrock Place')
在第一次交互式会话中,shelve.open创建的地址簿文件就像一个持久的字典。你之前输入的数据已经存储到磁盘上,即使你没有进行显式的磁盘写入。这正是shelve所做的事情。
更普遍地说,shelve.open返回一个shelf对象,它允许基本的字典操作,如键赋值或查找、del、in和keys方法。但是,与正常的字典不同,shelf对象将数据存储在磁盘上,而不是内存中。不幸的是,与字典相比,shelf对象有一个显著的限制:它们只能使用字符串作为键,而字典允许使用广泛的键类型。
理解shelf对象在处理大数据集时相对于字典的优势是很重要的。shelve.open使文件可访问;它不会将整个shelf对象文件读入内存。文件访问仅在需要时进行(通常,当查找元素时),文件结构以这种方式维护,使得查找非常快速。即使你的数据文件非常大,也只需要几次磁盘访问就可以在文件中定位所需的对象,这可以在几个方面提高你的程序。程序可能启动更快,因为它不需要将可能很大的文件读入内存。此外,程序可能执行得更快,因为更多的内存可供程序的其他部分使用;因此,需要交换到虚拟内存中的代码更少。你可以操作那些否则太大而无法放入内存的数据集。
当使用shelve模块时,你有一些限制。如前所述,shelf对象键只能是字符串,但任何可以序列化的 Python 对象都可以存储在shelf对象的键下。此外,shelf对象不适合多用户数据库,因为它们不提供对并发访问的控制。确保你在完成时关闭shelf对象;关闭有时是必需的,以便将你已做的更改(条目或删除)写回磁盘。
如其所述,列表 13.1 中的缓存示例是使用 shelves 处理的绝佳候选。例如,你不必依赖用户明确地将他们的工作保存到磁盘上。唯一可能的问题是,当你将数据写回文件时,你不会拥有低级控制。
快速检查:Shelve
使用shelf对象看起来非常像使用字典。在哪些方面使用shelf对象不同?你预计使用shelf对象会有哪些缺点?
实验室 13:wc 的最终修复
如果你查看wc实用程序的man页面,你会看到两个执行非常相似任务的命令行选项。-c使实用程序计算文件中的字节数,而-m使它计算字符数(在某些 Unicode 字符的情况下,字符可能占用两个或更多字节)。此外,如果提供了文件,它应该从该文件读取并处理,如果没有提供文件,它应该从标准输入读取并处理。
将你的wc实用程序版本重写,以实现区分字节和字符的功能,并能够从文件和标准输入中读取。
概述
-
在 Python 中,文件输入和输出使用各种内置函数来打开、读取、写入和关闭文件。
-
除了读取和写入文本外,
struct模块还赋予你读取或写入打包的二进制数据的能力。 -
pickle和shelve模块提供了简单、安全且强大的方法来保存和访问任意复杂的 Python 数据结构。
第十四章. 异常
本章涵盖
-
理解异常
-
在 Python 中处理异常
-
使用
with关键字
本章讨论了异常,这是专门针对在程序执行期间处理异常情况的语言特性。异常最常见的使用是处理程序执行期间出现的错误,但它们也可以有效地用于许多其他目的。Python 提供了一套全面的异常,用户可以定义新的异常以供自己使用。
异常作为错误处理机制的概念已经存在了一段时间。C 和 Perl,最常用的系统和脚本语言,都不提供任何异常功能,甚至使用像 C++这样的语言(它确实包括异常)的程序员通常也不熟悉它们。本章不假设你对异常有了解,而是提供详细的解释。
14.1. 异常简介
以下部分提供了异常的介绍以及它们是如何被使用的。如果你已经熟悉异常,可以直接跳转到“Python 中的异常”(第 14.2 节)。
14.1.1. 错误和异常处理的通用哲学
任何程序在执行过程中都可能遇到错误。为了说明异常,我考虑了一个字处理器将文件写入磁盘的情况,因此可能在所有数据都写入之前就耗尽了磁盘空间。有各种方法来处理这个问题。
解决方案 1:不处理问题
处理这种磁盘空间问题的最简单方法是假设你写的任何文件都会有足够的磁盘空间,你不需要担心这个问题。不幸的是,这似乎是最常用的选项。对于处理少量数据的小程序来说,这通常是可容忍的,但对于更关键的程序来说,这完全不能令人满意。
解决方案 2:所有函数返回成功/失败状态
错误处理的下一个高级层次是意识到错误会发生,并定义一种使用标准语言机制来检测和处理错误的方法。有无数种方法可以做到这一点,但典型的方法是让每个函数或过程返回一个状态值,表示该函数或过程调用是否成功执行。正常结果可以通过引用参数传递回调用。
考虑一下这个解决方案如何与一个假设的文字处理程序一起工作。假设程序调用一个高级函数save_to_file来将当前文档保存到文件。这个函数调用子函数将整个文档的不同部分保存到文件中,例如save_text_to_file用于保存实际文档文本,save_prefs_to_file用于保存该文档的用户偏好设置,save_formats_to_file用于保存用户定义的文档格式,等等。这些子函数中的任何一个都可能进一步调用自己的子函数,将更小的部分保存到文件中。在底层是内置的系统函数,它们将原始数据写入文件,并报告文件写入操作的成功或失败。
你可以在可能遇到磁盘空间错误的每个函数中放入错误处理代码,但这种做法几乎没有意义。错误处理器唯一能做的就是弹出一个对话框告诉用户没有更多磁盘空间,并要求用户删除一些文件再次保存。在每次磁盘写入操作中重复此代码是没有意义的。相反,将一段错误处理代码放入主要的磁盘写入函数中:save_to_file。
不幸的是,为了让save_to_file能够确定何时调用此错误处理代码,它调用的每个写入磁盘的函数都必须自己检查磁盘空间错误,并返回一个表示磁盘写入成功或失败的状态值。此外,save_to_file函数必须显式检查每个调用写入磁盘的函数,即使它不关心哪个函数失败。使用类似 C 语言的语法,代码看起来可能像这样:
const ERROR = 1;
const OK = 0;
int save_to_file(filename) {
int status;
status = save_prefs_to_file(filename);
if (status == ERROR) {
...handle the error...
}
status = save_text_to_file(filename);
if (status == ERROR) {
...handle the error...
}
status = save_formats_to_file(filename);
if (status == ERROR) {
...handle the error...
}
.
.
.
}
int save_text_to_file(filename) {
int status;
status = ...lower-level call to write size of text...
if (status == ERROR) {
return(ERROR);
}
status = ...lower-level call to write actual text data...
if (status == ERROR) {
return(ERROR);
}
.
.
.
}
同样适用于save_prefs_to_file、save_formats_to_file以及所有直接写入filename或以任何方式调用写入filename的函数的其他函数。
在这种方法论下,用于检测和处理错误的代码可能成为整个程序的一个重要部分,因为每个可能引发错误的函数和过程都需要包含检查错误的代码。通常,程序员没有时间或精力进行这种完整的错误检查,结果程序变得不可靠且容易崩溃。
解决方案 3:异常机制
显然,在前面类型程序中的大部分错误检查代码都是重复的:代码在每次尝试文件写入时检查错误,并在检测到错误时将错误状态信息传递回调用过程。磁盘空间错误仅在顶级save_to_file中处理。换句话说,大部分错误处理代码是管道代码,它将错误发生的地方与错误处理的地方连接起来。你真正想要做的是消除这种管道,编写看起来像这样的代码:
def save_to_file(filename)
try to execute the following block
save_text_to_file(filename)
save_formats_to_file(filename)
save_prefs_to_file(filename)
.
.
.
except that, if the disk runs out of space while
executing the above block, do this
...handle the error...
def save_text_to_file(filename)
...lower-level call to write size of text...
...lower-level call to write actual text data...
.
.
.
错误处理代码完全从底层函数中移除;错误(如果发生)由内置的文件写入例程生成,并直接传播到save_to_file例程,在那里你的错误处理代码(可能)会处理它。虽然你不能在 C 语言中编写此代码,但提供异常的语言允许这种类型的行为——当然,Python 就是这样一种语言。异常让你能够编写更清晰的代码并更好地处理错误条件。
14.1.2. 异常的更正式定义
生成异常的行为被称为抛出或引发异常。在先前的例子中,所有异常都是由磁盘写入函数抛出的,但异常也可以由任何其他函数抛出,或者可以由你自己的代码显式抛出。在先前的例子中,如果磁盘空间不足,低级别的磁盘写入函数(在代码中未显示)会抛出异常。
对异常做出响应的行为被称为捕获异常,处理异常的代码被称为异常处理代码或简称为异常处理器。在例子中,except that...行捕获了磁盘写入异常,而...handle the error...行所在的位置的代码将是一个磁盘写入(空间不足)异常的异常处理器。可能还有其他类型的异常处理器,甚至可能是同一类型的异常处理器,但位于你的代码的另一个位置。
14.1.3. 处理不同类型的异常
根据确切导致异常的事件,程序可能需要采取不同的行动。当磁盘空间耗尽时抛出的异常需要与如果你内存不足时抛出的异常完全不同的处理方式,而且这两种异常与当发生除以零错误时出现的异常也完全不同。处理这些不同类型的异常的一种方法是在全局记录一个指示异常原因的错误消息,并让所有异常处理程序检查这个错误消息并采取适当的行动。在实践中,一种不同的方法已被证明要灵活得多。
而不是定义一种单一的异常类型,Python,就像大多数实现异常的现代语言一样,定义了与可能发生的各种问题相对应的不同类型的异常。根据底层事件,可能会引发不同类型的异常。此外,捕获异常的代码可能被指示只捕获某些类型。这一特性在本书前面解决方案 3 中的伪代码中得到了应用,该伪代码表示“如果磁盘空间不足,则执行此操作”;这个伪代码指定了这段特定的异常处理代码只对磁盘空间异常感兴趣。其他类型的异常不会被该异常处理代码捕获。这种异常会被寻找数值异常的异常处理器捕获,或者(如果不存在这样的异常处理器)会导致程序因错误而提前退出。
14.2. Python 中的异常
本章的剩余部分将专门讨论 Python 中内置的异常机制。整个 Python 异常机制都是围绕面向对象范式构建的,这使得它既灵活又可扩展。如果你不熟悉面向对象编程(OOP),你不需要学习面向对象技术来使用异常。
异常是由 Python 函数使用raise语句自动生成的对象。在对象生成后,引发异常的raise语句会导致 Python 程序的执行方式与通常情况不同。在执行raise或引发异常的代码之后的下一个语句之前,会搜索一个可以处理生成的异常的处理程序。如果找到了这样的处理程序,它就会被调用,并且可以访问异常对象以获取更多信息。如果没有找到合适的异常处理程序,程序会因错误而终止。
求其宽恕,不如求其许可
Python 处理错误情况的方式与 Java 等语言中常见的处理方式不同。这些语言在发生之前尽可能多地检查可能出现的错误,因为异常发生后的处理往往在各方面都代价高昂。这种风格在本章的第一节中进行了描述,有时被称为“三思而后行”(LBYL)方法。
相反,Python 更有可能依赖异常来处理已发生的错误。尽管这种依赖可能看起来有些风险,但如果异常使用得当,代码会更简洁、更容易阅读,并且错误只会在发生时处理。这种 Python 处理错误的方法通常用短语“求其宽恕,不如求其许可”(EAFP)来描述。
14.2.1. Python 异常的类型
有可能生成不同类型的异常来反映报告的错误或异常情况的真实原因。Python 3.6 提供了几种异常类型:
BaseException
SystemExit
KeyboardInterrupt
GeneratorExit
Exception
StopIteration
ArithmeticError
FloatingPointError
OverflowError
ZeroDivisionError
AssertionError
AttributeError
BufferError
EOFError
ImportError
ModuleNoteFoundError
LookupError
IndexError
KeyError
MemoryError
NameError
UnboundLocalError
OSError
BlockingIOError
ChildProcessError
ConnectionError
BrokenPipeError
ConnectionAbortedError
ConnectionRefusedError
ConnectionResetError
FileExistsError
FileNotFoundError
InterruptedError
IsADirectoryError
NotADirectoryError
PermissionError
ProcessLookupError
TimeoutError
ReferenceError
RuntimeError
NotImplementedError
RecursionError
SyntaxError
IndentationError
TabError
SystemError
TypeError
ValueError
UnicodeError
UnicodeDecodeError
UnicodeEncodeError
UnicodeTranslateError
Warning
DeprecationWarning
PendingDeprecationWarning
RuntimeWarning
SyntaxWarning
UserWarning
FutureWarning
ImportWarning
UnicodeWarning
BytesWarningException
ResourceWarning
Python 的异常集是按层次结构组织的,如本异常列表中的缩进所示。正如你在前一章中看到的,你可以从__builtins__模块中获取一个按字母顺序排列的列表。
每种异常类型都是一个 Python 类,它继承自其父异常类型。但如果你还没有接触面向对象编程,不必担心。例如,IndexError也是一个LookupError,通过继承也是Exception和BaseException。
这种层次结构是有意为之的:大多数异常都继承自Exception,强烈建议任何用户定义的异常也继承自Exception,而不是BaseException。原因是如果你这样设置代码
try:
# do stuff
except Exception:
# handle exceptions
你仍然可以在try块中通过 Ctrl-C 中断代码,而不会触发异常处理代码,因为KeyboardInterrupt异常不是Exception的子类。
你可以在文档中找到每种异常含义的解释,但随着编程的深入,你将迅速熟悉最常见的异常类型!
14.2.2. 抛出异常
许多 Python 内置函数都会引发异常:
>>> alist = [1, 2, 3]
>>> element = alist[7]
Traceback (innermost last):
File "<stdin>", line 1, in ?
IndexError: list index out of range
Python 内置的错误检查代码检测到第二行输入请求的列表索引不存在,并引发一个IndexError异常。这个异常会一直传播到顶级(交互式 Python 解释器),它通过打印出一条消息来处理这个异常,说明异常已经发生。
你也可以通过使用raise语句在你的代码中显式地引发异常。这个语句的最基本形式是
raise exception(args)
代码中的exception(args)部分创建了一个异常。新异常的参数通常是帮助你确定发生了什么的值——这一点我将在下一节讨论。异常创建后,raise语句会将它向上抛出到调用raise语句的 Python 函数堆栈中。新的异常会抛到最近的(在堆栈上的)异常捕获器,寻找该类型的异常。如果在程序的最高级别找不到捕获器,程序将因错误而终止(在交互式会话中)或在控制台打印出错误信息。
尝试以下操作:
>>> raise IndexError("Just kidding")
Traceback (innermost last):
File "<stdin>", line 1, in ?
IndexError: Just kidding
在这里使用raise生成的错误消息乍一看与迄今为止看到的所有 Python 列表索引错误消息相似。但仔细检查会发现并非如此。实际报告的错误并不像那些其他错误那样严重。
在创建异常时使用字符串参数是常见的。大多数内置的 Python 异常,如果提供了第一个参数,假设该参数是一个消息,用于向您显示作为发生事件的解释。但这并不总是如此,因为每个异常类型都是其自己的类,当创建该类的新异常时预期的参数完全由类定义决定。此外,程序员定义的异常,无论是您创建的还是其他程序员创建的,通常用于除错误处理之外的原因;因此,它们可能不包含文本消息。
14.2.3. 捕获和处理异常
关于异常的重要之处不在于它们会导致程序因错误消息而停止。在程序中实现这种功能从来不是什么大问题。异常的特殊之处在于它们不必导致程序停止。通过定义适当的异常处理程序,您可以确保常见的异常情况不会导致程序失败;也许它们会向用户显示错误消息或执行其他操作,甚至可能是修复问题,但它们不会使程序崩溃。
Python 用于捕获和处理异常的基本语法如下,使用try、except和有时else关键字:
try:
body
except exception_type1 as var1:
exception_code1
except exception_type2 as var2:
exception_code2
.
.
.
except:
default_exception_code
else:
else_body
finally:
finally_body
try语句通过首先执行语句的body部分来执行。如果此执行成功(即没有抛出要由try语句捕获的异常),则执行else_body,然后try语句完成。由于存在finally语句,因此会执行finally_body。如果向try抛出异常,则会按顺序搜索except子句,寻找与抛出的异常类型匹配的一个。如果找到匹配的except子句,则抛出的异常被分配给与关联异常类型命名的变量,并执行与匹配异常关联的异常代码体。如果except exception_type as var:行与某些抛出的异常exc匹配,则创建变量var,在执行except语句的异常处理代码之前,将exc分配给var的值。您不需要放入var;您可以说类似except exception_type:的内容,这仍然可以捕获给定类型的异常,但不会将它们分配给任何变量。
如果没有找到匹配的except子句,则抛出的异常无法由该try语句处理,异常将被抛到调用链的更高处,希望某个封装的try能够处理它。
try 语句的最后一条 except 子句可以可选地不引用任何异常类型,在这种情况下,它将处理所有类型的异常。这种技术对于某些调试和极快的原型设计可能很方便,但通常不是一个好主意:所有错误都被 except 子句隐藏,这可能导致你的程序出现一些令人困惑的行为。
try 语句的 else 子句是可选的,很少使用。这个子句仅在 try 语句的主体执行而没有抛出任何错误时执行。
try 语句的 finally 子句也是可选的,并在 try、except 和 else 部分执行后执行。如果在 try 块中引发异常,并且没有被任何 except 块处理,那么在 finally 块执行后,该异常将被再次引发。因为 finally 块总是会执行,这给你一个机会来包含代码,以便在关闭文件、重置变量等操作后清理任何异常处理。
尝试这样做:捕获异常
编写代码,从用户那里获取两个数字并将第一个数字除以第二个数字。检查并捕获如果第二个数字为零时发生的异常(ZeroDivisionError)。
14.2.4. 定义新异常
你可以轻松地定义自己的异常。以下两行为你做了这件事:
class MyError(Exception):
pass
这段代码创建了一个从基类 Exception 继承所有内容的类。但如果你不想这么做,你不必担心。
你可以像处理任何其他异常一样引发、捕获和处理这个异常。如果你给它一个单一参数(并且你没有捕获和处理它),它将在跟踪信息结束时打印出来:
>>> raise MyError("Some information about what went wrong")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
__main__.MyError: Some information about what went wrong
当然,这个参数也适用于你编写的处理程序:
try:
raise MyError("Some information about what went wrong")
except MyError as error:
print("Situation:", error)
结果是
Situation: Some information about what went wrong
如果你用多个参数引发你的异常,这些参数作为元组传递给你的处理程序,你可以通过错误的 args 变量访问它:
try:
raise MyError("Some information", "my_filename", 3)
except MyError as error:
print("Situation: {0} with file {1}\n error code: {2}".format(
error.args[0],
error.args[1], error.args[2]))
结果是
Situation: Some information with file my_filename
error code: 3
因为异常类型在 Python 中是一个常规类,并且恰好继承自根 Exception 类,所以创建自己的异常子层次结构以供自己的代码使用是一件简单的事情。你不必在阅读这本书的第一遍时担心这个过程。你可以在阅读完第十五章后再回来。你如何创建自己的异常取决于你的特定需求。如果你正在编写一个可能只生成几个独特错误或异常的小程序,就像这里所做的那样,你可以将主 Exception 类作为子类。如果你正在编写一个具有特殊目标的大型多文件代码库——比如说,天气预报——你可能会决定定义一个名为 WeatherLibraryException 的独特类,然后定义库的所有独特异常作为 WeatherLibraryException 的子类。
快速检查:异常作为类
如果 MyError 继承自 Exception,那么 except Exception as e 和 except MyError as e 之间有什么区别?
14.2.5. 使用断言语句调试程序
assert 语句是 raise 语句的专用形式:
assert expression, argument
如果 expression 评估为 False 并且系统变量 __debug__ 是 True,则会引发带有可选 argument 的 AssertionError 异常。__debug__ 变量默认为 True,可以通过使用 -O 或 -OO 选项启动 Python 解释器或通过将系统变量 PYTHONOPTIMIZE 设置为 True 来关闭它。可选参数可以用来包含对断言的解释。
如果 __debug__ 是 False,代码生成器不会为断言语句生成代码。您可以在开发期间使用 assert 语句将调试语句插入到代码中,并在代码中保留它们以供将来可能的用途,而不会在常规使用期间产生运行时成本:
>>> x = (1, 2, 3)
>>> assert len(x) > 5, "len(x) not > 5"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError: len(x) not > 5
尝试这个:断言语句
编写一个简单的程序,从用户那里获取一个数字,然后使用 assert 语句在数字为零时引发异常。测试以确保 assert 语句被触发;然后关闭它,使用本节中提到的任何一种方法。
14.2.6. 异常继承层次结构
在本节中,我扩展了之前关于 Python 异常是层次结构化的概念,以及这个结构在 except 子句捕获异常方面的含义。
以下代码
try:
body
except LookupError as error:
exception code
except IndexError as error:
exception code
捕获两种类型的异常:IndexError 和 LookupError。碰巧的是 IndexError 是 LookupError 的子类。如果 body 抛出 IndexError,则该错误首先由 except LookupError as error: 行检查,因为 IndexError 通过继承是 LookupError,第一个 except 子句成功。第二个 except 子句永远不会使用,因为它被第一个 except 子句包含。
相反,交换两个 except 子句的顺序可能是有用的;那么第一个子句将处理 IndexError 异常,第二个子句将处理任何不是 IndexError 错误的 LookupError 异常。
14.2.7. 示例:Python 中的磁盘写入程序
在本节中,我重新审视了需要检查磁盘空间不足条件的字处理程序示例:
def save_to_file(filename) :
try:
save_text_to_file(filename)
save_formats_to_file(filename)
save_prefs_to_file(filename)
.
.
.
except IOError:
...handle the error...
def save_text_to_file(filename):
...lower-level call to write size of text...
...lower-level call to write actual text data...
.
.
.
注意错误处理代码是如何不引人注目的;它在 save_to_file 函数中的主要磁盘写入调用序列周围包装。没有任何辅助磁盘写入函数需要任何错误处理代码。首先开发程序然后添加错误处理代码很容易。程序员通常就是这样做的,尽管这种事件顺序并不是最优的。
另一个有趣的点是,此代码不会专门响应磁盘满的错误;相反,它响应 IOError 异常,Python 的内置函数在无法完成 I/O 请求时自动引发该异常,无论原因如何。这可能满足您的需求,但如果您需要识别磁盘满的情况,您可以做一些事情。except 块可以检查磁盘上还有多少空间。如果磁盘空间不足,显然问题是磁盘满的问题,应该在 except 块中处理;否则,except 块中的代码可以将 IOError 抛向调用链的更高层,由其他 except 处理。如果这个解决方案不够充分,您可以采取更极端的措施,例如进入 Python 磁盘写入函数的 C 源代码,并在需要时引发自己的 DiskFull 异常。我不推荐后者,但如果你需要使用它,了解这种可能性是好的。
14.2.8. 示例:正常评估中的异常
异常通常用于错误处理,但在某些涉及正常评估的情况中也非常有用。考虑实现类似电子表格的功能时的问题。像大多数电子表格一样,它必须允许涉及单元格的算术运算,并且它还允许单元格包含除数字以外的值。在这种情况下,在数值计算中使用的空白单元格可能被认为是包含值 0,而包含任何其他非数字字符串的单元格可能被认为是无效的,并以 Python 值 None 表示。涉及无效值的任何计算都应该返回无效值。
第一步是编写一个函数,该函数评估电子表格单元格中的字符串并返回适当的值:
def cell_value(string):
try:
return float(string)
except ValueError:
if string == "":
return 0
else:
return None
Python 的异常处理能力使得编写这个函数变得简单。代码尝试将单元格中的字符串转换为数字,并使用内置的 float 函数在 try 块中返回它。如果 float 无法将其字符串参数转换为数字,则会引发 ValueError 异常,因此代码会捕获该异常,并根据参数字符串是否为空返回 0 或 None。
下一步是处理可能需要处理None值的情况。在没有异常的语言中,通常的做法是定义一组自定义的算术函数,这些函数会检查它们的参数是否为None,然后使用这些函数而不是内置的算术函数来执行所有的电子表格算术。然而,这个过程既耗时又容易出错,并且会导致执行缓慢,因为你实际上在你的电子表格中构建了一个解释器。这个项目采取了一种不同的方法。所有的电子表格公式实际上可以是 Python 函数,这些函数接受正在评估的单元格的 x 和 y 坐标以及电子表格本身作为参数,并使用标准的 Python 算术运算符通过cell_value从电子表格中提取必要值来计算给定单元格的结果。你可以定义一个名为safe_apply的函数,该函数在一个try块中将其中一个公式应用于适当的参数,并根据公式是否成功评估返回公式结果或None:
def safe_apply(function, x, y, spreadsheet):
try:
return function(x, y, spreadsheet)
except TypeError:
return None
这两个更改足以将空(None)值的理念整合到电子表格的语义中。试图在不使用异常的情况下开发这种能力是一个高度教育性的练习。
14.2.9. 应该在哪里使用异常
异常几乎可以处理任何错误条件。不幸的是,错误处理通常是在程序的大部分内容基本完成时添加的,但异常特别擅长以可理解的方式管理这种事后错误处理代码(或者更乐观地说,当你事后添加更多错误处理时)。
异常在需要丢弃大量处理的情况中也非常有用,当程序中的计算分支变得不可行时,这种情况就变得明显。电子表格示例就是这样一种情况;其他情况包括分支和界限算法和解析算法。
快速检查:异常
Python 异常会强制程序停止吗?
假设你想要访问字典x时,如果键不存在于字典中(即如果引发KeyError异常),总是返回None。你会使用什么代码?
尝试这个:异常
你会用什么代码来创建一个自定义的ValueTooLarge异常,并在变量x超过 1000 时引发这个异常?
14.3. 使用 with 关键字进行上下文管理
一些情况,如读取文件,遵循一个可预测的模式,有一个明确的开始和结束。在读取文件的情况下,通常只需要打开一次文件:当数据正在读取时。然后可以关闭文件。在异常的术语中,你可以这样编码这种类型的文件访问:
try:
infile = open(filename)
data = infile.read()
finally:
infile.close()
Python 3 提供了一种更通用的方式来处理这种情况:上下文管理器。上下文管理器封装一个块,并管理块进入和离开时的需求,并由 with 关键字标记。文件对象是上下文管理器,你可以使用这种能力来读取文件:
with open(filename) as infile:
data = infile.read()
这两行代码与之前的五行代码等价。在两种情况下,你都知道文件将在最后一次读取后立即关闭,无论操作是否成功。在第二种情况下,文件的关闭也得到了保证,因为它属于文件对象的上下文管理的一部分,因此你不需要编写代码。换句话说,通过使用 with 与上下文管理(在这种情况下是文件对象)结合,你不需要担心常规的清理工作。
如你所料,如果你需要,也可以创建自己的上下文管理器。你可以通过查看标准库中 contextlib 模块的文档来了解更多关于如何创建上下文管理器以及它们可以以哪些方式被操作的信息。
上下文管理器非常适合诸如锁定和解锁资源、关闭文件、提交数据库事务等情况。自从它们被引入以来,上下文管理器已经成为此类用例的标准最佳实践。
快速检查:上下文管理器
假设你在一个脚本中使用上下文管理器来读取和/或写入多个文件。你认为以下哪种方法最好?
-
将整个脚本放在由
with语句管理的块中。 -
对于所有文件读取使用一个
with语句,对于所有文件写入使用另一个with语句。 -
每次读取或写入文件时(例如,对于每一行),都使用一个
with语句。 -
对于你读取或写入的每个文件,使用一个
with语句。
实验 14:自定义异常
想想你在第九章第九章中编写的用于计算词频的模块。这些函数中可能合理地出现哪些错误?将这些函数重构以适当地处理这些异常条件。
摘要
-
Python 的异常处理机制和异常类提供了一套丰富的系统来处理代码中的运行时错误。
-
通过使用
try、except、else和finally块,并选择甚至创建捕获的异常类型,你可以非常精细地控制异常的处理和忽略方式。 -
Python 的哲学是,除非它们被明确地静默处理,否则错误不应该无声地通过。
-
Python 的异常类型是按层次结构组织的,因为异常,像 Python 中的所有对象一样,都是基于类的。
第三部分. 高级语言特性
前几章已经概述了 Python 的基本特性:大多数程序员在大多数情况下会使用的特性。接下来,我们将探讨一些更高级的特性,虽然你可能不是每天都会用到(这取决于你的需求),但在你需要时它们是至关重要的。
第十五章. 类和面向对象编程
本章涵盖
-
定义类
-
使用实例变量和
@property -
定义方法
-
定义类变量和方法
-
从其他类继承
-
使变量和方法私有
-
从多个类继承
在本章中,我讨论了 Python 类,它可以用来存储数据和代码。尽管大多数程序员可能熟悉其他语言中的类或对象,但我对特定语言或范式的知识没有做出任何特定的假设。此外,本章仅描述 Python 中可用的构造,而不是面向对象编程(OOP)本身的阐述。
15.1. 定义类
Python 中的类实际上是一种数据类型。Python 中构建的所有数据类型都是类,Python 为您提供了强大的工具来操纵类的每个方面。您使用class语句定义类:
class MyClass:
body
body是 Python 语句的列表——通常是变量赋值和函数定义。不需要任何赋值或函数定义。主体可以只是一个单独的pass语句。
按照惯例,类标识符使用 CapCase——即每个组成部分的首字母大写,以便使标识符突出。定义类后,您可以通过将类名作为函数调用来创建该类类型的新对象(类的实例):
instance = MyClass()
15.1.1. 使用类实例作为结构或记录
类实例可以用作结构或记录。与 C 结构或 Java 类不同,实例的数据字段不需要提前声明;它们可以即时创建。以下简短示例定义了一个名为Circle的类,创建了一个Circle实例,将值赋给圆的radius字段,然后使用该字段计算圆的周长:
>>> class Circle:
... pass
...
>>> my_circle = Circle()
>>> my_circle.radius = 5
>>> print(2 * 3.14 * my_circle.radius)
31.4
如同 Java 和许多其他语言一样,实例/结构的字段通过点符号进行访问和赋值。
您可以通过在类体中包含一个__init__初始化方法来自动初始化实例的字段。每当创建类的实例时,都会运行此函数,并将新实例作为其第一个参数,即self。__init__方法类似于 Java 中的构造函数,但它实际上并不构建任何东西;它初始化类的字段。此外,与 Java 和 C++不同,Python 类可能只有一个__init__方法。以下示例通过默认半径为1创建圆形:
class Circle:
def __init__(self): *1*
self.radius = 1
my_circle = Circle() *2*
print(2 * 3.14 * my_circle.radius) *3*
6.28
my_circle.radius = 5 *4*
print(2 * 3.14 * my_circle.radius) *5*
31.400000000000002
按照惯例,self总是__init__的第一个参数的名称。当__init__运行时,self被设置为新创建的圆实例 1。接下来,代码使用类定义。你首先创建一个Circle实例对象 2。下一行利用了半径字段已经初始化的事实 3。你也可以覆盖半径字段 4;因此,最后一行打印的结果与之前的print语句不同 5。
Python 还有一个类似于构造函数的东西:__new__方法,它在对象创建时被调用,并返回一个未初始化的对象。除非你正在子类化不可变类型,如str或int,或者使用元类来修改对象创建过程,否则很少会覆盖现有的__new__方法。
通过使用真正的面向对象编程(OOP),你可以做更多的事情,如果你不熟悉它,我强烈建议你查阅相关资料。Python 的 OOP 构造是本章剩余部分的主题。
15.2. 实例变量
实例变量是 OOP 中最基本的功能。再次查看Circle类:
class Circle:
def __init__(self):
self.radius = 1
radius是Circle实例的实例变量。也就是说,Circle类的每个实例都有自己的radius副本,其中存储的值可能与其他实例中存储在radius变量中的值不同。在 Python 中,你可以根据需要通过将值赋给类实例的字段来创建实例变量:
instance.variable = value
如果变量尚未存在,它将自动创建,这就是__init__创建radius变量的方式。
所有实例变量的使用,无论是赋值还是访问,都需要明确指出包含的实例——即instance.variable。仅通过variable引用不是对实例变量的引用,而是对执行方法中的局部变量的引用。这与 C++和 Java 不同,在 C++和 Java 中,实例变量以与局部方法函数变量相同的方式引用。我非常喜欢 Python 要求明确指出包含实例的要求,因为它清楚地区分了实例变量和局部函数变量。
尝试这个:实例变量
你会用什么代码来创建一个Rectangle类?
15.3. 方法
方法是与特定类相关联的函数。你已经看到了特殊的__init__方法,当创建新实例时,会调用该方法。在下面的示例中,你为Circle类定义了另一个方法,area;此方法可以用于计算并返回任何Circle实例的面积。像大多数用户定义的方法一样,area使用类似于实例变量访问的方法调用语法来调用:
>>> class Circle:
... def __init__(self):
... self.radius = 1
... def area(self):
... return self.radius * self.radius * 3.14159
...
>>> c = Circle()
>>> c.radius = 3
>>> print(c.area())
28.27431
方法调用语法由一个实例、一个点号以及要调用实例上的方法组成。以这种方式调用方法时,它是一个 绑定 方法调用。然而,方法也可以通过其包含的类来作为 未绑定 方法调用。这种做法不太方便,几乎从不这样做,因为以这种方式调用方法时,其第一个参数必须是定义该方法的类的一个实例,并且不太清晰:
>>> print(Circle.area(c))
28.27431
与 __init__ 类似,area 方法是在类定义体内部定义为一个函数。任何方法的第一个参数都是调用该方法的实例或在其上调用,按照惯例命名为 self。在许多语言中,实例,通常称为 this,是隐式的,并且永远不会显式传递,但 Python 的设计哲学更倾向于使事物明确。
如果方法定义接受这些参数,则可以使用参数调用方法。这个版本的 Circle 给 __init__ 方法添加了一个参数,这样你就可以创建给定半径的圆,而无需在创建圆之后设置半径:
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return self.radius * self.radius * 3.14159
注意这里对 radius 的两种用法。self.radius 是名为 radius 的实例变量。radius 单独使用时是名为 radius 的局部函数参数。这两个不是同一个!在实际应用中,你可能会将局部函数参数命名为 r 或 rad 以避免任何混淆的可能性。
使用这个 Circle 的定义,你可以通过一次调用 Circle 类来创建任何半径的圆。以下创建了一个半径为 5 的 Circle:
c = Circle(5)
所有标准的 Python 函数特性——默认参数值、额外参数、关键字参数等——都可以与方法一起使用。你本可以将 __init__ 的第一行定义为
def __init__(self, radius=1):
然后调用 circle 时,无论是否有额外的参数都可以工作;Circle() 会返回半径为 1 的圆,而 Circle(3) 会返回半径为 3 的圆。
Python 中的方法调用没有魔法,可以被认为是正常函数调用的简写。给定一个方法调用 instance.method(arg1, arg2, . . .),Python 通过以下规则将其转换为正常函数调用:
-
在实例命名空间中查找方法名。如果为该实例更改或添加了方法,则优先调用类或超类中的方法。这种查找与本章后面 15.4.1 节 中讨论的查找类似。
-
如果在实例命名空间中没有找到方法,查找
instance的类类型class,并在那里查找方法。在先前的例子中,class是Circle——实例c的类型。 -
如果仍然没有找到方法,则在超类中查找方法。
-
当找到该方法后,就像调用一个普通的 Python 函数一样直接调用它,使用
instance作为函数的第一个参数,并将方法调用中的所有其他参数向右移动一个空格。因此,instance.method(arg1, arg2, . . .)变为class.method (instance, arg1, arg2, . . .)。
尝试这个:实例变量和方法
更新Rectangle类的代码,以便在创建实例时可以设置尺寸,就像上面的Circle类一样。还要添加一个area()方法。
15.4. 类变量
类变量是与类相关联的变量,而不是类的实例,并且可以被类的所有实例访问。类变量可能用于跟踪某些类级别的信息,例如在任何时候创建了多少个类的实例。Python 提供了类变量,尽管使用它们比在其他大多数语言中需要付出更多的努力。此外,你需要注意类变量和实例变量之间的交互。
类变量是通过在类体中赋值创建的,而不是在__init__函数中。一旦创建,它就可以被类的所有实例看到。你可以使用类变量来使Circle类的所有实例都能访问到pi的值:
class Circle:
pi = 3.14159
def __init__(self, radius):
self.radius = radius
def area(self):
return self.radius * self.radius * Circle.pi
输入定义后,你可以输入
>>> Circle.pi
3.14159
>>> Circle.pi = 4
>>> Circle.pi
4
>>> Circle.pi = 3.14159
>>> Circle.pi
3.14159
这个例子正是你期望类变量如何表现;它与定义它的类相关联并包含在其中。注意在这个例子中,你在创建任何圆实例之前就访问了Circle.pi。显然,Circle.pi独立于Circle类的任何特定实例存在。
你还可以通过类名从类的方法中访问类变量。你在Circle.area的定义中这样做,其中area函数对Circle.pi进行了特定引用。在操作中,这产生了预期的效果;从类中获取了正确的pi值并在计算中使用:
>>> c = Circle(3)
>>> c.area()
28.27431
你可能会反对在类的方法中硬编码类的名称。你可以通过使用对所有 Python 类实例都可用的高级__class__属性来避免这样做。此属性返回实例所属的类,例如:
>>> Circle
<class '__main__.Circle'>
>>> c.__class__
<class '__main__.Circle'>
内部表示名为Circle的类是通过一个抽象数据结构实现的,而这个数据结构正是从Circle类的实例c的__class__属性中获得的。这个例子让你能够从c中获取Circle.pi的值,而不需要显式地引用Circle类的名称:
>>> c.__class__.pi
3.14159
你可以在area方法内部使用此代码,以消除对Circle类的显式引用;将Circle.pi替换为self.__class__.pi。
15.4.1. 类变量的一个特性
类变量有一些奇怪的特性,如果你不知道,可能会让你感到困惑。当 Python 查找实例变量时,如果找不到该名称的实例变量,它会尝试查找并返回同名类变量中的值。只有当它找不到合适的类变量时,Python 才会发出错误信号。类变量使得为实例变量实现默认值变得高效;只需创建一个具有相同名称和适当默认值的类变量,并避免每次创建类实例时初始化该实例变量的时间和内存开销。但这也使得在不发出错误信号的情况下,意外地引用实例变量而不是类变量变得容易。在本节中,我将探讨类变量如何与前面的示例一起工作。
首先,你可以参考变量 c.pi,即使 c 没有名为 pi 的关联实例变量。Python 首先尝试查找这样的实例变量;当它找不到实例变量时,Python 会查找并找到 Circle 中的类变量 pi:
>>> c = Circle(3)
>>> c.pi
3.14159
这种结果可能不是你想要的。这种技术很方便,但容易出错,所以请小心。
现在,如果你尝试将 c.pi 作为真正的类变量使用,通过从一个实例更改它,意图让所有实例都能看到这个变化,会发生什么?再次,你使用之前定义的 Circle:
>>> c1 = Circle(1)
>>> c2 = Circle(2)
>>> c1.pi = 3.14
>>> c1.pi
3.14
>>> c2.pi
3.14159
>>> Circle.pi
3.14159
这个例子不会像真正的类变量那样工作;c1 现在有自己的 pi 复制,与通过 c2 访问的 Circle.pi 区分开来。这是因为对 c1.pi 的赋值在 c1 中 创建 了一个实例变量;它以任何方式都不会影响类变量 Circle.pi。随后的 c1.pi 查找返回该实例变量的值,而随后的 c2.pi 查找 c2 中的实例变量 pi,找不到它,然后回退到返回类变量 Circle.pi 的值。如果你想更改类变量的值,通过类名访问它,而不是通过实例变量 self。
15.5. 静态方法和类方法
Python 类也可以有与 Java 等语言中的静态方法相对应的方法。此外,Python 还有 类 方法,这要复杂一些。
15.5.1. 静态方法
就像在 Java 中一样,即使没有创建该类的实例,你也可以调用静态方法,尽管你可以通过类实例来调用它们。要创建静态方法,使用 @staticmethod 装饰器,如下所示。
列表 15.1. 文件 circle.py
"""circle module: contains the Circle class."""
class Circle:
"""Circle class"""
all_circles = [] *1*
pi = 3.14159
def __init__(self, r=1):
"""Create a Circle with the given radius"""
self.radius = r
self.__class__.all_circles.append(self) *2*
def area(self):
"""determine the area of the Circle"""
return self.__class__.pi * self.radius * self.radius
@staticmethod
def total_area():
"""Static method to total the areas of all Circles """
total = 0
for c in Circle.all_circles:
total = total + c.area()
return total
-
1 包含所有已创建圆的列表的类变量
-
2 当实例初始化时,它会将自己添加到 all_circles 列表中。
现在交互式地输入以下内容:
>>> import circle
>>> c1 = circle.Circle(1)
>>> c2 = circle.Circle(2)
>>> circle.Circle.total_area()
15.70795
>>> c2.radius = 3
>>> circle.Circle.total_area()
31.415899999999997
还要注意,使用了文档字符串。在实际模块中,你可能会放入更多信息性的字符串,在类的文档字符串中说明可用的方法,并在方法的文档字符串中包含使用信息:
>>> circle.__doc__
'circle module: contains the Circle class.'
>>> circle.Circle.__doc__
'Circle class'
>>> circle.Circle.area.__doc__
'determine the area of the Circle'
15.5.2. 类方法
类方法与静态方法类似,可以在创建类的对象之前调用,或者通过使用类的实例来调用。但类方法隐式地将它们所属的类作为第一个参数传递,因此你可以更简单地编写它们,如下所示。
列表 15.2. 文件 circle_cm.py
"""circle_cm module: contains the Circle class."""
class Circle:
"""Circle class"""
all_circles = [] *1*
pi = 3.14159
def __init__(self, r=1):
"""Create a Circle with the given radius"""
self.radius = r
self.__class__.all_circles.append(self)
def area(self):
"""determine the area of the Circle"""
return self.__class__.pi * self.radius * self.radius
@classmethod *2*
def total_area(cls): *3*
total = 0
for c in cls.all_circles: *4*
total = total + c.area()
return total
>>> import circle_cm
>>> c1 = circle_cm.Circle(1)
>>> c2 = circle_cm.Circle(2)
>>> circle_cm.Circle.total_area()
15.70795
>>> c2.radius = 3
>>> circle_cm.Circle.total_area()
31.415899999999997
- 1 包含所有已创建圆的列表的变量
在 def 前使用 @classmethod 装饰器 2。类参数传统上是 cls 3。你可以使用 cls 而不是 self.__class__ 4。
使用类方法而不是静态方法,你不必将类名硬编码到 total_area 中。因此,Circle 的任何子类都可以调用 total_area 并引用它们自己的成员,而不是 Circle 中的成员。
尝试这个:类方法
编写一个类似于 total_area() 的类方法,返回所有圆的总周长。
15.6. 继承
Python 中的继承比编译语言(如 Java 和 C++)中的继承更容易、更灵活,因为 Python 的动态特性不会对语言施加太多限制。
要了解 Python 中如何使用继承,可以从本章前面讨论的 Circle 类开始,并进行泛化。你可能还想定义一个额外的类用于正方形:
class Square:
def __init__(self, side=1):
self.side = side *1*
- 1 正方形的任意一边长度
现在,如果你想在绘图程序中使用这些类,它们必须定义每个实例在绘图表面上的位置。你可以通过在每个实例中定义 x 坐标和 y 坐标来实现这一点:
class Square:
def __init__(self, side=1, x=0, y=0):
self.side = side
self.x = x
self.y = y
class Circle:
def __init__(self, radius=1, x=0, y=0):
self.radius = radius
self.x = x
self.y = y
这种方法可行,但随着形状类数量的增加,会产生大量重复的代码,因为你可能希望每个形状都有这种位置的概念。毫无疑问,你知道我在这里想什么;这种情况是面向对象语言中使用继承的标准情况。你不必在每个形状类中定义 x 和 y 变量,可以将它们抽象成一个通用的 Shape 类,并让每个定义特定形状的类从这个通用类继承。在 Python 中,这种技术看起来是这样的:
class Shape:
def __init__(self, x, y):
self.x = x
self.y = y
class Square(Shape): *1*
def __init__(self, side=1, x=0, y=0):
super().__init__(x, y) *2*
self.side = side
class Circle(Shape): *3*
def __init__(self, r=1, x=0, y=0):
super().__init__(x, y) *4*
self.radius = r
-
1 说明 Square 继承自 Shape
-
2 必须调用 Shape 的 init 方法
-
3 说明 Circle 继承自 Shape
-
4 必须调用 Shape 的 init 方法
在 Python 中使用继承类时,通常有两个要求,这两个要求都可以在 Circle 和 Square 类中加粗的代码中看到。第一个要求是定义继承层次结构,这是通过在 class 关键字定义类名后立即给出继承的类来完成的。在上面的代码中,Circle 和 Square 都继承自 Shape。第二个且更为微妙的要求是必须显式调用继承类的 __init__ 方法。Python 不会自动为你做这件事,但你可以使用 super 函数让 Python 确定使用哪个继承类。这个任务在示例代码中是通过 super().__init__(x,y) 行完成的。这段代码调用 Shape 初始化函数,并传递正在初始化的实例和适当的参数。否则,在示例中,Circle 和 Square 的实例将不会设置它们的 x 和 y 实例变量。
与使用 super 不同,你可以通过显式地使用 Shape.__init__(self, x, y) 来调用 Shape 的 __init__ 方法,这样也会调用正在初始化的实例的 Shape 初始化函数。然而,这种技术在长期使用中可能不够灵活,因为它硬编码了继承类的名称,如果设计和继承层次结构发生变化,这可能会成为问题。另一方面,在更复杂的情况下使用 super 可能会遇到困难。由于这两种方法并不完全兼容,因此请在代码中清楚地记录你使用的方法。
当你尝试使用在基类中未定义但在超类中定义的方法时,继承也会生效。为了看到这个效果,在 Shape 类中定义另一个名为 move 的方法,该方法通过给定位移来移动形状。这个方法通过方法参数确定的数量修改形状的 x 和 y 坐标。现在 Shape 的定义变为
class Shape:
def __init__(self, x, y):
self.x = x
self.y = y
def move(self, delta_x, delta_y):
self.x = self.x + delta_x
self.y = self.y + delta_y
如果你输入 Shape 的这个定义以及 Circle 和 Square 的先前定义,你可以进行以下交互式会话:
>>> c = Circle(1)
>>> c.move(3, 4)
>>> c.x
3
>>> c.y
4
如果你在一个交互式会话中尝试这段代码,请确保在重新定义 Shape 类之后重新进入 Circle 类。
示例中的 Circle 类没有立即在其自身内部定义 move 方法,但由于它继承自实现了 move 的类,所以 Circle 的所有实例都可以使用 move 方法。在更传统的面向对象术语中,可以说所有 Python 方法都是虚拟的——也就是说,如果当前类中没有方法,则会搜索超类列表以找到该方法,并使用找到的第一个。
尝试以下方法:继承
将 Rectangle 类的代码重写为从 Shape 继承。由于正方形和矩形是相关的,从其中一个继承另一个是否有意义?如果有,哪个应该是基类,哪个应该继承?
你将如何编写代码为 Square 类添加一个 area() 方法?是否应该将 area 方法移动到基类 Shape 中,并由圆、正方形和矩形继承?如果是这样,会产生什么问题?
15.7. 类和实例变量的继承
继承允许实例继承类的属性。实例变量与对象实例相关联,并且对于给定的实例,只有一个具有给定名称的实例变量。
考虑以下示例。使用这些类定义,
class P:
z = "Hello"
def set_p(self):
self.x = "Class P"
def print_p(self):
print(self.x)
class C(P):
def set_c(self):
self.x = "Class C"
def print_c(self):
print(self.x)
执行以下代码:
>>> c = C()
>>> c.set_p()
>>> c.print_p()
Class P
>>> c.print_c()
Class P
>>> c.set_c()
>>> c.print_c()
Class C
>>> c.print_p()
Class C
在这个例子中,对象 c 是类 C 的一个实例。C 从 P 继承,但 c 并不是从某个不可见的 P 类实例继承。它直接从 P 继承方法和类变量。因为只有一个实例(c),在 c 的方法调用中对实例变量 x 的任何引用都必须指向 c.x。这适用于调用 c 上任何类定义的方法。正如你所看到的,当在 c 上调用时,set_p 和 print_p(在类 P 中定义)都引用相同的变量,当在 c 上调用时,set_c 和 print_c 引用相同的变量。
通常,这是对实例变量的期望行为,因为同名的实例变量应该引用相同的变量是有意义的。偶尔,你可能会希望有稍微不同的行为,你可以通过使用私有变量来实现(见第 15.9 节)。
类变量是继承的,但你应小心避免名称冲突,并注意你在类变量子节中看到的行为的泛化。在示例中,为超类 P 定义了一个类变量 z,可以通过三种方式访问它:通过实例 c,通过派生类 C,或者直接通过超类 P:
>>> c.z; C.z; P.z
'Hello'
'Hello'
'Hello'
但如果你尝试通过类 C 设置类变量 z,将为类 C 创建一个新的类变量。这个结果对 P 的类变量本身(通过 P 访问)没有影响。但未来通过类 C 或其实例 c 的访问将看到这个新变量而不是原始变量:
>>> C.z = "Bonjour"
>>> c.z; C.z; P.z
'Bonjour'
'Bonjour'
'Hello'
同样,如果你尝试通过实例 c 设置 z,将创建一个新的实例变量,你最终会有三个不同的变量:
>>> c.z = "Ciao"
>>> c.z; C.z; P.z
'Ciao'
'Bonjour'
'Hello'
15.8. 回顾:Python 类的基础
我到目前为止讨论的点是在 Python 中使用类和对象的基础。在我继续之前,我将这些基础知识汇总到一个单独的例子中。在本节中,你将创建几个具有前面讨论的功能的类,然后你将看到这些功能是如何表现的。
首先,创建一个基类:
class Shape:
def __init__(self, x, y): *1*
self.x = x *2*
self.y = y *2*
def move(self, delta_x, delta_y): *3*
self.x = self.x + delta_x *4*
self.y = self.y + delta_y
-
1
__init__方法接受实例(self)和两个参数 -
2 通过
self访问实例变量。 -
3
move方法接受实例(self)和两个参数 -
4 在
move方法中设置的实例变量
接下来,创建一个从基类 Shape 继承的子类:
class Circle(Shape): *1*
pi = 3.14159 *2*
all_circles = [] *2*
def __init__(self, r=1, x=0, y=0): *3*
super().__init__(x, y) *4*
self.radius = r
all_circles.append(self) *5*
@classmethod *6*
def total_area(cls):
area = 0
for circle in cls.all_circles:
area += cls.circle_area(circle.radius) *7*
return area
@staticmethod
def circle_area(radius): *8*
return Circle.pi * radius * radius *9*
-
1 Circle 类继承自 Shape 类
-
2 pi 和 all_circles 是 Circle 的类变量。
-
3 Circle 的 init 方法接受实例(self)和 3 个参数,它们都有默认值
-
4 Circle 的 init 方法使用 super() 调用 Shape 的 init
-
5 在 init 方法中,实例将自己添加到 all_circles 列表中
-
6 total_area 是一个类方法,它接受类本身(cls)作为参数。
-
7 使用 cls 参数访问 Circle 的静态方法 circle_area
-
8 circle_area 是一个静态方法,它不接收 self 或 cls 作为参数。
-
9 访问类变量 pi;也可以使用 class.pi
现在,您可以创建一些 Circle 类的实例并将它们进行测试。因为 Circle 的 __init__ 方法有默认参数,所以您可以在不提供任何参数的情况下创建一个 Circle:
>>> c1 = Circle()
>>> c1.radius, c1.x, c1.y
(1, 0, 0)
如果您确实提供了参数,它们将用于设置实例的值:
>>> c2 = Circle(2, 1, 1)
>>> c2.radius, c2.x, c2.y
(2, 1, 1)
如果您调用 move() 方法,Python 在 Circle 类中找不到 move(),因此它会沿着继承层次结构向上移动并使用 Shape 的 move() 方法:
>>> c2.move(2, 2)
>>> c2.radius, c2.x, c2.y
(2, 3, 3)
此外,因为 __init__ 方法的一部分是将每个实例添加到一个列表中,该列表是类变量,所以您会得到 Circle 实例:
>>> Circle.all_circles
[<__main__.Circle object at 0x7fa88835e9e8>, <__main__.Circle object at
0x7fa88835eb00>]
>>> [c1, c2]
[<__main__.Circle object at 0x7fa88835e9e8>, <__main__.Circle object at
0x7fa88835eb00>]
您也可以通过类本身或通过一个实例来调用 Circle 类的 total_area() 类方法:
>>> Circle.total_area()
15.70795
>>> c2.total_area()
15.70795
最后,您可以再次通过类本身或一个实例调用静态方法 circle_area()。作为一个静态方法,circle_area 不会传递实例或类,它更像是一个位于类命名空间内的独立函数。事实上,静态方法通常用于将实用函数捆绑到类中:
>>> Circle.circle_area(c1.radius)
3.14159
>>> c1.circle_area(c1.radius)
3.14159
这些示例展示了 Python 中类的基本行为。现在您已经掌握了类的基础知识,可以继续学习更高级的主题。
15.9. 私有变量和私有方法
私有变量 或 私有方法 是指在定义它的类的其他方法中不可见的变量或方法。私有变量和方法有两个用途:它们通过选择性地拒绝访问对象实现的重要或脆弱部分来增强安全性和可靠性,并防止由于继承的使用而出现的名称冲突。一个类可以定义一个私有变量并从定义了具有相同名称的私有变量的类继承,但这不会引起问题,因为变量是私有的这一事实确保了它们有独立的副本。私有变量使代码更容易阅读,因为它们明确指出仅在类内部使用的部分。其他任何东西都是类的接口。
大多数定义私有变量的语言都是通过使用关键字“private”或类似的东西来实现的。Python 的约定更简单,也更易于立即看出什么是私有的,什么不是。任何以双下划线 (__) 开头但不是以双下划线结尾的方法或实例变量都是私有的;其他都不是私有的。
例如,考虑以下类定义:
class Mine:
def __init__(self):
self.x = 2
self.__y = 3 *1*
def print_y(self):
print(self.__y)
- 1 使用前导双下划线将
y定义为私有
使用这个定义,创建一个类的实例:
>>> m = Mine()
x 不是一个私有变量,所以它是直接可访问的:
>>> print(m.x)
2
__y 是一个私有变量。直接尝试访问它将引发错误:
>>> print(m.__y)
Traceback (innermost last):
File "<stdin>", line 1, in ?
AttributeError: 'Mine' object has no attribute '__y'
print_y 方法不是私有的,因为它在 Mine 类中,所以它可以访问 __y 并打印它:
>>> m.print_y()
3
最后,你应该注意,用于提供隐私的机制在代码编译为字节码时会将私有变量和私有方法的名字进行混淆。具体发生的情况是,_classname 被添加到变量名之前:
>>> dir(m)
['_Mine__y', 'x', ...]
目的是防止任何意外的访问。如果有人想的话,他可以故意模拟混淆并访问值。但以这种易于阅读的形式进行混淆使得调试变得容易。
尝试这个:私有实例变量
修改 Rectangle 类的代码以使尺寸变量私有。这种修改会对使用该类施加什么限制?
15.10. 使用 @property 来实现更灵活的实例变量
Python 允许你作为程序员直接访问实例变量,而不需要像 Java 和其他面向对象语言中常用的 getter 和 setter 方法那样的额外机制。这种缺乏 getter 和 setter 方法使得编写 Python 类更加简洁和容易,但在某些情况下,使用 getter 和 setter 方法可能很有用。假设你需要在将值放入实例变量之前获取该值,或者需要即时确定属性的值。在这两种情况下,getter 和 setter 方法都会完成工作,但代价是失去了 Python 的简单实例变量访问。
解决方案是使用属性。属性结合了通过像 getters 和 setters 这样的方法传递对实例变量的访问权限的能力,以及通过点符号进行实例变量的直接访问。
要创建一个属性,你使用 property 装饰器与一个具有属性名的方法:
class Temperature:
def __init__(self):
self._temp_fahr = 0
@property
def temp(self):
return (self._temp_fahr - 32) * 5 / 9
没有设置器,这样的属性是只读的。要更改属性,你需要添加一个设置器:
@temp.setter
def temp(self, new_temp):
self._temp_fahr = new_temp * 9 / 5 + 32
现在你可以使用标准的点符号来获取和设置属性 temp。注意,方法名保持不变,但装饰器改为属性名(在这种情况下是 temp),加上 .setter 表示正在定义 temp 属性的设置器:
>>> t = Temperature()
>>> t._temp_fahr
0
>>> t.temp
-17.77777777777778
>>> t.temp = 34 *1*
>>> t._temp_fahr
93.2
>>> t.temp *2*
34.0
_temp_fahr 中的 0 在返回之前转换为摄氏度 1。34 通过设置器 2 转换回华氏度。
Python 能够添加属性的一大优点是,你可以在使用普通的实例变量进行初始开发后,无缝地更改到属性,无论何时何地,而无需更改任何客户端代码。访问方式仍然是相同的,使用点符号。
尝试这个:属性
将 Rectangle 类的维度更新为具有获取器和设置器的属性,这些获取器和设置器不允许负尺寸。
15.11. 类实例的作用域规则和命名空间
现在你已经拥有了拼凑出类实例的作用域规则和命名空间图画的全部部件。
当你在类的某个方法中时,你可以直接访问局部命名空间(在方法中声明的参数和变量)、全局命名空间(在模块级别声明的函数和变量),以及内置命名空间(内置函数和内置异常)。这三个命名空间按以下顺序搜索:局部、全局和内置(见图 15.1)。
图 15.1. 直接命名空间

你还可以通过 self 变量访问实例的命名空间(实例变量、私有实例变量和超类实例变量)、其类的命名空间(方法、类变量、私有方法和私有类变量),以及其超类的命名空间(超类方法和超类类变量)。这三个命名空间按以下顺序搜索:实例、类和超类(见图 15.2)。
图 15.2. self 变量命名空间

使用 self 无法访问私有超类实例变量、私有超类方法和私有超类类变量。一个类能够将其子类隐藏这些名称。
列表 15.3 中的模块将这些两个例子结合起来,具体演示了在方法内部可以访问的内容。
列表 15.3. 文件 cs.py
"""cs module: class scope demonstration module."""
mv ="module variable: mv"
def mf():
return "module function (can be used like a class method in " \
"other languages): mf()"
class SC:
scv = "superclass class variable: self.scv"
__pscv = "private superclass class variable: no access"
def __init__(self):
self.siv = "superclass instance variable: self.siv " \
"(but use SC.siv for assignment)"
self.__psiv = "private superclass instance variable: " \
"no access"
def sm(self):
return "superclass method: self.sm()"
def __spm(self):
return "superclass private method: no access"
class C(SC):
cv = "class variable: self.cv (but use C.cv for assignment)"
__pcv = "class private variable: self.__pcv (but use C.__pcv " \
"for assignment)"
def __init__(self):
SC.__init__(self)
self.__piv = "private instance variable: self.__piv"
def m2(self):
return "method: self.m2()"
def __pm(self):
return "private method: self.__pm()"
def m(self, p="parameter: p"):
lv = "local variable: lv"
self.iv = "instance variable: self.xi"
print("Access local, global and built-in " \
"namespaces directly")
print("local namespace:", list(locals().keys()))
print(p) *1*
print(lv) *2*
print("global namespace:", list(globals().keys()))
print(mv) *3*
print(mf()) *4*
print("Access instance, class, and superclass namespaces " \
"through 'self'")
print("Instance namespace:",dir(self))
print(self.iv) *5*
print(self.__piv) *6*
print(self.siv) *7*
print("Class namespace:",dir(C))
print(self.cv) *8*
print(self.m2()) *9*
print(self.__pcv) *10*
print(self.__pm()) *11*
print("Superclass namespace:",dir(SC))
print(self.sm()) *12*
print(self.scv) *13*
-
1 参数*
-
2 局部变量*
-
3 模块变量*
-
4 模块函数*
-
5 实例变量*
-
6 私有实例变量*
-
7 超类实例变量*
-
8 类变量*
-
9 方法*
-
10 私有类变量*
-
11 私有方法*
-
12 超类方法*
-
13 通过实例访问超类类变量*
这个输出相当多,所以我们将分部分来看。
在第一部分中,类 C' 的方法 m 的局部命名空间包含参数 self(它是实例变量)和 p,以及局部变量 lv(所有这些都可以直接访问):
>>> import cs
>>> c = cs.C()
>>> c.m()
Access local, global and built-in namespaces directly
local namespace: ['lv', 'p', 'self']
parameter: p
local variable: lv
接下来,方法 m 的全局命名空间包含模块变量 mv 和模块函数 mf(正如前一个章节所述,你可以使用它来提供类方法功能)。还包括模块中定义的类(类 C 和超类 SC)。所有这些类都可以直接访问:
global namespace: ['C', 'mf', '__builtins__', '__file__', '__package__',
'mv', 'SC', '__name__', '__doc__']
module variable: mv
module function (can be used like a class method in other languages): mf()
实例C的命名空间包含实例变量iv和超类的实例变量siv(正如前一部分所描述的,它与常规实例变量没有区别)。它还包括私有实例变量__piv的混淆名称(您可以通过self访问它)和超类私有实例变量__psiv的混淆名称(您无法访问它):
Access instance, class, and superclass namespaces through 'self'
Instance namespace: ['_C__pcv', '_C__piv', '_C__pm', '_SC__pscv',
'_SC__psiv', '_SC__spm', '__class__', '__delattr__', '__dict__',
'__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
'__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__',
'__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__',
'__weakref__', 'cv', 'iv', 'm', 'm2', 'scv', 'siv', 'sm']
instance variable: self.xi
private instance variable: self.__piv
superclass instance variable: self.siv (but use SC.siv for assignment)
类C的命名空间包含类变量cv和私有类变量__pcv的混淆名称。两者都可以通过self访问,但要对它们进行赋值,您需要使用类C。类C还有类的两个方法m和m2,以及私有方法__pm的混淆名称(可以通过self访问):
Class namespace: ['_C__pcv', '_C__pm', '_SC__pscv', '_SC__spm', '__class__',
'__delattr__', '__dict__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__gt__', '__hash__', '__init__', '__le__',
'__lt__', '__module__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', '__weakref__', 'cv', 'm', 'm2', 'scv', 'sm']
class variable: self.cv (but use C.cv for assignment)
method: self.m2()
class private variable: self.__pcv (but use C.__pcv for assignment)
private method: self.__pm()
最后,超类SC的命名空间包含超类类变量scv(可以通过self访问,但要对它进行赋值,您需要使用超类SC)和超类方法sm。它还包括私有超类方法__spm和私有超类类变量__pscv的混淆名称,这两个名称都无法通过self访问:
Superclass namespace: ['_SC__pscv', '_SC__spm', '__class__', '__delattr__',
'__dict__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__gt__', '__hash__', '__init__', '__le__',
'__lt__', '__module__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', '__weakref__', 'scv', 'sm']
superclass method: self.sm()
superclass class variable: self.scv
这个例子一开始可能相当复杂,难以理解。您可以用它作为参考或您自己探索的基础。与 Python 中的大多数其他概念一样,您可以通过玩一些简化的例子来建立对正在发生的事情的稳固理解。
15.12. 析构函数和内存管理
您已经看到了类初始化器(__init__方法)。可以为类定义析构函数。但与 C++不同,创建和调用析构函数并不是确保释放实例使用的内存所必需的。Python 通过引用计数机制提供自动内存管理。也就是说,它跟踪对实例的引用数量;当这个数量达到零时,实例使用的内存被回收,并且任何由实例引用的 Python 对象都将它们的引用计数减一。您几乎不需要定义析构函数。
您可能会偶尔遇到需要在对象被移除时显式地释放外部资源的情况。在这种情况下,最佳实践是使用上下文管理器,如第十四章所述。如前所述,您可以使用标准库中的contextlib模块为您的情况创建自定义上下文管理器。
15.13. 多重继承
编译型语言对多重继承的使用施加了严格的限制——对象从多个父类继承数据和行为的特性。例如,在 C++中使用多重继承的规则非常复杂,以至于许多人避免使用它。在 Java 中,多重继承是不允许的,尽管 Java 确实有接口机制。
Python 对多重继承没有这样的限制。一个类可以像继承单个父类一样从任意数量的父类中继承。在最简单的情况下,涉及的任何类(包括通过父类间接继承的类),都不包含相同名称的实例变量或方法。在这种情况下,继承的类就像其自己的定义和所有祖先的定义的合成。假设类 A 从类 B、C 和 D 继承;类 B 从类 E 和 F 继承;类 D 从类 G 继承(见图 15.3)。还假设这些类中没有共享方法名称。在这种情况下,类 A 的一个实例可以用作类 B–G 以及 A 的实例;类 B 的一个实例可以用作类 E 或 F 以及类 B 的实例;类 D 的一个实例可以用作类 G 以及类 D 的实例。从代码的角度来看,类定义看起来像这样:
class E:
. . .
class F:
. . .
class G:
. . .
class D(G):
. . .
class C:
. . .
class B(E, F):
. . .
class A(B, C, D):
. . .
图 15.3. 继承层次结构

当一些类共享方法名称时,情况会更复杂,因为 Python 必须决定哪个相同名称是正确的。假设你想要在类 A 的实例 a 上解析方法调用 a.f(),其中 f 在 A 中未定义,但在 F、C 和 G 中都有定义。各种方法中的哪一个将被调用?
答案在于 Python 在寻找在原始类中未定义的方法时,搜索基类的顺序。在最简单的情况下,Python 会按照从左到右的顺序遍历原始类的基类,但在查看下一个基类之前,它总是先遍历基类的所有祖先类。在尝试执行 a.f() 时,搜索过程大致如下:
-
Python 首先查看调用对象所在的类,即类
A。 -
因为
A没有定义方法f,Python 开始在A的基类中搜索。A的第一个基类是B,所以 Python 开始在B中搜索。 -
因为
B没有定义方法f,Python 继续在B的基类中搜索。它首先查看B的第一个基类,即类E。 -
E没有定义方法f,也没有基类,所以在E中没有更多的搜索要做。Python 返回到类B并查看B的下一个基类,即类F。
类 F 包含一个名为 f 的方法,并且因为它是最先找到的具有该名称的方法,所以它被使用。类 C 和 G 中名为 f 的方法被忽略。
使用这种内部逻辑不太可能导致最易读或易维护的程序,当然。随着更复杂的层次结构,其他因素也会介入以确保没有类被搜索两次,并支持对super的协作调用。
但这个层次结构可能比你预期的在实际中看到的要复杂。如果你坚持使用多重继承的更标准用法,比如创建 mixin 或 addin 类,你可以轻松地保持代码的可读性并避免名称冲突。
有些人坚信多重继承是一件坏事。它确实可能被误用,Python 中没有强制要求你使用它。最大的危险之一似乎是创建过深的继承层次结构,多重继承有时可以用来帮助防止这个问题发生。这个问题超出了本书的范围。这里使用的例子只是说明了 Python 中多重继承的工作方式,并没有尝试解释它的用例(例如在 mixin 或 addin 类中)。
实验 15:HTML 类
在这个实验中,你创建类来表示 HTML 文档。为了简化,假设每个元素只能包含文本和一个子元素。所以<html>元素只包含一个<body>元素,而<body>元素包含(可选)文本和一个只包含文本的<p>元素。
要实现的关键特性是__str__()方法,它反过来调用其子元素的__str__()方法,这样当在<html>元素上调用str()函数时,就会返回整个文档。你可以假设任何文本都在子元素之前。
这里是使用这些类的示例输出:
para = p(text="this is some body text")
doc_body = body(text="This is the body", subelement=para)
doc = html(subelement=doc_body)
print(doc)
<html>
<body>
This is the body
<p>
this is some body text
</p>
</body>
</html>
摘要
-
定义一个类实际上创建了一个新的数据类型。
-
__init__用于在创建类的新的实例时初始化数据,但它不是一个构造函数。 -
self参数指向类的当前实例,并且作为类的方法的第一个参数传递。 -
静态方法可以在不创建类的实例的情况下调用,因此它们不接收
self参数。 -
类方法传递一个
cls参数,它是对类的引用,而不是self。 -
所有 Python 方法都是虚拟的。也就是说,如果一个方法在子类中没有重写或者对超类是私有的,那么它对所有子类都是可访问的。
-
类变量从超类继承,除非它们以两个下划线
(__)开头,在这种情况下它们是私有的,子类无法看到。方法也可以用同样的方式设置为私有。 -
属性让你可以有具有定义的 getter 和 setter 方法的属性,但它们仍然像普通实例属性一样表现。
-
Python 允许多重继承,这通常与 mixin 类一起使用。
第十六章。正则表达式
本章涵盖
-
理解正则表达式
-
使用特殊字符创建正则表达式
-
在正则表达式中使用原始字符串
-
从字符串中提取匹配的文本
-
使用正则表达式替换文本
有些人可能会 wonder 为什么我会在本书中讨论正则表达式。正则表达式由一个单独的 Python 模块实现,足够高级,以至于它们甚至不是像 C 或 Java 这样的语言的标准库的一部分。但是如果你使用 Python,你很可能会进行文本解析;如果你这样做,正则表达式非常有用,不容忽视。如果你使用过 Perl、Tcl 或 Linux/UNIX,你可能熟悉正则表达式;如果不熟悉,本章将详细介绍它们。
16.1. 什么是正则表达式?
正则表达式(regex)是一种识别和通常从某些文本模式中提取数据的方法。一个识别文本或字符串的正则表达式被称为匹配那个文本或字符串。正则表达式由一个字符串定义,其中某些字符(所谓的 元字符)可以具有特殊含义,这使得单个正则表达式可以匹配许多不同的特定字符串。
通过示例比通过解释更容易理解这一点。以下是一个程序,它使用正则表达式来计算文本文件中有多少行包含单词 hello。包含 hello 一次以上的行只计算一次:
import re
regexp = re.compile("hello")
count = 0
file = open("textfile", 'r')
for line in file.readlines():
if regexp.search(line):
count = count + 1
file.close()
print(count)
程序首先导入 Python 正则表达式模块,称为 re。然后它将文本字符串 "hello" 作为 文本正则表达式 编译成一个 编译后的正则表达式,使用 re.compile 函数。这种编译不是严格必要的,但编译后的正则表达式可以显著提高程序的速度,因此在处理大量文本的程序中几乎总是使用它们。
从 "hello" 编译的正则表达式能用来做什么?你可以用它来识别另一个字符串中 "hello" 的其他实例;换句话说,你可以用它来确定另一个字符串是否包含 "hello" 作为子串。这个任务是通过 search 方法完成的,如果正则表达式在字符串参数中没有找到,则返回 None;Python 在布尔上下文中将 None 解释为 false。如果正则表达式在字符串中找到,Python 返回一个特殊对象,你可以用它来确定匹配的各种信息(例如它在字符串中的位置)。我将在后面讨论这个话题。
16.2. 带有特殊字符的正则表达式
之前的例子有一个小缺陷:它计算包含 "hello" 的行数,但忽略了包含 "Hello" 的行,因为它没有考虑大小写。
解决这个问题的另一种方法可以是使用两个正则表达式——一个用于 "hello",另一个用于 "Hello"——并对每一行进行测试。更好的方法是使用正则表达式的更高级功能。对于程序的第二行,替换为
regexp = re.compile("hello|Hello")
这个正则表达式使用了竖线特殊字符 |。特殊字符是指正则表达式中的一个字符,它不被解释为其自身;它具有某种特殊含义。| 表示 或,因此正则表达式匹配 "hello" 或 "Hello"。
解决这个问题的另一种方法是使用
regexp = re.compile("(h|H)ello")
除了使用 | 之外,这个正则表达式还使用了 括号 特殊字符来分组,在这种情况下意味着 | 在小写或大写 H 之间进行选择。结果正则表达式匹配一个 h 或一个 H,后面跟着 ello。
另一种执行匹配的方法是
regexp = re.compile("[hH]ello")
特殊字符 [ 和 ] 在它们之间匹配任何单个字符。在 [ 和 ] 中有一个特殊的缩写来表示字符范围;[a-z] 匹配 a 和 z 之间的单个字符,[0-9A-Z] 匹配任何数字或任何大写字母,等等。有时,您可能想在 [] 中包含一个真正的连字符,在这种情况下,您应该将其作为第一个字符放置,以避免定义一个范围;[-012] 匹配一个连字符、一个 0、一个 1 或一个 2,以及其他任何东西。
Python 正则表达式中有很多特殊字符可用,描述它们在正则表达式中的所有细微差别超出了本书的范围。Python 正则表达式中可用的特殊字符的完整列表,以及它们含义的描述,可以在标准库中正则表达式 re 模块的在线文档中找到。在本章的剩余部分,我将按它们出现的顺序描述我使用的特殊字符。
快速检查:正则表达式中的特殊字符
您会使用什么正则表达式来匹配表示 -5 到 5 的数字的字符串?
您会使用什么正则表达式来匹配十六进制数字?假设允许的十六进制数字是 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, A, a, B, b, C, c, D, d, E, e, F, 和 f。
16.3. 正则表达式和原始字符串
编译正则表达式或搜索正则表达式匹配的函数理解,字符串中的某些字符序列在正则表达式的上下文中具有特殊含义。例如,regex 函数理解 \n 表示换行字符。但是如果你使用正常的 Python 字符串作为正则表达式,regex 函数通常永远不会看到这样的特殊序列,因为这些序列中的许多在正常字符串中也有特殊含义。例如,\n 在正常的 Python 字符串上下文中也表示换行符,Python 在 regex 函数看到该序列之前自动将字符串序列 \n 替换为换行符。因此,regex 函数编译包含嵌入换行符的字符串——而不是嵌入的 \n 序列。
在 \n 的情况下,这种情况没有区别,因为 regex 函数将换行字符解释为正是那样,并执行预期的操作:尝试在搜索的文本中匹配另一个换行字符。
现在看看另一个特殊序列,\\,它代表正则表达式中的 单个 反斜杠。假设你想要在文本中搜索字符串 "\ten" 的出现。因为你知道你必须用双反斜杠来表示反斜杠,你可能会尝试
regexp = re.compile("\\ten")
这个例子编译时没有抱怨,但是它是错误的。问题在于在 Python 字符串中,\\ 也表示一个单独的反斜杠。在调用 re.compile 之前,Python 将你输入的字符串解释为 \ten,这就是传递给 re.compile 的内容。在正则表达式的上下文中,\t 表示 制表符,所以你编译的正则表达式搜索的是制表符字符后面跟着的两个字符 en。
要使用正常的 Python 字符串解决这个问题,你需要四个反斜杠。Python 将前两个反斜杠解释为表示单个反斜杠的特殊序列,同样对于第二对反斜杠也是如此,结果在 Python 字符串中有两个 实际 的反斜杠。然后这个字符串被传递给 re.compile,它将这两个实际的反斜杠解释为正则表达式特殊序列,表示单个反斜杠。你的代码看起来像这样:
regexp = re.compile("\\\\ten")
这看起来很令人困惑,这就是为什么 Python 有一种定义字符串的方法,它不会将正常的 Python 规则应用于特殊字符。以这种方式定义的字符串被称为 原始字符串。
16.3.1. 原始字符串的拯救
原始字符串看起来与普通字符串相似,只是它有一个前导的 r 字符,紧接在字符串的第一个引号之前。以下是一些原始字符串:
r"Hello"
r"""\tTo be\n\tor not to be"""
r'Goodbye'
r'''12345'''
如你所见,你可以使用单引号、双引号或正则表达式或三引号约定来使用原始字符串。你也可以选择用 R 开头而不是 r。无论你如何做,原始字符串的表示法都可以被视为对 Python 的指令:“在这个字符串中不要处理特殊序列。”在之前的例子中,所有原始字符串都与它们的普通字符串对应物等效,除了第二个例子,其中\t和\n序列不被解释为制表符或换行符,而是作为以反斜杠开头的两个字符串字符序列。
原始字符串并不是不同类型的字符串。它们代表了一种定义字符串的不同方式。通过运行几个交互式示例,你可以很容易地看到正在发生什么:
>>> r"Hello" == "Hello"
True
>>> r"\the" == "\\the"
True
>>> r"\the" == "\the"
False
>>> print(r"\the")
\the
>>> print("\the")
he
使用原始字符串与正则表达式结合意味着你不需要担心字符串特殊序列和正则表达式特殊序列之间的任何奇怪交互。你使用正则表达式特殊序列。然后,之前的正则表达式示例变为
regexp = re.compile(r"\\ten")
这个模式按预期工作。编译后的正则表达式寻找一个反斜杠后跟字母 ten。
你应该养成在定义正则表达式时使用原始字符串的习惯,并且你将在本章的剩余部分这样做。
16.4. 从字符串中提取匹配的文本
正则表达式最常见的一个用途是对文本进行基于简单模式的解析。这项任务你应该知道如何完成,这也是学习更多正则表达式特殊字符的好方法。
假设你有一个包含人名和电话号码的文本文件列表。文件的每一行看起来像这样:
surname, firstname middlename: phonenumber
你有一个姓氏,后面跟着一个逗号和空格,然后是一个名字,后面跟着一个空格,然后是一个中间名,后面跟着冒号和空格,然后是一个电话号码。
但是,为了使事情复杂化,中间名可能不存在,电话号码可能没有区号。(它可能是 800-123-4567 或 123-4567。)你可以编写代码来显式地从这样的行中解析数据,但这项工作将是乏味且容易出错的。正则表达式提供了一个更简单的解决方案。
首先,你需要想出一个正则表达式来匹配给定形式的行。接下来的几段会向你展示很多特殊字符。如果你第一次阅读时没有全部理解,不要担心;只要你能理解事情的大致内容,那就足够了。
为了简单起见,假设名字、姓氏和中间名由字母和可能的连字符组成。你可以使用上一节中讨论的[]特殊字符来定义一个只定义名字字符的模式:
[-a-zA-z]
这个模式匹配单个连字符、单个小写字母或单个大写字母。
要匹配全名(如 McDonald),你需要重复这个模式。+ 元字符会根据需要重复其前面的任何内容一次或多次,以匹配正在处理的字符串。所以这个模式
[-a-zA-Z]+
匹配单个名字,如 Kenneth、McDonald 或 Perkin-Elmer。它还匹配一些不是名字的字符串,如---或-a-b-c-,但对于这个例子来说这没关系。
那么,电话号码怎么办呢?特殊序列\d匹配任何数字,而[]外的连字符是一个普通的连字符。匹配电话号码的一个好模式是
\d\d\d-\d\d\d-\d\d\d\d
这是由三个数字组成,后面跟着一个连字符,然后是三个数字,再跟着一个连字符,最后是四个数字。这个模式只匹配带有区号的电话号码,而你的列表中可能包含没有区号的号码。最好的解决方案是将模式中的区号部分用()括起来;将其分组;然后在该组后面跟着一个?特殊字符,这意味着紧跟在?前面的内容是可选的:
(\d\d\d-)?\d\d\d-\d\d\d\d
这个模式匹配一个可能包含或不包含区号的电话号码。你可以使用类似的技巧来处理你列表中的一些人包含中间名(或首字母缩写)而另一些人没有的情况。(为此,通过分组和?特殊字符使中间名可选。)
你还可以使用{}来表示模式应该重复的次数,所以对于上面的电话号码示例,你可以使用:
(\d{3}-)?\d{3}-\d{4}
这个模式还意味着一个可选的三位数加连字符的组合,接着是三个数字跟着一个连字符,然后是四个数字。
逗号、冒号和空格在正则表达式中没有特殊含义;它们代表它们自己。
将所有这些放在一起,你得到的模式看起来像这样:
[-a-zA-Z]+, [-a-zA-Z]+( [-a-zA-Z]+)?: (\d{3}-)?\d{3}-\d{4}
一个真正的模式可能会更复杂一些,因为你不会假设逗号后面恰好有一个空格,第一个和中间名后面恰好有一个空格,冒号后面恰好有一个空格。但这很容易添加。
问题在于,虽然上述模式让你可以检查一行是否具有预期的格式,但你还不能提取任何数据。你所能做的就是编写一个像这样的程序:
import re
regexp = re.compile(r"[-a-zA-Z]+," *1*
r" [-a-zA-Z]+" *2*
r"( [-a-zA-Z]+)?" *3*
r": (\d{3}-)?\d{3}-\d{4}" *4*
)
file = open("textfile", 'r')
for line in file.readlines():
if regexp.search(line):
print("Yeah, I found a line with a name and number. So what?")
file.close()
-
1 姓氏和逗号
-
2 名字
-
3 可选的中名
-
4 冒号和电话号码
注意到你已经拆分了正则表达式模式,利用了 Python 隐式地将由空白字符分隔的任何字符串连接起来的事实。随着模式的增长,这种技术可以在保持模式可维护和可理解方面发挥巨大的作用。它还解决了行长度可能超出屏幕右边缘的问题。
幸运的是,你可以使用正则表达式从模式中提取数据,以及查看模式是否存在。第一步是使用()特殊字符将每个子模式分组,这些子模式对应于你想要提取的数据。然后给每个子模式一个唯一的名称,使用特殊序列?P<name>,如下所示:
(?P<last>[-a-zA-Z]+), (?P<first>[-a-zA-Z]+)( (?P<middle>([-a-zA-Z]+)))?:
(?P<phone>(\d{3}-)?\d{3}-\d{4}
(请注意,你应该将这些行作为单行输入,不要有行中断。由于空间限制,代码不能以这种方式表示。)
这里有一个明显的混淆点:?P<...>中的问号和表示中间名和区号可选的特殊字符问号没有任何关系。它们碰巧是同一个字符,这是一个不幸的半巧合。
现在你已经命名了模式的元素,你可以通过使用group方法来提取这些元素的匹配。你可以这样做,因为当search函数返回一个成功的匹配时,它不仅返回一个布尔值;它还返回一个记录了匹配内容的数据结构。你可以编写一个简单的程序来从你的列表中提取姓名和电话号码,并将它们再次打印出来,如下所示:
import re
regexp = re.compile(r"(?P<last>[-a-zA-Z]+)," *1*
r" (?P<first>[-a-zA-Z]+)" *2*
r"( (?P<middle>([-a-zA-Z]+)))?" *3*
r": (?P<phone>(\(\d{3}-)?\d{3}-\d{4})" *4*
)
file = open("textfile", 'r')
for line in file.readlines():
result = regexp.search(line)
if result == None:
print("Oops, I don't think this is a record")
else:
lastname = result.group('last')
firstname = result.group('first')
middlename = result.group('middle')
if middlename == None:
middlename = ""
phonenumber = result.group('phone')
print('Name:', firstname, middlename, lastname,' Number:', phonenumber)
file.close()
-
1 姓氏和逗号
-
2 名字
-
3 可选的中间名
-
4 冒号和电话号码
这里有一些有趣的地方:
-
你可以通过检查
search返回的值来找出匹配是否成功。如果值是None,则匹配失败;否则,匹配成功,你可以从search返回的对象中提取信息。 -
group用于提取与你的命名子模式匹配的任何数据。你传入你感兴趣的子模式的名称。 -
因为中间子模式是可选的,所以即使整个匹配成功,你也无法依赖它有一个值。如果匹配成功,但中间名的匹配失败,使用
group访问与中间子模式关联的数据将返回None。 -
电话号码的一部分是可选的,但另一部分不是。如果匹配成功,电话子模式必须有一些相关的文本,所以你不必担心它有
None的值。
尝试这个:提取匹配的文本
打国际电话通常需要一个加号和国家的代码。假设国家代码是两位数字,你将如何修改上面的代码来提取加号和国家代码作为号码的一部分?(再次强调,并非所有号码都有国家代码。)你将如何使代码处理一位到三位数字的国家代码?
16.5. 使用正则表达式替换文本
除了从文本中提取字符串外,你还可以使用 Python 的正则表达式模块在文本中查找字符串,并用其他字符串替换找到的字符串。你通过使用正则替换方法sub来完成此任务。以下示例将"the the"(可能是打字错误)替换为单个实例的"the":
>>> import re
>>> string = "If the the problem is textual, use the the re module"
>>> pattern = r"the the"
>>> regexp = re.compile(pattern)
>>> regexp.sub("the", string)
'If the problem is textual, use the re module'
sub方法使用调用的正则表达式(在这个例子中是regexp)扫描其第二个参数(在这个例子中是string),并通过用第一个参数(在这个例子中是"the")替换所有匹配的子字符串来产生一个新的字符串。
但如果你想要用新的子串替换匹配的子串,这些新的子串反映了匹配的值,该怎么办?这正是 Python 优雅之处所在。sub函数的第一个参数——替换子串,在示例中为"the"——根本不必是字符串。相反,它可以是函数。如果是一个函数,Python 会使用当前匹配对象调用它;然后它让该函数计算并返回一个替换字符串。
要查看此函数的实际效果,请构建一个示例,该示例接受一个包含整数值的字符串(没有小数点或小数部分),并返回一个具有相同数值但作为浮点数的字符串(带有尾随的小数点和零):
>>> import re
>>> int_string = "1 2 3 4 5"
>>> def int_match_to_float(match_obj):
... return(match_obj.group('num') + ".0")
...
>>> pattern = r"(?P<num>[0-9]+)"
>>> regexp = re.compile(pattern)
>>> regexp.sub(int_match_to_float, int_string)
'1.0 2.0 3.0 4.0 5.0'
在这种情况下,模式查找由一个或多个数字组成的数字([0-9]+部分)。但它还给出了一个名称(?P<num>...部分),这样替换字符串函数就可以通过引用该名称提取任何匹配的子串。然后sub方法扫描参数字符串"1 2 3 4 5",寻找任何匹配[0-9]+的内容。当sub找到一个匹配的子串时,它创建一个匹配对象,精确地定义了哪个子串与模式匹配,并使用该匹配对象作为唯一参数调用int_match_to_float函数。int_match_to_float使用group从匹配对象中提取匹配的子串(通过引用组名num)并通过连接匹配的子串和".0"生成一个新的字符串。sub返回新的字符串并将其作为子串合并到整体结果中。最后,sub从找到最后一个匹配子串的地方重新开始扫描,并继续这样做,直到找不到更多的匹配子串。
尝试这样做:替换文本
在第 16.4 节的检查点中,你扩展了电话号码正则表达式,使其也能识别国家代码。你将如何使用函数使现在没有国家代码的任何数字都带有+1(美国和加拿大的国家代码)?
实验室 16:电话号码标准化器
在美国和加拿大,电话号码由十个数字组成,通常分为三位区号、三位交换码和四位站点码。如第 16.4 节所述,它们可能或可能不带有+1 国家代码。然而,在实践中,你有许多种格式化电话号码的方法,例如(NNN) NNN-NNNN、NNN-NNN-NNNN、NNN NNN-NNNN、NNN.NNN.NNNN 和 NNN NNN NNNN,仅举几例。此外,国家代码可能不存在,可能没有加号,通常(但不总是)与号码之间由空格或破折号分隔。哇!
在这个实验中,你的任务是创建一个电话号码标准化器,它接受任何格式并返回一个格式化的电话号码 1-NNN-NNN-NNNN。
以下都是可能的电话号码:
| +1 223-456-7890 | 1-223-456-7890 | +1 223 456-7890 |
|---|---|---|
| (223) 456-7890 | 1 223 456 7890 | 223.456.7890 |
附加说明: 区号和交换码的第一个数字只能是 2-9,区号的第二个数字不能是 9。使用这些信息来验证输入,如果号码无效,则返回一个 ValueError 异常,异常信息为 invalid phone number。
概述
-
对于正则表达式特殊字符的完整列表和解释,请参阅 Python 文档。
-
除了
search和sub方法之外,还可以使用许多其他方法来分割字符串,从match对象中提取更多信息,在主参数字符串中查找子字符串的位置,以及精确控制正则表达式搜索在参数字符串上的迭代。 -
除了可以用来表示数字字符的
\d特殊序列之外,文档中还列出了许多其他特殊序列。 -
此外,还有正则表达式标志,您可以使用它们来控制执行极其复杂的匹配操作的一些更神秘方面。
第十七章. 数据类型作为对象
本章涵盖
-
将类型视为对象
-
使用类型
-
创建用户定义的类
-
理解鸭子类型
-
使用特殊方法属性
-
子类化内置类型
到目前为止,你已经学习了基本的 Python 类型以及如何使用类创建自己的数据类型。对于许多语言来说,这基本上就是数据类型了。但 Python 是动态类型的,这意味着类型是在运行时确定的,而不是在编译时。这是 Python 易于使用的原因之一。它还使得计算对象的类型(而不仅仅是对象本身)成为可能,有时甚至是必要的。
17.1. 类型也是对象
启动一个 Python 会话,尝试以下操作:
>>> type(5)
<class 'int'>
>>> type(['hello', 'goodbye'])
<class 'list'>
这是你第一次在 Python 中看到内置的type函数。它可以应用于任何 Python 对象,并返回该对象的类型。在这个例子中,该函数告诉你5是int(整数),而['hello', 'goodbye']是list——这些你可能已经知道了。
更有趣的是,Python 在调用类型时返回对象;<class 'int'>和<class 'list'>是返回对象的屏幕表示。type(5)的调用返回什么类型的对象?你有一个简单的方法可以找到答案。只需使用type在该结果上:
>>> type_result = type(5)
>>> type(type_result)
<class 'type'>
type函数返回的对象的类型恰好是<class 'type'>;你可以称之为类型对象。类型对象是另一种 Python 对象,它唯一的突出特点是它有时会引起的困惑。说类型对象是类型<class 'type'>与老艾博特和科斯特洛的“谁是第一?”喜剧节目有相同的清晰度。
17.2. 使用类型
现在你已经知道数据类型可以表示为 Python 类型对象,你能用它们做什么呢?你可以比较它们,因为任何两个 Python 对象都可以进行比较:
>>> type("Hello") == type("Goodbye")
True
>>> type("Hello") == type(5)
False
"Hello"和"Goodbye"的类型相同(它们都是字符串),但"Hello"和5的类型不同。除此之外,你还可以使用这种技术在你的函数和方法定义中提供类型检查。
17.3. 类型与用户定义的类
对象类型的兴趣最常见的原因是找出特定对象是否是用户定义类的实例。在确定对象是特定类型之后,代码可以适当地处理它。一个例子可以使事情更加清晰。首先,定义几个空类,以便设置一个简单的继承层次结构:
>>> class A:
... pass
...
>>> class B(A):
... pass
...
现在创建一个B类的实例:
>>> b = B()
如预期的那样,将type函数应用于b会告诉你b是当前__main__命名空间中定义的B类的实例:
>>> type(b)
<class '__main__.B'>
你也可以通过访问实例的特殊__class__属性来获得完全相同的信息:
>>> b.__class__
<class '__main__.B'>
你将相当频繁地使用该类来提取更多信息,所以请将其存储在某个地方:
>>> b_class = b.__class__
现在,为了强调 Python 中的一切都是对象,证明从 b 获取的类是你用 B 的名称定义的类:
>>> b_class == B
True
在这个例子中,你不需要存储 b 的类——你已经有它了,但我想要明确指出,一个类只是另一个 Python 对象,可以像任何 Python 对象一样存储或传递。
给定 b 的类,你可以通过使用其 __name__ 属性来找到该类的名称:
>>> b_class.__name__
'B'
你可以通过访问其 __bases__ 属性来找出一个类继承自哪些类,该属性包含一个包含所有基类的元组:
>>> b_class.__bases__
(<class '__main__.A'>,)
结合使用 __class__、__bases__ 和 __name__ 属性,可以对任何实例关联的类继承结构进行全面分析。
但有两个内置函数提供了更用户友好的方式来获取你通常需要的大部分信息:isinstance 和 issubclass。isinstance 函数是你应该用来确定,例如,传递给函数或方法的类是否为预期类型:
>>> class C:
... pass
...
>>> class D:
... pass
...
>>> class E(D):
... pass
...
>>> x = 12
>>> c = C()
>>> d = D()
>>> e = E()
>>> isinstance(x, E)
False
>>> isinstance(c, E) *1*
False
>>> isinstance(e, E)
True
>>> isinstance(e, D) *2*
True
>>> isinstance(d, E) *3*
False
>>> y = 12
>>> isinstance(y, type(5)) *4*
True
issubclass 函数仅适用于类类型。
>>> issubclass(C, D)
False
>>> issubclass(E, D)
True
>>> issubclass(D, D) *5*
True
>>> issubclass(e.__class__, D)
True
对于类实例,检查是否与类 1 相同。e 是类 D 的实例,因为 E 继承自 D 2。但 d 不是类 E 的实例 3。对于其他类型,你可以使用一个例子 4。一个类被认为是其自身的子类 5。
快速检查:类型
假设你想在尝试向对象 x 添加内容之前确保 x 是一个列表。你会使用什么代码?使用 type() 和 isinstance() 之间的区别是什么?这会是“跳之前先看”(LBYL)还是编程中更容易请求宽恕而不是许可(EAFP)?除了显式检查类型之外,你还有哪些其他选择?
17.4. 鸭式类型
使用 type、isinstance 和 issubclass 使得代码能够正确地确定对象或类的继承层次结构变得相当容易。尽管这个过程很简单,Python 还有一个使使用对象更加容易的功能:鸭式类型。鸭式类型(正如“如果它像鸭子走路,像鸭子嘎嘎叫,它可能就是一只鸭子”)指的是 Python 确定对象是否为操作所需类型的做法,它关注的是对象的接口而不是其类型。例如,如果操作需要一个迭代器,所使用的对象不需要是任何特定迭代器的子类,也不需要是任何迭代器的子类。唯一重要的是,用作迭代器的对象能够以预期的方式产生一系列对象。
相比之下,在像 Java 这样的语言中,会强制执行更严格的继承规则。简而言之,鸭子类型意味着在 Python 中,你不需要(并且可能不应该)担心类型检查函数或方法参数等。相反,你应该依靠可读性和文档化的代码,以及彻底的测试,以确保对象“像鸭子一样叫”。
鸭子类型可以增加代码的灵活性,并结合更高级的面向对象特性,使你能够创建类和对象来覆盖几乎所有情况。
17.5. 什么是特殊方法属性?
特殊方法属性 是 Python 类的一个属性,它对 Python 有特殊的意义。它被定义为一个方法,但不打算直接这样使用。特殊方法通常不是直接调用的;相反,它们是在对那个类的对象提出要求时由 Python 自动调用的。
可能最简单的例子是 __str__ 特殊方法属性。如果它在类中定义,那么每次在 Python 需要用户可读的实例字符串表示时,都会调用该类的 __str__ 方法属性,并使用它返回的值作为所需的字符串。为了看到这个属性的作用,定义一个表示红色、绿色和蓝色(RGB)颜色的类,它是一个包含三个数字的三元组,分别代表红色、绿色和蓝色的强度。除了定义标准的 __init__ 方法来初始化类的实例外,还定义一个 __str__ 方法来返回以合理的人性化格式表示实例的字符串。你的定义应该看起来像这样。
列表 17.1. 文件 color_module.py
class Color:
def __init__(self, red, green, blue):
self._red = red
self._green = green
self._blue = blue
def __str__(self):
return "Color: R={0:d}, G={1:d}, B={2:d}".format (self._red,
self._green, self._blue)
如果你将这个定义放入一个名为 color_module.py 的文件中,你可以加载它并以正常方式使用它:
>>> from color_module import Color
>>> c = Color(15, 35, 3)
如果你使用 print 打印 c,你可以看到 __str__ 特殊方法属性的存在:
>>> print(c)
Color: R=15, G=35, B=3
即使你的 __str__ 特殊方法属性没有被你的代码明确调用,Python 仍然使用了它,因为 Python 知道 __str__ 属性(如果存在)定义了一个将对象转换为用户可读字符串的方法。这是特殊方法属性的定义性特征;它允许你以特殊的方式定义功能,使其与 Python 挂钩。在其他方面,特殊方法属性可以用来定义对象,这些对象的行为在语法和语义上与列表或字典相当。例如,你可以使用这种能力来定义对象,这些对象的使用方式与 Python 列表完全相同,但使用平衡树而不是数组来存储数据。对于程序员来说,这样的对象看起来像是列表,但具有更快的插入、更慢的迭代和某些其他性能差异,这些差异可能有利于手头的任务。
本章的其余部分将涵盖使用特殊方法属性的更长的例子。本章并没有讨论 Python 所有可用的特殊方法属性,但它确实详细介绍了这个概念,使你可以轻松地使用其他特殊属性方法,所有这些方法都在内置类型的标准库文档中定义。
17.6. 使对象表现得像列表
这个示例问题涉及一个包含人们记录的大文本文件;每个记录由包含个人姓名、年龄和居住地的单行组成,字段之间用双分号(::)分隔。这样一个文件的一些行可能看起来像这样:
.
.
.
John Smith::37::Springfield, Massachusetts
Ellen Nelle::25::Springfield, Connecticut
Dale McGladdery::29::Springfield, Hawaii
.
.
.
假设你需要收集关于文件中人们年龄分布的信息。处理这个文件中的行有好多方法。这里有一种方法:
fileobject = open(filename, 'r')
lines = fileobject.readlines()
fileobject.close()
for line in lines:
. . . do whatever . . .
这种方法在理论上可行,但它会一次性将整个文件读入内存。如果文件太大而无法放入内存(这些文件可能就是那么大),程序将无法工作。
解决这个问题的另一种方法是:
fileobject = open(filename, 'r')
for line in fileobject:
. . . do whatever . . .
fileobject.close()
这段代码通过一次只读取一行来绕过内存不足的问题。它将正常工作,但假设你想要使打开文件更加简单,并且只想获取文件中的行的前两个字段(姓名和年龄)。你需要某种东西,至少在for循环的目的上,可以将文本文件当作行列表来处理,但不必一次性读取整个文本文件。
17.7. __getitem__ 特殊方法属性
一个解决方案是使用__getitem__特殊方法属性,你可以在任何用户定义的类中定义它,以使该类的实例能够响应列表访问语法和语义。如果AClass是一个定义了__getitem__的 Python 类,并且obj是该类的一个实例,那么像x = obj[n]和for x in obj:这样的操作是有意义的;obj可以像列表一样使用。
下面是生成的代码(解释随后):
class LineReader:
def __init__(self, filename):
self.fileobject = open(filename, 'r') *1*
def __getitem__(self, index):
line = self.fileobject.readline() *2*
if line == "": *3*
self.fileobject.close() *4*
raise IndexError *5*
else:
return line.split("::")[:2] *6*
for name, age in LineReader("filename"):
. . . do whatever . . .
-
1 打开文件进行读取
-
2 尝试读取行
-
3 如果没有更多数据 ...
-
4 ... 关闭文件对象 ...
-
5 ... 并引发
IndexError -
6 否则,分割行,返回前两个字段
初看之下,这个例子可能看起来比之前的解决方案更糟糕,因为代码更多,难以理解。但大部分代码都在一个类中,可以被放入它自己的模块中,例如myutils模块。然后程序就变成了
import myutils
for name, age in myutils.LineReader("filename"):
. . . do whatever . . .
LineReader类处理打开文件、逐行读取和关闭文件的全部细节。虽然需要稍微多一点初始开发时间,但它提供了一个工具,使得处理每行一个记录的大文本文件变得更加容易且错误率更低。请注意,Python 已经提供了几种强大的读取文件的方法,但这个例子有一个优点,那就是它相当容易理解。当你理解了这个概念后,你可以在许多情况下应用相同的原理。
17.7.1. 它是如何工作的
LineReader是一个类,__init__方法打开指定的文件以供读取,并将打开的fileobject存储起来以供后续访问。要理解__getitem__方法的使用,你需要知道以下三个要点:
-
任何定义了
__getitem__作为实例方法的对象都可以像列表一样返回元素:所有形式为object[i]的访问都被 Python 转换为形式为object.__getitem__(i)的方法调用,这被当作正常方法调用处理。它最终执行为__getitem__(object, i),使用类中定义的__getitem__版本。__getitem__每次调用的第一个参数是从中提取数据的对象,第二个参数是数据的索引。 -
因为
for循环逐个访问列表中的每条数据,所以形式为for arg in sequence:的循环通过反复调用__getitem__并使用递增的索引来实现。for循环首先将arg设置为sequence.__getitem__(0),然后设置为sequence.__getitem__(1),依此类推。 -
for循环捕获IndexError异常并通过退出循环来处理它们。这就是在正常列表或序列中使用时for循环终止的过程。
LineReader类仅用于for循环中,并且for循环始终生成具有均匀递增索引的调用:__getitem__(self, 0)、__getitem__(self, 1)、__getitem__(self, 2)等等。前面的代码利用了这一知识,逐行返回行,忽略index参数。
带着这些知识,理解LineReader对象如何在for循环中模拟序列是很容易的。循环的每次迭代都会在对象上调用特殊的 Python 属性方法__getitem__;结果,对象从其存储的fileobject中读取下一行并检查该行。如果该行非空,则返回。空行意味着已到达文件末尾;对象关闭fileobject并引发IndexError异常。IndexError被外层的for循环捕获,然后循环终止。
请记住,这个例子只是为了说明目的。通常,通过使用for line in fileobject:类型的循环遍历文件的行是足够的,但这个例子确实展示了在 Python 中创建类似列表或其他类型的对象是多么容易。
快速检查:getitem
__getitem__ 的示例使用非常有限,在许多情况下无法正确工作。在哪些情况下上述实现将失败或工作不正确?
17.7.2. 实现完整的列表功能
在前面的示例中,LineReader 类的对象仅在其正确响应从其读取的文件中的行顺序访问的范围内表现得像列表对象。你可能想知道如何扩展这种功能,使 LineReader(或其他)对象表现得更像列表。
首先,__getitem__ 方法应以某种方式处理其索引参数。因为 LineReader 类的全部目的是避免将大文件读入内存,所以将整个文件保留在内存中并返回适当的行是没有意义的。最明智的做法可能是检查 __getitem__ 调用中的每个索引都比前一个 __getitem__ 调用中的索引大一个(或者对于 LineReader 实例上的 __getitem__ 的第一次调用是 0)并且如果情况不是这样则引发错误。这种做法将确保 LineReader 实例仅按预期在 for 循环中使用。
更普遍地,Python 提供了几个与列表行为相关的特殊方法属性。__setitem__ 提供了一种定义当对象在列表赋值语法上下文中使用时应该执行什么操作的方法,即 obj[n] = val。一些其他特殊方法属性提供了不那么明显的列表功能,例如 __add__ 属性,它使对象能够响应 + 操作符,从而执行其列表连接版本。在类完全模拟列表之前,还需要定义几个其他特殊方法,但通过定义适当的 Python 特殊方法属性,你可以实现完整的列表模拟。下一节提供了一个示例,展示了如何实现一个完整的列表模拟类。
17.8. 给对象赋予完整的列表能力
__getitem__ 是许多 Python 特殊函数属性之一,可以在类中定义,以允许该类的实例显示特殊行为。要了解特殊方法属性如何进一步扩展,有效地以无缝的方式将新能力集成到 Python 中,请查看另一个更全面的示例。
当使用列表时,通常任何特定的列表只包含一种类型的元素,例如字符串列表或数字列表。一些语言,如 C++,具有强制执行这种限制的能力。在大型程序中,声明列表包含特定类型的元素可以帮助你追踪错误。尝试将错误类型的元素添加到类型列表中会导致错误消息,这可能会在程序开发的早期阶段识别出问题,否则可能不会发生。
Python 内置没有类型列表,大多数 Python 开发者并不觉得缺少它们。但是,如果你关心强制列表的同质性,特殊方法属性可以让你轻松创建一个表现得像类型列表的类。以下是这样的一个类的开头(它广泛使用了 Python 内置的 type 和 isinstance 函数来检查对象类型):
class TypedList:
def __init__(self, example_element, initial_list=[]):
self.type = type(example_element) *1*
if not isinstance(initial_list, list):
raise TypeError("Second argument of TypedList must "
"be a list.")
for element in initial_list:
if not isinstance(element, self.type):
raise TypeError("Attempted to add an element of "
"incorrect type to a typed list.")
self.elements = initial_list[:]
example_element 参数通过提供一个元素类型的示例来定义这个列表可以包含的类型 1。
这里定义的 TypedList 类,让你能够进行如下形式的调用
x = TypedList ('Hello', ["List", "of", "strings"])
第一个参数,'Hello',根本没有被包含到结果的数据结构中。它被用作一个例子,说明列表必须包含的元素类型(在这种情况下是字符串)。第二个参数是一个可选的列表,可以用来提供一个初始值列表。TypedList 类的 __init__ 函数会检查在创建 TypedList 实例时传入的任何列表元素是否与提供的示例值类型相同。如果有任何类型不匹配,则会引发异常。
这个版本的 TypedList 类不能用作列表,因为它不响应设置或访问列表元素的常规方法。要解决这个问题,你需要定义 __setitem__ 和 __getitem__ 特殊方法属性。__setitem__ 方法在执行形式为 TypedListInstance[i] = value 的语句时由 Python 自动调用,而 __getitem__ 方法在任何表达式 TypedListInstance[i] 被评估以返回 TypedListInstance 中第 i 个槽位的值时被调用。以下是 TypedList 类的下一个版本。因为你将要对许多新元素进行类型检查,所以这个函数被抽象成新的私有方法 __check:
class TypedList:
def __init__(self, example_element, initial_list=[]):
self.type = type(example_element)
if not isinstance(initial_list, list):
raise TypeError("Second argument of TypedList must "
"be a list.")
for element in initial_list:
self.__check(element)
self.elements = initial_list[:]
def __check(self, element):
if type(element) != self.type:
raise TypeError("Attempted to add an element of "
"incorrect type to a typed list.")
def __setitem__(self, i, element):
self.__check(element)
self.elements[i] = element
def __getitem__(self, i):
return self.elements[i]
现在 TypedList 类的实例看起来更像列表了。例如,以下代码是有效的:
>>> x = TypedList("", 5 * [""])
>>> x[2] = "Hello"
>>> x[3] = "There"
>>> print(x[2] + ' ' + x[3])
Hello There
>>> a, b, c, d, e = x
>>> a, b, c, d
('', '', 'Hello', 'There')
在 print 语句中对 x 的元素访问由 __getitem__ 处理,它将这些元素传递到存储在 TypedList 对象中的列表实例。对 x[2] 和 x[3] 的赋值由 __setitem__ 处理,它会检查被赋值的元素是否为适当的类型,然后对 self.elements 中包含的列表执行赋值操作。最后一行使用 __getitem__ 来解包 x 的前五个元素,然后将它们分别打包到变量 a、b、c、d 和 e 中。__getitem__ 和 __setitem__ 的调用是由 Python 自动进行的。
完成 TypedList 类,以便 TypedList 对象在所有方面都像列表对象一样表现,需要更多的代码。特殊方法属性 __setitem__ 和 __getitem__ 应该被定义,以便 TypedList 实例可以处理切片表示法以及单个项访问。__add__ 应该被定义,以便可以进行列表添加(连接)。__mul__ 应该被定义,以便可以进行列表乘法。__len__ 应该被定义,以便 len(TypedListInstance) 的调用可以正确评估。__delitem__ 应该被定义,以便 TypedList 类可以正确处理 del 语句。此外,还应该定义一个 append 方法,以便可以通过标准的列表样式 append、insert 和 extend 方法将元素附加到 TypedList 实例上。
尝试这样做:实现列表特殊方法
尝试实现特殊方法 __len__ 和 __delitem__,以及一个 append 方法。
17.9. 从内置类型子类化
之前的示例是理解如何从头实现一个类似列表的类的良好练习,但这也是一项大量工作。在实践中,如果你打算按照这里展示的方式实现自己的类似列表的结构,你可能会考虑子类化列表类型或 UserList 类型。
17.9.1. 子类化列表
与之前示例中从头开始创建一个类型列表的类不同,你可以通过子类化列表类型并覆盖所有需要知道允许类型的方来做到这一点。这种方法的一个主要优点是,由于它已经是一个列表,因此你的类具有所有列表操作的默认版本。需要记住的主要事情是,Python 中的每个类型都是一个类,如果你需要内置类型的某种行为变化,你可能需要考虑子类化该类型:
class TypedListList(list):
def __init__(self, example_element, initial_list=[]):
self.type = type(example_element)
if not isinstance(initial_list, list):
raise TypeError("Second argument of TypedList must "
"be a list.")
for element in initial_list:
self.__check(element)
super().__init__(initial_list)
def __check(self, element):
if type(element) != self.type:
raise TypeError("Attempted to add an element of "
"incorrect type to a typed list.")
def __setitem__(self, i, element):
self.__check(element)
super().__setitem__(i, element)
>>> x = TypedListList("", 5 * [""])
>>> x[2] = "Hello"
>>> x[3] = "There"
>>> print(x[2] + ' ' + x[3])
Hello There
>>> a, b, c, d, e = x
>>> a, b, c, d
('', '', 'Hello', 'There')
>>> x[:]
['', '', 'Hello', 'There', '']
>>> del x[2]
>>> x[:]
['', '', 'There', '']
>>> x.sort()
>>> x[:]
['', '', '', 'There']
注意,在这种情况下,你需要做的只是实现一个方法来检查要添加的项目类型,然后调整 __setitem__ 在调用列表的常规 __setitem__ 方法之前进行该检查。其他方法,如 sort 和 del,无需进一步编码即可工作。如果你只需要在行为上做少量变化,重载内置类型可以节省相当多的时间,因为类的大部分内容可以保持不变。
17.9.2. 子类化 UserList
如果你需要一个与列表不同的变体(如前例所示),还有一个第三种选择:你可以子类化 UserList 类,这是一个在 collections 模块中找到的列表包装类。UserList 是为 Python 的早期版本创建的,当时无法子类化列表类型,但它仍然很有用,尤其是在当前情况下,因为底层列表作为 data 属性可用:
from collections import UserList
class TypedUserList(UserList):
def __init__(self, example_element, initial_list=[]):
self.type = type(example_element)
if not isinstance(initial_list, list):
raise TypeError("Second argument of TypedList must "
"be a list.")
for element in initial_list:
self.__check(element)
super().__init__(initial_list)
def __check(self, element):
if type(element) != self.type:
raise TypeError("Attempted to add an element of "
"incorrect type to a typed list.")
def __setitem__(self, i, element):
self.__check(element)
self.data[i] = element
def __getitem__(self, i):
return self.data[i]
>>> x = TypedUserList("", 5 * [""])
>>> x[2] = "Hello"
>>> x[3] = "There"
>>> print(x[2] + ' ' + x[3])
Hello There
>>> a, b, c, d, e = x
>>> a, b, c, d
('', '', 'Hello', 'There')
>>> x[:]
['', '', 'Hello', 'There', '']
>>> del x[2]
>>> x[:]
['', '', 'There', '']
>>> x.sort()
>>> x[:]
['', '', '', 'There']
这个例子与子类化 list 非常相似,只是在类的实现中,项目列表作为 data 成员内部可用。在某些情况下,直接访问底层数据结构可能很有用。此外,除了 UserList,还有 UserDict 和 UserString 包装类。
17.10. 何时使用特殊方法属性
通常,在使用特殊方法属性时要谨慎。其他需要与你代码一起工作的程序员可能会想知道为什么一个序列类型对象能够正确响应标准索引符号,而另一个则不能。
我的一般指导原则是在以下两种情况下使用特殊方法属性:
-
如果我在自己的代码中有一个经常使用的类,它在某些方面类似于 Python 内置类型,我会定义这样的特殊方法属性作为有用的。这种情况最常见于以某种方式表现像序列的对象。
-
如果我有一个与内置类行为相同或几乎相同的类,我可能会选择定义所有适当特殊函数属性或子类化内置 Python 类型并将类分发出去。后者解决方案的一个例子可能是将列表实现为平衡树,这样访问速度较慢但插入速度比标准列表快。
这些规则并不是一成不变的。例如,定义类的 __str__ 特殊方法属性通常是一个好主意,这样你就可以在调试代码中使用 print(instance) 并在屏幕上打印出有信息量和良好外观的对象表示。
快速检查:特殊方法属性和现有类型的子类化
假设你想要一个类似字典的类型,只允许字符串作为键(可能为了使其像第十三章中描述的 shelf 对象那样工作)。你有哪些创建此类类的选项?每种选项的优点和缺点是什么?
摘要
-
Python 有工具可以在你的代码中进行类型检查,但通过利用鸭子类型,你可以编写更灵活的代码,无需过于关注类型检查。
-
特殊方法属性和内置类的子类化可以用于向用户创建的类添加类似列表的行为。
-
Python 使用鸭子类型、特殊方法属性和子类化,使得以多种方式构建和组合类成为可能。
第十八章. 包
本章涵盖
-
定义一个包
-
创建一个简单的包
-
探索一个具体例子
-
使用
__all__属性 -
正确使用包
模块使得重用小块代码变得容易。但当项目增长,你想要重新加载的代码在物理上或逻辑上超出了单个文件所能容纳的范围时,问题就出现了。如果有一个巨大的模块文件不是一个令人满意的解决方案,那么有一堆没有连接的小模块也不会好多少。这个问题的答案是,将相关的模块组合成一个包。
18.1. 什么是包?
模块 是包含代码的文件。模块定义了一组通常相关的 Python 函数或其他对象。模块的名称来源于文件的名称。
当你理解了模块,包就变得容易了,因为包是一个包含代码和可能进一步子目录的目录。包包含一组通常相关的代码文件(模块)。包的名称来源于主包目录的名称。
包是模块概念的天然扩展,旨在处理非常大的项目。就像模块将相关的函数、类和变量分组一样,包将相关的模块分组。
18.2. 第一个例子
为了了解包在实际中可能如何工作,考虑一个设计布局,这种项目在本质上是非常大的:类似于 Mathematica、Maple 或 MATLAB 的一般数学包。例如,Maple 由数千个文件组成,某种层次结构对于保持此类项目的有序性至关重要。将你的整个项目称为 mathproj。
你可以用许多方式组织这样的项目,但一个合理的设计是将项目分为两部分:ui,包含 UI 元素,和 comp,包含计算元素。在 comp 中,进一步将计算方面分割成 symbolic(实数和复数符号计算,如高中代数)和 numeric(实数和复数数值计算,如数值积分)可能是有意义的。然后,在项目的 symbolic 和 numeric 部分都可能有 constants.py 文件。
项目数值部分中的 constants.py 文件将 π 定义为
pi = 3.141592
而项目符号部分的 constants.py 文件将 π 定义为
class PiClass:
def __str__(self):
return "PI"
pi = PiClass()
这意味着像 pi 这样的名称可以在(并从)两个不同的名为 constants.py 的文件中使用(导入),如图 18.1 所示。
图 18.1. 组织数学包

符号常数 constants.py 文件将 pi 定义为一个抽象的 Python 对象,PiClass 类的唯一实例。随着系统的开发,可以在该类中实现各种操作,这些操作返回符号结果而不是数值结果。
从这个设计结构到目录结构有一个自然的映射。项目顶层目录,称为 mathproj,包含子目录 ui 和 comp;comp 又包含子目录 symbolic 和 numeric;而 symbolic 和 numeric 各自都包含自己的 constants.pi 文件。
给定这种目录结构,假设根目录 mathproj 安装在了 Python 搜索路径的某个位置,Python 代码无论是mathproj包内部还是外部都可以访问pi的两个变体,即mathproj.symbolic.constants.pi和mathproj.numeric.constants.pi。换句话说,包中项目的 Python 名称反映了包含该项目的文件路径名。
这就是包的全部内容。它们是将大量 Python 代码组织成连贯整体的方法,通过允许代码在不同的文件和目录之间分割,并基于包文件的目录结构实施模块/子模块命名方案。不幸的是,在实践中包并不这么简单,因为细节的干扰使得它们的使用比理论更复杂。包的实用方面是本章其余部分的基础。
18.3. 一个具体示例
本章的其余部分使用一个运行示例来说明包机制的内部工作原理(见图 18.2)。文件名和路径以纯文本形式显示,以明确我是在谈论文件/目录,还是由该文件/目录定义的模块/包。你将在示例包中使用的文件显示在列表 18.1 到 18.6 中。
列表 18.1. 文件 mathproj/init.py
print("Hello from mathproj init")
__all__ = ['comp']
version = 1.03
图 18.2. 示例包

列表 18.2. 文件 mathproj/comp/init.py
__all__ = ['c1']
print("Hello from mathproj.comp init")
列表 18.3. 文件 mathproj/comp/c1.py
x = 1.00
列表 18.4. 文件 mathproj/comp/numeric/init.py
print("Hello from numeric init")
列表 18.5. 文件 mathproj/comp/numeric/n1.py
from mathproj import version
from mathproj.comp import c1
from mathproj.comp.numeric.n2 import h
def g():
print("version is", version)
print(h())
列表 18.6. 文件 mathproj/comp/numeric/n2.py
def h():
return "Called function h in module n2"
为了本章示例的目的,假设你已经在 Python 搜索路径上的 mathproj 目录中创建了这些文件。(当执行这些示例时,确保 Python 的当前工作目录是包含 mathproj 的目录就足够了。)
注意
在本书的大多数示例中,不需要为每个示例启动一个新的 Python shell。你通常可以在之前示例使用的 Python shell 中执行示例,并仍然得到显示的结果。然而,本章的示例并非如此,因为为了示例能够正常工作,Python 命名空间必须保持干净(未被之前的import语句修改)。如果你运行以下示例,请确保你在自己的 shell 中运行每个单独的示例。 在 IDLE 中,这需要退出并重新启动程序,而不仅仅是关闭和重新打开其 Shell 窗口。
18.3.1. 包中的__init__.py文件
你会注意到,你包中的所有目录——mathproj、mathproj/comp 和 mathproj/numeric——都包含一个名为__init__.py的文件。__init__.py文件有两个作用:
-
Python 要求一个目录在被视为包之前必须包含一个
__init__.py文件。这个要求防止了包含杂乱 Python 代码的目录被意外导入,好像它们定义了一个包一样。 -
Python 在第一次加载包或子包时会自动执行
__init__.py文件。这次执行允许你进行任何想要的包初始化。
第一点通常更重要。对于许多包,你不需要在包的__init__.py文件中放置任何内容;只需确保存在一个空的__init__.py文件。
18.3.2. mathproj 包的基本使用
在深入了解包的细节之前,看看如何访问mathproj包中包含的项目。启动一个新的 Python shell,并执行以下操作:
>>> import mathproj
Hello from mathproj init
如果一切顺利,你应该会得到另一个输入提示,没有错误信息。此外,屏幕上应该通过mathproj/__init__.py文件中的代码打印出消息"Hello from mathproj init"。我很快会更多地讨论__init__.py文件;现在,你需要知道的是,文件在包首次加载时自动运行。
mathproj/__init__.py文件将 1.03 赋值给变量version。version在mathproj包命名空间的作用域内,创建后,你可以通过mathproj看到它,即使是从mathproj/__init__.py文件外部也可以:
>>> mathproj.version
1.03
在使用中,包可以看起来很像模块;它们可以通过属性提供对它们内部定义的对象的访问。这个事实并不令人惊讶,因为包是模块的泛化。
18.3.3. 加载子包和子模块
现在开始查看mathproj包中定义的各种文件是如何相互交互的。要做到这一点,调用在mathproj/comp/numeric/n1.py文件中定义的函数g。第一个明显的问题是这个模块是否已经被加载。你已经加载了mathproj,但它的子包呢?为了查看它是否为 Python 所知,输入
>>> mathproj.comp.numeric.n1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: module 'mathproj' has no attribute 'comp'
换句话说,加载包的顶级模块并不足以加载所有子模块,这与 Python 的哲学相符,即它不应该在你背后做事情。清晰比简洁更重要。
这个限制足够简单,可以克服。你导入感兴趣的模块,然后在该模块中执行函数g:
>>> import mathproj.comp.numeric.n1
Hello from mathproj.comp init
Hello from numeric init
>>> mathproj.comp.numeric.n1.g()
version is 1.03
Called function h in module n2
注意,然而,以Hello开头的行是在加载mathproj.comp.numeric.n1时作为副作用打印出来的。这两行是由 mathproj/comp 和 mathproj/comp/numeric 中的__init__.py文件中的print语句打印出来的。换句话说,在 Python 能够导入mathproj.comp.numeric.n1之前,它必须先导入mathproj.comp,然后是mathproj.comp.numeric。每当包首次导入时,其关联的__init__.py文件就会被执行,从而产生Hello行。为了确认mathproj.comp和mathproj.comp.numeric都是作为导入mathproj.comp.numeric.n1过程的一部分被导入的,你可以检查以确认mathproj.comp和mathproj.comp.numeric现在被 Python 会话所知:
>>> mathproj.comp
<module 'mathproj.comp' from 'mathproj/comp/__init__.py'>
>>> mathproj.comp.numeric
<module 'mathproj.comp.numeric' from 'mathproj/comp/numeric/__init__.py'>
18.3.4. import statements within packages
包内的文件默认情况下无法自动访问同一包内其他文件中定义的对象。就像在外部模块中一样,你必须使用import语句来显式访问其他包文件中的对象。为了了解这种import的使用在实际中是如何工作的,回顾一下n1子包。n1.py中包含的代码
from mathproj import version
from mathproj.comp import c1
from mathproj.comp.numeric.n2 import h
def g():
print("version is", version)
print(h())
g使用了顶级mathproj包中的version以及n2模块中的函数h;因此,包含g的模块必须导入version和h以使它们可访问。你像在mathproj包外部使用import语句一样导入version:通过说from mathproj import version。在这个例子中,你通过说from mathproj.comp.numeric.n2 import h显式地将h导入到代码中,这种技术在任何文件中都可以使用;包文件的显式导入总是允许的。但是,因为n2.py与n1.py在同一个目录下,你也可以使用相对导入,通过在子模块名称前添加一个点。换句话说,你可以这样写
from .n2 import h
如n1.py中的第三行所示,并且运行正常。
你可以添加更多的点来在包层次结构中向上移动更多层级,并且可以添加模块名称。而不是写成
from mathproj import version
from mathproj.comp import c1
from mathproj.comp.numeric.n2 import h
你原本可以将 n1.py 的导入写成
from ... import version
from .. import c1
from . n2 import h
相对导入既方便又快捷,但请注意,它们是相对于模块的__name__属性的。因此,任何作为主模块执行并因此具有__main__作为其__name__的模块都不能使用相对导入。
18.4. The all attribute
如果你回顾一下在mathproj中定义的各种__init__.py文件,你会注意到其中一些定义了一个名为__all__的属性。这个属性与from ... import *形式的语句的执行有关,需要解释。
通常来说,你希望如果外部代码执行了from mathproj import *这个语句,它将导入mathproj中所有非私有名称。但在实践中,事情要复杂得多。主要问题是某些操作系统在文件名方面对大小写有模糊的定义。由于包中的对象可以由文件或目录定义,这种情况导致了对子包可能导入的确切名称存在歧义。如果你说from mathproj import *,comp会被导入为comp、Comp还是COMP?如果你仅仅依赖于操作系统报告的名称,结果可能是不可预测的。
对于这个问题,没有好的解决方案,这是由糟糕的操作系统设计导致的固有问题。作为最好的解决方案,引入了__all__属性。如果存在于__init__.py文件中,__all__应该提供一个字符串列表,定义了在执行特定包上的from ... import *时应该导入的名称。如果没有__all__,给定包上的from ... import *将不起作用。因为文本文件中的大小写总是有意义的,所以导入的对象的名称不会产生歧义,如果操作系统认为comp和COMP是相同的,那是它的问题。
再次启动 Python,尝试以下操作:
>>> from mathproj import *
Hello from mathproj init
Hello from mathproj.comp init
mathproj/__init__.py中的__all__属性包含单个条目comp,而import语句只导入comp。检查comp现在是否被 Python 会话所知是很容易的:
>>> comp
<module 'mathproj.comp' from 'mathproj/comp/__init__.py'>
但请注意,使用from ... import *语句没有递归导入名称。comp包的__all__属性包含c1,但c1并不会因为你的from mathproj import *语句而神奇地被加载:
>>> c1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'c1' is not defined
要插入mathproj.comp中的名称,你必须再次进行显式导入:
>>> from mathproj.comp import c1
>>> c1
<module 'mathproj.comp.c1' from 'mathproj/comp/c1.py'>
18.5. 正确使用包
大多数包的结构不应该像这些例子所暗示的那样复杂。包机制允许你在包设计中的复杂性和嵌套方面有很大的灵活性。很明显,可以构建非常复杂的包,但并不明显的是它们应该被构建。
这里有一些在大多数情况下都适用的建议:
-
包不应该使用深度嵌套的目录结构。除了绝对巨大的代码集合外,通常不需要这样做。对于大多数包来说,一个单独的顶层目录就足够了。两层层次结构应该能够有效地处理除了少数情况之外的所有情况。正如 Tim Peters 在Python 之禅(见附录 A)中所写的那样,“扁平优于嵌套。”
-
虽然你可以使用
__all__属性通过不列出这些名称来隐藏from ... import *中的名称,但这样做可能并不是一个好主意,因为它不一致。如果你想隐藏名称,通过在名称前加上下划线使其成为私有名称。
快速检查:包
假设你正在编写一个包,该包接受一个 URL,检索该 URL 指向的页面上的所有图像,将它们调整到标准尺寸,并将它们存储起来。不考虑每个函数的确切编码细节,你将如何将这些功能组织到一个包中?
实验室 18:创建一个包
在第十四章(kindle_split_025.html#ch14)中,你为你在第十一章(kindle_split_022.html#ch11)中创建的文本清理和单词频率统计模块添加了错误处理。将此代码重构为一个包含一个用于清理函数的模块、一个用于处理函数的模块和一个用于自定义异常的模块的包。然后编写一个简单的 main 函数,使用这三个模块。
摘要
-
包让你能够创建跨越多个文件和目录的代码库。
-
使用包比使用单个模块更好地组织大量代码。
-
你应该小心不要在包中嵌套超过一两个级别的目录,除非你有一个非常大且复杂的库。
第十九章。使用 Python 库
本章涵盖
-
管理各种数据类型——字符串、数字等
-
操作文件和存储
-
访问操作系统服务
-
使用互联网协议和格式
-
开发和调试工具
-
访问 PyPI(也称为“奶酪店”)
-
使用 pip 和 venv 安装 Python 库和虚拟环境
Python 长期以来一直宣称其关键优势之一是“内置电池”哲学。这意味着 Python 的默认安装包含一个丰富的标准库,让你能够处理各种情况而无需安装额外的库。本章为您提供了标准库部分内容的概述,以及一些关于查找和安装外部模块的建议。
19.1。“内置电池”:标准库
在 Python 中,被认为是库的部分包括几个组件,包括可以不使用import语句使用的内置数据类型和常量,如数字和列表,以及一些内置函数和异常。库的最大部分是一个广泛的模块集合。如果你有 Python,你也有用于操作各种类型的数据和文件的库、与操作系统交互的库、为许多互联网协议编写服务器和客户端的库,以及用于开发和调试代码的库。
下面的内容是对高点的概述。尽管提到了大多数主要模块,但我仍然建议您花时间探索 Python 文档中的一部分库参考,以获得最完整和最新的信息。特别是,在寻找外部库之前,请务必浏览 Python 已经提供的功能。你可能会对你的发现感到惊讶。
19.1.1。管理各种数据类型
标准库自然包含对 Python 内置类型的支持,我在本节中提到了这一点。此外,标准库中的三个类别处理各种数据类型:字符串服务、数据类型和数值模块。
字符串服务包括表 19.1 中处理字节和字符串的模块。这些模块主要处理字符串和文本、字节序列以及 Unicode 操作。
表 19.1。字符串服务模块
| 模块 | 描述和可能用途 |
|---|---|
| 字符串 | 与字符串常量(如数字或空白字符)进行比较;格式化字符串(见第六章) |
| re | 使用正则表达式搜索和替换文本(见第十六章) |
| struct | 将字节解释为打包的二进制数据,并从文件中读取和写入结构化数据 |
| difflib | 使用计算差异、查找字符串或序列之间的差异以及创建补丁和 diff 文件的辅助工具 |
| textwrap | 包装和填充文本,并通过断行或添加空格来格式化文本 |
数据类型类别是一个多样化的模块集合,涵盖了各种数据类型,特别是时间、日期和集合,如表 19.2 所示。
表 19.2. 数据类型模块
| 模块 | 描述和可能用途 |
|---|---|
| datetime, calendar | 日期、时间和日历操作 |
| collections | 容器数据类型 |
| enum | 允许创建将符号名称绑定到常量值的枚举类 |
| 数组 | 高效的数值数组 |
| sched | 事件调度器 |
| queue | 同步队列类 |
| copy | 浅拷贝和深拷贝操作 |
| pprint | 数据美化打印器 |
| typing | 支持使用类型提示注解代码,特别是函数参数和返回值的类型 |
如其名所示,数值和数学模块处理数字和数学运算,其中最常见的模块列在表 19.3 中。这些模块包含了您创建自己的数值类型和处理广泛数学运算所需的一切。
表 19.3. 数值和数学模块
| 模块 | 描述和可能用途 |
|---|---|
| 数字 | 数值抽象基类 |
| math, cmath | 实数和复数的数学函数 |
| decimal | 十进制定点和浮点算术 |
| statistics | 计算数学统计的函数 |
| fractions | 有理数 |
| random | 生成伪随机数和选择,以及洗牌序列 |
| itertools | 创建迭代器的函数,用于高效循环 |
| functools | 高阶函数和对可调用对象的操作 |
| operator | 作为函数的标准运算符 |
19.1.2. 操作文件和存储
标准库中的另一个广泛类别涵盖了文件、存储和数据持久性,总结在表 19.4 中。这个类别从文件访问模块到数据持久性和压缩以及处理特殊文件格式的模块。
表 19.4. 文件和存储模块
| 模块 | 描述和可能用途 |
|---|---|
| os.path | 执行常见的路径名操作 |
| pathlib | 以面向对象的方式处理路径名 |
| fileinput | 遍历多个输入流的行 |
| filecmp | 比较文件和目录 |
| tempfile | 生成临时文件和目录 |
| glob, fnmatch | 使用 UNIX 风格的路径名和文件名模式处理 |
| linecache | 获取对文本行的随机访问 |
| shutil | 执行高级文件操作 |
| pickle, shelve | 启用 Python 对象序列化和持久化 |
| sqlite3 | 使用 SQLite 数据库的 DB-API 2.0 接口 |
| zlib, gzip, bz2, zipfile, tarfile | 与归档文件和压缩操作 |
| csv | 读取和写入 CSV 文件 |
| configparser | 使用配置文件解析器;读取/写入 Windows 风格的配置 .ini 文件 |
19.1.3. 访问操作系统服务
这个类别是另一个广泛的类别,包含处理操作系统模块。如表 19.5 所示,这个类别包括处理命令行参数、重定向文件和打印输出和输入、写入日志文件、运行多个线程或进程以及加载非 Python(通常是 C)库以在 Python 中使用的工具。
表 19.5. 操作系统模块
| Module | 描述 |
|---|---|
| os | 杂项操作系统接口 |
| io | 处理流的工具核心 |
| time | 时间访问和转换 |
| optparse | 强大的命令行选项解析器 |
| logging | Python 的日志记录设施 |
| getpass | 可移植的密码输入 |
| curses | 用于字符单元格显示的终端处理 |
| platform | 访问底层平台标识数据 |
| ctypes | Python 的外部函数库 |
| select | 等待 I/O 完成 |
| threading | 高级线程接口 |
| multiprocessing | 基于进程的线程接口 |
| subprocess | 子进程管理 |
19.1.4. 使用互联网协议和格式
互联网协议和格式类别涉及编码和解码在互联网上用于数据交换的许多标准格式,从 MIME 和其他编码到 JSON 和 XML。这个类别还包括编写常见服务(尤其是 HTTP)的客户端和服务器以及用于编写自定义服务的通用套接字服务器的模块。这些模块中最常用的列在表 19.6 中。
表 19.6. 支持互联网协议和格式的模块
| Module | 描述 |
|---|---|
| socket, ssl | 低级网络接口和套接字对象的 SSL 包装器 |
| 电子邮件和 MIME 处理包 | |
| json | JSON 编码和解码器 |
| mailbox | 操作各种格式的邮箱 |
| mimetypes | 将文件名映射到 MIME 类型 |
| base64, binhex, binascii, quopri, uu | 使用各种编码对文件或流进行编码/解码 |
| html.parser, html.entities | 解析 HTML 和 XHTML |
| xml.parsers.expat, xml.dom, xml.sax, xml.etree.ElementTree | XML 的各种解析器和工具 |
| cgi, cgitb | 常见网关接口支持 |
| wsgiref | WSGI 实用工具和参考实现 |
| urllib.request, urllib.parse | 打开和解析 URL |
| ftplib, poplib, imaplib, nntplib, smtplib, telnetlib | 各种互联网协议的客户端 |
| socketserver | 网络服务器框架 |
| http.server | HTTP 服务器 |
| xmlrpc.client, xmlrpc.server | XML-RPC 客户端和服务器 |
19.1.5. 开发和调试工具以及运行时服务
Python 有几个模块可以帮助您在运行时调试、测试、修改以及与其他 Python 代码交互。如 表 19.7 所示,此类类别包括两个测试工具、性能分析器、与错误跟踪交互的模块、解释器的垃圾回收以及允许您调整其他模块导入的模块。
表 19.7. 开发、调试和运行时模块
| 模块 | 描述 |
|---|---|
| pydoc | 文档生成器和在线帮助系统 |
| doctest | 测试交互式 Python 示例 |
| unittest | 单元测试框架 |
| test.support | 测试的实用函数 |
| pdb | Python 调试器 |
| profile, cProfile | Python 性能分析器 |
| timeit | 测量小代码片段的执行时间 |
| trace | 跟踪或追踪 Python 语句执行 |
| sys | 系统特定参数和函数 |
| atexit | 退出处理程序 |
| future | 未来语句定义——即将添加到 Python 的功能 |
| gc | 垃圾回收器接口 |
| 检查 | 检查活动对象 |
| imp | 访问导入内部 |
| zipimport | 从 zip 存档导入模块 |
| modulefinder | 查找脚本使用的模块 |
19.2. 超越标准库
虽然 Python 的“内置电池”哲学和丰富的标准库意味着您可以使用 Python 做很多事情,但不可避免地会有需要一些 Python 没有提供的功能的情况。本节概述了您在需要执行标准库中没有的功能时的选择。
19.3. 添加更多 Python 库
寻找一个 Python 包或模块可能就像在搜索引擎中输入您要查找的功能(例如 mp3 标签 和 Python)然后筛选结果一样简单。如果您幸运的话,您可能会找到为您的操作系统打包的模块——带有可执行的 Windows 或 macOS 安装程序或适用于您的 Linux 发行版的包。
这种技术是向您的 Python 安装添加库的最简单方法之一,因为安装程序或您的包管理器会负责将模块正确添加到您的系统中的所有细节。它也可以是安装更复杂库的答案,例如具有复杂构建要求和依赖关系的科学库。
通常情况下,除了科学库之外,这些预构建的包并不是 Python 软件的常规做法。这些包通常稍微旧一些,并且在安装位置和方式上提供的灵活性较少。
19.4. 使用 pip 和 venv 安装 Python 库
如果您需要一个未为您的平台预打包的第三方模块,您将不得不求助于其源代码分发。这一事实带来了一些问题:
-
要安装该模块,您必须找到并下载它。
-
正确安装单个 Python 模块可能需要处理 Python 的路径和系统权限的一定程度的麻烦,这使得标准的安装系统很有帮助。
Python 提供pip作为解决这两个问题的当前解决方案。pip试图在 Python 包索引中查找模块(关于这一点稍后讨论),下载它及其依赖项,并负责安装。pip的基本语法相当简单。例如,要从命令行安装流行的 requests 库,你只需要这样做:
$ python3.6 -m pip install requests
升级到库的最新版本只需要添加–-upgrade开关:
$ python3.6 -m pip install –-upgrade requests
最后,如果你需要指定包的特定版本,你可以像这样将其附加到名称上:
$ python3.6 -m pip install requests==2.11.1
$ python3.6 -m pip install requests>=2.9
19.4.1. 使用–user 标志安装
在许多情况下,你无法或在 Python 的主系统实例中不希望安装 Python 包。可能你需要库的最新版本,但某些其他应用程序(或系统本身)仍在使用较旧版本。或者,你可能没有权限修改系统的默认 Python。在这种情况下,一个解决方案是使用–-user标志安装库。这个标志将库安装到用户的家目录中,那里其他用户无法访问。要仅为本地用户安装requests:
$ python3.6 -m pip install --user requests
如我之前提到的,如果你在一个没有足够管理员权限安装软件的系统上工作,或者你想安装模块的不同版本,这种方案特别有用。如果你的需求超出了这里讨论的基本安装方法,一个好的起点是“安装 Python 模块”,你可以在 Python 文档中找到它。
19.4.2. 虚拟环境
如果你需要避免在系统 Python 中安装库,你还有一个更好的选择,这个选项被称为虚拟环境(virtualenv)。虚拟环境是一个包含 Python 安装及其附加包的独立目录结构。因为整个 Python 环境都包含在虚拟环境中,所以那里安装的库和模块不会与主系统或其他虚拟环境中的库冲突,允许不同的应用程序使用不同的 Python 及其包版本。
创建和使用虚拟环境需要两个步骤。首先,你创建环境:
$ python3.6 -m venv test-env
此步骤在名为 test-env 的目录中创建环境,其中安装了 Python 和pip。然后,当环境创建完成后,你激活它。在 Windows 上,你这样做:
> test-env\Scripts\activate.bat
在 Unix 或 MacOS 系统上,你使用 source 激活脚本:
$ source test-env/bin/activate
当你激活了环境后,你可以像之前一样使用pip管理包,但在虚拟环境中pip是一个独立的命令:
$ pip install requests
此外,无论你使用什么版本的 Python 创建环境,该环境默认的 Python 就是那个版本,因此你可以只使用 python 而不是 python3 或 python3.6。
虚拟环境对于管理项目和它们的依赖非常有用,并且已经成为一种标准做法,尤其是对于在多个项目上工作的开发者。更多信息,请参阅 Python 在线文档中 Python 教程的“虚拟环境和包”部分。
19.5. PyPI(又称“奶酪店”)
虽然使用 distutils 包可以完成任务,但有一个问题:你必须找到正确的包,这可能是一项繁琐的工作。而且在你找到包之后,有一个合理可靠的来源来下载该包会很好。
为了满足这一需求,多年来已经提供了各种 Python 包仓库。目前,Python 代码的官方(但绝非唯一)仓库是 Python 包索引,或 PyPI(以前也被称为“奶酪店”,这是蒙提·派森的喜剧片段),位于 Python 网站上。你可以从主页上的链接或直接在 pypi.python.org 访问它。PyPI 包含了超过 6,000 个针对各种 Python 版本的包,按添加日期和名称列出,但也可以按类别搜索和细分。
在撰写本文时,PyPI 的新版本正在准备中;目前,它被称为“仓库”。这个版本仍在测试中,但承诺将提供更流畅、更友好的搜索体验。
如果你无法通过搜索标准库找到所需的功能,PyPI 就是下一个合理的去处。
摘要
-
Python 拥有一个丰富的标准库,它涵盖了比许多其他语言更多的常见情况,因此在寻找外部模块之前,你应该仔细检查标准库中有什么。
-
如果你确实需要外部模块,为你的操作系统预构建的包是最容易的选择,但它们有时会过时,而且往往难以找到。
-
从源安装的标准方式是使用 pip,而创建虚拟环境以避免多个项目之间冲突的最佳方式是使用 venv 模块。
-
通常,在搜索外部模块时,逻辑上的第一步是查找 Python 包索引(PyPI)。
第四部分:处理数据
在这部分,您将获得一些使用 Python 的实践,特别是使用它来处理数据。处理数据是 Python 的优势之一。我从基本的文件处理开始;然后转向从平面文件中读取和写入,处理更结构化的格式,如 JSON 和 Excel,使用数据库,以及使用 Python 探索数据。
这些章节比本书的其他部分更偏向于项目导向,旨在为您提供使用 Python 处理数据的机会。这部分中的章节和项目可以按照您需要的任何顺序或组合进行。
第二十章。基本文件处理
本章涵盖
-
移动和重命名文件
-
压缩和加密文件
-
选择性删除文件
本章讨论了当你拥有不断增长的数据文件集合时可以使用的基本操作。这些文件可能是日志文件,也可能是常规数据源,但无论它们的来源如何,你都不能简单地立即丢弃它们。你如何保存它们、管理它们,并最终根据计划处置它们,但又不需要人工干预?
20.1. 问题:数据文件永无止境的流动
许多系统会生成一系列连续的数据文件。这些文件可能是电子商务服务器的日志文件或常规流程;它们可能是来自服务器的产品信息夜间更新;它们可能是在线广告项目的自动化数据源;股票交易的歷史数据;或者它们可能来自成千上万的其它来源。它们通常是平面文本文件,未压缩,包含的是输入数据或其它流程的副产品。然而,尽管它们性质平凡,但它们包含的数据具有一定的潜在价值,因此文件不能在一天结束时被丢弃——这意味着每天它们的数量都在增长。随着时间的推移,文件积累到手动处理变得不可行,以及它们消耗的存储空间变得无法接受。
20.2. 场景:来自地狱的产品数据源
我遇到的一个典型情况是产品数据的每日更新。这些数据可能来自供应商或用于在线营销,但基本方面是相同的。
考虑一个来自供应商的产品数据源示例。数据源文件每天更新一次,每行代表业务提供的每个项目。每行包含供应商的库存单位(SKU)编号;项目的简要描述;项目的成本、高度、宽度、长度和宽度;项目的状态(库存或已预订,等等);以及根据业务需求可能包含的其他几项信息。
除了这个基本信息文件外,你可能会收到其他文件,可能是相关产品的,更详细的物品属性,或者其他东西。在这种情况下,你每天都会收到几个具有相同文件名的文件,并进入相同的目录进行处理。
现在假设你每天都会收到三个相关的文件:item_info.txt、item_attributes.txt、related_items.txt。这三个文件每天都会到来并得到处理。如果处理是唯一的要求,你不必过于担心;你可以让每天的一组文件替换上一次的文件并完成。但如果你不能丢弃数据呢?你可能想保留原始数据,以防对处理过程的准确性有疑问,你需要参考过去的文件。或者你可能想跟踪数据随时间的变化。无论是什么原因,保留文件的需求意味着你需要进行一些处理。
你可能做的最简单的事情是标记文件接收的日期,并将它们移动到存档文件夹。这样,每批新文件都可以接收、处理、重命名并移开,以便可以无损失地重复该过程。
经过几次重复后,目录结构可能看起来像这样:
working/ *1*
item_info.txt
item_attributes.txt
related_items.txt
archive/ *2*
item_info_2017-09-15.txt
item_attributes_2017-09-15.txt
related_items_2017-09-15.txt
item_info_2016-07-16.txt
item_attributes_2017-09-16.txt
related_items_2017-09-16.txt
item_info_2017-09-17.txt
item_attributes_2017-09-17.txt
related_items_2017-09-17.txt
...
-
1 主要工作文件夹,包含当前要处理的文件
-
2 存档已处理文件的子目录
考虑使此过程发生所需的步骤。首先,你需要重命名文件,以便将当前日期添加到文件名中。为此,你需要获取你想要重命名的文件名;然后你需要获取不带扩展名的文件名主干。当你有了主干,你需要添加一个基于当前日期的字符串,将扩展名重新添加到末尾,然后实际上更改文件名并将其移动到存档目录。
快速检查:考虑选择
你处理我已识别的任务有哪些选项?你能想到哪些标准库模块来完成这项工作?如果你想,你现在就可以停下来,编写代码来完成这项工作。然后,将你的解决方案与稍后开发的解决方案进行比较。
你可以通过几种方式获取文件名。如果你确定文件名总是完全相同,并且文件不多,你可以将它们硬编码到你的脚本中。然而,更安全的方法是使用pathlib模块和路径对象的glob方法,如下所示:
>>> import pathlib
>>> cur_path = pathlib.Path(".")
>>> FILE_PATTERN = "*.txt"
>>> path_list = cur_path.glob(FILE_PATTERN)
>>> print(list(path_list))
[PosixPath('item_attributes.txt'), PosixPath('related_items.txt'),
PosixPath('item_info.txt')]
现在,你可以遍历与你的FILE_PATTERN匹配的路径,并应用所需更改。请记住,你需要将日期作为每个文件名称的一部分添加,同时将重命名的文件移动到存档目录。当你使用pathlib时,整个操作可能看起来像这样。
列表 20.1. 文件 files_01.py
import datetime
import pathlib
FILE_PATTERN = "*.txt" *1*
ARCHIVE = "archive" *2*
if __name__ == '__main__':
date_string = datetime.date.today().strftime("%Y-%m-%d") *3*
cur_path = pathlib.Path(".")
paths = cur_path.glob(FILE_PATTERN)
for path in paths:
new_filename = "{}_{}{}".format(path.stem, date_string, path.suffix)
new_path = cur_path.joinpath(ARCHIVE, new_filename) *4*
path.rename(new_path) *5*
-
1 设置匹配文件和存档目录的模式
-
2 必须存在一个名为“archive”的目录,以便此代码运行。
-
3 使用 datetime 库中的日期对象根据今天的日期创建日期字符串
-
4 从当前路径、存档目录和新文件名创建新的路径
-
5 一步重命名(并移动)文件
值得注意的是,Path对象使此操作更简单,因为不需要进行特殊解析来分离文件名的主干和后缀。此操作也比你可能预期的要简单,因为rename方法实际上可以通过包含新位置的路径来移动文件。
此脚本非常简单,用很少的代码行有效地完成了工作。在下一节中,你将考虑如何处理更复杂的要求。
快速检查:潜在问题
由于前面的解决方案非常简单,因此可能存在许多它处理不好的情况。示例脚本可能会出现哪些潜在问题或问题?你如何解决这些问题?
考虑文件使用的命名约定,它是基于年份、月份和名称的顺序。你在这个约定中看到什么优点?可能有什么缺点?你能否为将日期字符串放在文件名中的其他位置(如开头或结尾)提出任何论点?
20.3. 更多组织
前一节中描述的存储文件的解决方案是可行的,但它确实存在一些缺点。首先,随着文件的积累,管理它们可能会变得有些麻烦,因为在一年内,你会在同一个目录下有 365 组相关的文件,你只能通过检查它们的名称来找到相关的文件。当然,如果文件到达得更频繁,或者一组中相关的文件更多,麻烦会更大。
为了减轻这个问题,你可以改变归档文件的方式。而不是将文件名改为包含它们接收的日期,你可以为每组文件创建一个单独的子目录,并以接收的日期命名该子目录。你的目录结构可能看起来像这样:
working/ *1*
item_info.txt
item_attributes.txt
related_items.txt
archive/ *2*
2016-09-15/ *3*
item_info.txt
item_attributes.txt
related_items.txt
2016-09-16/ *3*
item_info.txt
item_attributes.txt
related_items.txt
2016-09-17/ *3*
item_info.txt
item_attributes.txt
related_items.txt
-
1 主要工作文件夹,包含当前待处理的文件
-
2 存档已处理文件的子目录
-
3 每组文件的子目录,以接收日期命名
这个方案的优势在于每组文件都聚集在一起。无论你得到多少组文件,或者一组中有多少文件,都很容易找到特定组的所有文件。
尝试这个:多目录实现
你会如何修改你开发的代码,以便将每组文件存档到以接收日期命名的子目录中?请随意花时间实现代码并测试它。
结果表明,通过子目录归档文件并不比第一个解决方案多费多少功夫。唯一的额外步骤是在重命名文件之前创建子目录。这个脚本是执行这一步骤的一种方法。
列表 20.2. 文件 files_02.py
import datetime
import pathlib
FILE_PATTERN = "*.txt"
ARCHIVE = "archive"
if __name__ == '__main__':
date_string = datetime.date.today().strftime("%Y-%m-%d")
cur_path = pathlib.Path(".")
new_path = cur_path.joinpath(ARCHIVE, date_string)
new_path.mkdir() *1*
paths = cur_path.glob(FILE_PATTERN)
for path in paths:
path.rename(new_path.joinpath(path.name))
- 1 注意,这个目录只需要在将文件移动到其中之前创建一次。
这个解决方案将相关的文件分组,这使得以集合的形式管理它们变得相对容易。
快速检查:替代方案
你如何创建一个不使用 pathlib 的脚本来完成相同的事情?你会使用哪些库和函数?
20.4. 保存存储空间:压缩和整理
到目前为止,你主要关注的是管理接收到的文件组。然而,随着时间的推移,数据文件会积累,直到它们所需的存储空间成为一个问题。当这种情况发生时,你有几个选择。一个选项是获取更大的磁盘。特别是如果你在基于云的平台,采用这种策略可能既容易又经济。但请记住,增加存储空间并不能真正解决问题;它只是推迟了解决问题。
20.4.1. 压缩文件
如果文件占用的空间是一个问题,你可能考虑的下一个方法是将它们压缩。你有多种压缩文件或文件集的方法,但通常这些方法相似。在本节中,你考虑将每天的数据文件存档到一个单独的 zip 文件中。如果文件主要是文本文件并且相当大,压缩节省的存储空间可能会非常显著。
对于这个脚本,你使用与每个 zip 文件相同的日期字符串,并带有.zip扩展名作为文件名。在列表 20.2 中,你在存档目录中创建了一个新目录,然后将文件移动到其中,从而形成了如下所示的目录结构:
working/ *1*
archive/
2016-09-15.zip *2*
2016-09-16.zip *2*
2016-09-17.zip *2*
-
1 主要工作文件夹,其中处理当前文件;这些文件在处理后被存档并删除。
-
2 Zip 文件,每个文件包含当天的 item_info.txt、attribute_info.text 和 related_items.txt
显然,为了使用 zip 文件,你需要更改之前使用的一些步骤。
尝试这个:存档到 zip 文件的伪代码
编写一个存储数据文件到 zip 文件中的解决方案的伪代码。你打算使用哪些模块、函数或方法?尝试编写你的解决方案以确保它能够工作。
新脚本中的一个关键添加是导入 zipfile 库,以及创建存档目录中的新 zip 文件对象的代码。之后,你可以使用 zip 文件对象将数据文件写入新 zip 文件。最后,因为你不再实际移动文件,你需要从工作目录中删除原始文件。一个解决方案如下。
列表 20.3. 文件 files_03.py
import datetime
import pathlib
import zipfile *1*
FILE_PATTERN = "*.txt"
ARCHIVE = "archive"
if __name__ == '__main__':
date_string = datetime.date.today().strftime("%Y-%m-%d")
cur_path = pathlib.Path(".")
paths = cur_path.glob(FILE_PATTERN)
zip_file_path = cur_path.joinpath(ARCHIVE, date_string + ".zip") *2*
zip_file = zipfile.ZipFile(str(zip_file_path), "w") *3*
for path in paths:
zip_file.write(str(path)) *4*
path.unlink() *5*
-
1 导入 zipfile 库
-
2 在存档目录中创建 zip 文件的路径
-
3 打开新的 zip 文件对象以进行写入;需要 str()将 Path 转换为字符串。
-
4 将当前文件写入 zip 文件
-
5 从工作目录中删除当前文件
20.4.2. 文件整理
将数据文件压缩到 zip 存档中可以节省大量的空间,可能就是你所需要的。然而,如果你有很多文件,或者文件压缩效果不佳(例如 JPEG 图像文件),你可能仍然发现自己存储空间不足。你也可能发现数据变化不大,这使得在较长时间内保留每个数据集的存档副本变得不必要。也就是说,尽管保留过去一周或一个月的每天数据可能有用,但保留更长时间的数据集可能不值得存储。对于几个月前的数据,可能每周或每月保留一组文件就足够了。
在文件达到一定年龄后删除文件的过程有时被称为整理。假设你每天接收一组数据文件并将其存档到 zip 文件中已经几个月了,你被告知你应该只保留超过一个月的文件中的一周文件。
最简单的整理脚本会删除你不再需要的任何文件——在这种情况下,对于一个月以上的任何东西,除了一个文件外。在设计此脚本时,了解以下两个问题的答案很有帮助:
-
由于你需要每周保存一个文件,是否简单地选择你想要保存的星期几会更容易?
-
你应该多久进行一次整理:每天、每周还是每月一次?如果你决定整理应该每天进行,那么将整理与存档脚本结合起来可能是有意义的。另一方面,如果你只需要每周或每月整理一次,这两个操作应该放在单独的脚本中。
对于这个例子,为了保持清晰,你编写了一个独立的整理脚本,可以在任何间隔运行,并删除所有不需要的文件。此外,假设你已经决定只保留超过一个月的周二接收的文件。以下是一个示例整理脚本。
列表 20.4. 文件 files_04.py
from datetime import datetime, timedelta
import pathlib
import zipfile
FILE_PATTERN = "*.zip"
ARCHIVE = "archive"
ARCHIVE_WEEKDAY = 1
if __name__ == '__main__':
cur_path = pathlib.Path(".")
zip_file_path = cur_path.joinpath(ARCHIVE)
paths = zip_file_path.glob(FILE_PATTERN)
current_date = datetime.today() *1*
for path in paths:
name = path.stem *2*
path_date = datetime.strptime(name, "%Y-%m-%d") *3*
path_timedelta = current_date - path_date *4*
if path_timedelta > timedelta(days=30) and path_date.weekday() !=
ARCHIVE_WEEKDAY: *5*
path.unlink()
-
1 获取当前日期的
datetime对象 -
2
path.stem返回不带任何扩展名的文件名。 -
3
strptime根据格式字符串将字符串解析为datetime对象。 -
4 从一个日期减去另一个日期会产生一个
timedelta对象。 -
5
timedelta(days=30)创建一个 30 天的timedelta对象;weekday()方法返回一周中某天的整数,其中星期一 = 0。
代码展示了如何通过几行代码将 Python 的 datetime 和 pathlib 库结合起来按日期整理文件。因为你的存档文件名是从它们接收的日期派生出来的,你可以使用 glob 方法获取这些文件路径,提取文件名,并使用 strptime 将其解析为 datetime 对象。从那里,你可以使用 datetime 的 timedelta 对象和 weekday() 方法来找到文件的年龄和星期几,然后删除(解除链接)你不需要的文件。
快速检查:考虑不同的参数
花些时间考虑不同的整理选项。你将如何修改代码列表 20.4 中的代码,以保留每月只保留一个文件?你将如何修改它,以便将上个月及更早的文件整理,每周保留一个?(注意:这不是指超过 30 天的情况!)
摘要
-
pathlib模块可以极大地简化文件操作,如查找根目录和扩展名、移动和重命名、匹配通配符。 -
随着文件数量和复杂性的增加,自动存档解决方案变得至关重要,Python 提供了多种简单的方法来创建它们。
-
通过压缩和整理数据文件,你可以显著节省存储空间。
第二十一章:处理数据文件
本章涵盖
-
使用 ETL(提取-转换-加载)
-
读取文本数据文件(纯文本和 CSV)
-
读取电子表格文件
-
标准化、清洗和排序数据
-
编写数据文件
大部分可用的数据都包含在文本文件中。这些数据可以从无结构的文本,如推文或文学文本的语料库,到更结构化的数据,其中每一行是一个记录,字段由特殊字符分隔,如逗号、制表符或管道(|)。文本文件可能非常大;一个数据集可能分布在十个甚至数百个文件中,其中的数据可能不完整或非常脏。在所有这些变化中,几乎不可避免的是,你需要读取和使用文本文件中的数据。本章为你提供了使用 Python 实现这一点的策略。
21.1. 欢迎来到 ETL
从文件中获取数据、解析它、将其转换为有用的格式,然后对其进行操作的需求,与数据文件存在的时间一样长。事实上,这个过程有一个标准的术语:提取-转换-加载(ETL)。提取指的是读取数据源并解析它,如果需要的话。转换可以是清洗和标准化数据,以及合并、拆分或重新组织它所包含的记录。加载指的是将转换后的数据存储在新的位置,无论是不同的文件还是数据库。本章将介绍 Python 中 ETL 的基础知识,从基于文本的数据文件开始,并将转换后的数据存储在其他文件中。我将在第二十二章(kindle_split_035.html#ch22)中查看更结构化的数据文件,在第二十三章(kindle_split_036.html#ch23)中查看数据库存储。
21.2. 读取文本文件
ETL 的第一部分——“提取”部分——涉及打开一个文件并读取其内容。这个过程看似简单,但即使在这个阶段也可能出现问题,例如文件的大小。如果一个文件太大,无法放入内存并操作,你需要对你的代码进行结构化以处理文件的小部分,可能是一行一行地操作。
21.2.1. 文本编码:ASCII、Unicode 及其他
另一个可能的陷阱是在编码上。本章涉及文本文件,实际上,在现实世界中交换的大部分数据都在文本文件中。但“文本”的确切性质可能因应用程序而异,因人而异,当然也因国家而异。
有时,文本在 ASCII 编码中意味着某种含义,ASCII 编码有 128 个字符,其中只有 95 个是可打印的。关于 ASCII 编码的好消息是,它是大多数数据交换的最低共同分母。坏消息是,它根本无法处理世界上许多字母表和书写系统的复杂性。使用 ASCII 编码读取文件几乎肯定会引起麻烦并抛出错误,无论是德语的 ü、葡萄牙语的 ç,还是除了英语之外几乎任何语言的字符值。
这些错误产生的原因是,ASCII 基于的是 7 位值,而典型文件中的字节是 8 位,因此可以有 256 种可能的值,而 7 位值只有 128 种。使用这些额外的值来存储额外的字符是常规操作——从额外的标点符号(例如打印机的 en 连字符和 em 连字符)到符号(例如商标、版权和度数符号)再到带音标的字母字符。问题始终在于,如果在读取文本文件时遇到 ASCII 范围之外的 128 个字符,你无法确定它是如何编码的。比如说,字符值 214 是除号、Ö 还是其他什么符号?除非你有创建文件的代码,否则你无法知道。
Unicode 和 UTF-8
缓解这种混淆的一种方法就是 Unicode。UTF-8 编码的 Unicode 接受基本的 ASCII 字符而不做任何改变,但同时也允许根据 Unicode 标准使用几乎无限多的其他字符和符号。由于其灵活性,UTF-8 在我写这一章的时候被用于超过 85% 的网页中,这意味着你阅读文本文件的最佳选择是假设 UTF-8 编码。如果文件只包含 ASCII 字符,它们仍然会被正确读取,但如果你遇到其他以 UTF-8 编码的字符,你也会有所准备。好消息是,Python 3 的字符串数据类型默认设计为处理 Unicode。
即使有了 Unicode,也可能会遇到你的文本包含无法成功编码的值的情况。幸运的是,Python 的 open 函数接受一个可选的 errors 参数,它告诉函数在读取或写入文件时如何处理编码错误。默认选项是 'strict',这意味着每当遇到编码错误时都会引发错误。其他有用的选项包括 'ignore',它会导致引发错误的字符被跳过;'replace',它会导致字符被替换为一个标记字符(通常是?);'backslashreplace',它将字符替换为反斜杠转义序列;以及 'surrogateescape',它在读取时将违规字符转换为私有的 Unicode 代码点,在写入时再转换回原始的字节序列。你的特定用例将决定你在处理或解决编码问题时需要多么严格。
看一个包含无效 UTF-8 字符的文件的简短示例,并看看不同的选项如何处理该字符。首先,使用字节和二进制模式写入文件:
>>> open('test.txt', 'wb').write(bytes([65, 66, 67, 255, 192,193]))
这段代码生成一个包含“ABC”后跟三个非 ASCII 字符的文件,这些字符的显示方式可能因所使用的编码而异。如果你用 vim 查看这个文件,你会看到
ABCÿÀÁ
~
现在你有了这个文件,尝试使用默认的'strict'错误选项来读取它:
>>> x = open('test.txt').read()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python3.6/codecs.py", line 321, in decode
(result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 3:
invalid start byte
第四字节,其值为 255,在那个位置不是有效的 UTF-8 字符,因此'strict'错误设置会引发异常。现在看看其他错误选项如何处理相同的文件,记住最后三个字符会引发错误:
>>> open('test.txt', errors='ignore').read()
'ABC'
>>> open('test.txt', errors='replace').read()
'ABC'
>>> open('test.txt', errors='surrogateescape').read()
'ABC\udcff\udcc0\udcc1'
>>> open('test.txt', errors='backslashreplace').read()
'ABC\\xff\\xc0\\xc1'
>>>
如果你希望任何问题字符消失,请使用'ignore'选项。'replace'选项仅标记无效字符所占的位置,而其他选项则以不同的方式尝试在不解释的情况下保留无效字符。
21.2.2. 无结构化文本
无结构化文本文件是最容易读取的数据类型,但也是从其中提取信息最困难的一种。处理无结构化文本的方式可能因文本的性质以及你想要用它做什么而大相径庭,因此,关于文本处理的全面讨论超出了本书的范围。然而,一个简短的例子可以说明一些基本问题,并为讨论结构化文本数据文件奠定基础。
其中一个最简单的问题就是决定文件中的基本逻辑单元是什么形式。如果你有一份包含数千条推文、《白鲸记》的文本或一系列新闻报道的语料库,你需要能够将它们分解成连贯的单元。在推文的情况下,每条可能只占一行,你可以相对简单地读取和处理文件的每一行。
对于《白鲸记》或甚至是一篇新闻报道,问题可能更复杂。在许多情况下,你可能不希望将整部小说或新闻报道视为一个单独的项目。但如果是这样,你需要决定你想要的单元类型,然后想出一个相应地分割文件的战略。也许你希望按段落考虑文本。在这种情况下,你需要确定你的文件中段落是如何分隔的,并相应地编写你的代码。如果一个段落与文本文件中的一行相同,那么这项工作就很简单。然而,通常文本文件中的行中断符会更短,你需要做更多的工作。
现在看看几个例子:
Call me Ishmael. Some years ago--never mind how long precisely--
having little or no money in my purse, and nothing particular
to interest me on shore, I thought I would sail about a little
and see the watery part of the world. It is a way I have
of driving off the spleen and regulating the circulation.
Whenever I find myself growing grim about the mouth;
whenever it is a damp, drizzly November in my soul; whenever I
find myself involuntarily pausing before coffin warehouses,
and bringing up the rear of every funeral I meet;
and especially whenever my hypos get such an upper hand of me,
that it requires a strong moral principle to prevent me from
deliberately stepping into the street, and methodically knocking
people's hats off--then, I account it high time to get to sea
as soon as I can. This is my substitute for pistol and ball.
With a philosophical flourish Cato throws himself upon his sword;
I quietly take to the ship. There is nothing surprising in this.
If they but knew it, almost all men in their degree, some time
or other, cherish very nearly the same feelings towards
the ocean with me.
There now is your insular city of the Manhattoes, belted round by wharves
as Indian isles by coral reefs--commerce surrounds it with her surf.
Right and left, the streets take you waterward. Its extreme downtown
is the battery, where that noble mole is washed by waves, and cooled
by breezes, which a few hours previous were out of sight of land.
Look at the crowds of water-gazers there.
在样本中,实际上这是《白鲸记》的开头,行与页上的行大致相同,段落由一个空行表示。如果你想将每个段落作为一个单元处理,你需要将文本在空行处断开。幸运的是,如果你使用字符串 split() 方法,这项任务很容易完成。字符串中的每个换行符都可以表示为 "\n"。自然地,段落文本的最后一行以换行符结束,如果下一行是空行,则紧随其后的是第二个换行符,表示空行:
>>> moby_text = open("moby_01.txt").read() *1*
>>> moby_paragraphs = moby_text.split("\n\n") *2*
>>> print(moby_paragraphs[1])
There now is your insular city of the Manhattoes, belted round by wharves
as Indian isles by coral reefs--commerce surrounds it with her surf.
Right and left, the streets take you waterward. Its extreme downtown
is the battery, where that noble mole is washed by waves, and cooled
by breezes, which a few hours previous were out of sight of land.
Look at the crowds of water-gazers there.
-
1 读取整个文件作为一个单一字符串
-
2 在两个换行符处拆分
将文本拆分为段落是处理非结构化文本的一个非常简单的第一步。在处理文本之前,你可能还需要进行更多的文本规范化。假设你想要计算文本文件中每个单词出现的频率。如果你只是根据空白字符拆分文件,你会得到文件中的单词列表。然而,准确计数将很困难,因为 This、this、this. 和 this, 并不相同。使此代码正常工作的方法是通过对文本进行规范化,在处理之前删除标点符号并使所有内容都变为同一大小写。对于上面的示例文本,规范化单词列表的代码可能看起来像这样:
>>> moby_text = open("moby_01.txt").read() *1*
>>> moby_paragraphs = moby_text.split("\n\n")
>>> moby = moby_paragraphs[1].lower() *2*
>>> moby = moby.replace(".", "") *3*
>>> moby = moby.replace(",", "") *4*
>>> moby_words = moby.split()
>>> print(moby_words)
['there', 'now', 'is', 'your', 'insular', 'city', 'of', 'the', 'manhattoes,',
'belted', 'round', 'by', 'wharves', 'as', 'indian', 'isles', 'by',
'coral', 'reefs--commerce', 'surrounds', 'it', 'with', 'her', 'surf',
'right', 'and', 'left,', 'the', 'streets', 'take', 'you', 'waterward',
'its', 'extreme', 'downtown', 'is', 'the', 'battery,', 'where', 'that',
'noble', 'mole', 'is', 'washed', 'by', 'waves,', 'and', 'cooled', 'by',
'breezes,', 'which', 'a', 'few', 'hours', 'previous', 'were', 'out',
'of', 'sight', 'of', 'land', 'look', 'at', 'the', 'crowds', 'of',
'water-gazers', 'there']
-
1 读取整个文件作为一个单一字符串
-
2 将所有内容转换为小写
-
3 删除句号
-
4 删除逗号
快速检查:规范化
仔细查看生成的单词列表。你看到规范化过程中有任何问题吗?你认为在较长的文本部分中可能会遇到哪些其他问题?你认为你将如何处理这些问题?
21.2.3. 分隔的平面文件
虽然读取非结构化文本文件很容易,但它们的缺点是结构非常缺乏。通常,在文件中有些组织结构会更有用,可以帮助挑选出单个值。最简单的方法是将文件拆分为行,每行有一个信息元素。你可能有一个要处理的文件名列表,一个需要打印的人名列表(比如在姓名标签上),或者可能是一系列来自远程监控器的温度读数。在这种情况下,数据处理非常简单:你读取行并将其转换为正确的类型(如果需要的话)。然后文件就准备好使用了。
然而,大多数时候事情并不那么简单。通常,你需要将多个相关的信息块分组,并且你需要你的代码一起读取它们。常见的做法是将相关的信息块放在同一行上,并用特殊字符分隔。这样,当你读取文件的每一行时,你可以使用特殊字符将文件拆分为不同的字段,并将这些字段的值放入变量中以便后续处理。
此文件是分隔格式温度数据的简单示例:
State|Month Day, Year Code|Avg Daily Max Air Temperature (F)|Record Count for
Daily Max Air Temp (F)
Illinois|1979/01/01|17.48|994
Illinois|1979/01/02|4.64|994
Illinois|1979/01/03|11.05|994
Illinois|1979/01/04|9.51|994
Illinois|1979/05/15|68.42|994
Illinois|1979/05/16|70.29|994
Illinois|1979/05/17|75.34|994
Illinois|1979/05/18|79.13|994
Illinois|1979/05/19|74.94|994
这份数据是管道分隔的,这意味着行中的每个字段都由管道(|)字符分隔,在这种情况下提供了四个字段:观测的状态、观测的日期、平均最高温度以及报告的站点数量。其他常见的分隔符是制表符和逗号。逗号可能是最常用的,但分隔符可以是任何你不会期望在值中出现的字符。(关于这个问题的更多内容将在下一节中介绍。)逗号分隔符如此常见,以至于这种格式通常被称为 CSV(逗号分隔值),这种类型的文件通常具有.csv 扩展名,作为其格式的提示。
无论使用什么字符作为分隔符,如果你知道它是哪个字符,你可以在 Python 中编写自己的代码来将每一行分割成字段并返回它们作为一个列表。在前一个例子中,你可以使用字符串的split()方法将一行分割成一个值列表:
>>> line = "Illinois|1979/01/01|17.48|994"
>>> print(line.split("|"))
['Illinois', '1979/01/01', '17.48', '994']
注意,这种技术非常容易做,但会将所有值都保留为字符串,这可能在后续处理中不太方便。
尝试这个:读取一个文件
编写代码以读取一个文本文件(假设为 temp_data_pipes_00a.txt,如示例所示),将文件的每一行分割成值列表,并将该列表添加到单个记录列表中。
在实现此代码时,你遇到了哪些问题或困难?你如何将最后三个字段转换为正确的日期、实数和整数类型?
21.2.4. csv模块
如果你需要处理大量的分隔数据文件,你应该熟悉csv模块及其选项。当我被问到我最喜欢的 Python 标准库模块时,不止一次提到过csv模块——并不是因为它很炫酷(它并不炫酷),而是因为它可能为我节省了更多的工作,并让我在职业生涯中避免了更多的自我造成的错误,比其他任何模块都要多。
csv模块是 Python“内置电池”哲学的一个完美案例。虽然自己编写代码来读取分隔文件是完全可能的,而且在许多情况下甚至并不困难,但使用 Python 模块会更简单、更可靠。csv模块已经过测试和优化,它具有如果你自己编写可能不会费心去写的功能,但在可用时确实很方便且节省时间。
查看前面的数据,并决定如何使用csv模块来读取它。解析数据的代码需要做两件事:读取每一行并移除尾随的换行符,然后根据管道字符分割行,并将该值列表追加到行列表中。你的练习解决方案可能看起来像这样:
>>> results = []
>>> for line in open("temp_data_pipes_00a.txt"):
... fields = line.strip().split("|")
... results.append(fields)
...
>>> results
[['State', 'Month Day, Year Code', 'Avg Daily Max Air Temperature (F)',
'Record Count for Daily Max Air Temp (F)'], ['Illinois', '1979/01/01',
'17.48', '994'], ['Illinois', '1979/01/02', '4.64', '994'], ['Illinois',
'1979/01/03', '11.05', '994'], ['Illinois', '1979/01/04', '9.51',
'994'], ['Illinois', '1979/05/15', '68.42', '994'], ['Illinois', '1979/
05/16', '70.29', '994'], ['Illinois', '1979/05/17', '75.34', '994'],
['Illinois', '1979/05/18', '79.13', '994'], ['Illinois', '1979/05/19',
'74.94', '994']]
要使用csv模块做同样的事情,代码可能如下所示:
>>> import csv
>>> results = [fields for fields in
csv.reader(open("temp_data_pipes_00a.txt", newline=''), delimiter="|")]
>>> results
[['State', 'Month Day, Year Code', 'Avg Daily Max Air Temperature (F)',
'Record Count for Daily Max Air Temp (F)'], ['Illinois', '1979/01/01',
'17.48', '994'], ['Illinois', '1979/01/02', '4.64', '994'], ['Illinois',
'1979/01/03', '11.05', '994'], ['Illinois', '1979/01/04', '9.51',
'994'], ['Illinois', '1979/05/15', '68.42', '994'], ['Illinois', '1979/
05/16', '70.29', '994'], ['Illinois', '1979/05/17', '75.34', '994'],
['Illinois', '1979/05/18', '79.13', '994'], ['Illinois', '1979/05/19',
'74.94', '994']]
在这个简单的情况下,自己编写代码的收益似乎并不那么大。尽管如此,代码行数减少了,而且更清晰,而且你不需要担心移除换行符。真正的优势在于当你想要处理更具挑战性的情况时。
示例中的数据是真实的,但实际上已经被简化和清理。原始数据更复杂。原始数据有更多字段,一些字段在引号内,而另一些则不是,第一个字段是空的。原始数据是制表符分隔的,但为了说明,我在这里将其表示为逗号分隔的:
"Notes","State","State Code","Month Day, Year","Month Day, Year Code",Avg
Daily Max Air Temperature (F),Record Count for Daily Max Air Temp
(F),Min Temp for Daily Max Air Temp (F),Max Temp for Daily Max Air Temp
(F),Avg Daily Max Heat Index (F),Record Count for Daily Max Heat Index
(F),Min for Daily Max Heat Index (F),Max for Daily Max Heat Index
(F),Daily Max Heat Index (F) % Coverage
,"Illinois","17","Jan 01, 1979","1979/01/
01",17.48,994,6.00,30.50,Missing,0,Missing,Missing,0.00%
,"Illinois","17","Jan 02, 1979","1979/01/02",4.64,994,-
6.40,15.80,Missing,0,Missing,Missing,0.00%
,"Illinois","17","Jan 03, 1979","1979/01/03",11.05,994,-
0.70,24.70,Missing,0,Missing,Missing,0.00%
,"Illinois","17","Jan 04, 1979","1979/01/
04",9.51,994,0.20,27.60,Missing,0,Missing,Missing,0.00%
,"Illinois","17","May 15, 1979","1979/05/
15",68.42,994,61.00,75.10,Missing,0,Missing,Missing,0.00%
,"Illinois","17","May 16, 1979","1979/05/
16",70.29,994,63.40,73.50,Missing,0,Missing,Missing,0.00%
,"Illinois","17","May 17, 1979","1979/05/
17",75.34,994,64.00,80.50,82.60,2,82.40,82.80,0.20%
,"Illinois","17","May 18, 1979","1979/05/
18",79.13,994,75.50,82.10,81.42,349,80.20,83.40,35.11%
,"Illinois","17","May 19, 1979","1979/05/
19",74.94,994,66.90,83.10,82.87,78,81.60,85.20,7.85%
注意,一些字段包含逗号。在这种情况下,惯例是在字段周围放置引号,以表示它不应该被解析为分隔符。像这里这样只引用一些字段是很常见的,特别是那些可能包含分隔符字符的值。同样,像这里一样,即使字段不太可能包含分隔符字符,一些字段也可能被引用。
在这种情况下,你自编的代码会变得很繁琐。现在你不能再在分隔符上分割行;你需要确保你只查看不在引号字符串内部的分隔符。此外,你需要移除可能出现在任何位置或根本不出现的引号字符串。使用csv模块,你根本不需要更改你的代码。事实上,因为逗号是默认分隔符,你甚至不需要指定它:
>>> results2 = [fields for fields in csv.reader(open("temp_data_01.csv",
newline=''))]
>>> results2
[['Notes', 'State', 'State Code', 'Month Day, Year', 'Month Day, Year Code',
'Avg Daily Max Air Temperature (F)', 'Record Count for Daily Max Air
Temp (F)', 'Min Temp for Daily Max Air Temp (F)', 'Max Temp for Daily
Max Air Temp (F)', 'Avg Daily Min Air Temperature (F)', 'Record Count
for Daily Min Air Temp (F)', 'Min Temp for Daily Min Air Temp (F)', 'Max
Temp for Daily Min Air Temp (F)', 'Avg Daily Max Heat Index (F)',
'Record Count for Daily Max Heat Index (F)', 'Min for Daily Max Heat
Index (F)', 'Max for Daily Max Heat Index (F)', 'Daily Max Heat Index
(F) % Coverage'], ['', 'Illinois', '17', 'Jan 01, 1979', '1979/01/01',
'17.48', '994', '6.00', '30.50', '2.89', '994', '-13.60', '15.80',
'Missing', '0', 'Missing', 'Missing', '0.00%'], ['', 'Illinois', '17',
'Jan 02, 1979', '1979/01/02', '4.64', '994', '-6.40', '15.80', '-9.03',
'994', '-23.60', '6.60', 'Missing', '0', 'Missing', 'Missing', '0.00%'],
['', 'Illinois', '17', 'Jan 03, 1979', '1979/01/03', '11.05', '994', '-
0.70', '24.70', '-2.17', '994', '-18.30', '12.90', 'Missing', '0',
'Missing', 'Missing', '0.00%'], ['', 'Illinois', '17', 'Jan 04, 1979',
'1979/01/04', '9.51', '994', '0.20', '27.60', '-0.43', '994', '-16.30',
'16.30', 'Missing', '0', 'Missing', 'Missing', '0.00%'], ['',
'Illinois', '17', 'May 15, 1979', '1979/05/15', '68.42', '994', '61.00',
'75.10', '51.30', '994', '43.30', '57.00', 'Missing', '0', 'Missing',
'Missing', '0.00%'], ['', 'Illinois', '17', 'May 16, 1979', '1979/05/
16', '70.29', '994', '63.40', '73.50', '48.09', '994', '41.10', '53.00',
'Missing', '0', 'Missing', 'Missing', '0.00%'], ['', 'Illinois', '17',
'May 17, 1979', '1979/05/17', '75.34', '994', '64.00', '80.50', '50.84',
'994', '44.30', '55.70', '82.60', '2', '82.40', '82.80', '0.20%'], ['',
'Illinois', '17', 'May 18, 1979', '1979/05/18', '79.13', '994', '75.50',
'82.10', '55.68', '994', '50.00', '61.10', '81.42', '349', '80.20',
'83.40', '35.11%'], ['', 'Illinois', '17', 'May 19, 1979', '1979/05/19',
'74.94', '994', '66.90', '83.10', '58.59', '994', '50.90', '63.20',
'82.87', '78', '81.60', '85.20', '7.85%']]
注意,多余的引号已经被移除,并且任何包含逗号的字段值在字段内部保留了逗号——所有这些都不需要命令中更多的字符。
快速检查:处理引号
考虑一下,如果你没有csv库,你会如何处理引号字段和嵌入的分隔符字符的问题。处理引号还是嵌入的分隔符更容易?
21.2.5. 将 csv 文件作为字典列表读取
在前面的示例中,你得到了一个字段列表作为数据行。这种结果在许多情况下都很好用,但有时将行作为字典返回,其中字段名是键,可能更方便。为此用例,csv库有一个DictReader,它可以接受一个字段列表作为参数,也可以从数据的第一行读取它们。如果你想用DictReader打开数据,代码看起来会是这样:
>>> results = [fields for fields in csv.DictReader(open("temp_data_01.csv",
newline=''))]
>>> results[0]
OrderedDict([('Notes', ''), ('State', 'Illinois'), ('State Code', '17'),
('Month Day, Year', 'Jan 01, 1979'), ('Month Day, Year Code', '1979/01/
01'), ('Avg Daily Max Air Temperature (F)', '17.48'), ('Record Count for
Daily Max Air Temp (F)', '994'), ('Min Temp for Daily Max Air Temp (F)',
'6.00'), ('Max Temp for Daily Max Air Temp (F)', '30.50'), ('Avg Daily
Min Air Temperature (F)', '2.89'), ('Record Count for Daily Min Air Temp
(F)', '994'), ('Min Temp for Daily Min Air Temp (F)', '-13.60'), ('Max
Temp for Daily Min Air Temp (F)', '15.80'), ('Avg Daily Max Heat Index
(F)', 'Missing'), ('Record Count for Daily Max Heat Index (F)', '0'),
('Min for Daily Max Heat Index (F)', 'Missing'), ('Max for Daily Max
Heat Index (F)', 'Missing'), ('Daily Max Heat Index (F) % Coverage',
'0.00%')])
注意,csv.DictReader返回OrderedDicts,因此字段保持其原始顺序。尽管它们的表示略有不同,但字段仍然像字典一样表现:
>>> results[0]['State']
'Illinois'
如果数据特别复杂,并且需要操作特定字段,使用 DictReader 可以使确保获取正确字段变得容易得多;它也使你的代码更容易理解。相反,如果你的数据集相当大,你需要记住 DictReader 读取相同数量的数据可能需要的时间是两倍。
21.3. Excel 文件
本章中我讨论的另一种常见文件格式是 Excel 文件,这是 Microsoft Excel 用于存储电子表格的格式。我包括 Excel 文件在这里,因为处理它们的方式与处理分隔文件的方式非常相似。事实上,因为 Excel 可以读取和写入 CSV 文件,所以从 Excel 电子表格文件中提取数据的最快和最简单的方法通常是将其在 Excel 中打开,然后保存为 CSV 文件。然而,这种方法并不总是合理,尤其是如果你有很多文件。在这种情况下,即使你可以从理论上自动化打开和保存每个文件为 CSV 格式的过程,直接处理 Excel 文件可能更快。
在本书中深入讨论电子表格文件,包括同一文件中的多个工作表、宏和各种格式选项超出了范围。相反,在本节中,我查看了一个读取简单单工作表文件的示例,仅为了从中提取数据。
事实上,Python 的标准库没有用于读取或写入 Excel 文件的模块。要读取该格式,你需要安装外部模块。幸运的是,有几个模块可以完成这项工作。在这个例子中,你使用一个名为 OpenPyXL 的模块,它可以从 Python 软件包仓库中获取。你可以从命令行使用以下命令安装它:
$pip install openpyxl
这里是之前数据的视图,但以电子表格的形式:

读取文件相对简单,但仍然比 CSV 文件需要更多的工作。首先,你需要加载工作簿;然后,你需要获取特定的工作表;然后你可以遍历行;然后从那里提取单元格的值。读取电子表格的一些示例代码如下:
>>> from openpyxl import load_workbook
>>> wb = load_workbook('temp_data_01.xlsx')
>>> results = []
>>> ws = wb.worksheets[0]
>>> for row in ws.iter_rows():
... results.append([cell.value for cell in row])
...
>>> print(results)
[['Notes', 'State', 'State Code', 'Month Day, Year', 'Month Day, Year Code',
'Avg Daily Max Air Temperature (F)', 'Record Count for Daily Max Air
Temp (F)', 'Min Temp for Daily Max Air Temp (F)', 'Max Temp for Daily
Max Air Temp (F)', 'Avg Daily Max Heat Index (F)', 'Record Count for
Daily Max Heat Index (F)', 'Min for Daily Max Heat Index (F)', 'Max for
Daily Max Heat Index (F)', 'Daily Max Heat Index (F) % Coverage'],
[None, 'Illinois', 17, 'Jan 01, 1979', '1979/01/01', 17.48, 994, 6,
30.5, 'Missing', 0, 'Missing', 'Missing', '0.00%'], [None, 'Illinois',
17, 'Jan 02, 1979', '1979/01/02', 4.64, 994, -6.4, 15.8, 'Missing', 0,
'Missing', 'Missing', '0.00%'], [None, 'Illinois', 17, 'Jan 03, 1979',
'1979/01/03', 11.05, 994, -0.7, 24.7, 'Missing', 0, 'Missing',
'Missing', '0.00%'], [None, 'Illinois', 17, 'Jan 04, 1979', '1979/01/
04', 9.51, 994, 0.2, 27.6, 'Missing', 0, 'Missing', 'Missing', '0.00%'],
[None, 'Illinois', 17, 'May 15, 1979', '1979/05/15', 68.42, 994, 61,
75.1, 'Missing', 0, 'Missing', 'Missing', '0.00%'], [None, 'Illinois',
17, 'May 16, 1979', '1979/05/16', 70.29, 994, 63.4, 73.5, 'Missing', 0,
'Missing', 'Missing', '0.00%'], [None, 'Illinois', 17, 'May 17, 1979',
'1979/05/17', 75.34, 994, 64, 80.5, 82.6, 2, 82.4, 82.8, '0.20%'],
[None, 'Illinois', 17, 'May 18, 1979', '1979/05/18', 79.13, 994, 75.5,
82.1, 81.42, 349, 80.2, 83.4, '35.11%'], [None, 'Illinois', 17, 'May 19,
1979', '1979/05/19', 74.94, 994, 66.9, 83.1, 82.87, 78, 81.6, 85.2,
'7.85%']]
这段代码得到的结果与用于 csv 文件的简单代码相同。代码读取电子表格更复杂并不奇怪,因为电子表格本身就是一个更复杂的对象。你还应该确保你理解数据在电子表格中的存储方式。如果电子表格包含具有某些重要性的格式,如果需要忽略或以不同方式处理标签,或者如果需要处理公式和引用,你需要深入了解这些元素应该如何处理,并且你需要编写更复杂的代码。
电子表格也可能存在其他问题。在撰写本文时,电子表格通常限制在大约一百万行。虽然这个限制听起来很大,但越来越经常需要处理更大的数据集。此外,电子表格有时会自动应用不便利的格式。我之前工作的一家公司,其零件号由一个数字和至少一个字母组成,后面跟着一些数字和字母的组合。例如,可以得到一个零件号如 1E20。大多数电子表格会自动将 1E20 解释为科学记数法,并保存为 1.00E+20(1 乘以 10 的 20 次方),而将 1F20 保留为字符串。由于某种原因,很难防止这种情况发生,尤其是在大型数据集中,问题可能直到管道的下游才会被发现。因此,我建议在可能的情况下使用 CSV 或定界文件。用户通常可以将电子表格保存为 CSV 格式,因此通常没有必要忍受电子表格带来的额外复杂性和格式化问题。
21.4. 数据清洗
在处理基于文本的数据文件时,你可能会遇到的一个常见问题是数据不干净。我所说的“不干净”是指数据中存在各种意外情况,例如空值、不适用于你的编码的值或额外的空白字符。数据可能未排序或顺序难以处理。处理这些情况的过程称为“数据清洗”。
21.4.1. 清洗
在一个非常简单的数据清洗示例中,你可能需要处理从电子表格或其他财务程序导出的文件,处理金钱相关的列可能包含百分比和货币符号(如%,$,£和?),以及使用句点或逗号进行额外分组的组合。来自其他来源的数据可能包含其他意外情况,如果事先没有发现,处理起来会很棘手。再次看看你之前看到的温度数据。第一行数据看起来像这样:
[None, 'Illinois', 17, 'Jan 01, 1979', '1979/01/01', 17.48, 994, 6, 30.5,
2.89, 994, -13.6, 15.8, 'Missing', 0, 'Missing', 'Missing', '0.00%']
一些列,例如'State'(字段 2)和'Notes'(字段 1),显然是文本类型,你不太可能对它们做太多操作。此外,还有两个日期字段,格式不同,你可能很希望对这些日期进行计算,比如改变数据的顺序,按月或日分组,或者计算两行之间的时间差。
其余字段似乎都是不同类型的数字;温度是十进制数,而记录计数列是整数。然而,请注意,热指数温度有变化:当“每日最高空气温度(F)最大值”字段的值低于 80 时,热指数字段的值不报告,而是列示为“缺失”,记录计数为 0。此外,请注意,“每日最大热指数(F)覆盖率”字段表示为具有热指数的温度记录数量的百分比。如果你想在那些字段中的值上进行任何数学计算,这两个问题都会变得有挑战性,因为“缺失”和任何以%结尾的数字都将被解析为字符串,而不是数字。
在处理过程的各个步骤中都可以进行此类数据清理。通常,我更喜欢在从文件读取数据时清理数据,所以我可能会在处理行时将“缺失”替换为 None 值或空字符串。你也可以保留“缺失”字符串,并编写代码,以便在值是“缺失”的情况下不执行任何数学运算。
尝试这个:清理数据
你会如何处理具有“缺失”作为可能值的字段进行数学计算?你能编写一个代码片段来计算这些列的平均值吗?
你会如何处理末尾的平均列,以便也能报告平均覆盖率?在你看来,这个问题的解决方案是否与处理“缺失”条目的方式有关?
21.4.2. 排序
如我之前提到的,在处理之前对数据进行排序通常很有用。排序数据可以更容易地发现和处理重复值,并且还可以帮助将相关行聚集在一起,以便更快或更容易地处理。在一个案例中,我收到了一个包含 2000 万行属性和值的文件,其中任意数量的它们需要与主 SKU 列表中的项目匹配。按项目 ID 对行进行排序可以使收集每个项目的属性变得更快。你如何进行排序取决于数据文件相对于可用内存的大小以及排序的复杂性。如果文件的所有行都可以舒适地放入可用内存中,最简单的事情可能是将所有行读取到一个列表中,并使用列表的排序方法:
>>> lines = open("datafile").readlines()
>>> lines.sort()
你也可以使用sorted()函数,例如sorted_lines = sorted(lines)。这个函数保留了原始列表中行的顺序,这通常是不必要的。使用sorted()函数的缺点是它会创建列表的新副本。这个过程会稍微慢一些,并且消耗两倍的内存,这可能会成为一个更大的问题。
如果数据集大于内存,并且排序非常简单(仅按易于获取的字段排序),则可能更容易使用外部实用程序,例如 UNIX sort命令,来预处理数据:
$ sort data > data.srt
在任何情况下,排序都可以按逆序进行,并且可以按值进行排序,而不是按行的开头。在这种情况下,你需要研究你选择的排序工具的文档。Python 中的一个简单例子是使文本行的排序不区分大小写。为此,你给sort方法提供一个键函数,在比较之前将元素转换为小写:
>>> lines.sort(key=str.lower)
这个例子使用了一个lambda函数来忽略每个字符串的前五个字符:
>>> lines.sort(key=lambda x: x[5:])
使用键函数来确定 Python 中排序的行为非常方便,但请注意,在排序过程中会多次调用键函数,因此复杂的键函数可能会导致真正的性能下降,尤其是在大数据集上。
21.4.3. 数据清理问题和陷阱
似乎脏数据的类型和数据的来源以及使用案例一样多。你的数据总会有些怪癖,从使处理不准确到使数据甚至无法加载,什么都有可能。因此,我无法提供一个详尽的列表,列出你可能会遇到的问题以及如何处理它们,但我可以给你一些一般性的提示。
-
小心空白符和空字符。 空白字符的问题是你看不到它们,但这并不意味着它们不能引起麻烦。数据行开头和结尾的额外空白,字段周围的额外空白,以及使用制表符而不是空格(或反之亦然)都可能使你的数据加载和处理更加麻烦,这些问题并不总是容易察觉。同样,包含空字符(ASCII 0)的文本文件在检查时可能看起来没问题,但在加载和处理时可能会出错。
-
小心标点符号。 标点符号也可能成为问题。额外的逗号或句号可能会搞乱 CSV 文件和数值字段的处理,未转义或未匹配的引号字符也可能使事情变得混乱。
-
分解并调试步骤。 如果每个步骤都是独立的,那么调试问题会更容易,这意味着将每个操作放在单独的一行上,更加详细,并使用更多的变量。但这项工作值得去做。一方面,它使得抛出的任何异常都更容易理解,同时也使得调试更容易,无论是通过打印语句、日志记录还是 Python 调试器。在每一步之后保存数据,并将文件大小缩减到只有几行导致错误的程度,这也可能是有帮助的。
21.5. 编写数据文件
ETL 过程的最后部分可能涉及将转换后的数据保存到数据库中(我在第二十二章中讨论了这一点),但通常它涉及将数据写入文件。这些文件可能被用作其他应用程序和分析的输入,无论是人工还是由其他应用程序。通常,你有一个特定的文件规范,列出应包含哪些数据字段,它们应该被命名为什么,每个字段的格式和约束应该是什么,等等。
21.5.1. CSV 和其他分隔文件
可能最容易的事情就是将你的数据写入 CSV 文件。因为你已经加载、解析、清洗和转换了数据,所以你不太可能遇到与数据本身相关的未解决的问题。再次强调,使用 Python 标准库中的csv模块可以使你的工作更加容易。
使用csv模块写入分隔文件基本上是读取过程的逆过程。同样,你需要指定你想要使用的分隔符,同样,csv模块会处理任何你的分隔字符包含在字段中的情况:
>>> temperature_data = [['State', 'Month Day, Year Code', 'Avg Daily Max Air
Temperature (F)', 'Record Count for Daily Max Air Temp (F)'],
['Illinois', '1979/01/01', '17.48', '994'], ['Illinois', '1979/01/02',
'4.64', '994'], ['Illinois', '1979/01/03', '11.05', '994'], ['Illinois',
'1979/01/04', '9.51', '994'], ['Illinois', '1979/05/15', '68.42',
'994'], ['Illinois', '1979/05/16', '70.29', '994'], ['Illinois', '1979/
05/17', '75.34', '994'], ['Illinois', '1979/05/18', '79.13', '994'],
['Illinois', '1979/05/19', '74.94', '994']]
>>> csv.writer(open("temp_data_03.csv", "w",
newline='')).writerows(temperature_data)
这段代码会产生以下文件:
State,"Month Day, Year Code",Avg Daily Max Air Temperature (F),Record Count
for Daily Max Air Temp (F)
Illinois,1979/01/01,17.48,994
Illinois,1979/01/02,4.64,994
Illinois,1979/01/03,11.05,994
Illinois,1979/01/04,9.51,994
Illinois,1979/05/15,68.42,994
Illinois,1979/05/16,70.29,994
Illinois,1979/05/17,75.34,994
Illinois,1979/05/18,79.13,994
Illinois,1979/05/19,74.94,994
就像从 CSV 文件读取时一样,如果你使用DictWriter,你可以写入字典而不是列表。如果你确实使用DictWriter,请注意以下几点:在创建写入器时,你必须指定字段名称列表,并且你可以使用DictWriter的writeheader方法在文件顶部写入标题。所以假设你拥有与之前相同的数据,但以字典格式:
{'State': 'Illinois', 'Month Day, Year Code': '1979/01/01', 'Avg Daily Max
Air Temperature (F)': '17.48', 'Record Count for Daily Max Air Temp
(F)': '994'}
你可以使用csv模块的DictWriter对象将每一行,一个字典,写入 CSV 文件的正确字段:
>>> fields = ['State', 'Month Day, Year Code', 'Avg Daily Max Air Temperature
(F)', 'Record Count for Daily Max Air Temp (F)']
>>> dict_writer = csv.DictWriter(open("temp_data_04.csv", "w"),
fieldnames=fields)
>>> dict_writer.writeheader()
>>> dict_writer.writerows(data)
>>> del dict_writer
21.5.2. 写入 Excel 文件
写入电子表格文件与读取它们非常相似。你需要创建一个工作簿,或者电子表格文件;然后你需要创建一个或多个工作表;最后,你需要在适当的单元格中写入数据。你可以像这样从 CSV 数据文件创建一个新的电子表格:
>>> from openpyxl import Workbook
>>> data_rows = [fields for fields in csv.reader(open("temp_data_01.csv"))]
>>> wb = Workbook()
>>> ws = wb.active
>>> ws.title = "temperature data"
>>> for row in data_rows:
... ws.append(row)
...
>>> wb.save("temp_data_02.xlsx")
在将数据写入电子表格文件时,也可以为单元格添加格式。有关如何添加格式的更多信息,请参阅xlswriter文档。
21.5.3. 打包数据文件
如果你有几个相关的数据文件,或者如果你的文件很大,将它们打包成压缩归档可能是有意义的。尽管使用各种归档格式,但 zip 文件仍然很受欢迎,并且几乎在几乎每个平台上都可以被用户访问。有关如何创建数据文件的 zip 文件包的提示,请参阅第二十章。
实验室 21:天气观测
这里提供的天气观测文件是根据月份和伊利诺伊州从 1979 年到 2011 年的县来划分的。编写代码处理此文件,将芝加哥(库克县)的数据提取到单个 CSV 或电子表格文件中。此过程包括将'Missing'字符串替换为空字符串,并将百分比转换为小数。你也可以考虑哪些字段是重复的(因此可以省略或存储在其他地方)。当你将文件加载到电子表格中时,你会得到正确的证明。你可以下载带有书籍源代码的解决方案。
摘要
-
ETL(提取-转换-加载)是从一种格式获取数据,确保其一致性,然后将其放入你可以使用的格式的过程。ETL 是大多数数据处理的基本步骤。
-
文本文件的编码可能会出现问题,但 Python 允许你在加载文件时处理一些编码问题。
-
分隔符或 CSV 文件很常见,处理它们最好的方式是使用
csv模块。 -
电子表格文件可能比 CSV 文件更复杂,但处理方式几乎相同。
-
货币符号、标点符号和空字符是最常见的数据清理问题;请注意它们。
-
对你的数据文件进行预排序可以使其他处理步骤更快。
第二十二章. 网络上的数据
本章涵盖
-
通过 FTP/SFTP、SSH/SCP 和 HTTPS 获取文件
-
通过 API 获取数据
-
结构化数据文件格式:JSON 和 XML
-
数据抓取
你已经看到了如何处理基于文本的数据文件。在本章中,你将使用 Python 在网络中移动数据文件。在某些情况下,这些文件可能是文本或电子表格文件,如第二十一章所述,但在其他情况下,它们可能是更结构化的格式,并从 REST 或 SOAP 应用程序编程接口(API)提供服务。有时,获取数据可能意味着从网站上抓取。本章讨论了所有这些情况,并展示了常见的用例。
22.1. 获取文件
在你可以对数据文件进行任何操作之前,你必须获取它们。有时,这个过程非常简单,比如手动下载单个 zip 存档,或者文件可能已经被从其他地方推送到你的机器上。然而,很多时候,这个过程更为复杂。可能需要从远程服务器检索大量文件,或者需要定期检索文件,或者检索过程足够复杂,手动操作会变得痛苦。在任何这些情况下,你很可能希望使用 Python 自动化获取数据文件。
首先,我想明确指出,使用 Python 脚本并不是唯一的方式,也不一定是最佳的方式,来获取文件。以下边栏提供了更多关于我在决定是否使用 Python 脚本进行文件检索时考虑的因素的解释。然而,假设使用 Python 确实适合你的特定用例,本节将展示你可能采用的一些常见模式。
我是否应该使用 Python?
虽然使用 Python 获取文件可以非常有效,但这并不总是最佳选择。在做出决定时,你可能需要考虑两个因素。
-
是否有更简单的选项? 根据你的操作系统和经验,你可能会发现简单的 shell 脚本和命令行工具更简单,更容易配置。如果你没有这些工具可用,或者不习惯使用它们(或者将维护它们的人不习惯使用它们),你可能希望考虑一个 Python 脚本。
-
获取过程复杂吗?或者与处理过程紧密耦合? 虽然这些情况通常不理想,但它们可能会发生。我现在的规则是,如果 shell 脚本需要超过几行,或者我必须认真思考如何在 shell 脚本中完成某事,那么可能就是时候切换到 Python 了。
22.1.1. 使用 Python 从 FTP 服务器获取文件
文件传输协议(FTP)已经存在很长时间了,但它在安全性不是主要关注点时,仍然是一种简单易用的文件共享方式。要在 Python 中访问 FTP 服务器,你可以使用标准库中的ftplib模块。要遵循的步骤很简单:创建一个 FTP 对象,连接到服务器,然后使用用户名和密码(或者相当常见的是,使用用户名为“anonymous”和空密码)登录。
要继续处理天气数据,你可以连接到国家海洋和大气管理局(NOAA)的 FTP 服务器,如下所示:
>>> import ftplib
>>> ftp = ftplib.FTP('tgftp.nws.noaa.gov')
>>> ftp.login()
'230 Login successful.'
当你连接时,你可以使用ftp对象来列出和更改目录:
>>> ftp.cwd('data')
'250 Directory successfully changed.'
>>> ftp.nlst()
['climate', 'fnmoc', 'forecasts', 'hurricane_products', 'ls_SS_services',
'marine', 'nsd_bbsss.txt', 'nsd_cccc.txt', 'observations', 'products',
'public_statement', 'raw', 'records', 'summaries', 'tampa',
'watches_warnings', 'zonecatalog.curr', 'zonecatalog.curr.tar']
然后,你可以获取例如芝加哥奥黑尔国际机场的最新 METAR 报告:
>>> x = ftp.retrbinary('RETR observations/metar/decoded/KORD.TXT',
open('KORD.TXT', 'wb').write)
'226 Transfer complete.'
你将远程服务器上文件的路径和一种处理该文件数据的本地方法传递给ftp.retrbinary方法——在这种情况下,是使用相同名称打开的文件用于二进制写入的write方法。当你查看 KORD.TXT 时,你会看到它包含下载的数据:
CHICAGO O'HARE INTERNATIONAL, IL, United States (KORD) 41-59N 087-55W 200M
Jan 01, 2017 - 09:51 PM EST / 2017.01.02 0251 UTC
Wind: from the E (090 degrees) at 6 MPH (5 KT):0
Visibility: 10 mile(s):0
Sky conditions: mostly cloudy
Temperature: 33.1 F (0.6 C)
Windchill: 28 F (-2 C):1
Dew Point: 21.9 F (-5.6 C)
Relative Humidity: 63%
Pressure (altimeter): 30.14 in. Hg (1020 hPa)
Pressure tendency: 0.01 inches (0.2 hPa) lower than three hours ago
ob: KORD 020251Z 09005KT 10SM SCT150 BKN250 01/M06 A3014 RMK AO2 SLP214
T00061056 58002
cycle: 3
你也可以使用ftplib通过使用 FTP_TLS 而不是 FTP 来连接使用 TLS 加密的服务器:
ftp = ftplib.FTPTLS('tgftp.nws.noaa.gov')
22.1.2. 使用 SFTP 获取文件
如果数据需要更高的安全性,例如在业务数据通过网络传输的企业环境中,使用 SFTP 相当普遍。SFTP 是一个功能齐全的协议,它允许通过 Secure Shell (SSH)连接进行文件访问、传输和管理。尽管 SFTP 代表 SSH 文件传输协议,而 FTP 代表文件传输协议,但这两个协议并不相关。SFTP 不是在 SSH 上重新实现 FTP,而是一个专门为 SSH 设计的全新设计。
使用基于 SSH 的传输很有吸引力,因为 SSH 已经是访问远程服务器的既定标准,并且启用服务器上的 SFTP 支持相当简单(并且通常默认开启)。
Python 的标准库中没有 SFTP/SCP 客户端模块,但一个社区开发的库paramiko可以管理 SFTP 操作以及 SSH 连接。要使用paramiko,最简单的方法是通过pip安装它。如果本章前面提到的 NOAA 网站使用 SFTP(它实际上没有使用,所以这段代码将无法工作!),上述代码的 SFTP 等价物将是
>>> import paramiko
>>> t = paramiko.Transport((hostname, port))
>>> t.connect(username, password)
>>> sftp = paramiko.SFTPClient.from_transport(t)
值得注意的是,尽管paramiko支持在远程服务器上运行命令并接收其输出,就像直接的ssh会话一样,但它不包含scp功能。这个功能很少是你会错过的东西;如果你只想通过ssh连接移动一个或两个文件,命令行的scp实用程序通常会使工作更简单、更简单。
22.1.3. 通过 HTTP/HTTPS 检索文件
在本章中,我讨论的最后一种获取数据文件的方法是通过 HTTP 或 HTTPS 连接获取文件。这个选项可能是所有选项中最简单的;实际上,你是在从 Web 服务器获取数据,访问 Web 服务器的支持非常广泛。同样,在这种情况下,你可能不需要使用 Python。各种命令行工具通过 HTTP/HTTPS 连接检索文件,并具有你可能需要的几乎所有功能。其中最常见的是 wget 和 curl。然而,如果你有理由在 Python 代码中执行检索,这个过程并不困难。requests库是从 Python 代码访问 HTTP/HTTPS 服务器最简单、最可靠的方式。再次强调,requests可以通过pip install requests命令轻松安装。
当你安装了requests后,获取文件的过程很简单:导入requests并使用正确的 HTTP 动词(通常是 GET)连接到服务器并返回你的数据。
以下示例代码获取了自 1948 年以来希思罗机场的月度温度数据——一个通过 Web 服务器提供的文本文件。如果你想的话,可以将 URL 输入到浏览器中,加载页面,然后保存它。然而,如果页面很大或者你需要获取很多页面,使用像这样的代码会更容易一些:
>>> import requests
>>> response = requests.get("http://www.metoffice.gov.uk/pub/data/weather/uk/
climate/stationdata/heathrowdata.txt")
响应将包含相当多的信息,包括 Web 服务器返回的头部信息,如果出现问题,这些信息在调试时可能很有帮助。然而,你通常最感兴趣的响应对象部分是返回的数据。为了检索这些数据,你需要访问响应的text属性,它包含响应体作为字符串,或者content属性,它包含响应体作为字节:
>>> print(response.text)
Heathrow (London Airport)
Location 507800E 176700N, Lat 51.479 Lon -0.449, 25m amsl
Estimated data is marked with a * after the value.
Missing data (more than 2 days missing in month) is marked by ---.
Sunshine data taken from an automatic Kipp & Zonen sensor marked with a #,
otherwise sunshine data taken from a Campbell Stokes recorder.
yyyy mm tmax tmin af rain sun
degC degC days mm hours
1948 1 8.9 3.3 --- 85.0 ---
1948 2 7.9 2.2 --- 26.0 ---
1948 3 14.2 3.8 --- 14.0 ---
1948 4 15.4 5.1 --- 35.0 ---
1948 5 18.1 6.9 --- 57.0 ---
通常,你会将响应文本写入文件以供以后处理,但根据你的需求,你可能首先进行一些清理,甚至直接处理。
尝试这个:检索一个文件
如果你正在处理示例数据文件,并且想要将每一行拆分成单独的字段,你该如何做?你预期还会进行哪些其他处理?尝试编写一些代码来检索这个文件,并计算平均年降雨量,或者(更具挑战性)计算每年平均最高和最低温度。
22.2. 通过 API 获取数据
通过 API 提供数据是一种相当常见的方式,这遵循了将应用程序解耦成通过 API 通信的服务的发展趋势。API 可以通过多种方式工作,但它们通常通过标准的 HTTP/HTTPS 协议使用标准的 HTTP 动词,如 GET、POST、PUT 和 DELETE 来操作。以这种方式获取数据与第 22.1.3 节中检索文件非常相似,但数据不是静态文件。不是应用程序提供包含数据的静态文件,而是查询其他数据源,然后在请求时动态组装和提供数据。
虽然 API 的设置方式有很多种变化,但最常见的一种是 RESTful(表示状态转移)接口,它通过相同的 HTTP/HTTPS 协议运行,就像网络一样。API 可能的工作方式有无数种变化,但通常,数据是通过使用 GET 请求来获取的,这就是你的网络浏览器用来请求网页的方式。当你通过 GET 请求获取数据时,选择所需数据的参数通常会被附加到 URL 的查询字符串中。
如果你想要从好奇号火星车获取火星上的当前天气,请使用mng.bz/g6UY作为你的 URL.^([1]) ?format=json是一个查询字符串参数,指定信息以 JSON 格式返回,我在第 22.3.1 节中讨论了这一点。如果你想要获取任务中特定火星日(或 sol)的火星天气,比如第 155 个 sol,请使用 URL mng.bz/4e0r。如果你想要获取特定地球日期范围内的火星天气,例如 2012 年 10 月,请使用mng.bz/83WO。请注意,查询字符串的元素之间由和号(&)分隔。
¹
该网站(ingenology.com)过去一直很可靠,但在撰写本文时已关闭,其未来尚不确定。
当你知道要使用的 URL 时,你可以使用 requests 库从 API 获取数据,并可以选择即时处理它或将其保存到文件以供稍后处理。这样做最简单的方式就是像检索文件一样:
>>> import requests
>>> response = requests.get("http://marsweather.ingenology.com/v1/latest/
?format=json")
>>> response.text
'{"report": {"terrestrial_date": "2017-01-08", "sol": 1573, "ls": 295.0,
"min_temp": -74.0, "min_temp_fahrenheit": -101.2, "max_temp": -2.0,
"max_temp_fahrenheit": 28.4, "pressure": 872.0, "pressure_string":
"Higher", "abs_humidity": null, "wind_speed": null, "wind_direction":
"--", "atmo_opacity": "Sunny", "season": "Month 10", "sunrise": "2017-01-
08T12:29:00Z", "sunset": "2017-01-09T00:45:00Z"}}'
>>> response = requests.get("http://marsweather.ingenology.com/v1/archive/
?sol=155&format=json")
>>> response.text
'{"count": 1, "next": null, "previous": null, "results":
[{"terrestrial_date": "2013-01-18", "sol": 155, "ls": 243.7, "min_temp":
-64.45, "min_temp_fahrenheit": -84.01, "max_temp": 2.15,
"max_temp_fahrenheit": 35.87, "pressure": 9.175, "pressure_string":
"Higher", "abs_humidity": null, "wind_speed": 2.0, "wind_direction":
null, "atmo_opacity": null, "season": "Month 9", "sunrise": null,
"sunset": null}]}'
请记住,你应该在查询参数中转义空格和大多数标点符号,因为这些元素在 URL 中是不允许的,尽管许多浏览器会自动对 URL 进行转义。
作为最后的例子,假设你想要获取 2017 年 1 月 10 日中午 12 点到下午 1 点之间芝加哥的犯罪数据。API 的工作方式是,你通过查询字符串参数指定一个日期范围,即$where date=between <start datetime>和<end datetime>,其中起始和结束的日期时间以 ISO 格式引用。因此,获取那一小时芝加哥犯罪数据的 URL 将是data.cityofchicago.org/resource/6zsd-86xi.json?$where=datebetween’2015-01-10T12:00:00’and’2015-01-10T13:00:00’.
在示例中,一些字符在 URL 中不受欢迎,例如引号字符和空格。这是 requests 库实现其让用户更轻松目标的一个例子,因为它在发送 URL 之前,会妥善地对其进行引号处理。实际发送的 URL 是data.cityofchicago.org/resource/6zsd-86xi.json?$where=date%20between%20%222015-01-10T12:00:00%22%20and%20%222015-01-10T14:00:00%22’。
注意,所有单引号字符都已用%22 引号括起来,所有空格都已用%20 替换,而你甚至不需要考虑这一点。
尝试这个操作:访问 API
编写一些代码从芝加哥市的网站获取数据。查看结果中提到的字段,看看你是否可以根据日期范围结合另一个字段来选择记录。
22.3. 结构化数据格式
虽然 API 有时会提供纯文本,但数据通常以结构化文件格式从 API 提供。最常用的两种文件格式是 JSON 和 XML。这两种格式都基于纯文本,但它们以更灵活的方式组织内容,能够存储更复杂的信息。
22.3.1. JSON 数据
JSON,即 JavaScript 对象表示法,起源于 1999 年。它仅由两种结构组成:称为结构的键值对,这些结构与 Python 字典非常相似;以及称为数组的有序值列表,这些列表与 Python 列表非常相似。
键只能是双引号内的字符串,值可以是双引号内的字符串、数字、true、false、null、数组或对象。这些元素使得 JSON 成为表示大多数数据的一种轻量级方式,这种方式易于在网络中传输,并且对人类来说也相对容易阅读。JSON 如此普遍,以至于大多数语言都有将 JSON 转换为本地数据类型以及从本地数据类型转换为 JSON 的功能。在 Python 的情况下,这个功能是json模块,它从版本 2.6 开始成为标准库的一部分。该模块的原始外部维护版本作为simplejson提供,目前仍然可用。然而,在 Python 3 中,使用标准库版本更为常见。
你从火星漫游车和芝加哥 API 中检索的数据在 第 22.2 节 中是 JSON 格式。为了在网络中发送 JSON,JSON 对象需要被序列化——也就是说,转换成一系列字节。所以,尽管你从火星漫游车和芝加哥 API 检索的数据看起来像是 JSON,但实际上它只是 JSON 对象的字节字符串表示。为了将这个字节字符串转换成真正的 JSON 对象并将其转换为 Python 字典,你需要使用 JSON 的 loads() 函数。例如,如果你想获取火星天气报告,你可以像之前一样做,但这次你需要将其转换为 Python 字典:
>>> import json
>>> import requests
>>> response = requests.get("http://marsweather.ingenology.com/v1/latest/
?format=json")
>>> weather = json.loads(response.text)
>>> weather
{'report': {'terrestrial_date': '2017-01-10', 'sol': 1575, 'ls': 296.0,
'min_temp': -58.0, 'min_temp_fahrenheit': -72.4, 'max_temp': 0.0,
'max_temp_fahrenheit': None, 'pressure': 860.0, 'pressure_string':
'Higher', 'abs_humidity': None, 'wind_speed': None, 'wind_direction': '-
-', 'atmo_opacity': 'Sunny', 'season': 'Month 10', 'sunrise': '2017-01-
10T12:30:00Z', 'sunset': '2017-01-11T00:46:00Z'}}
>>> weather['report']['sol']
1575
注意,json.loads() 的调用是将 JSON 对象的字符串表示形式转换或加载成 Python 字典。此外,json.load() 函数将读取任何支持读取方法的文件对象。
如果你查看字典的表示形式,就像之前一样,可能会很难理解其含义。改进的格式化,也称为 美化打印,可以使数据结构更容易理解。使用 Python 的 prettyprint 模块查看示例字典中的内容:
>>> from pprint import pprint as pp
>>> pp(weather)
{'report': {'abs_humidity': None,
'atmo_opacity': 'Sunny',
'ls': 296.0,
'max_temp': 0.0,
'max_temp_fahrenheit': None,
'min_temp': -58.0,
'min_temp_fahrenheit': -72.4,
'pressure': 860.0,
'pressure_string': 'Higher',
'season': 'Month 10',
'sol': 1575,
'sunrise': '2017-01-10T12:30:00Z',
'sunset': '2017-01-11T00:46:00Z',
'terrestrial_date': '2017-01-10',
'wind_direction': '--',
'wind_speed': None}}
两个加载函数都可以配置以控制如何解析和解码原始 JSON 到 Python 对象,但默认的转换列在 表 22.1 中。
表 22.1. JSON 到 Python 默认解码
| JSON | Python |
|---|---|
| 对象 | dict |
| 数组 | list |
| 字符串 | str |
| 数字 (int) | int |
| 实数 (number) | float |
| true | True |
| false | False |
| null | None |
使用 requests 库获取 JSON
在本节中,你使用了 requests 库来检索 JSON 格式的数据,然后使用 json.loads() 方法将其解析为 Python 对象。这种技术效果不错,但由于 requests 库经常用于此目的,因此库提供了一个快捷方式:响应对象实际上有一个 json() 方法,它会为你完成这个转换。所以在这个例子中,你不需要这样做:
>>> weather = json.loads(response.text)
你本可以使用
>>> weather = response.json()
结果相同,但代码更简单、更易读、更符合 Python 风格。
如果你想要将 JSON 写入文件或将其序列化为字符串,load() 和 loads() 的逆操作是 dump() 和 dumps()。json.dump() 函数接受一个带有 write() 方法的文件对象作为参数,而 json.dumps() 返回一个字符串。在这两种情况下,将编码为 JSON 格式字符串的过程可以高度定制,但默认仍然基于 表 22.1。所以,如果你想将你的火星天气报告写入 JSON 文件,你可以这样做:
>>> outfile = open("mars_data_01.json", "w")
>>> json.dump(weather, outfile)
>>> outfile.close()
>>> json.dumps(weather)
'{"report": {"terrestrial_date": "2017-01-11", "sol": 1576, "ls": 296.0,
"min_temp": -72.0, "min_temp_fahrenheit": -97.6, "max_temp": -1.0,
"max_temp_fahrenheit": 30.2, "pressure": 869.0, "pressure_string":
"Higher", "abs_humidity": null, "wind_speed": null, "wind_direction": "-
-", "atmo_opacity": "Sunny", "season": "Month 10", "sunrise": "2017-01-
11T12:31:00Z", "sunset": "2017-01-12T00:46:00Z"}}'
如你所见,整个对象都被编码为单个字符串。在这里,再次可能需要以更可读的方式格式化字符串,就像你使用 pprint 模块所做的那样。为了轻松做到这一点,使用 indent 参数与 dump 或 dumps 函数一起使用:
>>> print(json.dumps(weather, indent=2))
{
"report": {
"terrestrial_date": "2017-01-10",
"sol": 1575,
"ls": 296.0,
"min_temp": -58.0,
"min_temp_fahrenheit": -72.4,
"max_temp": 0.0,
"max_temp_fahrenheit": null,
"pressure": 860.0,
"pressure_string": "Higher",
"abs_humidity": null,
"wind_speed": null,
"wind_direction": "--",
"atmo_opacity": "Sunny",
"season": "Month 10",
"sunrise": "2017-01-10T12:30:00Z",
"sunset": "2017-01-11T00:46:00Z"
}
}
你应该意识到,然而,如果你使用重复调用json.dump()将一系列对象写入文件,结果是一系列合法的 JSON 格式对象,但文件整体的内容不是一个合法的 JSON 格式对象,并且尝试通过单次调用json.load()来读取和解析整个文件将会失败。如果你有多个对象想要编码成一个单一的 JSON 对象,你需要将这些对象全部放入一个列表(或者,更好的是,一个对象)中,然后将这个项目编码到文件中。
如果你有两三天火星天气数据想要存储为 JSON,你必须做出选择。你可以为每个对象使用一次json.dump(),这将导致包含 JSON 格式对象的文件。如果你假设weather_list是一个天气报告对象的列表,代码可能看起来像这样:
>>> outfile = open("mars_data.json", "w")
>>> for report in weather_list:
... json.dump(weather, outfile)
>>> outfile.close()
如果你这样做,那么你需要将每一行作为单独的 JSON 格式对象加载:
>>> for line in open("mars_data.json"):
... weather_list.append(json.loads(line))
作为另一种选择,你可以将列表放入一个单一的 JSON 对象中。因为 JSON 中顶层数组的可能存在漏洞,推荐的方式是将数组放入一个字典中:
>>> outfile = open("mars_data.json", "w")
>>> weather_obj = {"reports": weather_list, "count": 2}
>>> json.dump(weather, outfile)
>>> outfile.close()
使用这种方法,你可以使用一个操作从文件中加载 JSON 格式的对象:
>>> with open("mars_data.json") as infile:
>>> weather_obj = json.load(infile)
第二种方法如果 JSON 文件的大小可管理,那么是可行的,但对于非常大的文件来说可能不是最佳选择,因为处理错误可能有点困难,你可能会耗尽内存。
尝试这样做:保存一些 JSON 犯罪数据
修改你在第 22.2 节中编写的代码以获取芝加哥犯罪数据。然后将获取的数据从 JSON 格式字符串转换为 Python 对象。接下来,看看你是否可以将犯罪事件作为一系列单独的 JSON 对象保存到一个文件中,以及作为单个 JSON 对象保存到另一个文件中。然后看看加载每个文件所需的代码。
22.3.2. XML 数据
XML(可扩展标记语言)自 20 世纪末以来一直存在。XML 使用类似于 HTML 的尖括号标签符号,元素嵌套在其他元素中形成树结构。XML 旨在被机器和人类阅读,但由于 XML 通常非常冗长和复杂,因此人们很难理解。尽管如此,由于 XML 是一个既定的标准,因此在 XML 格式中找到数据是很常见的。尽管 XML 是机器可读的,但你很可能希望将其转换为更容易处理的东西。
看一下一些 XML 数据,在这个例子中是芝加哥天气数据的 XML 版本:
<dwml xmlns:xsi="http://
www.w3.org/2001/XMLSchema-instance" version="1.0"
xsi:noNamespaceSchemaLocation="http://www.nws.noaa.gov/forecasts/xml/
DWMLgen/schema/DWML.xsd">
<head>
<product srsName="WGS 1984" concise-name="glance" operational-
mode="official">
<title>
NOAA's National Weather Service Forecast at a Glance
</title>
<field>meteorological</field>
<category>forecast</category>
<creation-date refresh-frequency="PT1H">2017-01-08T02:52:41Z</creation-
date>
</product>
<source>
<more-information>http://www.nws.noaa.gov/forecasts/xml/</more-
information>
<production-center>
Meteorological Development Laboratory
<sub-center>Product Generation Branch</sub-center>
</production-center>
<disclaimer>http://www.nws.noaa.gov/disclaimer.html</disclaimer>
<credit>http://www.weather.gov/</credit>
<credit-logo>http://www.weather.gov/images/xml_logo.gif</credit-logo>
<feedback>http://www.weather.gov/feedback.php</feedback>
</source>
</head>
<data>
<location>
<location-key>point1</location-key>
<point latitude="41.78" longitude="-88.65"/>
</location>
...
</data>
</dwml>
这个例子只是文档的第一部分,省略了大部分数据。即便如此,它也说明了你在 XML 数据中通常会遇到的一些问题。特别是,你可以看到协议的冗长性质,在某些情况下,标签所占的空间比它们包含的值还要多。这个样本还展示了 XML 中常见的嵌套或树结构,以及在实际数据开始之前使用大量元数据作为标题的常见做法。从简单到复杂的数据文件来看,你可以将 CSV 或定界文件视为简单端,而 XML 视为复杂端。
最后,这个文件展示了 XML 的另一个特性,这使得提取数据变得更加具有挑战性。XML 支持使用属性来存储数据,以及标签内的文本值。所以如果你查看这个样本底部的point元素,你会看到point元素没有文本值。该元素在<point>标签内部只包含经纬度值:
<point latitude="41.78" longitude="-88.65"/>
这段代码无疑是合法的 XML,并且可以用来存储数据,但同样有可能(甚至很可能)以相同的方式存储相同的数据
<point>
<latitude>41.78</ latitude >
<longitude>-88.65</longitude>
</point>
没有仔细检查数据或研究规范文档,你真的不知道任何给定数据位将被如何处理。
这种复杂性可能会使得从 XML 中提取简单数据变得更加具有挑战性。你有几种处理 XML 的方法。Python 标准库包含解析和处理 XML 数据的模块,但没有一个特别适合简单的数据提取。
对于简单的数据提取,我发现最方便的实用工具是一个名为xmltodict的库,它解析你的 XML 数据并返回一个反映树的字典。实际上,在幕后它使用标准库的 expat XML 解析器,将你的 XML 文档解析成树,并使用该树来创建字典。因此,xmltodict可以处理解析器可以处理的所有内容,并且它还能在必要时将字典“反解析”为 XML,使其成为一个非常方便的工具。在多年的使用中,我发现这个解决方案完全满足我的 XML 处理需求。要获取xmltodict,你可以再次使用pip install xmltodict。
要将 XML 转换为字典,你可以导入xmltodict并使用 XML 格式字符串上的parse方法:
>>> import xmltodict
>>> data = xmltodict.parse(open("observations_01.xml").read())
在这种情况下,为了简洁,直接将文件内容传递给parse方法。解析后,这个数据对象是一个有序字典,其值与如果从该 JSON 加载的值相同:
{
"dwml": {
"@xmlns:xsd": "http://www.w3.org/2001/XMLSchema",
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
"@version": "1.0",
"@xsi:noNamespaceSchemaLocation": "http://www.nws.noaa.gov/forecasts/
xml/DWMLgen/schema/DWML.xsd",
"head": {
"product": {
"@srsName": "WGS 1984",
"@concise-name": "glance",
"@operational-mode": "official",
"title": "NOAA's National Weather Service Forecast at a Glance",
"field": "meteorological",
"category": "forecast",
"creation-date": {
"@refresh-frequency": "PT1H",
"#text": "2017-01-08T02:52:41Z"
}
},
"source": {
"more-information": "http://www.nws.noaa.gov/forecasts/xml/",
"production-center": {
"sub-center": "Product Generation Branch",
"#text": "Meteorological Development Laboratory"
},
"disclaimer": "http://www.nws.noaa.gov/disclaimer.html",
"credit": "http://www.weather.gov/",
"credit-logo": "http://www.weather.gov/images/xml_logo.gif",
"feedback": "http://www.weather.gov/feedback.php"
}
},
"data": {
"location": {
"location-key": "point1",
"point": {
"@latitude": "41.78",
"@longitude": "-88.65"
}
}
}
}
}
注意,属性已经被从标签中提取出来,但前面加上了@符号来表示它们原本是其父标签的属性。如果一个 XML 节点既有文本值又有嵌套元素,请注意文本值的键是"#text",就像在"production-center"元素下的"sub-center"元素一样。
之前我说过,解析的结果是一个有序字典(官方名称为OrderedDict),所以如果你打印它,代码看起来就像这样:
OrderedDict([('dwml', OrderedDict([('@xmlns:xsd', 'http://www.w3.org/2001/
XMLSchema'), ('@xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-
instance'), ('@version', '1.0'), ('@xsi:noNamespaceSchemaLocation',
'http://www.nws.noaa.gov/forecasts/xml/DWMLgen/schema/DWML.xsd'),
('head', OrderedDict([('product', OrderedDict([('@srsName', 'WGS 1984'),
('@concise-name', 'glance'), ('@operational-mode', 'official'),
('title', "NOAA's National Weather Service Forecast at a Glance"),
('field', 'meteorological'), ('category', 'forecast'), ('creation-date',
OrderedDict([('@refresh-frequency', 'PT1H'), ('#text', '2017-01-
08T02:52:41Z')]))])), ('source', OrderedDict([('more-information',
'http://www.nws.noaa.gov/forecasts/xml/'), ('production-center',
OrderedDict([('sub-center', 'Product Generation Branch'), ('#text',
'Meteorological Development Laboratory')])), ('disclaimer', 'http://
www.nws.noaa.gov/disclaimer.html'), ('credit', 'http://www.weather.gov/
'), ('credit-logo', 'http://www.weather.gov/images/xml_logo.gif'),
('feedback', 'http://www.weather.gov/feedback.php')]))])), ('data',
OrderedDict([('location', OrderedDict([('location-key', 'point1'),
('point', OrderedDict([('@latitude', '41.78'), ('@longitude', '-
88.65')]))])), ('#text', '...')]))]))])
即使OrderedDict及其元组的列表表示看起来相当奇怪,但它的行为与正常的dict完全相同,只是它承诺保持元素的顺序,这在这种情况下很有用。
如果一个元素被重复,它就变成了一个列表。在之前显示的文件的完整版本的一个进一步部分中,以下元素出现(一些元素被省略在这个样本中):
<time-layout >
<start-valid-time period-name="Monday">2017-01-09T07:00:00-06:00</start-
valid-time>
<end-valid-time>2017-01-09T19:00:00-06:00</end-valid-time>
<start-valid-time period-name="Tuesday">2017-01-10T07:00:00-06:00</start-
valid-time>
<end-valid-time>2017-01-10T19:00:00-06:00</end-valid-time>
<start-valid-time period-name="Wednesday">2017-01-11T07:00:00-06:00</
start-valid-time>
<end-valid-time>2017-01-11T19:00:00-06:00</end-valid-time>
</time-layout>
注意,两个元素——“start-valid-time”和“end-valid-time”——交替重复。这两个重复的元素在字典中各自被转换成列表,保持每组元素的正确顺序:
"time-layout":
{
"start-valid-time": [
{
"@period-name": "Monday",
"#text": "2017-01-09T07:00:00-06:00"
},
{
"@period-name": "Tuesday",
"#text": "2017-01-10T07:00:00-06:00"
},
{
"@period-name": "Wednesday",
"#text": "2017-01-11T07:00:00-06:00"
}
],
"end-valid-time": [
"2017-01-09T19:00:00-06:00",
"2017-01-10T19:00:00-06:00",
"2017-01-11T19:00:00-06:00"
]
},
由于在 Python 中处理字典和列表(即使是嵌套的字典和列表)相对容易,因此使用xmltodict是处理大多数 XML 的有效方法。实际上,我在过去几年中在生产环境中使用它处理了各种 XML 文档,从未遇到过问题。
尝试这样做:获取和解析 XML
编写代码以从mng.bz/103V获取芝加哥 XML 天气预报。然后使用xmltodict将 XML 解析成 Python 字典,并提取明天的最高温度预报。提示:为了匹配时间布局和值,比较第一个时间布局部分的布局-key 值和参数元素中温度元素的 time-layout 属性。
22.4. 爬取网页数据
在某些情况下,数据在网站上,但由于某种原因在其他地方不可用。在这些情况下,可能有必要通过称为爬取或抓取的过程从网页本身收集数据。
在进一步讨论爬取之前,让我先声明一下:爬取或抓取你并不拥有或控制的网站,在最好的情况下也是一个法律灰色地带,涉及许多不明确且相互矛盾的考虑因素,例如网站的条款、访问网站的方式以及抓取数据的用途。除非你控制你想爬取的网站,否则对于“我爬取这个网站是否合法?”这个问题,通常的回答是“这取决于。”
如果你确实决定爬取一个生产网站,你还需要对你在网站上施加的负载保持敏感。虽然一个建立已久、流量大的网站可能能够处理你扔给它的任何东西,但一个较小、不太活跃的网站可能会因为一系列连续的请求而陷入停滞。至少,你需要小心,确保你的爬取不会变成一个无意的拒绝服务攻击。
相反,我曾在某些情况下发现,爬取我们自己的网站以获取一些所需数据实际上比通过公司渠道更容易。尽管爬取网络数据有其位置,但在这里全面处理它过于复杂。在本节中,我提供了一个非常简单的例子,以给你一个基本方法的一般概念,并在更复杂的情况下提供进一步的建议。
爬取网站包括两个部分:获取网页和从中提取数据。获取页面可以通过请求完成,这相当简单。
考虑一个非常简单的网页的代码,内容很少,没有 CSS 或 JavaScript,就像这样。
列表 22.1. 文件 test.html
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html> <head>
<title>Title</title>
</head>
<body>
<h1>Heading 1</h1>
This is plan text, and is boring
<span class="special">this is special</span>
Here is a <a href="http://bitbucket.dev.null">link</a>
<hr>
<address>Ann Address, Somewhere, AState 00000
</address>
</body> </html>
假设你只对这个页面上的几种数据感兴趣:任何具有"special"类名的元素以及任何链接。你可以通过搜索字符串'class="special"'和"<a href"来处理文件,然后编写代码从那里挑选数据,但即使使用正则表达式,这个过程也会很繁琐,容易出错,难以维护。使用知道如何解析 HTML 的库,如 Beautiful Soup,会容易得多。如果你想尝试以下代码并实验解析 HTML 页面,可以使用pip install bs4。
当你安装了 Beautiful Soup 后,解析 HTML 页面就变得简单了。在这个例子中,假设你已经获取了网页(可能使用 requests 库),所以你只需解析 HTML。
第一步是加载文本并创建 Beautiful Soup 解析器:
>>> import bs4
>>> html = open("test.html").read()
>>> bs = bs4.BeautifulSoup(html, "html.parser")
这就是将 HTML 解析到解析器对象bs所需的全部代码。Beautiful Soup 解析器对象有很多酷炫的技巧,如果你在处理 HTML,花点时间去实验并了解它能为你做什么是非常值得的。在这个例子中,你只看两件事:通过 HTML 标签提取内容以及通过 CSS 类获取数据。
首先,找到链接。链接的 HTML 标签是<a>(Beautiful Soup 默认将所有标签转换为小写),所以为了找到所有链接标签,你可以使用"a"作为参数并调用bs对象本身:
>>> a_list = bs("a")
>>> print(a_list)
[<a href="http://bitbucket.dev.null">link</a>]
现在你已经有一个包含所有(在这个例子中只有一个)HTML 链接标签的列表。如果这就是你得到的所有内容,那倒也还不错,但实际上,列表中返回的元素也是解析器对象,并且可以为你完成获取链接和文本的其余工作:
>>> a_item = a_list[0]
>>> a_item.text
'link'
>>> a_item["href"]
'http://bitbucket.dev.null'
你正在寻找的另一个功能是任何具有"special"CSS 类的元素,你可以通过使用解析器的select方法如下提取:
>>> special_list = bs.select(".special")
>>> print(special_list)
[<span class="special">this is special</span>]
>>> special_item = special_list[0]
>>> special_item.text
'this is special'
>>> special_item["class"]
['special']
因为标签或select方法返回的项目本身就是解析器对象,所以你可以嵌套它们,这允许你从 HTML 甚至 XML 中提取几乎所有内容。
尝试这个:解析 HTML
给定文件forecast.html(你可以在本书的网站上找到该代码),编写一个使用 Beautiful Soup 提取数据并将其保存为 CSV 文件的脚本,如下所示。
列表 22.2. 文件 forecast.html
<html>
<body>
<div class="row row-forecast">
<div class="grid col-25 forecast-label"><b>Tonight</b></div>
<div class="grid col-75 forecast-text">A slight chance of showers and
thunderstorms before 10pm. Mostly cloudy, with a low around 66\. West
southwest wind around 9 mph. Chance of precipitation is 20%. New
rainfall amounts between a tenth and quarter of an inch possible.</div>
</div>
<div class="row row-forecast">
<div class="grid col-25 forecast-label"><b>Friday</b></div>
<div class="grid col-75 forecast-text">Partly sunny. High near 77,
with temperatures falling to around 75 in the afternoon. Northwest wind
7 to 12 mph, with gusts as high as 18 mph.</div>
</div>
<div class="row row-forecast">
<div class="grid col-25 forecast-label"><b>Friday Night</b></div>
<div class="grid col-75 forecast-text">Mostly cloudy, with a low
around 63\. North wind 7 to 10 mph.</div>
</div>
<div class="row row-forecast">
<div class="grid col-25 forecast-label"><b>Saturday</b></div>
<div class="grid col-75 forecast-text">Mostly sunny, with a high near
73\. North wind around 10 mph.</div>
</div>
<div class="row row-forecast">
<div class="grid col-25 forecast-label"><b>Saturday Night</b></div>
<div class="grid col-75 forecast-text">Partly cloudy, with a low
around 63\. North wind 5 to 10 mph.</div>
</div>
<div class="row row-forecast">
<div class="grid col-25 forecast-label"><b>Sunday</b></div>
<div class="grid col-75 forecast-text">Mostly sunny, with a high near
73.</div>
</div>
<div class="row row-forecast">
<div class="grid col-25 forecast-label"><b>Sunday Night</b></div>
<div class="grid col-75 forecast-text">Mostly cloudy, with a low
around 64.</div>
</div>
<div class="row row-forecast">
<div class="grid col-25 forecast-label"><b>Monday</b></div>
<div class="grid col-75 forecast-text">Mostly sunny, with a high near
74.</div>
</div>
<div class="row row-forecast">
<div class="grid col-25 forecast-label"><b>Monday Night</b></div>
<div class="grid col-75 forecast-text">Mostly clear, with a low
around 65.</div>
</div>
<div class="row row-forecast">
<div class="grid col-25 forecast-label"><b>Tuesday</b></div>
<div class="grid col-75 forecast-text">Sunny, with a high near 75.</
div>
</div>
<div class="row row-forecast">
<div class="grid col-25 forecast-label"><b>Tuesday Night</b></div>
<div class="grid col-75 forecast-text">Mostly clear, with a low
around 65.</div>
</div>
<div class="row row-forecast">
<div class="grid col-25 forecast-label"><b>Wednesday</b></div>
<div class="grid col-75 forecast-text">Sunny, with a high near 77.</
div>
</div>
<div class="row row-forecast">
<div class="grid col-25 forecast-label"><b>Wednesday Night</b></div>
<div class="grid col-75 forecast-text">Mostly clear, with a low
around 67.</div>
</div>
<div class="row row-forecast">
<div class="grid col-25 forecast-label"><b>Thursday</b></div>
<div class="grid col-75 forecast-text">A chance of rain showers after
1pm. Mostly sunny, with a high near 81\. Chance of precipitation is
30%.</div>
</div>
</body>
</html>
实验 22:跟踪“好奇号”的天气
使用 第 22.2 节 中描述的 API 来收集“好奇号”在火星上停留一个月的天气历史。提示:你可以通过在存档查询的末尾添加 ?sol=sol_number 来指定火星日(sols),如下所示:
marsweather.ingenology.com/v1/archive/?sol=155
将数据转换成可以加载到电子表格和绘制图表的形式。有关此项目的版本,请参阅书籍的源代码。
摘要
-
使用 Python 脚本来获取文件可能不是最佳选择。请务必考虑其他选项。
-
使用
requests模块通过 HTTP/HTTPS 和 Python 获取文件是最佳选择。 -
从 API 获取文件与获取静态文件非常相似。
-
API 请求的参数通常需要引用并作为查询字符串添加到请求 URL 中。
-
JSON 格式的字符串在 API 提供的数据中很常见,XML 也被使用。
-
爬取你无法控制的网站可能不合法或不道德,并且需要考虑不要过度负载服务器。
第二十三章。保存数据
本章涵盖
-
在关系型数据库中存储数据
-
使用 Python DB-API
-
通过对象关系映射器(ORM)访问数据库
-
理解 NoSQL 数据库以及它们与关系型数据库的不同之处
当你拥有数据并且已经清理完毕,你很可能会想要存储它。你不仅想要存储它,还希望将来能够尽可能少地麻烦地访问它。存储和检索大量数据通常需要某种类型的数据库。关系型数据库,如 PostgreSQL、MySQL 和 SQL Server,几十年来一直是数据存储的流行选择,并且对于许多用例来说仍然是很好的选择。近年来,NoSQL 数据库,包括 MongoDB 和 Redis,已经受到青睐,并且对于各种用例非常有用。详细讨论数据库需要几本书的内容,所以在本章中,我将探讨一些场景,以展示如何使用 Python 访问 SQL 和 NoSQL 数据库。
23.1. 关系型数据库
关系型数据库长期以来一直是存储和操作数据的标准。它们是一项成熟且普遍的技术。Python 可以连接到多种关系型数据库,但我没有时间或意愿在这本书中详细介绍每一种。相反,由于 Python 以基本一致的方式处理数据库,我将以 sqlite3 为例来展示基础知识,并讨论选择和使用关系型数据库进行数据存储的一些差异和考虑因素。
23.1.1. Python 数据库 API
正如我提到的,由于 PEP-249 (www.python.org/dev/peps/pep-0249/),Python 在多个数据库实现中处理 SQL 数据库访问的方式非常相似,该 PEP 指定了连接到 SQL 数据库的一些常见做法。通常被称为数据库 API 或 DB-API,它的创建是为了鼓励“代码在数据库之间更具可移植性,以及更广泛的数据库连接范围。”多亏了 DB-API,本章中展示的 SQLite 示例与用于 PostgreSQL、MySQL 或几个其他数据库的示例非常相似。
23.2. SQLite:使用 sqlite3 数据库
虽然 Python 有许多数据库模块,但在以下示例中,我将探讨 sqlite3。尽管它不适合大型、高流量应用,但 sqlite3 有两个优点:
-
由于它是标准库的一部分,你可以在需要数据库的任何地方使用它,而无需担心添加依赖项。
-
sqlite3 将所有记录存储在本地文件中,因此它不需要客户端和服务器,这对于 PostgreSQL、MySQL 和其他大型数据库来说是必需的。
这些特性使得 sqlite3 成为小型应用和快速原型的一个便捷选择。
要使用 sqlite3 数据库,你需要的第一件事是获取一个Connection对象。获取Connection对象只需要调用connect函数并传入用于存储数据的文件名:
>>> import sqlite3
>>> conn = sqlite3.connect("datafile.db")
还可以使用":memory:"作为文件名来在内存中保存数据。对于存储 Python 整数、字符串和浮点数,不需要更多。如果你希望 sqlite3 自动将某些列的查询结果转换为其他类型,包含detect_types参数设置为sqlite3.PARSE_DECLTYPES |sqlite3.PARSE_COLNAMES是有用的,这会指导Connection对象解析查询中的列名和类型,并尝试将它们与您已定义的转换器相匹配。
第二步是从连接创建一个Cursor 对象:
>>> cursor = conn.cursor()
>>> cursor
<sqlite3.Cursor object at 0xb7a12980>
在这个阶段,你能够对数据库进行查询。在当前情况下,由于数据库还没有表或记录,你首先需要创建一个表并插入几条记录:
>>> cursor.execute("create table people (id integer primary key, name text,
count integer)")
>>> cursor.execute("insert into people (name, count) values ('Bob', 1)")
>>> cursor.execute("insert into people (name, count) values (?, ?)",
... ("Jill", 15))
>>> conn.commit()
最后一个insert查询说明了使用变量进行查询的首选方式。与其构建查询字符串,不如为每个变量使用?,然后将变量作为元组参数传递给execute方法。这样做的好处是,你不需要担心值被错误地转义;sqlite3 会为你处理。
你也可以在查询中使用以:为前缀的变量名,并传递一个包含要插入值的相应字典:
>>> cursor.execute("insert into people (name, count) values (:username, \
:usercount)", {"username": "Joe", "usercount": 10})
表被填充后,你可以使用 SQL 命令查询数据,再次使用?进行变量绑定或使用名称和字典:
>>> result = cursor.execute("select * from people")
>>> print(result.fetchall())
[('Bob', 1), ('Jill', 15), ('Joe', 10)]
>>> result = cursor.execute("select * from people where name like :name",
... {"name": "bob"})
>>> print(result.fetchall())
[('Bob', 1)]
>>> cursor.execute("update people set count=? where name=?", (20, "Jill"))
>>> result = cursor.execute("select * from people")
>>> print(result.fetchall())
[('Bob', 1), ('Jill', 20), ('Joe', 10)]
除了fetchall方法外,fetchone方法获取结果中的一行,而fetchmany返回任意数量的行。为了方便,也可以像迭代文件一样迭代游标对象的行:
>>> result = cursor.execute("select * from people")
>>> for row in result:
... print(row)
...
('Bob', 1)
('Jill', 20)
('Joe', 10)
最后,默认情况下,sqlite3 不会立即提交事务。这个事实意味着如果你的事务失败,你可以选择回滚事务,但也意味着你需要使用Connection对象的commit方法来确保所做的任何更改都已保存。在关闭数据库连接之前这样做尤其是个好主意,因为close方法不会自动提交任何活动事务:
>>> cursor.execute("update people set count=? where name=?", (20, "Jill"))
>>> conn.commit()
>>> conn.close()
表 23.1 概述了 sqlite3 数据库上最常见的操作。
表 23.1. 常见的 sqlite3 数据库操作
| 操作 | sqlite3 命令 |
|---|---|
| 创建到数据库的连接。 | conn = sqlite3.connect(filename) |
| 为连接创建一个游标。 | Cursor = conn.cursor() |
| 使用游标执行查询。 | cursor.execute(query) |
| 返回查询的结果。 | cursor.fetchall(),cursor.fetchmany(num_rows), cursor.fetchone()
for row in cursor:
.... |
| 向数据库提交事务。 | conn.commit() |
|---|---|
| 关闭连接。 | conn.close() |
这些操作通常是你需要操作 sqlite3 数据库的全部。当然,有几个选项可以让你控制它们的精确行为;有关更多信息,请参阅 Python 文档。
尝试这个:创建和修改表
使用 sqlite3,编写创建数据库表的代码,用于存储从 第 21.2 节 中加载的伊利诺伊州天气数据。假设你还有更多州的数据,并想存储更多关于各州本身的信息。你如何修改数据库以使用相关表来存储州信息?
23.3. 使用 MySQL、PostgreSQL 和其他关系型数据库
如我在本章前面提到的,几个其他 SQL 数据库也有遵循 DB-API 的客户端库。因此,在 Python 中访问这些数据库相当相似,但也有一些差异需要注意:
-
与 SQLite 不同,这些数据库需要客户端连接的数据库服务器,而这个服务器可能位于不同的机器上,因此连接需要更多的参数——通常包括主机、账户名称和密码。
-
将参数插入查询的方式,例如
"select * from test where name like :name",可能使用不同的格式——类似于?, %s 5(name)s。
这些变化并不大,但它们往往使代码在不同数据库之间完全不可移植。
23.4. 使用 ORM 使数据库处理更简单
本章前面提到的 DB-API 数据库客户端库及其要求编写原始 SQL 存在一些问题:
-
不同的 SQL 数据库以微妙不同的方式实现了 SQL,因此当你从一个数据库切换到另一个数据库时,相同的 SQL 语句可能不会总是工作,比如,如果你在本地开发中使用 sqlite3,然后想在生产中使用 MySQL 或 PostgreSQL。此外,如前所述,不同的实现有不同的方法来处理诸如将参数传递到查询中这样的操作。
-
第二个缺点是需要使用原始 SQL 语句。在代码中包含 SQL 语句可以使代码更难维护,尤其是如果你有很多这样的语句。在这种情况下,其中一些将是样板和常规的;其他将是复杂和棘手的;而且所有这些都需要测试,这可能会变得繁琐。
-
需要编写 SQL 的需求意味着你需要至少用两种语言进行思考:Python 和特定的 SQL 变体。在许多情况下,使用原始 SQL 是值得这些麻烦的,但在许多其他情况下则不然。
鉴于这些问题,人们想要一种在 Python 中处理数据库的方法,这种方法更容易管理,并且不需要编写除常规 Python 代码之外的内容。解决方案是一个对象关系映射器(ORM),它将关系数据库的类型和结构转换为 Python 中的对象。Python 世界中最常见的 ORM 是 Django ORM 和 SQLAlchemy,尽管当然还有许多其他 ORM。Django ORM 与 Django Web 框架紧密集成,通常不会在它之外使用。因为我在这本书中不会深入探讨 Django,所以除了指出它是 Django 应用程序的默认选择,并且是一个很好的选择,拥有完善的工具和丰富的社区支持外,我不会讨论 Django ORM。
23.4.1. SQLAlchemy
SQLAlchemy 是 Python 空间中另一个知名的 ORM。SQLAlchemy 的目标是自动化冗余的数据库任务,在允许开发者控制数据库和访问底层 SQL 的同时,提供基于 Python 对象的数据接口。在本节中,我将查看一些使用 SQLAlchemy 将数据存储到关系数据库并检索它的基本示例。
你可以使用pip在你的环境中安装 SQLAlchemy:
> pip install sqlalchemy
注意
在从这个点开始使用 SQLAlchemy 及其相关工具时,在同一虚拟环境中打开两个 shell 窗口会更方便:一个用于 Python,另一个用于你的系统命令行。
SQLAlchemy 提供了多种与数据库及其表交互的方式。尽管 ORM 允许你在需要或想要的时候编写 SQL 语句,但 ORM 的强大之处在于它所暗示的功能:将关系数据库的表和列映射到 Python 对象。
使用 SQLAlchemy 来复制你在第 23.2 节中做的事情:创建一个表,添加三行数据,查询表,并更新一行。你需要进行一些额外的设置来使用 ORM,但在大型项目中,这种努力是非常值得的。
首先,你需要导入连接到数据库并将表映射到 Python 对象所需的组件。从基础sqlalchemy包中,你需要create_engine和select方法以及MetaData和Table类。但由于你需要在创建table对象时指定模式信息,因此你还需要导入Column类以及每个列的数据类型类——在这种情况下,Integer和String。从sqlalchemy.orm子包中,你还需要sessionmaker函数:
>>> from sqlalchemy import create_engine, select, MetaData, Table, Column,
Integer, String
>>> from sqlalchemy.orm import sessionmaker
现在你可以考虑连接到数据库:
>>> dbPath = 'datafile2.db'
>>> engine = create_engine('sqlite:///%s' % dbPath)
>>> metadata = MetaData(engine)
>>> people = Table('people', metadata,
... Column('id', Integer, primary_key=True),
... Column('name', String),
... Column('count', Integer),
... )
>>> Session = sessionmaker(bind=engine)
>>> session = Session()
>>> metadata.create_all(engine)
要创建和连接,您需要为您的数据库创建一个合适的数据库引擎;然后您需要一个 MetaData 对象,它是一个用于管理表及其模式的容器。创建一个名为 data 的 Table 对象,指定数据库中的表名、您刚刚创建的 MetaData 对象以及您想要创建的列及其数据类型。最后,您使用 sessionmaker 函数为您引擎创建一个 Session 类,并使用该类实例化会话对象。此时,您已连接到数据库,最后一步是使用 create_all 方法创建表。
当表创建完成后,下一步是插入一些记录。在 SQLAlchemy 中,您有多种方法可以做到这一点,但在这个例子中您将非常明确。创建一个 insert 对象,然后执行它:
>>> people_ins = people.insert().values(name='Bob', count=1)
>>> str(people_ins)
'INSERT INTO people (name, count) VALUES (?, ?)'
>>> session.execute(people_ins)
<sqlalchemy.engine.result.ResultProxy object at 0x7f126c6dd438>
>>> session.commit()
在这里,您使用 insert() 方法创建一个 insert 对象,并指定您想要插入的字段和值。people_ins 是 insert 对象,您使用 str() 函数来显示在幕后您创建了正确的 SQL 命令。然后您使用会话对象的 execute 方法执行插入,并使用 commit 方法将其提交到数据库:
>>> session.execute(people_ins, [
... {'name': 'Jill', 'count':15},
... {'name': 'Joe', 'count':10}
... ])
<sqlalchemy.engine.result.ResultProxy object at 0x7f126c6dd908>
>>> session.commit()
>>> result = session.execute(select([people]))
>>> for row in result:
... print(row)
...
(1, 'Bob', 1)
(2, 'Jill', 15)
(3, 'Joe', 10)
您可以通过传递一个包含每个插入的字段名称和值的字典列表来简化操作,并执行多个插入:
>>> result = session.execute(select([people]).where(people.c.name == 'Jill'))
>>> for row in result:
... print(row)
...
(2, 'Jill', 15)
您还可以使用 select() 方法与 where() 方法结合使用,以查找特定的记录。在示例中,您正在寻找任何 name 列等于 'Jill' 的记录。请注意,where 表达式使用 people.c.name,其中 c 表示 name 是 people 表中的一列:
>>> result = session.execute(people.update().values(count=20).where
(people.c.name == 'Jill'))
>>> session.commit()
>>> result = session.execute(select([people]).where(people.c.name == 'Jill'))
>>> for row in result:
... print(row)
...
(2, 'Jill', 20)
>>>
最后,您可以将 update() 方法与 where() 方法结合使用,以更新单行。
将表对象映射到类
到目前为止,您直接使用了表对象,但也可以使用 SQLAlchemy 将表直接映射到类。这种技术的优点是列直接映射到类属性。为了说明,创建一个名为 People 的类:
>>> from sqlalchemy.ext.declarative import declarative_base
>>> Base = declarative_base()
>>> class People(Base):
... __tablename__ = "people"
... id = Column(Integer, primary_key=True)
... name = Column(String)
... count = Column(Integer)
...
>>> results = session.query(People).filter_by(name='Jill')
>>> for person in results:
... print(person.id, person.name, person.count)
...
2 Jill 20
插入可以通过创建映射类的实例并将其添加到会话中来实现:
>>> new_person = People(name='Jane', count=5)
>>> session.add(new_person)
>>> session.commit()
>>>
>>> results = session.query(People).all()
>>> for person in results:
... print(person.id, person.name, person.count)
...
1 Bob 1
2 Jill 20
3 Joe 10
4 Jane 5
更新操作也非常直接。您检索要更新的记录,更改映射实例中的值,然后将更新后的记录添加到会话中以写回数据库:
>>> jill = session.query(People).filter_by(name='Jill').first()
>>> jill.name
'Jill'
>>> jill.count = 22
>>> session.add(jill)
>>> session.commit()
>>> results = session.query(People).all()
>>> for person in results:
... print(person.id, person.name, person.count)
...
1 Bob 1
2 Jill 22
3 Joe 10
4 Jane 5
删除操作与更新类似;您检索要删除的记录,然后使用会话的 delete() 方法将其删除:
>>> jane = session.query(People).filter_by(name='Jane').first()
>>> session.delete(jane)
>>> session.commit()
>>> jane = session.query(People).filter_by(name='Jane').first()
>>> print(jane)
None
使用 SQLAlchemy 比直接使用原始 SQL 需要更多的设置,但它也有一些真正的优点。首先,使用 ORM 意味着您不需要担心不同数据库支持的 SQL 之间的细微差异。示例在 sqlite3、MySQL 和 PostgreSQL 上都能正常工作,无需在代码中进行任何更改,只需将字符串传递给创建引擎并确保正确的数据库驱动程序可用。
另一个优点是,可以通过 Python 对象与数据交互,这可能对缺乏 SQL 经验的程序员来说更容易访问。他们不需要构造 SQL 语句,而是可以使用 Python 对象及其方法。
尝试这样做:使用 ORM
使用之前的数据库,编写一个 SQLAlchemy 类来映射数据表,并使用它来读取表中的记录。
23.4.2. 使用 Alembic 进行数据库模式更改
在开发使用关系型数据库的代码过程中,在开始工作后更改数据库的结构或模式是很常见,如果不是普遍现象。需要添加字段,或者更改它们的类型,等等。当然,可以手动更改数据库表和访问它们的 ORM 代码,但这种方法有一些缺点。首先,如果需要,这样的更改很难回滚,而且很难跟踪与代码特定版本相对应的数据库配置。
解决方案是使用数据库迁移工具来帮助你进行更改并跟踪它们。迁移以代码的形式编写,应包括应用所需更改的代码以及撤销它们的代码。然后,可以跟踪并按正确顺序应用或撤销更改。因此,你可以可靠地将数据库升级或降级到开发过程中的任何状态。
作为示例,本节简要介绍了 Alembic,这是一个流行的轻量级迁移工具,用于 SQLAlchemy。首先,切换到项目目录中的系统命令行窗口,安装 Alembic,并使用alemic init创建一个通用环境:
> pip install alembic
> alembic init alembic
此代码创建了你使用 Alembic 进行数据迁移所需文件结构。有一个需要至少在一个地方编辑的 alembic.ini 文件。squalchemy.url行需要更新以匹配你的当前情况:
sqlalchemy.url = driver://user:pass@localhost/dbname
将行更改为
sqlalchemy.url = sqlite:///datafile.db
由于你使用的是本地 sqlite 文件,因此不需要用户名或密码。
下一步是使用 Alembic 的修订命令创建修订:
> alembic revision -m "create an address table"
Generating /home/naomi/qpb_testing/alembic/versions/
384ead9efdfd_create_a_test_address_table.py ... done
此代码在 alembic/versions 目录中创建了一个修订脚本,文件名为 384ead9efdfd_create_a_test_address_table.py。此文件看起来如下:
"""create an address table
Revision ID: 384ead9efdfd
Revises:
Create Date: 2017-07-26 21:03:29.042762
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '384ead9efdfd'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
pass
def downgrade():
pass
你可以看到,文件在标题中包含修订 ID 和日期。它还包含一个down_revision变量,用于指导每个版本的回滚。如果你进行第二次修订,其down_revision变量应包含此修订的 ID。
要执行修订,更新修订脚本以提供在upgrade()方法中执行修订的代码以及在downgrade()方法中撤销它的代码:
def upgrade():
op.create_table(
'address',
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('address', sa.String(50), nullable=False),
sa.Column('city', sa.String(50), nullable=False),
sa.Column('state', sa.String(20), nullable=False),
)
def downgrade():
op.drop_table('address')
当此代码创建后,可以应用升级。但首先,切换回 Python 外壳窗口以查看你的数据库中有哪些表:
>>> print(engine.table_names())
['people']
正如你所预期的,你只有你之前创建的那个表。现在你可以运行 Alembic 的 upgrade 命令来应用升级并添加一个新表。切换到你的系统命令行,并运行
> alembic upgrade head
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> 384ead9efdfd, create an
address table
如果你回到 Python 并检查,你会看到数据库增加了两个额外的表:
>>> engine.table_names()
['alembic_version', 'people', 'address'
第一个新表,'alembic version',是由 Alembic 创建的,以帮助跟踪你的数据库当前处于哪个版本(供未来的升级和降级参考)。第二个新表,'address',是你通过升级添加的表,已经准备好使用。
如果你想要将数据库的状态回滚到之前的状态,你只需要在系统窗口中运行 Alembic 的 downgrade 命令。你给出降级命令 -1 来告诉 Alembic 你想要降级一个版本:
> alembic downgrade -1
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running downgrade 384ead9efdfd -> , create
an address table
现在如果你在你的 Python 会话中检查,你会回到你开始的地方,除了版本跟踪表仍然存在:
>>> engine.table_names()
['alembic_version', 'people']
如果你愿意,当然可以再次运行升级命令,将表恢复,添加更多修订,进行升级等等。
尝试这个:使用 Alembic 修改数据库
尝试创建一个 Alembic 升级,向你的数据库添加一个状态表,包含 ID、状态名称和缩写。升级和降级。如果你打算使用状态表和现有的数据表,还需要进行哪些其他更改?
23.5. NoSQL 数据库
尽管关系型数据库长期以来一直很受欢迎,但它们并不是存储数据的唯一方式。虽然关系型数据库主要关注在相关表中规范化数据,但其他方法看待数据的方式不同。这些类型的数据库通常被称为 NoSQL 数据库,因为它们通常不遵循 SQL 创建用来描述的行/列/表结构。
与将数据作为行、列和表的集合来处理不同,NoSQL 数据库可以将它们存储的数据视为键值对、索引文档,甚至作为图。许多 NoSQL 数据库都可用,它们处理数据的方式各有不同。一般来说,它们不太可能严格规范化,这可以使检索信息更快、更容易。作为例子,在本章中,我将探讨使用 Python 访问两个常见的 NoSQL 数据库:Redis 和 MongoDB。以下内容只是触及了你可以用 NoSQL 数据库和 Python 做的事情的表面,但它应该能给你一个基本的概念。如果你已经熟悉 Redis 或 MongoDB,你会看到 Python 客户端库的一些工作方式,如果你是 NoSQL 数据库的新手,你至少会得到这些数据库如何工作的一个概念。
23.6. 使用 Redis 的键值存储
Redis 是一个内存中的网络键值存储。因为值在内存中,所以查找可以非常快,而且它设计为可以通过网络访问,这使得它在各种情况下都很有用。Redis 通常用于缓存、作为消息代理以及快速查找信息。实际上,这个名字(来源于远程字典服务器)是一个很好的思考方式;它表现得就像一个翻译成网络服务的 Python 字典。
以下示例展示了 Redis 与 Python 的工作方式。如果你熟悉 Redis 命令行界面或使用过其他语言的 Redis 客户端,这些简短的示例应该能帮助你开始使用 Redis 与 Python。如果你对 Redis 是新手,以下示例为你提供了它的工作方式;你可以在 redis.io 上探索更多。
尽管有多个 Python 客户端可用于 Redis,但根据 Redis 网站的说法,目前的方法是使用一个名为 redis-py 的客户端。你可以使用 pip install redis 命令来安装它。
运行 Redis 服务器
为了进行实验,你需要有一个正在运行的 Redis 服务器。虽然你可以使用基于云的 Redis 服务,但对于实验来说,你的最佳选择是使用 Docker 实例或在机器上安装服务器。
如果你已经安装了 Docker,使用 Redis Docker 实例可能是启动和运行服务器最快、最简单的方式。你应该可以使用类似 > docker run -p 6379:6379 redis 的命令从命令行启动一个 Redis 实例。
在 Linux 系统上,使用系统包管理器安装 Redis 应该相当简单,在 Mac 系统上,brew install redis 应该有效。在 Windows 系统上,你应该检查 redis.io 网站,或在网上搜索 Windows 上运行 Redis 的当前选项。当 Redis 安装完成后,你可能需要在网上查找说明,以确保 Redis 服务器正在运行。
当你启动一个服务器后,以下是一些简单的 Redis 与 Python 交互的示例。首先,你需要导入 Redis 库并创建一个 Redis 连接对象:
>>> import redis
>>> r = redis.Redis(host='localhost', port=6379)
在创建 Redis 连接时,你可以使用多个连接选项,包括主机、端口、密码或 SSH 证书。如果服务器在本地主机的默认端口 6379 上运行,则不需要任何选项。当你有了连接,你可以使用它来访问键值存储。
你可能会做的第一件事是使用 keys() 方法来获取数据库中的键列表,该方法返回当前存储的键列表(如果有)。然后你可以设置不同类型的键,并尝试一些方法来检索它们的值:
>>> r.keys()
[]
>>> r.set('a_key', 'my value')
True
>>> r.keys()
[b'a_key']
>>> v = r.get('a_key')
>>> v
b'my value'
>>> r.incr('counter')
1
>>> r.get('counter')
b'1'
>>> r.incr('counter')
2
>>> r.get('counter')
b'2'
这些示例展示了如何获取 Redis 数据库中的键列表,如何设置带有值的键,以及如何设置带有 counter 变量的键并对其进行递增。
这些示例涉及存储数组或列表:
>>> r.rpush("words", "one")
1
>>> r.rpush("words", "two")
2
>>> r.lrange("words", 0, -1)
[b'one', b'two']
>>> r.rpush("words", "three")
3
>>> r.lrange("words", 0, -1)
[b'one', b'two', b'three']
>>> r.llen("words")
3
>>> r.lpush("words", "zero")
4
>>> r.lrange("words", 0, -1)
[b'zero', b'one', b'two', b'three']
>>> r.lrange("words", 2, 2)
[b'two']
>>> r.lindex("words", 1)
b'one'
>>> r.lindex("words", 2)
b'two'
当你开始键时,"words" 不在数据库中,但将值添加到末尾(从右到左,r 在 rpush 中)会创建键,将其值设为空列表,然后追加值 'one'。再次使用 rpush 会将另一个单词添加到末尾。要检索列表中的值,你可以使用 lrange() 函数,提供键以及起始索引和结束索引,其中 -1 表示列表的末尾。
还要注意,你可以使用 lpush() 在列表的开头或左侧添加元素。你可以使用 lindex() 以与 lrange() 相同的方式检索单个值,只是你需要提供你想要值的索引。
值的过期
Redis 的一个特性是能够为键值对设置过期时间。在这段时间过去后,键和值会被移除。这种技术特别适用于将 Redis 作为缓存使用。当你为键设置值时,你可以设置超时值(以秒为单位):
>>> r.setex("timed", "10 seconds", 10)
True
>>> r.pttl("timed")
7165
>>> r.pttl("timed")
5208
>>> r.pttl("timed")
1542
>>> r.pttl("timed")
>>>
在这种情况下,你将 "timed" 的过期时间设置为 10 秒。然后,当你使用 pttl() 方法时,你可以看到在过期前剩余的时间(以毫秒为单位)。当值过期时,键和值都会自动从数据库中删除。这个特性和 Redis 提供的对其的精细控制功能非常有用。对于简单的缓存,你可能不需要编写更多的代码就能解决问题。
值得注意的是,Redis 将其数据存储在内存中,所以请记住,数据不是持久的;如果服务器崩溃,一些数据可能会丢失。为了减轻数据丢失的可能性,Redis 提供了管理持久性的选项——从每次更改发生时将更改写入磁盘,到在预定时间进行定期快照,甚至完全不保存到磁盘。你还可以使用 Python 客户端的 save() 和 bgsave() 方法来程序性地强制保存快照,要么使用 save() 阻塞直到保存完成,要么在 bgsave() 的情况下在后台保存。
在本章中,我仅简要介绍了 Redis 可以做什么,以及它的数据类型和它可以如何操作它们。如果你有兴趣了解更多,网上有多个文档来源可供参考,包括 redislabs.com 和 redis-py.readthedocs.io。
快速检查:键值存储的用途
哪些类型的数据和应用会从像 Redis 这样的键值存储中受益最多?
23.7. MongoDB 中的文档
另一个流行的 NoSQL 数据库是 MongoDB,有时被称为基于文档的数据库,因为它不是按行和列组织,而是存储文档。MongoDB 设计为可以跨多个集群的多个节点进行扩展,同时可以处理数十亿个文档。在 MongoDB 的情况下,文档以称为 BSON(Binary JSON)的格式存储,因此文档由键值对组成,看起来像 JSON 对象或 Python 字典。以下示例展示了你可以如何使用 Python 与 MongoDB 集合和文档交互,但有一个警告是适当的。在需要数据规模和分布、高插入率、复杂和不稳定的模式等情况,MongoDB 是一个很好的选择。然而,MongoDB 在许多情况下并不是最佳选择,所以在选择之前,务必彻底调查你的需求和选项。
运行 MongoDB 服务器
与 Redis 类似,如果你想尝试 MongoDB,你需要访问一个 MongoDB 服务器。有许多云托管 MongoDB 服务可用,但如果你只是进行实验,运行一个 Docker 实例或在你自己的服务器上安装可能更好。
与 Redis 类似,最简单的解决方案是运行一个 Docker 实例。如果你有 Docker,只需在命令行中输入 > docker run -p 27017:27017 mongo 即可。在 Linux 系统上,你的包管理器应该会完成这项工作,Mac 的 brew install mongodb 也会这样做。在 Windows 系统上,请访问 www.mongodb.com 查找 Windows 版本和安装说明。与 Redis 类似,在网上搜索有关如何配置和启动服务器的任何说明。
与 Redis 类似,有几个 Python 客户端库连接到 MongoDB 数据库。为了让你了解它们的工作方式,看看 pymongo。使用 pymongo 的第一步是安装它,你可以使用 pip 来完成:
> pip install pymongo
当你安装了 pymongo 后,你可以通过创建 MongoClient 的实例并指定常规连接详情来连接到 MongoDB 服务器:
>>> from pymongo import MongoClient
>>> mongo = MongoClient(host='localhost', port=27017) *1*
- 1 主机=’localhost’ 和端口=27017 是默认值,不需要指定。
MongoDB 是按数据库组织的,其中包含集合,每个集合可以包含文档。然而,在尝试访问它们之前,不需要创建数据库和集合。如果它们不存在,当你向它们插入数据时,它们会被创建,或者如果你尝试从它们检索记录,它们会简单地返回无结果。
要测试客户端,创建一个示例文档,它可以是 Python 字典:
>>> import datetime
>>> a_document = {'name': 'Jane',
... 'age': 34,
... 'interests': ['Python', 'databases', 'statistics'],
... 'date_added': datetime.datetime.now()
... }
>>> db = mongo.my_data *1*
>>> collection = db.docs *2*
>>> collection.find_one() *3*
>>> db.collection_names()
[]
-
1 选择一个数据库(尚未创建)
-
2 选择数据库中的集合(也尚未创建)
-
3 搜索第一个项目;即使集合或数据库尚未存在,也不会抛出异常
在这里,你将连接到一个数据库和一组文档。在这种情况下,它们尚不存在,但当你访问它们时将会被创建。请注意,即使数据库和集合不存在,也没有抛出任何异常。然而,当你请求集合列表时,你得到了一个空列表,因为你的集合中还没有存储任何内容。要存储文档,请使用集合的 insert() 方法,如果操作成功,它将返回文档的唯一 ObjectId:
>>> collection.insert(a_document)
ObjectId('59701cc4f5ef0516e1da0dec') *1*
>>> db.collection_names()
['docs']
- 1 唯一的 ObjectId
现在你已经将文档存储在 docs 集合中,当你请求数据库中的集合名称时,它就会出现。当文档存储在集合中时,你可以查询它、更新它、替换它和删除它:
>>> collection.find_one() *1*
{'_id': ObjectId('59701cc4f5ef0516e1da0dec'), 'name': 'Jane', 'age': 34,
'interests': ['Python', 'databases', 'statistics'], 'date_added':
datetime.datetime(2017, 7, 19, 21, 59, 32, 752000)}
>>> from bson.objectid import ObjectId
>>> collection.find_one({"_id":ObjectId('59701cc4f5ef0516e1da0dec')}) *2*
{'_id': ObjectId('59701cc4f5ef0516e1da0dec'), 'name': 'Jane',
'age': 34, 'interests': ['Python', 'databases',
'statistics'], 'date_added': datetime.datetime(2017,
7, 19, 21, 59, 32, 752000)}
>>> collection.update_one({"_id":ObjectId('59701cc4f5ef0516e1da0dec')},
{"$set": {"name":"Ann"}}) *3*
<pymongo.results.UpdateResult object at 0x7f4ebd601d38>
>>> collection.find_one({"_id":ObjectId('59701cc4f5ef0516e1da0dec')})
{'_id': ObjectId('59701cc4f5ef0516e1da0dec'), 'name': 'Ann', 'age': 34,
'interests': ['Python', 'databases', 'statistics'], 'date_added':
datetime.datetime(2017, 7, 19, 21, 59, 32, 752000)}
>>> collection.replace_one({"_id":ObjectId('59701cc4f5ef0516e1da0dec')},
{"name":"Ann"}) *4*
<pymongo.results.UpdateResult object at 0x7f4ebd601750>
>>> collection.find_one({"_id":ObjectId('59701cc4f5ef0516e1da0dec')})
{'_id': ObjectId('59701cc4f5ef0516e1da0dec'), 'name': 'Ann'}
>>> collection.delete_one({"_id":ObjectId('59701cc4f5ef0516e1da0dec')}) *5*
<pymongo.results.DeleteResult object at 0x7f4ebd601d80>
>>> collection.find_one()
-
1 获取第一条记录
-
2 获取与指定条件匹配的记录——在这种情况下,ObjectId
-
3 根据 set 对象的内 容更新记录
-
4 用新对象替换记录
-
5 删除与指定条件匹配的记录
首先,请注意,MongoDB 根据字段及其值的字典进行匹配。字典还用于表示运算符,如 $lt(小于)和 $gt(大于),以及更新时使用的命令,如 $set。另一件要注意的事情是,即使记录已被删除且集合现在是空的,除非它被明确删除,否则它仍然存在:
>>> db.collection_names()
['docs']
>>> collection.drop()
>>> db.collection_names()
[]
当然,MongoDB 可以做很多事情。除了操作单个记录外,同一命令的版本还覆盖了多个记录,如 find_many 和 update_many。MongoDB 还支持索引以提高性能,并具有几个用于分组、计数和聚合数据的方法,以及内置的 map-reduce 方法。
快速检查:MONGODB 的使用
回顾一下你迄今为止看到的各个数据样本以及你经验中的其他类型的数据,你认为哪些适合存储在像 MongoDB 这样的数据库中?哪些显然不适合,为什么?
Lab 23: 创建数据库
选择我在过去几章中讨论过的数据集之一,并决定哪种类型的数据库最适合存储该数据。创建该数据库,并编写代码将数据加载到其中。然后选择两种最常见和/或可能使用的搜索条件类型,并编写代码以检索单个和多个匹配的记录。
摘要
-
Python 有一个数据库 API(DB-API),它为几个关系数据库的客户提供了一个一致的接口。
-
使用对象关系映射器(ORM)可以使数据库代码在数据库之间更加标准化。
-
使用对象关系映射器(ORM)还可以让你通过 Python 代码和对象而不是 SQL 查询来访问关系数据库。
-
工具如 Alembic 与 ORM 一起工作,使用代码对关系数据库模式进行可逆更改。
-
如 Redis 这样的键值存储提供快速内存数据访问。
-
MongoDB 提供了无需关系数据库严格结构的可扩展性。
第二十四章. 探索数据
本章涵盖
-
Python 处理数据的优势
-
Jupyter Notebook
-
pandas
-
数据聚合
-
使用 matplotlib 绘制的图表
在过去的几章中,我处理了一些使用 Python 获取和清理数据方面的内容。现在,是时候看看 Python 可以帮助你完成的一些操纵和探索数据的事情了。
24.1. Python 数据探索工具
在本章中,我们将探讨一些常见的 Python 数据探索工具:Jupyter 笔记本、pandas 和 matplotlib。我只会简要介绍这些工具的一些功能,但目的是给你一个可能的思路,以及一些在用 Python 探索数据时可以使用的初始工具。
24.1.1. Python 探索数据的优势
Python 已经成为数据科学领域的主要语言之一,并继续在该领域增长。然而,正如我提到的,Python 在原始性能方面并不总是最快的语言。相反,一些数据处理库,如 NumPy,大部分是用 C 编写的,并且高度优化到速度不再是问题。此外,可读性和可访问性等考虑因素往往超过纯速度;最小化开发者所需的时间通常更为重要。Python 是可读的且易于访问的,并且无论是单独使用还是与 Python 社区开发的工具结合使用,它都是操纵和探索数据的强大工具。
24.1.2. Python 可以比电子表格做得更好
电子表格几十年来一直是临时数据处理的工具。擅长使用电子表格的人可以让它们完成真正令人印象深刻的技巧:电子表格可以结合不同的但相关的数据集,使用数据透视表,使用查找表来链接数据集,等等。尽管如此,尽管人们每天都在使用它们完成大量工作,但电子表格确实存在局限性,Python 可以帮助你超越这些局限性。
我已经提到的一个限制是,大多数电子表格软件都有一个行数限制——目前大约是 100 万行,这对于许多数据集来说是不够的。另一个限制是电子表格本身的中心隐喻。电子表格是二维网格,行和列,或者最多是网格的堆叠,这限制了你可以操纵和思考复杂数据的方式。
使用 Python,你可以通过编写代码来克服电子表格的限制,并按照你想要的方式操纵数据。你可以以无数灵活的方式组合 Python 数据结构,如列表、元组、集合和字典,或者你可以创建自己的类来精确地封装数据和行为。
24.2. Jupyter Notebook
可能是使用 Python 探索数据最有说服力的工具之一,它并没有增强语言本身的功能,而是改变了你使用语言与数据交互的方式。Jupyter 笔记本是一个网络应用程序,允许你创建和分享包含实时代码、方程、可视化和解释性文本的文档。尽管现在支持多种其他语言,但它起源于与 IPython 的联系,这是科学社区为 Python 开发的替代 shell。
使 Jupyter 成为一个方便且强大的工具的原因在于,你可以通过网页浏览器与之交互。它允许你结合文本和代码,以及交互式地修改和执行你的代码。你不仅可以分块运行和修改代码,还可以与他人保存和分享笔记本。
体验 Jupyter 笔记本能做什么的最佳方式就是开始尝试使用它。在本地机器上运行 Jupyter 进程相对容易,或者你也可以访问在线版本。有关一些选项,请参阅关于运行 Jupyter 方式的侧边栏。
运行 Jupyter 的方式
Jupyter 在线: 访问 Jupyter 的在线实例是开始使用的一种最简单的方式。目前,Jupyter 背后的社区 Project Jupyter 在jupyter.org/try上提供免费的笔记本。你还可以找到其他语言的演示笔记本和内核。在撰写本文时,你还可以在 Microsoft 的 Azure 平台上访问免费的笔记本notebooks.azure.com,以及许多其他方式。
Jupyter 本地: 虽然使用在线实例非常方便,但在你的电脑上设置自己的 Jupyter 实例并不需要太多工作。通常对于本地版本,你只需将浏览器指向 localhost:8888。
如果你使用 Docker,你可以在几个容器中进行选择。要运行数据科学笔记本容器,可以使用类似以下命令:
docker run -it --rm -p 8888:8888 jupyter/datascience-notebook
如果你更愿意直接在你的系统上运行,安装和运行 Jupyter 在 virtualenv 中非常容易。
macOS 和 Linux 系统: 首先,打开一个命令窗口,并输入以下命令:
> python3 -m venv jupyter
> cd jupyter
> source bin/activate
> pip install jupyter
> jupyter-notebook
Windows 系统:
> python3 -m venv jupyter
> cd jupyter
> Scripts/bin/activate
> pip install jupyter
> Scripts/jupyter-notebook
最后一个命令应该运行 Jupyter 笔记本网络应用程序,并打开指向它的浏览器窗口。
24.2.1. 启动内核
当你在浏览器中安装、运行并打开 Jupyter 时,你需要启动一个 Python 内核。Jupyter 的一个优点是它允许你同时运行多个内核。你可以运行不同版本的 Python 内核,以及其他语言如 R、Julia 甚至 Ruby 的内核。
启动内核很简单。只需点击新建按钮,并选择 Python 3(图 24.1)。
图 24.1. 启动 Python 内核

24.2.2. 在单元格中执行代码
当你有一个正在运行的内核时,你可以开始输入和运行 Python 代码。立即,你会注意到与普通 Python 命令行的一些不同。你不会在标准 Python 命令行中看到 >>> 提示符,按 Enter 只是在单元格中添加新行。要在一个单元格中执行代码,如图 24.2 所示,选择单元格 > 运行单元格,点击按钮栏上箭头下方的运行按钮,或使用快捷键 Alt-Enter。使用 Jupyter notebook 一段时间后,Alt-Enter 键组合可能会变得非常自然。
图 24.2. 在笔记本单元格中执行代码

你可以通过在新的笔记本的第一个单元格中输入一些代码或表达式来测试其工作方式,然后按 Alt-Enter。
如你所见,任何输出都显示在单元格下方,并且会创建一个新的单元格,准备接收你的下一个输入。此外,请注意,每个已执行的单元格都会按照执行顺序进行编号。
尝试这样做:使用 Jupyter Notebook
在笔记本中输入一些代码并尝试运行它。查看编辑、单元格和内核菜单,看看有哪些选项。当你运行了一小段代码后,使用内核菜单来重启内核,重复你的步骤,然后使用单元格菜单在所有单元格中重新运行代码。
24.3. Python 和 pandas
在探索和操作数据的过程中,你会执行许多常见的操作,例如将数据加载到列表或字典中,清理数据,以及过滤数据。这些操作大多数都是经常重复的,必须按照标准模式执行,简单且通常很繁琐。如果你认为这种组合是自动化这些任务的一个强有力的理由,你并不孤单。Python 中现在用于处理数据的标准工具之一——pandas——就是为了自动化处理数据集的繁琐工作而创建的。
24.3.1. 你可能想要使用 pandas 的原因
pandas 的创建是为了通过提供一个标准的框架来存储数据,并提供方便的工具来执行频繁的操作,使得操作和解析表格或关系型数据变得容易。因此,它几乎更像是 Python 的一个扩展,而不是一个库,它改变了你与数据交互的方式。好处是,一旦你掌握了 pandas 的工作原理,你就可以做一些令人印象深刻的事情,并节省大量时间。然而,学习如何充分利用 pandas 需要时间。与许多工具一样,如果你按照 pandas 的设计目的使用它,它就会表现出色。以下章节中展示的简单示例应该能给你一个大致的概念,即 pandas 是否适合你的使用场景。
24.3.2. 安装 pandas
使用 pip 可以轻松安装 pandas。它通常与 matplotlib 一起用于绘图,因此你可以使用以下代码从 Jupyter 虚拟环境的命令行安装这两个工具:
> pip install pandas matplotlib
从 Jupyter 笔记本的一个单元格中,你可以使用
In [ ]: !pip install pandas matplotlib
如果你使用 pandas,使用以下三行会使生活变得更简单:
%matplotlib inline
import pandas as pd
import numpy as np
第一行是一个 Jupyter“魔法”函数,它使 matplotlib 能够在你的代码所在的单元格中绘制数据(这非常有用)。第二行使用别名 pd 导入 pandas,这使得输入更简单,并且是 pandas 用户中常见的做法;最后一行也导入了 numpy。尽管 pandas 在很大程度上依赖于 numpy,但在下面的例子中你不会明确使用它,但养成导入它的习惯是合理的。
24.3.3. 数据框
使用 pandas,你可以得到一个基本结构,即数据框。数据框是一个二维网格,与内存中的关系数据库表非常相似。创建数据框很容易;给它一些数据。为了使事情尽可能简单,以一个 3×3 的数字网格作为第一个例子。在 Python 中,这样的网格是列表的列表:
grid = [[1,2,3], [4,5,6], [7,8,9]]
print(grid)
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
很遗憾,在 Python 中,除非你做出额外的努力,否则网格看起来不会像网格。所以看看你能否用同一个网格作为 pandas 数据框来做什么:
import pandas as pd
df = pd.DataFrame(grid)
print(df)
0 1 2
0 1 2 3
1 4 5 6
2 7 8 9
那段代码相当简单;你所需要做的只是将你的网格转换成数据框。你现在有了更网格化的显示,并且现在你既有行号也有列号。当然,跟踪列号是什么往往相当麻烦,所以给你的列起个名字:
df = pd.DataFrame(grid, columns=["one", "two", "three"] )
print(df)
one two three
0 1 2 3
1 4 5 6
2 7 8 9
你可能会想知道给列命名是否有任何好处,但列名可以用另一个 pandas 技巧来使用:通过名称选择列的能力。例如,如果你想只获取列"two"的内容,你可以非常简单地得到它:
print(df["two"])
0 2
1 5
2 8
Name: two, dtype: int64
这里,与 Python 相比,你已经节省了时间。要只获取网格的第二列,你需要使用列表推导,同时记得使用零基索引(而且你仍然不会得到那么好的输出):
print([x[1] for x in grid])
[2, 5, 8]
你可以像使用推导得到的列表一样轻松地遍历数据框的列值:
for x in df["two"]:
print(x)
2
5
8
这对于开始来说还不错,但通过使用双括号中的列列表,你可以做得更好,得到另一个数据框的数据子集。而不是获取中间列,获取你的数据框的第一列和最后一列作为另一个数据框:
edges = df[["one", "three"]]
print(edges)
one three
0 1 3
1 4 6
2 7 9
数据框也有几个方法,可以对框架中的每个项目应用相同的操作和参数。如果你想将数据框边缘的每个项目加 2,你可以使用add()方法:
print(edges.add(2))
one three
0 3 5
1 6 8
2 9 11
在这里,同样可以通过使用列表推导和/或嵌套循环来得到相同的结果,但这些技术并不那么方便。很容易看出这样的功能可以使生活变得更轻松,尤其是对于那些对数据包含的信息比对操作过程更感兴趣的人。
24.4. 数据清洗
在前面的章节中,我讨论了几种使用 Python 清理数据的方法。现在,我已经将 pandas 添加到其中,我将展示如何使用其功能来清理数据。在介绍以下操作时,我也提到了在纯 Python 中执行相同操作的方法,既为了说明使用 pandas 的不同之处,也为了展示为什么 pandas 并不适合每个用例(或者用户,无论如何)。
24.4.1. 使用 pandas 加载和保存数据
pandas 有一系列令人印象深刻的方法可以从不同的来源加载数据。它支持多种文件格式(包括固定宽度和分隔文本文件、电子表格、JSON、XML 和 HTML),但也可以从 SQL 数据库、Google BigQuery、HDF 甚至剪贴板数据中读取。你应该意识到,许多这些操作实际上并不是 pandas 本身的一部分;pandas 依赖于安装其他库来处理这些操作,例如使用 SQLAlchemy 从 SQL 数据库读取。这种区别在出现问题的时候很重要;很多时候,需要解决的问题在 pandas 之外,你只能处理底层库。
使用read_json()方法读取 JSON 文件很简单:
mars = pd.read_json("mars_data_01.json")
这段代码会给你一个如下所示的数据框:
report
abs_humidity None
atmo_opacity Sunny
ls 296
max_temp -1
max_temp_fahrenheit 30.2
min_temp -72
min_temp_fahrenheit -97.6
pressure 869
pressure_string Higher
season Month 10
sol 1576
sunrise 2017-01-11T12:31:00Z
sunset 2017-01-12T00:46:00Z
terrestrial_date 2017-01-11
wind_direction --
wind_speed None
为了展示如何简单地将数据读入 pandas,从第二十一章的温度数据 CSV 文件和第二十二章中使用的火星天气数据 JSON 文件加载数据。在第一种情况下,使用read_csv()方法:
temp = pd.read_csv("temp_data_01.csv")
4 5 6 7 8 9 10 11 12 13 14 \ *1*
0 1979/01/01 17.48 994 6.0 30.5 2.89 994 -13.6 15.8 NaN 0
1 1979/01/02 4.64 994 -6.4 15.8 -9.03 994 -23.6 6.6 NaN 0
2 1979/01/03 11.05 994 -0.7 24.7 -2.17 994 -18.3 12.9 NaN 0
3 1979/01/04 9.51 994 0.2 27.6 -0.43 994 -16.3 16.3 NaN 0
4 1979/05/15 68.42 994 61.0 75.1 51.30 994 43.3 57.0 NaN 0
5 1979/05/16 70.29 994 63.4 73.5 48.09 994 41.1 53.0 NaN 0
6 1979/05/17 75.34 994 64.0 80.5 50.84 994 44.3 55.7 82.60 2
7 1979/05/18 79.13 994 75.5 82.1 55.68 994 50.0 61.1 81.42 349
8 1979/05/19 74.94 994 66.9 83.1 58.59 994 50.9 63.2 82.87 78
15 16 17
0 NaN NaN 0.0000
1 NaN NaN 0.0000
2 NaN NaN 0.0000
3 NaN NaN 0.0000
4 NaN NaN 0.0000
5 NaN NaN 0.0000
6 82.4 82.8 0.0020
7 80.2 83.4 0.3511
8 81.6 85.2 0.0785
- 1 注意,标题行末尾的\是一个指示,表明表格太长,无法在一行中打印,更多列将在下面打印。
显然,单步加载文件很吸引人,你可以看到 pandas 没有在加载文件时遇到任何问题。你也可以看到空的第一列已经被转换为NaN(不是一个数字)。你仍然有一些值存在'Missing'的问题,实际上,将那些'Missing'值转换为NaN可能是有意义的:
temp = pd.read_csv("temp_data_01.csv", na_values=['Missing'])
na_values参数的添加控制了在加载时哪些值会被转换为NaN。在这种情况下,你添加了字符串'Missing',因此数据框的行从
NaN Illinois 17 Jan 01, 1979 1979/01/01 17.48 994 6.0 30.5 2.89994
-13.6 15.8 Missing 0 Missing Missing 0.00%
转换为
NaN Illinois 17 Jan 01, 1979 1979/01/01 17.48 994 6.0 30.5 2.89994
-13.6 15.8 NaN0 NaN NaN 0.00%
如果你有那种数据文件,无论出于什么原因,“无数据”以各种方式表示:NA、N/A、?、-等等,这种技术特别有用。为了处理这种情况,你可以检查数据以找出使用了什么,然后重新加载它,使用na_values参数将所有这些变化标准化为NaN。
保存数据
如果你想要保存数据框的内容,pandas 数据框有一系列广泛的方法。如果你有一个简单的网格数据框,你可以以几种方式写入它。这一行
df.to_csv("df_out.csv", index=False) *1*
- 1 将索引设置为 False 意味着行索引将不会被写入。
写入一个如下所示的文件:
one,two,three
1,2,3
4,5,6
7,8,9
同样,你可以将数据网格转换为 JSON 对象或将它写入文件:
df.to_json() *1*
'{"one":{"0":1,"1":4,"2":7},"two":{"0":2,"1":5,"2":8},"three":{"0":3,"1":6,"2
":9}}'
- 1 将文件路径作为参数传递会将 JSON 写入该文件而不是返回它。
24.4.2. 使用数据框进行数据清洗
在加载时将特定的一组值转换为 NaN 是 pandas 使数据清洗变得非常简单的一个小技巧。超出这个范围,数据框支持几个操作,可以使数据清洗变得不那么繁琐。为了了解这是如何工作的,重新打开温度 CSV 文件,但这次,不是使用标题来命名列,而是使用 range() 函数和 names 参数来给它们编号,这将使引用它们更容易。你也许还记得,从之前的例子中,每一行的第一个字段——“Notes”字段是空的,并加载了 NaN 值。虽然你可以忽略这个列,但如果没有它会更简单。你可以再次使用 range() 函数,这次从 1 开始,告诉 pandas 加载除了第一个列之外的所有列。但如果你知道所有值都来自伊利诺伊州,并且你不在乎长格式日期字段,你可以从 4 开始,使事情变得更容易管理:
temp = pd.read_csv("temp_data_01.csv", na_values=['Missing'], header=0,
names=range(18), usecols=range(4,18)) *1*
print(temp)
4 5 6 7 8 9 10 11 12 13 14 \
0 1979/01/01 17.48 994 6.0 30.5 2.89 994 -13.6 15.8 NaN 0
1 1979/01/02 4.64 994 -6.4 15.8 -9.03 994 -23.6 6.6 NaN 0
2 1979/01/03 11.05 994 -0.7 24.7 -2.17 994 -18.3 12.9 NaN 0
3 1979/01/04 9.51 994 0.2 27.6 -0.43 994 -16.3 16.3 NaN 0
4 1979/05/15 68.42 994 61.0 75.1 51.30 994 43.3 57.0 NaN 0
5 1979/05/16 70.29 994 63.4 73.5 48.09 994 41.1 53.0 NaN 0
6 1979/05/17 75.34 994 64.0 80.5 50.84 994 44.3 55.7 82.60 2
7 1979/05/18 79.13 994 75.5 82.1 55.68 994 50.0 61.1 81.42 349
8 1979/05/19 74.94 994 66.9 83.1 58.59 994 50.9 63.2 82.87 78
15 16 17
0 NaN NaN 0.00%
1 NaN NaN 0.00%
2 NaN NaN 0.00%
3 NaN NaN 0.00%
4 NaN NaN 0.00%
5 NaN NaN 0.00%
6 82.4 82.8 0.20%
7 80.2 83.4 35.11%
8 81.6 85.2 7.85%
- 1 设置 header=0 会关闭读取列标签的标题。
现在你有一个只包含你可能想要处理的列的数据框。但你仍然有一个问题:最后一个列,它列出了热指数的覆盖率百分比,仍然是一个以百分号结尾的字符串,而不是实际的百分比。如果你查看第一行的列 17 的值,这个问题就会很明显:
temp[17][0]
'0.00%'
要解决这个问题,你需要做两件事:从值末尾移除 %,然后将值从字符串转换为数字。如果想要将得到的百分比表示为分数,你需要将其除以 100。第一部分很简单,因为 pandas 允许你用一个命令在列上重复操作:
temp[17] = temp[17].str.strip("%")
temp[17][0]
'0.00'
这段代码对列进行操作,并在其上调用一个字符串 strip() 操作来移除尾部的 %。现在当你查看该列的第一个值(或任何其他值)时,你会看到那个令人讨厌的百分号已经消失了。还值得注意的是,你可以使用其他操作,例如 replace("%", ""),来达到相同的结果。
第二个操作是将字符串转换为数值。同样,pandas 允许你用一个命令来完成这个操作:
temp[17] = pd.to_numeric(temp[17])
temp[17][0]
0.0
现在列 17 中的值是数值型的,如果你想的话,可以使用 div() 方法来完成将那些值转换为分数的工作:
temp[17] = temp[17].div(100)
temp[17]
0 0.0000
1 0.0000
2 0.0000
3 0.0000
4 0.0000
5 0.0000
6 0.0020
7 0.3511
8 0.0785
Name: 17, dtype: float64
实际上,通过将三个操作串联在一起,可以在一行中实现相同的结果:
temp[17] = pd.to_numeric(temp[17].str.strip("%")).div(100)
这个例子很简单,但它给你一个关于 pandas 如何方便地清理数据的想法。pandas 具有广泛的数据转换操作,以及使用自定义函数的能力,因此很难想象一个无法通过 pandas 简化数据清理的场景。
尽管选项数量几乎令人难以承受,但仍然有各种各样的教程和视频可供选择,pandas.pydata.org上的文档也非常出色。
尝试这个:使用和不用 pandas 清理数据
尝试这些操作。当最终列已转换为分数时,你能想到一种方法将其转换回带有百分号后缀的字符串吗?
相比之下,通过使用csv模块将相同的数据加载到普通的 Python 列表中,并使用纯 Python 应用相同的更改。
24.5. 数据聚合和处理
前面的例子可能让你对 pandas 提供的许多选项有了些了解,这些选项只需几个命令就可以在数据上执行相当复杂的操作。正如你所期望的,这种功能级别也适用于数据聚合。在本节中,我将通过几个简单的数据聚合示例来展示许多可能性。尽管有许多选项可用,但我专注于合并数据框、执行简单的数据聚合以及分组和过滤。
24.5.1. 合并数据框
在处理数据的过程中,你经常需要关联两个数据集。假设你有一个文件包含销售团队成员每月进行的销售电话次数,在另一个文件中,你有他们各自地区销售的美元金额:
calls = pd.read_csv("sales_calls.csv")
print(calls)
Team member Territory Month Calls
0 Jorge 3 1 107
1 Jorge 3 2 88
2 Jorge 3 3 84
3 Jorge 3 4 113
4 Ana 1 1 91
5 Ana 1 2 129
6 Ana 1 3 96
7 Ana 1 4 128
8 Ali 2 1 120
9 Ali 2 2 85
10 Ali 2 3 87
11 Ali 2 4 87
revenue = pd.read_csv("sales_revenue.csv")
print(revenue)
Territory Month Amount
0 1 1 54228
1 1 2 61640
2 1 3 43491
3 1 4 52173
4 2 1 36061
5 2 2 44957
6 2 3 35058
7 2 4 33855
8 3 1 50876
9 3 2 57682
10 3 3 53689
11 3 4 49173
显然,将收入和团队成员活动联系起来将非常有用。这两个文件非常简单,但用纯 Python 合并它们并不完全简单。pandas 有一个用于合并两个数据框的函数:
calls_revenue = pd.merge(calls, revenue, on=['Territory', 'Month'])
merge函数通过在列字段中指定的列将两个数据框连接起来创建一个新的数据框。merge函数的工作方式类似于关系型数据库的连接,为你提供一个结合了两个文件列的表:
print(calls_revenue)
Team member Territory Month Calls Amount
0 Jorge 3 1 107 50876
1 Jorge 3 2 88 57682
2 Jorge 3 3 84 53689
3 Jorge 3 4 113 49173
4 Ana 1 1 91 54228
5 Ana 1 2 129 61640
6 Ana 1 3 96 43491
7 Ana 1 4 128 52173
8 Ali 2 1 120 36061
9 Ali 2 2 85 44957
10 Ali 2 3 87 35058
11 Ali 2 4 87 33855
在这种情况下,两个字段中的行之间存在一对一的对应关系,但merge函数也可以执行一对一和多对多连接,以及右连接和左连接。
快速检查:合并数据集
你会如何处理像 Python 示例中的数据集合并?
24.5.2. 选择数据
根据某些条件选择或过滤数据框中的行也可能很有用。在示例销售数据中,你可能只想查看地区 3,这也很简单:
print(calls_revenue[calls_revenue.Territory==3])
Team member Territory Month Calls Amount
0 Jorge 3 1 107 50876
1 Jorge 3 2 88 57682
2 Jorge 3 3 84 53689
3 Jorge 3 4 113 49173
在这个例子中,您只选择地区等于 3 的行,但使用恰好那个表达式,revenue.Territory==3,作为数据框的索引。从纯 Python 的角度来看,这种用法是没有意义且非法的,但对于 pandas 数据框来说,它是有效的,并且使得表达式更加简洁。
当然,也允许更复杂的表达式。如果您想选择通话金额大于 500 的行,您可以使用这个表达式代替:
print(calls_revenue[calls_revenue.Amount/calls_revenue.Calls>500])
Team member Territory Month Calls Amount
1 Jorge 3 2 88 57682
2 Jorge 3 3 84 53689
4 Ana 1 1 91 54228
9 Ali 2 2 85 44957
更好的是,您可以使用类似的操作计算并添加该列到您的数据框中:
calls_revenue['Call_Amount'] = calls_revenue.Amount/calls_revenue.Calls
print(calls_revenue)
Team member Territory Month Calls Amount Call_Amount
0 Jorge 3 1 107 50876 475.476636
1 Jorge 3 2 88 57682 655.477273
2 Jorge 3 3 84 53689 639.154762
3 Jorge 3 4 113 49173 435.159292
4 Ana 1 1 91 54228 595.912088
5 Ana 1 2 129 61640 477.829457
6 Ana 1 3 96 43491 453.031250
7 Ana 1 4 128 52173 407.601562
8 Ali 2 1 120 36061 300.508333
9 Ali 2 2 85 44957 528.905882
10 Ali 2 3 87 35058 402.965517
11 Ali 2 4 87 33855 389.137931
再次注意,pandas 的内置逻辑替换了纯 Python 中更繁琐的结构。
快速检查:在 Python 中选择
您会使用什么 Python 代码结构来选择仅满足某些条件的行?
24.5.3. 分组和聚合
如您所预期,pandas 有很多工具可以总结和聚合数据。特别是,从列中获取总和、平均值、中位数、最小值和最大值使用明确命名的列方法:
print(calls_revenue.Calls.sum())
print(calls_revenue.Calls.mean())
print(calls_revenue.Calls.median())
print(calls_revenue.Calls.max())
print(calls_revenue.Calls.min())
1215
101.25
93.5
129
84
例如,如果您想获取所有通话金额超过中值的行,您可以结合这个技巧与选择操作:
print(calls_revenue.Call_Amount.median())
print(calls_revenue[calls_revenue.Call_Amount >=
calls_revenue.Call_Amount.median()])
464.2539427570093
Team member Territory Month Calls Amount Call_Amount
0 Jorge 3 1 107 50876 475.476636
1 Jorge 3 2 88 57682 655.477273
2 Jorge 3 3 84 53689 639.154762
4 Ana 1 1 91 54228 595.912088
5 Ana 1 2 129 61640 477.829457
9 Ali 2 2 85 44957 528.905882
除了能够挑选出汇总值之外,根据其他列分组数据通常也很有用。在这个简单的例子中,您可以使用groupby方法来分组您的数据。例如,您可能想知道按月份或地区汇总的总通话次数和金额。在这些情况下,使用这些字段与数据框的groupby方法:
print(calls_revenue[['Month', 'Calls', 'Amount']].groupby(['Month']).sum())
Calls Amount
Month
1 318 141165
2 302 164279
3 267 132238
4 328 135201
print(calls_revenue[['Territory', 'Calls',
'Amount']].groupby(['Territory']).sum())
Calls Amount
Territory
1 444 211532
2 379 149931
3 392 211420
在每种情况下,您选择要聚合的列,按这些列中的一个列的值分组,然后(在这种情况下)对每个组求和。您也可以使用本章前面提到的其他任何方法。
再次,所有这些例子都很简单,但它们说明了您使用 pandas 操作和选择数据的一些选项。如果这些想法与您的需求相呼应,您可以通过研究 pandas 文档在pandas.pydata.org上了解更多信息。
尝试这个:分组和聚合
在 pandas 和前例中的数据上实验。您能否按团队成员和月份获取通话和金额?
24.6. 绘制数据
pandas 的另一个非常吸引人的特性是能够非常容易地在数据框中绘制数据。尽管您在 Python 和 Jupyter 笔记本中有很多绘制数据的选择,但 pandas 可以直接从数据框中使用 matplotlib。您可能还记得,当您开始 Jupyter 会话时,您给出的第一个命令之一是启用 matplotlib 进行内联绘图的 Jupyter“魔法”命令:
%matplotlib inline
因为您有绘图的能力,看看您如何绘制一些数据(图 24.3)。继续使用销售示例,如果您想按地区绘制季度的平均销售额,您只需在笔记本中添加 .plot.bar() 就可以立即得到一个图表:
calls_revenue[['Territory', 'Calls']].groupby(['Territory']).sum().plot.bar()
图 24.3. Jupyter 笔记本中的 pandas 数据框条形图

其他选项也是可用的。plot()单独使用或.plot.line()创建折线图,.plot.pie()创建饼图,等等。
多亏了 pandas 和 matplotlib 的结合,在 Jupyter 笔记本中绘制数据变得相当容易。我也应该指出,尽管这种绘图很容易,但它并不擅长做很多事情。
尝试这个:绘图
绘制每月平均通话金额的折线图。
24.7. 为什么你可能不想使用 pandas
上述示例仅展示了 pandas 在数据清洗、探索和操作方面可以为你提供的工具的一小部分。正如我在本章开头提到的,pandas 是一个出色的工具集,擅长它被设计去做的任务。但这并不意味着 pandas 是所有情况或所有人的工具。
你可能会选择使用传统的 Python(或其他工具)而不是它。一方面,正如我之前提到的,学会完全使用 pandas 在某种程度上就像学习另一种语言,这可能不是你愿意或有时间去做的事情。此外,pandas 在所有生产环境中可能并不理想,尤其是在处理不需要太多数学运算的大数据集或难以用 pandas 最佳格式处理的数据时。例如,对大量产品信息的整理可能不会从 pandas 中获得太多好处;对交易流的简单处理也不会。
重点是,你应该根据手头的问题仔细选择你的工具。在许多情况下,pandas 确实会使你在处理数据时生活变得更轻松,但在其他情况下,传统的 Python 可能才是你的最佳选择。
摘要
-
Python 在数据处理方面提供了许多好处,包括处理非常大的数据集的能力以及以符合你需求的方式处理数据的能力。
-
Jupyter 笔记本是一种通过网页浏览器访问 Python 的有用方式,这也使得改进演示变得更容易。
-
pandas 是一个使许多常见数据处理操作变得容易的工具,包括数据清洗、合并和汇总。
-
pandas 还使简单的绘图变得容易。
案例研究
在这个案例研究中,你将使用 Python 获取一些数据,清理它,然后绘制图表。这个项目可能是一个短项目,但它结合了我所讨论的语言的几个特性,并给你一个从头到尾完成项目的机会。在几乎每一个步骤中,我都会简要指出你可以做出的替代方案和改进。
全球温度变化是许多讨论的主题,但这些讨论都是基于全球规模的。假设你想知道你所在地区的温度变化情况。一种了解的方法是获取你所在位置的历史数据,处理这些数据,并绘制图表以查看确切发生了什么。
获取案例研究代码
以下案例研究是通过使用 Jupyter 笔记本完成的,如第二十四章所述。第二十四章。如果你使用 Jupyter,你可以在源代码下载中找到我使用的笔记本(包含此文本和代码),作为Case Study.ipynb。你还可以在标准的 Python shell 中执行代码,支持该 shell 的版本在源代码中作为Case Study.py。
幸运的是,有几个历史天气数据来源是免费提供的。我将带你通过使用全球历史气候网络的数据,这个网络拥有来自世界各地的数据。你可能还会找到其他来源,它们可能具有不同的数据格式,但我在这里讨论的步骤和过程应该适用于任何数据集。
下载数据
第一步将是获取数据。在www1.ncdc.noaa.gov/pub/data/ghcn/daily/的存档中有大量的每日历史天气数据。第一步是确定你想要哪些文件以及它们的确切位置;然后下载它们。当你有了数据后,你可以继续处理,最终显示你的结果。
要下载这些文件,这些文件可以通过 HTTPS 访问,你需要requests库。你可以在命令提示符下使用pip install requests来获取requests。当你有了requests后,你的第一步是获取 readme.txt 文件,它可以指导你了解所需数据文件的格式和位置:
# import requests
import requests
# get readme.txt file
r = requests.get('https://www1.ncdc.noaa.gov/pub/data/ghcn/daily/readme.txt')
readme = r.text.
当你查看 readme 文件时,你应该会看到类似以下内容:
print(readme)
README FILE FOR DAILY GLOBAL HISTORICAL CLIMATOLOGY NETWORK (GHCN-DAILY)
Version 3.22
----------------------------------------------------------------------------
How to cite:
Note that the GHCN-Daily dataset itself now has a DOI (Digital Object Identifier)
so it may be relevant to cite both the methods/overview journal article as well
as the specific version of the dataset used.
The journal article describing GHCN-Daily is:
Menne, M.J., I. Durre, R.S. Vose, B.E. Gleason, and T.G. Houston, 2012: An
overview
of the Global Historical Climatology Network-Daily Database. Journal of
Atmospheric
and Oceanic Technology, 29, 897-910, doi:10.1175/JTECH-D-11-00103.1.
To acknowledge the specific version of the dataset used, please cite:
Menne, M.J., I. Durre, B. Korzeniewski, S. McNeal, K. Thomas, X. Yin, S.
Anthony, R. Ray,
R.S. Vose, B.E.Gleason, and T.G. Houston, 2012: Global Historical Climatology
Network -
Daily (GHCN-Daily), Version 3\. [indicate subset used following decimal,
e.g. Version 3.12].
NOAA National Climatic Data Center. http://doi.org/10.7289/V5D21VHZ [access
date].
尤其是你对第 II 部分感兴趣,其中列出了内容:
II. CONTENTS OF ftp://ftp.ncdc.noaa.gov/pub/data/ghcn/daily
all: Directory with ".dly" files for all of GHCN-Daily
gsn: Directory with ".dly" files for the GCOS Surface Network
(GSN)
hcn: Directory with ".dly" files for U.S. HCN
by_year: Directory with GHCN Daily files parsed into yearly
subsets with observation times where available. See the
/by_year/readme.txt and
/by_year/ghcn-daily-by_year-format.rtf
files for further information
grid: Directory with the GHCN-Daily gridded dataset known
as HadGHCND
papers: Directory with pdf versions of journal articles relevant
to the GHCN-Daily dataset
figures: Directory containing figures that summarize the inventory
of GHCN-Daily station records
ghcnd-all.tar.gz: TAR file of the GZIP-compressed files in the "all"
directory
ghcnd-gsn.tar.gz: TAR file of the GZIP-compressed "gsn" directory
ghcnd-hcn.tar.gz: TAR file of the GZIP-compressed "hcn" directory
ghcnd-countries.txt: List of country codes (FIPS) and names
ghcnd-inventory.txt: File listing the periods of record for each station and
element
ghcnd-stations.txt: List of stations and their metadata (e.g., coordinates)
ghcnd-states.txt: List of U.S. state and Canadian Province codes
used in ghcnd-stations.txt
ghcnd-version.txt: File that specifies the current version of GHCN Daily
readme.txt: This file
status.txt: Notes on the current status of GHCN-Daily
当你查看可用的文件时,你会看到 ghcnd-inventory.txt 列出了每个站点的记录周期,这将帮助你找到一个好的数据集;而 ghcnd-stations.txt 列出了站点,这将帮助你找到离你位置最近的站点,所以你首先会获取这两个文件:
II. CONTENTS OF ftp://ftp.ncdc.noaa.gov/pub/data/ghcn/daily
all: Directory with ".dly" files for all of GHCN-Daily
gsn: Directory with ".dly" files for the GCOS Surface
Network
(GSN)
hcn: Directory with ".dly" files for U.S. HCN
by_year: Directory with GHCN Daily files parsed into yearly
subsets with observation times where available. See
the
/by_year/readme.txt and
/by_year/ghcn-daily-by_year-format.rtf
files for further information
grid: Directory with the GHCN-Daily gridded dataset known
as HadGHCND
papers: Directory with pdf versions of journal articles relevant
to the GHCN-Daily dataset
figures: Directory containing figures that summarize the inventory
of GHCN-Daily station records
ghcnd-all.tar.gz: TAR file of the GZIP-compressed files in the "all"
directory
ghcnd-gsn.tar.gz: TAR file of the GZIP-compressed "gsn" directory
ghcnd-hcn.tar.gz: TAR file of the GZIP-compressed "hcn" directory
ghcnd-countries.txt: List of country codes (FIPS) and names
ghcnd-inventory.txt: File listing the periods of record for each station and
element
ghcnd-stations.txt: List of stations and their metadata (e.g., coordinates)
ghcnd-states.txt: List of U.S. state and Canadian Province codes
used in ghcnd-stations.txt
ghcnd-version.txt: File that specifies the current version of GHCN Daily
readme.txt: This file
status.txt: Notes on the current status of GHCN-Daily
# get inventory and stations files
r = requests.get('https://www1.ncdc.noaa.gov/pub/data/ghcn/daily/ghcnd-
inventory.txt')
inventory_txt = r.text
r = requests.get('https://www1.ncdc.noaa.gov/pub/data/ghcn/daily/ghcnd-
stations.txt')
stations_txt = r.text
当你有了这些文件后,你可以将它们保存到本地磁盘上,这样你就不需要再次下载它们,如果你需要回到原始数据:
# save both the inventory and stations files to disk, in case we need them
with open("inventory.txt", "w") as inventory_file:
inventory_file.write(inventory_txt)
with open("stations.txt", "w") as stations_file:
stations_file.write(stations_txt)
首先,查看库存文件。以下是你前 137 个字符所显示的内容:
print(inventory_txt[:137])
ACW00011604 17.1167 -61.7833 TMAX 1949 1949
ACW00011604 17.1167 -61.7833 TMIN 1949 1949
ACW00011604 17.1167 -61.7833 PRCP 1949 1949
If we look at section VII of the readme.txt file we can see that the format
of the inventory file is:
VII. FORMAT OF "ghcnd-inventory.txt"
------------------------------
Variable Columns Type
------------------------------
ID 1-11 Character
LATITUDE 13-20 Real
LONGITUDE 22-30 Real
ELEMENT 32-35 Character
FIRSTYEAR 37-40 Integer
LASTYEAR 42-45 Integer
------------------------------
这些变量有以下定义:
ID is the station identification code. Please see "ghcnd-stations.txt"
for a complete list of stations and their metadata.
LATITUDE is the latitude of the station (in decimal degrees).
LONGITUDE is the longitude of the station (in decimal degrees).
ELEMENT is the element type. See section III for a definition of elements.
FIRSTYEAR is the first year of unflagged data for the given element.
LASTYEAR is the last year of unflagged data for the given element.
从这个描述中,你可以看出库存列表包含了你寻找所需站点所需的大部分信息。你可以使用纬度和经度找到离你最近的站点;然后你可以使用 FIRSTYEAR 和 LASTYEAR 字段找到记录跨度较长的站点。
剩下的唯一问题是 ELEMENT 字段是什么;为此,文件建议你查看第 III 部分。在第 III 部分(我稍后会更详细地查看),你可以找到以下主要元素的描述:
ELEMENT is the element type. There are five core elements as well as a
number of addition elements.
The five core elements are:
PRCP = Precipitation (tenths of mm)
SNOW = Snowfall (mm)
SNWD = Snow depth (mm)
TMAX = Maximum temperature (tenths of degrees C)
TMIN = Minimum temperature (tenths of degrees C)
为了这个示例的目的,你感兴趣的是 TMAX 和 TMIN 元素,它们是以十分之一摄氏度为单位的最高和最低温度。
解析库存数据
readme.txt 文件告诉你库存文件中有哪些内容,以便你可以将数据解析成更易用的格式。你可以只是将解析后的库存数据存储为列表的列表或列表的元组,但只需多花一点力气,就可以使用 collections 库中的namedtuple创建一个具有以下属性命名的自定义类:
# parse to named tuples
# use namedtuple to create a custom Inventory class
from collections import namedtuple
Inventory = namedtuple("Inventory", ['station', 'latitude', 'longitude',
'element', 'start', 'end'])
使用你创建的Inventory类非常简单;你只需从适当的值创建每个实例,在这种情况下,是解析后的库存数据行。
解析涉及两个步骤。首先,你需要根据指定的字段大小挑选出行的片段。当你查看 readme 文件中的字段描述时,也很清楚文件之间有一个额外的空格,这在制定任何解析方法时需要考虑。在这种情况下,因为你指定了每个片段,所以额外的空格被忽略。此外,因为 STATION 和 ELEMENT 字段的大小与其中存储的值完全对应,所以你不需要担心从它们中去除多余的空格。
第二件好事是将纬度和经度值转换为浮点数,将起始和结束年份转换为整数。你可以在数据清理的后期阶段做这件事,实际上,如果数据不一致且不是每一行都能正确转换的值,你可能需要等待。但在这个案例中,数据允许你在解析步骤中处理这些转换,所以现在就做吧:
# parse inventory lines and convert some values to floats and ints
inventory = [Inventory(x[0:11], float(x[12:20]), float(x[21:30]), x[31:35],
int(x[36:40]), int(x[41:45]))
for x in inventory_txt.split("\n") if x.startswith("US")]
for line in inventory[:5]:
print(line)
Inventory(station='US009052008', latitude=43.7333, longitude=-96.6333,
element='TMAX', start=2008, end=2016)
Inventory(station='US009052008', latitude=43.7333, longitude=-96.6333,
element='TMIN', start=2008, end=2016)
Inventory(station='US009052008', latitude=43.7333, longitude=-96.6333,
element='PRCP', start=2008, end=2016)
Inventory(station='US009052008', latitude=43.7333, longitude=-96.6333,
element='SNWD', start=2009, end=2016)
Inventory(station='US10RMHS145', latitude=40.5268, longitude=-105.1113,
element='PRCP', start=2004, end=2004)
根据纬度和经度选择站点
现在库存已加载,你可以使用纬度和经度找到离你位置最近的站点,然后根据起始和结束年份选择温度记录最长的站点。即使在数据的第一行,你也能看到两个需要注意的地方:
-
有各种元素类型,但你只需关注 TMIN 和 TMAX,分别代表最低和最高温度。
-
你看到的第一个库存条目没有覆盖超过几年。如果你要寻找历史视角,你想要找到更长时间的温度数据序列。
为了快速挑选出你需要的内容,我们可以使用列表推导来创建一个子列表,其中只包含元素为 TMIN 或 TMAX 的站点清单项。你关心的另一件事是获取一个数据序列较长的站点,所以在创建这个子列表的同时,也要确保起始年份在 1920 年之前,结束年份至少在 2015 年。这样,你只查看至少有 95 年数据的站点:
inventory_temps = [x for x in inventory if x.element in ['TMIN', 'TMAX']
and x.end >= 2015 and x.start < 1920]
inventory_temps[:5]
[Inventory(station='USC00010252', latitude=31.3072, longitude=-86.5225,
element='TMAX', start=1912, end=2017),
Inventory(station='USC00010252', latitude=31.3072, longitude=-86.5225,
element='TMIN', start=1912, end=2017),
Inventory(station='USC00010583', latitude=30.8839, longitude=-87.7853,
element='TMAX', start=1915, end=2017),
Inventory(station='USC00010583', latitude=30.8839, longitude=-87.7853,
element='TMIN', start=1915, end=2017),
Inventory(station='USC00012758', latitude=31.445, longitude=-86.9533,
element='TMAX', start=1890, end=2017)]
查看你新列表的前五条记录,你会发现你的情况更好。现在你只有温度记录,起始年和结束年显示你有更长的记录序列。
这就留下了选择离你位置最近的站点的问题。为了做到这一点,比较站点清单的纬度和经度与你的位置的纬度和经度。获取任何地方的纬度和经度有多种方法,但可能最简单的方法是使用在线地图应用或在线搜索。(当我为芝加哥 Loop 做这件事时,我得到了纬度 41.882 和经度-87.629。)
由于你对离你位置最近的站点感兴趣,这种兴趣意味着根据站点的纬度和经度与你的位置的接近程度进行排序。排序列表并不难,按纬度和经度排序也不太困难。但你是如何按纬度和经度之间的距离进行排序的呢?
答案是为你的排序定义一个键函数,该函数获取你的纬度与站点的纬度之间的差异,以及你的经度与站点的经度之间的差异,并将它们合并成一个数字。唯一要记住的另一件事是,在合并之前,你想要添加差异的绝对值,以避免将一个高负差异与一个同样高的正差异结合起来,这可能会欺骗你的排序:
# Downtown Chicago, obtained via online map
latitude, longitude = 41.882, -87.629
inventory_temps.sort(key=lambda x: abs(latitude-x.latitude) + abs(longitude-x.longitude))
inventory_temps[:20]
Out[24]:
[Inventory(station='USC00110338', latitude=41.7806, longitude=-88.3092,
element='TMAX', start=1893, end=2017),
Inventory(station='USC00110338', latitude=41.7806, longitude=-88.3092,
element='TMIN', start=1893, end=2017),
Inventory(station='USC00112736', latitude=42.0628, longitude=-88.2861,
element='TMAX', start=1897, end=2017),
Inventory(station='USC00112736', latitude=42.0628, longitude=-88.2861,
element='TMIN', start=1897, end=2017),
Inventory(station='USC00476922', latitude=42.7022, longitude=-87.7861,
element='TMAX', start=1896, end=2017),
Inventory(station='USC00476922', latitude=42.7022, longitude=-87.7861,
element='TMIN', start=1896, end=2017),
Inventory(station='USC00124837', latitude=41.6117, longitude=-86.7297,
element='TMAX', start=1897, end=2017),
Inventory(station='USC00124837', latitude=41.6117, longitude=-86.7297,
element='TMIN', start=1897, end=2017),
Inventory(station='USC00119021', latitude=40.7928, longitude=-87.7556,
element='TMAX', start=1893, end=2017),
Inventory(station='USC00119021', latitude=40.7928, longitude=-87.7556,
element='TMIN', start=1894, end=2017),
Inventory(station='USC00115825', latitude=41.3708, longitude=-88.4336,
element='TMAX', start=1912, end=2017),
Inventory(station='USC00115825', latitude=41.3708, longitude=-88.4336,
element='TMIN', start=1912, end=2017),
Inventory(station='USC00115326', latitude=42.2636, longitude=-88.6078,
element='TMAX', start=1893, end=2017),
Inventory(station='USC00115326', latitude=42.2636, longitude=-88.6078,
element='TMIN', start=1893, end=2017),
Inventory(station='USC00200710', latitude=42.1244, longitude=-86.4267,
element='TMAX', start=1893, end=2017),
Inventory(station='USC00200710', latitude=42.1244, longitude=-86.4267,
element='TMIN', start=1893, end=2017),
Inventory(station='USC00114198', latitude=40.4664, longitude=-87.685,
element='TMAX', start=1902, end=2017),
Inventory(station='USC00114198', latitude=40.4664, longitude=-87.685,
element='TMIN', start=1902, end=2017),
Inventory(station='USW00014848', latitude=41.7072, longitude=-86.3164,
element='TMAX', start=1893, end=2017),
Inventory(station='USW00014848', latitude=41.7072, longitude=-86.3164,
element='TMIN', start=1893, end=2017)]
选择站点并获取站点元数据
当你查看新排序列表的前 20 个条目时,似乎第一个站点 USC00110338 是一个很好的选择。它既有 TMIN 也有 TMAX,并且是较长的序列之一,从 1893 年开始,一直持续到 2017 年,超过 120 年的数据。所以将这个站点保存到你的站点变量中,并快速解析你已经获取的站点数据,以获取更多关于站点的信息。
在 readme 文件中,你可以找到以下关于站点的数据信息:
IV. FORMAT OF "ghcnd-stations.txt"
------------------------------
Variable Columns Type
------------------------------
ID 1-11 Character
LATITUDE 13-20 Real
LONGITUDE 22-30 Real
ELEVATION 32-37 Real
STATE 39-40 Character
NAME 42-71 Character
GSN FLAG 73-75 Character
HCN/CRN FLAG 77-79 Character
WMO ID 81-85 Character
------------------------------
These variables have the following definitions:
ID is the station identification code. Note that the first two
characters denote the FIPS country code, the third character
is a network code that identifies the station numbering system
used, and the remaining eight characters contain the actual
station ID.
See "ghcnd-countries.txt" for a complete list of country codes.
See "ghcnd-states.txt" for a list of state/province/territory
codes.
The network code has the following five values:
0 = unspecified (station identified by up to eight
alphanumeric characters)
1 = Community Collaborative Rain, Hail,and Snow (CoCoRaHS)
based identification number. To ensure consistency with
with GHCN Daily, all numbers in the original CoCoRaHS IDs
have been left-filled to make them all four digits long.
In addition, the characters "-" and "_" have been removed
to ensure that the IDs do not exceed 11 characters when
preceded by "US1". For example, the CoCoRaHS ID
"AZ-MR-156" becomes "US1AZMR0156" in GHCN-Daily
C = U.S. Cooperative Network identification number (last six
characters of the GHCN-Daily ID)
E = Identification number used in the ECA&D non-blended
dataset
M = World Meteorological Organization ID (last five
characters of the GHCN-Daily ID)
N = Identification number used in data supplied by a
National Meteorological or Hydrological Center
R = U.S. Interagency Remote Automatic Weather Station (RAWS)
identifier
S = U.S. Natural Resources Conservation Service SNOwpack
TELemtry (SNOTEL) station identifier
W = WBAN identification number (last five characters of the
GHCN-Daily ID)
LATITUDE is latitude of the station (in decimal degrees).
LONGITUDE is the longitude of the station (in decimal degrees).
ELEVATION is the elevation of the station (in meters, missing = -999.9).
STATE is the U.S. postal code for the state (for U.S. stations only).
NAME is the name of the station.
GSN FLAG is a flag that indicates whether the station is part of the GCOS
Surface Network (GSN). The flag is assigned by cross-referencing
the number in the WMOID field with the official list of GSN
stations. There are two possible values:
Blank = non-GSN station or WMO Station number not available
GSN = GSN station
HCN/ is a flag that indicates whether the station is part of the U.S.
CRN FLAG Historical Climatology Network (HCN). There are three possible
values:
Blank = Not a member of the U.S. Historical Climatology
or U.S. Climate Reference Networks
HCN = U.S. Historical Climatology Network station
CRN = U.S. Climate Reference Network or U.S. Regional Climate
Network Station
WMO ID is the World Meteorological Organization (WMO) number for the
station. If the station has no WMO number (or one has not yet
been matched to this station), then the field is blank.
尽管你可能更关心更严肃研究中的元数据字段,但现在你想要将清单记录中的起始年和结束年与站点文件中的其余站点元数据进行匹配。
你有几种方法可以筛选出文件中的某个气象站,以匹配你选择的气象站 ID。你可以创建一个for循环遍历每一行,找到匹配的行后跳出循环;你也可以将数据拆分成行,然后排序并使用二分搜索,等等。根据你拥有的数据性质和数量,可能有一种或多种方法适合。在这种情况下,因为你已经加载了数据,而且数据量不是很大,可以使用列表推导式来返回一个包含单个元素的列表,该元素是你正在寻找的气象站:
station_id = 'USC00110338'
# parse stations
Station = namedtuple("Station", ['station_id', 'latitude', 'longitude',
'elevation', 'state', 'name', 'start', 'end'])
stations = [(x[0:11], float(x[12:20]), float(x[21:30]), float(x[31:37]),
x[38:40].strip(), x[41:71].strip())
for x in stations_txt.split("\n") if x.startswith(station_id)]
station = Station(*stations[0] + (inventory_temps[0].start,
inventory_temps[0].end))
print(station)
Station(station_id='USC00110338', latitude=41.7806, longitude=-88.3092,
elevation=201.2, state='IL', name='AURORA', start=1893, end=2017)
到目前为止,你已经确定你需要从伊利诺伊州的奥罗拉气象站获取天气数据,这是距离芝加哥市中心最近且拥有一个多世纪温度数据的气象站。
获取并解析实际天气数据
确定了气象站后,下一步是获取该站的实际天气数据并进行解析。这个过程与上一节你所做的是非常相似的。
获取数据
首先,获取数据文件并保存它,以防你需要回溯:
# fetch daily records for selected station
r = requests.get('https://www1.ncdc.noaa.gov/pub/data/ghcn/daily/all/
{}.dly'.format(station.station_id))
weather = r.text
# save into a text file, so we won't need to fetch again
with open('weather_{}.txt'.format(station), "w") as weather_file:
weather_file.write(weather)
# read from saved daily file if needed (only used if we want to start the
process over without downloadng the file)
with open('weather_{}.txt'.format(station)) as weather_file:
weather = weather_file.read()
print(weather[:540])
USC00110338189301TMAX -11 6 -44 6 -139 6 -83 6 -100 6 -83 6 -72 6
-83 6 -33 6 -178 6 -150 6 -128 6 -172 6 -200 6 -189 6 -150 6 -
106 6 -61 6 -94 6 -33 6 -33 6 -33 6 -33 6 6 6 -33 6
-78 6 -33 6 44 6 -89 I6 -22 6 6 6
USC00110338189301TMIN -50 6 -139 6 -250 6 -144 6 -178 6 -228 6 -144 6
-222 6 -178 6 -250 6 -200 6 -206 6 -267 6 -272 6 -294 6 -294 6
-311 6 -200 6 -233 6 -178 6 -156 6 -89 6 -200 6 -194 6 -194 6
-178 6 -200 6 -33 I6 -156 6 -139 6 -167 6
解析天气数据
再次,现在你有了数据,你可以看到它比气象站和库存数据要复杂得多。显然,是时候回到readme.txt文件和第三部分了,这部分是关于天气数据文件的描述。你有许多选项,所以筛选出与你相关的选项,并排除其他元素类型以及指定值来源、质量和类型的整个标志系统:
III. FORMAT OF DATA FILES (".dly" FILES)
Each ".dly" file contains data for one station. The name of the file
corresponds to a station's identification code. For example,
"USC00026481.dly"
contains the data for the station with the identification code USC00026481).
Each record in a file contains one month of daily data. The variables on
each
line include the following:
------------------------------
Variable Columns Type
------------------------------
ID 1-11 Character
YEAR 12-15 Integer
MONTH 16-17 Integer
ELEMENT 18-21 Character
VALUE1 22-26 Integer
MFLAG1 27-27 Character
QFLAG1 28-28 Character
SFLAG1 29-29 Character
VALUE2 30-34 Integer
MFLAG2 35-35 Character
QFLAG2 36-36 Character
SFLAG2 37-37 Character
. . .
. . .
. . .
VALUE31 262-266 Integer
MFLAG31 267-267 Character
QFLAG31 268-268 Character
SFLAG31 269-269 Character
------------------------------
These variables have the following definitions:
ID is the station identification code. Please see "ghcnd-stations.txt"
for a complete list of stations and their metadata.
YEAR is the year of the record.
MONTH is the month of the record.
ELEMENT is the element type. There are five core elements as well as a
number of addition elements.
The five core elements are:
PRCP = Precipitation (tenths of mm)
SNOW = Snowfall (mm)
SNWD = Snow depth (mm)
TMAX = Maximum temperature (tenths of degrees C)
TMIN = Minimum temperature (tenths of degrees C)
...
VALUE1 is the value on the first day of the month (missing = -9999).
MFLAG1 is the measurement flag for the first day of the month.
QFLAG1 is the quality flag for the first day of the month.
SFLAG1 is the source flag for the first day of the month.
VALUE2 is the value on the second day of the month
MFLAG2 is the measurement flag for the second day of the month.
QFLAG2 is the quality flag for the second day of the month.
SFLAG2 is the source flag for the second day of the month.
... and so on through the 31st day of the month. Note: If the month has less
than 31 days, then the remaining variables are set to missing (e.g., for April,
VALUE31 = -9999, MFLAG31 = blank, QFLAG31 = blank, SFLAG31 = blank).
目前你关心的关键点是气象站 ID 是行中的 11 个字符,年份是下一个 4 个字符,月份是下一个 2 个字符,元素是之后的下一个 4 个字符。之后,有 31 个用于每日数据的槽位,每个槽位由表示温度的 5 个字符组成,温度以摄氏十分之一度表示,以及 3 个字符的标志。如我之前提到的,你可以忽略这个练习中的标志。你也可以看到,如果某一天不在该月,温度的缺失值用-9999 表示,例如,对于一个典型的二月,29 日、30 日和 31 日的值将是-9999。
在这个练习中处理你的数据时,你希望得到整体趋势,所以你不必过多担心个别日子。相反,找到每个月的平均值。你可以保存整个月的最大值、最小值和平均值,并使用这些值。
这意味着处理每行天气数据时,你需要:
-
将行拆分成其各自的字段,并忽略或丢弃每个每日值的标志。
-
移除值为-9999 的值,并将年份和月份转换为整数,将温度值转换为浮点数,同时考虑到温度读数是以摄氏十分之一度表示的。
-
计算平均值,并挑选出最高和最低值。
为了完成所有这些任务,你可以采取几种方法。你可以对数据进行多次遍历,分割字段,丢弃占位符,将字符串转换为数字,最后计算汇总值。或者,你可以编写一个函数,在单行中执行所有这些操作,并在一次遍历中完成所有操作。两种方法都可以是有效的。在这种情况下,采取后一种方法,创建一个parse_line函数来执行所有数据转换:
def parse_line(line):
""" parses line of weather data
removes values of -9999 (missing value)
"""
# return None if line is empty
if not line:
return None
# split out first 4 fields and string containing temperature values
record, temperature_string = (line[:11], int(line[11:15]),
int(line[15:17]), line[17:21]), line[21:]
# raise exception if the temperature string is too short
if len(temperature_string) < 248:
raise ValueError("String not long enough - {}
{}".format(temperature_string, str(line)))
# use a list comprehension on the temperature_string to extract and
convert the
values = [float(temperature_string[i:i + 5])/10 for i in range(0, 248, 8)
if not temperature_string[i:i + 5].startswith("-9999")]
# get the number of values, the max and min, and calculate average
count = len(values)
tmax = round(max(values), 1)
tmin = round(min(values), 1)
mean = round(sum(values)/count, 1)
# add the temperature summary values to the record fields extracted
earlier and return
return record + (tmax, tmin, mean, count)
如果你用你的原始天气数据的第一个行测试这个函数,你会得到以下结果:
parse_line(weather[:270])
Out[115]:
('USC00110338', 1893, 1, 'TMAX', 4.4, -20.0, -7.8, 31)
所以看起来你有一个可以用来解析你数据的函数。如果这个函数能正常工作,你就可以解析天气数据,要么存储它,要么继续你的处理:
# process all weather data
# list comprehension, will not parse empty lines
weather_data = [parse_line(x) for x in weather.split("\n") if x]
len(weather_data)
weather_data[:10]
[('USC00110338', 1893, 1, 'TMAX', 4.4, -20.0, -7.8, 31),
('USC00110338', 1893, 1, 'TMIN', -3.3, -31.1, -19.2, 31),
('USC00110338', 1893, 1, 'PRCP', 8.9, 0.0, 1.1, 31),
('USC00110338', 1893, 1, 'SNOW', 10.2, 0.0, 1.0, 31),
('USC00110338', 1893, 1, 'WT16', 0.1, 0.1, 0.1, 2),
('USC00110338', 1893, 1, 'WT18', 0.1, 0.1, 0.1, 11),
('USC00110338', 1893, 2, 'TMAX', 5.6, -17.2, -0.9, 27),
('USC00110338', 1893, 2, 'TMIN', 0.6, -26.1, -11.7, 27),
('USC00110338', 1893, 2, 'PRCP', 15.0, 0.0, 2.0, 28),
('USC00110338', 1893, 2, 'SNOW', 12.7, 0.0, 0.6, 28)]
现在你有了所有天气记录,不仅仅是温度记录,解析并保存在你的列表中。
将天气数据保存在数据库中(可选)
到目前为止,你可以将所有天气记录(如果你愿意,还包括站点记录和库存记录)保存在数据库中。这样做可以让你在以后的会话中返回并使用相同的数据,而无需再次麻烦地获取和解析数据。
例如,以下代码是如何将天气数据保存在 sqlite3 数据库中的:
import sqlite3
conn = sqlite3.connect("weather_data.db")
cursor = conn.cursor()
# create weather table
create_weather = """CREATE TABLE "weather" (
"id" text NOT NULL,
"year" integer NOT NULL,
"month" integer NOT NULL,
"element" text NOT NULL,
"max" real,
"min" real,
"mean" real,
"count" integer)"""
cursor.execute(create_weather)
conn.commit()
# store parsed weather data in database
for record in weather_data:
cursor.execute("""insert into weather (id, year, month, element, max,
min, mean, count) values (?,?,?,?,?,?,?,?) """,
record)
conn.commit()
当你将数据存储后,你可以使用如下代码从数据库中检索它,该代码仅获取 TMAX 记录:
cursor.execute("""select * from weather where element='TMAX' order by year,
month""")
tmax_data = cursor.fetchall()
tmax_data[:5]
[('USC00110338', 1893, 1, 'TMAX', 4.4, -20.0, -7.8, 31),
('USC00110338', 1893, 2, 'TMAX', 5.6, -17.2, -0.9, 27),
('USC00110338', 1893, 3, 'TMAX', 20.6, -7.2, 5.6, 30),
('USC00110338', 1893, 4, 'TMAX', 28.9, 3.3, 13.5, 30),
('USC00110338', 1893, 5, 'TMAX', 30.6, 7.2, 19.2, 31)]
选择和绘图数据
因为你只关心温度,你需要选择仅包含温度记录。你可以通过使用几个列表推导式来快速选择 TMAX 和 TMIN 的列表来完成这个任务。或者,你可以使用 pandas 的特性,你将使用它来绘图日期,来过滤掉你不需要的记录。因为你更关心纯 Python 而不是 pandas,所以采取第一种方法:
tmax_data = [x for x in weather_data if x[3] == 'TMAX']
tmin_data = [x for x in weather_data if x[3] == 'TMIN']
tmin_data[:5]
[('USC00110338', 1893, 1, 'TMIN', -3.3, -31.1, -19.2, 31),
('USC00110338', 1893, 2, 'TMIN', 0.6, -26.1, -11.7, 27),
('USC00110338', 1893, 3, 'TMIN', 3.3, -13.3, -4.6, 31),
('USC00110338', 1893, 4, 'TMIN', 12.2, -5.6, 2.2, 30),
('USC00110338', 1893, 5, 'TMIN', 14.4, -0.6, 5.7, 31)]
使用 pandas 绘图你的数据
到目前为止,你的数据已经清理完毕,准备绘图。为了使绘图更简单,你可以使用 pandas 和 matplotlib,如第二十四章所述。为此,你需要有一个运行的 Jupyter 服务器,并且已经安装了 pandas 和 matplotlib。为了确保它们是在你的 Jupyter 笔记本中安装的,请使用以下命令:
# Install pandas and matplotlib using pip
! pip3.6 install pandas matplotlib
import pandas as pd
%matplotlib inline
当 pandas 和 matplotlib 安装好后,你可以加载 pandas 并为你的 TMAX 和 TMIN 数据创建数据框:
tmax_df = pd.DataFrame(tmax_data, columns=['Station', 'Year', 'Month',
'Element', 'Max', 'Min', 'Mean', 'Days'])
tmin_df = pd.DataFrame(tmin_data, columns=['Station', 'Year', 'Month',
'Element', 'Max', 'Min', 'Mean', 'Days'])
你可以绘制月度值,但 123 年的 12 个月数据几乎有 1,500 个数据点,季节循环也使得找出模式变得困难。
相反,可能将最高、最低和平均月值平均到年值并绘制这些值更有意义。你可以在 Python 中这样做,但由于你的数据已经加载到 pandas 数据框中,你可以使用它按年分组并获取平均值:
# select Year, Min, Max, Mean columns, group by year, average and line plot
tmin_df[['Year','Min', 'Mean', 'Max']].groupby('Year').mean().plot(
kind='line', figsize=(16, 4))
这个结果变化幅度相当大,但似乎确实表明过去 20 年来最低温度一直在上升。
注意,如果你想要在不使用 Jupyter 笔记本和 matplotlib 的情况下获得相同的图表,你仍然可以使用 pandas,但你需要将数据写入 CSV 或 Microsoft Excel 文件,使用数据框的to_csv或to_excel方法。然后你可以将生成的文件加载到电子表格中,并从那里进行绘图。
附录 A. Python 文档指南
对于 Python 来说,最好的也是最全面的参考资料是 Python 自带的文档。考虑到这一点,探索如何访问这些文档的方式比打印编辑过的文档页面更有用。
标准文档包包含几个部分,包括在各个平台上记录、分发、安装和扩展 Python 的说明,当您寻找有关 Python 的问题答案时,它是逻辑上的起点。Python 文档中最可能最有用的两个区域是 库参考 和 语言参考。库参考 是绝对必要的,因为它包含了内置数据类型和 Python 所包含的每个模块的解释。语言参考 是对 Python 核心工作方式的解释,它包含了语言核心的官方说明,解释了数据类型、语句等的工作原理。“新增功能”部分也值得阅读,尤其是在 Python 新版本发布时,因为它总结了新版本中的所有变化。
A.1. 在网络上访问 Python 文档
对于许多人来说,访问 Python 文档最方便的方式是访问 www.python.org 并浏览那里的文档集合。尽管这样做需要连接到网络,但它有一个优点,即内容总是最新的。鉴于对于许多项目来说,经常在网络上搜索其他文档和信息是有用的,因此始终打开一个浏览器标签并指向在线 Python 文档,是一种方便地将 Python 参考手册放在手边的简单方法。
A.1.1. 在您的计算机上浏览 Python 文档
许多 Python 发行版默认包含完整的文档。在某些 Linux 发行版中,文档是一个需要单独安装的单独包。然而,在大多数情况下,完整的文档已经存在于您的计算机上,并且易于访问。
在交互式外壳或命令行中访问帮助
在第二章中,您学习了如何在交互式解释器中使用 help 命令来访问任何 Python 模块或对象的在线帮助:
>>> help(int)
Help on int object:
class int(object)
| int(x[, base]) -> integer
|
| Convert a string or number to an integer, if possible. A floating
| point argument will be truncated towards zero (this does not include a
| string representation of a floating point number!) When converting a
| string, use the optional base. It is an error to supply a base when
| converting a non-string.
|
| Methods defined here:
... (continues with a list of methods for an int)
发生的情况是,解释器正在调用 pydoc 模块来生成文档。您也可以使用 pydoc 模块从命令行搜索 Python 文档。在 Linux 或 macOS 系统上,要在终端窗口中获得相同的输出,您只需在提示符下输入 pydoc int;要退出,请输入 q。在 Windows 命令窗口中,除非您已将搜索路径设置为包含 Python Lib 目录,否则您需要输入完整的路径——可能类似于 C:\Users\<user>\AppData\Local\Programs\Python\Python36\Lib\pydoc.py int,其中 <user> 是您的 Windows 用户名。
使用 pydoc 生成 HTML 帮助页面
如果你想要 pydoc 为 Python 对象或模块生成的文档看起来更简洁,你可以将输出写入一个 HTML 文件,你可以在任何浏览器中查看它。为此,将 –w 选项添加到 pydoc 命令中,在 Windows 上将是 C:\Users\<user>\AppData\Local\Programs\Python \Python36\Lib\pydoc.py –w int。在这种情况下,你正在查找 int 对象的文档,pydoc 在当前目录中创建一个名为 int.html 的文件,你可以在那里打开并在浏览器中查看它。图 A.1 展示了在浏览器中 int.html 的样子。
如果出于某种原因你只想有有限页数的文档可用,这种方法很有效。但在大多数情况下,可能最好使用 pydoc 提供更完整的文档,如下一节所述。

使用 pydoc 作为文档服务器
除了能够在任何 Python 对象上生成文本和 HTML 文档之外,pydoc 模块还可以用作服务器来提供基于 Web 的文档。你可以使用 –p 和端口号运行 pydoc,在指定端口上打开服务器。然后你可以输入 "b" 命令来打开浏览器并访问所有模块的文档,如图 A.2 所示。
图 A.2. pydoc 提供的模块文档的部分视图


使用 pydoc 提供文档的一个好处是,它还会扫描当前目录,并从它找到的任何模块的 docstrings 中提取文档,即使它们不是标准库的一部分。这使得它可以用于访问任何 Python 模块的文档。然而,有一个注意事项。要从模块中提取文档,pydoc 必须导入它,这意味着它将执行模块顶层中的任何代码。因此,那些没有编写为导入时不会产生副作用(如第十一章所述)的脚本将被运行,所以请谨慎使用此功能。
使用 Windows 帮助文件
在 Windows 系统上,标准的 Python 3 软件包包括完整的 Python 文档作为 Windows 帮助文件。你可以在 Python 安装文件夹中的 Doc 文件夹中找到这些文件;通常,它们在程序菜单上的 Python 3 程序组中。当你打开主手册文件时,它看起来就像图 A.3 所示的那样。
A.1.2. 下载文档
如果你想要计算机上的 Python 文档,但并不一定需要运行 Python,你也可以从 www.python.org 下载完整的文档,格式为 PDF、HTML 或文本,如果你想要能够从电子书阅读器或类似设备访问文档,这很方便。
A.2. 最佳实践:如何成为一名 Pythonista
每种编程语言都会发展出自己的传统和文化,Python 是一个很好的例子。大多数经验丰富的 Python 程序员(有时被称为 Pythonistas)非常关心以符合 Python 风格和最佳实践的方式编写 Python。这种类型的代码通常被称为 Pythonic 代码,并且非常受重视,与看起来像 Java、C 或 JavaScript 的 Python 代码形成对比。
对于刚开始接触 Python 的程序员来说,他们面临的问题是如何学习编写 Python 风格的代码。尽管感受语言及其风格需要一点时间和努力,但本附录的其余部分为你提供了一些如何开始的建议。
A.2.1. 成为 Pythonista 的十个技巧
本节中的技巧是我与中级 Python 班级分享的,也是我对提升 Python 技能的建议。我并不是说每个人都绝对同意我的观点,但从我多年来看到的来看,这些技巧将使你稳稳地走上成为真正的 Pythonista 的道路:
-
考虑 Python 的禅意. Python 的禅意,或称为 PEP 20,总结了作为语言基础的 Python 设计哲学,并在讨论什么使脚本更具 Python 风格的讨论中经常被引用。特别是,“优美胜于丑陋”和“简单胜于复杂”应该指导你的编码。我在本附录的末尾包含了 Python 的禅意;你可以在 Python 命令行提示符中输入
import this来始终找到它。 -
遵循 PEP 8. PEP 8 是官方的 Python 风格指南,也包含在本附录的后面。PEP 8 提供了关于代码格式化、变量命名到语言使用的良好建议。如果你想编写 Python 风格的代码,熟悉 PEP 8 是很有必要的。
-
熟悉文档. Python 拥有一个丰富、维护良好的文档集合,你应该经常参考它。最有用的文档可能是标准库文档,但教程和如何使用文件也是关于有效使用语言的信息宝库。
-
尽可能少地编写尽可能多的代码. 虽然这些建议可能适用于许多语言,但它们非常适合 Python。我的意思是,你应该努力使你的程序尽可能短和简单(但不要更短或更简单),并且你应该尽可能多地练习这种编码风格。
-
尽可能多地阅读代码. 从一开始,Python 社区就意识到阅读代码比编写代码更重要。尽可能多地阅读 Python 代码,并在可能的情况下,与他人讨论你阅读的代码。
-
优先使用内置数据结构. 在编写自己的类来存储数据之前,你应该首先转向 Python 的内置结构。Python 的各种数据类型可以几乎无限灵活地组合,并且具有多年调试和优化的优势。利用它们。
-
关注生成器和推导式。 对于 Python 新手来说,几乎总是无法欣赏到列表和字典推导式以及生成器表达式在 Python 编程中的重要性。查看你阅读的 Python 代码中的示例,并练习它们。除非你能几乎不假思索地编写列表推导式,否则你不会成为 Python 程序员。
-
使用标准库。 当内置函数无法满足你的需求时,请查看标准库。标准库中的元素是 Python 著名的“内置电池”。它们经受了时间的考验,并且比几乎任何其他 Python 代码都经过了更好的优化和文档化。如果可能,请使用它们。
-
尽可能少写类。 只有在必须的时候才编写自己的类。经验丰富的 Python 程序员通常非常节省类,因为他们知道设计好的类并不简单,而且他们创建的任何类都是他们必须测试和调试的类。
-
对框架保持警惕。 框架可能很有吸引力,尤其是对于 Python 新手来说,因为它们提供了许多强大的快捷方式。当然,当框架有帮助时,你应该使用它们,但也要意识到它们的缺点。你可能会花更多的时间学习一个非 Python 风格框架的怪癖,而不是学习 Python 本身,或者你可能发现自己是在适应框架,而不是反过来。
A.3. PEP 8—Python 代码风格指南
本节包含了对 PEP (Python Enhancement Proposal) 8 的略微编辑的摘录。由 Guido van Rossum 和 Barry Warsaw 撰写,PEP 8 是 Python 最接近风格手册的东西。一些更具体的部分已被省略,但主要观点都得到了涵盖。你应该尽可能使你的代码符合 PEP 8;你的 Python 风格会因此变得更好。
你可以通过访问 www.python.org 的文档部分并查找 PEP 索引来获取 PEP 8 的完整文本以及 Python 历史中发布的所有其他 PEP。PEP 是了解 Python 的历史和传说以及解释当前问题和未来计划的绝佳来源。
A.3.1. 简介
本文档提供了 Python 主分布标准库中 Python 代码的编码约定。请参阅描述 C 实现中 C 代码风格指南的配套信息性 PEP。¹ 本文档是从 Guido 的原始 Python 风格指南文章改编的,其中包含了一些来自 Barry 的风格指南的补充。² 如果存在冲突,本 PEP 的目的是采用 Guido 的风格规则。本 PEP 可能仍然不完整(实际上,它可能永远无法完成
¹
PEP 7,C 代码风格指南,van Rossum,
www.python.org/dev/peps/pep-0007/。²
巴里的 GNU Mailman 风格指南,
barry.warsaw.us/software/STYLEGUIDE.txt。尽管它以 PEP 8 风格指南的形式呈现,但 URL 是空的。
愚蠢的一致性是小思想的怪物
吉多(Guido)的关键洞察之一是代码的阅读次数远多于其编写次数。这里提供的指南旨在提高代码的可读性,并使其在广泛的 Python 代码中保持一致性。正如 PEP 20^([3])所说,“可读性很重要。”
³
PEP 20,Python 的禅,www.python.org/dev/peps/pep-0020/。
风格指南关乎一致性。与这个风格指南的一致性很重要。项目内的一致性更重要。一个模块或函数内的一致性最为重要。
但最重要的是,要知道何时可以不一致——有时候风格指南并不适用。如果有疑问,请使用你的最佳判断。参考其他示例并决定哪种看起来最好。并且不要犹豫去询问!
这里有两个很好的理由来打破特定的规则:
-
如果应用规则会使代码的可读性降低,即使对于习惯于阅读遵循规则代码的人来说也是如此
-
为了与周围也打破规则的代码保持一致(可能是出于历史原因),尽管这也是清理别人混乱的机会(在真正的 XP 风格中)
A.3.2. 代码布局
缩进
每个缩进级别使用四个空格。
对于你不想弄乱的老代码,你可以继续使用八个空格的制表符。
制表符或空格?
永远不要混合使用制表符和空格。
Python 中最受欢迎的缩进方式是只使用空格。第二种最受欢迎的方式是只使用制表符。混合使用制表符和空格的缩进代码应转换为仅使用空格。当你使用带有-t选项的 Python 命令行解释器时,它会发出有关非法混合制表符和空格的代码的警告。当你使用-tt时,这些警告变为错误。这些选项强烈推荐!
对于新项目,强烈推荐只使用空格,而不是制表符。大多数编辑器都有使这变得容易的功能。
最大行长度
将所有行限制在最多 79 个字符。
许多设备仍然存在,它们的行限制在 80 个字符以内;此外,将窗口限制在 80 个字符内使得可以并排打开几个窗口。在这些设备上的默认换行会破坏代码的视觉结构,使其更难以理解。因此,请将所有行限制在最多 79 个字符。对于流动的长文本块(如文档字符串或注释),建议限制长度为 72 个字符。
捆绑长行的首选方法是使用 Python 在括号、方括号和大括号中的隐式行续行。如果需要,可以在表达式周围添加额外的括号对,但有时使用反斜杠看起来更好。确保适当地缩进续行。在二元运算符周围断行的首选位置是在运算符之后,而不是之前。以下是一些示例:
class Rectangle(Blob):
def __init__(self, width, height,
color='black', emphasis=None, highlight=0):
if width == 0 and height == 0 and \
color == 'red' and emphasis == 'strong' or \
highlight > 100:
raise ValueError("sorry, you lose")
if width == 0 and height == 0 and (color == 'red' or
emphasis is None):
raise ValueError("I don't think so -- values are %s, %s" %
(width, height))
Blob.__init__(self, width, height,
color, emphasis, highlight)
空白行
使用两个空白行分隔顶层函数和类定义。
类内部的函数定义之间用单个空白行分隔。
可以使用额外的空白行(适当地)来分隔相关函数组。在相关的一行代码之间(例如,一组占位符实现)可以省略空白行。
在函数中,适当地使用空白行来表示逻辑部分。
Python 接受控制-L (^L) 形式进纸字符作为空白。许多工具将这些字符视为页面分隔符,因此您可以使用它们来分隔文件中相关部分的页面。
导入
导入通常应单独一行,例如:
import os
import sys
不要像这样放在一起:
import sys, os
虽然这样说可以,但:
from subprocess import Popen, PIPE
导入始终放在文件顶部,紧接在模块注释和文档字符串之后,在模块全局变量和常量之前。
导入应按以下顺序分组:
-
标准库导入
-
相关第三方导入
-
本地应用程序/库特定导入
在每组导入之间放置一个空白行。
在导入之后放置任何相关的__all__规范。
对于包内导入,相对导入被高度不建议。始终使用绝对包路径进行所有导入。即使现在 PEP 328^([4]) 已在 Python 2.5 中完全实现,其显式相对导入的风格也受到积极反对;绝对导入更便携且通常更易读。
⁴
PEP 328,导入:多行和绝对/相对,www.python.org/dev/peps/pep-0328/.
从包含类的模块中导入类时,通常可以这样拼写
from myclass import MyClass
from foo.bar.yourclass import YourClass
如果这种拼写导致局部名称冲突,那么请这样拼写
import myclass
import foo.bar.yourclass
and use myclass.MyClass and foo.bar.yourclass.YourClass.
表达式和语句中的空白
容忍的烦恼——避免在以下情况下出现多余的空白:
-
在括号、方括号或大括号内部立即使用 Yes:
spam(ham[1], {eggs: 2})No:
spam( ham[ 1 ], { eggs: 2 } ) -
在逗号、分号或冒号之前立即使用 Yes:
if x == 4: print x, y; x, y = y, xNo:
if x == 4 : print x , y ; x , y = y , x -
在开始函数调用参数列表的左括号之前立即使用 Yes:
spam(1)No:
spam (1) -
在开始索引或切片的左括号之前立即使用 Yes:
dict['key'] = list[index]No:
dict ['key'] = list [index] -
在赋值(或其他)运算符周围留有多个空格以对齐另一个 Yes:
x = 1 y = 2 long_variable = 3No:
x = 1 y = 2 long_variable = 3
其他建议
总是使用单侧空格包围这些二元运算符:赋值(=)、增量赋值(+=, -=, 等等)、比较(==, <, >, !=, <>, <=, >=, in, not in, is, is not)和布尔运算符(and, or, not)。
在算术运算符周围使用空格。
Yes:
i = i + 1
submitted += 1
x = x * 2 – 1
hypot2 = x * x + y * y
c = (a + b) * (a - b)
No:
i=i+1
submitted +=1
x = x*2 – 1
hypot2 = x*x + y*y
c = (a+b) * (a-b)
当用=表示关键字参数或默认参数值时,不要在=周围使用空格。
是的:
def complex(real, imag=0.0):
return magic(r=real, i=imag)
不:
def complex(real, imag = 0.0):
return magic(r = real, i = imag)
复合语句(同一行上的多个语句)通常是不被鼓励的。
是的:
if foo == 'blah':
do_blah_thing()
do_one()
do_two()
do_three()
不太愿意:
if foo == 'blah': do_blah_thing()
do_one(); do_two(); do_three()
虽然有时可以在同一行上放置具有小体量的if/for/while,但绝不要为多子句语句这样做。还避免折叠这样长的行!
不太愿意:
if foo == 'blah': do_blah_thing()
for x in lst: total += x
while t < 10: t = delay()
绝对不是:
if foo == 'blah': do_blah_thing()
else: do_non_blah_thing()
try: something()
finally: cleanup()
do_one(); do_two(); do_three(long, argument,
list, like, this)
if foo == 'blah': one(); two(); three()
A.4. 注释
与代码相矛盾的注释比没有注释更糟。当代码更改时,始终将保持注释更新作为优先事项!
注释应该是完整的句子。如果注释是一个短语或句子,其第一个单词应大写,除非它是一个以小写字母开头的标识符(永远不要改变标识符的大小写!)。
如果注释很短,可以省略结尾的句号。块注释通常由一个或多个由完整句子组成的段落组成,每个句子应以句号结束。
句号后应使用两个空格。
当用英语写作时,应遵循 Strunk 和 White 的原则。
来自非英语国家的 Python 程序员:请用英语编写注释,除非你 120%确信代码永远不会被不会说你的语言的人阅读。
块注释
块注释通常适用于它们后面的某些(或所有)代码,并且缩进与该代码相同级别。块注释的每一行都以#和一个空格开始(除非它是注释内的缩进文本)。
块注释内的段落由包含单个#的行分隔。
内联注释
适度地使用内联注释。
内联注释是与语句在同一行的注释。内联注释应至少与语句隔开两个空格。它们应以#和一个空格开始。
如果内联注释只是陈述显而易见的事情,则是不必要的,甚至可能会分散注意力。不要这样做:
x = x + 1 # Increment x
但有时,这很有用:
x = x + 1 # Compensate for border
文档字符串
良好文档字符串(即 docstrings)的约定被永久记录在 PEP 257 中.^([5])
⁵
PEP 257,文档字符串约定,Goodger,van Rossum,www.python.org/dev/peps/pep-0257/.
为所有公共模块、函数、类和方法编写文档字符串。对于非公共方法,文档字符串不是必需的,但你应该有一个描述方法做什么的注释。这个注释应出现在def行之后。
PEP 257 描述了良好的文档字符串约定。请注意,最重要的是,结束多行文档字符串的"""应单独一行,并且最好前面有一个空行,例如:
"""Return a foobang
Optional plotz says to frobnicate the bizbaz first.
"""
对于单行文档字符串,可以保留结尾的"""在同一行上。
版本簿记
如果你必须在源文件中包含 Subversion、CVS 或 RCS 的垃圾,请按以下方式操作:
__version__ = "$Revision: 68852 $" # $Source$
这些行应在模块的文档字符串之后、任何其他代码之前包含,上方和下方各有一个空白行。
A.4.1. 命名约定
Python 库的命名约定有些混乱,所以我们永远无法完全一致。尽管如此,以下是目前推荐的标准命名规范。新的模块和包(包括第三方框架)应按照这些标准编写,但如果现有的库有不同的风格,则内部一致性更受青睐。
描述性:命名风格
存在许多不同的命名风格。能够识别正在使用的命名风格,而不管它用于什么目的,这很有帮助。
以下命名风格通常被区分:
-
b(单个小写字母)
-
B(单个大写字母)
-
小写
-
lower_case_with_underscores
-
全大写
-
UPPER_CASE_WITH_UNDERSCORES
-
CapitalizedWords(或 CapWords,或 CamelCase——之所以这样命名是因为其字母的凹凸外观^([6]))。这也被称为 StudlyCaps。
⁶
对于完整的描述,请参阅www.wikipedia.com/wiki/CamelCase。
注意:当在 CapWords 中使用缩写时,应将缩写中的所有字母都大写。因此
HTTPServerError比HttpServerError更好。 -
mixedCase(与 CapitalizedWords 不同,以小写字母开头!)
-
Capitalized_Words_With_Underscores(丑陋!)
还有使用短唯一前缀来分组相关名称的风格。这在 Python 中很少使用,但我提一下以示完整。例如,os.stat()函数返回一个元组,其元素传统上具有st_mode、st_size、st_mtime等名称(这样做是为了强调与 POSIX 系统调用 struct 字段的对应关系,这有助于熟悉该结构的程序员)。
X11 库为其所有公共函数使用前缀 X。在 Python 中,这种风格通常被认为是不必要的,因为属性和方法名称前都有一个对象前缀,而函数名称前有一个模块名称前缀。
此外,以下使用前导或尾随下划线的特殊形式也被识别(这些通常可以与任何大小写约定结合使用):
-
_single_leading_underscore 弱“内部使用”指示符。例如,
from M import *不会导入以下划线开头的对象。 -
single_trailing_underscore_ 习惯上用于避免与 Python 关键字冲突。例如,
tkinter.Toplevel(master, class_='ClassName')。 -
_double_leading_underscore 当命名类属性时,它将触发名称混淆(在类
FooBar内部,__boo变为_FooBar__boo;见下文)。 -
double_leading_and_trailing_underscore “魔法”对象或属性,它们存在于用户控制的命名空间中。例如,
__init__、__import__或__file__。永远不要发明这样的名称;仅按文档使用它们。
规范性:命名约定
-
避免使用的名称 永远不要使用字符 l(小写字母 el)、O(大写字母 oh)或 I(大写字母 eye)作为单个字符的变量名。在某些字体中,这些字符与数字 1(一个)和 0(零)难以区分。当想要使用 l 时,使用 L 代替。
-
包和模块名称 模块应该有简短的全小写名称。如果它提高了可读性,可以在模块名称中使用下划线。Python 包也应该有简短的全小写名称,尽管不鼓励使用下划线。由于模块名称映射到文件名,并且某些文件系统不区分大小写并且截断长名称,因此模块名称应该相对较短——在 UNIX 上这不会是问题,但在代码被传输到较老的 Mac 或 Windows 版本或 DOS 时可能会成为问题。当一个用 C 或 C++ 编写的扩展模块有一个伴随的 Python 模块,该模块提供了一个更高级的(例如,更面向对象的)接口时,C/C++ 模块有一个前导下划线(例如,
_socket)。 -
类名 几乎没有例外,类名使用 CapWords 规范。用于内部使用的类名前面有一个下划线。
-
异常名 因为异常应该是类,所以类命名规范也适用于这里。然而,你应该在你的异常名称中使用后缀
Error(如果异常实际上是一个错误)。 -
全局变量名称(让我们希望这些变量仅用于一个模块内。)这些规范与函数的规范大致相同。设计为通过
from M import *使用的模块应该使用__all__机制来防止导出全局变量,或者使用较旧的约定,即使用下划线作为这样的全局变量的前缀(你可能想这样做来表示这些全局变量是模块非公开的)。 -
函数名 函数名应该是小写的,必要时使用下划线分隔单词以提高可读性。在已经普遍使用 mixedCase 的上下文中,允许使用 mixedCase(例如,threading.py),以保持向后兼容性。
-
函数和方法参数 总是使用
self作为实例方法的第一个参数。总是使用cls作为类方法的第一个参数。如果一个函数参数的名称与保留关键字冲突,通常最好在后面添加一个单下划线,而不是使用缩写或拼写错误。因此,print_比较于prnt更好。(也许更好的做法是使用同义词来避免这种冲突。) -
方法名称和实例变量:使用函数命名规则:小写字母,必要时用下划线分隔单词以提高可读性。仅对非公开方法和实例变量使用一个前导下划线。为了避免与子类发生名称冲突,使用两个前导下划线来调用 Python 的名称混淆规则。Python 通过类名混淆这些名称:如果类
Foo有一个名为__a的属性,则不能通过Foo.__a访问它。(一个固执的用户仍然可以通过调用Foo._Foo__a来获得访问权限。)通常,双前导下划线仅用于避免与设计为可继承的类中的属性发生名称冲突。注意:关于 __names 的使用存在一些争议(见下文)。 -
常量:常量通常在模块级别声明,并使用全部大写字母,单词之间用下划线分隔。例如包括
MAX_OVERFLOW和TOTAL。 -
设计继承:始终决定一个类的公共方法、实例变量(统称为属性)应该是公开的还是非公开的。如果有疑问,请选择非公开;将其公开比将其公开属性非公开更容易。公共属性是你期望与你的类无关的客户使用,你承诺避免向后不兼容的更改。非公共属性是不打算由第三方使用的;你不对非公共属性不会更改或甚至被删除做出保证。在这里我们不使用术语“私有”,因为在 Python 中(没有大量不必要的额外工作),没有任何属性真正是私有的。属性的另一类包括那些是子类 API 的一部分(在其他语言中通常称为“受保护的”)。一些类被设计为可以继承,无论是为了扩展还是修改类的行为方面。在设计这样的类时,请务必明确决定哪些属性是公开的,哪些是子类 API 的一部分,以及哪些真正只应由基类使用。
考虑到这一点,以下是一些 Python 风格的指南:
-
公共属性不应以下划线开头。
-
如果你的公共属性名称与保留关键字冲突,请在你属性名称的末尾添加一个单独的后缀下划线。这比缩写或拼写错误更可取。(然而,尽管有此规则,对于任何已知是类的变量或参数,
cls是首选的拼写,特别是类方法的第一个参数。)注意 1:请参考上述关于类方法参数名称的建议。 -
对于简单的公共数据属性,最好只公开属性名称,而不使用复杂的访问器/修改器方法。记住,如果发现简单的数据属性需要增长功能行为,Python 提供了一条简单的路径。在这种情况下,使用属性来隐藏简单数据属性访问语法背后的功能实现。注意 1:属性仅在新的类上工作。注意 2:尽量保持功能行为无副作用,尽管如缓存这样的副作用通常是可以接受的。注意 3:避免使用属性进行计算密集型操作;属性符号使调用者相信访问(相对)是廉价的。
-
如果你的类打算被继承,并且你有不想让子类使用的属性,考虑使用双前导下划线和无尾随下划线来命名它们。这会调用 Python 的名称混淆算法,其中类的名称被混淆到属性名称中。这有助于避免子类意外包含具有相同名称的属性。注意 1:仅使用简单的类名在混淆名称中,所以如果子类选择了相同的类名和属性名,你仍然可能会得到名称冲突。注意 2:名称混淆可能会使某些用途(如调试和
__getattr__())不太方便。然而,名称混淆算法有很好的文档记录,并且易于手动执行。注意 3:并非每个人都喜欢名称混淆。尝试在避免意外名称冲突的需求与高级调用者的潜在使用之间取得平衡。
A.4.2. 编程建议
应该以不损害其他 Python(PyPy、Jython、IronPython、Pyrex、Psyco 等)实现的方式编写代码。
例如,不要依赖于 CPython 对原地字符串连接的高效实现,用于形式为 a+=b 或 a=a+b 的语句。这些语句在 Jython 中运行得更慢。在库的性能敏感部分,你应该使用 ''.join() 形式。这将确保连接在各种实现中按线性时间发生。
与 None 这样的单例的比较应该始终使用 is 或 is not,而不是相等运算符。
此外,请注意,当你真正想表达的是 if x is not None 时,不要写成 if x,例如,当测试一个默认值为 None 的变量或参数是否被设置为其他值时。其他值可能具有(如容器)在布尔上下文中可能为假的数据类型!
使用基于类的异常。
在新代码中禁止使用字符串异常,因为这种语言特性已在 Python 2.6 中被移除。
模块或包应定义自己的领域特定基异常类,该类应从内置的 Exception 类继承。始终包含类文档字符串,例如:
class MessageError(Exception):
"""Base class for errors in the email package."""
类命名约定适用于此处,尽管你应该在异常类中添加后缀 Error,如果异常是错误。非错误异常不需要特殊后缀。
当抛出异常时,使用 raise ValueError('message') 而不是较旧的格式 raise ValueError, 'message'。
使用括号的形式是首选的,因为当异常参数很长或包含字符串格式化时,由于包含括号,你不需要使用行续行字符。较旧的形式已在 Python 3 中被移除。
在捕获异常时,尽可能提及具体的异常,而不是使用裸的 except: 子句。例如,使用
try:
import platform_specific_module
except ImportError:
platform_specific_module = None
裸的 except: 子句将捕获 SystemExit 和 KeyboardInterrupt 异常,这使得使用 Control-C 中断程序变得更加困难,并且可能掩盖其他问题。如果你想捕获所有表示程序错误的异常,请使用 except Exception:。
一个好的经验法则是将裸的 except 子句的使用限制在两种情况:
-
如果异常处理程序将打印或记录跟踪信息;至少用户会知道已发生错误。
-
如果代码需要做一些清理工作,但随后又使用
raise让异常向上传播,那么try...finally是处理这种情况的更好方式。
此外,对于所有 try/except 子句,将 try 子句限制在绝对必要的最小代码量。再次强调,这可以避免掩盖错误。
Yes:
try:
value = collection[key]
except KeyError:
return key_not_found(key)
else:
return handle_value(value)
No:
try: # Too broad!
return handle_value(collection[key])
except KeyError: *1*
return key_not_found(key)
- 1 也会捕获由 handle_value() 触发的 KeyError
使用字符串方法而不是字符串模块。
字符串方法总是更快,并且与 Unicode 字符串共享相同的 API。如果需要与 Python 2.0 之前的版本保持向后兼容性,则可以覆盖此规则。
使用 ''.startswith() 和 ''.endswith() 而不是字符串切片来检查前缀或后缀。
startswith() 和 endswith() 更简洁且错误更少。
Yes:
if foo.startswith('bar'):
No:
if foo[:3] == 'bar':
例外情况是如果你的代码必须与 Python 1.5.2 兼容(但让我们希望不是这样!)。
对象类型比较应始终使用 isinstance() 而不是直接比较类型。
Yes:
if isinstance(obj, int):
No:
if type(obj) is type(1):
当检查一个对象是否为字符串时,请记住它可能也是一个 Unicode 字符串!在 Python 2.3 中,str 和 unicode 有一个共同的基类 basestring,因此你可以这样做:
if isinstance(obj, basestring):
在 Python 2.2 中,types 模块定义了 StringTypes 类型用于此目的,例如:
from types import StringTypes
if isinstance(obj, StringTypes):
在 Python 2.0 和 2.1 中,你应该这样做:
from types import StringType, UnicodeType
if isinstance(obj, StringType) or \
isinstance(obj, UnicodeType) :
对于序列(字符串、列表、元组),使用空序列为假的事实。
Yes:
if not seq: if seq:
No:
if len(seq) if not len(seq)
不要编写依赖于大量尾随空白的字符串字面量。这种尾随空白在视觉上无法区分,并且一些编辑器(或更近期的 reindent.py)会将其删除。
不要使用 == 将布尔值与 True 或 False 进行比较。
Yes:
if greeting:
No:
if greeting == True:
更糟的是:
if greeting is True:
版权——本文件已被置于公有领域。
A.4.3. 其他 Python 风格指南
尽管 PEP 8 仍然是 Python 最有影响力的风格指南,但你还有其他选择。通常,这些指南并不与 PEP 8 冲突,但它们提供了更广泛的示例和更充分的理由,说明如何使你的代码更符合 Python 风格。一个不错的选择是 《Python 风格要素》,可在 github.com/amontalenti/elements-of-python-style/blob/master/README.md 免费获取。另一个有用的指南是 Kenneth Reitz 和 Tanya Schlusser 编著的《Python 漫游指南》,同样可在 docs.python-guide.org/en/latest/ 免费获取。
随着语言和程序员技能的不断演变,肯定会有其他指南,我鼓励你在新指南发布后充分利用它们,但前提是先从 PEP 8 开始。
A.5. Python 的禅意
以下文档是 PEP 20,也被称为“Python 的禅意”,这是一句带点幽默的话,阐述了 Python 的哲学。除了包含在 Python 文档中,Python 的禅意也是 Python 解释器中的一个彩蛋。在交互式提示符中输入 import this 即可查看。
长期从事 Python 开发的 Tim Peters 简明扼要地将 BDFL(终身仁慈独裁者)为 Python 设计的指导原则融入了 20 条格言,其中只有 19 条被记录下来。
Python 的禅意
美丽优于丑陋。
明确优于隐晦。
简单优于复杂。
复杂优于复杂。
扁平结构优于嵌套结构。
稀疏优于密集。
可读性很重要。
特殊情况并不足以打破规则。
尽管实用性胜过纯粹性。
错误不应该默默无闻地通过。
除非明确禁止。
面对歧义,拒绝猜测的诱惑。
应该只有一个——最好是唯一一个——明显的做法。
尽管一开始可能不明显,除非你是荷兰人。
现在比从未好。
尽管永远比现在好。
如果实现起来难以解释,那是个坏主意。
如果实现起来容易解释,那可能是个好主意。
命名空间是一个非常好的想法——让我们多做些这样的工作!。
版权——本文件已进入公有领域。
附录 B. 练习答案
B.1. 第四章
尝试这样做:变量和表达式
在 Python 命令行中创建一些变量。尝试在变量名中放入空格、连字符或其他非字母数字字符会发生什么?尝试一些复杂的表达式,例如 x = 2 + 4 * 5 – 6 / 3. 使用括号以不同的方式分组数字,并查看这如何改变与原始未分组表达式的结果相比。
>>> x = 3
>>> y = 3.14
>>> y
3.14
>>> x
3
>>> big var = 12
File "<stdin>", line 1
big var = 12
^
SyntaxError: invalid syntax
>>> big-var
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'big' is not defined
>>> big&var
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'big' is not defined
>>> x = 2 + 4 * 5 - 6 /3
>>> x
20.0
>>> x = (2 + 4) * 5 - 6 /3
>>> x
28.0
>>> x = (2 + 4) * (5 - 6) /3
>>> x
-2.0
尝试这样做:操作字符串和数字
在 Python 命令行中,创建一些字符串和数字变量(整数、浮点数,以及复数)。尝试一下对这些变量进行操作时会发生什么,包括跨类型操作。例如,你能将一个字符串乘以一个整数,或者乘以一个浮点数或复数吗?此外,加载 math 模块并尝试几个函数;然后加载 cmath 模块并做同样的操作。加载 cmath 模块后尝试使用这些函数对整数或浮点数进行操作会发生什么?你如何获取 math 模块中的函数?
>>> i = 3
>>> f = 3.14
>>> c = 3j2
File "<stdin>", line 1
c = 3j2
^
SyntaxError: invalid syntax
>>> c = 3J2
File "<stdin>", line 1
c = 3J2
^
SyntaxError: invalid syntax
>>> c = 3 + 2j
>>> c
(3+2j)
>>> s = 'hello'
>>> s * f
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't multiply sequence by non-int of type 'float'
>>> s * i
'hellohellohello'
>>> s * c
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't multiply sequence by non-int of type 'complex'
>>> c * i
(9+6j)
>>> c * f
(9.42+6.28j)
>>> from math import sqrt
>>> sqrt(16)
4.0
>>> from cmath import sqrt
>>> sqrt(16)
(4+0j)
要将第一个 sqrt 重新连接到当前命名空间,你可以重新导入它。请注意,此代码不会重新加载文件:
>>> from math import sqrt
>>> sqrt(4)
2.0
尝试这样做:获取输入
尝试使用 input() 函数获取字符串和整数输入。使用与上面类似的代码,不使用 int() 来调用 input() 对整数输入有什么影响?你能修改该代码以接受浮点数,例如 28.5 吗?如果你故意输入“错误”的类型值,例如在期望整数的地方输入浮点数或在期望数字的地方输入字符串,反之亦然,会发生什么?
>>> x = input("int?")
int?3
>>> x
'3'
>>> y = float(input("float?"))
float?3.5
>>> y
3.5
>>> z = int(input("int?"))
int?3.5
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: '3.5'
快速检查:Pythonic 风格
你认为以下哪些变量和函数名不符合 Pythonic 风格,为什么?:bar(, varName, VERYLONGVARNAME, foobar, longvarname, foo_bar(), really_very_long_var_name
bar(: 不好,不合法,包含符号
varName: Not good, mixed case
VERYLONGVARNAME: Not good, long, all caps, hard to read
foobar: Good
longvarname: Good, although underscores to separate words would be better
foo_bar(): Good
really_very_long_var_name: Long, but good if all of the words are needed, perhaps to distinguish among similar variables
B.2. 第五章
快速检查:len()
对于以下各项,len() 会返回什么:0; []; [[1, 3, [4, 5], 6], 7]?
len([0]) - 1
len([]) - 0
len([[1, 3, [4, 5], 6], 7 s]) - 2
([1, 3, [4, 5], 6] 是一个列表,列表中的第二个项目之前有一个单独的项目,7.。
尝试这样做:列表切片和索引
使用你对 len() 函数和列表切片的了解,当你不知道列表的大小时要如何结合两者以获取列表的后半部分?在 Python 命令行中进行实验以确认你的解决方案是否有效。
>>> my_list = [1, 2, 3, 4, 5, 6]
>>> last_half = my_list[len(my_list)//2:]
>>> last_half
[4, 5, 6]
len(my_list) // 2 is the halfway point; slice from there to the end.
尝试这样做:修改列表
假设你有一个长度为 10 的列表。你如何将列表末尾的最后三个元素移动到列表开头,同时保持它们的顺序?
>>> my_list = my_list[-3:] + my_list[:-3]
>>> my_list
[4, 5, 6, 1, 2, 3]
尝试这个:排序列表
假设你有一个列表,其中每个元素依次是一个列表:[[1, 2, 3], [2, 1, 3], [4, 0, 1]]。如果你想按每个列表中的第二个元素对列表进行排序,以便结果是 [[4, 0, 1], [2, 1, 3], [1, 2, 3]],你应该编写什么函数作为 sort() 方法的 key 值?
>>> the_list = [[1, 2, 3], [2, 1, 3], [4, 0, 1]]
>>> the_list.sort(key=lambda x: x[1])
>>> the_list
[[4, 0, 1], [2, 1, 3], [1, 2, 3]]
或者
>>> the_list = [[1, 2, 3], [2, 1, 3], [4, 0, 1]]
>>> the_list.sort(key=lambda x: x[1])
>>> the_list
[[4, 0, 1], [2, 1, 3], [1, 2, 3]]
快速检查:列表操作
len([[1,2]] * 3) 的结果是什么?
3
使用 in 操作符和列表的 index() 方法之间有两个区别是什么?
-
索引给出位置;in 给出一个真/假答案。
-
如果元素不在列表中,索引会引发错误。
以下哪个会引发异常?min(["a", "b", "c"]); max([1, 2, "three"]); [1, 2, 3].count("one")
max([1, 2, "three"]):字符串和整数不能比较,因此无法得到 max 值。
尝试这个:列表操作
如果你有一个列表 x,请编写代码以安全地删除列表中的项目,仅当该值在列表中时。
if element in x:
x.remove(element)
将该代码修改为仅当项目在列表中多次出现时才删除元素。
if x.count(element) > 1:
x.remove(element)
注意:此代码仅删除 element 的第一个出现。
尝试这个:列表副本
假设你有一个以下列表:x = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]。你可以使用什么代码来获取该列表的副本 y,在其中你可以更改其元素,而不会改变 x 的内容?
import copy
copy_x = copy.deepcopy(x)
快速检查:元组
解释以下操作为什么对元组 x = (1, 2, 3, 4) 是非法的:
x.append(1)
x[1] = "hello"
del x[2]
所有这些操作都会就地更改对象,而元组不能更改。
如果你有一个元组 x = (3, 1, 4, 2),你如何将其排序?
x = sorted(x)
快速检查:集合
如果你从以下列表构造一个集合,它将有多少个元素?[1, 2, 5, 1, 0, 2, 3, 1, 1, (1, 2, 3)]
六个独特的元素:1,2,5,0,3,以及元组(1,2,3)
实验五:检查列表
在这个实验中,任务是读取一组温度数据(实际上是 1948-2016 年希思罗机场的月最高温度),然后找到一些基本信息:最高和最低温度,平均温度,以及中位数温度(如果所有温度都已排序,则位于中间的温度)。
温度数据存储在源代码目录中该章节的 lab_05.txt 文件中。因为我还没有讨论读取文件,所以将文件读取到列表中的代码如下:
with open('lab_05.txt') as infile:
for row in infile:
temperatures.append(float(row.strip()))
如前所述,你应该找到最高和最低温度,平均值,以及中位数。你可能需要使用 min()、max()、sum()、len() 和 sort()。
max_temp = max(temperatures)
min_temp = min(temperatures)
mean_temp = sum(temperatures)/len(temperatures)
# we'll need to sort to get the median temp
temperatures.sort()
median_temp = temperatures[len(temperatures)//2]
print("max = {}".format(max_temp))
print("min = {}".format(min_temp))
print("mean = {}".format(mean_temp))
print("median = {}".format(median_temp))
max = 28.2
min = 0.8
mean = 14.848309178743966
median = 14.7
奖励:确定列表中有多少个独特的温度。
unique_temps = len(set(temperatures))
print("number of temps - {}".format(len(temperatures)))
print("number of temps - {}".format(unique_temps))
number of temps - 828
number of unique temps – 217
B.3. 第六章
快速检查:分割和连接
你如何使用 split 和 join 将字符串 x 中的所有空白字符替换为破折号(例如,将 "this is a test" 转换为 "this-is-a-test")?
>>> x = "this is a test"
>>> "-".join(x.split())
'this-is-a-test'
快速检查:字符串转换为数字
以下哪个不会转换为数字,为什么?
-
int('a1') -
int('12G', 16) -
float("12345678901234567890") -
int("12*2")
只有 #3 float("12345678901234567890") 可以转换;其他所有都有在转换为 int 时不允许的字符。
快速检查:strip
如果字符串 x 等于 "(name, date),\n",以下哪个会返回包含 "name, date" 的字符串?
-
x.rstrip("),") -
x.strip("),\n") -
x.strip("\n)(,") -
x.strip("\n)(,")将删除换行符以及逗号和括号。
快速检查:字符串搜索
如果你想要查看一行是否以字符串 "rejected" 结尾,你会使用什么字符串方法?还有其他方法可以得到相同的结果吗?
endswith('rejected')
你也可以这样做 line[:-8] == rejected,但这不会那么清晰或 Pythonic。
快速检查:修改字符串
有什么快速的方法可以将字符串中的所有标点符号更改为空格?
>>> punct = str.maketrans("!.,:;-?", " ")
>>> x = "This is text, with: punctuation! Right?"
>>> x.translate(punct)
'This is text with punctuation Right '
尝试这个:字符串操作
假设你有一个字符串列表,其中一些(但不一定是所有)字符串以双引号字符开始和结束:
x = ['"abc"', 'def', '"ghi"', '"klm"', 'nop']
你将使用什么代码对每个元素进行操作以仅删除双引号?
>>> for item in x:
... print(item.strip('"'))
...
abc
def
ghi
klm
nop
你可以使用什么代码来找到 "Mississippi" 中最后一个 p 的位置?当你找到它的位置后,你会使用什么代码来仅删除那个字母?
>>> state = "Mississippi"
>>> pos = state.rfind("p")
>>> state = state[:pos] + state[pos+1:]
>>> print(state)
Mississipi
快速检查:format() 方法
当以下代码片段执行后,x 中将会有什么内容?
x = "{1:{0}}".format(3, 4)
' 4'
x = "{0:$>5}".format(3)
'$$$$3'
x = "{a:{b}}".format(a=1, b=5)
' 1'
x = "{a:{b}}:{0:$>5}".format(3, 4, a=1, b=5, c=10)
' 1:$$$$3'
快速检查:使用 % 格式化字符串
在以下代码片段执行后,变量 x 中会有什么内容?
x = "%.2f" % 1.1111
x will contain '1.11'
x = "%(a).2f" % {'a':1.1111}
x will contain '1.11'
x = "%(a).08f" % {'a':1.1111}
x will contain '1.11110000'
快速检查:字节
对于以下哪种类型的数据,你会想使用字符串?对于哪种可以使用字节?
(1) 存储二进制数据的文件
字节。因为数据是二进制的,你更关心的是作为数字的内容而不是文本。因此,使用字节是有意义的。
(2) 使用带重音字符的语言的文本
字符串。Python 3 的字符串是 Unicode,因此可以处理带重音的字符。
(3) 仅包含大写和小写罗马字符的文本
字符串。在 Python 3 中,应该使用字符串来处理所有文本。
(4) 一系列不超过 255 的整数
字节。字节是一个不超过 255 的整数,因此字节类型非常适合存储这样的整数。
实验室 6:文本预处理
在处理原始文本时,在执行其他任何操作之前清理和规范化文本通常是必要的。例如,如果你想找到文本中单词的频率,你可以在开始计数之前确保所有内容都是小写(或如果你更喜欢,大写),并且所有标点符号都已删除。这也可以使将文本分解成一系列单词更容易。
In this lab, the task is to read an excerpt of the first chapter of Moby Dick, make sure that everything is one case, remove all punctuation, and write the words one per line to a second file. Again, because I haven’t yet covered reading and writing files, the code for those operations is supplied below.
Your task is to come up with the code to replace the commented lines in the sample below:
with open("moby_01.txt") as infile, open("moby_01_clean.txt", "w") as
outfile:
for line in infile:
# make all one case
# remove punctuation
# split into words
# write all words for line
outfile.write(cleaned_words)
punct = str.maketrans("", "", "!.,:;-?")
with open("moby_01.txt") as infile, open("moby_01_clean.txt", "w") as
outfile:
for line in infile:
# make all one case
cleaned_line = line.lower()
# remove punctuation
cleaned_line = cleaned_line.translate(punct)
# split into words
words = cleaned_line.split()
cleaned_words = "\n".join(words)
# write all words for line
outfile.write(cleaned_words)
B.4. Chapter 7
Try this: Create a dictionary
Write the code to ask the user for three names and three ages. After the names and ages are entered, ask the user for one of the names, and print the correct age.
>>> name_age = {}
>>> for i in range(3):
... name = input("Name? ")
... age = int(input("Age? "))
... name_age[name] = age
>>> name_choice = input("Name to find? ")
>>> print(name_age[name_choice])
Name? Tom
Age? 33
Name? Talita
Age? 28
Name? Rania
Age? 35
Name to find? Talita
28
Quick Check: Dictionary operations
Assume that you have a dictionary `x = {'a':1, 'b':2,
del x['d']
z = x.setdefault('g', 7)
x.update(y)
>>> x = {'a':1, 'b':2, 'c':3, 'd':4}
>>> y = {'a':6, 'e':5, 'f':6}
>>> del x['d']
>>> print(x)
{'a': 1, 'b': 2, 'c': 3}
>>> z = x.setdefault('g', 7)
>>> print(x)
{'a': 1, 'b': 2, 'c': 3, 'g': 7}
>>> x.update(y)
>>> print(x)
{'a': 6, 'b': 2, 'c': 3, 'g': 7, 'e': 5, 'f': 6}
Quick Check: What can be a key?
Decide which of the following expressions can be a dictionary key: 1; 'bob'; ('tom', [1, 2, 3]); ["filename"]; "filename"; ("filename", "extension")
1: Yes.
'bob': Yes.
('tom', [1, 2, 3]): No; it contains a list, which isn’t hashable.
["filename"]: No; it’s a list, which isn’t hashable.
"filename": Yes.
("filename", "extension"): Yes; it’s a tuple.
Try this: Using dictionaries
Suppose that you’re writing a program that works like a spreadsheet. How might you use a dictionary to store the contents of a sheet? Write some sample code to both store a value and retrieve a value in a particular cell. What might be some drawbacks to this approach?
You could use tuples of row, column values as keys to store the values in a dictionary. One drawback would be that the keys wouldn’t be sorted, so you’d have to manage that situation as you grabbed the keys/values to render as a spreadsheet.
>>> sheet = {}
>>> sheet[('A', 1)] = 100
>>> sheet[('B', 1)] = 1000
>>> print(sheet[('A', 1)])
100
Lab 7: Word Counting
In Lab 6, you took the text of the first chapter of Moby Dick, normalized the case, removed punctuation, and wrote the separated words to a file. In this lab, you read that file, use a dictionary to count the number of times each word occurs, and report the most common and least common words.
Use this code to read the words from the file into a list called moby_words:
moby_words = []
for word in infile:
if word.strip():
moby_words.append(word.strip())
moby_words = []
with open('moby_01_clean.txt') as infile:
for word in infile:
if word.strip():
moby_words.append(word.strip())
word_count = {}
for word in moby_words:
count = word_count.setdefault(word, 0)
count += 1
word_count[word] += 1
word_list = list(word_count.items())
word_list.sort(key=lambda x: x[1])
print("Most common words:")
for word in reversed(word_list[-5:]):
print(word)
print("\nLeast common words:")
for word in word_list[:5]:
print(word)
Most common words:
('the', 14)
('and', 9)
('i', 9)
('of', 8)
('is', 7)
Least common words:
('see', 1)
('growing', 1)
('soul', 1)
('having', 1)
('regulating', 1)
B.5. Chapter 8
Try this: Looping and if statements
Suppose that you have a list x = [1, 3, 5, 0, -1, 3, -2], and you need to remove all negative numbers from that list. Write the code to do this.
x = [1, 3, 5, 0, -1, 3, -2]
for i in x:
if i < 0:
x.remove(i)
print(x)
[1, 3, 5, 0, 3]
How would you count the total number of negative numbers in a list y = [[1, -1, 0], [2, 5, -9], [-2, -3, 0]]?
count = 0
y = [[1, -1, 0], [2, 5, -9], [-2, -3, 0]]
for row in y:
for col in row:
if col < 0:
count += 1
print(count)
4
What code would you use to print "very low" if the value of x is below -5, "low" if it’s from -4 up to 0, "neutral" if it’s equal to 0, "high" if it’s greater than 0 up to 4, and "very high" if it’s greater than 5?
if x < -5:
print("very low")
elif x <= 0:
print("low")
elif x <= 5:
print("high")
else:
print("very high")
Try this: Comprehensions
What list comprehension would you use to process the list x so that all negative values are removed?
x = [1, 3, 5, 0, -1, 3, -2]
new_x = [i for i in x if i >= 0]
print(new_x)
[1, 3, 5, 0, 3]
创建一个生成器,它只返回 1 到 100 之间的奇数。(提示:一个数是奇数,如果它除以 2 有余数;使用% 2来做这件事。)
odd_100 = (x for x in range(100) if x % 2)
for i in odd_100:
print(i))
编写代码创建一个从 11 到 15 的数字及其立方体的字典。
cubes = {x: x**3 for x in range(11, 16)}
print(cubes)
{11: 1331, 12: 1728, 13: 2197, 14: 2744, 15: 3375}
快速检查:布尔值和真值
判断以下陈述是对还是错:1, 0, -1, 0, 1 and 0, 1 > 0 or []
1 ->: True.
0 ->: False.
-1: True.
1 and 0: False.
1 > 0 或 []: True.
实验室:重构 word_count
将单词计数程序重写为第 8.7 节,使其更短。你可能需要查看已讨论的字符串和列表操作,以及考虑不同的代码组织方式。你可能还想使程序更智能,以便只有字母字符串(不是符号或标点符号)被视为单词。
列表 B.1. 文件:word_count_refactored.py
# File: word_count_refactored.py
""" Reads a file and returns the number of lines, words,
and characters - similar to the UNIX wc utility
"""
# initialze counts
line_count = 0
word_count = 0
char_count = 0
# open the file
with open('word_count.tst') as infile:
for line in infile:
line_count += 1
char_count += len(line)
words = line.split()
word_count += len(words)
# print the answers using the format() method
print("File has {0} lines, {1} words, {2} characters".format(line_count,
word_count, char_count))
B.6. 第九章
快速检查:函数和参数
你会如何编写一个函数,它可以接受任意数量的未命名参数,并按相反顺序打印它们的值?
def my_funct(*params):
for i in reversed(params):
print(i)
my_funct(1,2,3,4)
你需要做什么来创建一个过程或 void 函数——即没有返回值的函数?
要么不返回值(使用裸返回),要么根本不使用返回语句。
如果你用一个变量捕获函数的返回值会发生什么?
唯一的结果是你可以使用那个值,无论它是什么。
快速检查:可变函数参数
将列表或字典作为参数传递给函数时,修改这些列表或字典会产生什么结果?哪些操作可能会创建在函数外部可见的更改?你可以采取哪些步骤来最小化这种风险?
这些更改将持久应用于默认参数的后续使用。添加和删除元素、以及更改元素值等操作尤其可能导致问题。为了最小化风险,最好不使用可变类型作为默认参数。
尝试这个:全局变量与局部变量
假设x = 5,下面funct_1()执行后x的值将会是什么?执行funct_2()后呢?
def funct_1():
x = 3
def funct_2():
global x
x = 2
调用funct_1()后,x的值将保持不变;调用funct_2()后,全局变量x的值将变为 2。
快速检查:生成器函数
你需要修改上面four()函数的代码以使其适用于任何数字吗?你需要添加什么来允许设置起点?
>>> def four(limit):
... x = 0
... while x < limit:
... print("in generator, x =", x)
... yield x
... x += 1
...
>>> for i in four(4):
... print(i)
To specify the start:
>>> def four(start, limit):
... x = start
... while x < limit:
... print("in generator, x =", x)
... yield x
... x += 1
...
>>> for i in four(1, 4):
... print(i)
尝试这个:装饰器
你会如何修改上面装饰器函数的代码,以删除不必要的消息,并将包装函数的返回值用"<html>"和"</html>"括起来,以便myfunction ("hello")返回"<html>hello<html>"?
这项练习很难,因为要定义一个改变返回值的函数,你需要添加一个内部包装函数来调用原始函数并添加到返回值中。
def decorate(func):
def wrapper_func(*args):
def inner_wrapper(*args):
return_value = func(*args)
return "<html>{}<html>".format(return_value)
return inner_wrapper(*args)
return wrapper_func
@decorate
def myfunction(parameter):
return parameter
print(myfunction("Test"))
<html>Test<html>
实验室 9:有用的函数
回顾 第六章 和 第七章,将代码重构为用于清理和处理数据的函数。目标应该是将大部分逻辑移动到函数中。根据你的判断来决定函数的类型和参数,但请记住,函数应该只做一件事,并且它们不应该有任何在函数外部延续的副作用。
punct = str.maketrans("", "", "!.,:;-?")
def clean_line(line):
"""changes case and removes punctuation"""
# make all one case
cleaned_line = line.lower()
# remove punctuation
cleaned_line = cleaned_line.translate(punct)
return cleaned_line
def get_words(line):
"""splits line into words, and rejoins with newlines"""
words = line.split()
return "\n".join(words) + "\n"
with open("moby_01.txt") as infile, open("moby_01_clean.txt", "w")
as outfile:
for line in infile:
cleaned_line = clean_line(line)
cleaned_words = get_words(cleaned_line)
# write all words for line
outfile.write(cleaned_words)
def count_words(words):
"""takes list of cleaned words, returns count dictionary"""
word_count = {}
for word in moby_words:
count = word_count.setdefault(word, 0)
word_count[word] += 1
return word_count
def word_stats(word_count):
"""Takes word count dictionary and returns top and bottom five
entries"""
word_list = list(word_count.items())
word_list.sort(key=lambda x: x[1])
least_common = word_list[:5]
most_common = word_list[-1:-6:-1]
return most_common, least_common
moby_words = []
with open('moby_01_clean.txt') as infile:
for word in infile:
if word.strip():
moby_words.append(word.strip())
word_count = count_words(moby_words)
most, least = word_stats(word_count)
print("Most common words:")
for word in most:
print(word)
print("\nLeast common words:")
for word in least:
print(word)
B.7. 第十章
快速检查:模块
假设你有一个名为 new_math 的模块,其中包含一个名为 new_divide 的函数。你可能会用哪些方法来导入并使用该函数?每种方法有哪些优缺点?
import new_math
new_math.new_divide(...)
这种解决方案通常更受欢迎,因为 new_module 中的任何标识符都不会与导入命名空间中的标识符发生冲突。然而,这种解决方案不太方便输入。
from new_math import new_divide
new_divide(...)
这个版本更方便使用,但增加了模块中的标识符和导入命名空间中的标识符之间发生名称冲突的机会。
假设 new_math 模块包含一个名为 _helper_math() 的函数调用。下划线字符将如何影响 _helper_math() 的导入方式?
如果你使用 use from new_math import *,则不会导入。
快速检查:命名空间和作用域
考虑一个位于 make_window.py 模块中的变量宽度。在以下哪个上下文中,宽度是作用域内的?
(A) 在模块本身中
(B) 在模块中的 resize() 函数内
(C) 在导入 make_window.py 模块的脚本中
A 和 B 但不是 C
实验室 10:创建模块
将你在 第九章 结尾创建的函数打包成一个独立的模块。虽然你可以包含代码以将模块作为主程序运行,但目标应该是使函数可以从另一个脚本完全使用。
(无答案)
B.8. 第十一章
尝试这个:使脚本可执行
在你的平台上尝试执行脚本。还尝试将输入和输出重定向到和从你的脚本中。
(无答案)
快速检查:程序和模块
if __name__ == "__main__": 的使用旨在防止什么问题,它是如何做到这一点的?你能想到其他防止此问题的方法吗?
当 Python 加载一个模块时,它的所有代码都会被执行。通过使用上述模式,你可以让某些代码仅在作为主脚本文件执行时运行。
实验室 11:创建程序
在 第八章 中,你创建了一个 UNIX wc 工具的版本来统计文件中的行、单词和字符。现在你有更多工具可用,重构该程序使其更像原始版本。特别是,它应该有选项仅显示行(-l)、仅显示单词(-w)和仅显示字符(-c)。如果没有给出这些选项中的任何一个,则显示所有三个统计信息,但如果任何一个选项存在,则仅显示指定的统计信息。
作为额外挑战,查看 Linux/UNIX 系统上 wc 的 man 页面,并添加 -L 以显示最长行长度。你可以自由尝试实现 man 页面上列出的完整行为,并对其进行系统 wc 工具的测试。
# File: word_count_program.py
""" Reads a file and returns the number of lines, words,
and characters - similar to the UNIX wc utility
"""
import sys
def main():
# initialze counts
line_count = 0
word_count = 0
char_count = 0
option = None
params = sys.argv[1:]
if len(params) > 1:
# if more than one param, pop the first one as the option
option = params.pop(0).lower().strip()
filename = params[0] # open the file
with open(filename) as infile:
for line in infile:
line_count += 1
char_count += len(line)
words = line.split()
word_count += len(words)
if option == "-c":
print("File has {} characters".format(char_count))
elif option == "-w":
print("File has {} words".format(word_count))
elif option == "-l":
print("File has {} lines".format(line_count))
else:
# print the answers using the format() method
print("File has {0} lines, {1} words, {2}
characters".format(line_count,
word_count, char_count))
if __name__ == '__main__':
main()
B.9. 第十二章
快速检查:操作路径
你将如何使用 os 模块的功能从一个名为 test.log 的文件路径创建同一目录中名为 test.log.old 的新文件路径?你将如何使用 pathlib 模块做同样的事情?
import os.path
old_path = os.path.abspath('test.log')
print(old_path)
new_path = '{}.{}'.format(old_path, "old")
print(new_path)
import pathlib
path = pathlib.Path('test.log')
abs_path = path.resolve()
print(abs_path)
new_path = str(abs_path) + ".old"
print(new_path)
如果你从 os.pardir 创建一个 pathlib Path 对象,你会得到什么路径?尝试一下,看看结果。
test_path = pathlib.Path(os.pardir)
print(test_path)
test_path.resolve()
..
PosixPath('/home/naomi/Documents/QPB3E/qpbe3e')
Lab 12: 更多文件操作
你如何计算目录中所有以 .txt 结尾且不是符号链接的文件的总体大小?如果你的第一个答案是使用 os.path,也尝试使用 pathlib,反之亦然。
import pathlib
cur_path = pathlib.Path(".")
size = 0
for text_path in cur_path.glob("*.txt"):
if not text_path.is_symlink():
size += text_path.stat().st_size
print(size)
编写一些代码,基于你上面的解决方案将上述问题中的相同 .txt 文件移动到同一目录中的新目录 backup。
import pathlib
cur_path = pathlib.Path(".")
new_path = cur_path.joinpath("backup")
size = 0
for text_path in cur_path.glob("*.txt"):
if not text_path.is_symlink():
size += text_path.stat().st_size
text_path.rename(new_path.joinpath(text_path.name))
print(size)
B.10. 第十三章
快速检查
在文件打开模式字符串中添加 "b" 有什么意义?
它使得文件以二进制模式打开,读取和写入字节,而不是字符。
假设你想打开一个名为 myfile.txt 的文件,并在其末尾写入一些额外的数据。你将使用什么命令来打开 myfile.txt?你将使用什么命令来重新打开文件以从开头读取?
open("myfile.txt", "a")
open("myfile.txt")
尝试这个:重定向输入和输出
编写一些代码,使用上面的 mio.py 模块将脚本的全部打印输出捕获到名为 myfile.txt 的文件中,将标准输出重置到屏幕,并将该文件打印到屏幕。
# mio_test.py
import mio
def main():
mio.capture_output("myfile.txt")
print("hello")
print(1 + 3)
mio.restore_output()
mio.print_file("myfile.txt")
if __name__ == '__main__':
main()
output will be sent to file: myfile.txt
restore to normal by calling 'mio.restore_output()'
standard output has been restored back to normal
hello
4
快速检查:结构体
你能想到哪些用例,其中 struct 模块对于读取或写入二进制数据是有用的?
-
你正在尝试从二进制格式的应用程序文件或图像文件中读取/写入。
-
你正在从某些外部接口读取,例如温度计或加速度计,并且你想将原始数据保存得与传输时完全相同。
快速检查:Pickles
考虑以下用例中 pickle 是否是一个好的解决方案:
(A) 从一次运行保存到下一次运行的一些状态变量
(B) 为游戏保持高分列表
(C) 存储用户名和密码
(D) 存储一个包含大量英语术语的大字典
A 和 B 是合理的,尽管 pickle 不安全。
C 和 D 不会很好;对于 C 来说,安全性的缺乏会是一个大问题,而对于 D 来说,需要将整个 pickle 加载到内存中。
快速检查:Shelve
使用 shelf 对象看起来非常像使用字典。使用 shelf 对象有哪些不同之处?你预计使用 shelf 对象会有哪些缺点?
关键区别在于对象存储在磁盘上,而不是内存中。在处理大量数据时,尤其是有很多插入和/或删除操作时,你会期望磁盘访问使事情变慢。
实验室:wc 的最终修复
如果你查看 wc 工具的 man 页面,你会看到两个命令行选项做非常相似的事情。-c 让工具计算文件中的字节数,而 -m 让它计算字符数(在某些 Unicode 字符的情况下,这可能需要两个或更多字节)。此外,如果提供了文件,它应该从该文件读取并处理,如果没有提供文件,它应该从标准输入读取并处理。
重新编写你的 wc 工具版本,以实现字节和字符之间的区别以及从文件和标准输入读取的能力。
# File: word_count_program_stdin.py
""" Reads a file and returns the number of lines, words,
and characters - similar to the UNIX wc utility
"""
import sys
def main():
# initialze counts
line_count = 0
word_count = 0
char_count = 0
filename = None
option = None
if len(sys.argv) > 1:
params = sys.argv[1:]
if params[0].startswith("-"):
# if more than one param, pop the first one as the option
option = params.pop(0).lower().strip()
if params:
filename = params[0] # open the file
file_mode = "r"
if option == "-c":
file_mode = "rb"
if filename:
infile = open(filename, file_mode)
else:
infile = sys.stdin
with infile:
for line in infile:
line_count += 1
char_count += len(line)
words = line.split()
word_count += len(words)
if option in ("-c", "-m"):
print("File has {} characters".format(char_count))
elif option == "-w":
print("File has {} words".format(word_count))
elif option == "-l":
print("File has {} lines".format(line_count))
else:
# print the answers using the format() method
print("File has {0} lines, {1} words, {2}
characters".format(line_count, word_count, char_count))
if __name__ == '__main__':
main()
B.11. 第十四章
尝试这个:捕获异常
编写一些代码,从用户那里获取两个数字,并将第一个数字除以第二个数字。检查并捕获如果第二个数字为零时发生的异常(ZeroDivisionError)。
# the code of your program should do the following
x = int(input("Please enter an integer: "))
y = int(input("Please enter another integer: "))
try:
z = x / y
except ZeroDivisionError as e:
print("Can't divide by zero.")
Please enter an integer: 1
Please enter another integer: 0
Can't divide by zero.
快速检查:异常作为类
如果 MyError 继承自 Exception,那么 except Exception as e 和 except MyError as e 之间会有什么区别?
第一个捕获继承自 Exception 的任何异常(大多数),而第二个只捕获 MyError 异常。
尝试这个:断言语句
编写一个简单的程序,从用户那里获取一个数字,然后使用 assert 语句在数字为零时抛出异常。测试以确保 assert 被触发,然后使用上述提到的方法之一将其关闭。
x = int(input("Please enter a non-zero integer: "))
assert x != 0, "Integer can not be zero."
Please enter a non-zero integer: 0
----------------------------------------------------------------------
AssertionError Traceback (most recent call last)
<ipython-input-222-9f7a09820a1c> in <module>()
2 x = int(input("Please enter a non-zero integer: "))
3
----> 4 assert x != 0, "Integer can not be zero."
AssertionError: Integer can not be zero.
快速检查:异常
Python 异常是否会导致程序停止?
No. 如果异常被捕获并正确处理,程序不需要停止。
假设你想让字典 x 在键不存在时(即如果抛出 KeyError 异常)总是返回 None。你将使用什么代码来实现这个目标?
try:
x = my_dict[some_key]
except KeyError as e:
x = None
尝试这个:异常
你将使用什么代码来创建一个自定义的 ValueTooLarge 异常,并在变量 x 超过 1000 时抛出该异常?
class ValueTooLarge(Exception):
pass
x = 1001
if x > 1000:
raise ValueTooLarge()
快速检查:上下文管理器
假设你在一个脚本中使用上下文管理器读取和/或写入几个文件。以下哪种方法你认为是最合适的?
(A) 将整个脚本放在一个由 with 语句管理的块中。
(B) 对于所有文件读取使用一个 with 语句,对于所有文件写入使用另一个 with 语句。
(C) 每次读取文件或写入文件时使用一个 with 语句(即对于每一行)。
(D) 对于你读取或写入的每个文件使用一个 with 语句。
实验室 14:自定义异常
考虑你在第九章编写的模块来计算单词频率。这些函数中可能发生哪些合理的错误?重新编写代码以适当地处理这些异常条件。
class EmptyStringError(Exception):
pass
def clean_line(line):
"""changes case and removes punctuation"""
# raise exception if line is empty
if not line.strip():
raise EmptyStringError()
# make all one case
cleaned_line = line.lower()
# remove punctuation
cleaned_line = cleaned_line.translate(punct)
return cleaned_line
def count_words(words):
"""takes list of cleaned words, returns count dictionary"""
word_count = {}
for word in words:
try:
count = word_count.setdefault(word, 0)
except TypeError:
#if 'word' is not hashable, skip to next word.
pass
word_count[word] += 1
return word_count
def word_stats(word_count):
"""Takes word count dictionary and returns top and bottom five
entries"""
word_list = list(word_count.items())
word_list.sort(key=lambda x: x[1])
try:
least_common = word_list[:5]
most_common = word_list[-1:-6:-1]
except IndexError as e:
# if list is empty or too short, just return list
least_common = word_list
most_common = list(reversed(word_list))
return most_common, least_common
B.12. 第十五章
尝试这个:实例变量
你会用什么代码来创建一个 Rectangle 类?
class Rectangle:
def __init__(self):
self.height = 1
self.width = 2
尝试这个:实例变量和方法
更新 Rectangle 类的代码,以便在创建实例时可以设置维度,就像上面的 Circle 类一样。还要添加一个 area() 方法。
class Rectangle:
def __init__(self, width, height):
self.height = height
self.width = width
def area(self):
return self.height * self.width
尝试这个:类方法
编写一个类似于 total_area() 的类方法,但返回所有圆的总周长。
class Circle:
pi = 3.14159
all_circles = []
def __init__(self, radius):
self.radius = radius
self.__class__.all_circles.append(self)
def area(self):
return self.radius * self.radius * Circle.pi
def circumference(self):
return 2 * self.radius * Circle.pi
@classmethod
def total_circumference(cls):
"""class method to total the circumference of all Circles """
total = 0
for c in cls.all_circles:
total = total + c.circumference()
return total
尝试这个:继承
将 Rectangle 类的代码重写为从 Shape 继承。因为正方形和矩形是相关的,从其中一个继承另一个是否有意义?如果有,哪个是基类,哪个继承?
class Shape:
def __init__(self, x, y):
self.x = x
self.y = y
class Rectangle(Shape):
def __init__(self, x, y):
super().__init__(x, y)
可能会有意义进行继承。因为正方形是特殊类型的矩形,Square 应该从 Rectangle 类继承。
如果 Square 被专门化,使其只有一个维度 x,你会写
def area(self):
return self.x * self.x
你会如何编写代码为 Square 类添加一个 area() 方法?是否应该将 area() 方法移动到基类 Shape 中,并由 Circle、Square 和 Rectangle 继承?这种改变会引发哪些问题?
将 area() 方法放在 Rectangle 类中,该类是 Square 继承自的,是有意义的,但将其放在 Shape 类中不会很有帮助,因为不同类型的形状有自己的计算面积规则。每个形状都会覆盖基类 area() 方法。
尝试这个:私有实例变量
修改 Rectangle 类的代码,使其维度变量为私有。这种改变会对使用该类施加什么限制?
维度变量将不再可以通过 .x 和 .y 在类外访问。
class Rectangle():
def __init__(self, x, y):
self.__x = x
self.__y = y
尝试这个:属性
更新 Rectangle 类的维度为属性,具有不允许负大小的获取器和设置器。
class Rectangle():
def __init__(self, x, y):
self.__x = x
self.__y = y
@property
def x(self):
return self.__x
@x.setter
def x(self, new_x):
if new_x >= 0:
self.__x = new_x
@property
def y(self):
return self.__y
@y.setter
def y(self, new_y):
if new_y >= 0:
self.__y = new_y
my_rect = Rectangle(1,2)
print(my_rect.x, my_rect.y)
my_rect.x = 4
my_rect.y = 5
print(my_rect.x, my_rect.y)
1 2
4 5
实验室 15:HTML 类
在这个实验中,你创建类来表示 HTML 文档。为了使事情简单,假设每个元素只能包含文本和一个子元素。所以 <html> 元素只包含 <body> 元素,而 <body> 元素包含(可选)文本和一个 <p> 元素,该元素只包含文本。
实现的关键特性是 __str__() 方法,它反过来调用其子元素的 __str__() 方法,这样当在 <html> 元素上调用 str() 函数时,整个文档就会被返回。你可以假设任何文本都在子元素之前。
以下是使用这些类的示例输出:
para = p(text="this is some body text")
doc_body = body(text="This is the body", subelement=para)
doc = html(subelement=doc_body)
print(doc)
<html>
<body>
This is the body
<p>
this is some body text
</p>
</body>
</html>
答案:
class element:
def __init__(self, text=None, subelement=None):
self.subelement = subelement
self.text = text
def __str__(self):
value = "<{}>\n".format(self.__class__.__name__)
if self.text:
value += "{}\n".format(self.text)
if self.subelement:
value += str(self.subelement)
value += "</{}>\n".format(self.__class__.__name__)
return value
class html(element):
def __init__ (self, text=None, subelement=None):
super().__init__(text, subelement)
def __str__(self):
return super().__str__()
class body(element):
def __init__ (self, text=None, subelement=None):
return super().__init__(text, subelement)
def __str__(self):
return super().__str__()
class p(element):
def __init__(self, text=None, subelement=None):
super().__init__(text, subelement)
def __str__(self):
return super().__str__()
para = p(text="this is some body text")
doc_body = body(text="This is the body", subelement=para)
doc = html(subelement=doc_body)
print(doc)
B.13. 第十六章
快速检查:正则表达式中的特殊字符
你会用什么正则表达式来匹配表示从 -5 到 5 的数字的字符串?
`r"-{0,1}[0-5]"` 匹配表示从 -5 到 5 的数字的字符串。
你会用什么正则表达式来匹配十六进制数字?假设允许的十六进制数字是 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, A, a, B, b, C, c, D, d, E, e, F, 和 f。
`r"[0-9A-Fa-f]"`
尝试这个:提取匹配的文本
打国际电话通常需要加号(+)和国家代码。假设国家代码是两位数字,你将如何修改上面的代码以提取加号和国家代码作为号码的一部分?(再次提醒,并非所有号码都有国家代码。)你将如何使代码处理一位到三位数字的国家代码?
re.match(r": (?P<phone>(\+\d{2}-)?(\d\d\d-)?\d\d\d-\d\d\d\d)", ":
+01-111-222-3333")
或者
re.match(r": (?P<phone>(\+\d{2}-)?(\d{3}-)?\d{3}-\d{4})", ":
+01-111-222-3333")
对于一到三位数字的国家代码:
re.match(r": (?P<phone>(\+\d{1,3}-)?(\d{3}-)?\d{3}-\d{4})", ":
+011-111-222-3333")
尝试这个:替换文本
在上面的检查点中,你扩展了电话号码正则表达式,使其也能识别国家代码。你将如何使用函数来使现在没有国家代码的任何号码都带有+1(美国和加拿大的国家代码)?
def add_code(match_obj):
return("+1 "+match_obj.group('phone'))
re.sub(r"(?P<phone>(\d{3}-)?\d{3}-\d{4})", add_code, "111-222-3333")
实验室 16:电话号码标准化器
在美国和加拿大,电话号码由 10 位数字组成,通常分为 3 位区号、3 位交换码和 4 位站点码。如上所述,电话号码可能带有也可能不带+1(国家代码)。在实践中,有许多格式电话号码的方式,例如(NNN) NNN-NNNN、NNN-NNN-NNNN、NNN NNN-NNNN、NNN.NNN.NNNN 和 NNN NNN NNNN。此外,国家代码可能不存在,可能没有加号,通常(但不总是)通过空格或破折号与号码分开。哇!
在这个实验室中,任务是创建一个电话号码标准化器,它接受上述任何格式,并返回格式化为 1-NNN-NNN-NNNN 的电话号码。
以下都是可能的电话号码:
| +1 223-456-7890 | 1-223-456-7890 | +1 223 456-7890 |
|---|---|---|
| (223) 456-7890 | 1 223 456 7890 | 223.456.7890 |
奖励: 区号和交换码的第一位数字只能是 2-9,区号的第二位数字不能是 9。使用这些信息来验证输入,如果号码无效,则返回消息"invalid phone number"。
test_numbers = ["+1 223-456-7890",
"1-223-456-7890",
"+1 223 456-7890",
"(223) 456-7890",
"1 223 456 7890",
"223.456.7890",
"1-989-111-2222"]
def return_number(match_obj):
# validate number raise ValueError if not valid
if not re.match(r"[2-9][0-8]\d", match_obj.group("area") ):
raise ValueError("invalid phone number area code
{}".format(match_obj.group("area")))
if not re.match(r"[2-9]\d\d", match_obj.group("exch") ):
raise ValueError("invalid phone number exchange
{}".format(match_obj.group("exch")))
return("{}-{}-{}-{}".format(country, match_obj.group('area'),
match_obj.group('exch'),
match_obj.group('number')))
country = match_obj.group("country")
if not country:
country = "1"
regexp = re.compile(r"\+?(?P<country>\d{1,3})?[- .]?\(?(?P<area>\
d{3})\)?[- .]?(?P<exch>(\d{3}))- .")
for number in test_numbers:
print(regexp.sub(return_number, number))
B.14. 第十七章
快速检查:类型
假设你想在尝试向对象x中添加元素之前确保它是一个列表。你会使用什么代码?使用type()和isinstance()会有什么区别?这会是 LBYL(先检查再行动)还是 EAFP(请求原谅比请求许可更容易)风格的编程?除了显式检查类型之外,你还有哪些其他选择?
x = []
if isinstance(x, list):
print("is list")
使用类型只会得到列表,不会得到任何子类列表的内容。无论如何,这都是 LBYL 编程。
你也可以将append包裹在try...except块中,并捕获TypeError异常,这将更加 EAFP。
快速检查:getitem
上面的__getitem__示例非常有限,在许多情况下都不会正确工作。在哪些情况下,上述实现将失败或工作不正确?
如果你尝试直接通过索引访问项目,这个实现将不起作用;你也不能向后移动。
尝试这个:实现列表特殊方法
尝试实现前面列出的 __len__ 和 __delitem__ 特殊方法,以及一个 append 方法。代码实现以粗体显示。
class TypedList:
def __init__(self, example_element, initial_list=[]):
self.type = type(example_element)
if not isinstance(initial_list, list):
raise TypeError("Second argument of TypedList must "
"be a list.")
for element in initial_list:
self.__check(element)
self.elements = initial_list[:]
def __check(self, element):
if type(element) != self.type:
raise TypeError("Attempted to add an element of "
"incorrect type to a typed list.")
def __setitem__(self, i, element):
self.__check(element)
self.elements[i] = element
def __getitem__(self, i):
return self.elements[i]
# added methods
def __delitem__(self, i):
del self.elements[i]
def __len__(self):
return len(self.elements)
def append(self, element):
self.__check(element)
self.elements.append(element)
x = TypedList(1, [1,2,3])
print(len(x))
x.append(1)
del x[2]
快速检查:特殊方法属性和现有类型的子类化
假设您需要一个类似于字典的类型,它只允许字符串作为键(可能为了使其像第十三章中描述的shelf对象一样工作)。您有哪些创建此类类的选项?每种选项的优点和缺点是什么?
您可以使用与 TypedList 相同的方法,从 UserDict 类继承。您也可以直接从 dict 继承,或者您可以自己实现所有的 dict 功能。
自己实现一切可以提供最多的控制,但也是最耗时和最容易出现错误的方法。如果您需要做的更改很小(在这种情况下,只是在添加键之前检查类型),直接从 dict 继承可能最有意义。另一方面,从 UserDict 继承可能更安全,因为内部的 dict 对象将继续是一个常规的 dict,这是一个高度优化和成熟的实现。
B.15. 第十八章
快速检查:包
假设您正在编写一个包,该包接受一个 URL,检索该 URL 指向页面上的所有图像,将它们调整到标准大小,并将它们存储起来。不考虑每个函数的确切编码细节,您会如何将这些功能组织到一个包中?
该包将执行三种类型的操作:获取页面并解析 HTML 以获取图像 URL,获取图像,并调整图像大小。因此,您可能考虑拥有三个模块以保持操作分离:
picture_fetch/
__init__.py
find.py
fetch.py
resize.py
实验室 18:创建一个包
在第十四章中,您为在第十一章中创建的文本清理和单词频率统计模块添加了错误处理。将此代码重构为一个包含一个用于清理函数的模块、一个用于处理函数的模块和一个用于自定义异常的模块的包。然后编写一个简单的 main 函数,使用这三个模块。
word_count
__init__.py
exceptions.py
cleaning.py
counter.py
B.16. 第二十章
快速检查:考虑选择
花点时间考虑上述任务的解决方案。您能想到哪些标准库模块可以完成这项工作?如果您愿意,现在就可以停止,编写代码来完成这项工作,并将您的解决方案与下一节中开发的解决方案进行比较。
从标准库中,使用 datetime 来管理文件的日期/时间,以及使用 os.path 和 os 或 pathlib 来重命名和归档文件。
快速检查:潜在问题
由于前面的解决方案非常简单,可能存在许多它处理不好的情况。上述脚本可能会出现哪些潜在问题或问题?你将如何解决这些问题?
同一天内的多个文件可能会成为一个问题。如果你有很多文件,导航存档目录将变得越来越困难。
考虑文件使用的命名约定,该约定基于年份、月份和名称,顺序依次排列。你看到这个约定的哪些优点?可能存在哪些缺点?你能否为将日期字符串放在文件名中的其他位置(如开头或结尾)提出任何论点?
使用年-月-日日期格式会使基于文本的文件排序也按日期排序。将日期放在文件名末尾但扩展名之前会使视觉上解析日期元素更加困难。
尝试这个:实现多个目录
以上面章节中开发的代码为基础,你会如何修改它以实现将每组文件存档到按接收日期命名的子目录中?请随意花时间实现代码并测试它。
import datetime
import pathlib
FILE_PATTERN = "*.txt"
ARCHIVE = "archive"
if __name__ == '__main__':
date_string = datetime.date.today().strftime("%Y-%m-%d")
cur_path = pathlib.Path(".")
new_path = cur_path.joinpath(ARCHIVE, date_string)
new_path.mkdir()
paths = cur_path.glob(FILE_PATTERN)
for path in paths:
path.rename(new_path.joinpath(path.name))
快速检查:其他解决方案
你将如何创建一个不使用pathlib的脚本,以实现相同的功能?你会使用哪些库和函数?
你会使用os.path和os库——具体来说,os.path.join()、os.mkdir()和os.rename()。
尝试这个:存档到 zip 文件的抽象代码
花点时间编写一个解决方案的抽象代码,该方案将数据文件存储为上面所示的形式。你打算使用哪些模块、函数或方法?尝试编写你的解决方案以确保它有效。
抽象代码:
create path for zip file
create empty zipfile
for each file
write into zipfile
remove original file
(有关实现此功能的示例代码,请参阅下一节。)
快速检查:考虑不同的参数
花些时间考虑不同的整理选项。你将如何修改前面的“尝试这个”中的代码,以保留每月一个文件?你将如何修改代码,以便将上个月和更早的文件整理成每周保留一个?(注意:这不同于超过 30 天的情况!)
你可以使用与上面代码类似的东西,但也要检查文件月份是否与当前月份相同。
B.17. 第二十一章
快速检查:规范化
仔细查看上面生成的单词列表。到目前为止,你看到任何规范化问题吗?你认为在较长的文本部分中可能会遇到哪些其他问题?你认为你将如何处理这些问题?
双破折号用于破折号,连字符用于换行,以及其他任何标点符号都可能成为潜在问题。
通过增强你在第十八章中创建的单词清洗模块,是解决大多数问题的好方法。
尝试这个:读取一个文件
编写代码来读取一个文本文件(假设它是上面示例中显示的 temp_data_00a.txt 文件),将文件的每一行拆分为值列表,并将该列表添加到单个记录列表中。
(无答案)
在实施此解决方案时,你遇到了哪些问题或困难?你如何将最后三个字段转换为正确的日期、实数和整数类型?
你可以使用列表推导式显式地将这些字段转换。
快速检查:处理引号
考虑如果没有csv库,你会如何处理处理引号字段和嵌入的分隔符字符的问题。哪个更容易处理:引号还是嵌入的分隔符?
不使用csv模块,你必须检查一个字段是否以引号字符开始和结束,然后使用strip()去除它们。
要在不使用csv库的情况下处理嵌入的分隔符,你必须隔离引号字段,并对其进行不同的处理;然后你将使用分隔符拆分其余字段。
尝试这个:清理数据
你会如何处理可能具有“缺失”值的字段进行数学计算?你能编写一个代码片段来计算这些列的平均值吗?
clean_field = [float(x[13]) for x in data_rows if x[13] != 'Missing']
average = sum(clean_field)/len(clean_field)
你会如何处理末尾的平均列,以便也能报告平均覆盖率?在你看来,这个问题的解决方案是否与处理“缺失”条目的方式有任何关联?
coverage_values = [float(x[-1].strip("%"))/100]
这可能不会与处理“缺失”值同时完成。
实验:天气观测
这里提供的天气观测文件是按月份和县份为伊利诺伊州 1979 年至 2011 年的数据。编写代码处理此文件,并将芝加哥(库克县)的数据提取到单个 CSV 或电子表格文件中。此代码包括将“缺失”字符串替换为空字符串,并将百分比转换为小数。你也可以考虑哪些字段是重复的,可以省略或存储在其他地方。当你将文件加载到电子表格中时,你会看到你做对了的证据。你可以下载带有书籍源代码的解决方案。
B.18. 第二十二章
尝试这个:检索文件
如果你正在处理上面的数据文件,并想将每一行拆分为单独的字段,你该如何做?你预期还会进行哪些其他处理?尝试编写一些代码来检索此文件并计算平均年降雨量,或者作为一个更大的挑战,计算每年平均最高和最低温度。
import requests
response = requests.get("http://www.metoffice.gov.uk/pub/data/weather
/uk/climate/stationdata/heathrowdata.txt")
data = response.text
data_rows = []
rainfall = []
for row in data.split("\r\n")[7:]:
fields = [x for x in row.split(" ") if x]
data_rows.append(fields)
rainfall.append(float(fields[5]))
print("Average rainfall = {} mm".format(sum(rainfall)/len(rainfall)))
Average rainfall = 50.43794749403351 mm
尝试这个:访问 API
编写一些代码从上面使用的芝加哥市网站获取数据。查看结果中提到的字段,看看你是否可以根据日期范围结合另一个字段来选择记录。
import requests
response = requests.get("https://data.cityofchicago.org/resource/
6zsd-86xi.json?$where=date between '2015-01-10T12:00:00' and
'2015-01-10T13:00:00'&arrest=true")
print(response.text)
尝试这个:保存一些 JSON 犯罪数据
修改你在第二十二章第 22.2 节中编写的用于获取芝加哥犯罪数据的代码,将其从 JSON 格式的字符串转换为 Python 对象。看看你是否可以将犯罪事件保存为一个文件中的多个单独的 JSON 对象,以及另一个文件中的一个 JSON 对象。然后看看加载每个文件所需的代码。
import json
import requests
response = requests.get("https://data.cityofchicago.org/resource/
6zsd-86xi.json?$where=date between '2015-01-10T12:00:00' and
'2015-01-10T13:00:00'&arrest=true")
crime_data = json.loads(response.text)
with open("crime_all.json", "w") as outfile:
json.dump(crime_data, outfile)
with open("crime_series.json", "w") as outfile:
for record in crime_data:
json.dump(record, outfile)
outfile.write("\n")
with open("crime_all.json") as infile:
crime_data_2 = json.load(infile)
crime_data_3 = []
with open("crime_series.json") as infile:
for line in infile:
crime_data_3 = json.loads(line)
尝试这个:获取和解析 XML
编写代码从mng.bz/103V获取芝加哥 XML 天气预报。然后使用xmltodict将 XML 解析为 Python 字典,并提取明天的最高温度预报。提示:为了匹配时间布局和值,比较第一个时间布局部分的布局-key 值和参数元素中温度元素的 time-layout 属性。
import requests
import xmltodict
response = requests.get("https://graphical.weather.gov/xml/SOAP_server/
ndfdXMLclient.php?whichClient=NDFDgen&lat=41.87&lon=+-87.65&
product=glance")
parsed_dict = xmltodict.parse(response.text)
layout_key = parsed_dict['dwml']['data']['time-layout'][0]['layout-key']
forecast_temp =
parsed_dict['dwml']['data']['parameters']['temperature'][0]['value'][0]
print(layout_key)
print(forecast_temp)
尝试这个:解析 HTML
给定文件 forecast.html(你可以在本书的网站上找到相应的代码),编写一个使用 Beautiful Soup 提取数据并将其保存为 CSV 文件的脚本。
import csv
import bs4
def read_html(filename):
with open(filename) as html_file:
html = html_file.read()
return html
def parse_html(html):
bs = bs4.BeautifulSoup(html, "html.parser")
labels = [x.text for x in bs.select(".forecast-label")]
forecasts = [x.text for x in bs.select(".forecast-text")]
return list(zip(labels, forecasts))
def write_to_csv(data, outfilename):
csv.writer(open(outfilename, "w")).writerows(data)
if __name__ == '__main__':
html = read_html("forecast.html")
values = parse_html(html)
write_to_csv(values, "forecast.csv")
print(values)
实验室 22:跟踪好奇号的天气
使用第二十二章第 22.2 节中描述的应用程序编程接口(API)收集“好奇号”在火星上停留一个月的天气历史。提示:你可以在存档查询的末尾添加?sol=sol_number来指定火星日(sols),如下所示:
marsweather.ingenology.com/v1/archive/?sol=155
将数据转换成可以加载到电子表格和图表中的形式。关于这个项目的某个版本,请参阅本书的源代码。
import json
import csv
import requests
for sol in range(1830, 1863):
response = requests.get("http://marsweather.ingenology.com/v1/
archive/?sol={}&format=json".format(sol))
result = json.loads(response.text)
if not result['count']:
continue
weather = result['results'][0]
print(weather)
csv.DictWriter(open("mars_weather.csv", "a"), list(weather.keys())).writerow(weather)
B.19. 第二十三章
尝试这个:创建和修改表
使用 sqlite3,编写代码创建一个数据库表,用于存储你在第二十一章第 21.2 节中从平面文件加载的伊利诺伊州天气数据。假设你还有更多州的类似数据,并想存储更多关于各州本身的信息。你如何修改数据库以使用相关表来存储州信息?
import sqlite3
conn = sqlite3.connect("datafile.db")
cursor = conn.cursor()
cursor.execute("""create table weather (id integer primary key,
state text, state_code text,
year_text text, year_code text, avg_max_temp real,
max_temp_count integer,
max_temp_low real, max_temp_high real,
avg_min_temp real, min_temp_count integer,
min_temp_low real, min_temp_high real,
heat_index real, heat_index_count integer,
heat_index_low real, heat_index_high real,
heat_index_coverage text)
""")
conn.commit()
你可以在天气数据库中添加一个州表,并只存储每个州的 ID 字段。
尝试这个:使用 ORM
使用第二十二章第 22.3 节中的数据库,编写一个SQLAlchemy类来映射数据表,并使用它来读取表中的记录。
from sqlalchemy import create_engine, select, MetaData, Table, Column,
Integer, String, Float
from sqlalchemy.orm import sessionmaker
dbPath = 'datafile.db'
engine = create_engine('sqlite:///%s' % dbPath)
metadata = MetaData(engine)
weather = Table('weather', metadata,
Column('id', Integer, primary_key=True),
Column("state", String),
Column("state_code", String),
Column("year_text", String ),
Column("year_code", String),
Column("avg_max_temp", Float),
Column("max_temp_count", Integer),
Column("max_temp_low", Float),
Column("max_temp_high", Float),
Column("avg_min_temp", Float),
Column("min_temp_count", Integer),
Column("min_temp_low", Float),
Column("min_temp_high", Float),
Column("heat_index", Float),
Column("heat_index_count", Integer),
Column("heat_index_low", Float),
Column("heat_index_high", Float),
Column("heat_index_coverage", String)
)
Session = sessionmaker(bind=engine)
session = Session()
result = session.execute(select([weather]))
for row in result:
print(row)
尝试这个:使用 Alembic 修改数据库
尝试创建一个 Alembic 升级,向你的数据库添加一个州表,包含 ID、州名和缩写列。升级和降级。如果你打算使用州表与现有数据表一起使用,还需要进行哪些其他更改?
(无回答)
快速检查:键值存储的使用
哪些数据和应用程序最受益于像 Redis 这样的键值存储?
-
快速查找数据
-
缓存
快速检查:MONGODB 的使用
回想一下你迄今为止看到的各个数据样本以及你经验中的其他类型的数据,你能想到任何你认为非常适合存储在像 MongoDB 这样的数据库中的数据吗?其他类型的数据显然不适合,如果是这样,为什么?
大量且/或组织较松散的数据块适合 MongoDB,例如网页或文档的内容。
具有特定结构的数据更适合关系型数据。你看到的天气数据就是一个很好的例子。
实验室 23:创建数据库
选择过去几章中讨论的其中一个数据集,并决定哪种类型的数据库最适合存储该数据。创建该数据库,并编写代码将数据加载到其中。然后选择两种最常见和/或可能类型的搜索条件,并编写代码检索单个和多个匹配记录。
(无回答)
B.20. 第二十四章
尝试这个:使用 Jupyter Notebook
在笔记本中输入一些代码,并尝试运行它。查看编辑、单元格和内核菜单,看看有哪些选项。当你运行了一小段代码后,使用内核菜单来重启内核,重复你的步骤,然后使用单元格菜单在所有单元格中重新运行代码。
(无回答)
尝试这个:使用和不用 Pandas 清洗数据
尝试上述操作。当最终列已转换为分数时,你能想到将其转换回带有百分号尾部的字符串的方法吗?
相比之下,使用 csv 模块将相同的数据加载到普通的 Python 列表中,并使用纯 Python 应用相同的更改。
快速检查:合并数据集
你会如何在 Python 中合并像上述这样的数据集?
如果你确定每个集合中恰好有相同数量的项目,并且项目顺序正确,你可以使用 zip() 函数。否则,你可以创建一个字典,键是两个数据集之间的共同点,然后从两个集合中按键追加日期。
快速检查:在 Python 中选择
你会使用哪种 Python 代码结构来选择满足特定条件的行?
你可能会使用列表推导式:
selected = [x for x in old_list if <x meets selection criteria>]
尝试这个:分组和聚合
在 pandas 和上述数据上实验。你能根据团队成员和月份获取通话和金额吗?
calls_revenue[['Team member','Month', 'Calls', 'Amount']]
.groupby(['Team member','Month']).sum())
尝试这个:绘图
绘制每月平均通话金额的折线图。
%matplotlib inline
import pandas as pd
import numpy as np
# see text for these
calls = pd.read_csv("sales_calls.csv")
revenue = pd.read_csv("sales_revenue.csv")
calls_revenue = pd.merge(calls, revenue, on=['Territory', 'Month'])
calls_revenue['Call_Amount'] = calls_revenue.Amount/calls_revenue.Calls
# plot
calls_revenue[['Month', 'Call_Amount']].groupby(['Month']).mean().plot()




浙公网安备 33010602011771号