Python-入门指南第二版-全-

Python 入门指南第二版(全)

原文:annas-archive.org/md5/4b0fd2cf0da7c8edae4b5ecfd40159bf

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

正如标题所承诺的,本书将向你介绍世界上最流行的编程语言之一:Python。它旨在面向初学者和希望将 Python 添加到他们已经掌握的语言中的更有经验的程序员。

在大多数情况下,学习计算机语言比学习人类语言更容易。没有那么多歧义和例外需要记住。Python 是最一致和清晰的计算机语言之一。它平衡了学习的简易性、使用的便捷性和表达力量。

计算机语言由数据(类似口语中的名词)和指令代码(类似动词)组成。你需要两者兼具。在交替的章节中,你将被介绍 Python 的基本代码和数据结构,学会如何结合它们,并逐步构建更高级的结构。你阅读和编写的程序将变得越来越长、越来越复杂。用木工的类比来说,我们将从锤子、钉子和木片开始。在本书的前半部分,我们将引入更多专业的组件,直到等同于车床和其他电动工具的水平。

你不仅会学习语言,还会学习如何使用它。我们将从 Python 语言及其“一揽子”标准库开始,但我也会向你展示如何查找、下载、安装和使用一些优秀的第三方包。我强调的是我在超过 10 年的 Python 开发中实际发现有用的东西,而不是边缘话题或复杂的技巧。

尽管这是一个介绍,但我会包含一些高级主题,因为我希望向你们介绍。像数据库和网络这样的领域仍然涵盖在内,但技术变化很快。现在,一个 Python 程序员可能期望了解云计算、机器学习或事件流等内容。在这里,你将找到所有这些内容。

Python 具有一些特殊的功能,它们比从其他语言适应的风格更有效。例如,使用for迭代器比手动递增某些计数器变量更直接地进行循环。

当你学习新东西时,很难分辨哪些术语是特定的而不是口头的,哪些概念实际上很重要。换句话说,“这个会出现在考试中吗?”我会突出显示在 Python 中具有特定含义或重要性的术语和思想,但不会一次性介绍太多。真实的 Python 代码早早就包含在内。

注意

当有些内容可能令人困惑或者存在更适合Pythonic的解决方式时,我会包含这样的注释。

Python 并不完美。我会向你展示一些看似奇怪或应该避免的东西,并提供你可以使用的替代方案。

有时,我的一些观点(例如对象继承,或者用于 Web 的 MVC 和 REST 设计)可能与常见的智慧有所不同。看看你的想法。

受众

本书适用于任何对学习世界上最流行的计算语言之一感兴趣的人,无论您以前是否学过任何编程。

第二版的变化

与第一版相比有什么变化?

  • 还有大约一百页,包括猫的图片。

  • 两倍于之前的章节,每一章现在都更短。

  • 一个早期专门讨论数据类型、变量和名称的章节。

  • 新的标准 Python 功能,如 f-strings

  • 新的或改进的第三方软件包。

  • 全书穿插着新的代码示例。

  • 一个关于基本硬件和软件的附录,适用于新程序员。

  • 一个关于 asyncio 的附录,适用于不那么新的程序员。

  • “新的堆栈”覆盖范围:容器、云、数据科学和机器学习。

  • 关于如何在 Python 编程中找到工作的提示。

什么没有改变?使用糟糕诗歌和鸭子的示例。这些是不朽的。

大纲

第一部分(第 1-11 章)解释了 Python 的基础知识。您应该按顺序阅读这些章节。我从最简单的数据和代码结构开始工作,逐步将它们结合到更详细和现实的程序中。第二部分(第 12-22 章)展示了 Python 在特定应用领域(如网络、数据库、网络等)中的使用方式;您可以按任意顺序阅读这些章节。

这里是章节和附录的简要预览,包括一些您将在其中遇到的术语:

第一章,Python 的一点味道

计算机程序与您每天看到的指南并没有太大的区别。一些小的 Python 程序让您一窥该语言的外观、能力和在现实世界中的用途。您将看到如何在其交互式解释器(或shell)中运行 Python 程序,或者从计算机上保存的文本文件中运行。

第二章,数据:类型、值、变量和名称

计算机语言将数据和指令混合在一起。不同的数据类型会以不同的方式被计算机存储和处理。它们可能允许它们的值被更改(可变的)或不允许(不可变的)。在 Python 程序中,数据可以是文字(例如 78 这样的数字,文本 字符串,例如 "waffle")或由命名的变量表示。Python 将变量视为名称,这与许多其他语言不同,并且具有一些重要的后果。

第三章,数字

本章展示了 Python 的最简单的数据类型:布尔整数浮点 数。您还将学习基本的数学运算。示例使用 Python 的交互式解释器就像一个计算器一样。

第四章,使用 if 进行选择

我们将在几章之间往返于 Python 的名词(数据类型)和动词(程序结构)。Python 代码通常一次运行一行,从程序的开头到结尾。if 代码结构允许您根据某些数据比较运行不同的代码行。

第五章,文本字符串

回到名词和文本字符串的世界。学习如何创建、组合、更改、检索和打印字符串。

第六章,循环:while 和 for

再谈动词,以及创建循环的两种方法:forwhile。您将了解到 Python 的核心概念:迭代器

第七章,元组和列表

现在是 Python 更高级内置数据结构的第一步:列表元组。这些是值序列,就像 LEGO 一样,用于构建更复杂的数据结构。通过迭代器快速浏览它们,并通过推导式快速构建列表。

第八章,字典和集合

字典(又称dicts)和集合允许您通过其值而不是位置保存数据。这是非常方便的功能,也将成为您喜爱的 Python 特性之一。

第九章,函数

编织前几章的数据和代码结构,以便比较、选择或重复。将代码打包在函数中,并使用异常处理错误。

第十章,哦哦:对象和类

对象这个词有点模糊,在许多计算机语言中都很重要,包括 Python。如果您在其他语言中做过面向对象编程,Python 会更加灵活。本章解释如何使用对象和类,以及在何时更好地使用替代方法。

第十一章,模块、包和好东西

本章演示如何扩展到更大的代码结构:模块程序。您将了解到代码和数据的存放位置,如何输入和输出数据,处理选项,浏览 Python 标准库,并略观未来。

第十二章,整理和操纵数据

学会像专家一样管理(或操纵)数据。本章涵盖文本和二进制数据,Unicode 字符的乐趣以及正则表达式文本搜索。还介绍了字节字节数组这两种数据类型,它们是字符串的对应物,包含的是原始的二进制值而不是文本字符。

第十三章,日历和时钟

处理日期和时间可能会很混乱。本章展示了常见问题和实用解决方案。

第十四章,文件和目录

基本数据存储使用文件目录。本章将向您展示如何创建和使用它们。

第十五章,时间数据:进程和并发

这是第一个硬核系统章节。它的主题是时间数据——如何使用程序进程线程同时执行多项任务(并发)。提到了 Python 最近的async增强功能,详细信息在附录 C 中。

第十六章,盒中的数据:持久存储

数据可以通过基本的平面文件和文件系统中的目录进行存储和检索。它们通过常见的文本格式(如 CSV、JSON 和 XML)获得一些结构。随着数据变得更大更复杂,它们需要传统的 关系型 数据库和一些较新的 NoSQL 数据存储提供的服务。

第 17 章, 空间中的数据:网络

通过 网络服务,使用 协议API 将您的代码和数据发送到空间。示例涵盖从低级别的 TCP 套接字消息传递 库和排队系统,再到 部署。

第 18 章, Web, 解析

Web 有专门的章节——客户端、服务器、API 和框架。您将会 爬取抓取 网站,并使用 请求 参数和 模板 构建真实的网站。

第 19 章, 成为 Python 爱好者

本章包含 Python 开发者的技巧——使用 pipvirtualenv 进行安装、使用集成开发环境、测试、调试、日志记录、源代码控制和文档编写。它还帮助您找到并安装有用的第三方软件包,将您自己的代码打包以供重用,并了解获取更多信息的途径。

第 20 章, Py 艺术

人们在艺术领域正在用 Python 做一些很酷的事情:图形、音乐、动画和游戏。

第 21 章, Py 在工作中

Python 在商业领域有特定的应用:数据可视化(绘图、图表和地图)、安全性和监管。

第 22 章, Py Sci

在过去的几年中,Python 已经成为科学领域的顶级语言:数学和统计学、物理科学、生物科学和医学。数据科学机器学习 是显著的优势。本章涉及 NumPy、SciPy 和 Pandas。

附录 A, 初学者的硬件和软件

如果您是编程新手,这部分描述了硬件和软件的实际工作原理。它介绍了一些术语,您将经常遇到它们。

附录 B, 安装 Python 3

如果您的计算机上还没有安装 Python 3,本附录将向您展示如何安装,无论您使用的是 Windows、macOS、Linux 还是其他 Unix 变种。

附录 C, 截然不同的事情:异步

Python 在不同版本中不断添加异步特性,这些特性并不容易理解。我会在各个章节中提到它们,但详细讨论留在附录中。

附录 D, 练习答案

这里包含了章节末尾练习的答案。在您自己尝试练习之前,请勿偷看,否则可能会变成一只螃蟹。

附录 E, 速查表

本附录包含了作为快速参考使用的速查表。

Python 版本

随着开发人员增加功能并修复错误,计算机语言随时间而变化。本书中的示例是在运行 Python 3.7 时编写和测试的。在本书编辑时,版本 3.7 是最新版本。我会谈一谈它的显著增强功能。版本 3.8 预计将于 2019 年底正式发布,并将包含一些预期功能。如果您想了解 Python 的新功能及其发布时间,请查看Python 的新特性页面。这是一个技术参考,当您刚开始使用 Python 时可能会感到有些繁重,但如果未来需要在不同 Python 版本的计算机上运行程序,它可能会有用。

本书使用的约定

本书使用以下排版约定:

斜体

表示新术语、网址、电子邮件地址、文件名和文件扩展名。

等宽字体

用于程序清单,以及段落中引用程序元素,如变量、函数和数据类型。

恒定宽度加粗

显示用户应直接输入的命令或其他文本。

等宽斜体

显示应替换为用户提供值或根据上下文确定值的文本。

注意

这个图标表示提示、建议或一般说明。

警告

这个图标表示警告或注意事项。

使用代码示例

本书中的大量代码示例和练习可以在线下载。本书旨在帮助您完成工作。通常情况下,您可以在您的程序和文档中使用本书中的代码,无需征得我们的许可。除非您复制了本书中的大部分代码,否则无需许可。例如,编写一个使用本书中几个代码块的程序不需要许可。销售或分发奥莱利图书中的示例需要许可。引用本书并引用示例代码回答问题不需要许可。将本书中大量示例代码整合到产品文档中需要许可。

我们感谢但不需要归属。归属通常包括标题、作者、出版商和 ISBN。例如:“Python 入门,作者 Bill Lubanovic(奥莱利)。版权 2020 年 Bill Lubanovic,978-1-492-05136-7。”

如果您觉得使用代码示例超出了公平使用范围或这里给出的权限,请随时联系我们,邮箱是 permissions@oreilly.com。

奥莱利在线学习

注意

奥莱利媒体(O’Reilly Media)已经提供了 40 多年的技术和商业培训、知识和见解,帮助企业取得成功。

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

如需联系我们

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

  • 奥莱利传媒公司

  • 1005 Gravenstein Highway North

  • Sebastopol, CA 95472

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

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

  • 707-829-0104(传真)

我们为这本书设立了一个网页,列出勘误、示例和任何额外信息。您可以访问此页面:https://oreil.ly/introducing-python-2e

欲就本书提出评论或技术问题,请发送电子邮件至bookquestions@oreilly.com

欲了解更多关于我们的图书、课程、会议和新闻,请访问我们的网站:http://www.oreilly.com

在 Facebook 上找到我们:http://facebook.com/oreilly

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

观看我们的 YouTube 频道:http://www.youtube.com/oreillymedia

致谢

衷心感谢帮助使这本书更好的审稿人和读者们:

Corbin Collins, Charles Givre, Nathan Stocks, Dave George, and Mike James

第一部分:Python 基础

第一章:Python 的一点味道

只有丑陋的语言才会流行。Python 是唯一的例外。

唐纳德·克努斯

让我们从两个迷你谜题及其解决方案开始。你认为以下两行文字是什么意思?

(Row 1): (RS) K18,ssk,k1,turn work.
(Row 2): (WS) Sl 1 pwise,p5,p2tog,p1,turn.

它看起来很技术化,像某种计算机程序。实际上,它是一个编织图案;具体来说,是一个描述如何转弯袜子的脚后跟的片段,就像图 1-1 中的那只袜子一样。

袜子

图 1-1. 针织袜子

这对我来说一点都不合理,就像数独对我的猫一样,但我的妻子完全理解。如果你是一个编织者,你也会的。

让我们尝试另一个神秘的文本,发现在一张索引卡上。你会立即理解它的目的,尽管你可能不知道它的最终产品:

  1/2 c. butter or margarine
  1/2 c. cream
  2 1/2 c. flour
  1 t. salt
  1 T. sugar
  4 c. riced potatoes (cold)

Be sure all ingredients are cold before adding flour.
Mix all ingredients.
Knead thoroughly.
Form into 20 balls.  Store cold until the next step.
For each ball:
  Spread flour on cloth.
  Roll ball into a circle with a grooved rolling pin.
  Fry on griddle until brown spots appear.
  Turn over and fry other side.

即使你不会做饭,你可能也认出了它是一个食谱¹:一份食物配料列表,后面是制作指南。但它制作的是什么?这是lefse,一种类似于玉米饼的挪威美食(见图 1-2)。涂上一些黄油和果酱或你喜欢的任何东西,卷起来,享受吧。

lefse

图 1-2. Lefse

编织图案和食谱共享一些特征:

  • 一种常规的词汇,包括单词、缩写和符号。有些可能很熟悉,其他的可能令人困惑。

  • 关于可以说什么和在哪里说的语法规则。

  • 需要按顺序执行的操作序列

  • 有时,会重复执行一些操作(一个循环),比如油煎每片 lefse 的方法。

  • 有时,参考另一个操作序列(在计算机术语中,是一个函数)。在食谱中,你可能需要参考另一个关于土豆泥的食谱。

  • 假设你对上下文有所了解。这个配方假设你知道水是什么,以及如何把它煮沸。这个编织图案假设你能够编织和针织而不经常刺伤自己。

  • 一些要使用、创建或修改的数据——土豆和纱线。

  • 用于处理数据的工具——锅、搅拌机、烤箱、编织棒。

  • 一个预期的结果。在我们的例子中,是给你的脚和你的胃的东西。只是别把它们搞混了。

无论你如何称呼它们——成语、行话、小语言——你都会在各处看到它们的例子。这些行话为懂它的人节省了时间,而对我们其他人则使事情变得神秘。试着解密一篇关于桥牌的报纸专栏,如果你不玩这个游戏,或者解释一篇科学论文,如果你不是科学家(甚至如果你是,但在不同领域)。

小程序

你会在计算机程序中看到所有这些想法,它们本身就像小语言,专门为人类告诉计算机要做什么而设计。我用编织图案和食谱来说明编程并不神秘。这在很大程度上取决于学习正确的词汇和规则。

现在,如果单词和规则不多,并且你不需要一次学习太多东西,这会帮助很多。我们的大脑一次只能记住那么多东西。

让我们最终看一个真正的计算机程序(示例 1-1)。你觉得这个程序做什么?

示例 1-1. countdown.py
for countdown in 5, 4, 3, 2, 1, "hey!":
    print(countdown)

如果你猜测这是一个 Python 程序,它打印以下行

5
4
3
2
1
hey!

那么你就知道 Python 比食谱或编织图案更容易学习。而且你可以在舒适和安全的桌子旁练习编写 Python 程序,远离热水和尖锐的棍子的危险。

Python 程序有一些特殊的单词和符号——forinprint、逗号、冒号、括号等等,这些是语言语法(规则)的重要部分。好消息是,Python 具有更好的语法,而且需要记住的东西比大多数计算机语言少。它似乎更自然——几乎像一个食谱。

示例 1-2 是另一个微小的 Python 程序;它从一个 Python 列表 中选择一个哈利波特法术并打印它。

示例 1-2. spells.py
spells = [
    "Riddikulus!",
    "Wingardium Leviosa!",
    "Avada Kedavra!",
    "Expecto Patronum!",
    "Nox!",
    "Lumos!",
    ]
print(spells[3])

个体法术是 Python 字符串(文本字符序列,用引号括起来)。它们由逗号分隔,并且放在由方括号([])定义的 Python 列表中。单词spells是一个变量,给列表命名,这样我们就可以对其进行操作。在这种情况下,程序会打印第四个法术:

Expecto Patronum!

为什么我们说3,如果我们想要第四个?Python 列表(如spells)是从列表开始的值的序列,通过它们从列表开始的偏移量来访问。第一个值在偏移量0处,第四个值在偏移量3处。

注意

人们从 1 开始计数,所以从 0 开始计数可能看起来很奇怪。以偏移量而不是位置来思考有助于理解。是的,这是计算机程序有时与普通语言用法不同的示例。

列表在 Python 中是非常常见的数据结构,并且第七章展示了如何使用它们。

示例 1-3 中的程序打印了一个 Three Stooges 的引用,但是通过说它的人而不是在列表中的位置来引用。

示例 1-3. quotes.py
quotes = {
    "Moe": "A wise guy, huh?",
    "Larry": "Ow!",
    "Curly": "Nyuk nyuk!",
    }
stooge = "Curly"
print(stooge, "says:", quotes[stooge])

如果你运行这个小程序,它会打印以下内容:

Curly says: Nyuk nyuk!

quotes是一个变量,它命名了一个 Python 字典——一组唯一的(在这个示例中是三个小丑的名字)和相关的(这里是这些小丑的著名说法)。使用字典,你可以通过名称存储和查找东西,这通常是列表的有用替代方法。

spells示例使用方括号([])创建了一个 Python 列表,而quotes示例使用花括号({},与 Curly 无关)创建了一个 Python 字典。此外,冒号(:)用于将字典中的每个键与其值关联起来。你可以在第八章中读到更多关于字典的内容。

希望这不会让你一下子记住太多语法规则。在接下来的几章中,你将逐步了解更多这些小规则。

更大的程序

现在,让我们完全不同的内容:示例 1-4 展示了一个执行更复杂任务的 Python 程序。暂时不要期望能理解程序的工作原理;这正是本书的目的!它的目的是让你了解典型的非平凡 Python 程序的外观和感觉。如果你懂其他编程语言,可以评估一下 Python 的差异。即使还不了解 Python,你能在阅读程序解释之前大致猜到每行代码的作用吗?你已经见过 Python 列表和字典的示例,这里还包含了更多功能。

在本书早期版本中,示例程序连接到 YouTube 网站并获取其最受欢迎视频的信息,如“查理咬我的手指”。这个功能在第二版印刷后不久停止工作。那时,Google 停止支持此服务,导致标志性的示例程序无法继续运行。我们的新 示例 1-4 前往另一个网站,这个网站应该能持续更长时间——位于 互联网档案馆Wayback Machine,这是一个免费服务,保存了二十年来数十亿个网页(以及电影、电视节目、音乐、游戏和其他数字文物)。你将在 第 18 章 中看到更多这样的 网络 API 示例。

程序会要求你输入一个 URL 和一个日期。然后,它询问 Wayback Machine 是否在那个日期左右保存了该网站的副本。如果找到了,它将把信息返回给 Python 程序,打印出 URL 并在你的网络浏览器中显示。这个例子展示了 Python 如何处理各种任务:获取你输入的内容、通过互联网与网站进行通信、获取一些内容、从中提取一个 URL,并说服你的网络浏览器显示该 URL。

如果我们得到一个充满 HTML 格式文本的普通网页,我们需要弄清如何显示它,这是一项需要交给网络浏览器处理的大量工作。我们也可以尝试提取我们想要的部分(详见 第 18 章 关于 网页抓取 的更多细节)。无论选择哪种方式,都会增加工作量和程序的复杂度。相反,Wayback Machine 返回的数据是 JSON 格式。JSON(JavaScript 对象表示法)是一种人类可读的文本格式,描述了其中数据的类型、值和顺序。它是另一种小语言,已成为不同计算机语言和系统之间交换数据的流行方式。你将在 第 12 章 进一步了解 JSON。

Python 程序可以将 JSON 文本转换为 Python 数据结构 —— 就像你自己编写程序创建它们一样。我们的小程序只选择了一部分(来自 Internet Archive 网站的旧页面的 URL)。再次说明,这是一个完整的 Python 程序,你可以自己运行。我们只包含了少量的错误检查,只是为了保持示例的简洁。行号不是程序的一部分;它们包含在内,以帮助你跟随我们在程序后提供的描述。

示例 1-4. archive.py
1 import webbrowser
2 import json
3 from urllib.request import urlopen
4 
5 print("Let's find an old website.")
6 site = input("Type a website URL: ")
7 era = input("Type a year, month, and day, like 20150613: ")
8 url = "http://archive.org/wayback/available?url=%s&timestamp=%s" % (site, era)
9 response = urlopen(url)
10 contents = response.read()
11 text = contents.decode("utf-8")
12 data = json.loads(text)
13 try:
14     old_site = data["archived_snapshots"]["closest"]["url"]
15     print("Found this copy: ", old_site)
16     print("It should appear in your browser now.")
17     webbrowser.open(old_site)
18 except:
19     print("Sorry, no luck finding", site)

这个小 Python 程序在几行中做了很多事情,而且相当易读。你现在还不知道所有这些术语,但在接下来的几章中你会了解到。以下是每一行正在发生的事情:

  1. 导入(使这个程序可用)来自 Python 标准库模块名为webbrowser的所有代码。

  2. 导入 Python 标准库模块json的所有代码。

  3. 仅从标准库模块urllib.request中导入urlopen 函数

  4. 一个空行,因为我们不想感到拥挤。

  5. 将一些初始文本打印到您的显示器上。

  6. 打印一个关于 URL 的问题,读取你输入的内容,并保存在一个名为site的程序 变量 中。

  7. 打印另一个问题,这次读取年、月和日,并将其保存在名为era的变量中。

  8. 构造一个名为url的字符串变量,以使 Wayback Machine 查找您输入的站点和日期的副本。

  9. 连接到该 URL 的 Web 服务器并请求特定的Web 服务

  10. 获取响应数据并将其分配给变量contents

  11. 解码 contents为 JSON 格式的文本字符串,并将其分配给变量text

  12. text转换为data —— Python 数据结构。

  13. 错误检查:try运行接下来的四行,如果任何一个失败,则运行程序的最后一行(在except之后)。

  14. 如果我们找到了这个站点和日期的匹配项,则从一个三级 Python 字典中提取其值。注意,这一行和接下来的两行是缩进的。这就是 Python 如何知道它们属于前面的try行。

  15. 打印我们找到的 URL。

  16. 打印下一行执行后会发生什么。

  17. 在你的浏览器中显示我们找到的 URL。

  18. 如果前面的四行有任何失败,Python 跳到这里。

  19. 如果失败了,打印一条消息和我们正在查找的站点。这是缩进的,因为它只能在前面的except行运行时运行。

当我在终端窗口中运行这个命令时,我输入了一个站点 URL 和一个日期,并得到了这段文本输出:

$ python archive.py
Let's find an old website.
Type a website URL: lolcats.com
Type a year, month, and day, like 20150613: 20151022
Found this copy: http://web.archive.org/web/20151102055938/http://www.lolcats.com/
It should appear in your browser now.

并且图 1-3 显示了在我的浏览器中显示的内容。

inp2 0103

图 1-3. 来自 Wayback Machine

在前面的示例中,我们使用了 Python 的一些标准库模块(安装 Python 时包含的程序),但它们并非神圣不可侵犯。Python 还拥有大量优秀的第三方软件。示例 1-5 是一个重写,使用名为requests的外部 Python 软件包访问 Internet Archive 网站。

示例 1-5. archive2.py
1 import webbrowser
2 import requests
3 
4 print("Let's find an old website.")
5 site = input("Type a website URL: ")
6 era = input("Type a year, month, and day, like 20150613: ")
7 url = "http://archive.org/wayback/available?url=%s&timestamp=%s" % (site, era)
8 response = requests.get(url)
9 data = response.json()
10 try:
11     old_site = data["archived_snapshots"]["closest"]["url"]
12     print("Found this copy: ", old_site)
13     print("It should appear in your browser now.")
14     webbrowser.open(old_site)
15 except:
16     print("Sorry, no luck finding", site)

新版本更短,我猜对大多数人来说更易读。您将在 第十八章 中详细了解requests,以及在 第十一章 中了解 Python 的外部编写软件。

Python 在现实世界中

那么,学习 Python 是否值得时间和精力?Python 自 1991 年以来就存在(比 Java 老,比 C 新),一直稳居最受欢迎的五大计算机语言之列。人们受雇编写 Python 程序——比如您每天使用的严肃内容,如 Google、YouTube、Instagram、Netflix 和 Hulu。我在许多领域的生产应用中都使用了它。Python 以其高效率而著称,适合快节奏的组织。

您会发现 Python 存在于许多计算环境中,包括以下几种:

  • 监视器或终端窗口中的命令行

  • 包括 Web 在内的图形用户界面(GUI)

  • Web 的客户端和服务器端

  • 支持大型流行站点的后端服务器

  • (由第三方管理的服务器)

  • 移动设备

  • 嵌入式设备

Python 程序从一次性脚本(比如本章中到目前为止看到的那些)到百万行系统都有。

2018 年 Python 开发者调查 提供了有关 Python 在计算世界中当前地位的数据和图表。

我们将看到它在网站、系统管理和数据处理中的应用。在最后的章节中,我们将看到 Python 在艺术、科学和商业中的具体用途。

Python 与来自 X 星球的语言相比

Python 与其他语言相比如何?在何时何地选择一种而不是另一种?在本节中,我展示了其他语言的代码示例,这样您就可以看到竞争情况。如果您没有使用过这些语言,您不需要理解它们。(当您看到最后的 Python 示例时,您可能会因为没有必要与其他语言一起工作而感到宽慰。)

每个程序都应打印一个数字,并对语言做简要介绍。

如果您使用终端或终端窗口,那么读取您输入的内容并运行它并显示结果的程序称为shell程序。Windows 的 shell 称为cmd,它运行带有后缀.bat批处理文件。Linux 和其他类 Unix 系统(包括 macOS)有许多 shell 程序。最流行的称为bashsh。Shell 具有简单的功能,如简单逻辑和将通配符符号如*扩展为文件名。您可以将命令保存在称为shell 脚本的文件中,以后运行它们。这些可能是您作为程序员遇到的第一个程序。问题在于 shell 脚本在超过几百行后不容易扩展,并且比其他语言慢得多。下一段显示了一个小的 shell 程序:

#!/bin/sh
language=0
echo "Language $language: I am the shell. So there."

如果您将这段代码保存为test.sh并用sh test.sh运行它,您会在显示器上看到以下内容:

Language 0: I am the shell. So there.

旧有的CC++是相当底层的语言,当速度最重要时使用。您的操作系统及其许多程序(包括计算机上的python程序)可能是用 C 或 C++编写的。

这两种语言更难学习和维护。您需要注意许多细节,如内存管理,这可能导致程序崩溃和难以诊断的问题。这是一个小的 C 程序:

#include <stdio.h>
int main(int argc, char *argv[]) {
    int language = 1;
    printf("Language %d: I am C! See? Si!\n", language);
    return 0;
}

C++具有 C 家族的特征,但也发展了一些独特的功能:

#include <iostream>
using namespace std;
int main() {
    int language = 2;
    cout << "Language " << language << \
       ": I am C++!  Pay no attention to my little brother!" << \
       endl;
    return(0);
}

JavaC#是 C 和 C++的继任者,避免了它们前辈的一些问题——特别是内存管理问题——但有时会显得有些冗长。接下来的示例展示了一些 Java:

public class Anecdote {
    public static void main (String[] args) {
        int language = 3;
        System.out.format("Language %d: I am Java! So there!\n", language);
    }
}

如果您还没有用过这些语言编写程序,您可能会想知道:所有那些东西是什么?我们只想打印一行简单的代码。一些语言带有大量的语法装备。您将在第二章中了解更多信息。

C、C++和 Java 是静态语言的示例。它们要求您在使用变量之前指定一些低级细节,如数据类型。附录 A 显示了像整数这样的数据类型在计算机中有特定数量的位,并且只能执行整数操作。相比之下,动态语言(也称为脚本语言)在使用变量之前不强制您声明变量类型。

多年来,通用的动态语言是Perl。Perl 非常强大,拥有广泛的库。然而,它的语法可能有些笨拙,在过去几年中似乎失去了向 Python 和 Ruby 的势头。这个例子向您展示了 Perl 的一个名言:

my $language = 4;
print "Language $language: I am Perl, the camel of languages.\n";

Ruby是一种较新的语言。它从 Perl 借鉴了一些,主要因为Ruby on Rails,一个 Web 开发框架而流行。它在很多和 Python 相同的领域中使用,选择其中之一可能归结为品味或者您特定应用程序的可用库。这是一个 Ruby 片段:

language = 5
puts "Language #{language}: I am Ruby, ready and aglow."

PHP,如下例所示,非常流行于 Web 开发,因为它方便地结合了 HTML 和代码。然而,PHP 语言本身有一些陷阱,并且在 Web 之外的通用语言中并不流行。这是它的样子:

<?PHP
$language = 6;
echo "Language $language: I am PHP, a language and palindrome.\n";
?>

Go(或者Golang,如果你想要在 Google 中搜索)是一种近来的语言,试图在高效和友好之间找到平衡:

package main

import "fmt"

func main() {
  language := 7
  fmt.Printf("Language %d: Hey, ho, let's Go!\n", language)
}

另一种现代的 C 和 C++的替代品是Rust

fn main() {
    println!("Language {}: Rust here!", 8)

还剩下谁?哦对了,Python

language = 9
print(f"Language {language}: I am Python. What's for supper?")

为什么选择 Python?

其中一个原因,虽然不一定是最重要的,是它的流行度。通过各种指标来看,Python 是:

  • 作为最快增长的主要编程语言,正如你在图 1-4 中看到的那样。

  • 2019 年 6 月的TIOBE 指数的编辑们说:“本月 Python 在 TIOBE 指数中再次达到了历史新高的 8.5%。如果 Python 能保持这样的增长速度,它可能在 3 到 4 年内取代 C 和 Java,成为世界上最流行的编程语言。”

  • 2018 年的年度编程语言(TIOBE),并且在IEEE SpectrumPyPL中排名靠前。

  • 在顶级美国学院中,最流行的初级计算机科学课程语言。

  • 法国高中的官方教学语言。

inp2 0104

图 1-4. Python 在主要编程语言增长中处于领先地位

近年来,在数据科学和机器学习领域变得极其流行。如果你想在一个有趣的领域找到一份高薪的编程工作,Python 现在是一个不错的选择。而且,如果你在招聘,有越来越多经验丰富的 Python 开发人员。

但是为什么它如此流行?编程语言并不是非常有吸引力。有哪些潜在原因呢?

Python 是一种优秀的通用高级语言。其设计使得它非常可读,这比听起来更重要。每个计算机程序只写一次,但要被多次阅读和修订,通常由多人完成。可读性高使得它更容易学习和记忆;因此,更可写。与其他流行的语言相比,Python 具有渐进的学习曲线,使您更快进入生产状态,但它又有深度,您可以随着经验的积累探索。

Python 相对简洁的语法使你能够写出比静态语言中等价程序更小的程序。研究表明,无论使用何种语言,程序员每天产生的代码行数大致相同,因此,减半代码行数就会使你的生产力翻倍,就这么简单。Python 是许多公司的“不那么秘密武器”,这些公司认为这很重要。

当然,Python 是免费的,无论是像啤酒(价格)还是言论(自由)。用 Python 写任何你想要的东西,在任何地方都可以自由使用。没有人能读懂你的 Python 程序然后说,“你写的这个小程序不错啊,要是出了什么问题就太可惜了。”

Python 几乎可以在任何地方运行,并且“电池已包括”——其标准库中包含大量有用的软件。本书介绍了许多标准库和有用的第三方 Python 代码示例。

但是,也许使用 Python 的最好理由是一个意外的理由:人们通常享受使用它进行编程,而不是把它看作是必要的恶来完成工作。它不会妨碍你。一个常见的说法是它“符合你的思维方式”。通常,开发人员会说,在需要使用其他语言时,他们会错过一些 Python 的设计。这将 Python 与其大多数同行区分开来。

为什么选择 Python?

Python 并不是适合每种情况的最佳语言。

Python 并非默认安装在所有地方。附录 B 向您展示如何在计算机上安装 Python(如果您尚未安装)。

对于大多数应用来说,Python 的速度已经足够快,但对于一些要求更高的应用来说可能还不够快。如果你的程序大部分时间都在计算东西(技术术语是CPU-bound),那么用 C、C++、C#、Java、Rust 或 Go 写的程序通常会比 Python 等效程序运行得更快。但并非总是如此!

这里有一些解决方案:

  • 有时在 Python 中,一个更好的算法(逐步解决方案)胜过 C 中的一个低效算法。Python 更快的开发速度让你有更多时间尝试不同的解决方案。

  • 在许多应用程序中(特别是 Web 应用程序),程序在等待来自网络上某个服务器的响应时会无所事事。CPU(中央处理单元,计算机的芯片,负责所有计算)几乎没有参与;因此,静态和动态程序的端到端时间会非常接近。

  • 标准 Python 解释器是用 C 编写的,可以通过 C 代码进行扩展。我在第十九章中稍作讨论。

  • Python 解释器变得越来越快。Java 在早期非常慢,进行了大量的研究和资金投入来加快速度。Python 不归任何公司所有,因此其改进更为渐进。在“PyPy”中,我谈到了PyPy项目及其影响。

  • 你可能有一个非常苛刻的应用程序,无论你做什么,Python 都无法满足你的需求。通常的替代方案是 C、C++ 和 Java。Go(感觉像 Python,但性能像 C)或 Rust 也值得一试。

Python 2 与 Python 3

一个中等大小的复杂性是 Python 有两个版本。Python 2 已经存在很长时间,预装在 Linux 和 Apple 计算机上。它是一种优秀的语言,但没有十全十美。在计算机语言中,像许多其他领域一样,有些错误是表面的,容易修复,而其他则很难。难以修复的问题是不兼容的:用这些问题编写的新程序将无法在旧的 Python 系统上工作,而在修复之前编写的旧程序将无法在新系统上工作。

Python 的创始人(Guido van Rossum)和其他人决定将难以修复的问题一起打包,并在 2008 年作为 Python 3 引入。Python 2 是过去,Python 3 是未来。Python 2 的最终版本是 2.7,它将在一段时间内存在,但这是终点;不会有 Python 2.8。Python 2 的语言支持终止日期是 2020 年 1 月。安全和其他修复将不再进行,许多知名的 Python 软件包将在那时放弃对 Python 2 的支持。操作系统很快将放弃 Python 2 或将 Python 3 设为默认版本。流行的 Python 软件向 Python 3 的转换是逐步进行的,但现在我们已经远远超过了临界点。所有新的开发将使用 Python 3。

本书关注的是 Python 3。它几乎与 Python 2 相同。最明显的变化是在 Python 3 中print是一个函数,因此你需要用括号将其参数括起来。最重要的变化是对Unicode字符的处理,这在第十二章中有详细介绍。我会在其他重要的不同点出现时指出它们。

安装 Python

为了不使本章混乱,你可以在附录 B 中找到关于如何安装 Python 3 的详细信息。如果你没有 Python 3,或者不确定,去那里看看你需要为你的计算机做什么。是的,这有点麻烦(具体来说,是右前部的麻烦),但你只需做一次。

运行 Python

安装了 Python 3 的工作副本后,你可以用它来运行本书中的 Python 程序以及你自己的 Python 代码。你到底如何运行 Python 程序?主要有两种方式:

  • Python 的内置交互解释器(也称为shell)是尝试小型程序的简便方法。你逐行输入命令并立即看到结果。由于输入和显示紧密耦合,你可以更快地进行实验。我将使用交互解释器演示语言特性,你也可以在自己的 Python 环境中输入相同的命令。

  • 对于其他所有内容,将你的 Python 程序存储在文本文件中,通常使用*.py*扩展名,并通过输入python后跟这些文件名来运行它们。

让我们现在尝试这两种方法。

使用交互式解释器

本书中的大多数代码示例使用内置的交互式解释器。当你输入与示例中相同的命令并获得相同的结果时,你就知道你走在正确的道路上了。

你可以通过在计算机上输入主 Python 程序的名称来启动解释器:它应该是pythonpython3或类似的名称。在本书的其余部分,我们假设它称为python;如果你的命名不同,请在代码示例中看到python时输入那个名称。

交互式解释器几乎与 Python 文件的工作方式完全相同,只有一个例外:当你输入有值的东西时,交互式解释器会自动为你打印其值。这不是 Python 语言的一部分,只是解释器的一个特性,可以帮助你省去每次都输入print()的麻烦。例如,如果你在解释器中输入数字27,它将被回显到你的终端(如果你在文件中有一行27,Python 不会感到沮丧,但当你运行程序时不会看到任何打印输出):

$ python
Python 3.7.2 (v3.7.2:9a3ffc0492, Dec 24 2018, 02:44:43)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 27
27
注意

在上一个示例中,$是一个样本系统提示符,用于在终端窗口中输入类似python的命令。我们在本书的代码示例中使用它,尽管你的提示符可能不同。

顺便说一句,在解释器中随时使用print()也可以打印输出:

>>> print(27)
27

如果你在交互式解释器中尝试了这些示例,并看到了相同的结果,那么你刚刚运行了一些真实(虽然很小)的 Python 代码。在接下来的几章中,你将从一行代码逐步过渡到更长的 Python 程序。

使用 Python 文件

如果你将27单独放在一个文件中并通过 Python 运行它,它会运行,但不会打印任何东西。在正常的非交互式 Python 程序中,你需要调用print函数来打印输出:

print(27)

让我们创建一个 Python 程序文件并运行它:

  1. 打开你的文本编辑器。

  2. 输入print(27)这一行,就像这里显示的一样。

  3. 将此保存为名为test.py的文件。确保将其保存为纯文本而不是 RTF 或 Word 等“丰富”格式。你不需要为 Python 程序文件使用*.py*后缀,但这有助于你记住它们的类型。

  4. 如果你使用的是 GUI——几乎所有人都是——打开一个终端窗口。²

  5. 通过输入以下内容来运行你的程序:

$ python test.py

你应该看到一行输出:

27

成功了吗?如果是这样,恭喜你运行了你的第一个独立 Python 程序。

下一步是什么?

你将向一个真实的 Python 系统输入命令,它们需要遵循合法的 Python 语法。与一次性倾泻语法规则不同,我们将在接下来的几章中逐步介绍它们。

开发 Python 程序的基本方法是使用纯文本编辑器和终端窗口。我在本书中使用纯文本显示,有时显示交互式终端会话,有时显示 Python 文件片段。你应该知道还有许多优秀的 集成开发环境(IDE)适用于 Python。这些可能具有带有高级文本编辑和帮助显示的 GUI。你可以在第十九章中了解其中一些的详细信息。

你的禅意时刻

每种计算机语言都有自己的风格。在前言中,我提到过表达自己的 Pythonic 方法通常会有。Python 中嵌入了一点自由诗,简洁地表达了 Python 的哲学(据我所知,Python 是唯一一个包含这样一个彩蛋的语言)。只需在交互式解释器中键入 import this,然后在需要这个禅意时按下回车键:

>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one--and preferably only one--obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea--let's do more of those!

我会在整本书中举例说明这些情感。

即将发生的事情

下一章将讨论 Python 的数据类型和变量。这将为接下来的章节做准备,这些章节将详细介绍 Python 的数据类型和代码结构。

要做的事情

本章是 Python 语言的介绍——它的功能、外观以及它在计算世界中的位置。在每章末尾,我建议一些迷你项目,帮助你记住你刚刚阅读的内容,并为即将到来的内容做好准备。

1.1 如果你的计算机上还没有安装 Python 3,请立即安装。阅读附录 B 获取有关你的计算机系统的详细信息。

1.2 启动 Python 3 交互式解释器。同样的详情请参阅附录 B。它应该打印几行关于自身的信息,然后是一行以 >>> 开头的单行。这就是你输入 Python 命令的提示。

1.3 与解释器玩一会儿。像使用计算器一样使用它,键入 8 * 9。按回车键查看结果。Python 应该打印 72

1.4 输入数字 47,然后按回车键。下一行是否打印出 47

1.5 现在,键入 print(47),然后按 Enter 键。这也在下一行为你打印出 47 吗?

¹ 通常只在食谱书和舒适的推理小说中找到。

² 如果你不确定这意味着什么,请参阅附录 B 获取不同操作系统的详细信息。

第二章:数据:类型、值、变量和名称

选一个好名字胜过大财。

箴言 22:1

在计算机底层,一切都只是一串比特(见附录 A)。计算的一个洞察是,我们可以按任意方式解释这些比特——作为不同大小和类型的数据(数字、文本字符),甚至是计算机代码本身。我们使用 Python 来定义这些比特的块,以及在 CPU 中的输入输出。

我们从 Python 的数据类型和它们可以包含的开始。然后我们看如何将数据表示为字面值变量

Python 数据是对象。

你可以将计算机的内存视为一系列长长的货架。每个货架上的插槽宽度为一个字节(八比特),编号从 0(第一个)到末尾。现代计算机具有数十亿字节的内存(吉字节),因此这些货架将填满一个巨大的虚构仓库。

你的操作系统为 Python 程序提供对计算机内存的一些访问权限。该内存用于程序本身的代码以及它使用的数据。操作系统确保程序无法读取或写入其他内存位置而不通过某种方式获得权限。

程序跟踪它们的比特位于何处(内存位置),以及它们是什么类型(数据类型)。对于计算机来说,一切都只是比特。同样的比特模式可能表示整数 65 或文本字符 A

不同类型可能使用不同数量的比特。当你听说“64 位机器”时,这意味着整数使用 64 比特(8 字节)。

有些语言在内存中直接取出和拨弄这些原始值,并跟踪它们的大小和类型。Python 不直接处理这些原始数据值,而是将每个数据值包装在内存中作为一个对象——布尔值、整数、浮点数、字符串,甚至大型数据结构、函数和程序。关于如何在 Python 中定义自己的对象,有一个完整的章节(第十章)。但现在,我们只是讨论处理基本内置数据类型的对象。

使用内存货架的类比,你可以把对象想象成变量大小的盒子,占据这些货架上的空间,如图 2-1 所示。Python 制作这些对象盒子,将它们放在货架上的空位上,并在它们不再使用时移除它们。

inp2 0201

图 2-1. 一个对象就像一个盒子;这个是一个值为7的整数。

在 Python 中,对象是一个包含至少以下内容的数据块:

  • 定义对象能做什么的type(见下一节)。

  • 一个独特的id,以区别于其他对象。

  • 与其类型一致的value

  • reference count 跟踪这个对象被使用的频率。

它的标识就像它在架子上的位置,一个唯一的标识符。它的类型就像盒子上的工厂印记,说明它能做什么。如果一个 Python 对象是整数,它的类型是 int,并且可以(除了其他事情,你将在第 3 章中看到)加到另一个 int 上。如果我们把盒子想象成由透明塑料制成,我们可以看到里面的。在不久的几节中,当我们谈论变量和名称时,你将学到引用计数的用法。

类型

表 2-1 展示了 Python 中的基本数据类型。第二列(类型)包含该类型的 Python 名称。第三列(可变?)指示值在创建后是否可以更改,这在下一节中有更详细的解释。示例显示了该类型的一个或多个字面示例。最后一列(章节)指向本书中详细讲述该类型的章节。

表 2-1. Python 的基本数据类型

名称 类型 可变? 示例 章节
布尔值 bool True, False 第 3 章
整数 int 47, 25000, 25_000 第 3 章
浮点数 float 3.14, 2.7e5 第 3 章
复数 complex 3j, 5 + 9j 第 22 章
文本字符串 str 'alas', "alack", '''a verse attack''' 第 5 章
列表 list ['Winken', 'Blinken', 'Nod'] 第 7 章
元组 tuple (2, 4, 8) 第 7 章
字节 bytes b'ab\xff' 第 12 章
字节数组 bytearray bytearray(...) 第 12 章
集合 set set([3, 5, 7]) 第 8 章
冻结集合 frozenset frozenset(['Elsa', 'Otto']) 第 8 章
字典 dict {'game': 'bingo', 'dog': 'dingo', 'drummer': 'Ringo'} 第 8 章

在这些基本数据类型的章节后,您将看到如何在第 10 章中创建新类型。

可变性

无物能长存,唯变性永恒。

Percy Shelley

类型还决定了盒子中包含的数据是否可以更改(可变)或是常量(不可变)。把一个不可变对象想象成一个密封的盒子,但有透明的侧面,就像图 2-1;你可以看到值,但不能改变它。按同样的类比,可变对象就像有盖子的盒子:不仅可以看到里面的值,还可以改变它;但是,你不能改变它的类型。

Python 是强类型的,这意味着对象的类型即使其值是可变的,也不会改变(图 2-2)。

inp2 0202

图 2-2. 强类型并不意味着要更用力按键

字面值

有两种方法在 Python 中指定数据值:

  • 字面

  • 变量

在接下来的章节中,你将看到如何为不同的数据类型指定字面值的详细信息——整数是一串数字,浮点数包含小数点,文本字符串被引号包围,等等。但是,在本章的其余部分,为了避免手指起茧,我们的示例将仅使用短小的十进制整数和一两个 Python 列表。十进制整数就像数学中的整数一样:从09的一串数字。还有一些额外的整数细节(例如符号和非十进制基数),我们将在第三章中讨论。

变量

现在,我们来到了计算语言的一个关键概念。

Python,像大多数计算机语言一样,允许您定义变量——即计算机内存中值的名称,您希望在程序中使用。

Python 变量名有一些规则:

  • 它们只能包含以下这些字符:

    • 小写字母(az

    • 大写字母(AZ

    • 数字(09

    • 下划线(_

  • 它们是区分大小写的thingThingTHING 是不同的名称。

  • 它们必须以字母或下划线开头,不能以数字开头。

  • 以下划线开头的名称被特殊对待(你可以在第九章中阅读有关此处的内容)。

  • 它们不能是 Python 的保留词(也称为关键字)之一。

保留词¹有:

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

在 Python 程序中,您可以找到保留词与

>>> help("keywords")

或:

>>> import keyword
>>> keyword.kwlist

以下是有效的名称:

  • a

  • a1

  • a_b_c___95

  • _abc

  • _1a

然而,这些名称是无效的:

  • 1

  • 1a

  • 1_

  • name!

  • another-name

分配

在 Python 中,您使用 =分配一个值给一个变量。

注意

我们都在小学算术中学到了 = 的意思是等于。那么为什么许多计算机语言,包括 Python,使用 = 进行赋值?一个原因是标准键盘缺乏逻辑的替代品,如左箭头键,并且 = 似乎不会太令人困惑。另外,在计算机程序中,您使用分配的频率比测试相等要高得多。

程序像代数。当您在学校学习数学时,您看到了这样的方程式:

y = x + 12

您可以通过“插入”一个值来解决方程式中的等式。如果您给 x 赋值 55 + 12 就是 17,所以 y 的值将是 17。为 x 插入 6 以获得 y18,依此类推。

计算机程序行看起来可能像方程式,但它们的含义是不同的。在 Python 和其他计算机语言中,xy变量。Python 知道像 125 这样的裸数字是字面整数。因此,这里有一个小小的 Python 程序模仿这个方程式,打印出 y 的结果值:

>>> x = 5
>>> y = x + 12
>>> y
17

这是数学和程序之间的重大区别:在数学中,=表示两边的相等,但在程序中表示赋值将右边的值赋给左边的变量

在程序中,右侧的每一部分都需要有一个值(这称为初始化)。右侧可以是字面值,或者已经被赋值的变量,或者它们的组合。Python 知道512是字面整数。第一行将整数值5赋给变量x。现在我们可以在下一行中使用变量x。当 Python 读取y = x + 12时,它执行以下操作:

  • 看到中间的=

  • 知道这是一个任务。

  • 计算右侧(获取由x引用的对象的值并将其添加到12

  • 将结果分配给左侧变量y

然后在交互解释器中输入变量y的名称将打印其新值。

如果您在程序中以y = x + 12开头,Python 会生成一个异常(一个错误),因为变量x还没有值:

>>> y = x + 12
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined

您将在第九章中详细了解异常。在计算机术语中,我们会说这个x未初始化的。

在代数中,您可以反向工作,并将一个值分配给y来计算x。要在 Python 中做到这一点,您需要在赋值给左侧的x之前获取右侧的字面值和初始化变量。

>>> y = 5
>>> x = 12 - y
>>> x
7

变量是名称,不是位置

现在是时候提一个关于 Python 变量的关键点了:变量只是名称。这与许多其他计算机语言不同,这是 Python 特别要知道的一点,特别是当我们涉及到像列表这样的可变对象时。赋值不会复制一个值;它只是将一个名称绑定到包含数据的对象上。名称是对事物的引用,而不是事物本身。将名称想象成一个标签,附在计算机内存中的对象框上 (图 2-3)。

inp2 0203

图 2-3. 名称指向对象(变量a指向一个值为7的整数对象)

在其他语言中,变量本身有一个类型,并绑定到一个内存位置。您可以更改该位置的值,但它必须是相同的类型。这就是静态语言要求您声明变量类型的原因。Python 不需要,因为一个名称可以引用任何东西,我们通过“跟随字符串”到数据对象本身来获取值和类型。这节省了时间,但也有一些缺点:

  • 可能会拼错一个变量并且得到一个异常,因为它并不引用任何东西,而且 Python 并不像静态语言那样自动检查。第十九章展示了提前检查变量以避免这种情况的方法。

  • Python 的原始速度比像 C 这样的语言慢。它让计算机做更多的工作,这样你就不必亲自去做。

在交互解释器中尝试这个(在图 2-4 中可视化):

  1. 如前所述,将值7分配给名称a。这创建了一个包含整数值7的对象框。

  2. 打印a的值。

  3. a分配给b,使b也指向包含7的对象盒子。

  4. 打印变量b的值:

    >>> a = 7
    >>> print(a)
    7
    >>> b = a
    >>> print(b)
    7
    

inp2 0204

图 2-4. 复制一个名称(现在变量b也指向相同的整数对象)

在 Python 中,如果您想知道任何东西的类型(变量或字面值),可以使用type(*`thing`*)type()是 Python 的内置函数之一。如果要检查变量是否指向特定类型的对象,请使用isinstance(*`type`*)

>>> type(7)
<class 'int'>
>>> type(7) == int
True
>>> isinstance(7, int)
True
注意

当我提到函数时,我会在其后加上括号(()),以强调它是一个函数而不是变量名或其他东西。

让我们试试更多的字面值(5899.9'abc')和变量(ab):

>>> a = 7
>>> b = a
>>> type(a)
<class 'int'>
>>> type(b)
<class 'int'>
>>> type(58)
<class 'int'>
>>> type(99.9)
<class 'float'>
>>> type('abc')
<class 'str'>

是对象的定义;第十章详细介绍了类。在 Python 中,“类”和“类型”几乎意味着相同的事情。

正如您所见,当您在 Python 中使用变量时,它会查找它所引用的对象。在幕后,Python 正在忙碌地创建临时对象,这些对象将在一两行后被丢弃。

让我们重复一个早期的例子:

>>> y = 5
>>> x = 12 - y
>>> x
7

在这段代码片段中,Python 执行了以下操作:

  • 创建一个值为5的整数对象

  • 使变量y指向该5对象

  • 增加了值为5的对象的引用计数

  • 创建另一个值为12的整数对象

  • y指向的对象的值(5)中减去具有该值的(匿名)对象中的12的值

  • 将此值(7)分配给新的(到目前为止未命名的)整数对象

  • 让变量x指向这个新对象

  • 增加了x指向的这个新对象的引用计数

  • 查找x指向的对象的值(7)并打印它

当对象的引用计数达到零时,没有名称指向它,因此它不需要保留。 Python 有一个名为垃圾收集器的迷人工具,可以重新使用不再需要的内存。想象有人在那些内存架子后面,拉取过时的盒子进行回收利用。

在这种情况下,我们不再需要具有值5127的对象,也不需要变量xy。 Python 垃圾收集器可以选择将它们送往对象天堂²,或者出于性能原因保留一些,因为小整数通常被广泛使用。

分配给多个名称

您可以同时将值分配给多个变量名:

>>> two = deux = zwei = 2
>>> two
2
>>> deux
2
>>> zwei
2

重新分配名称

因为名称指向对象,所以更改分配给名称的值只是使名称指向新对象。旧对象的引用计数将减少,新对象的引用计数将增加。

复制

正如您在图 2-4 中看到的,将现有变量a赋值给名为b的新变量只是使b指向与a相同的对象。如果您选择任一ab标签并跟随它们的字符串,您将到达同一个对象。

如果对象是不可变的(比如整数),其值不能被更改,因此这两个名称本质上是只读的。试试这个:

>>> x = 5
>>> x
5
>>> y = x
>>> y
5
>>> x = 29
>>> x
29
>>> y
5

当我们将 x 分配给 y 时,这使得名称 y 指向 x 也指向的值为 5 的整数对象。更改 x 使其指向值为 29 的新整数对象。它没有更改 y 所指向的仍然是 5 的那个对象。

但如果两个名称都指向一个可变对象,则可以通过任一名称更改对象的值,并在使用任一名称时看到更改后的值。如果你一开始不知道这一点,可能会感到惊讶。

列表是一个可变的值数组,第七章详细介绍了它们。在本例中,ab分别指向具有三个整数成员的列表:

>>> a = [2, 4, 6]
>>> b = a
>>> a
[2, 4, 6]
>>> b
[2, 4, 6]

这些列表成员(a[0]a[1]a[2])本身就像名称,指向具有值 246 的整数对象。列表对象按顺序保留其成员。

现在通过名称 a 更改第一个列表元素,并看到 b 也已更改:

>>> a[0] = 99
>>> a
[99, 4, 6]
>>> b
[99, 4, 6]

当更改第一个列表元素时,它不再指向值为 2 的对象,而是指向值为 99 的新对象。列表仍然是 list 类型,但其值(列表元素及其顺序)是可变的。

选择好的变量名称

他说了真实的事情,但用了错误的名字来称呼它们。

伊丽莎白·巴雷特·勃朗宁

选择好变量名称是多么重要啊。在迄今为止的许多代码示例中,我一直在使用像 ax 这样的临时名称。在实际程序中,你将同时跟踪更多的变量,并且需要在简洁性和清晰性之间取得平衡。例如,键入 num_loons 要比 number``_of_loonsgaviidae_inventory 快,但比 n 更具解释性。

即将发生

数字!它们像你期望的那样令人兴奋。嗯,也许没有那么糟糕。³ 你将看到如何将 Python 用作计算器,以及一只猫如何创立了数字系统。

要做的事情

2.1 将整数值 99 分配给变量 prince,并打印它。

2.2 值 5 的类型是什么?

2.3 值 2.0 的类型是什么?

2.4 表达式 5 + 2.0 的类型是什么?

¹ asyncawait 是 Python 3.7 中的新功能。

² 或者是不合格对象之岛。

³ 8 看起来像一个雪人!

第三章:数字

行动是最大程度地为最大数量的人带来最大幸福的行动。

弗朗西斯·哈切森

在本章中,我们首先看一下 Python 最简单的内置数据类型:

  • 布尔值(其值为TrueFalse

  • 整数(如42100000000

  • 浮点数(带有小数点的数字,如3.14159,或者有时指数形式如1.0e8,表示10 的 8 次方,或者100000000.0

从某种意义上说,它们就像原子一样。本章中我们单独使用它们,而在后续章节中,您将看到如何将它们组合成像列表和字典这样更大的“分子”。

每种类型都有其使用的特定规则,并由计算机以不同方式处理。我还展示了如何使用像973.1416这样的字面值值,以及我在第二章中提到的变量

本章的代码示例都是有效的 Python 代码,但它们只是片段。我们将使用 Python 交互解释器,键入这些片段并立即查看结果。尝试在您的计算机上使用所安装的 Python 版本来运行它们。您可以通过>>>提示符识别这些示例。

布尔值

在 Python 中,布尔数据类型的唯一值为TrueFalse。有时直接使用它们,其他时候从它们的值评估“真假性”。特殊的 Python 函数bool()可以将任何 Python 数据类型转换为布尔值。

函数在第九章中有专门的章节,但现在您只需要知道函数有一个名称,零个或多个用逗号分隔的参数,用括号括起来,以及零个或多个返回值bool()函数接受任何值作为其参数,并返回布尔等价值。

非零数字被视为True

>>> bool(True)
True
>>> bool(1)
True
>>> bool(45)
True
>>> bool(-45)
True

零值也被视为False

>>> bool(False)
False
>>> bool(0)
False
>>> bool(0.0)
False

您将在第四章中看到布尔值的实用性。在后续章节中,您将看到如何将列表、字典和其他类型视为TrueFalse

整数

整数是整数——没有分数,没有小数点,没有花哨的东西。嗯,除了可能的初始符号。如果您希望以除了通常的十进制(10 进制)以外的其他方式表示数字,还有基数。

整数字面值

在 Python 中,任何数字序列都表示一个整数字面值

>>> 5
5

纯零(0)是有效的:

>>> 0
0

但是您不能以0开头,后面跟着介于19之间的数字:

>>> 05
  File "<stdin>", line 1
    05
     ^
SyntaxError: invalid token
注意

此 Python 异常警告您输入了违反 Python 规则的内容。我在“基数”中解释了这意味着什么。在本书中,您将看到更多异常的示例,因为它们是 Python 的主要错误处理机制。

您可以以0b0o0x开头一个整数。参见“基数”。

数字序列指定一个正整数。如果在数字前面加上+号,则数字保持不变:

>>> 123
123
>>> +123
123

要指定负整数,在数字前面插入一个-

>>> -123
-123

整数中不能有逗号:

>>> 1,000,000
(1, 0, 0)

与百万不同,你会得到一个元组(参见第七章了解更多关于元组的信息),包含三个值。但是你可以使用下划线(_)作为数字分隔符:¹

>>> million = 1_000_000
>>> million
1000000

实际上,你可以在第一个数字后的任何位置放置下划线;它们会被忽略:

>>> 1_2_3
123

整数运算

在接下来的几页中,我展示了 Python 作为一个简单计算器的示例。你可以通过使用表格中的数学操作符在 Python 中进行常规算术运算:

操作符 描述 示例 结果
+ 加法 5 + 8 13
- 减法 90 - 10 80
* 乘法 4 * 7 28
/ 浮点数除法 7 / 2 3.5
// 整数(截断)除法 7 // 2 3
% 取模(余数) 7 % 3 1
** 指数运算 3 ** 4 81

加法和减法的工作方式与你期望的一样:

>>> 5 + 9
14
>>> 100 - 7
93
>>> 4 - 10
-6

你可以包含任意数量的数字和操作符:

>>> 5 + 9 + 3
17
>>> 4 + 3 - 2 - 1 + 6
10

请注意,每个数字和操作符之间不需要有空格:

>>> 5+9   +      3
17

在风格上看起来更好,而且更容易阅读。

乘法也很简单:

>>> 6 * 7
42
>>> 7 * 6
42
>>> 6 * 7 * 2 * 3
252

除法更有趣一些,因为它有两种形式:

  • / 执行浮点数(十进制)除法

  • // 执行整数(截断)除法

即使你将整数除以整数,使用 / 也会给你一个浮点结果(浮点数稍后在本章讨论):

>>> 9 / 5
1.8

截断整数除法返回一个整数答案,丢弃任何余数:

>>> 9 // 5
1

无论使用哪种除法,将整数除以零都会引发 Python 异常,而不是撕开时空维度:

>>> 5 / 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> 7 // 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: integer division or modulo by z

整数和变量

所有前面的例子都使用了文字整数。你可以混合使用文字整数和已经赋予整数值的变量:

>>> a = 95
>>> a
95
>>> a - 3
92

你会记得从第二章中,a 是一个指向整数对象的名称。当我说 a - 3 时,我没有将结果再赋给 a,因此 a 的值没有改变:

>>> a
95

如果想要改变 a,你需要这样做:

>>> a = a - 3
>>> a
92

再次强调,这不是一个合法的数学方程,而是在 Python 中重新分配变量值的方式。在 Python 中,等号右侧的表达式先计算,然后再赋给左侧的变量。

如果有帮助,可以这样考虑:

  • a 减去 3

  • 将减法的结果赋给一个临时变量

  • 将临时变量的值赋给 a

>>> a = 95
>>> temp = a - 3
>>> a = temp

因此,当你说

>>> a = a - 3

Python 在等号右侧计算减法,记住结果,然后将其分配给 a 在加号左侧。这比使用临时变量更快、更整洁。

你可以通过在等号前放置操作符将算术运算符与赋值结合起来。例如,a -= 3 相当于 a = a - 3

>>> a = 95
>>> a -= 3
>>> a
92

这就像 a = a + 8 一样:

>>> a = 92
>>> a += 8
>>> a
100

这就像 a = a * 2 一样:

>>> a = 100
>>> a *= 2
>>> a
200

这是一个浮点除法的例子,例如 a = a / 3

>>> a = 200
>>> a /= 3
>>> a
66.66666666666667

现在让我们试试 a = a // 4 的缩写(截断整数除法):

>>> a = 13
>>> a //= 4
>>> a
3

% 字符在 Python 中有多种用法。当它在两个数字之间时,它产生第一个数除以第二个数的余数:

>>> 9 % 5
4

这里是如何同时获得截断的商和余数:

>>> divmod(9,5)
(1, 4)

否则,你可以分别计算它们:

>>> 9 // 5
1
>>> 9 % 5
4

你刚刚在这里看到了一些新东西:一个名为 divmod函数被给定整数 95,并返回一个两项的元组。正如我之前提到的,元组将在第七章亮相;函数则在第九章首次登场。

最后一个数学特性是指数运算符 **,它还允许你混合整数和浮点数:

>>> 2**3
8
>>> 2.0 ** 3
8.0
>>> 2 ** 3.0
8.0
>>> 0 ** 3
0

优先级

如果你输入以下内容,会得到什么?

>>> 2 + 3 * 4

如果你先做加法,2 + 35,然后 5 * 420。但如果你先做乘法,3 * 412,然后 2 + 1214。在 Python 中,与大多数语言一样,乘法比加法具有更高的优先级,所以第二个版本是你会看到的:

>>> 2 + 3 * 4
14

你如何知道运算符的优先级规则?在附录 E 中有一张大表列出了所有规则,但实际上我从不查看这些规则。只需通过添加括号来组合代码以执行你意图的计算,会更容易得多:

>>> 2 + (3 * 4)
14

这个带有指数的例子

>>> -5 ** 2
-25

是相同的

>>> - (5 ** 2)
-25

而且可能不是你想要的。括号可以明确表达:

>>> (-5) ** 2
25

这样,任何阅读代码的人都不需要猜测它的意图或查阅优先级规则。

基数

假设整数默认为十进制(十进制),除非你使用前缀来指定另一个基数。你可能永远不需要使用这些其他基数,但在某个时候你可能会在 Python 代码中见到它们。

通常我们有 10 个手指和 10 个脚趾,所以我们数 0, 1, 2, 3, 4, 5, 6, 7, 8, 9。然后,我们用完了个位数,并把一个1进位到“十位”,在个位数放一个010表示“1 十和 0 个”。不像罗马数字,阿拉伯数字没有单独表示10的字符。然后是1112,一直到19,进位成20(2 十和 0 个),以此类推。

基数是你可以使用多少位数,直到需要“进位为一”的数。在二进制(binary)中,唯一的位数是01。这就是著名的比特位0与普通的十进制 0 相同,而1与十进制 1 相同。然而,在二进制中,如果你将1加到1上,你会得到10(1 十进制二加 0 十进制一)。

在 Python 中,除了十进制以外,你可以用这些整数前缀表示字面整数的三种基数:

  • 0b0B 代表二进制(基数 2)。

  • 0o0O 代表八进制(基数 8)。

  • 0x0X 代表十六进制(基数 16)。

这些基数都是二的幂,某些情况下很方便,尽管你可能永远不需要使用除了好旧的十进制整数之外的任何东西。

解释器将这些打印出来作为十进制整数。让我们尝试每一个基数。首先是一个普通的十进制 10,这意味着1 十和 0 个一

>>> 10
10

现在,二进制(基数 2) 0b10 表示1(十进制)二和 0 个一

>>> 0b10
2

八进制(基数 8) 0o10 代表1(十进制)八和 0 个一

>>> 0o10
8

十六进制(基数 16) 0x10 表示1(十进制)十六和 0 个一

>>> 0x10
16

你可以反向操作,使用任何这些基数将整数转换为字符串:

>>> value = 65
>>> bin(value)
'0b1000001'
>>> oct(value)
'0o101'
>>> hex(value)
'0x41'

chr() 函数将一个整数转换为其单字符字符串等效物:

>>> chr(65)
'A'

ord() 反向操作:

>>> ord('A')
65

如果你想知道十六进制中所使用的“数字”,它们是:0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, 和 f0xa 是十进制 10,而 0xf 是十进制 15。在 0xf 上加 1,你得到 0x10(十进制 16)。

为什么要使用不同于十进制的基数?它们在位级操作中非常有用,详见第十二章,以及有关从一种进制转换到另一种进制的更多细节。

猫通常每只前爪有五个趾,每只后爪有四个趾,总共有 18 个。如果你曾经在他们穿着实验服的猫科学家们之间,他们经常讨论十八进制算术。我的猫切斯特,看到他在图 3-1 中悠闲地躺着,是一只多趾,给了他大约 22 或更多(很难分辨)的脚趾。如果他想要用它们来计算围绕碗的食物碎片,他可能会使用一个基数 22 的系统(这里称为切斯特数字系统),使用 09al

chester

图 3-1. 切斯特——一只出色的毛茸茸小伙子,切斯特数字系统的发明者

类型转换

要将其他 Python 数据类型转换为整数,请使用 int() 函数。

int() 函数接受一个输入参数并返回一个值,即输入参数的整数化等效物。这将保留整数部分并丢弃任何小数部分。

正如你在本章开头所见,Python 最简单的数据类型是布尔值,它只有 TrueFalse 两个值。当转换为整数时,它们分别表示 10

>>> int(True)
1
>>> int(False)
0

相反地,bool() 函数返回一个整数的布尔等效物:

>>> bool(1)
True
>>> bool(0)
False

将浮点数转换为整数只需去掉小数点后的所有内容:

>>> int(98.6)
98
>>> int(1.0e4)
10000

将浮点数转换为布尔值并不奇怪:

>>> bool(1.0)
True
>>> bool(0.0)
False

最后,这里有一个示例,从一个只包含数字的文本字符串中获取整数值(第五章)可能包含 _ 数字分隔符或初始 +- 号:

>>> int('99')
99
>>> int('-23')
-23
>>> int('+12')
12
>>> int('1_000_000')
1000000

如果字符串代表一个非十进制整数,你可以包括基数:

>>> int('10', 2) # binary
2
>>> int('10', 8) # octal
8
>>> int('10', 16) # hexadecimal
16
>>> int('10', 22) # chesterdigital
22

将整数转换为整数不会改变任何内容,但也不会造成伤害:

>>> int(12345)
12345

如果你试图转换一个看起来不像是数字的东西,你会得到一个异常

>>> int('99 bottles of beer on the wall')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: '99 bottles of beer on the wall'
>>> int('')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: ''

之前的文本字符串以有效的数字字符(99)开头,但随后出现了其他字符,这些字符使得int()函数无法处理。

int()可以从浮点数或数字字符串创建整数,但不能处理包含小数点或指数的字符串:

>>> int('98.6')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: '98.6'
>>> int('1.0e4')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: '1.0e4'

如果你混合使用数值类型,Python 有时会尝试自动进行类型转换:

>>> 4 + 7.0
11.0

布尔值False在与整数或浮点数混合时被视为00.0,而True则被视为11.0

>>> True + 2
3
>>> False + 5.0
5.0

一个整数有多大?

在 Python 2 中,int的大小可以限制为 32 位或 64 位,具体取决于你的 CPU;32 位可以存储任何从–2,147,483,648 到 2,147,483,647 的整数。

64 位的long类型允许存储范围从–9,223,372,036,854,775,808 到 9,223,372,036,854,775,807。在 Python 3 中,long类型已经消失,而int可以是任意大小,甚至可以比 64 位还大。你可以处理像谷歌(一个后面跟着一百个零的数,在 1920 年被一个九岁的男孩命名)这样的大数:

>>>
>>> googol = 10**100
>>> googol
100000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000
>>> googol * googol
100000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000

谷歌宇宙10**googol(如果你想自己尝试的话,有一千个零)。这是谷歌在决定用谷歌之前的一个建议名称,但他们在注册域名google.com之前没有检查其拼写。

在许多语言中,尝试这样做会导致称为整数溢出的问题,其中数字需要比计算机允许的空间更多,会产生各种不良影响。Python 处理谷歌般大的整数毫无问题。

浮点数

整数是整数,但 Python 中的浮点数(称为floats)带有小数点:

>>> 5.
5.0
>>> 5.0
5.0
>>> 05.0
5.0

浮点数可以在字母e后包含一个十进制整数指数:

>>> 5e0
5.0
>>> 5e1
50.0
>>> 5.0e1
50.0
>>> 5.0 * (10 ** 1)
50.0

你可以使用下划线(_)分隔数字以增加清晰度,就像你可以用于整数一样:

>>> million = 1_000_000.0
>>> million
1000000.0
>>> 1.0_0_1
1.001

浮点数的处理方式与整数类似:你可以使用操作符(+*///**%)以及divmod()函数。

要将其他类型转换为浮点数,你可以使用float()函数。和以前一样,布尔值表现得像小整数一样:

>>> float(True)
1.0
>>> float(False)
0.0

将整数转换为浮点数只会使其成为一个带有小数点的自豪所有者:

>>> float(98)
98.0
>>> float('99')
99.0

你可以将包含字符的字符串转换为有效的浮点数(数字、符号、小数点或e后跟一个指数):

>>> float('98.6')
98.6
>>> float('-1.5')
-1.5
>>> float('1.0e4')
10000.0

当你混合整数和浮点数时,Python 会自动将整数值提升为浮点数值:

>>> 43 + 2.
45.0

Python 还会将布尔值提升为整数或浮点数:

>>> False + 0
0
>>> False + 0.
0.0
>>> True + 0
1
>>> True + 0.
1.0

数学函数

Python 支持复数,并且具有常见的数学函数,如平方根、余弦等等。我们将它们留到第二十二章中,在那里我们还将讨论在科学环境中使用 Python。

即将发生

在下一章中,你将终于从单行的 Python 示例中毕业。通过if语句,你将学会如何使用代码做出决策。

要做的事情

本章介绍了 Python 的基本组成部分:数字、布尔值和变量。让我们在交互式解释器中尝试一些与它们相关的小练习。

3.1 一个小时有多少秒?使用交互式解释器作为计算器,将每分钟的秒数(60)乘以每小时的分钟数(同样是60)。

3.2 将前一个任务(每小时的秒数)的结果赋给名为seconds_per_hour的变量。

3.3 一天有多少秒?使用你的seconds_per_hour变量。

3.4 再次计算每天的秒数,但这次将结果保存在名为seconds_per_day的变量中。

3.5 将seconds_per_day除以seconds_per_hour。使用浮点数(/)除法。

3.6 将seconds_per_day除以seconds_per_hour,使用整数(//)除法。除了最后的.0,这个数字是否与前一个问题中的浮点值一致?

¹ 适用于 Python 3.6 及更新版本。

第四章:选择使用 if

如果你能保持头脑冷静

如果你能保持头脑冷静,当周围的一切都在失控

鲁道德·吉卜林,《如果——》

在前面的章节中,你已经看到了许多数据示例,但还没有深入研究过。大多数代码示例都使用交互式解释器,并且很简短。在这一章中,你将学习如何构建 Python 代码,而不仅仅是数据。

许多计算机语言使用诸如花括号 ({}) 或关键字如 beginend 来标记代码段落。在这些语言中,使用一致的缩进习惯是提高代码可读性的良好实践,不仅适合自己阅读,也便于他人理解。甚至有工具可以帮助你使代码排列整齐。

当他设计成为 Python 的语言时,吉多·范罗苏姆决定使用缩进来定义程序结构,避免使用所有那些括号和花括号。Python 在使用空格来定义程序结构方面非常不同寻常。这是新手注意到的第一个方面,对有其他语言经验的人来说可能会感觉奇怪。但事实证明,使用 Python 一段时间后,这种方式会变得自然,你甚至不再注意到它。你甚至会习惯于在输入更少的情况下做更多事情。

我们最初的代码示例都是单行代码。让我们先看看如何进行注释和多行命令。

使用 # 进行注释

注释 是程序中被 Python 解释器忽略的文本片段。你可以使用注释来澄清附近的 Python 代码,做笔记提醒自己以后修复问题,或者任何你喜欢的目的。通过使用 # 字符标记注释;从该点到当前行末尾的所有内容都是注释。通常你会在单独的一行上看到注释,如下所示:

>>> # 60 sec/min * 60 min/hr * 24 hr/day
>>> seconds_per_day = 86400

或者,在代码同一行上进行注释:

>>> seconds_per_day = 86400 # 60 sec/min * 60 min/hr * 24 hr/day

# 字符有很多名称:井号sharppound 或听起来邪恶的 octothorpe。¹ 无论你如何称呼它,² 它的效果仅限于出现在该行的末尾。

Python 没有多行注释。你需要明确地用 # 开始每一行或每一节注释:

>>> # I can say anything here, even if Python doesn't like it,
... # because I'm protected by the awesome
... # octothorpe.
...
>>>

然而,如果它在文本字符串中,强大的井号将恢复其作为普通旧#字符的角色:

>>> print("No comment: quotes make the # harmless.")
No comment: quotes make the # harmless.

使用 \ 继续多行

当行长度合理时,程序更易读。推荐(非必须)的最大行长度为 80 个字符。如果你无法在这个长度内表达所有想要说的内容,你可以使用续行字符\(反斜杠)。只需在行尾加上 \,Python 就会认为你仍然在同一行上。

例如,如果我想要添加前五个数字,我可以一行一行地进行:

>>> sum = 0
>>> sum += 1
>>> sum += 2
>>> sum += 3
>>> sum += 4
>>> sum
10

或者,我可以使用续行字符一步到位:

>>> sum = 1 + \
...       2 + \
...       3 + \
...       4
>>> sum
10

如果我们在表达式中跳过中间的反斜杠,我们会得到一个异常:

>>> sum = 1 +
  File "<stdin>", line 1
    sum = 1 +
            ^
SyntaxError: invalid syntax

这里有一个小技巧——如果你在成对的括号(或方括号或花括号)中间,Python 不会对行结束发出警告:

>>> sum = (
...     1 +
...     2 +
...     3 +
...     4)
>>>
>>> sum
10

在第五章中,你还会看到成对的三重引号让你创建多行字符串。

与 if、elif 和 else 比较

现在,我们终于迈出了进入编程的第一步,这是一个小小的 Python 程序,检查布尔变量disaster的值,并打印相应的注释:

>>> disaster = True
>>> if disaster:
...     print("Woe!")
... else:
...     print("Whee!")
...
Woe!
>>>

ifelse行是 Python 的语句,用于检查条件(这里是disaster的值)是否为布尔True值,或者可以评估为True。记住,print()是 Python 的内置函数,用于打印东西,通常打印到屏幕上。

注意

如果你在其他语言中编程过,请注意,对于if测试,不需要括号。例如,不要写像if (disaster == True)这样的内容(相等操作符==在几段后面描述)。但是需要在末尾加上冒号(:)。如果像我一样有时会忘记输入冒号,Python 会显示错误消息。

每个print()行在其测试下缩进。我使用四个空格来缩进每个子节。虽然你可以使用任何你喜欢的缩进方式,但 Python 期望你在一个部分内保持一致——每行都需要缩进相同的数量,左对齐。推荐的风格,称为PEP-8,是使用四个空格。不要使用制表符,也不要混合制表符和空格;这会搞乱缩进计数。

在本节逐渐展开时,我们做了很多事情,我会详细解释:

  • 将布尔值True赋给名为disaster的变量。

  • 通过使用ifelse执行条件比较

  • 调用 print() 函数来打印一些文本。

你可以进行嵌套测试,需要多少层都可以:

>>> furry = True
>>> large = True
>>> if furry:
...     if large:
...         print("It's a yeti.")
...     else:
...         print("It's a cat!")
... else:
...     if large:
...         print("It's a whale!")
...     else:
...         print("It's a human. Or a hairless cat.")
...
It's a yeti.

在 Python 中,缩进决定了如何配对ifelse部分。我们的第一个测试是检查furry。因为furryTrue,Python 进入缩进的if large测试。因为我们将large设为Trueif large评估为True,忽略以下的else行。这使得 Python 运行缩进在if large:下的行,并打印It's a yeti.

如果有超过两个可能性需要测试,使用if来进行第一个测试,elif(意为else if)来进行中间的测试,else用于最后一个:

>>> color = "mauve"
>>> if color == "red":
...     print("It's a tomato")
... elif color == "green":
...     print("It's a green pepper")
... elif color == "bee purple":
...     print("I don't know what it is, but only bees can see it")
... else:
...     print("I've never heard of the color", color)
...
I've never heard of the color mauve

在上面的例子中,我们使用==操作符进行了相等性测试。这里是 Python 的比较操作符

等于 ==
不等于 !=
小于 <
小于或等于 <=
大于 >
大于或等于 >=

这些返回布尔值TrueFalse。让我们看看它们如何工作,但首先,给x赋一个值:

>>> x = 7

现在,让我们尝试一些测试:

>>> x == 5
False
>>> x == 7
True
>>> 5 < x
True
>>> x < 10
True

注意,两个等号 (==) 用于测试相等性;记住,单个等号 (=) 用于给变量赋值。

如果你需要同时进行多个比较,可以使用逻辑(或布尔运算符 andornot 来确定最终的布尔结果。

逻辑运算符比它们比较的代码块具有较低的优先级。这意味着首先计算这些代码块,然后再比较。在这个例子中,因为我们将 x 设置为 75 < x 计算为 Truex < 10 也是 True,所以最终我们得到 True and True

>>> 5 < x and x < 10
True

正如“优先级”所指出的,避免关于优先级混淆的最简单方法是添加括号:

>>> (5 < x) and (x < 10)
True

这里有一些其他的测试:

>>> 5 < x or x < 10
True
>>> 5 < x and x > 10
False
>>> 5 < x and not x > 10
True

如果你在一个变量上进行多个 and 运算的比较,Python 允许你这样做:

>>> 5 < x < 10
True

这与 5 < x and x < 10 是一样的。你也可以编写更长的比较:

>>> 5 < x < 10 < 999
True

什么是真?

如果我们检查的元素不是布尔值,Python 认为什么是 TrueFalse

一个 false 值并不一定需要显式地是布尔 False。例如,下面这些都被认为是 False

布尔 False
None
零整数 0
零浮点数 0.0
空字符串 ''
空列表 []
空元组 ()
空字典 {}
空集合 set()

其他任何情况都被认为是 True。Python 程序使用这些“真实性”和“虚假性”的定义来检查空数据结构以及 False 条件:

>>> some_list = []
>>> if some_list:
...     print("There's something in here")
... else:
...     print("Hey, it's empty!")
...
Hey, it's empty!

如果你要测试的是一个表达式而不是一个简单的变量,Python 会评估该表达式并返回一个布尔结果。因此,如果你输入:

if color == "red":

Python 评估 color == "red"。在我们之前的例子中,我们将字符串 "mauve" 分配给 color,所以 color == "red"False,Python 继续下一个测试:

elif color == "green":

使用 in 进行多个比较

假设你有一个字母,并想知道它是否是元音字母。一种方法是编写一个长长的 if 语句:

>>> letter = 'o'
>>> if letter == 'a' or letter == 'e' or letter == 'i' \
...     or letter == 'o' or letter == 'u':
...     print(letter, 'is a vowel')
... else:
...     print(letter, 'is not a vowel')
...
o is a vowel
>>>

每当你需要进行大量使用 or 分隔的比较时,使用 Python 的成员运算符 in 更加 Pythonic。下面是如何使用由元音字符组成的字符串与 in 结合来检查元音性:

>>> vowels = 'aeiou'
>>> letter = 'o'
>>> letter in vowels
True
>>> if letter in vowels:
...     print(letter, 'is a vowel')
...
o is a vowel

下面是如何在接下来的几章节中详细阅读的一些数据类型的使用示例:

>>> letter = 'o'
>>> vowel_set = {'a', 'e', 'i', 'o', 'u'}
>>> letter in vowel_set
True
>>> vowel_list = ['a', 'e', 'i', 'o', 'u']
>>> letter in vowel_list
True
>>> vowel_tuple = ('a', 'e', 'i', 'o', 'u')
>>> letter in vowel_tuple
True
>>> vowel_dict = {'a': 'apple', 'e': 'elephant',
...               'i': 'impala', 'o': 'ocelot', 'u': 'unicorn'}
>>> letter in vowel_dict
True
>>> vowel_string = "aeiou"
>>> letter in vowel_string
True

对于字典,in 查看的是键(: 的左边),而不是它们的值。

新内容:我是海象

在 Python 3.8 中引入了海象运算符,它看起来像这样:

*`name`* := *`expression`*

看到海象了吗?(像笑脸一样,但更多了一些象牙。)

通常,赋值和测试需要两个步骤:

>>> tweet_limit = 280
>>> tweet_string = "Blah" * 50
>>> diff = tweet_limit - len(tweet_string)
>>> if diff >= 0:
...     print("A fitting tweet")
... else:
...     print("Went over by", abs(diff))
...
A fitting tweet

通过我们的新的分配表达式,我们可以将这些组合成一个步骤:

>>> tweet_limit = 280
>>> tweet_string = "Blah" * 50
>>> if diff := tweet_limit - len(tweet_string) >= 0:
...     print("A fitting tweet")
... else:
...     print("Went over by", abs(diff))
...
A fitting tweet

“海象运算符”还与 forwhile 很好地配合,我们将在第六章中详细讨论。

即将到来

玩弄字符串,并遇见有趣的字符。

要做的事情

4.1 选择一个 1 到 10 之间的数字,并将其赋给变量 secret。然后,再选择另一个 1 到 10 之间的数字,并将其赋给变量 guess。接下来,编写条件测试(ifelseelif)来打印字符串'too low',如果 guess 小于 secret,打印'too high',如果 guess 大于 secret,打印'just right',如果 guess 等于 secret

4.2 为变量 smallgreen 赋值TrueFalse。编写一些if/else语句来打印这些选择匹配哪些选项:cherry(樱桃)、pea(豌豆)、watermelon(西瓜)、pumpkin(南瓜)。

¹ 就像那只八脚的绿色东西在你后面

² 请不要打电话给它。它可能会回来。

第五章:文本字符串

我总是喜欢奇怪的字符。

蒂姆·伯顿

计算机书籍通常给人一种编程都是关于数学的印象。实际上,大多数程序员更常用于处理文本的字符串,而不是数字。逻辑(和创造性!)思维通常比数学技能更重要。

字符串是 Python 的第一个序列示例。在这种情况下,它们是字符的序列。但是什么是字符?它是书写系统中的最小单位,包括字母、数字、符号、标点符号,甚至空格或类似换行符的指令。字符由其含义(如何使用它)来定义,而不是它的外观。它可以有多个视觉表示(在不同字体中),而且多个字符可以具有相同的外观(比如在拉丁字母表中表示 H 音的视觉 H,但在西里尔字母表中表示拉丁 N 音)。

本章集中讨论如何制作和格式化简单文本字符串,使用 ASCII(基本字符集)示例。两个重要的文本主题推迟到第十二章:Unicode 字符(如我刚提到的 HN 问题)和正则表达式(模式匹配)。

与其他语言不同,Python 中的字符串是不可变的。你不能直接改变一个字符串,但你可以将字符串的部分复制到另一个字符串以达到相同的效果。我们马上看看如何做到这一点。

用引号创建

通过将字符包含在匹配的单引号或双引号中,你可以创建一个 Python 字符串:

>>> 'Snap'
'Snap'
>>> "Crackle"
'Crackle'

交互式解释器用单引号回显字符串,但 Python 对所有字符串处理都是完全相同的。

注意

Python 有几种特殊类型的字符串,第一个引号前面的字母指示。fF 开始一个f 字符串,用于格式化,在本章末尾描述。rR 开始一个原始字符串,用于防止字符串中的转义序列(参见“用 \ 进行转义” 和 第十二章 中有关它在字符串模式匹配中的用法)。然后,有组合 fr(或 FRFrfR)开始一个原始 f-string。u 开始一个 Unicode 字符串,它与普通字符串相同。b 开始一个 bytes 类型的值(参见第十二章)。除非我提到这些特殊类型之一,我总是在谈论普通的 Python Unicode 文本字符串。

为什么要有两种引号字符?主要目的是创建包含引号字符的字符串。你可以在双引号字符串中放单引号,或在单引号字符串中放双引号:

>>> "'Nay!' said the naysayer. 'Neigh?' said the horse."
"'Nay!' said the naysayer. 'Neigh?' said the horse."
>>> 'The rare double quote in captivity: ".'
'The rare double quote in captivity: ".'
>>> 'A "two by four" is actually 1 1⁄2" × 3 1⁄2".'
'A "two by four" is actually 1 1⁄2" × 3 1⁄2".'
>>> "'There's the man that shot my paw!' cried the limping hound."
"'There's the man that shot my paw!' cried the limping hound."

你也可以使用三个单引号(''')或三个双引号("""):

>>> '''Boom!'''
'Boom'
>>> """Eek!"""
'Eek!'

三重引号对于这些短字符串并不是很有用。它们最常见的用途是创建多行字符串,就像爱德华·利尔的这首经典诗歌:

>>> poem =  '''There was a Young Lady of Norway,
... Who casually sat in a doorway;
... When the door squeezed her flat,
... She exclaimed, "What of that?"
... This courageous Young Lady of Norway.'''
>>>

(这是在交互式解释器中输入的,第一行我们用 >>> 提示,接着是 ... 直到我们输入最后的三重引号并进入下一行。)

如果你尝试在没有三重引号的情况下创建那首诗,当你转到第二行时,Python 会抱怨:

>>> poem = 'There was a young lady of Norway,
  File "<stdin>", line 1
    poem = 'There was a young lady of Norway,
                                            ^
SyntaxError: EOL while scanning string literal
>>>

如果在三重引号中有多行文本,行尾字符将保留在字符串中。如果有前导或尾随空格,它们也将被保留:

>>> poem2 = '''I do not like thee, Doctor Fell.
...     The reason why, I cannot tell.
...     But this I know, and know full well:
...     I do not like thee, Doctor Fell.
... '''
>>> print(poem2)
I do not like thee, Doctor Fell.
 The reason why, I cannot tell.
 But this I know, and know full well:
 I do not like thee, Doctor Fell.

>>>

顺便提一下,print()的输出与交互式解释器的自动回显是有区别的。

>>> poem2
'I do not like thee, Doctor Fell.\n    The reason why, I cannot tell.\n    But
this I know, and know full well:\n    I do not like thee, Doctor Fell.\n'

print()会去除字符串的引号并打印它们的内容。它适用于人类输出。它会在打印的每个内容之间添加一个空格,并在末尾添加一个换行符:

>>> print('Give', "us", '''some''', """space""")
Give us some space

如果你不想要空格或换行符,请参阅第十四章中的说明以避免它们。

交互式解释器打印字符串时带有单独的引号和转义字符,例如\n,这些在“使用\进行转义”中有解释。

>>> """'Guten Morgen, mein Herr!'
... said mad king Ludwig to his wig."""
"'Guten Morgen, mein Herr!'\nsaid mad king Ludwig to his wig."

最后,还有空字符串,它完全没有字符但却是完全有效的。你可以用前述任何引号创建空字符串:

>>> ''
''
>>> ""
''
>>> ''''''
''
>>> """"""
''
>>>

使用str()创建字符串。

你可以使用str()函数从其他数据类型创建字符串:

>>> str(98.6)
'98.6'
>>> str(1.0e4)
'10000.0'
>>> str(True)
'True'

在调用print()时,Python 在对象不是字符串且在字符串格式化时内部使用str()函数,稍后在本章节中你会看到。

使用\进行转义

Python 允许你转义字符串中某些字符的含义,以实现其他难以表达的效果。通过在字符前加上反斜杠(\),你赋予它特殊的含义。最常见的转义序列是\n,表示开始新的一行。这样你可以从单行字符串创建多行字符串:

>>> palindrome = 'A man,\nA plan,\nA canal:\nPanama.'
>>> print(palindrome)
A man,
A plan,
A canal:
Panama.

你会看到\t(制表符)的转义序列用于对齐文本:

>>> print('\tabc')
 abc
>>> print('a\tbc')
a    bc
>>> print('ab\tc')
ab	c
>>> print('abc\t')
abc

(最终字符串具有终止的制表符,当然,你看不到。)

你可能还需要\'\"来指定一个由相同字符引用的字符串中的字面单引号或双引号:

>>> testimony = "\"I did nothing!\" he said. \"Or that other thing.\""
>>> testimony
'"I did nothing!" he said. "Or that other thing."'
>>> print(testimony)
"I did nothing!" he said. "Or that other thing."
>>> fact = "The world's largest rubber duck was 54'2\" by 65'7\" by 105'"
>>> print(fact)
The world's largest rubber duck was 54'2" by 65'7" by 105'

如果你需要一个字面上的反斜杠,请输入两个(第一个转义第二个):

>>> speech = 'The backslash (\\) bends over backwards to please you.'
>>> print(speech)
The backslash (\) bends over backwards to please you.
>>>

正如本章开头所提到的,原始字符串会取消这些转义。

>>> info = r'Type a \n to get a new line in a normal string'
>>> info
'Type a \\n to get a new line in a normal string'
>>> print(info)
Type a \n to get a new line in a normal string

(第一个info输出中的额外反斜杠是交互式解释器添加的。)

原始字符串不会取消任何真正的(不是'\n')换行符:

>>> poem = r'''Boys and girls, come out to play.
... The moon doth shine as bright as day.'''
>>> poem
'Boys and girls, come out to play.\nThe moon doth shine as bright as day.'
>>> print(poem)
Boys and girls, come out to play.
The moon doth shine as bright as day.

通过+进行组合

在 Python 中,你可以通过使用+运算符来组合字面字符串或字符串变量。

>>> 'Release the kraken! ' + 'No, wait!'
'Release the kraken! No, wait!'

你还可以通过简单地将一个字符串放在另一个字符串后面来组合字面字符串(而不是字符串变量):

>>> "My word! " "A gentleman caller!"
'My word! A gentleman caller!'
>>> "Alas! ""The kraken!"
'Alas! The kraken!'

如果有很多这样的内容,你可以通过将其用括号括起来来避免转义行尾。

>>> vowels = ( 'a'
... "e" '''i'''
... 'o' """u"""
... )
>>> vowels
'aeiou'

Python 在连接字符串时不会为你添加空格,因此在一些早期的示例中,我们需要显式地包含空格。Python 在print()语句的每个参数之间添加一个空格,并在末尾添加一个换行符。

>>> a = 'Duck.'
>>> b = a
>>> c = 'Grey Duck!'
>>> a + b + c
'Duck.Duck.Grey Duck!'
>>> print(a, b, c)
Duck. Duck. Grey Duck!

使用*进行复制

你可以使用*运算符来复制一个字符串。尝试在交互式解释器中输入这些行,并查看它们打印出什么:

>>> start = 'Na ' * 4 + '\n'
>>> middle = 'Hey ' * 3 + '\n'
>>> end = 'Goodbye.'
>>> print(start + start + middle + end)

请注意,*+ 优先级更高,因此在换行符附加之前字符串被复制。

通过 [] 获取一个字符

要从字符串中获取单个字符,请在字符串名称后的方括号内指定其 offset。第一个(最左边的)偏移量是 0,接下来是 1,依此类推。最后一个(最右边的)偏移量可以用 -1 指定,因此你不必计数;向左是 -2,-3,等等:

>>> letters = 'abcdefghijklmnopqrstuvwxyz'
>>> letters[0]
'a'
>>> letters[1]
'b'
>>> letters[-1]
'z'
>>> letters[-2]
'y'
>>> letters[25]
'z'
>>> letters[5]
'f'

如果指定的偏移量等于或超过字符串的长度(记住,偏移量从 0 到长度减 1),将会引发异常:

>>> letters[100]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: string index out of range

索引在其他序列类型(列表和元组)中的工作方式相同,我在第七章中介绍。

因为字符串是不可变的,您无法直接插入字符或更改特定索引处的字符。让我们尝试将 'Henny' 更改为 'Penny' 看看会发生什么:

>>> name = 'Henny'
>>> name[0] = 'P'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

相反,您需要使用 replace()slice 的某些组合(我们马上会看到的)等字符串函数:

>>> name = 'Henny'
>>> name.replace('H', 'P')
'Penny'
>>> 'P' + name[1:]
'Penny'

我们没有更改 name 的值。交互式解释器只是打印替换的结果。

使用 slice 提取一个 substring

您可以通过使用 slice 从字符串中提取 substring(字符串的一部分)。您可以通过使用方括号、start 偏移量、end 偏移量和它们之间的可选 step 计数来定义 slice。您可以省略其中一些。切片将包括从 start 偏移量到 end 偏移量之前的字符:

  • [:] 提取从开头到结尾的整个序列。

  • [ start :] 指定从 start 偏移量到结尾。

  • [: end ] 指定从开头到 end 偏移量减 1。

  • [ start : end ] 表示从 start 偏移量到 end 偏移量减 1。

  • [ start : end : step ] 提取从 start 偏移量到 end 偏移量减 1,跳过 step 个字符。

如前所述,偏移量从左到右为 0、1 等等,从右到左为 -1、-2 等等。如果不指定 start,则切片使用 0(开头)。如果不指定 end,则使用字符串的末尾。

让我们创建一个包含小写英文字母的字符串:

>>> letters = 'abcdefghijklmnopqrstuvwxyz'

使用一个普通的 : 等同于 0:(整个字符串):

>>> letters[:]
'abcdefghijklmnopqrstuvwxyz'

这里是一个从偏移量 20 开始到结尾的示例:

>>> letters[20:]
'uvwxyz'

现在,从偏移量 10 到结尾:

>>> letters[10:]
'klmnopqrstuvwxyz'

另一个,偏移量从 12 到 14。Python 不包括切片中的结束偏移量。开始偏移量是 包含 的,结束偏移量是 不包含 的:

>>> letters[12:15]
'mno'

最后三个字符:

>>> letters[-3:]
'xyz'

在下一个示例中,我们从偏移量 18 到倒数第 4 个提取;请注意与上一个示例的区别,在上一个示例中,从 -3 开始获取 x,但在 -3 结束实际上停在 -4,w

>>> letters[18:-3]
'stuvw'

在以下示例中,我们从倒数第 6 个到倒数第 3 个提取:

>>> letters[-6:-2]
'uvwx'

如果要使用除 1 外的步长大小,请在第二个冒号后指定它,如下一系列示例所示。

从开头到结尾,步长为 7 个字符:

>>> letters[::7]
'ahov'

从偏移量 4 到 19,步长为 3:

>>> letters[4:20:3]
'ehknqt'

从偏移量 19 到末尾,步进为 4:

>>> letters[19::4]
'tx'

从开头到偏移量 20 加 5:

>>> letters[:21:5]
'afkpu'

(同样,结束需要比实际偏移量多一位。)

这还不是全部!给定一个负步长,这个方便的 Python 切片器还可以向后步进。它从末尾开始,直到开头,跳过一切:

>>> letters[-1::-1]
'zyxwvutsrqponmlkjihgfedcba'

结果表明,你可以通过以下方式获得相同的结果:

>>> letters[::-1]
'zyxwvutsrqponmlkjihgfedcba'

切片对于错误的偏移量更宽容,不像单索引查找[]那样严格。一个早于字符串开始的切片偏移量将被视为0,一个超过末尾的将被视为-1,正如在下面的示例中展示的那样。

从倒数第 50 位到末尾:

>>> letters[-50:]
'abcdefghijklmnopqrstuvwxyz'

从倒数第 51 位到倒数第 50 位之前:

>>> letters[-51:-50]
''

从开头到开头后的第 69 位:

>>> letters[:70]
'abcdefghijklmnopqrstuvwxyz'

从开头后的第 70 位到开头后的第 70 位:

>>> letters[70:71]
''

使用 len()获取长度

到目前为止,我们已经使用特殊的标点字符如+来操作字符串。但这些字符并不多。现在让我们开始使用一些 Python 内置的函数:这些是执行特定操作的命名代码片段。

len()函数用于计算字符串中的字符数:

>>> len(letters)
26
>>> empty = ""
>>> len(empty)
0

你可以像在第七章中看到的那样,使用len()处理其他序列类型。

使用 split()分割

len()不同,有些函数专门用于字符串。要使用字符串函数,输入字符串名称,一个点,函数名称和函数需要的参数*string*.*function*(*arguments*)。关于函数的更长讨论请参见第九章。

你可以使用内置的字符串split()函数根据某个分隔符将字符串分割成一个列表。我们将在第七章中讨论列表。列表是一系列由逗号分隔并用方括号括起来的值:

>>> tasks = 'get gloves,get mask,give cat vitamins,call ambulance'
>>> tasks.split(',')
['get gloves', 'get mask', 'give cat vitamins', 'call ambulance']

在前面的例子中,字符串称为tasks,字符串函数称为split(),带有单一的分隔符参数','。如果不指定分隔符,split()将使用任何连续的空白字符——换行符、空格和制表符:

>>> tasks.split()
['get', 'gloves,get', 'mask,give', 'cat', 'vitamins,call', 'ambulance']

在不带参数调用split时,你仍然需要括号——这是 Python 知道你在调用函数的方式。

使用 join()合并

不太意外的是,join()函数是split()的反向操作:它将字符串列表合并成一个单独的字符串。看起来有点反向,因为你首先指定将所有东西粘合在一起的字符串,然后是要粘合的字符串列表:string .join( list )。所以,要使用换行符将列表lines连接起来,你会说'\n'.join(lines)。在下面的示例中,让我们用逗号和空格将列表中的一些名字连接起来:

>>> crypto_list = ['Yeti', 'Bigfoot', 'Loch Ness Monster']
>>> crypto_string = ', '.join(crypto_list)
>>> print('Found and signing book deals:', crypto_string)
Found and signing book deals: Yeti, Bigfoot, Loch Ness Monster

使用 replace()替换

你用replace()进行简单的子字符串替换。给它旧的子字符串、新的子字符串,以及要替换的旧子字符串的实例数量。它返回更改后的字符串,但不修改原始字符串。如果省略这个最后的计数参数,它会替换所有实例。在这个例子中,只有一个字符串('duck')在返回的字符串中被匹配并替换:

>>> setup = "a duck goes into a bar..."
>>> setup.replace('duck', 'marmoset')
'a marmoset goes into a bar...'
>>> setup
'a duck goes into a bar...'

更改多达 100 个:

>>> setup.replace('a ', 'a famous ', 100)
'a famous duck goes into a famous bar...'

当你知道确切的子字符串要更改时,replace() 是一个很好的选择。但要小心。在第二个例子中,如果我们替换为单个字符字符串'a'而不是两个字符字符串'a 'a后跟一个空格),我们也会改变其他单词中间的a

>>> setup.replace('a', 'a famous', 100)
'a famous duck goes into a famous ba famousr...'

有时,你想确保子字符串是一个完整的单词,或者是一个单词的开头等。在这些情况下,你需要正则表达式,在第十二章中详细描述。

用 strip() 去除

从字符串中去除前导或尾随的“填充”字符,尤其是空格,这是非常常见的。这里显示的strip()函数假设你想要去除空白字符(' ', '\t', '\n'),如果你不给它们参数的话。strip()会去除两端,lstrip()只从左边,rstrip()只从右边。假设字符串变量world包含字符串"earth"浮动在空格中:

>>> world = "    earth   "
>>> world.strip()
'earth'
>>> world.strip(' ')
'earth'
>>> world.lstrip()
'earth   '
>>> world.rstrip()
'    earth'

如果字符不在那里,什么也不会发生:

>>> world.strip('!')
'    earth   '

除了没有参数(意味着空白字符)或单个字符外,你还可以告诉strip()去除多字符字符串中的任何字符:

>>> blurt = "What the...!!?"
>>> blurt.strip('.?!')
'What the'

附录 E 显示了一些对于strip()有用的字符组的定义:

>>> import string
>>> string.whitespace
' \t\n\r\x0b\x0c'
>>> string.punctuation
'!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
>>> blurt = "What the...!!?"
>>> blurt.strip(string.punctuation)
'What the'
>>> prospector = "What in tarnation ...??!!"
>>> prospector.strip(string.whitespace + string.punctuation)
'What in tarnation'

搜索和选择

Python 有一组庞大的字符串函数。让我们探讨它们中最常见的一些如何工作。我们的测试对象是以下字符串,其中包含了玛格丽特·卡文迪许,纽卡斯尔公爵夫人的不朽诗作“液体是什么?”的文字:

>>> poem = '''All that doth flow we cannot liquid name
... Or else would fire and water be the same;
... But that is liquid which is moist and wet
... Fire that property can never get.
... Then 'tis not cold that doth the fire put out
... But 'tis the wet that makes it die, no doubt.'''

鼓舞人心!

首先,获取前 13 个字符(偏移量 0 到 12):

>>> poem[:13]
'All that doth'

这首诗有多少个字符?(空格和换行符都包括在计数中。)

>>> len(poem)
250

它以All开头吗?

>>> poem.startswith('All')
True

它以That's all, folks!结尾吗?

>>> poem.endswith('That\'s all, folks!')
False

Python 有两个方法(find()index())用于找到子字符串的偏移量,并且有两个版本(从开始或结尾)。如果找到子字符串,它们的工作方式相同。如果找不到,find()返回-1,而index()引发异常。

让我们找到诗中单词the的第一次出现的偏移量:

>>> word = 'the'
>>> poem.find(word)
73
>>> poem.index(word)
73

最后一个the的偏移量:

>>> word = 'the'
>>> poem.rfind(word)
214
>>> poem.rindex(word)
214

但如果子字符串不在其中:

>>> word = "duck"
>>> poem.find(word)
-1
>>> poem.rfind(word)
-1
>>> poem.index(word)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: substring not found
>>> poem.rfind(word)
-1
>>> poem.rindex(word)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: substring not found

三个字母序列the出现了多少次?

>>> word = 'the'
>>> poem.count(word)
3

诗中的所有字符都是字母或数字吗?

>>> poem.isalnum()
False

不,有一些标点字符。

案例

在这一部分,我们将看一些内置字符串函数的更多用法。我们的测试字符串再次是以下内容:

>>> setup = 'a duck goes into a bar...'

从两端去除.序列:

>>> setup.strip('.')
'a duck goes into a bar'
注意

由于字符串是不可变的,这些示例中没有一个实际上更改了setup字符串。每个示例只是取setup的值,对其进行处理,并将结果作为新字符串返回。

将第一个单词大写:

>>> setup.capitalize()
'A duck goes into a bar...'

将所有单词大写:

>>> setup.title()
'A Duck Goes Into A Bar...'

将所有字符转换为大写:

>>> setup.upper()
'A DUCK GOES INTO A BAR...'

将所有字符转换为小写:

>>> setup.lower()
'a duck goes into a bar...'

交换大写和小写:

>>> setup.swapcase()
'A DUCK GOES INTO A BAR...'

对齐

现在,让我们使用一些布局对齐函数。字符串在指定的总空间内(这里为30)居中对齐。

在 30 个空格内居中对齐字符串:

>>> setup.center(30)
'  a duck goes into a bar...   '

左对齐:

>>> setup.ljust(30)
'a duck goes into a bar...     '

右对齐:

>>> setup.rjust(30)
'     a duck goes into a bar...'

接下来,我们看一下更多如何对齐字符串的方法。

格式化

您已经看到可以使用+连接字符串。让我们看看如何使用各种格式将数据值插值到字符串中。您可以用此方法生成需要外观精确的报告、表格和其他输出。

除了上一节中的函数外,Python 还有三种格式化字符串的方法:

  • 旧风格(支持 Python 2 和 3)

  • 新风格(Python 2.6 及更高版本)

  • f-strings(Python 3.6 及更高版本)

旧风格:%

旧式字符串格式化的形式为*format_string* % data。格式字符串中包含插值序列。表 5-1 说明了最简单的序列是一个%后跟一个指示要格式化的数据类型的字母。

表 5-1. 转换类型

%s 字符串
%d 十进制整数
%x 十六进制整数
%o 八进制整数
%f 十进制浮点数
%e 指数浮点数
%g 十进制或指数浮点数
%% 一个字面量%

您可以使用%s来表示任何数据类型,Python 会将其格式化为无额外空格的字符串。

以下是一些简单的示例。首先,一个整数:

>>> '%s' % 42
'42'
>>> '%d' % 42
'42'
>>> '%x' % 42
'2a'
>>> '%o' % 42
'52'

一个浮点数:

>>> '%s' % 7.03
'7.03'
>>> '%f' % 7.03
'7.030000'
>>> '%e' % 7.03
'7.030000e+00'
>>> '%g' % 7.03
'7.03'

一个整数和一个字面量%

>>> '%d%%' % 100
'100%'

让我们尝试一些字符串和整数插值:

>>> actor = 'Richard Gere'
>>> cat = 'Chester'
>>> weight = 28
>>> "My wife's favorite actor is %s" % actor
"My wife's favorite actor is Richard Gere"
>>> "Our cat %s weighs %s pounds" % (cat, weight)
'Our cat Chester weighs 28 pounds'

字符串中的%s表示插入一个字符串。字符串中%的数量需要与跟随字符串之后的数据项的数量匹配。单个数据项,如actor,直接放在最后一个%之后。多个数据必须分组成元组(详细信息见第七章; 由括号界定,逗号分隔),如(cat, weight)

尽管weight是一个整数,但字符串中的%s将其转换为字符串。

您可以在格式字符串的%和类型说明符之间添加其他值来指定最小宽度、最大宽度、对齐和字符填充。这本质上是一种小语言,比接下来的两个部分的语言更有限。让我们快速看看这些值:

  • 初始'%'字符。

  • 可选的对齐字符:没有或'+'表示右对齐,'-'表示左对齐。

  • 可选的最小宽度字段宽度。

  • 可选的'.'字符用于分隔最小宽度最大字符数

  • 可选的maxchars(如果转换类型为s)指定要从数据值中打印多少个字符。如果转换类型为f,则指定精度(小数点后要打印多少位数)。

  • 早期表格中的转换类型字符。

这很令人困惑,所以这里有一些字符串的示例:

>>> thing = 'woodchuck'
>>> '%s' % thing
'woodchuck'
>>> '%12s' % thing
'   woodchuck'
>>> '%+12s' % thing
'   woodchuck'
>>> '%-12s' % thing
'woodchuck   '
>>> '%.3s' % thing
'woo'
>>> '%12.3s' % thing
'         woo'
>>> '%-12.3s' % thing
'woo         '

再来一次,和一个带有%f变体的浮点数:

>>> thing = 98.6
>>> '%f' % thing
'98.600000'
>>> '%12f' % thing
'   98.600000'
>>> '%+12f' % thing
'  +98.600000'
>>> '%-12f' % thing
'98.600000   '
>>> '%.3f' % thing
'98.600'
>>> '%12.3f' % thing
'      98.600'
>>> '%-12.3f' % thing
'98.600      '

和一个整数与%d

>>> thing = 9876
>>> '%d' % thing
'9876'
>>> '%12d' % thing
'        9876'
>>> '%+12d' % thing
'       +9876'
>>> '%-12d' % thing
'9876        '
>>> '%.3d' % thing
'9876'
>>> '%12.3d' % thing
'        9876'
>>> '%-12.3d' % thing
'9876        '

对于整数,%+12d只是强制打印符号,并且带有.3的格式字符串对其无效,就像对浮点数一样。

新风格:

仍然支持旧风格格式化。在 Python 2 中,将冻结在版本 2.7 上,将永远支持。对于 Python 3,请使用本节中描述的“新风格”格式化。如果你使用的是 Python 3.6 或更新版本,f-strings(“最新风格:f-strings”)更加推荐。

“新风格”格式化的形式为*format_string*.format(*data*)

格式字符串与前一节不完全相同。这里演示了最简单的用法:

>>> thing = 'woodchuck'
>>> '{}'.format(thing)
'woodchuck'

format()函数的参数需要按照格式字符串中的{}占位符的顺序:

>>> thing = 'woodchuck'
>>> place = 'lake'
>>> 'The {} is in the {}.'.format(thing, place)
'The woodchuck is in the lake.'

使用新风格格式,你还可以像这样按位置指定参数:

>>> 'The {1} is in the {0}.'.format(place, thing)
'The woodchuck is in the lake.'

0指的是第一个参数place1指的是thing

format()的参数也可以是命名参数

>>> 'The {thing} is in the {place}'.format(thing='duck', place='bathtub')
'The duck is in the bathtub'

或者是一个字典:

>>> d = {'thing': 'duck', 'place': 'bathtub'}

在以下示例中,{0}format()的第一个参数(字典d):

>>> 'The {0[thing]} is in the {0[place]}.'.format(d)
'The duck is in the bathtub.'

这些示例都使用默认格式打印它们的参数。新风格格式化与旧风格的格式字符串定义略有不同(示例如下):

  • 初始冒号(':')。

  • 可选的填充字符(默认为' ')以填充值字符串,如果比minwidth短。

  • 可选的对齐字符。这次,左对齐是默认的。'<'也表示左对齐,'>'表示右对齐,'^'表示居中。

  • 数字的可选符号。没有意味着仅为负数添加减号('-')。' '表示负数前添加减号,正数前添加空格(' ')。

  • 可选的minwidth。一个可选的句点('.')用于分隔minwidthmaxchars

  • 可选的maxchars

  • 转换类型

>>> thing = 'wraith'
>>> place = 'window'
>>> 'The {} is at the {}'.format(thing, place)
'The wraith is at the window'
>>> 'The {:10s} is at the {:10s}'.format(thing, place)
'The wraith     is at the window    '
>>> 'The {:<10s} is at the {:<10s}'.format(thing, place)
'The wraith     is at the window    '
>>> 'The {:¹⁰s} is at the {:¹⁰s}'.format(thing, place)
'The   wraith   is at the   window  '
>>> 'The {:>10s} is at the {:>10s}'.format(thing, place)
'The     wraith is at the     window'
>>> 'The {:!¹⁰s} is at the {:!¹⁰s}'.format(thing, place)
'The !!wraith!! is at the !!window!!'

最新的风格:f-strings

f-strings出现在 Python 3.6 中,现在是推荐的字符串格式化方式。

制作 f-string:

  • 直接在初始引号之前输入字母fF

  • 在大括号({})中包含变量名或表达式,以将它们的值放入字符串中。

这就像前一节的“新风格”格式化,但没有format()函数,并且格式字符串中没有空括号({})或位置参数({1})。

>>> thing = 'wereduck'
>>> place = 'werepond'
>>> f'The {thing} is in the {place}'
'The wereduck is in the werepond'

正如我之前提到的,大括号内也允许表达式:

>>> f'The {thing.capitalize()} is in the {place.rjust(20)}'
'The Wereduck is in the             werepond'

这意味着在前一节的format()中可以做的事情,在主字符串的{}内部现在也可以做到。这看起来更容易阅读。

f-strings 使用与新式格式化相同的格式化语言(宽度、填充、对齐),在':'之后。

>>> f'The {thing:>20} is in the {place:.²⁰}'
'The             wereduck is in the ......werepond......'

从 Python 3.8 开始,f-strings 增加了一个新的快捷方式,当你想打印变量名及其值时非常有帮助。在调试时非常方便。窍门是在 f-string 的{}括号内的变量名后面加上一个单独的=

>>> f'{thing =}, {place =}'
thing = 'wereduck', place = 'werepond'

名称实际上可以是一个表达式,并且会按字面意思打印出来:

>>> f'{thing[-4:] =}, {place.title() =}'
thing[-4:] = 'duck', place.title() = 'Werepond'

最后,=后面可以跟着一个:和格式化参数,如宽度和对齐方式:

>>> f'{thing = :>4.4}'
thing = 'were'

更多字符串事项

Python 有比我展示的更多字符串函数。一些将出现在稍后的章节中(尤其是第十二章),但你可以在标准文档链接找到所有细节。

即将到来

你会在杂货店找到 Froot Loops,但 Python 循环在下一章的第一个柜台上。

待办事项

5.1 将以m开头的单词大写:

>>> song = """When an eel grabs your arm,
... And it causes great harm,
... That's - a moray!"""

5.2 打印每个列表问题及其正确匹配的答案,格式为:

Q: 问题

A: 答案

>>> questions = [
...     "We don't serve strings around here. Are you a string?",
...     "What is said on Father's Day in the forest?",
...     "What makes the sound 'Sis! Boom! Bah!'?"
...     ]
>>> answers = [
...     "An exploding sheep.",
...     "No, I'm a frayed knot.",
...     "'Pop!' goes the weasel."
...     ]

5.3 通过旧式格式编写以下诗歌。将字符串'roast beef''ham''head''clam'替换为此字符串中的内容:

My kitty cat likes %s,
My kitty cat likes %s,
My kitty cat fell on his %s
And now thinks he's a %s.

5.4 使用新式格式化编写一封表单信。将以下字符串保存为letter(你将在下一个练习中使用它):

Dear {salutation} {name},

Thank you for your letter. We are sorry that our {product}
{verbed} in your {room}. Please note that it should never
be used in a {room}, especially near any {animals}.

Send us your receipt and {amount} for shipping and handling.
We will send you another {product} that, in our tests,
is {percent}% less likely to have {verbed}.

Thank you for your support.

Sincerely,
{spokesman}
{job_title}

5.5 为字符串变量'salutation''name''product''verbed'(过去时动词)、'room''animals''percent''spokesman''job_title'分配值。使用letter.format()打印letter

5.6 在公众投票之后为事物命名,出现了一个模式:英国潜艇(Boaty McBoatface)、澳大利亚赛马(Horsey McHorseface)和瑞典火车(Trainy McTrainface)。使用%格式化来打印国家集市上的获奖名字,以及鸭子、葫芦和 spitz 的奖品。

5.7 使用format()格式化方法做同样的事情。

5.8 再来一次,使用f strings

第六章:使用 while 和 for 循环

对于所有的事情,我们辛劳工作,我们辛劳工作,我们所有的努力都被忽视……

罗伯特·彭斯,《为了那些,为了那些》

使用 ifelifelse 进行测试时,从上到下执行。有时,我们需要执行多次操作。我们需要一个循环,而 Python 给了我们两个选择:whilefor

使用 while 重复

Python 中最简单的循环机制是 while。使用交互式解释器,尝试这个例子,这是一个简单的循环,打印从 1 到 5 的数字:

>>> count = 1
>>> while count <= 5:
...     print(count)
...     count += 1
...
1
2
3
4
5
>>>

我们首先将值 1 赋给了 countwhile 循环将 count 的值与 5 进行比较,如果 count 小于或等于 5,则继续。在循环内部,我们打印了 count 的值,然后使用语句 count += 1 将其值增加了一。Python 返回循环顶部,再次将 count5 进行比较。此时 count 的值为 2,因此再次执行 while 循环的内容,并将 count 增加到 3

这将持续到在循环底部将 count5 增加到 6 为止。在下一次回到顶部时,count <= 5 现在为 Falsewhile 循环结束。Python 继续执行下一行。

使用 break 取消

如果您想循环直到某些事情发生,但不确定什么时候会发生,可以使用带有 break 语句的无限循环。这次,让我们通过 Python 的 input() 函数从键盘读取一行输入,然后将其打印为首字母大写。当键入仅包含字母 q 的行时,我们跳出循环:

>>> while True:
...     stuff = input("String to capitalize [type q to quit]: ")
...     if stuff == "q":
...         break
...     print(stuff.capitalize())
...
String to capitalize [type q to quit]: test
Test
String to capitalize [type q to quit]: hey, it works
Hey, it works
String to capitalize [type q to quit]: q
>>>

使用 continue 跳过

有时,您不想中断循环,而只是想因某种原因跳到下一个迭代。这是一个牵强的例子:让我们读取一个整数,如果它是奇数,则打印它的平方,并在它是偶数时跳过。我们甚至添加了一些注释。同样,我们使用 q 来停止循环。

>>> while True:
...     value = input("Integer, please [q to quit]: ")
...     if value == 'q':      # quit
...         break
...     number = int(value)
...     if number % 2 == 0:   # an even number
...        continue
...     print(number, "squared is", number*number)
...
Integer, please [q to quit]: 1
1 squared is 1
Integer, please [q to quit]: 2
Integer, please [q to quit]: 3
3 squared is 9
Integer, please [q to quit]: 4
Integer, please [q to quit]: 5
5 squared is 25
Integer, please [q to quit]: q
>>>

使用 break 检查与 else 一起使用

如果 while 循环正常结束(没有调用 break),控制将传递给可选的 else。当您已编写了一个 while 循环来检查某些内容,并在找到时立即中断时,您会使用它。如果 while 循环完成但未找到对象,则会运行 else

>>> numbers = [1, 3, 5]
>>> position = 0
>>> while position < len(numbers):
...     number = numbers[position]
...     if number % 2 == 0:
...         print('Found even number', number)
...         break
...     position += 1
... else:  # break not called
...     print('No even number found')
...
No even number found
注意

对于else的这种用法可能看起来不直观。将其视为中断检查器

使用 for 和 in 进行迭代

Python 经常使用迭代器,有很好的理由。它们使您能够遍历数据结构,而无需知道其大小或实现方式。您甚至可以迭代即时创建的数据,允许处理否则无法一次性放入计算机内存中的数据

要展示迭代,我们需要一个可以迭代的对象。你已经在第五章看到了字符串,但还没有详细了解其他可迭代对象,比如列表和元组(见第七章)或字典(见第八章)。这里我将展示两种遍历字符串的方法,并在它们各自的章节中展示其他类型的迭代。

在 Python 中,通过以下方式逐步遍历字符串是合法的:

>>> word = 'thud'
>>> offset = 0
>>> while offset < len(word):
...     print(word[offset])
...     offset += 1
...
t
h
u
d

但有一个更好、更符合 Python 风格的方法:

>>> for letter in word:
...     print(letter)
...
t
h
u
d

字符串迭代每次产生一个字符。

通过break取消

for循环中的break会跳出循环,就像在while循环中一样:

>>> word = 'thud'
>>> for letter in word:
...     if letter == 'u':
...         break
...     print(letter)
...
t
h

使用continue跳过

for循环中插入continue会跳到下一个迭代,就像在while循环中一样。

检查breakelse的用法

类似于whilefor也有一个可选的else语句,用于检查for是否正常完成。如果没有调用break,则会执行else语句。

当你希望确认前一个for循环是否完全执行而不是因为break而提前停止时,这是非常有用的:

>>> word = 'thud'
>>> for letter in word:
...     if letter == 'x':
...         print("Eek! An 'x'!")
...         break
...     print(letter)
... else:
...     print("No 'x' in there.")
...
t
h
u
d
No 'x' in there.
注意

while一样,使用forelse可能看起来不直观。如果你把for看作在寻找某些东西,那么当你没有找到时,else会被调用。如果想要在没有else的情况下达到相同的效果,可以使用某个变量来指示在for循环中是否找到了想要的内容。

使用range()生成数字序列

range()函数返回在指定范围内的一系列数字。无需首先创建和存储大数据结构(如列表或元组),就可以创建大范围,避免占用计算机所有内存并导致程序崩溃。

使用range()与使用切片类似:range( start, stop, step )。如果省略*start,范围将从0开始。与切片一样,创建的最后一个值将恰好在stop之前。step*的默认值是1,但可以使用-1向后遍历。

类似于zip()range()返回一个可迭代对象,因此你需要用for ... in逐个遍历其值,或者将该对象转换为像列表这样的序列。让我们创建一个范围为0, 1, 2的示例:

>>> for x in range(0,3):
...     print(x)
...
0
1
2
>>> list( range(0, 3) )
[0, 1, 2]

下面是如何从20生成一个范围:

>>> for x in range(2, -1, -1):
...     print(x)
...
2
1
0
>>> list( range(2, -1, -1) )
[2, 1, 0]

以下片段使用步长为2来获取从010的偶数:

>>> list( range(0, 11, 2) )
[0, 2, 4, 6, 8, 10]

其他迭代器

第十四章展示了如何迭代文件。在第十章,你可以看到如何启用对自定义对象的迭代。此外,第十一章讨论了itertools——一个带有许多有用快捷方式的标准 Python 模块。

即将到来

将各个数据链入列表元组中。

待完成的事情

6.1 使用for循环打印列表[3, 2, 1, 0]的值。

将值7赋给变量guess_me,并将值1赋给变量number。编写一个while循环,比较numberguess_me。如果number小于guess me,则打印'too low'。如果number等于guess_me,则打印'found it!',然后退出循环。如果number大于guess_me,则打印'oops',然后退出循环。在循环结束时增加number

将值5赋给变量guess_me。使用for循环迭代名为number的变量在range(10)上。如果number小于guess_me,则打印'too low'。如果它等于guess_me,则打印'found it!',然后退出 for 循环。如果number大于guess_me,则打印'oops',然后退出循环。

第七章:元组和列表

人类与低等灵长类动物的区别在于他对列表的热爱。

  1. Allen Smith

在前几章中,我们从 Python 的一些基本数据类型开始:布尔值,整数,浮点数和字符串。如果你把它们看作是原子,那么本章中的数据结构就像分子一样。也就是说,我们将这些基本类型以更复杂的方式组合在一起。你将每天都用到它们。编程的很大一部分就是将数据切割和粘贴成特定形式,而这些就是你的金刚钻和胶枪。

大多数计算机语言可以表示按其整数位置索引的项目序列:第一个,第二个,依此类推直到最后一个。你已经见过 Python 的字符串,它们是字符序列。

Python 还有另外两种序列结构:元组列表。它们包含零个或多个元素。与字符串不同,元素可以是不同的类型。事实上,每个元素都可以是任何Python 对象。这使你可以创建像你喜欢的那样深度和复杂的结构。

为什么 Python 同时包含列表和元组?元组是不可变的;当你将元素(仅一次)分配给元组时,它们就成为了固定的部分,不能更改。列表是可变的,这意味着你可以兴致勃勃地插入和删除元素。我将展示每种的许多例子,并着重于列表。

元组

让我们先把一件事搞清楚。你可能会听到两种不同的tuple发音。哪个是正确的?如果你猜错了,是否会被认为是 Python 的冒牌货?别担心。Python 的创造者 Guido van Rossum 在Twitter 上说过:

我在周一/三/五会念作 too-pull,周二/四/六会念作 tub-pull。周日我不谈它们。 😃

使用逗号和()创建

创建元组的语法有点不一致,如下面的例子所示。让我们从使用()创建一个空元组开始:

>>> empty_tuple = ()
>>> empty_tuple
()

要创建一个或多个元素的元组,请在每个元素后面都跟一个逗号。这适用于单元素元组:

>>> one_marx = 'Groucho',
>>> one_marx
('Groucho',)

你可以将它们括在括号中,仍然得到相同的元组:

>>> one_marx = ('Groucho',)
>>> one_marx
('Groucho',)

这里有一个小陷阱:如果括号中只有一个东西而省略了逗号,你将得不到一个元组,而只是那个东西(在这个例子中是字符串'Groucho'):

>>> one_marx = ('Groucho')
>>> one_marx
'Groucho'
>>> type(one_marx)
<class 'str'>

如果有多个元素,请除了最后一个元素外,每个元素后面都跟一个逗号:

>>> marx_tuple = 'Groucho', 'Chico', 'Harpo'
>>> marx_tuple
('Groucho', 'Chico', 'Harpo')

Python 在回显元组时包括括号。当你定义一个元组时通常不需要它们,但使用括号会更安全,并且有助于使元组更可见:

>>> marx_tuple = ('Groucho', 'Chico', 'Harpo')
>>> marx_tuple
('Groucho', 'Chico', 'Harpo')

在某些情况下,如果逗号可能具有其他用途,则确实需要括号。例如,在这个例子中,你可以只用一个尾随逗号创建并分配一个单元素元组,但你不能将其作为函数的参数传递。

>>> one_marx = 'Groucho',
>>> type(one_marx)
<class 'tuple'>
>>> type('Groucho',)
<class 'str'>
>>> type(('Groucho',))
<class 'tuple'>

元组让你一次性赋值多个变量:

>>> marx_tuple = ('Groucho', 'Chico', 'Harpo')
>>> a, b, c = marx_tuple
>>> a
'Groucho'
>>> b
'Chico'
>>> c
'Harpo'

有时被称为元组解包

你可以使用元组在一条语句中交换值,而不使用临时变量:

>>> password = 'swordfish'
>>> icecream = 'tuttifrutti'
>>> password, icecream = icecream, password
>>> password
'tuttifrutti'
>>> icecream
'swordfish'
>>>

使用 tuple()创建

tuple()转换函数从其他内容制作元组:

>>> marx_list = ['Groucho', 'Chico', 'Harpo']
>>> tuple(marx_list)
('Groucho', 'Chico', 'Harpo')

使用+组合元组

这类似于组合字符串:

>>> ('Groucho',) + ('Chico', 'Harpo')
('Groucho', 'Chico', 'Harpo')

使用复制

这就像重复使用+一样:

>>> ('yada',) * 3
('yada', 'yada', 'yada')

比较元组

这与列表比较类似:

>>> a = (7, 2)
>>> b = (7, 2, 9)
>>> a == b
False
>>> a <= b
True
>>> a < b
True

使用 for 和 in 进行迭代

元组迭代类似于其他类型的迭代:

>>> words = ('fresh','out', 'of', 'ideas')
>>> for word in words:
...     print(word)
...
fresh
out
of
ideas

修改元组

你不能!与字符串一样,元组是不可变的,因此您不能更改现有元组。就像您之前看到的那样,您可以连接(组合)元组以制作新元组,就像您可以连接字符串一样:

>>> t1 = ('Fee', 'Fie', 'Foe')
>>> t2 = ('Flop,')
>>> t1 + t2
('Fee', 'Fie', 'Foe', 'Flop')

这意味着您可以看起来修改元组,就像这样:

>>> t1 = ('Fee', 'Fie', 'Foe')
>>> t2 = ('Flop,')
>>> t1 += t2
>>> t1
('Fee', 'Fie', 'Foe', 'Flop')

但它不是相同的t1。Python 从由t1t2指向的原始元组制作了一个新元组,并将名称t1指向了这个新元组。您可以使用id()查看变量名称何时指向新值:

>>> t1 = ('Fee', 'Fie', 'Foe')
>>> t2 = ('Flop',)
>>> id(t1)
4365405712
>>> t1 += t2
>>> id(t1)
4364770744

列表

列表适合按其顺序跟踪事物,特别是当顺序和内容可能会变化时。与字符串不同,列表是可变的。您可以就地更改列表,添加新元素,并删除或替换现有元素。相同的值可以在列表中出现多次。

使用[]创建

列表由零个或多个元素组成,用逗号分隔,并用方括号括起来:

>>> empty_list = [ ]
>>> weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']
>>> big_birds = ['emu', 'ostrich', 'cassowary']
>>> first_names = ['Graham', 'John', 'Terry', 'Terry', 'Michael']
>>> leap_years = [2000, 2004, 2008]
>>> randomness = 'Punxsatawney", {"groundhog": "Phil"}, "Feb. 2"}

first_names列表显示值不需要是唯一的。

注意

如果您只想跟踪唯一值并且不关心顺序,则 Python set可能比列表更好。在前面的示例中,big_birds可以是一个集合。我们在[第八章中探讨了集合。

使用 list()创建或转换

您还可以使用list()函数创建一个空列表:

>>> another_empty_list = list()
>>> another_empty_list
[]

Python 的list()函数还将其他可迭代数据类型(如元组、字符串、集合和字典)转换为列表。以下示例将字符串转换为一个字符的字符串列表:

>>> list('cat')
['c', 'a', 't']

该示例将元组转换为列表:

>>> a_tuple = ('ready', 'fire', 'aim')
>>> list(a_tuple)
['ready', 'fire', 'aim']

使用 split()从字符串创建

正如我之前在“使用 split()拆分”中提到的,使用split()通过某个分隔符将字符串切割为列表:

>>> talk_like_a_pirate_day = '9/19/2019'
>>> talk_like_a_pirate_day.split('/')
['9', '19', '2019']

如果您的原始字符串中有多个连续的分隔符字符串怎么办?好吧,你会得到一个空字符串作为列表项:

>>> splitme = 'a/b//c/d///e'
>>> splitme.split('/')
['a', 'b', '', 'c', 'd', '', '', 'e']

如果您使用两个字符的分隔符字符串//,则会得到这个结果:

>>> splitme = 'a/b//c/d///e'
>>> splitme.split('//')
>>>
['a/b', 'c/d', '/e']

通过[ 偏移量 ]获取项

与字符串类似,您可以通过指定其偏移量从列表中提取单个值:

>>> marxes = ['Groucho', 'Chico', 'Harpo']
>>> marxes[0]
'Groucho'
>>> marxes[1]
'Chico'
>>> marxes[2]
'Harpo'

同样,与字符串类似,负索引从末尾向后计数:

>>> marxes[-1]
'Harpo'
>>> marxes[-2]
'Chico'
>>> marxes[-3]
'Groucho'
>>>
注意

偏移量必须是此列表的有效偏移量,即您先前分配了一个值的位置。如果指定了开始之前或结束之后的偏移量,您将收到一个异常(错误)。这是如果我们尝试获取第六个马克思兄弟(偏移量50开始计数),或倒数第五个会发生的情况:

>>> marxes = ['Groucho', 'Chico', 'Harpo']
>>> marxes[5]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range
>>> marxes[-5]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

使用切片获取项目

您可以通过使用切片提取列表的子序列:

>>> marxes = ['Groucho', 'Chico', 'Harpo']
>>> marxes[0:2]
['Groucho', 'Chico']

列表的切片也是一个列表。

与字符串类似,切片可以步进除了一之外的其他值。下一个示例从开始处开始,每次向右移动 2 个位置:

>>> marxes[::2]
['Groucho', 'Harpo']

在这里,我们从末尾开始,左移 2 个位置:

>>> marxes[::-2]
['Harpo', 'Groucho']

最后,反转列表的窍门:

>>> marxes[::-1]
['Harpo', 'Chico', 'Groucho']

这些切片都没有改变marxes列表本身,因为我们没有把它们赋给marxes。要就地反转列表,请使用*列表*.reverse()

>>> marxes = ['Groucho', 'Chico', 'Harpo']
>>> marxes.reverse()
>>> marxes
['Harpo', 'Chico', 'Groucho']
注意

reverse()函数改变了列表但不返回其值。

正如你在字符串中看到的,切片可以指定一个无效的索引,但不会导致异常。它会“捕捉”到最接近的有效索引或者返回空:

>>> marxes = ['Groucho', 'Chico', 'Harpo']
>>> marxes[4:]
[]
>>> marxes[-6:]
['Groucho', 'Chico', 'Harpo']
>>> marxes[-6:-2]
['Groucho']
>>> marxes[-6:-4]
[]

通过 append()在末尾添加项目

添加项目到列表的传统方法是一个接一个地用append()将它们添加到末尾。在前面的例子中,我们忘记了 Zeppo,但这没关系,因为列表是可变的,所以我们现在可以添加他:

>>> marxes = ['Groucho', 'Chico', 'Harpo']
>>> marxes.append('Zeppo')
>>> marxes
['Groucho', 'Chico', 'Harpo', 'Zeppo']

通过 insert()在偏移处添加项目

append()函数只在列表末尾添加项目。当你想在列表的任何偏移之前添加项目时,请使用insert()。偏移0在开头插入。超出列表末尾的偏移会像append()一样在末尾插入,所以你不需要担心 Python 抛出异常:

>>> marxes = ['Groucho', 'Chico', 'Harpo']
>>> marxes.insert(2, 'Gummo')
>>> marxes
['Groucho', 'Chico', 'Gummo', 'Harpo']
>>> marxes.insert(10, 'Zeppo')
>>> marxes
['Groucho', 'Chico', 'Gummo', 'Harpo', 'Zeppo']

用*复制所有项目

在第五章中,你看到你可以用*来复制字符串的字符。对列表也同样适用:

>>> ["blah"] * 3
['blah', 'blah', 'blah']

通过 extend()或者+合并列表

你可以通过使用extend()将一个列表合并到另一个列表中。假设一个好心的人给了我们一个名为others的新马克思斯列表,并且我们想要将它们合并到主要的marxes列表中:

>>> marxes = ['Groucho', 'Chico', 'Harpo', 'Zeppo']
>>> others = ['Gummo', 'Karl']
>>> marxes.extend(others)
>>> marxes
['Groucho', 'Chico', 'Harpo', 'Zeppo', 'Gummo', 'Karl']

或者,你可以使用+或者+=

>>> marxes = ['Groucho', 'Chico', 'Harpo', 'Zeppo']
>>> others = ['Gummo', 'Karl']
>>> marxes += others
>>> marxes
['Groucho', 'Chico', 'Harpo', 'Zeppo', 'Gummo', 'Karl']

如果我们使用了append()others会被添加为单个列表项,而不是合并其项目:

>>> marxes = ['Groucho', 'Chico', 'Harpo', 'Zeppo']
>>> others = ['Gummo', 'Karl']
>>> marxes.append(others)
>>> marxes
['Groucho', 'Chico', 'Harpo', 'Zeppo', ['Gummo', 'Karl']]

这再次证明了列表可以包含不同类型的元素。在这种情况下,四个字符串和一个包含两个字符串的列表。

通过[offset]更改项目

就像你可以通过偏移获取列表项的值一样,你也可以修改它:

>>> marxes = ['Groucho', 'Chico', 'Harpo']
>>> marxes[2] = 'Wanda'
>>> marxes
['Groucho', 'Chico', 'Wanda']

再次,列表偏移需要是这个列表的有效偏移之一。

你不能用这种方式改变字符串中的字符,因为字符串是不可变的。列表是可变的。你可以改变列表包含的项目数量以及项目本身。

使用切片更改项目

前一节展示了如何使用切片获取子列表。你也可以使用切片为子列表赋值:

>>> numbers = [1, 2, 3, 4]
>>> numbers[1:3] = [8, 9]
>>> numbers
[1, 8, 9, 4]

您分配给列表的右侧对象甚至不需要与左侧切片具有相同数量的元素:

>>> numbers = [1, 2, 3, 4]
>>> numbers[1:3] = [7, 8, 9]
>>> numbers
[1, 7, 8, 9, 4]
>>> numbers = [1, 2, 3, 4]
>>> numbers[1:3] = []
>>> numbers
[1, 4]

实际上,右边的东西甚至不需要是一个列表。任何 Python可迭代对象都可以,分离其项目并将其分配给列表元素:

>>> numbers = [1, 2, 3, 4]
>>> numbers[1:3] = (98, 99, 100)
>>> numbers
[1, 98, 99, 100, 4]
>>> numbers = [1, 2, 3, 4]
>>> numbers[1:3] = 'wat?'
>>> numbers
[1, 'w', 'a', 't', '?', 4]

通过 del 按偏移删除项目

我们的事实核查员刚刚告诉我们,Gummo 确实是马克思兄弟之一,但卡尔不是,并且早先插入他的人非常无礼。让我们来修复一下:

>>> marxes = ['Groucho', 'Chico', 'Harpo', 'Gummo', 'Karl']
>>> marxes[-1]
'Karl'
>>> del marxes[-1]
>>> marxes
['Groucho', 'Chico', 'Harpo', 'Gummo']

当你按列表中的位置删除一个项目时,随后的项目会向后移动以填补删除项目的空间,并且列表的长度会减少一个。如果我们从marxes列表的最后一个版本中删除了'Chico',我们会得到这样的结果:

>>> marxes = ['Groucho', 'Chico', 'Harpo', 'Gummo']
>>> del marxes[1]
>>> marxes
['Groucho', 'Harpo', 'Gummo']
注意

del 是一个 Python 语句,不是列表的方法 —— 你不会说 marxes[-1].del()。这与赋值 (=) 的相反:它会将一个名称从 Python 对象中分离出来,如果该名称是对该对象的最后一个引用,则可以释放该对象的内存。

使用remove()按值删除项目

如果不确定或不关心项目在列表中的位置,请使用remove()按值删除它。再见,Groucho:

>>> marxes = ['Groucho', 'Chico', 'Harpo']
>>> marxes.remove('Groucho')
>>> marxes
['Chico', 'Harpo']

如果列表中有相同值的重复项目,remove()仅删除找到的第一个。

使用偏移获取项目并使用pop()删除它

你可以通过使用pop()从列表中获取项目并同时删除它。如果使用偏移调用pop(),它将返回该偏移量处的项目;如果没有参数,则使用-1。因此,pop(0)返回列表的头部(起始处),而pop()pop(-1)返回尾部(结束处),如下所示:

>>> marxes = ['Groucho', 'Chico', 'Harpo', 'Zeppo']
>>> marxes.pop()
'Zeppo'
>>> marxes
['Groucho', 'Chico', 'Harpo']
>>> marxes.pop(1)
'Chico'
>>> marxes
['Groucho', 'Harpo']
注意

现在是计算机术语时间!别担心,这些不会出现在期末考试中。如果你使用append()在末尾添加新项目,并使用pop()从同一端移除它们,你就实现了一种称为 LIFO(后进先出)队列的数据结构。这更常被称为 pop(0)将创建一个 FIFO(先进先出)队列。当你希望按到达顺序收集数据并首先使用最旧的数据(FIFO),或者首先使用最新的数据(LIFO)时,这些非常有用。

使用clear()删除所有项目

Python 3.3 引入了清空列表所有元素的方法:

>>> work_quotes = ['Working hard?', 'Quick question!', 'Number one priorities!']
>>> work_quotes
['Working hard?', 'Quick question!', 'Number one priorities!']
>>> work_quotes.clear()
>>> work_quotes
[]

使用index()按值查找项目的偏移量

如果想知道列表中项目按其值的偏移量,使用index()

>>> marxes = ['Groucho', 'Chico', 'Harpo', 'Zeppo']
>>> marxes.index('Chico')
1

如果该值在列表中出现多次,只返回第一个的偏移量:

>>> simpsons = ['Lisa', 'Bart', 'Marge', 'Homer', 'Bart']
>>> simpsons.index('Bart')
1

使用in测试值是否存在

在列表中检查值是否存在的 Python 方式是使用in

>>> marxes = ['Groucho', 'Chico', 'Harpo', 'Zeppo']
>>> 'Groucho' in marxes
True
>>> 'Bob' in marxes
False

同一个值可能在列表中出现多次。只要至少出现一次,in 就会返回 True

>>> words = ['a', 'deer', 'a' 'female', 'deer']
>>> 'deer' in words
True
注意

如果经常检查列表中某个值的存在性,并且不关心项目的顺序,Python set 是存储和查找唯一值的更合适方式。我们在第八章中讨论了集合。

使用count()计算值的出现次数

要统计列表中特定值出现的次数,请使用count()

>>> marxes = ['Groucho', 'Chico', 'Harpo']
>>> marxes.count('Harpo')
1
>>> marxes.count('Bob')
0
>>> snl_skit = ['cheeseburger', 'cheeseburger', 'cheeseburger']
>>> snl_skit.count('cheeseburger')
3

使用join()将列表转换为字符串

“使用 join()组合” 更详细地讨论了join(),这里是另一个示例:

>>> marxes = ['Groucho', 'Chico', 'Harpo']
>>> ', '.join(marxes)
'Groucho, Chico, Harpo'

你可能会认为这似乎有点反向。join()是一个字符串方法,而不是一个列表方法。你不能说marxes.join(', '),即使它看起来更直观。join()的参数是一个字符串或任何可迭代的字符串序列(包括列表),其输出是一个字符串。如果join()只是一个列表方法,你不能将其与其他可迭代对象如元组或字符串一起使用。如果你确实希望它能处理任何可迭代类型,你需要为每种类型编写特殊的代码来处理实际的连接。记住——join() split()是相反的,如下所示:

>>> friends = ['Harry', 'Hermione', 'Ron']
>>> separator = ' * '
>>> joined = separator.join(friends)
>>> joined
'Harry * Hermione * Ron'
>>> separated = joined.split(separator)
>>> separated
['Harry', 'Hermione', 'Ron']
>>> separated == friends
True

使用sort()sorted()重新排序项目

通常需要按值而不是偏移量对列表中的项目进行排序。Python 提供了两个函数:

  • 列表方法sort()原地对列表进行排序。

  • 通用函数sorted()返回列表的已排序副本

如果列表中的项是数字,则默认按升序数字顺序排序。如果它们是字符串,则按字母顺序排序:

>>> marxes = ['Groucho', 'Chico', 'Harpo']
>>> sorted_marxes = sorted(marxes)
>>> sorted_marxes
['Chico', 'Groucho', 'Harpo']

sorted_marxes是一个新列表,创建它并未改变原始列表:

>>> marxes
['Groucho', 'Chico', 'Harpo']

但在marxes列表上调用列表函数sort()确实会改变marxes

>>> marxes.sort()
>>> marxes
['Chico', 'Groucho', 'Harpo']

如果列表的元素都是相同类型的(例如在marxes中的字符串),sort()将正常工作。有时甚至可以混合类型——例如整数和浮点数——因为 Python 在表达式中会自动转换它们:

>>> numbers = [2, 1, 4.0, 3]
>>> numbers.sort()
>>> numbers
[1, 2, 3, 4.0]

默认排序顺序是升序,但可以添加参数reverse=True将其设置为降序:

>>> numbers = [2, 1, 4.0, 3]
>>> numbers.sort(reverse=True)
>>> numbers
[4.0, 3, 2, 1]

使用len()获取长度

len()返回列表中的项数:

>>> marxes = ['Groucho', 'Chico', 'Harpo']
>>> len(marxes)
3

使用=分配

当你将一个列表分配给多个变量时,在一个地方改变列表也会在另一个地方改变,如下所示:

>>> a = [1, 2, 3]
>>> a
[1, 2, 3]
>>> b = a
>>> b
[1, 2, 3]
>>> a[0] = 'surprise'
>>> a
['surprise', 2, 3]

那么现在b里面是什么?它还是[1, 2, 3],还是['surprise', 2, 3]?让我们看看:

>>> b
['surprise', 2, 3]

记住第二章中的盒子(对象)和带有注释的字符串(变量名)类比?b只是引用与a相同的列表对象(两个名称字符串引导到同一个对象盒子)。无论我们使用名称a还是b改变列表内容,都会反映在两者上:

>>> b
['surprise', 2, 3]
>>> b[0] = 'I hate surprises'
>>> b
['I hate surprises', 2, 3]
>>> a
['I hate surprises', 2, 3]

使用copy()list()或切片复制

你可以通过以下任一方法将列表的值复制到独立的新列表中:

  • 列表copy()方法

  • list()转换函数

  • 列表切片[:]

我们的原始列表将再次是a。我们用列表copy()函数制作b,用list()转换函数制作c,用列表切片制作d

>>> a = [1, 2, 3]
>>> b = a.copy()
>>> c = list(a)
>>> d = a[:]

同样,bcda副本:它们是具有自己值且与原始列表对象[1, 2, 3]没有连接的新对象。改变a不会影响副本bcd

>>> a[0] = 'integer lists are boring'
>>> a
['integer lists are boring', 2, 3]
>>> b
[1, 2, 3]
>>> c
[1, 2, 3]
>>> d
[1, 2, 3]

使用deepcopy()复制所有内容

如果列表的值全部是不可变的,copy() 函数可以很好地工作。正如之前所见,可变值(如列表、元组或字典)是引用。对原始对象或副本的更改将反映在两者中。

让我们使用前面的例子,但将列表 a 中的最后一个元素更改为列表 [8, 9] 而不是整数 3

>>> a = [1, 2, [8, 9]]
>>> b = a.copy()
>>> c = list(a)
>>> d = a[:]
>>> a
[1, 2, [8, 9]]
>>> b
[1, 2, [8, 9]]
>>> c
[1, 2, [8, 9]]
>>> d
[1, 2, [8, 9]]

目前为止一切顺利。现在更改 a 中的子列表中的一个元素:

>>> a[2][1] = 10
>>> a
[1, 2, [8, 10]]
>>> b
[1, 2, [8, 10]]
>>> c
[1, 2, [8, 10]]
>>> d
[1, 2, [8, 10]]

现在,a[2] 的值是一个列表,它的元素可以被改变。我们使用的所有列表复制方法都是 浅复制(不是价值判断,而是深度判断)。

要修复这个问题,我们需要使用 deepcopy() 函数:

>>> import copy
>>> a = [1, 2, [8, 9]]
>>> b = copy.deepcopy(a)
>>> a
[1, 2, [8, 9]]
>>> b
[1, 2, [8, 9]]
>>> a[2][1] = 10
>>> a
[1, 2, [8, 10]]
>>> b
[1, 2, [8, 9]]

deepcopy() 可以处理深度嵌套的列表、字典和其他对象。

你将在第九章更多地了解 import

比较列表

你可以直接使用比较运算符如 ==< 等来比较列表。这些运算符遍历两个列表,比较相同偏移量的元素。如果列表 a 比列表 b 短,并且所有元素都相等,则 a 小于 b

>>> a = [7, 2]
>>> b = [7, 2, 9]
>>> a == b
False
>>> a <= b
True
>>> a < b
True

使用 for 和 in 迭代

在第六章中,你看到了如何使用 for 迭代字符串,但更常见的是迭代列表:

>>> cheeses = ['brie', 'gjetost', 'havarti']
>>> for cheese in cheeses:
...     print(cheese)
...
brie
gjetost
havarti

与以前一样,break 结束 for 循环,continue 跳到下一个迭代:

>>> cheeses = ['brie', 'gjetost', 'havarti']
>>> for cheese in cheeses:
...     if cheese.startswith('g'):
...         print("I won't eat anything that starts with 'g'")
...         break
...     else:
...         print(cheese)
...
brie
I won't eat anything that starts with 'g'

如果 for 循环完成而没有 break,你仍然可以使用可选的 else

>>> cheeses = ['brie', 'gjetost', 'havarti']
>>> for cheese in cheeses:
...     if cheese.startswith('x'):
...         print("I won't eat anything that starts with 'x'")
...         break
...     else:
...         print(cheese)
... else:
...     print("Didn't find anything that started with 'x'")
...
brie
gjetost
havarti
Didn't find anything that started with 'x'

如果初始的 for 从未运行,则控制也转到 else

>>> cheeses = []
>>> for cheese in cheeses:
...     print('This shop has some lovely', cheese)
...     break
... else:  # no break means no cheese
...     print('This is not much of a cheese shop, is it?')
...
This is not much of a cheese shop, is it?

因为在这个例子中 cheeses 列表为空,所以 for cheese in cheeses 从未完成过单个循环,它的 break 语句从未执行过。

使用 zip() 迭代多个序列

还有一个很好的迭代技巧:通过使用 zip() 函数并行迭代多个序列:

>>> days = ['Monday', 'Tuesday', 'Wednesday']
>>> fruits = ['banana', 'orange', 'peach']
>>> drinks = ['coffee', 'tea', 'beer']
>>> desserts = ['tiramisu', 'ice cream', 'pie', 'pudding']
>>> for day, fruit, drink, dessert in zip(days, fruits, drinks, desserts):
...     print(day, ": drink", drink, "- eat", fruit, "- enjoy", dessert)
...
Monday : drink coffee - eat banana - enjoy tiramisu
Tuesday : drink tea - eat orange - enjoy ice cream
Wednesday : drink beer - eat peach - enjoy pie

zip() 在最短的序列结束时停止。 其中一个列表(desserts)比其他列表长,因此除非我们扩展其他列表,否则没有人会得到任何布丁。

第八章 展示了 dict() 函数如何从包含两个元素的序列(如元组、列表或字符串)创建字典。你可以使用 zip() 遍历多个序列,并从相同偏移量的项目创建元组。让我们创建两个对应的英文和法文单词的元组:

>>> english = 'Monday', 'Tuesday', 'Wednesday'
>>> french = 'Lundi', 'Mardi', 'Mercredi'

现在,使用 zip() 将这些元组配对。zip() 返回的值本身不是元组或列表,而是一个可迭代的值,可以转换为元组:

>>> list( zip(english, french) )
[('Monday', 'Lundi'), ('Tuesday', 'Mardi'), ('Wednesday', 'Mercredi')]

zip() 的结果直接提供给 dict(),完成:一个微小的英法词典!

>>> dict( zip(english, french) )
{'Monday': 'Lundi', 'Tuesday': 'Mardi', 'Wednesday': 'Mercredi'}

利用列表推导式创建列表

你已经了解如何使用方括号或 list() 函数创建列表。这里,我们将看看如何使用 列表推导式 创建列表,它包含了你刚刚看到的 for/in 迭代。

你可以像这样逐个项地构建从 15 的整数列表:

>>> number_list = []
>>> number_list.append(1)
>>> number_list.append(2)
>>> number_list.append(3)
>>> number_list.append(4)
>>> number_list.append(5)
>>> number_list
[1, 2, 3, 4, 5]

或者,你也可以使用迭代器和 range() 函数:

>>> number_list = []
>>> for number in range(1, 6):
...     number_list.append(number)
...
>>> number_list
[1, 2, 3, 4, 5]

或者,你可以直接将 range() 的输出转换为列表:

>>> number_list = list(range(1, 6))
>>> number_list
[1, 2, 3, 4, 5]

所有这些方法都是有效的 Python 代码,并且会产生相同的结果。然而,更 Pythonic(而且通常更快)的构建列表的方式是使用列表推导式。列表推导式的最简单形式看起来像这样:

[*`expression`* for *`item`* in *`iterable`*]

下面是一个列表推导式如何构建整数列表:

>>> number_list = [number for number in range(1,6)]
>>> number_list
[1, 2, 3, 4, 5]

在第一行,你需要第一个number变量为列表生成值:也就是说,将循环的结果放入number_list中。第二个number是循环的一部分。为了显示第一个number是一个表达式,请尝试这个变体:

>>> number_list = [number-1 for number in range(1,6)]
>>> number_list
[0, 1, 2, 3, 4]

列表推导式将循环移到方括号内部。这个推导式示例并不比之前的示例更简单,但你可以做更多。列表推导式可以包含条件表达式,看起来像这样:

[*`expression`* for *`item`*
in *`iterable`* if *`condition`*]

让我们创建一个新的推导式,构建一个仅包含15之间奇数的列表(记住number % 2对于奇数为True,对于偶数为False):

>>> a_list = [number for number in range(1,6) if number % 2 == 1]
>>> a_list
[1, 3, 5]

现在,推导式比其传统的对应部分更紧凑了一点:

>>> a_list = []
>>> for number in range(1,6):
...     if number % 2 == 1:
...         a_list.append(number)
...
>>>  a_list
[1, 3, 5]

最后,就像可以有嵌套循环一样,对应的推导式中还可以有超过一个for ...子句集。为了展示这一点,让我们首先尝试一个简单的嵌套循环并打印结果:

>>> rows = range(1,4)
>>> cols = range(1,3)
>>> for row in rows:
...     for col in cols:
...         print(row, col)
...
1 1
1 2
2 1
2 2
3 1
3 2

现在,让我们使用一个推导式,并将其分配给变量cells,使其成为一个(row, col)元组的列表:

>>> rows = range(1,4)
>>> cols = range(1,3)
>>> cells = [(row, col) for row in rows for col in cols]
>>> for cell in cells:
...     print(cell)
...
(1, 1)
(1, 2)
(2, 1)
(2, 2)
(3, 1)
(3, 2)

顺便说一句,你也可以使用元组解包从每个元组中获取rowcol值,当你迭代cells列表时:

>>> for row, col in cells:
...     print(row, col)
...
1 1
1 2
2 1
2 2
3 1
3 2

列表推导式中的for row ...for col ...片段也可以有它们自己的if测试。

列表的列表

列表可以包含不同类型的元素,包括其他列表,如下所示:

>>> small_birds = ['hummingbird', 'finch']
>>> extinct_birds = ['dodo', 'passenger pigeon', 'Norwegian Blue']
>>> carol_birds = [3, 'French hens', 2, 'turtledoves']
>>> all_birds = [small_birds, extinct_birds, 'macaw', carol_birds]

那么,作为列表的列表的all_birds看起来是什么样子?

>>> all_birds
[['hummingbird', 'finch'], ['dodo', 'passenger pigeon', 'Norwegian Blue'], 'macaw',
[3, 'French hens', 2, 'turtledoves']]

让我们看看其中的第一个项目:

>>> all_birds[0]
['hummingbird', 'finch']

第一项是一个列表:实际上,它是我们创建all_birds时指定的small_birds的第一项。你应该能够猜到第二项是什么:

>>> all_birds[1]
['dodo', 'passenger pigeon', 'Norwegian Blue']

它是我们指定的第二项,extinct_birds。如果我们想要extinct_birds的第一项,我们可以通过指定两个索引从all_birds中提取它:

>>> all_birds[1][0]
'dodo'

[1]指的是all_birds中第二个项目的列表,而[0]指的是该内部列表中的第一个项目。

元组与列表

你经常可以在列表的地方使用元组,但它们的功能要少得多——没有append()insert()等方法——因为它们创建后无法修改。为什么不在任何地方都使用列表呢?

  • 元组使用更少的空间。

  • 你不会因为错误而破坏元组项。

  • 你可以使用元组作为字典的键(参见第八章)。

  • 命名元组(参见“命名元组”)可以作为对象的简单替代。

我不会在这里详细讨论元组。在日常编程中,你将更多地使用列表和字典。

没有元组推导式

可变类型(列表、字典和集合)有理解式。不可变类型如字符串和元组需要使用其各自章节中列出的其他方法创建。

您可能认为将列表推导的方括号更改为圆括号将创建元组推导。它似乎确实有效,因为如果您键入以下内容,将不会出现异常:

>>> number_thing = (number for number in range(1, 6))

括号中的东西完全不同:生成器推导,它返回一个 生成器对象

>>> type(number_thing)
<class 'generator'>

我将在“生成器”一章中详细讨论生成器。生成器是向迭代器提供数据的一种方式。

即将到来

它们如此出色,它们有自己的章节:字典集合

要做的事情

使用列表和元组与数字(第三章)和字符串(第五章)来表示具有丰富多样性的现实世界元素。

7.1 创建一个名为years_list的列表,从您的出生年份开始,直到您五岁生日的年份。例如,如果您出生于 1980 年,则列表将是years_list = [1980, 1981, 1982, 1983, 1984, 1985]。如果您不到五岁正在阅读本书,那我也不知道该怎么办。

7.2 years_list中哪一年是你三岁生日的那一年?记住,你的第一年是 0 岁。

7.3 years_list中哪一年你最大?

7.4 使用这三个字符串作为元素创建名为things的列表:"mozzarella""cinderella""salmonella"

7.5 将things中指向人的元素大写,然后打印列表。它改变了列表中的元素吗?

7.6 将things中“cheesy”的元素全部大写,然后打印列表。

7.7 删除things中的"disease"元素,收集您的诺贝尔奖,并打印列表。

7.8 创建一个名为surprise的列表,其中包含元素"Groucho""Chico""Harpo"

7.9 将surprise列表的最后一个元素转为小写,反转它,然后大写化。

7.10 使用列表推导创建一个名为even的列表,其中包含range(10)中的偶数。

7.11 让我们创建一个跳绳打油诗生成器。您将打印一系列两行打油诗。从以下程序片段开始:

start1 = ["fee", "fie", "foe"]
rhymes = [
    ("flop", "get a mop"),
    ("fope", "turn the rope"),
    ("fa", "get your ma"),
    ("fudge", "call the judge"),
    ("fat", "pet the cat"),
    ("fog", "walk the dog"),
    ("fun", "say we're done"),
    ]
start2 = "Someone better"

对于rhymes中的每个元组(firstsecond):

对于第一行:

  • 打印start1中的每个字符串,大写化,并跟一个感叹号和一个空格。

  • 打印first,并将其大写化,然后跟一个感叹号。

对于第二行:

  • 打印start2和一个空格。

  • 打印second和一个句点。

第八章:字典和集合

如果字典中的单词拼错了,我们怎么知道呢?

史蒂文·赖特

字典

字典 类似于列表,但条目的顺序无关紧要,也不是通过像 0 或 1 这样的偏移来选择。而是为每个 指定一个唯一的 key 来关联。这个键通常是一个字符串,但实际上可以是 Python 的任何不可变类型:布尔值、整数、浮点数、元组、字符串和其他在后面章节中会看到的类型。字典是可变的,因此可以添加、删除和更改它们的键值元素。如果您曾经使用过只支持数组或列表的语言,您会喜欢字典的。

注意

在其他语言中,字典可能被称为 关联数组哈希哈希映射。在 Python 中,字典也被称为 dict,以节省音节并让十几岁的男孩窃笑。

使用

要创建字典,您需要在逗号分隔的 key : value 对之间放置花括号 ({})。最简单的字典是一个完全不包含任何键或值的空字典:

>>> empty_dict = {}
>>> empty_dict
{}

让我们从安布罗斯·彼尔斯的 魔鬼的词典 中摘取一些引文,制作一个小字典:

>>> bierce = {
...     "day": "A period of twenty-four hours, mostly misspent",
...     "positive": "Mistaken at the top of one's voice",
...     "misfortune": "The kind of fortune that never misses",
...     }
>>>

在交互解释器中输入字典的名称将打印其键和值:

>>> bierce
{'day': 'A period of twenty-four hours, mostly misspent',
'positive': "Mistaken at the top of one's voice",
'misfortune': 'The kind of fortune that never misses'}
注意

在 Python 中,如果列表、元组或字典的最后一项后面留有逗号是可以的。当您在花括号中键入键和值时,就像在前面的例子中所做的那样,不需要缩进。这只是为了提高可读性。

使用 dict() 创建

有些人不喜欢输入那么多花括号和引号。您也可以通过将命名参数和值传递给 dict() 函数来创建字典。

传统的方式:

>>> acme_customer = {'first': 'Wile', 'middle': 'E', 'last': 'Coyote'}
>>> acme_customer
{'first': 'Wile', 'middle': 'E', 'last': 'Coyote'}

使用 dict()

>>> acme_customer = dict(first="Wile", middle="E", last="Coyote")
>>> acme_customer
{'first': 'Wile', 'middle': 'E', 'last': 'Coyote'}

第二种方式的一个限制是参数名必须是合法的变量名(不能有空格,不能是保留字):

>>> x = dict(name="Elmer", def="hunter")
  File "<stdin>", line 1
    x = dict(name="Elmer", def="hunter")
                             ^
SyntaxError: invalid syntax

使用 dict() 转换

您也可以使用 dict() 函数将包含两个值的序列转换为字典。有时您可能会遇到这样的键值序列,例如 “锶,90,碳,14。”¹ 每个序列的第一项用作键,第二项用作值。

首先,这里是使用 lol(一个包含两项列表的列表)的一个小例子:

>>> lol = [ ['a', 'b'], ['c', 'd'], ['e', 'f'] ]
>>> dict(lol)
{'a': 'b', 'c': 'd', 'e': 'f'}

我们可以使用包含两项序列的任何序列。以下是其他示例。

一个包含两项元组的列表:

>>> lot = [ ('a', 'b'), ('c', 'd'), ('e', 'f') ]
>>> dict(lot)
{'a': 'b', 'c': 'd', 'e': 'f'}

一个包含两项列表的元组:

>>> tol = ( ['a', 'b'], ['c', 'd'], ['e', 'f'] )
>>> dict(tol)
{'a': 'b', 'c': 'd', 'e': 'f'}

一个包含两个字符字符串的列表:

>>> los = [ 'ab', 'cd', 'ef' ]
>>> dict(los)
{'a': 'b', 'c': 'd', 'e': 'f'}

一个包含两个字符字符串的元组:

>>> tos = ( 'ab', 'cd', 'ef' )
>>> dict(tos)
{'a': 'b', 'c': 'd', 'e': 'f'}

章节 “使用 zip() 迭代多个序列” 介绍了 zip() 函数,它使得创建这些两项序列变得容易。

添加或更改一个项目由 [ key ]

向字典中添加项很简单。只需通过其键引用该项并赋予一个值。如果键已存在于字典中,现有值将被新值替换。如果键是新的,则将其添加到字典中并赋予其值。与列表不同,你不必担心 Python 在分配时抛出超出范围的索引异常。

让我们创建一个蒙提·派森大部分成员的字典,使用他们的姓作为键,名作为值:

>>> pythons = {
...     'Chapman': 'Graham',
...     'Cleese': 'John',
...     'Idle': 'Eric',
...     'Jones': 'Terry',
...     'Palin': 'Michael',
...     }
>>> pythons
{'Chapman': 'Graham', 'Cleese': 'John', 'Idle': 'Eric',
'Jones': 'Terry', 'Palin': 'Michael'}

我们遗漏了一个成员:出生在美国的那个,特里·吉列姆。这里有一个匿名程序员试图添加他的尝试,但是他搞砸了名字的第一个字:

>>> pythons['Gilliam'] = 'Gerry'
>>> pythons
{'Chapman': 'Graham', 'Cleese': 'John', 'Idle': 'Eric',
'Jones': 'Terry', 'Palin': 'Michael', 'Gilliam': 'Gerry'}

这里有另一位程序员编写的修复代码,他在多个方面都符合 Python 风格:

>>> pythons['Gilliam'] = 'Terry'
>>> pythons
{'Chapman': 'Graham', 'Cleese': 'John', 'Idle': 'Eric',
'Jones': 'Terry', 'Palin': 'Michael', 'Gilliam': 'Terry'}

通过使用相同的键('Gilliam'),我们用'Terry'替换了原始值'Gerry'

记住字典键必须是唯一的。这就是为什么我们在这里使用姓氏而不是名字作为键——蒙提·派森的两个成员都有名字 'Terry'!如果你使用一个键多次,最后一个值会覆盖之前的值:

>>> some_pythons = {
...     'Graham': 'Chapman',
...     'John': 'Cleese',
...     'Eric': 'Idle',
...     'Terry': 'Gilliam',
...     'Michael': 'Palin',
...     'Terry': 'Jones',
...     }
>>> some_pythons
{'Graham': 'Chapman', 'John': 'Cleese', 'Eric': 'Idle',
'Terry': 'Jones', 'Michael': 'Palin'}

我们首先将值'Gilliam'分配给键'Terry',然后将其替换为值'Jones'

通过[key]或者使用 get()获取项

这是字典的最常见用法。你指定字典和键以获取对应的值:使用前面部分的some_pythons

>>> some_pythons['John']
'Cleese'

如果字典中不存在该键,则会引发异常:

>>> some_pythons['Groucho']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'Groucho'

有两种很好的方法可以避免这种情况。第一种是在开始时使用in来测试键,就像你在上一节看到的那样:

>>> 'Groucho' in some_pythons
False

第二种方法是使用特殊的字典get()函数。你提供字典、键和一个可选值。如果键存在,你将得到其值:

>>> some_pythons.get('John')
'Cleese'

否则,你将得到可选的值(如果你指定了的话):

>>> some_pythons.get('Groucho', 'Not a Python')
'Not a Python'

否则,你将得到None(在交互式解释器中显示为空):

>>> some_pythons.get('Groucho')
>>>

使用 keys()获取所有的键

你可以使用keys()来获取字典中的所有键。我们将在接下来的几个示例中使用一个不同的样本字典:

>>> signals = {'green': 'go', 'yellow': 'go faster', 'red': 'smile for the camera'}
>>> signals.keys()
dict_keys(['green', 'yellow', 'red'])
注意

在 Python 2 中,keys()仅返回一个列表。而 Python 3 返回dict_keys(),它是键的可迭代视图。这在处理大型字典时很方便,因为它不会使用时间和内存来创建和存储一个可能不会使用的列表。但通常你确实需要一个列表。在 Python 3 中,你需要调用list()dict_keys对象转换为列表。

>>> list( signals.keys() )
['green', 'yellow', 'red']

在 Python 3 中,你还需要使用list()函数将values()items()的结果转换为普通的 Python 列表。我在这些例子中使用了这个。

使用 values()获取所有的值

要获取字典中的所有值,使用 values():

>>> list( signals.values() )
['go', 'go faster', 'smile for the camera']

使用 items()获取所有的键-值对

当你想从字典中获取所有的键-值对时,使用items()函数:

>>> list( signals.items() )
[('green', 'go'), ('yellow', 'go faster'), ('red', 'smile for the camera')]

每个键和值都作为元组返回,例如('green', 'go')

使用 len()获取长度

计算你的键值对数:

>>> len(signals)
3

使用{**a, **b}合并字典

从 Python 3.5 开始,有一种新的方法可以合并字典,使用**魔法,这在第九章中有着非常不同的用途:

>>> first = {'a': 'agony', 'b': 'bliss'}
>>> second = {'b': 'bagels', 'c': 'candy'}
>>> {**first, **second}
{'a': 'agony', 'b': 'bagels', 'c': 'candy'}

实际上,您可以传递多于两个字典:

>>> third = {'d': 'donuts'}
>>> {**first, **third, **second}
{'a': 'agony', 'b': 'bagels', 'd': 'donuts', 'c': 'candy'}

这些是复制。 如果您希望得到键和值的完整副本,并且与它们的原始字典没有关联,请参阅deepcopy()的讨论(“使用 deepcopy()复制一切”)。

使用 update()合并字典

您可以使用update()函数将一个字典的键和值复制到另一个字典中。

让我们定义包含所有成员的pythons字典:

>>> pythons = {
...     'Chapman': 'Graham',
...     'Cleese': 'John',
...     'Gilliam': 'Terry',
...     'Idle': 'Eric',
...     'Jones': 'Terry',
...     'Palin': 'Michael',
...     }
>>> pythons
{'Chapman': 'Graham', 'Cleese': 'John', 'Gilliam': 'Terry',
'Idle': 'Eric', 'Jones': 'Terry', 'Palin': 'Michael'}

我们还有一个名为others的其他幽默人物字典:

>>> others = { 'Marx': 'Groucho', 'Howard': 'Moe' }

现在,又来了另一个匿名程序员,他决定将others的成员作为蒙提·派森的成员:

>>> pythons.update(others)
>>> pythons
{'Chapman': 'Graham', 'Cleese': 'John', 'Gilliam': 'Terry',
'Idle': 'Eric', 'Jones': 'Terry', 'Palin': 'Michael',
'Marx': 'Groucho', 'Howard': 'Moe'}

如果第二个字典与它要合并的字典具有相同的键,会发生什么? 第二个字典的值将覆盖第一个字典的值:

>>> first = {'a': 1, 'b': 2}
>>> second = {'b': 'platypus'}
>>> first.update(second)
>>> first
{'a': 1, 'b': 'platypus'}

使用 del 按键删除项目

先前来自我们的匿名程序员的pythons.update(others)代码在技术上是正确的,但事实上是错误的。 虽然others的成员风趣而著名,但并非蒙提·派森的成员。 让我们撤销最后两次添加:

>>> del pythons['Marx']
>>> pythons
{'Chapman': 'Graham', 'Cleese': 'John', 'Gilliam': 'Terry',
'Idle': 'Eric', 'Jones': 'Terry', 'Palin': 'Michael',
'Howard': 'Moe'}
>>> del pythons['Howard']
>>> pythons
{'Chapman': 'Graham', 'Cleese': 'John', 'Gilliam': 'Terry',
'Idle': 'Eric', 'Jones': 'Terry', 'Palin': 'Michael'}

使用 pop()按键获取项目并删除它

这结合了get()del。 如果给pop()传递一个键,并且它存在于字典中,则返回匹配的值并删除键值对。 如果不存在,则会引发异常:

>>> len(pythons)
6
>>> pythons.pop('Palin')
'Michael'
>>> len(pythons)
5
>>> pythons.pop('Palin')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'Palin'

但是,如果给pop()传递第二个默认参数(与get()一样),一切都会很好,字典不会更改:

>>> pythons.pop('First', 'Hugo')
'Hugo'
>>> len(pythons)
5

使用 clear()删除所有项目

要从字典中删除所有键和值,请使用clear()或只需重新分配一个空字典({})给名称:

>>> pythons.clear()
>>> pythons
{}
>>> pythons = {}
>>> pythons
{}

使用 in 测试键是否存在

如果您想知道字典中是否存在某个键,请使用in。 让我们再次重新定义pythons字典,这次省略一个或两个名称:

>>> pythons = {'Chapman': 'Graham', 'Cleese': 'John',
... 'Jones': 'Terry', 'Palin': 'Michael', 'Idle': 'Eric'}

现在让我们看看里面有谁:

>>> 'Chapman' in pythons
True
>>> 'Palin' in pythons
True

这次我们记得添加特里·吉列姆了吗?

>>> 'Gilliam' in pythons
False

唉。

使用=分配

与列表一样,如果您对字典进行更改,则将反映在引用它的所有名称中:

>>> signals = {'green': 'go',
... 'yellow': 'go faster',
... 'red': 'smile for the camera'}
>>> save_signals = signals
>>> signals['blue'] = 'confuse everyone'
>>> save_signals
{'green': 'go',
'yellow': 'go faster',
'red': 'smile for the camera',
'blue': 'confuse everyone'}

使用 copy()复制

要实际从一个字典复制键和值到另一个字典,并避免这种情况,您可以使用copy()

>>> signals = {'green': 'go',
... 'yellow': 'go faster',
... 'red': 'smile for the camera'}
>>> original_signals = signals.copy()
>>> signals['blue'] = 'confuse everyone'
>>> signals
{'green': 'go',
'yellow': 'go faster',
'red': 'smile for the camera',
'blue': 'confuse everyone'}
>>> original_signals
{'green': 'go',
'yellow': 'go faster',
'red': 'smile for the camera'}
>>>

这是一个复制,并且在字典值是不可变的情况下有效。 如果不是,您需要deepcopy()

使用 deepcopy()复制一切

假设前面示例中red的值是一个列表而不是一个单独的字符串:

>>> signals = {'green': 'go',
... 'yellow': 'go faster',
... 'red': ['stop', 'smile']}
>>> signals_copy = signals.copy()
>>> signals
{'green': 'go',
'yellow': 'go faster',
'red': ['stop', 'smile']}
>>> signals_copy
{'green': 'go',
'yellow': 'go faster',
'red': ['stop', 'smile']}
>>>

让我们更改red列表中的一个值:

>>> signals['red'][1] = 'sweat'
>>> signals
{'green': 'go',
'yellow': 'go faster',
'red': ['stop', 'sweat']}
>>> signals_copy
{'green': 'go',
'yellow': 'go faster',
'red': ['stop', 'sweat']}

您将获得按名称更改的通常行为。 copy()方法直接复制了值,这意味着signal_copy对于'red'得到了与signals相同的列表值。

解决方案是deepcopy()

>>> import copy
>>> signals = {'green': 'go',
... 'yellow': 'go faster',
... 'red': ['stop', 'smile']}
>>> signals_copy = copy.deepcopy(signals)
>>> signals
{'green': 'go',
'yellow': 'go faster',
'red': ['stop', 'smile']}
>>> signals_copy
{'green': 'go',
'yellow':'go faster',
'red': ['stop', 'smile']}
>>> signals['red'][1] = 'sweat'
>>> signals
{'green': 'go',
'yellow': 'go faster',
red': ['stop', 'sweat']}
>>> signals_copy
{'green': 'go',
'yellow': 'go faster',
red': ['stop', 'smile']}

比较字典

与上一章的列表和元组一样,字典可以使用简单的比较运算符==!=进行比较:

>>> a = {1:1, 2:2, 3:3}
>>> b = {3:3, 1:1, 2:2}
>>> a == b
True

其他运算符不起作用:

>>> a = {1:1, 2:2, 3:3}
>>> b = {3:3, 1:1, 2:2}
>>> a <= b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<=' not supported between instances of 'dict' and 'dict'

Python 逐一比较键和值。它们最初创建的顺序无关紧要。在这个例子中,ab是相等的,除了键1a中具有列表值[1, 2],而在b中具有列表值[1, 1]

>>> a = {1: [1, 2], 2: [1], 3:[1]}
>>> b = {1: [1, 1], 2: [1], 3:[1]}
>>> a == b
False

使用forin进行迭代

遍历字典(或其keys()函数)会返回键。在这个例子中,键是桌游 Clue(在北美以外称为 Cluedo)中的卡片类型:

>>> accusation = {'room': 'ballroom', 'weapon': 'lead pipe',
...               'person': 'Col. Mustard'}
>>> for card in accusation:  #  or, for card in accusation.keys():
...     print(card)
...
room
weapon
person

要迭代值而不是键,可以使用字典的values()函数:

>>> for value in accusation.values():
...     print(value)
...
ballroom
lead pipe
Col. Mustard

要返回键和值作为元组,可以使用items()函数:

>>> for item in accusation.items():
...     print(item)
...
('room', 'ballroom')
('weapon', 'lead pipe')
('person', 'Col. Mustard')

可以一步到位地将元组分配给元组。对于由items()返回的每个元组,将第一个值(键)分配给card,第二个值(值)分配给contents

>>> for card, contents in accusation.items():
...     print('Card', card, 'has the contents', contents)
...
Card weapon has the contents lead pipe
Card person has the contents Col. Mustard
Card room has the contents ballroom

字典推导

为了不被那些资产阶级列表所抛弃,字典也有推导。最简单的形式看起来很熟悉:

{*`key_expression`* : *`value_expression`* for *`expression`* in *`iterable`*}
>>> word = 'letters'
>>> letter_counts = {letter: word.count(letter) for letter in word}
>>> letter_counts
{'l': 1, 'e': 2, 't': 2, 'r': 1, 's': 1}

我们正在循环遍历字符串'letters'中的每个字母,并计算该字母出现的次数。两次使用word.count(letter)是浪费时间的,因为我们必须两次计算所有e和所有t。但是当我们第二次计算e时,我们不会有任何损害,因为我们只是替换已经存在的字典条目;对于计算t的情况也是如此。因此,以下方式可能会更符合 Python 的风格:

>>> word = 'letters'
>>> letter_counts = {letter: word.count(letter) for letter in set(word)}
>>> letter_counts
{'t': 2, 'l': 1, 'e': 2, 'r': 1, 's': 1}

与前面的例子不同,字典的键的顺序不同,因为对set(word)进行迭代会以不同的顺序返回字母,而迭代字符串word则以不同的顺序返回。

与列表推导类似,字典推导也可以有if测试和多个for子句:

{*`key_expression`* : *`value_expression`* for *`expression`* in *`iterable`* if *`condition`*}
>>> vowels = 'aeiou'
>>> word = 'onomatopoeia'
>>> vowel_counts = {letter: word.count(letter) for letter in set(word)
 if letter in vowels}
>>> vowel_counts
{'e': 1, 'i': 1, 'o': 4, 'a': 2}

查看PEP-274以获取更多字典推导的例子。

集合

集合类似于一个字典,它的值被丢弃,只留下键。与字典一样,每个键必须是唯一的。当你只想知道某个东西是否存在时,可以使用集合,而不需要其他信息。它是一组键的袋子。如果你想将一些信息附加到键上作为值,请使用字典。

在某些过去的时代和某些地方,集合理论是与基本数学一起教授的内容。如果你的学校跳过了它(或者你当时在看窗外),图 8-1 展示了集合并和交的概念。

假设你对有一些共同键的两个集合取并集。因为集合必须仅包含每个项的一个副本,两个集合的并集将只包含每个键的一个副本。集合是一个没有元素的集合。在图 8-1 中,空集合的一个例子是以X开头的女性名字。

inp2 0801

图 8-1. 集合的常见操作

使用set()创建集合

要创建一个集合,可以使用set()函数或将一个或多个逗号分隔的值放在花括号中,如下所示:

>>> empty_set = set()
>>> empty_set
set()
>>> even_numbers = {0, 2, 4, 6, 8}
>>> even_numbers
{0, 2, 4, 6, 8}
>>> odd_numbers = {1, 3, 5, 7, 9}
>>> odd_numbers
{1, 3, 5, 7, 9}

集合是无序的。

注意

因为 [] 创建一个空列表,你可能期望 {} 创建一个空集合。相反,{} 创建一个空字典。这也是为什么解释器将空集合打印为 set() 而不是 {}。为什么?字典在 Python 中先出现,并且拿下了花括号的所有权。拥有是法律的九分之一。²

使用 set() 进行转换:

你可以从列表、字符串、元组或字典创建一个集合,丢弃任何重复的值。

首先,让我们看看一个字符串中某些字母出现多次的情况:

>>> set( 'letters' )
{'l', 'r', 's', 't', 'e'}

注意集合只包含一个 'e''t',即使 'letters' 包含两个每个。

现在,让我们从列表创建一个集合:

>>> set( ['Dasher', 'Dancer', 'Prancer', 'Mason-Dixon'] )
{'Dancer', 'Dasher', 'Mason-Dixon', 'Prancer'}

这次,从元组创建一个集合:

>>> set( ('Ummagumma', 'Echoes', 'Atom Heart Mother') )
{'Ummagumma', 'Atom Heart Mother', 'Echoes'}

当你给 set() 一个字典时,它只使用键:

>>> set( {'apple': 'red', 'orange': 'orange', 'cherry': 'red'} )
{'cherry', 'orange', 'apple'}

使用 len() 获取长度:

让我们数一数我们的驯鹿:

>>> reindeer = set( ['Dasher', 'Dancer', 'Prancer', 'Mason-Dixon'] )
>>> len(reindeer)
4

使用 add() 添加一个项目:

使用集合的 add() 方法将另一个项目添加到集合中:

>>> s = set((1,2,3))
>>> s
{1, 2, 3}
>>> s.add(4)
>>> s
{1, 2, 3, 4}

使用 remove() 删除一个项目:

你可以通过值从集合中删除一个值:

>>> s = set((1,2,3))
>>> s.remove(3)
>>> s
{1, 2}

使用 for 和 in 进行迭代:

像字典一样,你可以遍历集合中的所有项目:

>>> furniture = set(('sofa', 'ottoman', 'table'))
>>> for piece in furniture:
...     print(piece)
...
ottoman
table
sofa

使用 in 测试一个值:

这是集合的最常见用法。我们将创建一个名为 drinks 的字典。每个键是混合饮料的名称,相应的值是该饮料成分的集合:

>>> drinks = {
...     'martini': {'vodka', 'vermouth'},
...     'black russian': {'vodka', 'kahlua'},
...     'white russian': {'cream', 'kahlua', 'vodka'},
...     'manhattan': {'rye', 'vermouth', 'bitters'},
...     'screwdriver': {'orange juice', 'vodka'}
...     }

即使两者都用花括号({})括起来,一个集合只是一堆值,而字典包含 对。

哪些饮料含有伏特加?

>>> for name, contents in drinks.items():
...     if 'vodka' in contents:
...         print(name)
...
screwdriver
martini
black russian
white russian

我们想要一些伏特加,但我们对乳糖不耐受,并且认为苦艾酒味道像煤油:

>>> for name, contents in drinks.items():
...     if 'vodka' in contents and not ('vermouth' in contents or
...         'cream' in contents):
...         print(name)
...
screwdriver
black russian

我们将在下一节中简要重写这段话。

组合和操作符

如果你想要检查集合值的组合怎么办?假设你想要找到任何含有橙汁或苦艾酒的饮料?让我们使用 集合交集操作符,即 &

>>> for name, contents in drinks.items():
...     if contents & {'vermouth', 'orange juice'}:
...         print(name)
...
screwdriver
martini
manhattan

& 操作符的结果是一个集合,其中包含与比较的两个列表中都出现的所有项。如果这些成分都不在 contents 中,则 & 返回一个空集合,被视为 False

现在,让我们重新写前一节的示例,在这个示例中,我们想要伏特加,但不要奶油和苦艾酒:

>>> for name, contents in drinks.items():
...     if 'vodka' in contents and not contents & {'vermouth', 'cream'}:
...         print(name)
...
screwdriver
black russian

让我们把这两种饮料的成分集合保存在变量中,以节省我们娇贵的手指在接下来的示例中的打字:

>>> bruss = drinks['black russian']
>>> wruss = drinks['white russian']

下面是所有集合操作符的示例。有些有特殊的标点符号,有些有特殊的功能,还有些两者兼有。让我们使用测试集合 a(包含 12)和 b(包含 23):

>>> a = {1, 2}
>>> b = {2, 3}

正如你之前看到的,使用特殊标点符号 & 获取 交集(两个集合共同的成员)。集合 intersection() 函数也是如此。

>>> a & b
{2}
>>> a.intersection(b)
{2}

此代码片段使用我们保存的饮料变量:

>>> bruss & wruss
{'kahlua', 'vodka'}

在这个例子中,使用特殊标点符号 & 获取 并集(两个集合的成员)或集合 union() 函数。

>>> a | b
{1, 2, 3}
>>> a.union(b)
{1, 2, 3}

这是酒精版本:

>>> bruss | wruss
{'cream', 'kahlua', 'vodka'}

差集(第一个集合的成员但不是第二个集合的成员)可以通过使用字符-difference()函数获得:

>>> a - b
{1}
>>> a.difference(b)
{1}
>>> bruss - wruss
set()
>>> wruss - bruss
{'cream'}

到目前为止,最常见的集合操作是并集、交集和差集。我在接下来的示例中包括了其他操作,但你可能永远不会用到它们。

异或(一个集合中的项目或另一个,但不是两者都有)使用^symmetric_difference()

>>> a ^ b
{1, 3}
>>> a.symmetric_difference(b)
{1, 3}

这找到我们两种俄罗斯饮料中独特的成分:

>>> bruss ^ wruss
{'cream'}

你可以通过使用<=issubset()来检查一个集合是否是另一个的子集(第一个集合的所有成员也在第二个集合中):

>>> a <= b
False
>>> a.issubset(b)
False

给黑俄罗斯加奶油会变成白俄罗斯,所以wrussbruss的超集:

>>> bruss <= wruss
True

任何集合都是其自身的子集吗?是的。³

>>> a <= a
True
>>> a.issubset(a)
True

要成为一个真子集,第二个集合需要包含第一个集合的所有成员及更多。通过使用<,如下例所示:

>>> a < b
False
>>> a < a
False
>>> bruss < wruss
True

超集是子集的反义词(第二个集合的所有成员也是第一个集合的成员)。这使用>=issuperset()

>>> a >= b
False
>>> a.issuperset(b)
False
>>> wruss >= bruss
True

任何集合都是其自身的超集:

>>> a >= a
True
>>> a.issuperset(a)
True

最后,你可以通过使用>来找到一个真子集(第一个集合包含第二个集合的所有成员,且更多)如下所示:

>>> a > b
False
>>> wruss > bruss
True

你不能是你自己的真超集:

>>> a > a
False

集合推导式

没有人想被忽略,所以即使是集合也有推导式。最简单的版本看起来像你刚刚看到的列表和字典推导式:

{ expression for expression in iterable }

它还可以具有可选的条件测试:

{ expression for expression in iterable if condition }

>>> a_set = {number for number in range(1,6) if number % 3 == 1}
>>> a_set
{1, 4}

使用frozenset()创建不可变集合

如果你想创建一个不能被更改的集合,可以使用frozenset()函数和任何可迭代参数:

>>> frozenset([3, 2, 1])
frozenset({1, 2, 3})
>>> frozenset(set([2, 1, 3]))
frozenset({1, 2, 3})
>>> frozenset({3, 1, 2})
frozenset({1, 2, 3})
>>> frozenset( (2, 3, 1) )
frozenset({1, 2, 3})

它真的冻结了吗?

>>> fs = frozenset([3, 2, 1])
>>> fs
frozenset({1, 2, 3})
>>> fs.add(4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'frozenset' object has no attribute 'add'

是的,非常冷。

到目前为止的数据结构

回顾一下,你可以创建

  • 通过使用方括号([])创建列表

  • 通过逗号和可选括号创建元组

  • 通过使用花括号({})创建字典或集合

除了集合,你可以通过使用方括号访问单个元素。对于列表和元组,方括号中的值是整数偏移量。对于字典,它是一个键。对于这三个,结果是一个值。对于集合,要么有,要么没有;没有索引或键:

>>> marx_list = ['Groucho', 'Chico', 'Harpo']
>>> marx_tuple = ('Groucho', 'Chico', 'Harpo')
>>> marx_dict = {'Groucho': 'banjo', 'Chico': 'piano', 'Harpo': 'harp'}
>>> marx_set = {'Groucho', 'Chico', 'Harpo'}
>>> marx_list[2]
'Harpo'
>>> marx_tuple[2]
'Harpo'
>>> marx_dict['Harpo']
'harp'
>>> 'Harpo' in marx_list
True
>>> 'Harpo' in marx_tuple
True
>>> 'Harpo' in marx_dict
True
>>> 'Harpo' in marx_set
True

创建更大的数据结构

我们从简单的布尔值、数字和字符串逐步进阶到列表、元组、集合和字典。你可以将这些内置数据结构组合成更大、更复杂的结构。让我们从三个不同的列表开始:

>>> marxes = ['Groucho', 'Chico', 'Harpo']
>>> pythons = ['Chapman', 'Cleese', 'Gilliam', 'Jones', 'Palin']
>>> stooges = ['Moe', 'Curly', 'Larry']

我们可以创建一个包含每个列表作为元素的元组:

>>> tuple_of_lists = marxes, pythons, stooges
>>> tuple_of_lists
(['Groucho', 'Chico', 'Harpo'],
['Chapman', 'Cleese', 'Gilliam', 'Jones', 'Palin'],
['Moe', 'Curly', 'Larry'])

我们可以创建一个包含这三个列表的列表:

>>> list_of_lists = [marxes, pythons, stooges]
>>> list_of_lists
[['Groucho', 'Chico', 'Harpo'],
['Chapman', 'Cleese', 'Gilliam', 'Jones', 'Palin'],
['Moe', 'Curly', 'Larry']]

最后,让我们创建一个字典的列表。在这个例子中,让我们使用喜剧组的名字作为键,成员列表作为值:

>>> dict_of_lists = {'Marxes': marxes, 'Pythons': pythons, 'Stooges': stooges}
>> dict_of_lists
{'Marxes': ['Groucho', 'Chico', 'Harpo'],
'Pythons': ['Chapman', 'Cleese', 'Gilliam', 'Jones', 'Palin'],
'Stooges': ['Moe', 'Curly', 'Larry']}

你的唯一限制是数据类型本身的限制。例如,字典键必须是不可变的,因此列表、字典或集合不能作为另一个字典的键。但元组可以。例如,你可以通过 GPS 坐标(纬度、经度和高度;请参见 第二十一章 获取更多映射示例)来索引感兴趣的站点:

>>> houses = {
 (44.79, -93.14, 285): 'My House',
 (38.89, -77.03, 13): 'The White House'
 }

即将发生

回到代码结构。你将学习如何将代码包装在 函数 中,以及在出现 异常 时如何处理。

要做的事情

8.1 制作一个英语到法语的字典 e2f 并打印出来。以下是你的起始词汇:dogchiencatchatwalrusmorse

8.2 使用你的三词字典 e2f,打印法语单词 walrus 的英文对应词。

8.3 使用 e2f 创建一个法语到英语的字典 f2e。使用 items 方法。

8.4 打印法语单词 chien 的英文对应词。

8.5 打印从 e2f 中得到的英语单词集合。

8.6 制作一个名为 life 的多级字典。使用以下字符串作为最顶层键:'animals''plants''other'。使 'animals' 键参考另一个具有键 'cats''octopi''emus' 的字典。使 'cats' 键参考一个包含值 'Henri''Grumpy''Lucy' 的字符串列表。使所有其他键参考空字典。

8.7 打印 life 的顶层键。

8.8 打印 life['animals'] 的键。

8.9 打印 life['animals']['cats'] 的值。

8.10 使用字典推导来创建字典 squares。使用 range(10) 返回键,并使用每个键的平方作为其值。

8.11 使用集合推导来创建集合 odd,其中包含 range(10) 中的奇数。

8.12 使用生成器推导来返回字符串 'Got 'range(10) 中的数字。通过使用 for 循环进行迭代。

8.13 使用 zip() 从键元组 ('optimist', 'pessimist', 'troll') 和值元组 ('The glass is half full', 'The glass is half empty', 'How did you get a glass?') 创建一个字典。

8.14 使用 zip() 创建一个名为 movies 的字典,将以下列表配对:titles = ['Creature of Habit', 'Crewel Fate', 'Sharks On a Plane']plots = ['A nun turns into a monster', 'A haunted yarn shop', 'Check your exits']

¹ 同时,Strontium-Carbon 游戏的最终得分。

² 根据律师和驱魔师的说法。

³ 尽管,借用格劳乔·马尔克斯的话,“我不想加入那种会接受我作为成员的俱乐部。”

第九章:函数

函数越小,管理越大。

  1. 北科特·帕金森

到目前为止,我们所有的 Python 代码示例都是小片段。这些对于小任务很好,但没人想一直重复输入片段。我们需要一种方法将更大的代码组织成可管理的片段。

代码重用的第一步是函数:一个命名的代码片段,独立于所有其他代码。函数可以接受任意数量和类型的输入参数,并返回任意数量和类型的输出结果

你可以用一个函数做两件事情:

  • 定义它,带有零个或多个参数

  • 调用它,然后得到零个或多个结果

用 def 定义一个函数

要定义一个 Python 函数,你需要输入def,函数名,括号包围任何输入参数到函数中,然后最后是一个冒号(:)。函数名的规则与变量名相同(必须以字母或_开头,只能包含字母、数字或_)。

让我们一步一步来,首先定义并调用一个没有参数的函数。这是最简单的 Python 函数:

>>> def do_nothing():
...     pass

即使对于像这样没有参数的函数,你仍然需要在其定义中使用括号和冒号。下一行需要缩进,就像你在if语句下缩进代码一样。Python 需要pass语句来表明这个函数什么也不做。这相当于本页有意留白(尽管它不再是空白的)。

用括号调用一个函数

你只需输入函数名和括号就可以调用这个函数。它像广告一样运行,什么也不做,但做得非常好:

>>> do_nothing()
>>>

现在让我们定义并调用另一个没有参数但打印单词的函数:

>>> def make_a_sound():
...     print('quack')
...
>>> make_a_sound()
quack

当你调用make_a_sound()函数时,Python 会执行其定义内的代码。在这种情况下,它打印了一个单词并返回到主程序。

让我们试试一个没有参数但返回值的函数:

>>> def agree():
...    return True
...

你可以调用这个函数并使用if测试其返回值:

>>> if agree():
...     print('Splendid!')
... else:
...     print('That was unexpected.')
...
Splendid!

你刚刚迈出了一个大步。函数与诸如if这样的测试以及诸如while这样的循环的结合,使你能够做一些以前无法做到的事情。

参数和参数

此时,是时候在括号里放些东西了。让我们定义一个名为echo()的函数,其中有一个名为anything的参数。它使用return语句两次将anything的值发送回调用者,之间用空格隔开:

>>> def echo(anything):
...    return anything + ' ' + anything
...
>>>

现在让我们用字符串'Rumplestiltskin'调用echo()

>>> echo('Rumplestiltskin')
'Rumplestiltskin Rumplestiltskin'

当你调用带有参数的函数时,你传递的这些值称为参数。当你带有参数调用函数时,这些参数的值被复制到函数内部的对应参数中。

注意

换句话说:它们在函数外被称为参数,但在函数内部称为参数

在上一个示例中,函数echo()被调用时带有参数字符串'Rumplestiltskin'。此值在echo()内部复制到参数anything中,然后(在本例中加倍,并带有一个空格)返回给调用者。

这些函数示例非常基本。让我们编写一个函数,它接受一个输入参数并实际处理它。我们将调整早期评论颜色的代码片段。称之为commentary,并使其接受名为color的输入字符串参数。使其返回字符串描述给它的调用者,调用者可以决定如何处理它:

>>> def commentary(color):
...     if color == 'red':
...         return "It's a tomato."
...     elif color == "green":
...         return "It's a green pepper."
...     elif color == 'bee purple':
...         return "I don't know what it is, but only bees can see it."
...     else:
...         return "I've never heard of the color "  + color +  "."
...
>>>

使用字符串参数'blue'调用函数commentary()

>>> comment = commentary('blue')

函数执行以下操作:

  • 将值'blue'分配给函数内部的color参数

  • 通过if-elif-else逻辑链运行

  • 返回一个字符串

然后调用者将字符串赋给变量comment

我们得到了什么?

>>> print(comment)
I've never heard of the color blue.

函数可以接受任意数量(包括零个)任意类型的输入参数。它可以返回任意数量(包括零个)任意类型的输出结果。如果函数没有显式调用return,调用者将得到None的结果。

>>> print(do_nothing())
None

None 很有用

None是 Python 中的特殊值,表示当没有内容时占据的位置。它与布尔值False不同,尽管在布尔值评估时看起来是假的。以下是一个例子:

>>> thing = None
>>> if thing:
...     print("It's some thing")
... else:
...     print("It's no thing")
...
It's no thing

要区分None和布尔值False,请使用 Python 的is运算符:

>>> thing = None
>>> if thing is None:
...     print("It's nothing")
... else:
...     print("It's something")
...
It's nothing

这似乎是一个微妙的区别,但在 Python 中很重要。您将需要None来区分缺失值和空值。请记住,零值整数或浮点数,空字符串(''),列表([]),元组((,)),字典({})和集合(set())都为False,但与None不同。

让我们编写一个快速函数,打印其参数是NoneTrue还是False

>>> def whatis(thing):
...     if thing is None:
...         print(thing, "is None")
...     elif thing:
...         print(thing, "is True")
...     else:
...         print(thing, "is False")
...

让我们运行一些健全性测试:

>>> whatis(None)
None is None
>>> whatis(True)
True is True
>>> whatis(False)
False is False

一些真实的值如何?

>>> whatis(0)
0 is False
>>> whatis(0.0)
0.0 is False
>>> whatis('')
 is False
>>> whatis("")
 is False
>>> whatis('''''')
 is False
>>> whatis(())
() is False
>>> whatis([])
[] is False
>>> whatis({})
{} is False
>>> whatis(set())
set() is False
>>> whatis(0.00001)
1e-05 is True
>>> whatis([0])
[0] is True
>>> whatis([''])
[''] is True
>>> whatis(' ')
 is True

位置参数

与许多语言相比,Python 处理函数参数的方式非常灵活。最熟悉的类型是位置参数,其值按顺序复制到相应的参数中。

此函数从其位置输入参数构建字典并返回它:

>>> def menu(wine, entree, dessert):
...     return {'wine': wine, 'entree': entree, 'dessert': dessert}
...
>>> menu('chardonnay', 'chicken', 'cake')
{'wine': 'chardonnay', 'entree': 'chicken', 'dessert': 'cake'}

尽管非常普遍,位置参数的缺点是您需要记住每个位置的含义。如果我们忘记并将menu()以葡萄酒作为最后一个参数而不是第一个参数进行调用,那么餐点会非常不同:

>>> menu('beef', 'bagel', 'bordeaux')
{'wine': 'beef', 'entree': 'bagel', 'dessert': 'bordeaux'}

关键字参数

为了避免位置参数混淆,您可以按照与函数定义中不同顺序的参数名称指定参数:

>>> menu(entree='beef', dessert='bagel', wine='bordeaux')
{'wine': 'bordeaux', 'entree': 'beef', 'dessert': 'bagel'}

您可以混合位置和关键字参数。让我们首先指定葡萄酒,但使用关键字参数为主菜和甜点:

>>> menu('frontenac', dessert='flan', entree='fish')
{'wine': 'frontenac', 'entree': 'fish', 'dessert': 'flan'}

如果您同时使用位置和关键字参数调用函数,则位置参数需要首先出现。

指定默认参数值

您可以为参数指定默认值。如果调用者未提供相应的参数,则使用默认值。这个听起来平淡无奇的特性实际上非常有用。使用前面的例子:

>>> def menu(wine, entree, dessert='pudding'):
...     return {'wine': wine, 'entree': entree, 'dessert': dessert}

这次,尝试调用menu()而不带dessert参数:

>>> menu('chardonnay', 'chicken')
{'wine': 'chardonnay', 'entree': 'chicken', 'dessert': 'pudding'}

如果提供了参数,则使用该参数而不是默认值:

>>> menu('dunkelfelder', 'duck', 'doughnut')
{'wine': 'dunkelfelder', 'entree': 'duck', 'dessert': 'doughnut'}
注意

默认参数值在函数定义时计算,而不是在运行时。对于新手(有时甚至不太新的)Python 程序员来说,常见的错误是使用可变数据类型(如列表或字典)作为默认参数。

在以下测试中,buggy()函数预计每次都将使用一个新的空result列表运行,并将arg参数添加到其中,然后打印一个单项列表。然而,这里有一个错误:它只有在第一次调用时为空。第二次调用时,result仍然保留了上一次调用的一个项目:

>>> def buggy(arg, result=[]):
...     result.append(arg)
...     print(result)
...
>>> buggy('a')
['a']
>>> buggy('b')   # expect ['b']
['a', 'b']

如果写成这样,它会起作用:

>>> def works(arg):
...     result = []
...     result.append(arg)
...     return result
...
>>> works('a')
['a']
>>> works('b')
['b']

修复方法是传递其他内容以指示第一次调用:

>>> def nonbuggy(arg, result=None):
...     if result is None:
...         result = []
...     result.append(arg)
...     print(result)
...
>>> nonbuggy('a')
['a']
>>> nonbuggy('b')
['b']

这有时是 Python 工作面试的问题。你已经被警告了。

使用*爆炸/聚合位置参数

如果你在 C 或 C++中编程过,你可能会认为 Python 程序中的星号(*)与指针有关。不,Python 没有指针。

当在函数中与参数一起使用时,星号将可变数量的位置参数组合成单个参数值元组。在下面的例子中,args是由传递给函数print_args()的零个或多个参数组成的参数元组:

>>> def print_args(*args):
...     print('Positional tuple:', args)
...

如果没有参数调用函数,则在*args中什么都得不到:

>>> print_args()
Positional tuple: ()

无论您给它什么,它都将被打印为args元组:

>>> print_args(3, 2, 1, 'wait!', 'uh...')
Positional tuple: (3, 2, 1, 'wait!', 'uh...')

这对编写像print()这样接受可变数量参数的函数非常有用。如果您的函数还有必需的位置参数,将它们放在第一位;*args放在最后并获取其余所有参数:

>>> def print_more(required1, required2, *args):
...     print('Need this one:', required1)
...     print('Need this one too:', required2)
...     print('All the rest:', args)
...
>>> print_more('cap', 'gloves', 'scarf', 'monocle', 'mustache wax')
Need this one: cap
Need this one too: gloves
All the rest: ('scarf', 'monocle', 'mustache wax')
注意

当使用*时,您不需要将元组参数称为*args,但在 Python 中这是一种常见的习惯用法。在函数内部使用*args也很常见,就像前面的例子中描述的那样,尽管严格来说它被称为参数,可以称为*params

总结:

  • 您可以将位置参数传递给函数,它们将在内部与位置参数匹配。这是你在本书中到目前为止看到的内容。

  • 您可以将元组参数传递给函数,其中它将成为元组参数的一部分。这是前面一个示例的简单情况。

  • 您可以将位置参数传递给函数,并将它们聚合在参数*args中,该参数解析为元组args。这在本节中已经描述过。

  • 您还可以将名为args的元组参数“爆炸”为函数内部的位置参数*args,然后将其重新聚合到元组参数args中:

>>> print_args(2, 5, 7, 'x')
Positional tuple: (2, 5, 7, 'x')
>>> args = (2,5,7,'x')
>>> print_args(args)
Positional tuple: ((2, 5, 7, 'x'),)
>>> print_args(*args)
Positional tuple: (2, 5, 7, 'x')

您只能在函数调用或定义中使用*语法:

>>> *args
  File "<stdin>", line 1
SyntaxError: can't use starred expression here

所以:

  • 在函数外部,*args 将元组 args 展开为逗号分隔的位置参数。

  • 在函数内部,*args 将所有位置参数收集到一个名为 args 的元组中。你可以使用名称 *paramsparams,但通常在外部参数和内部参数中都使用 *args 是常见的做法。

有音感共鸣的读者也可能会在外部听到 *args 声称为 puff-args,在内部听到 inhale-args,因为值要么被展开要么被汇集。

用 ** 扩展/汇集关键字参数

你可以使用两个星号 (**) 将关键字参数组合成一个字典,其中参数名是键,它们的值是对应的字典值。下面的示例定义了函数 print_kwargs() 来打印它的关键字参数:

>>> def print_kwargs(**kwargs):
...     print('Keyword arguments:', kwargs)
...

现在试着使用一些关键字参数调用它:

>>> print_kwargs()
Keyword arguments: {}
>>> print_kwargs(wine='merlot', entree='mutton', dessert='macaroon')
Keyword arguments: {'dessert': 'macaroon', 'wine': 'merlot',
'entree': 'mutton'}

在函数内部,kwargs 是一个字典参数。

参数顺序是:

  • 必需的位置参数

  • 可选的位置参数 (*args)

  • 可选的关键字参数 (**kwargs)

args 一样,你不需要将这个关键字参数称为 kwargs,但这是常见用法:¹

** 语法仅在函数调用或定义中有效:²

>>> **kwparams
  File "<stdin>", line 1
    **kwparams
     ^
SyntaxError: invalid syntax

总结:

  • 你可以向函数传递关键字参数,函数内部将它们与关键字参数匹配。这就是你迄今为止看到的内容。

  • 你可以将字典参数传递给一个函数,函数内部会解析这些字典参数。这是前面讨论的一个简单情况。

  • 你可以向函数传递一个或多个关键字参数 (name=value),并在函数内部作为 **kwargs 收集它们,这已经在本节中讨论过了。

  • 在函数外部,**kwargs 展开 字典 kwargsname=value 参数。

  • 在函数内部,**kwargs 收集 name=value 参数到单个字典参数 kwargs 中。

如果听觉幻觉有所帮助,想象每个星号都在函数外面爆炸一下,而每个星号聚集在里面时会有一点吸气的声音。

仅关键字参数

可以传入一个与位置参数同名的关键字参数,这可能不会得到你想要的结果。Python 3 允许你指定 仅关键字参数。顾名思义,它们必须作为 name=value 而不是作为位置参数 value 提供。函数定义中的单个 * 意味着接下来的参数 startend 如果我们不想使用它们的默认值,必须作为命名参数提供:

>>> def print_data(data, *, start=0, end=100):
...     for value in (data[start:end]):
...         print(value)
...
>>> data = ['a', 'b', 'c', 'd', 'e', 'f']
>>> print_data(data)
a
b
c
d
e
f
>>> print_data(data, start=4)
e
f
>>> print_data(data, end=2)
a
b

可变与不可变参数

记住,如果你将同一个列表分配给两个变量,你可以通过任何一个变量来修改它?而如果这两个变量都引用像整数或字符串之类的不可变对象时就不行了?那是因为列表是可变的,而整数和字符串是不可变的。

在将参数传递给函数时,需要注意相同的行为。如果一个参数是可变的,它的值可以通过相应的参数在函数内部被改变:³

>>> outside = ['one', 'fine', 'day']
>>> def mangle(arg):
...    arg[1] = 'terrible!'
...
>>> outside
['one', 'fine', 'day']
>>> mangle(outside)
>>> outside
['one', 'terrible!', 'day']

最好的做法是,呃,不要这样做。⁴ 要么记录参数可能被更改,要么通过return返回新值。

文档字符串

可读性很重要,Python 禅宗确实如此。你可以通过在函数体的开头包含一个字符串来附加文档到函数定义中。这就是函数的文档字符串

>>> def echo(anything):
...     'echo returns its input argument'
...     return anything

你可以使文档字符串相当长,并且如果你愿意,甚至可以添加丰富的格式:

def print_if_true(thing, check):
 '''
 Prints the first argument if a second argument is true.
 The operation is:
 1\. Check whether the *second* argument is true.
 2\. If it is, print the *first* argument.
 '''
 if check:
 print(thing)

要打印函数的文档字符串,请调用 Python 的help()函数。传递函数的名称以获取带有精美格式的参数列表和文档字符串:

>>> help(echo)
Help on function echo in module __main__:

echo(anything)
 echo returns its input argument

如果你只想看到原始的文档字符串,而没有格式:

>>> print(echo.__doc__)
echo returns its input argument

那个看起来奇怪的__doc__是函数内部文档字符串作为变量的内部名称。双下划线(Python 术语中称为dunder)在许多地方用于命名 Python 内部变量,因为程序员不太可能在自己的变量名中使用它们。

函数是一等公民

我提到了 Python 的口头禅,一切皆为对象。这包括数字、字符串、元组、列表、字典——还有函数。在 Python 中,函数是一等公民。你可以将它们赋值给变量,将它们用作其他函数的参数,并从函数中返回它们。这使得你能够在 Python 中做一些在许多其他语言中难以或不可能实现的事情。

要测试这个,让我们定义一个简单的函数叫做answer(),它没有任何参数;它只是打印数字42

>>> def answer():
...     print(42)

如果你运行这个函数,你知道会得到什么:

>>> answer()
42

现在让我们定义另一个名为run_something的函数。它有一个名为func的参数,一个要运行的函数。进入函数后,它只是调用这个函数:

>>> def run_something(func):
...     func()

如果我们将answer传递给run_something(),我们正在使用函数作为数据,就像其他任何东西一样:

>>> run_something(answer)
42

注意,你传递的是answer,而不是answer()。在 Python 中,那些括号意味着调用这个函数。没有括号,Python 只是将函数视为任何其他对象一样对待。这是因为,像 Python 中的一切其他东西一样,它一个对象:

>>> type(run_something)
<class 'function'>

让我们尝试运行一个带有参数的函数。定义一个名为add_args()的函数,打印其两个数值参数arg1arg2的和:

>>> def add_args(arg1, arg2):
...     print(arg1 + arg2)

add_args()是什么?

>>> type(add_args)
<class 'function'>

此时,让我们定义一个名为run_something_with_args()的函数,它接受三个参数:

func

要运行的函数

arg1

func的第一个参数

arg2

func的第二个参数

>>> def run_something_with_args(func, arg1, arg2):
...     func(arg1, arg2)

当您调用run_something_with_args()时,调用者传递的函数被赋给func参数,而arg1arg2得到了在参数列表中跟随的值。然后,运行func(arg1, arg2)使用这些参数执行该函数,因为括号告诉 Python 这样做。

让我们通过向函数名add_args和参数59传递给run_something_with_args()来测试它:

>>> run_something_with_args(add_args, 5, 9)
14

在函数run_something_with_args()中,函数名参数add_args被赋给了参数func5被赋给了参数arg19被赋给了参数arg2。这最终执行了:

add_args(5, 9)

您可以将此与*args**kwargs技术结合使用。

让我们定义一个测试函数,它接受任意数量的位置参数,通过使用sum()函数计算它们的总和,然后返回该总和:

>>> def sum_args(*args):
...    return sum(args)

我之前没有提到sum()。它是一个内置的 Python 函数,用于计算其可迭代数值(int 或 float)参数中值的总和。

让我们定义新函数run_with_positional_args(),它接受一个函数和任意数量的位置参数以传递给它:

>>> def run_with_positional_args(func, *args):
...    return func(*args)

现在继续调用它:

>>> run_with_positional_args(sum_args, 1, 2, 3, 4)
10

您可以将函数用作列表、元组、集合和字典的元素。函数是不可变的,因此您也可以将它们用作字典键。

内部函数

你可以在一个函数内定义另一个函数:

>>> def outer(a, b):
...     def inner(c, d):
...         return c + d
...     return inner(a, b)
...
>>>
>>> outer(4, 7)
11

当在另一个函数内部执行某个复杂任务超过一次时,内部函数可以很有用,以避免循环或代码重复。例如,对于字符串示例,此内部函数向其参数添加一些文本:

>>> def knights(saying):
...     def inner(quote):
...         return "We are the knights who say: '%s'" % quote
...     return inner(saying)
...
>>> knights('Ni!')
"We are the knights who say: 'Ni!'"

闭包

内部函数可以充当闭包。这是由另一个函数动态生成的函数,可以改变并记住在函数外部创建的变量的值。

以下示例是基于先前的knights()示例构建的。让我们称其为knights2(),因为我们没有想象力,并将inner()函数转变为称为inner2()的闭包。以下是它们之间的区别:

  • inner2()直接使用外部的saying参数,而不是作为参数获取它。

  • knights2()返回了inner2函数名而不是调用它:

    >>> def knights2(saying):
    ...     def inner2():
    ...         return "We are the knights who say: '%s'" % saying
    ...     return inner2
    ...
    

inner2()函数知道传入的saying值,并记住它。return inner2这一行返回了这个专门的inner2函数副本(但没有调用它)。这是一种闭包:一个动态创建的函数,记住了它来自何处。

让我们两次调用knights2(),使用不同的参数:

>>> a = knights2('Duck')
>>> b = knights2('Hasenpfeffer')

好的,那么ab是什么?

>>> type(a)
<class 'function'>
>>> type(b)
<class 'function'>

它们是函数,但它们也是闭包:

>>> a
<function knights2.<locals>.inner2 at 0x10193e158>
>>> b
<function knights2.<locals>.inner2 at 0x10193e1e0>

如果我们调用它们,它们会记住由knights2创建时使用的saying

>>> a()
"We are the knights who say: 'Duck'"
>>> b()
"We are the knights who say: 'Hasenpfeffer'"

匿名函数:lambda

Python lambda 函数 是作为单个语句表示的匿名函数。您可以使用它来代替普通的小函数。

为了说明这一点,让我们首先做一个使用普通函数的示例。首先,让我们定义函数 edit_story()。它的参数如下:

  • words—一个单词列表

  • func—应用于 words 中每个单词的函数

>>> def edit_story(words, func):
...     for word in words:
...         print(func(word))

现在我们需要一个单词列表和一个应用到每个单词的函数。对于单词,这里是我家猫(假设的情况下)如果(假设的情况下)错过了其中一个楼梯可能发出的一系列(假设的)声音:

>>> stairs = ['thud', 'meow', 'thud', 'hiss']

而对于函数,这将使每个单词大写并追加一个感叹号,非常适合猫类小报的头条新闻:

>>> def enliven(word):   # give that prose more punch
...     return word.capitalize() + '!'

混合我们的成分:

>>> edit_story(stairs, enliven)
Thud!
Meow!
Thud!
Hiss!

最后,我们来到 lambda。enliven() 函数如此简短,以至于我们可以用 lambda 替换它:

>>> edit_story(stairs, lambda word: word.capitalize() + '!')
Thud!
Meow!
Thud!
Hiss!

Lambda 函数有零个或多个逗号分隔的参数,后跟一个冒号(:),然后是函数的定义。我们给这个 lambda 一个参数 word。你不像调用 def 创建的函数那样在 lambda 函数中使用括号。

通常,使用真正的函数如 enliven() 要比使用 lambda 函数更清晰。Lambda 函数主要用于在否则需要定义许多小函数并记住它们名称的情况下非常有用。特别是在图形用户界面中,你可以用 lambda 来定义 回调函数;详见第二十章的示例。

生成器

一个 生成器 是 Python 的序列创建对象。利用它,你可以在不一次性创建和存储整个序列于内存中的情况下遍历可能非常庞大的序列。生成器通常是迭代器的数据源。如果你还记得,在之前的代码示例中我们已经使用了其中一个,range(),来生成一系列整数。在 Python 2 中,range() 返回一个列表,这限制了它的内存使用。Python 2 还有生成器 xrange(),在 Python 3 中变成了普通的 range()。这个示例将所有的整数从 1 加到 100:

>>> sum(range(1, 101))
5050

每次你遍历一个生成器时,它都会记住上次被调用时的位置,并返回下一个值。这与普通函数不同,普通函数没有记忆先前调用的状态,每次都从第一行开始执行。

生成器函数

如果你想创建一个可能很大的序列,可以编写一个 生成器函数。它是一个普通函数,但它通过 yield 语句而不是 return 返回它的值。让我们来写我们自己的 range() 版本:

>>> def my_range(first=0, last=10, step=1):
...     number = first
...     while number < last:
...         yield number
...         number += step
...

这是一个普通函数:

>>> my_range
<function my_range at 0x10193e268>

并返回一个生成器对象:

>>> ranger = my_range(1, 5)
>>> ranger
<generator object my_range at 0x101a0a168>

我们可以遍历这个生成器对象:

>>> for x in ranger:
...     print(x)
...
1
2
3
4
注意

生成器只能运行一次。列表、集合、字符串和字典存在于内存中,但生成器会即时创建其值,并逐个通过迭代器分发它们。它不记住它们,因此你无法重新启动或备份生成器。

如果你尝试再次迭代这个生成器,你会发现它已经耗尽了:

>>> for try_again in ranger:
...     print(try_again)
...
>>>

生成器推导式

您已经看到了用于列表、字典和集合的理解。生成器理解 看起来像那些,但是用圆括号而不是方括号或花括号括起来。这就像是生成器函数的简写版本,隐式地执行 yield 并返回生成器对象:

>>> genobj = (pair for pair in zip(['a', 'b'], ['1', '2']))
>>> genobj
<generator object <genexpr> at 0x10308fde0>
>>> for thing in genobj:
...     print(thing)
...
('a', '1')
('b', '2')

装饰器

有时,您希望修改现有函数而不更改其源代码。一个常见的例子是添加调试语句以查看传递的参数是什么。

装饰器 是一个接受一个函数作为输入并返回另一个函数的函数。让我们深入探讨一下我们的 Python 技巧,并使用以下内容:

  • *args**kwargs

  • 内部函数

  • 函数作为参数

函数 document_it() 定义了一个装饰器,该装饰器将执行以下操作:

  • 打印函数的名称和其参数的值

  • 使用参数运行函数

  • 打印结果

  • 返回修改后的函数以供使用

以下是代码的样子:

>>> def document_it(func):
...     def new_function(*args, **kwargs):
...         print('Running function:', func.__name__)
...         print('Positional arguments:', args)
...         print('Keyword arguments:', kwargs)
...         result = func(*args, **kwargs)
...         print('Result:', result)
...         return result
...     return new_function

无论您传递给 document_it()func 是什么,您都会得到一个包含 document_it() 添加的额外语句的新函数。装饰器实际上不必从 func 运行任何代码,但 document_it() 在执行过程中调用 func,以便您既可以获得 func 的结果,又可以获得所有额外的内容。

那么,您如何使用它呢?您可以手动应用装饰器:

>>> def add_ints(a, b):
...    return a + b
...
>>> add_ints(3, 5)
8
>>> cooler_add_ints = document_it(add_ints)  # manual decorator assignment
>>> cooler_add_ints(3, 5)
Running function: add_ints
Positional arguments: (3, 5)
Keyword arguments: {}
Result: 8
8

作为手动装饰器分配的替代方案,您可以在您想要装饰的函数之前添加 @decorator_name* :

>>> @document_it
... def add_ints(a, b):
...     return a + b
...
>>> add_ints(3, 5)
Start function add_ints
Positional arguments: (3, 5)
Keyword arguments: {}
Result: 8
8

一个函数可以有多个装饰器。让我们再写一个名为 square_it() 的装饰器,用于对结果进行平方:

>>> def square_it(func):
...     def new_function(*args, **kwargs):
...         result = func(*args, **kwargs)
...         return result * result
...     return new_function
...

最接近函数的装饰器(就在 def 的上方)首先运行,然后是它上面的装饰器。无论顺序如何,最终结果都相同,但您可以看到中间步骤如何改变:

>>> @document_it
... @square_it
... def add_ints(a, b):
...     return a + b
...
>>> add_ints(3, 5)
Running function: new_function
Positional arguments: (3, 5)
Keyword arguments: {}
Result: 64
64

让我们试试颠倒装饰器的顺序:

>>> @square_it
... @document_it
... def add_ints(a, b):
...     return a + b
...
>>> add_ints(3, 5)
Running function: add_ints
Positional arguments: (3, 5)
Keyword arguments: {}
Result: 8
64

命名空间和作用域

渴望这个人的艺术和那个人的视野

威廉·莎士比亚

一个名称可以根据其使用的位置引用不同的事物。Python 程序有各种命名空间 —— 某个名称在其中是唯一的,并且与其他命名空间中相同名称无关。

每个函数定义其自己的命名空间。如果您在主程序中定义了一个名为 x 的变量,并在函数中定义了另一个名为 x 的变量,它们将指代不同的事物。但是,墙壁是可以打破的:如果需要,可以通过各种方式访问其他命名空间中的名称。

程序的主要部分定义了全局命名空间;因此,在该命名空间中的变量是全局变量

您可以从函数内部获取全局变量的值:

>>> animal = 'fruitbat'
>>> def print_global():
...     print('inside print_global:', animal)
...
>>> print('at the top level:', animal)
at the top level: fruitbat
>>> print_global()
inside print_global: fruitbat

但是,如果您尝试在函数内获取全局变量的值并且更改它,您将会得到一个错误:

>>> def change_and_print_global():
...     print('inside change_and_print_global:', animal)
...     animal = 'wombat'
...     print('after the change:', animal)
...
>>> change_and_print_global()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in change_and_print_global
UnboundLocalError: local variable 'animal' referenced before assignment

如果您只是更改它,则还会更改另一个名为 animal 的变量,但此变量位于函数内部:

>>> def change_local():
...     animal = 'wombat'
...     print('inside change_local:', animal, id(animal))
...
>>> change_local()
inside change_local: wombat 4330406160
>>> animal
'fruitbat'
>>> id(animal)
4330390832

发生了什么?第一行将字符串 'fruitbat' 分配给名为 animal 的全局变量。change_local() 函数中也有一个名为 animal 的变量,但它在其局部命名空间中。

我在这里使用了 Python 函数 id() 来打印每个对象的唯一值,并证明 change_local() 内部的变量 animal 不同于程序主层级的 animal

在函数内部访问全局变量而不是局部变量,你需要显式使用 global 关键字(你知道这是必须的:显式优于隐式):

>>> animal = 'fruitbat'
>>> def change_and_print_global():
...     global animal
...     animal = 'wombat'
...     print('inside change_and_print_global:', animal)
...
>>> animal
'fruitbat'
>>> change_and_print_global()
inside change_and_print_global: wombat
>>> animal
'wombat'

如果在函数内部不使用 global,Python 使用局部命名空间,变量是局部的。函数执行完成后,它就消失了。

Python 提供了两个函数来访问你的命名空间的内容:

  • locals() 返回本地命名空间内容的字典。

  • globals() 返回全局命名空间内容的字典。

并且它们在使用中:

>>> animal = 'fruitbat'
>>> def change_local():
...     animal = 'wombat'  # local variable
...     print('locals:', locals())
...
>>> animal
'fruitbat'
>>> change_local()
locals: {'animal': 'wombat'}
>>> print('globals:', globals()) # reformatted a little for presentation
globals: {'animal': 'fruitbat',
'__doc__': None,
'change_local': <function change_local at 0x1006c0170>,
'__package__': None,
'__name__': '__main__',
'__loader__': <class '_frozen_importlib.BuiltinImporter'>,
'__builtins__': <module 'builtins'>}
>>> animal
'fruitbat'

change_local() 内的局部命名空间仅包含局部变量 animal。全局命名空间包含独立的全局变量 animal 和许多其他内容。

在名称中使用 _ 和 __

以两个下划线 (__) 开头和结尾的名称保留供 Python 使用,因此你不应该将它们用于自己的变量。选择这种命名模式是因为看起来不太可能被应用开发人员选为其自己的变量名。

例如,函数的名称在系统变量 function.__name__ 中,其文档字符串在 function.__doc__ 中:

>>> def amazing():
...     '''This is the amazing function.
...     Want to see it again?'''
...     print('This function is named:', amazing.__name__)
...     print('And its docstring is:', amazing.__doc__)
...
>>> amazing()
This function is named: amazing
And its docstring is: This is the amazing function.
 Want to see it again?

正如你在前面的 globals 打印中看到的那样,主程序被赋予特殊名称 __main__

递归

到目前为止,我们已经调用了一些直接执行某些操作的函数,并可能调用其他函数。但如果一个函数调用自身呢?⁵ 这就是递归。就像使用 whilefor 的无限循环一样,你不希望出现无限递归。我们仍然需要担心时空连续性的裂缝吗?

Python 再次拯救了宇宙,如果你深入太多,它会引发异常:

>>> def dive():
...     return dive()
...
>>> dive()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in dive
  File "<stdin>", line 2, in dive
  File "<stdin>", line 2, in dive
 [Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded

当处理像列表的列表的列表这样的“不均匀”数据时,递归非常有用。假设你想要“展平”列表的所有子列表,⁶ 无论嵌套多深,生成器函数正是你需要的:

>>> def flatten(lol):
...     for item in lol:
...         if isinstance(item, list):
...             for subitem in flatten(item):
...                 yield subitem
...         else:
...             yield item
...
>>> lol = [1, 2, [3,4,5], [6,[7,8,9], []]]
>>> flatten(lol)
<generator object flatten at 0x10509a750>
>>> list(flatten(lol))
[1, 2, 3, 4, 5, 6, 7, 8, 9]

Python 3.3 添加了 yield from 表达式,允许生成器将一些工作交给另一个生成器。我们可以用它来简化 flatten()

>>> def flatten(lol):
...     for item in lol:
...         if isinstance(item, list):
...             yield from flatten(item)
...         else:
...             yield item
...
>>> lol = [1, 2, [3,4,5], [6,[7,8,9], []]]
>>> list(flatten(lol))
[1, 2, 3, 4, 5, 6, 7, 8, 9]

异步函数

关键字 asyncawait 被添加到 Python 3.5 中,用于定义和运行异步函数。它们是:

  • 相对较新

  • 足够不同,以至于更难理解

  • 随着时间的推移,它会变得更重要且更为人熟知

出于这些原因,我已将这些及其他异步主题的讨论移到了附录 C 中的 Appendix C。

现在,你需要知道,如果在函数的def行之前看到async,那么这是一个异步函数。同样,如果在函数调用之前看到await,那么该函数是异步的。

异步函数和普通函数的主要区别在于异步函数可以“放弃控制”,而不是一直运行到完成。

异常

在某些语言中,错误通过特殊的函数返回值来指示。当事情出错时,Python 使用异常:当关联的错误发生时执行的代码。

你已经看到了一些例子,比如访问列表或元组时使用超出范围的位置,或者使用不存在的键访问字典。当你运行在某些情况下可能失败的代码时,还需要适当的异常处理程序来拦截任何潜在的错误。

在任何可能发生异常的地方添加异常处理是一个好习惯,以便让用户知道发生了什么。你可能无法修复问题,但至少可以记录情况并优雅地关闭程序。如果异常发生在某个函数中并且没有在那里捕获,它会冒泡直到在某个调用函数中找到匹配的处理程序。如果不提供自己的异常处理程序,Python 会打印错误消息和关于错误发生位置的一些信息,然后终止程序,如以下代码片段所示:

>>> short_list = [1, 2, 3]
>>> position = 5
>>> short_list[position]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

使用 try 和 except 处理错误

做,或者不做。没有尝试。

尤达

不要留事情交给机会,使用try包装你的代码,并使用except提供错误处理:

>>> short_list = [1, 2, 3]
>>> position = 5
>>> try:
...     short_list[position]
... except:
...     print('Need a position between 0 and', len(short_list)-1, ' but got',
...            position)
...
Need a position between 0 and 2 but got 5

执行try块内的代码。如果出现错误,将引发异常并执行except块内的代码。如果没有错误,则跳过except块。

在这里使用没有参数的纯except来指定是一个通用的异常捕获。如果可能会出现多种类型的异常,最好为每种类型提供单独的异常处理程序。没有人强迫你这样做;你可以使用裸露的except来捕获所有异常,但你对它们的处理可能是通用的(类似于打印发生了一些错误)。你可以使用任意数量的特定异常处理程序。

有时候,你希望除了类型以外的异常详情。如果使用以下形式,你会在变量name中得到完整的异常对象:

except *`exceptiontype`* as *`name`*

以下示例首先查找IndexError,因为当你向序列提供非法位置时,会引发该异常类型。它将IndexError异常保存在变量err中,将其他任何异常保存在变量other中。示例打印other中存储的所有内容,以展示你在该对象中得到的内容:

>>> short_list = [1, 2, 3]
>>> while True:
...     value = input('Position [q to quit]? ')
...     if value == 'q':
...         break
...     try:
...         position = int(value)
...         print(short_list[position])
...     except IndexError as err:
...         print('Bad index:', position)
...     except Exception as other:
...         print('Something else broke:', other)
...
Position [q to quit]? 1
2
Position [q to quit]? 0
1
Position [q to quit]? 2
3
Position [q to quit]? 3
Bad index: 3
Position [q to quit]? 2
3
Position [q to quit]? two
Something else broke: invalid literal for int() with base 10: 'two'
Position [q to quit]? q

输入位置3引发了预期的IndexError。输入two使int()函数感到恼火,我们在第二个通用的except代码中处理了它。

创建自定义异常

上一节讨论了处理异常,但所有的异常(如IndexError)都是在 Python 或其标准库中预定义的。你可以为自己的程序使用其中任何一个。你也可以定义自己的异常类型来处理可能在你自己的程序中出现的特殊情况。

注意

这需要定义一个新的对象类型,使用一个——这是我们直到第十章才会详细讨论的内容。所以,如果你对类不熟悉,可能需要稍后再回到本节。

异常是一个类。它是类Exception的子类。让我们创建一个叫做UppercaseException的异常,在字符串中遇到大写字母时引发它:

>>> class UppercaseException(Exception):
...     pass
...
>>> words = ['eenie', 'meenie', 'miny', 'MO']
>>> for word in words:
...     if word.isupper():
...         raise UppercaseException(word)
...
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
__main__.UppercaseException: MO

我们甚至没有为UppercaseException定义任何行为(注意我们只是使用了pass),让它的父类Exception来决定在引发异常时打印什么。

你可以访问异常对象本身并打印它:

>>> try:
...     raise OopsException('panic')
... except OopsException as exc:
...     print(exc)
...
panic

即将出现

对象!在一本关于面向对象语言的书中,我们必须介绍它们。

要做的事情

9.1 定义一个名为good()的函数,返回以下列表:['Harry', 'Ron', 'Hermione']

9.2 定义一个名为get_odds()的生成器函数,返回range(10)中的奇数。使用for循环找到并打印第三个返回的值。

9.3 定义一个名为test的装饰器,在调用函数时打印'start',在函数结束时打印'end'

9.4 定义一个名为OopsException的异常。引发这个异常看看会发生什么。然后,编写代码捕捉这个异常并打印'Caught an oops'

¹ 虽然ArgsKwargs听起来像海盗鹦鹉的名字。

² 或者,如 Python 3.5 中的字典合并形式{**a, **b},就像你在第八章看到的那样。

³ 就像那些青少年陷入危险的电影中他们学会了“电话是从房子里打来的!”

⁴ 就像那个老医生笑话:“当我这样做时很痛。” “那么,就别这样做。”

⁵ 这就像说,“如果我每次希望有一美元,我就能有一美元。”

⁶ 又是一个 Python 面试问题。收集整套吧!

⁷ 这是北半球主义吗?澳大利亚人和新西兰人说东西乱了会说“north”吗?

第十章:哦哦:对象和类

没有神秘的对象。神秘的是你的眼睛。

伊丽莎白·鲍文

拿一个对象。对它做点什么。再对它做点别的事。

贾斯珀·约翰斯

正如我在各个页面中提到的,Python 中的所有东西,从数字到函数,都是对象。但是,Python 通过特殊语法隐藏了大部分对象机制。你可以输入num = 7来创建一个类型为整数、值为 7 的对象,并将对象引用分配给名称num。只有当你想要创建自己的对象或修改现有对象的行为时,你才需要查看对象的内部。在本章中,你将看到如何执行这两个操作。

什么是对象?

一个对象是一个自定义的数据结构,包含数据(变量,称为属性)和代码(函数,称为方法)。它代表某个具体事物的唯一实例。把对象看作名词,它的方法是动词。一个对象代表一个个体,它的方法定义了它与其他事物的交互方式。

例如,值为7的整数对象是一个对象,可以执行加法和乘法等方法,就像你在第三章中看到的那样。8是另一个对象。这意味着 Python 中某处内置了一个整数类,78都属于这个类。字符串'cat''duck'也是 Python 中的对象,具有你在第五章中看到的字符串方法,如capitalize()replace()

与模块不同,你可以同时拥有多个对象(通常称为实例),每个对象可能具有不同的属性。它们就像是超级数据结构,内含代码。

简单的对象

让我们从基本对象类开始;我们将在几页后讨论继承。

使用 class 定义一个类

要创建一个以前从未创建过的新对象,首先需要定义一个,指示它包含什么内容。

在第二章中,我将对象比作一个塑料盒。一个就像是制造那个盒子的模具。例如,Python 有一个内置类,用来创建字符串对象如'cat''duck',以及其他标准数据类型——列表、字典等等。要在 Python 中创建自定义对象,首先需要使用class关键字定义一个类。让我们通过一些简单的示例来详细了解。

假设你想定义对象来表示关于猫的信息。¹ 每个对象将代表一只猫。你首先需要定义一个名为Cat的类作为模板。在接下来的示例中,我们将尝试多个版本的这个类,从最简单的类逐步构建到真正有用的类。

注意

我们遵循 Python 的命名约定PEP-8

我们的第一个尝试是最简单的类,一个空类:

>>> class Cat():
...     pass

你也可以说:

>>> class Cat:
...     pass

就像函数一样,我们需要使用pass来指示这个类是空的。这个定义是创建对象的最低要求。

通过像调用函数一样调用类名,你可以从类创建一个对象:

>>> a_cat = Cat()
>>> another_cat = Cat()

在这种情况下,调用Cat()创建了两个来自Cat类的单独对象,并将它们分配给了名称a_catanother_cat。但是我们的Cat类没有其他代码,所以我们从它创建的对象只是存在,不能做太多其他事情。

嗯,它们可以做一点点。

属性

属性是类或对象内部的变量。在创建对象或类期间以及之后,你可以给它赋予属性。属性可以是任何其他对象。让我们再次创建两个猫对象:

>>> class Cat:
...     pass
...
>>> a_cat = Cat()
>>> a_cat
<__main__.Cat object at 0x100cd1da0>
>>> another_cat = Cat()
>>> another_cat
<__main__.Cat object at 0x100cd1e48>

当我们定义Cat类时,并没有指定如何打印来自该类的对象。Python 则会打印类似 <__main__.Cat object at 0x100cd1da0> 的东西。在“魔术方法”中,你会看到如何改变这个默认行为。

现在给我们的第一个对象分配一些属性:

>>> a_cat.age = 3
>>> a_cat.name = "Mr. Fuzzybuttons"
>>> a_cat.nemesis = another_cat

我们能访问它们吗?我们当然希望如此:

>>> a_cat.age
3
>>> a_cat.name
'Mr. Fuzzybuttons'
>>> a_cat.nemesis
<__main__.Cat object at 0x100cd1e48>

因为nemesis是指向另一个Cat对象的属性,我们可以使用a_cat.nemesis来访问它,但是这个其他对象还没有name属性:

>>> a_cat.nemesis.name
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Cat' object has no attribute 'name'

让我们给我们的大猫起个名字:

>>> a_cat.nemesis.name = "Mr. Bigglesworth"
>>> a_cat.nemesis.name
'Mr. Bigglesworth'

即使是像这样的最简单的对象也可以用来存储多个属性。因此,你可以使用多个对象来存储不同的值,而不是使用类似列表或字典的东西。

当你听到attributes(属性)时,通常指的是对象的属性。还有class attributes(类属性),稍后你会在“类和对象属性”中看到它们的区别。

方法

方法是类或对象中的函数。方法看起来像任何其他函数,但可以用特殊的方式使用,你将在“属性访问的属性”和“方法类型”中看到。

初始化

如果你想在创建时分配对象属性,你需要使用特殊的 Python 对象初始化方法__init__()

>>> class Cat:
...     def __init__(self):
...         pass

这就是你在真实的 Python 类定义中会看到的内容。我承认__init__()self看起来很奇怪。__init__()是 Python 中一个特殊的方法名,用于初始化一个类定义中的单个对象。² self参数指定它是指向个体对象本身。

当你在类定义中定义__init__()时,它的第一个参数应该命名为self。虽然self在 Python 中不是一个保留字,但它是常见的用法。如果你使用self,日后阅读你的代码的人(包括你自己!)不会猜测你的意图。

但是即使是这第二个Cat类定义也没有创建一个真正做任何事情的对象。第三次尝试是真正展示如何在 Python 中创建简单对象并分配其属性的方法。这次,我们将name参数添加到初始化方法中:

>>> class Cat():
...     def __init__(self, name):
...         self.name = name
...
>>>

现在,我们可以通过为name参数传递一个字符串来从Cat类创建一个对象:

>>> furball = Cat('Grumpy')

下面是这行代码的作用:

  • 查找Cat类的定义

  • 实例化(创建)一个新的内存对象

  • 调用对象的__init__()方法,将这个新创建的对象作为self传递,并将另一个参数('Grumpy')作为name传递

  • name的值存储在对象中

  • 返回新对象

  • 将变量furball附加到对象上

这个新对象像 Python 中的任何其他对象一样。你可以将它用作列表、元组、字典或集合的元素,可以将它作为参数传递给函数,或将它作为结果返回。

那么我们传入的name值呢?它以属性的形式保存在对象中。你可以直接读取和写入它:

>>> print('Our latest addition: ', furball.name)
Our latest addition: Grumpy

记住,在Cat类定义的内部,你通过self.name访问name属性。当你创建一个实际对象并将其赋给像furball这样的变量时,你可以使用furball.name来引用它。

并非每个类定义都必须有一个__init__()方法;它用于执行任何需要区分此对象与同类其他对象的操作。它并不是某些其他语言所称的“构造函数”。Python 已经为你构造好了对象。将__init__()视为初始化方法

注意

你可以从一个类创建许多个体对象。但要记住,Python 将数据实现为对象,因此类本身也是一个对象。但是,在你的程序中只有一个类对象。如果像我们这里定义了class Cat,它就像《猎魔人》一样——只能有一个。

继承

当你试图解决某个编程问题时,通常会发现一个现有的类可以创建几乎符合你需求的对象。你能做什么?

你可以修改这个旧类,但会使它变得更加复杂,并且可能会破坏一些曾经工作的功能。

或者你可以编写一个新类,从旧类中剪切和粘贴代码并合并新代码。但这意味着你需要维护更多的代码,并且原来和新类中曾经相同的部分可能会因为它们现在位于不同的位置而有所不同。

一个解决方案是继承:从现有类创建一个新类,并进行一些添加或更改。这是代码重用的一个很好的方式。使用继承时,新类可以自动使用旧类的所有代码,而不需要你复制任何代码。

继承自父类

你只需定义新类中需要添加或更改的内容,这样就可以覆盖旧类的行为。原始类称为父类超类基类;新类称为子类子类派生类。这些术语在面向对象编程中是可以互换使用的。

所以,让我们继承一些东西。在下一个例子中,我们定义一个空类称为 Car。接下来,我们定义 Car 的一个子类称为 Yugo。³ 您可以使用相同的 class 关键字,但在括号中使用父类名称(这里是 class Yugo(Car))来定义子类:

>>> class Car():
...     pass
...
>>> class Yugo(Car):
...     pass
...

您可以使用 issubclass() 来检查一个类是否派生自另一个类:

>>> issubclass(Yugo, Car)
True

接下来,从每个类创建一个对象:

>>> give_me_a_car = Car()
>>> give_me_a_yugo = Yugo()

子类是父类的一种特殊化;在面向对象的术语中,Yugo 是一个 Car。名为 give_me_a_yugo 的对象是 Yugo 类的实例,但它也继承了 Car 的所有功能。在这种情况下,CarYugo 就像潜水艇上的水手一样有用,因此让我们尝试实际做点事的新类定义:

>>> class Car():
...     def exclaim(self):
...         print("I'm a Car!")
...
>>> class Yugo(Car):
...     pass
...

最后,分别从每个类创建一个对象并调用 exclaim 方法:

>>> give_me_a_car = Car()
>>> give_me_a_yugo = Yugo()
>>> give_me_a_car.exclaim()
I'm a Car!
>>> give_me_a_yugo.exclaim()
I'm a Car!

不做任何特殊处理,Yugo 继承了 Carexclaim() 方法。事实上,Yugo 表示它一辆 Car,这可能导致身份危机。让我们看看我们能做些什么。

注意

继承很吸引人,但可能被滥用。多年的面向对象编程经验表明,过多使用继承会使程序难以管理。相反,通常建议强调其他技术,如聚合和组合。我们在本章中介绍这些替代方法。

覆盖一个方法

正如你刚才看到的,一个新类最初会从其父类继承所有东西。接下来,您将看到如何替换或覆盖父类方法。Yugo 可能在某种方式上应该与 Car 不同;否则,定义一个新类有什么意义?让我们改变 exclaim() 方法在 Yugo 中的工作方式:

>>> class Car():
...     def exclaim(self):
...         print("I'm a Car!")
...
>>> class Yugo(Car):
...     def exclaim(self):
...         print("I'm a Yugo! Much like a Car, but more Yugo-ish.")
...

现在从这些类中创建两个对象:

>>> give_me_a_car = Car()
>>> give_me_a_yugo = Yugo()

他们说什么?

>>> give_me_a_car.exclaim()
I'm a Car!
>>> give_me_a_yugo.exclaim()
I'm a Yugo! Much like a Car, but more Yugo-ish.

在这些示例中,我们覆盖了 exclaim() 方法。我们可以覆盖任何方法,包括 __init__()。这里有另一个使用 Person 类的例子。让我们创建代表医生(MDPerson)和律师(JDPerson)的子类:

>>> class Person():
...     def __init__(self, name):
...         self.name = name
...
>>> class MDPerson(Person):
...     def __init__(self, name):
...         self.name = "Doctor " + name
...
>>> class JDPerson(Person):
...     def __init__(self, name):
...         self.name = name + ", Esquire"
...

在这些情况下,初始化方法 __init__() 接受与父类 Person 相同的参数,但在对象实例内部以不同的方式存储 name 的值:

>>> person = Person('Fudd')
>>> doctor = MDPerson('Fudd')
>>> lawyer = JDPerson('Fudd')
>>> print(person.name)
Fudd
>>> print(doctor.name)
Doctor Fudd
>>> print(lawyer.name)
Fudd, Esquire

添加一个方法

子类还可以添加在其父类中不存在的方法。回到 CarYugo 类,我们将为仅 Yugo 类定义新方法 need_a_push()

>>> class Car():
...     def exclaim(self):
...         print("I'm a Car!")
...
>>> class Yugo(Car):
...     def exclaim(self):
...         print("I'm a Yugo! Much like a Car, but more Yugo-ish.")
...     def need_a_push(self):
...         print("A little help here?")
...

接下来,创建一个 Car 和一个 Yugo

>>> give_me_a_car = Car()
>>> give_me_a_yugo = Yugo()

Yugo 对象可以对 need_a_push() 方法调用做出反应:

>>> give_me_a_yugo.need_a_push()
A little help here?

但是一个普通的 Car 对象不能:

>>> give_me_a_car.need_a_push()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Car' object has no attribute 'need_a_push'

到这一点,一个 Yugo 能做一些 Car 不能的事情,Yugo 的独特个性可以显现出来。

通过 super() 从父类获得帮助

我们看到子类如何添加或覆盖父类的方法。如果它想调用那个父类方法呢?“我很高兴你问”,super() 这里,我们定义了一个名为 EmailPerson 的新类,代表一个带有电子邮件地址的 Person。首先是我们熟悉的 Person 定义:

>>> class Person():
...     def __init__(self, name):
...         self.name = name
...

请注意,以下子类中的__init__()调用具有额外的email参数:

>>> class EmailPerson(Person):
...     def __init__(self, name, email):
...         super().__init__(name)
...         self.email = email

当为类定义__init__()方法时,您正在替换其父类的__init__()方法,后者不再自动调用。因此,我们需要显式调用它。以下是发生的情况:

  • super()获取父类Person的定义。

  • __init__()方法调用了Person.__init__()方法。它负责将self参数传递给超类,因此您只需提供任何可选参数。在我们的情况下,Person()接受的唯一其他参数是name

  • self.email = email行是使这个EmailPersonPerson不同的新代码。

继续,让我们制作其中的一个生物:

>>> bob = EmailPerson('Bob Frapples', 'bob@frapples.com')

我们应该能够访问nameemail属性:

>>> bob.name
'Bob Frapples'
>>> bob.email
'bob@frapples.com'

为什么我们不直接定义我们的新类如下?

>>> class EmailPerson(Person):
...     def __init__(self, name, email):
...         self.name = name
...         self.email = email

我们本可以这样做,但那样会破坏我们对继承的使用。我们使用super()使Person执行其工作,就像一个普通的Person对象一样。还有另一个好处:如果将来Person的定义发生变化,使用super()将确保EmailPersonPerson继承的属性和方法将反映这些变化。

当子类以自己的方式执行某些操作但仍需要来自父类的东西(就像现实生活中一样)时,请使用super()

多重继承

你刚刚看到一些没有父类的经典示例,还有一些有一个父类的示例。实际上,对象可以从多个父类继承。

如果您的类引用其没有的方法或属性,Python 将在所有父类中查找。如果其中有多个类具有相同名称的东西,谁会胜出?

与人类的遗传不同,在那里无论来自谁的优势基因都会获胜,Python 中的继承取决于方法解析顺序。每个 Python 类都有一个特殊的方法称为mro(),返回一个访问该类对象的方法或属性时要访问的类的列表。类似的属性称为__mro__,是这些类的元组。就像突然死亡的季后赛一样,第一个赢家胜出。

在这里,我们定义了一个顶级Animal类,两个子类(HorseDonkey),然后从这些类派生出两个:⁴

>>> class Animal:
...     def says(self):
 return 'I speak!'
...
>>> class Horse(Animal):
...     def says(self):
...         return 'Neigh!'
...
>>> class Donkey(Animal):
...     def says(self):
...         return 'Hee-haw!'
...
>>> class Mule(Donkey, Horse):
...     pass
...
>>> class Hinny(Horse, Donkey):
...     pass
...

如果我们查找Mule的方法或属性,Python 将按照以下顺序查找:

  1. 对象本身(类型为Mule

  2. 对象的类(Mule

  3. 类的第一个父类(Donkey

  4. 类的第二个父类(Horse

  5. 祖父类(Animal)类

对于Hinny来说,情况大致相同,但是HorseDonkey之前:

>>> Mule.mro()
[<class '__main__.Mule'>, <class '__main__.Donkey'>,
<class '__main__.Horse'>, <class '__main__.Animal'>,
<class 'object'>]
>>> Hinny.mro()
[<class '__main__.Hinny'>, <class '__main__.Horse'>,
<class '__main__.Donkey'>, <class '__main__.Animal'>,
class 'object'>]

那么这些优雅的动物怎么说呢?

>>> mule = Mule()
>>> hinny = Hinny()
>>> mule.says()
'hee-haw'
>>> hinny.says()
'neigh'

我们按(父亲,母亲)顺序列出了父类,所以它们说话像他们的爸爸一样。

如果HorseDonkey没有says()方法,那么骡或骡马将使用祖父类Animal类的says()方法,并返回'I speak!'

Mixins

您可以在类定义中包含一个额外的父类,但仅作为助手使用。也就是说,它不与其他父类共享任何方法,并避免了我在上一节中提到的方法解析歧义。

这样的父类有时被称为mixin类。用途可能包括像日志记录这样的“副”任务。这里是一个漂亮打印对象属性的 mixin:

>>> class PrettyMixin():
...     def dump(self):
...         import pprint
...         pprint.pprint(vars(self))
...
>>> class Thing(PrettyMixin):
...     pass
...
>>> t = Thing()
>>> t.name = "Nyarlathotep"
>>> t.feature = "ichor"
>>> t.age = "eldritch"
>>> t.dump()
{'age': 'eldritch', 'feature': 'ichor', 'name': 'Nyarlathotep'}

在自卫中

除了使用空格之外,Python 的一个批评是需要将self作为实例方法的第一个参数(您在前面的示例中看到的方法类型)。Python 使用self参数来查找正确的对象属性和方法。例如,我将展示如何调用对象的方法,以及 Python 在幕后实际做了什么。

还记得之前例子中的Car类吗?让我们再次调用它的exclaim()方法:

>>> a_car = Car()
>>> a_car.exclaim()
I'm a Car!

下面是 Python 在幕后实际做的事情:

  • 查找对象a_car的类(Car)。

  • 将对象a_car作为self参数传递给Car类的exclaim()方法。

只是为了好玩,您甚至可以以这种方式自行运行它,它将与正常的(a_car.exclaim())语法相同运行:

>>> Car.exclaim(a_car)
I'm a Car!

然而,永远没有理由使用那种更冗长的风格。

属性访问

在 Python 中,对象的属性和方法通常是公开的,您被期望自律行事(这有时被称为“成年人同意”政策)。让我们比较直接方法与一些替代方法。

直接访问

正如您所见,您可以直接获取和设置属性值:

>>> class Duck:
...     def __init__(self, input_name):
...         self.name = input_name
...
>>> fowl = Duck('Daffy')
>>> fowl.name
'Daffy'

但是如果有人行为不端呢?

>>> fowl.name = 'Daphne'
>>> fowl.name
'Daphne'

接下来的两个部分展示了如何为不希望被意外覆盖的属性获取一些隐私。

Getter 和 Setter

一些面向对象的语言支持私有对象属性,这些属性无法从外部直接访问。程序员可能需要编写gettersetter方法来读取和写入这些私有属性的值。

Python 没有私有属性,但是您可以使用名称混淆的 getter 和 setter 来获得一些隐私。(最佳解决方案是使用下一节描述的属性。)

在下面的示例中,我们定义了一个名为Duck的类,具有一个名为hidden_name的单个实例属性。我们不希望直接访问这个属性,因此我们定义了两个方法:一个 getter(get_name())和一个 setter(set_name())。每个方法都通过名为name的属性访问。我在每个方法中添加了一个print()语句,以显示它何时被调用:

>>> class Duck():
...     def __init__(self, input_name):
...         self.hidden_name = input_name
...     def get_name(self):
...         print('inside the getter')
...         return self.hidden_name
...     def set_name(self, input_name):
...         print('inside the setter')
...         self.hidden_name = input_name
>>> don = Duck('Donald')
>>> don.get_name()
inside the getter
'Donald'
>>> don.set_name('Donna')
inside the setter
>>> don.get_name()
inside the getter
'Donna'

用于属性访问的属性

对于属性隐私的 Python 解决方案是使用属性

有两种方法可以做到这一点。第一种方法是将name = property(get_name, set_name)添加为我们之前Duck类定义的最后一行:

>>> class Duck():
>>>     def __init__(self, input_name):
>>>         self.hidden_name = input_name
>>>     def get_name(self):
>>>         print('inside the getter')
>>>         return self.hidden_name
>>>     def set_name(self, input_name):
>>>         print('inside the setter')
>>>         self.hidden_name = input_name
>>>     name = property(get_name, set_name)

旧的 getter 和 setter 仍然有效:

>>> don = Duck('Donald')
>>> don.get_name()
inside the getter
'Donald'
>>> don.set_name('Donna')
inside the setter
>>> don.get_name()
inside the getter
'Donna'

现在您还可以使用属性name来获取和设置隐藏的名称:

>>> don = Duck('Donald')
>>> don.name
inside the getter
'Donald'
>>> don.name = 'Donna'
inside the setter
>>> don.name
inside the getter
'Donna'

在第二种方法中,您添加了一些装饰器,并用name替换了方法名get_nameset_name

  • @property,放在 getter 方法之前

  • @*name*.setter,放在 setter 方法之前

这是它们在代码中的实际表现:

>>> class Duck():
...     def __init__(self, input_name):
...         self.hidden_name = input_name
...     @property
...     def name(self):
...         print('inside the getter')
...         return self.hidden_name
...     @name.setter
...     def name(self, input_name):
...         print('inside the setter')
...         self.hidden_name = input_name

你仍然可以像访问属性一样访问name

>>> fowl = Duck('Howard')
>>> fowl.name
inside the getter
'Howard'
>>> fowl.name = 'Donald'
inside the setter
>>> fowl.name
inside the getter
'Donald'
注意

如果有人猜到我们称呼我们的属性为hidden_name,他们仍然可以直接作为fowl.hidden_name读取和写入它。在“隐私的名称混淆”中,你将看到 Python 提供了一种特殊的方式来隐藏属性名称。

用于计算值的属性

在之前的例子中,我们使用name属性来引用存储在对象内部的单个属性(hidden_name)。

属性还可以返回一个计算值。让我们定义一个Circle类,它有一个radius属性和一个计算出的diameter属性:

>>> class Circle():
...     def __init__(self, radius):
...         self.radius = radius
...     @property
...     def diameter(self):
...         return 2 * self.radius
...

创建一个带有其radius初始值的Circle对象:

>>> c = Circle(5)
>>> c.radius
5

我们可以将diameter称为像radius这样的属性:

>>> c.diameter
10

这是有趣的一部分:我们可以随时改变radius属性,并且diameter属性将从当前的radius值计算出来:

>>> c.radius = 7
>>> c.diameter
14

如果你没有为属性指定一个 setter 属性,你不能从外部设置它。这对于只读属性很方便:

>>> c.diameter = 20
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

使用属性而不是直接访问属性有另一个优点:如果你改变属性的定义,你只需要修复类定义内的代码,而不是所有的调用者。

用于隐私的名称混淆

在稍早的Duck类示例中,我们称呼我们的(不完全)隐藏属性为hidden_name。Python 有一个属性命名约定,这些属性不应该在其类定义之外可见:以双下划线(__)开头。

让我们将hidden_name重命名为__name,如此所示:

>>> class Duck():
...     def __init__(self, input_name):
...         self.__name = input_name
...     @property
...     def name(self):
...         print('inside the getter')
...         return self.__name
...     @name.setter
...     def name(self, input_name):
...         print('inside the setter')
...         self.__name = input_name
...

看一看是否一切仍在正常工作:

>>> fowl = Duck('Howard')
>>> fowl.name
inside the getter
'Howard'
>>> fowl.name = 'Donald'
inside the setter
>>> fowl.name
inside the getter
'Donald'

看起来不错。而且你不能访问__name属性:

>>> fowl.__name
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Duck' object has no attribute '__name'

这种命名约定并不完全使其私有,但 Python 确实mangle属性名称以使外部代码不太可能偶然发现它。如果你好奇并且承诺不告诉每个人,⁵ 这里是它变成什么样子:

>>> fowl._Duck__name
'Donald'

请注意,它没有打印inside the getter。虽然这并不是完美的保护,但名称混淆阻止了对属性的意外或故意的直接访问。

类和对象属性

您可以将属性分配给类,并且它们将被其子对象继承:

>>> class Fruit:
...     color = 'red'
...
>>> blueberry = Fruit()
>>> Fruit.color
'red'
>>> blueberry.color
'red'

但是如果您更改子对象中属性的值,则不会影响类属性:

>>> blueberry.color = 'blue'
>>> blueberry.color
'blue'
>>> Fruit.color
'red'

如果您稍后更改类属性,它不会影响现有的子对象:

>>> Fruit.color = 'orange'
>>> Fruit.color
'orange'
>>> blueberry.color
'blue'

但这将影响到新的:

>>> new_fruit = Fruit()
>>> new_fruit.color
'orange'

方法类型

一些方法是类本身的一部分,一些是从该类创建的对象的一部分,还有一些都不是:

  • 如果没有前置装饰器,那么它是一个实例方法,其第一个参数应该是self,用于引用对象本身。

  • 如果有一个前置的@classmethod装饰器,那么它是一个类方法,其第一个参数应该是cls(或任何其他,只要不是保留字class),用于指代类本身。

  • 如果有一个前置的@staticmethod装饰器,那么它是一个静态方法,它的第一个参数不是对象或类。

以下各节有一些详细信息。

实例方法

当你在类定义中的方法中看到初始的self参数时,它是一个实例方法。这些是您通常在创建自己的类时编写的方法类型。实例方法的第一个参数是self,当您调用它时,Python 会将对象传递给方法。到目前为止,您已经看到了这些。

类方法

相反,类方法影响整个类。对类进行的任何更改都会影响其所有对象。在类定义中,前置的@classmethod装饰器表示随后的函数是一个类方法。此方法的第一个参数也是类本身。Python 的传统是将参数称为cls,因为class是一个保留字,不能在这里使用。让我们为A定义一个类方法来统计有多少个对象实例已经创建了:

>>> class A():
...     count = 0
...     def __init__(self):
...         A.count += 1
...     def exclaim(self):
...         print("I'm an A!")
...     @classmethod
...     def kids(cls):
...         print("A has", cls.count, "little objects.")
...
>>>
>>> easy_a = A()
>>> breezy_a = A()
>>> wheezy_a = A()
>>> A.kids()
A has 3 little objects.

注意在__init__()中我们引用了A.count(类属性),而不是self.count(这将是一个对象实例属性)。在kids()方法中,我们使用了cls.count,但我们也可以使用A.count

静态方法

类定义中的第三种方法既不影响类也不影响其对象;它只是为了方便而存在,而不是漂浮在自己周围。它是一个静态方法,前面有一个@staticmethod装饰器,没有初始的selfcls参数。以下是作为CoyoteWeapon类的商业广告的示例:

>>> class CoyoteWeapon():
...     @staticmethod
...     def commercial():
...         print('This CoyoteWeapon has been brought to you by Acme')
...
>>>
>>> CoyoteWeapon.commercial()
This CoyoteWeapon has been brought to you by Acme

注意,我们不需要从CoyoteWeapon类创建对象来访问此方法。非常“class-y”。

鸭子类型

Python 对多态性有着宽松的实现;它根据方法的名称和参数,无论它们的类如何,都将相同的操作应用于不同的对象。

现在让我们为所有三个Quote类使用相同的__init__()初始化程序,但添加两个新函数:

  • who()只返回保存的person字符串的值

  • says()返回具有特定标点符号的保存的words字符串

现在让我们看看它们的运作方式:

>>> class Quote():
...     def __init__(self, person, words):
...         self.person = person
...         self.words = words
...     def who(self):
...         return self.person
...     def says(self):
...         return self.words + '.'
...
>>> class QuestionQuote(Quote):
...      def says(self):
...          return self.words + '?'
...
>>> class ExclamationQuote(Quote):
...      def says(self):
...          return self.words + '!'
...
>>>

我们没有改变QuestionQuoteExclamationQuote的初始化方式,因此我们没有重写它们的__init__()方法。然后 Python 会自动调用父类Quote__init__()方法来存储实例变量personwords。这就是为什么我们可以在从子类QuestionQuoteExclamationQuote创建的对象中访问self.words的原因。

接下来,让我们创建一些对象:

>>> hunter = Quote('Elmer Fudd', "I'm hunting wabbits")
>>> print(hunter.who(), 'says:', hunter.says())
Elmer Fudd says: I'm hunting wabbits.
>>> hunted1 = QuestionQuote('Bugs Bunny', "What's up, doc")
>>> print(hunted1.who(), 'says:', hunted1.says())
Bugs Bunny says: What's up, doc?
>>> hunted2 = ExclamationQuote('Daffy Duck', "It's rabbit season")
>>> print(hunted2.who(), 'says:', hunted2.says())
Daffy Duck says: It's rabbit season!

says() 方法的三个不同版本为三个类提供了不同的行为。这是面向对象语言中的传统多态性。Python 更进一步,让你运行具有这些方法的 任何 对象的 who()says() 方法。让我们定义一个名为 BabblingBrook 的类,它与我们之前的树林猎人和被猎物(Quote 类的后代)没有关系:

>>> class BabblingBrook():
...     def who(self):
...         return 'Brook'
...     def says(self):
...         return 'Babble'
...
>>> brook = BabblingBrook()

现在运行各种对象的 who()says() 方法,其中一个(brook)与其他对象完全无关:

>>> def who_says(obj):
...     print(obj.who(), 'says', obj.says())
...
>>> who_says(hunter)
Elmer Fudd says I'm hunting wabbits.
>>> who_says(hunted1)
Bugs Bunny says What's up, doc?
>>> who_says(hunted2)
Daffy Duck says It's rabbit season!
>>> who_says(brook)
Brook says Babble

有时这种行为被称为 鸭子类型,来自一句古老的谚语:

如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。

一个聪明人

我们能否对鸭子的一句智慧的话提出异议?

inp2 1001

图 10-1. 鸭子类型并非按字母顺序寻找

魔术方法

现在你可以创建并使用基本对象了。你将在本节中学到的内容可能会让你感到惊讶——是一种好的方式。

当你键入诸如 a = 3 + 8 这样的内容时,整数对象如何知道如何实现 + 呢?或者,如果你键入 name = "Daffy" + " " + "Duck",Python 如何知道 + 现在意味着将这些字符串连接起来?aname 如何知道如何使用 = 来得到结果?你可以通过使用 Python 的 特殊方法(或者更戏剧化地说,魔术方法)来解决这些运算符。

这些方法的名称以双下划线 (__) 开头和结尾。为什么呢?它们极不可能被程序员选为变量名。你已经见过一个:__init__() 从其类定义和传入的任何参数中初始化一个新创建的对象。你还见过(“用于隐私的名称混淆”)“dunder”命名如何帮助混淆类属性名称以及方法。

假设你有一个简单的 Word 类,并且你想要一个 equals() 方法来比较两个单词但忽略大小写。也就是说,包含值 'ha'Word 将被视为等于包含 'HA' 的一个。

接下来的示例是一个首次尝试,使用一个我们称之为 equals() 的普通方法。self.text 是这个 Word 对象包含的文本字符串,equals() 方法将其与 word2 的文本字符串(另一个 Word 对象)进行比较:

>>> class Word():
...    def __init__(self, text):
...        self.text = text
...
...    def equals(self, word2):
...        return self.text.lower() == word2.text.lower()
...

然后,从三个不同的文本字符串创建三个 Word 对象:

>>> first = Word('ha')
>>> second = Word('HA')
>>> third = Word('eh')

当字符串 'ha''HA' 与小写比较时,它们应该相等:

>>> first.equals(second)
True

但字符串 'eh' 将不匹配 'ha'

>>> first.equals(third)
False

我们定义了方法 equals() 来进行这个小写转换和比较。只需说 if first == second 就好了,就像 Python 的内置类型一样。那么,我们就这样做吧。我们将 equals() 方法更改为特殊名称 __eq__()(一会儿你就会明白为什么):

>>> class Word():
...     def __init__(self, text):
...         self.text = text
...     def __eq__(self, word2):
...         return self.text.lower() == word2.text.lower()
...

让我们看看它是否奏效:

>>> first = Word('ha')
>>> second = Word('HA')
>>> third = Word('eh')
>>> first == second
True
>>> first == third
False

太神奇了!我们只需要 Python 的特殊方法名称来测试相等性,__eq__()。表 10-1 和 10-2 列出了最有用的魔术方法的名称。

Table 10-1. 比较的魔术方法

方法 描述
__eq__( self, other ) self == other
__ne__( self, other ) self != other
__lt__( self, other ) self < other
__gt__( self, other ) self > other
__le__( self, other ) self <= other
__ge__( self, other ) self >= other

Table 10-2. 数学运算的魔术方法

方法 描述
__add__( self, other ) self + other
__sub__( self, other ) self other
__mul__( self, other ) self * other
__floordiv__( self, other ) self // other
__truediv__( self, other ) self / other
__mod__( self, other ) self % other
__pow__( self, other ) self ** other

您并不受限于使用数学运算符,如+(魔术方法__add__())和(魔术方法__sub__())与数字。例如,Python 字符串对象使用+进行连接和*进行复制。还有许多其他方法,在特殊方法名称中有详细记录。其中最常见的方法在表 10-3 中呈现。

Table 10-3. 其他杂项魔术方法

方法 描述
__str__( self ) str( self )
__repr__( self ) repr( self )
__len__( self ) len( self )

除了__init__()之外,您可能会发现自己在自己的方法中最常使用__str__()。这是您打印对象的方式。它被print()str()和字符串格式化器使用,您可以在第五章中了解更多。交互式解释器使用__repr__()函数将变量回显到输出。如果您未定义__str__()__repr__()中的任何一个,您将获得 Python 对象的默认字符串版本:

>>> first = Word('ha')
>>> first
<__main__.Word object at 0x1006ba3d0>
>>> print(first)
<__main__.Word object at 0x1006ba3d0>

让我们为Word类添加__str__()__repr__()方法,使其更加美观:

>>> class Word():
...     def __init__(self, text):
...         self.text = text
...     def __eq__(self, word2):
...         return self.text.lower() == word2.text.lower()
...     def __str__(self):
...         return self.text
...     def __repr__(self):
...         return 'Word("'  + self.text  + '")'
...
>>> first = Word('ha')
>>> first          # uses __repr__
Word("ha")
>>> print(first)   # uses __str__
ha

要深入了解更多特殊方法,请查看Python 文档

聚合与组合

继承是一种很好的技术,当你希望子类在大多数情况下像其父类一样时使用(当子类 是一个 父类)。在构建精细的继承层次时很诱人,但有时组合聚合更有意义。它们的区别是什么?在组合中,一个东西是另一个东西的一部分。一只鸭子 是一个 鸟(继承),但 有一个 尾巴(组合)。尾巴不是鸭子的一种,而是鸭子的一部分。在下面的例子中,让我们创建billtail对象,并将它们提供给一个新的duck对象:

>>> class Bill():
...     def __init__(self, description):
...         self.description = description
...
>>> class Tail():
...     def __init__(self, length):
...         self.length = length
...
>>> class Duck():
...     def __init__(self, bill, tail):
...         self.bill = bill
...         self.tail = tail
...     def about(self):
...         print('This duck has a', self.bill.description,
...             'bill and a', self.tail.length, 'tail')
...
>>> a_tail = Tail('long')
>>> a_bill = Bill('wide orange')
>>> duck = Duck(a_bill, a_tail)
>>> duck.about()
This duck has a wide orange bill and a long tail

聚合表达了关系,但要松散一些:一个东西 使用 另一个东西,但两者都是独立存在的。一只鸭子 使用 一片湖水,但它们并非彼此的一部分。

对象或其他东西何时使用

这里有一些指南,帮助你决定是否将代码和数据放入类、模块(在第十一章中讨论)或其他某些地方:

  • 当你需要许多具有相似行为(方法)但在内部状态(属性)上不同的个体实例时,对象是最有用的。

  • 类支持继承,而模块不支持。

  • 如果只需要一个东西,模块可能是最好的选择。无论在程序中引用多少次 Python 模块,只加载一个副本。(Java 和 C++程序员:你可以将 Python 模块用作单例。)

  • 如果你有多个变量,其中包含多个值并且可以作为多个函数的参数传递,那么将它们定义为类可能更好。例如,你可以使用一个带有sizecolor等键的字典来表示彩色图像。你可以为程序中的每个图像创建一个不同的字典,并将它们作为参数传递给scale()transform()等函数。随着键和函数的增加,这可能会变得混乱。定义一个Image类更加一致,包含sizecolor属性以及scale()transform()方法会更好。这样,所有与彩色图像相关的数据和方法都定义在同一个地方。

  • 使用问题的最简解决方案。字典、列表或元组比模块更简单、更小、更快,通常比类更简单。

    Guido 的建议:

    避免过度工程化的数据结构。元组比对象更好(尝试使用命名元组也是如此)。优先选择简单字段而不是 getter/setter 函数……内置数据类型是你的朋友。使用更多的数字、字符串、元组、列表、集合、字典。还要查看 collections 库,特别是 deque。

    Guido van Rossum

  • 一个更新的替代方案是数据类,在“数据类”中。

命名元组

因为 Guido 刚提到它们,而我还没有,这是谈论命名元组的好地方。命名元组是元组的一个子类,可以通过名称(使用*.name)以及位置(使用[偏移]*)访问值。

让我们从上一节的示例中获取示例,并将Duck类转换为一个命名元组,其中billtail是简单的字符串属性。我们将调用namedtuple函数并传入两个参数:

  • 名称

  • 字段名称的字符串,用空格分隔

Python 不会自动提供命名元组,因此在使用它们之前需要加载一个模块。我们在以下示例的第一行中执行了这样的操作:

>>> from collections import namedtuple
>>> Duck = namedtuple('Duck', 'bill tail')
>>> duck = Duck('wide orange', 'long')
>>> duck
Duck(bill='wide orange', tail='long')
>>> duck.bill
'wide orange'
>>> duck.tail
'long'

你也可以从一个字典中创建一个命名元组:

>>> parts = {'bill': 'wide orange', 'tail': 'long'}
>>> duck2 = Duck(**parts)
>>> duck2
Duck(bill='wide orange', tail='long')

在上面的代码中,看看**parts。这是一个关键字参数。它从parts字典中提取键和值,并将它们作为参数提供给Duck()。它与以下代码具有相同的效果:

>>> duck2 = Duck(bill = 'wide orange', tail = 'long')

命名元组是不可变的,但可以替换一个或多个字段并返回另一个命名元组:

>>> duck3 = duck2._replace(tail='magnificent', bill='crushing')
>>> duck3
Duck(bill='crushing', tail='magnificent')

我们本可以将duck定义为一个字典:

>>> duck_dict = {'bill': 'wide orange', 'tail': 'long'}
>>> duck_dict
{'tail': 'long', 'bill': 'wide orange'}

你可以向字典添加字段:

>>> duck_dict['color'] = 'green'
>>> duck_dict
{'color': 'green', 'tail': 'long', 'bill': 'wide orange'}

但不是一个命名元组:

>>> duck.color = 'green'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Duck' object has no attribute 'color'

总结一下,以下是命名元组的一些优点:

  • 它看起来和表现得像一个不可变对象。

  • 它比对象更省空间和时间。

  • 你可以使用点符号而不是类似字典风格的方括号来访问属性。

  • 你可以将其用作字典键。

数据类

许多人喜欢创建对象主要用于存储数据(作为对象属性),而不是行为(方法)。刚才你看到命名元组可以作为替代数据存储的一种选择。Python 3.7 引入了数据类

这是一个普通的对象,只有一个名为name的属性:

>> class TeenyClass():
...     def __init__(self, name):
...         self.name = name
...
>>> teeny = TeenyClass('itsy')
>>> teeny.name
'itsy'

使用数据类做同样的事情看起来有些不同:

>>> from dataclasses import dataclass
>>> @dataclass
... class TeenyDataClass:
...     name: str
...
>>> teeny = TeenyDataClass('bitsy')
>>> teeny.name
'bitsy'

除了需要一个@dataclass装饰器外,你还需要使用变量注释的形式定义类的属性,如*name*: *type**name*: *type* = *val*,比如color: strcolor: str = "red"*type*可以是任何 Python 对象类型,包括你创建的类,而不仅限于像strint这样的内置类型。

当你创建数据类对象时,可以按照类中指定的顺序提供参数,或者以任意顺序使用命名参数:

>>> from dataclasses import dataclass
>>> @dataclass
... class AnimalClass:
...     name: str
...     habitat: str
...     teeth: int = 0
...
>>> snowman = AnimalClass('yeti', 'Himalayas', 46)
>>> duck = AnimalClass(habitat='lake', name='duck')
>>> snowman
AnimalClass(name='yeti', habitat='Himalayas', teeth=46)
>>> duck
AnimalClass(name='duck', habitat='lake', teeth=0)

AnimalClass为其teeth属性定义了默认值,因此在创建duck时无需提供它。

你可以像访问任何其他对象一样引用对象属性:

>>> duck.habitat
'lake'
>>> snowman.teeth
46

数据类还有很多功能。参见这篇指南或官方(详尽的)文档

属性

你已经看到如何创建类并添加属性,以及它们可能涉及大量打字的事情,比如定义__init__(),将其参数分配给self的对应项,并创建所有这些 dunder 方法,如__str__()。命名元组和数据类是标准库中的替代品,当你主要想创建数据集时可能更容易。

每个人都需要的一个 Python 库比较了普通类、命名元组和数据类。它推荐第三方包attrs,原因很多——少打字、数据验证等等。看一看,看看你是否喜欢它胜过内置解决方案。

即将到来

在下一章中,你将在代码结构中进一步提升到 Python 的模块

要做的事情

10.1 创建一个名为Thing的类,不包含内容并打印它。然后,从这个类创建一个名为example的对象并打印它。打印的值是相同的还是不同的?

10.2 创建一个名为Thing2的新类,并将值'abc'赋给名为letters的类属性。打印letters

10.3 再次创建一个名为Thing3的类,这次将值'xyz'赋给名为letters的实例(对象)属性。打印letters。你需要从该类创建一个对象吗?

10.4 创建一个名为Element的类,具有实例属性namesymbolnumber。使用值'Hydrogen''H'1创建该类的一个对象。

10.5 使用这些键和值创建一个字典:'name': 'Hydrogen', 'symbol': 'H', 'number': 1。然后,使用这个字典从Element类创建一个名为hydrogen的对象。

10.6 对于Element类,定义一个名为dump()的方法,打印对象属性(namesymbolnumber)的值。使用这个新定义创建hydrogen对象,并使用dump()打印其属性。

10.7 调用print(hydrogen)。在Element的定义中,将方法dump的名称更改为__str__,创建一个新的hydrogen对象,并再次调用print(hydrogen)

10.8 修改Element使属性namesymbolnumber变为私有。为每个属性定义一个 getter 属性来返回其值。

10.9 定义三个类:BearRabbitOctothorpe。对于每个类,只定义一个方法:eats()。这应该返回'berries'Bear)、'clover'Rabbit)或'campers'Octothorpe)。分别创建一个对象并打印它们吃的是什么。

10.10 定义这些类:LaserClawSmartPhone。每个类只有一个方法:does()。它返回'disintegrate'Laser)、'crush'Claw)或'ring'SmartPhone)。然后,定义一个名为Robot的类,其中包含每个组件对象的一个实例。为Robot定义一个does()方法,打印其组件对象的功能。

¹ 即使你不想。

² Python 中的命名经常会看到双下划线的例子;为了节省音节,有些人将其发音为dunder

³ 一个 80 年代的便宜但不怎么样的汽车。

⁴ 骡马的父亲是驴,母亲是马;小母驴的父亲是马,母亲是驴。

⁵ 你能保守秘密吗?显然,我不能。

第十一章:模块、包和好东西

在您从底层向上攀登的过程中,您已经从内置数据类型进展到构建越来越大的数据和代码结构。在本章中,您终于学会如何在 Python 中编写现实的完整程序。您将编写自己的模块,并学习如何使用来自 Python 标准库和其他来源的模块。

本书的文本是按层次结构组织的:单词、句子、段落和章节。否则,它会很快变得难以阅读。¹ 代码的组织原则大致相同:数据类型类似于单词;表达式和语句类似于句子;函数类似于段落;模块类似于章节。继续这个类比,在本书中,当我说某些内容将在第八章中解释时,在编程中这就像是引用另一个模块中的代码。

模块和 import 语句

我们将在多个文件中创建和使用 Python 代码。模块只是包含任何 Python 代码的文件。您不需要做任何特殊处理——任何 Python 代码都可以被其他人用作模块。

我们通过使用 Python 的import语句引用其他模块的代码。这使得导入模块中的代码和变量对您的程序可用。

导入一个模块

import语句的最简单用法是import module,其中*module是另一个 Python 文件的名称,不带.py*扩展名。

假设你和几个人想要快速解决午餐问题,但又不想进行长时间的讨论,最后总是由最吵闹的那个人决定。让电脑来决定吧!让我们编写一个单一函数的模块,返回一个随机的快餐选择,以及调用该函数并打印选择的主程序。

模块(fast.py)显示在示例 11-1 中。

示例 11-1。fast.py
from random import choice

places = ['McDonalds", "KFC", "Burger King", "Taco Bell",
     "Wendys", "Arbys", "Pizza Hut"]

def pick():  # see the docstring below?
    """Return random fast food place"""
    return choice(places)

并且示例 11-2 展示了导入它的主程序(称为lunch.py)。

示例 11-2。lunch.py
import fast

place = fast.pick()
print("Let's go to", place)

如果您将这两个文件放在同一个目录中,并指示 Python 将lunch.py作为主程序运行,它将访问fast模块并运行其pick()函数。我们编写了pick()的这个版本,以从字符串列表中返回一个随机结果,因此主程序将获得并打印出这个结果:

$ python lunch.py
Let's go to Burger King
$ python lunch.py
Let's go to Pizza Hut
$ python lunch.py
Let's go to Arbys

我们在两个不同的地方使用了导入:

  • 主程序lunch.py导入了我们的新模块fast

  • 模块文件fast.py从 Python 的标准库模块random中导入了choice函数。

我们在主程序和模块中以两种不同的方式使用了导入:

  • 在第一种情况下,我们导入了整个 fast 模块,但需要使用 fast 作为 pick() 的前缀。在这个 import 语句之后,只要我们在名称前加上 fast.fast.py 中的所有内容对主程序都是可用的。通过用模块的名称限定模块的内容,我们避免了任何糟糕的命名冲突。其他模块可能有一个 pick() 函数,我们不会误调用它。

  • 在第二种情况下,我们在一个模块内部,并且知道这里没有其他名为 choice 的内容,因此直接从 random 模块中导入了 choice() 函数。

我们本可以像示例 11-3 中所示那样编写 fast.py,在 pick() 函数内部导入 random 而不是在文件顶部。

示例 11-3. fast2.py
places = ['McDonalds", "KFC", "Burger King", "Taco Bell",
     "Wendys", "Arbys", "Pizza Hut"]

def pick():
    import random
    return random.choice(places)

像编程的许多方面一样,使用最清晰的风格。使用模块限定的名称(random.choice)更安全,但需要稍微多输入一些字。

如果导入的代码可能在多个地方使用,请考虑从函数外部导入;如果知道其使用将受限制,请从函数内部导入。有些人喜欢将所有导入都放在文件顶部,以明确代码的所有依赖关系。无论哪种方式都可以。

使用另一个名称导入模块

在我们的主 lunch.py 程序中,我们调用了 import fast。但如果你:

  • 还有另一个名为 fast 的模块吗?

  • 想使用一个更易记的名称吗?

  • 门夹到手指想减少打字?

在这些情况下,你可以使用一个别名导入,如示例 11-4 所示。让我们使用别名 f

示例 11-4. fast3.py
import fast as f
place = f.pick()
print("Let's go to", place)

从一个模块中仅导入你想要的内容

你可以导入整个模块,也可以只导入部分。你刚刚看到了后者:我们只想要 random 模块中的 choice() 函数。

像模块本身一样,你可以为每个导入的东西使用别名。

让我们再重新做几次 lunch.py。首先,从 fast 模块中以其原始名称导入 pick()(示例 11-5)。

示例 11-5. fast4.py
from fast import pick
place = pick()
print("Let's go to", place)

现在将其导入为 who_cares(示例 11-6)。

示例 11-6. fast5.py
from fast import pick as who_cares
place = who_cares()
print("Let's go to", place)

我们从单行代码、到多行函数、到独立程序、再到同一目录中的多个模块。如果你没有很多模块,同一目录也可以正常工作。

为了使 Python 应用程序能够更好地扩展,你可以将模块组织成称为的文件和模块层次结构。一个包只是包含 .py 文件的子目录。而且你可以进行多层次的组织,有目录在其中。

我们刚刚写了一个选择快餐的模块。让我们再添加一个类似的模块来提供人生建议。我们将在当前目录中创建一个名为 questions.py 的新主程序。现在在其中创建一个名为 choices 的子目录,并将两个模块放入其中——fast.pyadvice.py。每个模块都有一个返回字符串的函数。

主程序(questions.py)有额外的导入和行(示例 11-7)。

示例 11-7. questions.py
from sources import fast, advice

print("Let's go to", fast.pick())
print("Should we take out?", advice.give())

那个from sources让 Python 在当前目录下查找名为sources的目录。在sources内部,它查找fast.pyadvice.py文件。

第一个模块(choices/fast.py)与以前相同的代码,只是移动到了choices目录中(示例 11-8)。

示例 11-8. choices/fast.py
from random import choice

places = ["McDonalds", "KFC", "Burger King", "Taco Bell",
     "Wendys", "Arbys", "Pizza Hut"]

def pick():
    """Return random fast food place"""
    return choice(places)

第二个模块(choices/advice.py)是新的,但功能与快餐相似(示例 11-9)。

示例 11-9. choices/advice.py
from random import choice

answers = ["Yes!", "No!", "Reply hazy", "Sorry, what?"]

def give():
    """Return random advice"""
    return choice(answers)
注意

如果你的 Python 版本早于 3.3,那么在sources子目录中还需要一件事才能使其成为 Python 包:一个名为*__init__.py*的文件。这可以是一个空文件,但是在 3.3 之前的 Python 中,需要这样做才能将包含它的目录视为包。(这是另一个常见的 Python 面试问题。)

运行主程序questions.py(从当前目录,而不是sources中)来看看会发生什么:

$ python questions.py
Let's go to KFC
Should we take out? Yes!
$ python questions.py
Let's go to Wendys
Should we take out? Reply hazy
$ python questions.py
Let's go to McDonalds
Should we take out? Reply hazy

模块搜索路径

我刚才说过,Python 会在当前目录下查找子目录choices及其模块。实际上,它还会在其他地方查找,并且你可以控制这一过程。

早些时候,我们从标准库的random模块导入了函数choice()。这不在你的当前目录中,因此 Python 还需要在其他地方查找。

要查看 Python 解释器查找的所有位置,导入标准的sys模块并使用它的path列表。这是一个目录名称和 ZIP 存档文件列表,Python 按顺序搜索以找到要导入的模块。

你可以访问并修改这个列表。这是我 Mac 上 Python 3.7 的sys.path值:

>>> import sys
>>> for place in sys.path:
...     print(place)
...

/Library/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload

那个初始的空输出行是空字符串'',它表示当前目录。如果''sys.path的第一位,当你尝试导入某些东西时,Python 首先查找当前目录:import fast会寻找fast.py。这是 Python 的常规设置。此外,当我们创建名为sources的子目录并在其中放置 Python 文件时,它们可以通过import sourcesfrom sources import fast来导入。

将使用第一个匹配项。这意味着如果你定义了一个名为random的模块,并且它在标准库之前的搜索路径中,那么现在就无法访问标准库的random了。

你可以在代码中修改搜索路径。假设你希望 Python 在其他任何位置之前查找*/my/modules*目录:

>>> import sys
>>> sys.path.insert(0, "/my/modules")

相对和绝对导入

到目前为止,在我们的示例中,我们从以下位置导入了我们自己的模块:

  • 当前目录

  • 子目录choices

  • Python 标准库

这在你有与标准模块同名的本地模块时效果很好。你想要哪一个?

Python 支持绝对相对导入。到目前为止你看到的例子都是绝对导入。如果你键入import rougarou,对于搜索路径中的每个目录,Python 会查找名为rougarou.py(一个模块)或名为rougarou(一个包)的文件。

  • 如果rougarou.py与你调用问题的同一目录中,你可以用from . import rougarou来相对于你所在位置导入它。

  • 如果它位于你的上一级目录中:from .. import rougarou

  • 如果它位于名为creatures的同级目录下:from ..creatures import rougarou

...的符号借鉴于 Unix 对当前目录父目录的简写。

对于 Python 导入系统中可能遇到的问题的深入讨论,请参阅Python 导入系统的陷阱

命名空间包

你已经看到可以将 Python 模块打包为:

  • 一个单一的模块.py 文件)

  • (包含模块及可能其他包的目录)

你也可以通过命名空间包在多个目录中分割一个包。假设你想要一个名为critters的包,其中包含每种危险生物(真实或想象中,据说具有背景信息和防护提示)的 Python 模块。随着时间的推移,这可能会变得很庞大,你可能希望按地理位置细分。一种选择是在critters下添加位置子包,并将现有的*.py模块文件移到它们下面,但这会打破其他导入它们的模块。相反,我们可以向上*进行如下操作:

  • critters上创建新的位置目录

  • 在这些新父目录下创建表兄弟目录critters

  • 将现有模块移动到它们各自的目录中。

这需要一些说明。假设我们从这样的文件布局开始:

critters
 ⌞ rougarou.py
 ⌞ wendigo.py

这些模块的正常导入看起来像这样:

from critters import wendigo, rougarou

现在如果我们决定在美国的地点,文件和目录看起来会像这样:

north
 ⌞ critters
   ⌞ wendigo.py
south
 ⌞ critters
   ⌞ rougarou.py

如果northsouth都在你的模块搜索路径中,你可以像它们仍然共存于单目录包一样导入这些模块:

from critters import wendigo, rougarou

模块与对象

你应该把你的代码放入一个模块中,还是放入一个对象中?什么时候适合?

它们在许多方面看起来很相似。一个名为thing的对象或模块,具有称为stuff的内部数据值,让你可以像thing.stuff那样访问该值。stuff可能在创建模块或类时已经定义,也可能是后来分配的。

模块中的所有类、函数和全局变量对外部都是可用的。对象可以使用属性和“dunder”(__ …)命名来隐藏或控制对它们数据属性的访问。

这意味着你可以这样做:

>>> import math
>>> math.pi
3.141592653589793
>>> math.pi = 3.0
>>> math.pi
3.0

你刚刚搞砸了这台计算机上每个人的计算吗?是的!不,开玩笑的。² 这并未影响 Python 的 math 模块。你只是改变了你调用程序导入的 math 模块代码的副本中 pi 的值,并且所有关于你罪行的证据将在程序结束时消失。

你的程序导入的任何模块只有一个副本,即使你多次导入它。你可以用它保存对任何导入代码感兴趣的全局事物。这与类似,尽管你可以从中创建许多对象,但类也只有一个副本。

Python 标准库中的好东西

Python 的一个显著特点是其“即插即用”——一个包含许多有用任务的大型标准库模块。它们被保持分开,以避免膨胀核心语言。当你打算写一些 Python 代码时,经常值得先检查是否已经有标准模块实现了你想要的功能。令人惊讶的是,你会经常遇到标准库中的一些小宝石。Python 还为这些模块提供了权威的 文档,以及一个 教程。Doug Hellmann 的网站 Python Module of the Week 和书籍 The Python Standard Library by Example(Addison-Wesley Professional)也是非常有用的指南。

本书的即将到来的章节涵盖了许多特定于网络、系统、数据库等的标准模块。在本节中,我讨论一些具有通用用途的标准模块。

使用 setdefault()defaultdict() 处理缺失键

你已经看到尝试访问字典的不存在键会引发异常。使用字典的 get() 函数返回一个默认值可以避免异常。setdefault() 函数类似于 get(),但也会在键缺失时向字典中分配一个项目:

>>> periodic_table = {'Hydrogen': 1, 'Helium': 2}
>>> periodic_table
{'Hydrogen': 1, 'Helium': 2}

如果键原先 在字典中,新值就会被使用:

>>> carbon = periodic_table.setdefault('Carbon', 12)
>>> carbon
12
>>> periodic_table
{'Hydrogen': 1, 'Helium': 2, 'Carbon': 12}

如果我们尝试为 现有 键分配不同的默认值,将返回原始值且不会发生任何更改:

>>> helium = periodic_table.setdefault('Helium', 947)
>>> helium
2
>>> periodic_table
{'Hydrogen': 1, 'Helium': 2, 'Carbon': 12}

defaultdict() 类似,但在创建字典时就指定了任何新键的默认值。它的参数是一个函数。在这个例子中,我们传递了函数 int,它将被调用为 int() 并返回整数 0

>>> from collections import defaultdict
>>> periodic_table = defaultdict(int)

现在任何缺失的值将是整数 (int), 其值为 0:

>>> periodic_table['Hydrogen'] = 1
>>> periodic_table['Lead']
0
>>> periodic_table
defaultdict(<class 'int'>, {'Hydrogen': 1, 'Lead': 0})

defaultdict() 的参数是一个返回要分配给缺失键的值的函数。在下面的例子中,当需要时执行 no_idea() 来返回一个值:

>>> from collections import defaultdict
>>>
>>> def no_idea():
...     return 'Huh?'
...
>>> bestiary = defaultdict(no_idea)
>>> bestiary['A'] = 'Abominable Snowman'
>>> bestiary['B'] = 'Basilisk'
>>> bestiary['A']
'Abominable Snowman'
>>> bestiary['B']
'Basilisk'
>>> bestiary['C']
'Huh?'

你可以使用 int()list()dict() 函数来返回这些类型的默认空值:int() 返回 0list() 返回一个空列表 ([]),dict() 返回一个空字典 ({})。如果省略参数,新键的初始值将设置为 None

顺便说一下,您可以使用lambda在调用内部定义您的默认生成函数:

>>> bestiary = defaultdict(lambda: 'Huh?')
>>> bestiary['E']
'Huh?'

使用int是制作自己的计数器的一种方法:

>>> from collections import defaultdict
>>> food_counter = defaultdict(int)
>>> for food in ['spam', 'spam', 'eggs', 'spam']:
...     food_counter[food] += 1
...
>>> for food, count in food_counter.items():
...     print(food, count)
...
eggs 1
spam 3

在上面的示例中,如果food_counter是一个普通的字典而不是defaultdict,每次尝试增加字典元素food_counter[food]时,Python 都会引发一个异常,因为它不会被初始化。我们需要做一些额外的工作,如下所示:

>>> dict_counter = {}
>>> for food in ['spam', 'spam', 'eggs', 'spam']:
...     if not food in dict_counter:
...         dict_counter[food] = 0
...     dict_counter[food] += 1
...
>>> for food, count in dict_counter.items():
...     print(food, count)
...
spam 3
eggs 1

使用 Counter()计算项数

说到计数器,标准库中有一个可以执行前面示例工作以及更多工作的计数器:

>>> from collections import Counter
>>> breakfast = ['spam', 'spam', 'eggs', 'spam']
>>> breakfast_counter = Counter(breakfast)
>>> breakfast_counter
Counter({'spam': 3, 'eggs': 1})

函数most_common()以降序返回所有元素,或者如果给定了计数,则仅返回前count个元素:

>>> breakfast_counter.most_common()
[('spam', 3), ('eggs', 1)]
>>> breakfast_counter.most_common(1)
[('spam', 3)]

您可以组合计数器。首先,让我们再次看看breakfast_counter中有什么:

>>> breakfast_counter
>>> Counter({'spam': 3, 'eggs': 1})

这一次,我们创建了一个名为lunch的新列表,以及一个名为lunch_counter的计数器:

>>> lunch = ['eggs', 'eggs', 'bacon']
>>> lunch_counter = Counter(lunch)
>>> lunch_counter
Counter({'eggs': 2, 'bacon': 1})

我们组合两个计数器的第一种方法是通过加法,使用+

>>> breakfast_counter + lunch_counter
Counter({'spam': 3, 'eggs': 3, 'bacon': 1})

正如您所预期的,您可以使用-从另一个计数器中减去一个计数器。早餐吃什么而午餐不吃呢?

>>> breakfast_counter - lunch_counter
Counter({'spam': 3})

好的,现在我们可以吃午餐了,但是我们早餐不能吃什么呢?

>>> lunch_counter - breakfast_counter
Counter({'bacon': 1, 'eggs': 1})

类似于第八章中的集合,您可以使用交集运算符&获取共同的项:

>>> breakfast_counter & lunch_counter
Counter({'eggs': 1})

交集选择了具有较低计数的共同元素('eggs')。这是有道理的:早餐只提供了一个鸡蛋,所以这是共同的计数。

最后,您可以使用并集运算符|获取所有项:

>>> breakfast_counter | lunch_counter
Counter({'spam': 3, 'eggs': 2, 'bacon': 1})

项目'eggs'再次是两者共同的。与加法不同,联合操作并未将它们的计数相加,而是选择计数较大的那个。

使用 OrderedDict()按键排序

这是使用 Python 2 解释器运行的示例:

>>> quotes = {
...     'Moe': 'A wise guy, huh?',
...     'Larry': 'Ow!',
...     'Curly': 'Nyuk nyuk!',
...     }
>>> for stooge in quotes:
...  print(stooge)
...
Larry
Curly
Moe
注意

从 Python 3.7 开始,字典会按照它们被添加的顺序保留键。OrderedDict对于早期版本非常有用,因为它们具有不可预测的顺序。本节中的示例仅在您使用的 Python 版本早于 3.7 时才相关。

OrderedDict()记住键添加的顺序,并从迭代器中以相同的顺序返回它们。尝试从一个(, )元组序列创建一个OrderedDict

>>> from collections import OrderedDict
>>> quotes = OrderedDict([
...     ('Moe', 'A wise guy, huh?'),
...     ('Larry', 'Ow!'),
...     ('Curly', 'Nyuk nyuk!'),
...     ])
>>>
>>> for stooge in quotes:
...     print(stooge)
...
Moe
Larry
Curly

栈+队列==deque

一个deque(发音为deck)是一个双端队列,具有栈和队列的特性。当你想要从序列的任一端添加或删除项时,它非常有用。在这里,我们从单词的两端向中间工作,以查看它是否是回文。函数popleft()从 deque 中删除最左边的项并返回它;pop()则删除最右边的项并返回它。它们一起从两端向中间工作。只要末尾字符匹配,它就会持续弹出,直到达到中间位置:

>>> def palindrome(word):
...     from collections import deque
...     dq = deque(word)
...     while len(dq) > 1:
...        if dq.popleft() != dq.pop():
...            return False
...     return True
...
...
>>> palindrome('a')
True
>>> palindrome('racecar')
True
>>> palindrome('')
True
>>> palindrome('radar')
True
>>> palindrome('halibut')
False

我将其用作双端队列的简单说明。如果你真的想要一个快速的回文检查器,只需将字符串与其反转比较就简单得多了。Python 没有字符串的 reverse() 函数,但它确实有一种通过切片来反转字符串的方法,如下例所示:

>>> def another_palindrome(word):
...     return word == word[::-1]
...
>>> another_palindrome('radar')
True
>>> another_palindrome('halibut')
False

用 itertools 遍历代码结构

itertools包含特殊用途的迭代器函数。每次在forin循环中调用时,它返回一个项目,并在调用之间记住其状态。

chain() 将其参数视为单个可迭代对象运行:

>>> import itertools
>>> for item in itertools.chain([1, 2], ['a', 'b']):
...     print(item)
...
1
2
a
b

cycle()是一个无限迭代器,循环遍历其参数:

>>> import itertools
>>> for item in itertools.cycle([1, 2]):
...     print(item)
...
1
2
1
2
.
.
.

等等。

accumulate() 计算累积值。默认情况下,它计算总和:

>>> import itertools
>>> for item in itertools.accumulate([1, 2, 3, 4]):
...     print(item)
...
1
3
6
10

您可以将一个函数作为accumulate()的第二个参数提供,它将被用于代替加法。该函数应该接受两个参数并返回一个单一的结果。这个例子计算一个累积乘积:

>>> import itertools
>>> def multiply(a, b):
...     return a * b
...
>>> for item in itertools.accumulate([1, 2, 3, 4], multiply):
...     print(item)
...
1
2
6
24

itertools 模块还有许多其他函数,尤其是一些组合和排列函数,在需要时可以节省时间。

使用pprint()进行漂亮打印

我们所有的例子都使用print()(或者在交互式解释器中仅使用变量名)来打印东西。有时,结果很难读取。我们需要一个漂亮打印机,比如pprint()

>>> from pprint import pprint
>>> quotes = OrderedDict([
...     ('Moe', 'A wise guy, huh?'),
...     ('Larry', 'Ow!'),
...     ('Curly', 'Nyuk nyuk!'),
...     ])
>>>

简单的print()只是将东西倒出来:

>>> print(quotes)
OrderedDict([('Moe', 'A wise guy, huh?'), ('Larry', 'Ow!'),
 ('Curly', 'Nyuk nyuk!')])

但是,pprint()试图对齐元素以提高可读性:

>>> pprint(quotes)
{'Moe': 'A wise guy, huh?',
 'Larry': 'Ow!',
 'Curly': 'Nyuk nyuk!'}

获取随机数

我们在本章的开头玩了random.choice()。它从给定的序列(列表、元组、字典、字符串)中返回一个值:

>>> from random import choice
>>> choice([23, 9, 46, 'bacon', 0x123abc])
1194684
>>> choice( ('a', 'one', 'and-a', 'two') )
'one'
>>> choice(range(100))
68
>>> choice('alphabet')
'l'

使用sample()函数一次获取多个值:

>>> from random import sample
>>> sample([23, 9, 46, 'bacon', 0x123abc], 3)
[1194684, 23, 9]
>>> sample(('a', 'one', 'and-a', 'two'), 2)
['two', 'and-a']
>>> sample(range(100), 4)
[54, 82, 10, 78]
>>> sample('alphabet', 7)
['l', 'e', 'a', 't', 'p', 'a', 'b']

要从任意范围获取一个随机整数,您可以使用choice()sample()range(),或者使用randint()randrange()

>>> from random import randint
>>> randint(38, 74)
71
>>> randint(38, 74)
60
>>> randint(38, 74)
61

randrange()range()一样,有起始(包含)和结束(不包含)整数的参数,还有一个可选的整数步长:

>>> from random import randrange
>>> randrange(38, 74)
65
>>> randrange(38, 74, 10)
68
>>> randrange(38, 74, 10)
48

最后,获取一个在 0.0 到 1.0 之间的随机实数(浮点数):

>>> from random import random
>>> random()
0.07193393312692198
>>> random()
0.7403243673826271
>>> random()
0.9716517846775018

更多电池:获取其他 Python 代码

有时,标准库没有您需要的功能,或者没有以正确的方式执行。有一个完整的开源、第三方 Python 软件世界。良好的资源包括以下内容:

你可以在activestate找到许多较小的代码示例。

本书几乎所有的 Python 代码都使用您计算机上的标准 Python 安装,其中包括所有内置函数和标准库。某些地方特别提到了requests在第一章中;更多细节请参见第十八章。附录 B 展示了如何安装第三方 Python 软件,以及许多其他开发细节。

即将发生的事情

下一章是一个实用章节,涵盖 Python 中许多数据操作的方面。您将遇到二进制bytesbytearray数据类型,在文本字符串中处理 Unicode 字符,并使用正则表达式搜索文本字符串。

要做的事情

11.1 创建一个名为zoo.py的文件。在其中,定义一个名为hours()的函数,打印字符串'Open 9-5 daily'。然后,使用交互解释器导入zoo模块并调用其hours()函数。

11.2 在交互解释器中,将zoo模块作为menagerie导入,并调用其hours()函数。

11.3 仍然在解释器中,直接从zoo中导入hours()函数并调用它。

11.4 将hours()函数作为info导入并调用它。

11.5 创建一个名为plain的字典,其键值对为'a': 1'b': 2'c': 3,然后打印它。

11.6 从上一个问题中列出的相同对创建一个名为fancyOrderedDict并打印它。它是否按照plain的顺序打印?

11.7 创建一个名为dict_of_listsdefaultdict,并传递list作为参数。用一次赋值操作将列表dict_of_lists['a']并附加值'something for a'。打印dict_of_lists['a']

¹ 至少,比它现在的阅读性少一点。

² 还是会?布娃哈哈。

第二部分:Python 实践篇

第十二章:整理和处理数据

如果你折磨数据足够久,自然会招认。

罗纳德·科斯

到目前为止,我们主要讨论了 Python 语言本身——其数据类型、代码结构、语法等等。本书的其余部分是关于将这些应用到现实世界问题的内容。

在本章中,你将学习许多实用的数据处理技术。有时,这被称为数据清洗,或者更商业化的ETL(提取/转换/加载)在数据库世界中。尽管编程书籍通常不会明确涵盖这个主题,程序员们花费了大量时间来将数据塑造成符合其目的的正确形式。

数据科学这一专业在过去几年变得非常流行。《哈佛商业评论》的一篇文章称数据科学家是“21 世纪最性感的职业”。如果这意味着需求量大且薪资丰厚,那就好,但也有足够的单调乏味。数据科学超越了数据库的 ETL 需求,通常涉及机器学习,以发掘人眼看不到的洞察力。

我将从基本的数据格式开始,然后介绍最有用的新数据科学工具。

数据格式大致分为两类:文本二进制。Python 的字符串用于文本数据,本章包含了我们迄今为止跳过的字符串信息:

  • Unicode字符

  • 正则表达式模式匹配。

然后,我们转向二进制数据,以及 Python 的另外两种内置类型:

  • 字节用于不可变的八位值

  • 字节数组用于可变的字节

文本字符串:Unicode

你在第五章中看到了 Python 字符串的基础知识。现在是深入了解 Unicode 的时候了。

Python 3 的字符串是 Unicode 字符序列,而不是字节数组。这是从 Python 2 最大的语言变化。

到目前为止,本书中的所有文本示例都是普通的 ASCII(美国标准信息交换码)。ASCII 是在六十年代定义的,在鲑鱼头发流行之前。当时的计算机大小如冰箱,稍微聪明一点。

计算机存储的基本单元是字节,它可以存储 256 个独特的值在它的八中。出于各种原因,ASCII 只使用了七位(128 个独特的值):26 个大写字母、26 个小写字母、10 个数字、一些标点符号、一些间隔字符和一些不可打印的控制码。

不幸的是,世界上的字母比 ASCII 提供的还要多。你可以在餐馆吃热狗,但在咖啡馆永远也买不到 Gewürztraminer¹。已经尝试过许多方法将更多的字母和符号塞入八位中,有时你会看到它们。其中只有一些包括:

  • Latin-1,或者ISO 8859-1

  • Windows 代码页1252

这些字符都使用了所有的八位,但即使如此也不够,特别是在需要非欧洲语言时。Unicode 是一个持续进行的国际标准,用于定义所有世界语言的字符,以及数学和其他领域的符号。还有表情符号!

Unicode 为每个字符提供了一个唯一的编号,无论是什么平台、什么程序、什么语言。

Unicode 联盟

Unicode 代码图表页面包含所有当前定义的字符集的链接及其图像。最新版本(12.0)定义了超过 137,000 个字符,每个字符都有唯一的名称和标识号码。Python 3.8 可以处理所有这些字符。这些字符被分为称为平面的八位集合。前 256 个平面是基本多语言平面。详细信息请参阅关于Unicode 平面的维基百科页面。

Python 3 Unicode 字符串

如果您知道字符的 Unicode ID 或名称,可以在 Python 字符串中使用它。以下是一些示例:

  • \u后跟个十六进制数字²指定 Unicode 的 256 个基本多语言平面中的一个字符。前两个数字是平面编号(00FF),后两个数字是平面内字符的索引。平面00是老旧的 ASCII,该平面内的字符位置与 ASCII 相同。

  • 对于高平面中的字符,我们需要更多的位数。Python 中这些字符的转义序列是\U后跟个十六进制字符;最左边的数字需要是0

  • 对于所有字符,\N{*`name`*} 允许您通过其标准名称指定它。Unicode 字符名称索引页面列出了这些名称。

Python 的 unicodedata 模块具有双向转换的功能:

  • lookup()—接受一个不区分大小写的名称,并返回一个 Unicode 字符。

  • name()—接受一个 Unicode 字符并返回其大写名称。

在下面的示例中,我们将编写一个测试函数,该函数接受一个 Python Unicode 字符,查找其名称,然后根据名称再次查找字符(应该与原始字符匹配):

>>> def unicode_test(value):
...     import unicodedata
...     name = unicodedata.name(value)
...     value2 = unicodedata.lookup(name)
...     print('value="%s", name="%s", value2="%s"' % (value, name, value2))
...

让我们尝试一些字符,首先是一个普通的 ASCII 字母:

>>> unicode_test('A')
value="A", name="LATIN CAPITAL LETTER A", value2="A"

ASCII 标点符号:

>>> unicode_test('$')
value="$", name="DOLLAR SIGN", value2="$"

Unicode 货币字符:

>>> unicode_test('\u00a2')
value="¢", name="CENT SIGN", value2="¢"

另一个 Unicode 货币字符:

>>> unicode_test('\u20ac')
value="€", name="EURO SIGN", value2="€"

你可能遇到的唯一问题是字体显示文本的限制。很少有字体包含所有 Unicode 字符的图像,可能会为缺失的字符显示一些占位符字符。例如,这是 SNOWMAN 的 Unicode 符号,类似于装饰符字体中的符号:

>>> unicode_test('\u2603')
value="☃", name="SNOWMAN", value2="☃"

假设我们想要在 Python 字符串中保存单词 café。一种方法是从文件或网站复制并粘贴它,然后希望它能正常工作:

>>> place = 'café'
>>> place
'café'

这有效是因为我从使用 UTF-8 编码的源复制和粘贴了文本。

如何指定最后的 é 字符?如果你查看 E 的字符索引,你会看到名称 E WITH ACUTE, LATIN SMALL LETTER 具有值 00E9。让我们用刚才玩过的 name()lookup() 函数来检查。首先给出获取名称的代码:

>>> unicodedata.name('\u00e9')
'LATIN SMALL LETTER E WITH ACUTE'

接下来,给出查找代码的名称:

>>> unicodedata.lookup('E WITH ACUTE, LATIN SMALL LETTER')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: "undefined character name 'E WITH ACUTE, LATIN SMALL LETTER'"
注意

Unicode 字符名称索引页上列出的名称已经重新格式化,使其在显示时可以很好地排序。要将它们转换为真实的 Unicode 名称(Python 使用的名称),去掉逗号并将逗号后面的部分移动到开头。因此,将 E WITH ACUTE, LATIN SMALL LETTER 改为 LATIN SMALL LETTER E WITH ACUTE

>>> unicodedata.lookup('LATIN SMALL LETTER E WITH ACUTE')
'é'

现在我们可以通过代码或名称指定字符串 café

>>> place = 'caf\u00e9'
>>> place
'café'
>>> place = 'caf\N{LATIN SMALL LETTER E WITH ACUTE}'
>>> place
'café'

在前面的代码片段中,我们直接在字符串中插入了 é,但我们也可以通过附加构建字符串:

>>> u_umlaut = '\N{LATIN SMALL LETTER U WITH DIAERESIS}'
>>> u_umlaut
'ü'
>>> drink = 'Gew' + u_umlaut + 'rztraminer'
>>> print('Now I can finally have my', drink, 'in a', place)
Now I can finally have my Gewürztraminer in a café

字符串 len() 函数计算 Unicode 字符 数量,而不是字节数:

>>> len('$')
1
>>> len('\U0001f47b')
1
注意

如果你知道 Unicode 的数值 ID,你可以使用标准的 ord()chr() 函数快速转换整数 ID 和单字符 Unicode 字符串:

>>> chr(233)
'é'
>>> chr(0xe9)
'é'
>>> chr(0x1fc6)
'ῆ'

UTF-8

在正常的字符串处理中,你不需要担心 Python 如何存储每个 Unicode 字符。

但是,当你与外界交换数据时,你需要一些东西:

  • 编码 字符字符串为字节的方法

  • 解码 字节到字符字符串的方法

如果 Unicode 中的字符少于 65,536 个,我们可以将每个 Unicode 字符 ID 塞入两个字节中。不幸的是,字符太多了。我们可以将每个 ID 编码为四个字节,但这将使常见文本字符串的内存和磁盘存储空间需求增加四倍。

Ken Thompson 和 Rob Pike,Unix 开发者熟悉的名字,设计了一夜之间在新泽西餐馆的餐垫上的 UTF-8 动态编码方案。它每个 Unicode 字符使用一到四个字节:

  • ASCII 占一个字节

  • 大多数拉丁衍生(但不包括西里尔语)语言需要两个字节

  • 基本多语言平面的其余部分需要三个字节

  • 其余部分包括一些亚洲语言和符号需要四个字节

UTF-8 是 Python、Linux 和 HTML 中的标准文本编码。它快速、全面且运行良好。如果你在代码中始终使用 UTF-8 编码,生活将比试图在各种编码之间跳转要容易得多。

注意

如果你从网页等其他源复制粘贴创建 Python 字符串,请确保源以 UTF-8 格式编码。经常看到将以 Latin-1 或 Windows 1252 编码的文本复制到 Python 字符串中,这将导致后来出现无效字节序列的异常。

编码

你可以将字符串 编码字节。字符串 encode() 函数的第一个参数是编码名称。选择包括 Table 12-1 中的那些。

表 12-1. 编码

编码名称 描述
'ascii' 七比特 ASCII 编码
'utf-8' 八位变长编码,几乎总是你想要使用的
'latin-1' 也称为 ISO 8859-1
'cp-1252' 常见的 Windows 编码
'unicode-escape' Python Unicode 文本格式,\uxxxx\Uxxxxxxxx

你可以将任何东西编码为 UTF-8。让我们将 Unicode 字符串'\u2603'赋给名称snowman

>>> snowman = '\u2603'

snowman是一个 Python Unicode 字符串,只有一个字符,无论内部存储它需要多少字节:

>>> len(snowman)
1

接下来,让我们将这个 Unicode 字符编码为一个字节序列:

>>> ds = snowman.encode('utf-8')

正如我之前提到的,UTF-8 是一种变长编码。在这种情况下,它用三个字节来编码单个snowman Unicode 字符:

>>> len(ds)
3
>>> ds
b'\xe2\x98\x83'

现在,len()返回字节数(3),因为ds是一个bytes变量。

你可以使用除 UTF-8 之外的其他编码,但如果 Unicode 字符串不能被处理,你会得到错误。例如,如果你使用ascii编码,除非你的 Unicode 字符恰好是有效的 ASCII 字符,否则会失败:

>>> ds = snowman.encode('ascii')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character '\u2603'
in position 0: ordinal not in range(128)

encode()函数接受第二个参数,帮助你避免编码异常。它的默认值,在前面的例子中你可以看到,是'strict';如果遇到非 ASCII 字符,它会引发一个UnicodeEncodeError。还有其他编码方式。使用'ignore'来丢弃任何无法编码的内容:

>>> snowman.encode('ascii', 'ignore')
b''

使用'replace'来用?替换未知字符:

>>> snowman.encode('ascii', 'replace')
b'?'

使用'backslashreplace'来生成一个 Python Unicode 字符字符串,比如unicode-escape

>>> snowman.encode('ascii', 'backslashreplace')
b'\\u2603'

如果你需要一个 Unicode 转义序列的可打印版本,你可以使用这个方法。

使用'xmlcharrefreplace'来生成 HTML 安全字符串:

>>> snowman.encode('ascii', 'xmlcharrefreplace')
b'&#9731;'

我在“HTML 实体”中提供了更多 HTML 转换的细节。

解码

我们将字节字符串解码为 Unicode 文本字符串。每当我们从外部来源(文件、数据库、网站、网络 API 等)获取文本时,它都会被编码为字节字符串。棘手的部分是知道实际使用了哪种编码方式,这样我们才能逆向操作并获取 Unicode 字符串。

问题在于字节字符串本身没有说明使用了哪种编码。我之前提到过从网站复制粘贴的危险。你可能访问过一些奇怪字符的网站,本应是普通的 ASCII 字符。

让我们创建一个名为place的 Unicode 字符串,其值为'café'

>>> place = 'caf\u00e9'
>>> place
'café'
>>> type(place)
<class 'str'>

用 UTF-8 格式编码,存入一个名为place_bytesbytes变量:

>>> place_bytes = place.encode('utf-8')
>>> place_bytes
b'caf\xc3\xa9'
>>> type(place_bytes)
<class 'bytes'>

注意place_bytes有五个字节。前三个与 ASCII 相同(UTF-8 的优势),最后两个编码了'é'。现在让我们将该字节字符串解码回 Unicode 字符串:

>>> place2 = place_bytes.decode('utf-8')
>>> place2
'café'

这个方法的原因是我们编码为 UTF-8 并解码为 UTF-8。如果我们告诉它从其他编码解码会怎样呢?

>>> place3 = place_bytes.decode('ascii')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 3:
ordinal not in range(128)

ASCII 解码器因为字节值0xc3在 ASCII 中是非法的而抛出了异常。有些 8 位字符集编码允许 128(十六进制80)到 255(十六进制FF)之间的值合法,但与 UTF-8 不同。

>>> place4 = place_bytes.decode('latin-1')
>>> place4
'café'
>>> place5 = place_bytes.decode('windows-1252')
>>> place5
'café'

唔。

这个故事的教训是:只要可能,请使用 UTF-8 编码。它适用,被到处支持,可以表示每个 Unicode 字符,并且快速解码和编码。

注意

尽管您可以指定任何 Unicode 字符,这并不意味着您的计算机将显示所有这些字符。这取决于您使用的字体,该字体可能对许多字符显示空白或填充图像。苹果为 Unicode 联盟创建了最后的应急字体,并在其自己的操作系统中使用它。这个Wikipedia 页面有更多细节。另一种包含从\u0000\uffff以及更多字符的字体是Unifont

HTML 实体

Python 3.4 增加了另一种转换 Unicode 的方法,但是使用 HTML 字符实体。³ 这可能比查找 Unicode 名称更容易,特别是在您在网络上工作时:

>>> import html
>>> html.unescape("&egrave;")
'è'

这种转换也适用于编号实体,十进制或十六进制:

>>> import html
>>> html.unescape("&#233;")
'é'
>>> html.unescape("&#xe9;")
'é'

您甚至可以将命名实体转换导入为字典并自行进行转换。删除字典键的初始'&'(您也可以删除最后的;,但似乎两种方式都可以工作):

>>> from html.entities import html5
>>> html5["egrave"]
'è'
>>> html5["egrave;"]
'è'

要从单个 Python Unicode 字符向 HTML 实体名称的另一方向转换,请首先使用ord()获取字符的十进制值:

>>> import html
>>> char = '\u00e9'
>>> dec_value = ord(char)
>>> html.entities.codepoint2name[dec_value]
'eacute'

对于超过一个字符的 Unicode 字符串,请使用这两步转换:

>>> place = 'caf\u00e9'
>>> byte_value = place.encode('ascii', 'xmlcharrefreplace')
>>> byte_value
b'caf&#233;'
>>> byte_value.decode()
'caf&#233;'

表达式place.encode('ascii', 'xmlcharrefreplace')返回 ASCII 字符,但是作为类型bytes(因为它是*编码的)。需要以下byte_value.decode()来将byte_value转换为 HTML 兼容字符串。

标准化

一些 Unicode 字符可以用多种 Unicode 编码表示。它们看起来一样,但由于具有不同的内部字节序列,它们不能进行比较。例如,在'café'中,急性重音'é'可以用多种方式制作单个字符'é'

>>> eacute1 = 'é'                              # UTF-8, pasted
>>> eacute2 = '\u00e9'                         # Unicode code point
>>> eacute3 = \                                # Unicode name
...     '\N{LATIN SMALL LETTER E WITH ACUTE}'
>>> eacute4 = chr(233)                         # decimal byte value
>>> eacute5 = chr(0xe9)                        # hex byte value
>>> eacute1, eacute2, eacute3, eacute4, eacute5
('é', 'é', 'é', 'é', 'é')
>>> eacute1 == eacute2 == eacute3 == eacute4 == eacute5
True

尝试几个健全性检查:

>>> import unicodedata
>>> unicodedata.name(eacute1)
'LATIN SMALL LETTER E WITH ACUTE'
>>> ord(eacute1)             # as a decimal integer
233
>>> 0xe9                     # Unicode hex integer
233

现在让我们通过将一个普通的e与一个重音符号结合来制作一个带重音的e

>>> eacute_combined1 = "e\u0301"
>>> eacute_combined2 = "e\N{COMBINING ACUTE ACCENT}"
>>> eacute_combined3 = "e" + "\u0301"
>>> eacute_combined1, eacute_combined2, eacute_combined3
('é', 'é', 'é'))
>>> eacute_combined1 == eacute_combined2 == eacute_combined3
True
>>> len(eacute_combined1)
2

我们用两个字符构建了一个 Unicode 字符,它看起来与原始的'é'相同。但正如他们在芝麻街上所说的那样,其中一个与其他不同:

>>> eacute1 == eacute_combined1
False

如果您有来自不同来源的两个不同的 Unicode 文本字符串,一个使用eacute1,另一个使用eacute_combined1,它们看起来相同,但是神秘地不起作用。

您可以使用unicodedata模块中的normalize()函数修复这个问题:

>>> import unicodedata
>>> eacute_normalized = unicodedata.normalize('NFC', eacute_combined1)
>>> len(eacute_normalized)
1
>>> eacute_normalized == eacute1
True
>>> unicodedata.name(eacute_normalized)
'LATIN SMALL LETTER E WITH ACUTE'

'NFC'的意思是组合的正常形式

更多信息

如果您想了解更多关于 Unicode 的信息,这些链接特别有帮助:

文本字符串:正则表达式

第五章讨论了简单的字符串操作。掌握了这些基础知识后,你可能已经在命令行上使用了简单的“通配符”模式,比如 UNIX 命令 ls *.py,意思是列出所有以 .py 结尾的文件名

是时候通过使用正则表达式来探索更复杂的模式匹配了。这些功能在标准模块 re 中提供。你定义一个要匹配的字符串模式,以及要匹配的字符串。对于简单的匹配,用法如下:

>>> import re
>>> result = re.match('You', 'Young Frankenstein')

在这里,'You' 是我们要查找的模式'Young Frankenstein'(我们要搜索的字符串)。match() 检查是否以模式开头。

对于更复杂的匹配,你可以先编译你的模式以加快后续的匹配速度:

>>> import re
>>> youpattern = re.compile('You')

然后,你可以对编译后的模式执行匹配:

>>> import re
>>> result = youpattern.match('Young Frankenstein')
注意

因为这是一个常见的 Python 陷阱,我在这里再次强调:match() 只匹配从开头开始的模式。search() 则可以在任何位置匹配模式。

match() 不是比较模式和源的唯一方法。以下是你可以使用的几种其他方法(我们在下面的各节中讨论每一种方法):

  • search() 如果有的话返回第一个匹配项。

  • findall() 返回所有非重叠匹配项的列表(如果有的话)。

  • split()中匹配模式并返回字符串片段列表。

  • sub() 还需要另一个替换参数,并将中与模式匹配的所有部分更改为替换

注意

这里大多数正则表达式示例都使用 ASCII,但 Python 的字符串函数,包括正则表达式,可以处理任何 Python 字符串和任何 Unicode 字符。

使用 match() 找到确切的起始匹配

字符串 'Young Frankenstein' 是否以 'You' 开头?以下是带有注释的代码:

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.match('You', source)  # match starts at the beginning of source
>>> if m:  # match returns an object; do this to see what matched
...     print(m.group())
...
You
>>> m = re.match('^You', source) # start anchor does the same
>>> if m:
...     print(m.group())
...
You

'Frank' 怎么样?

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.match('Frank', source)
>>> if m:
...     print(m.group())
...

这次,match() 没有返回任何内容,因此 if 语句没有运行 print 语句。

正如我在 “新功能:我是海象” 中提到的,在 Python 3.8 中,你可以使用所谓的海象操作符简化这个例子:

>>> import re
>>> source = 'Young Frankenstein'
>>> if m := re.match('Frank', source):
...     print(m.group())
...

现在让我们使用 search() 来查看 'Frank' 是否出现在源字符串中:

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.search('Frank', source)
>>> if m:
...      print(m.group())
...
Frank

让我们改变模式,再次尝试使用 match() 进行起始匹配:

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.match('.*Frank', source)
>>> if m:  # match returns an object
...     print(m.group())
...
Young Frank

这里简要解释了我们新的 '.*Frank' 模式的工作原理:

  • . 表示任何单个字符

  • * 表示前一个内容的零个或多个.* 在一起表示任意数量的字符(甚至是零个)。

  • Frank 是我们想要匹配的短语,某个地方。

match() 返回与 .*Frank 匹配的字符串:'Young Frank'

你可以使用 search() 在字符串 'Young Frankenstein' 中找到模式 'Frank' 的任何位置,而不需要使用 .* 通配符:

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.search('Frank', source)
>>> if m:  # search returns an object
...     print(m.group())
...
Frank

使用 findall() 查找所有匹配项

前面的例子只查找了一个匹配。但是如果你想知道字符串中单字母 'n' 的实例数量呢?

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.findall('n', source)
>>> m   # findall returns a list
['n', 'n', 'n', 'n']
>>> print('Found', len(m), 'matches')
Found 4 matches

后面跟着任何字符的 'n' 是怎样的?

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.findall('n.', source)
>>> m
['ng', 'nk', 'ns']

注意它没有匹配最后的 'n'。我们需要说 'n' 后面的字符是可选的,用 ?

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.findall('n.?', source)
>>> m
['ng', 'nk', 'ns', 'n']

使用 split() 在匹配处分割

下一个示例展示了如何通过模式而不是简单字符串(正常字符串 split() 方法会执行的方式)将字符串分割成列表:

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.split('n', source)
>>> m    # split returns a list
['You', 'g Fra', 'ke', 'stei', '']

使用 sub() 替换匹配项

这类似于字符串 replace() 方法,但用于模式而不是字面字符串:

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.sub('n', '?', source)
>>> m   # sub returns a string
'You?g Fra?ke?stei?'

模式:特殊字符

许多正则表达式的描述从如何定义它们的所有细节开始。我认为这是一个错误。正则表达式是一个不那么小的语言,有太多细节无法一次掌握。它们使用了很多标点符号,看起来像卡通人物在咒骂。

掌握了这些表达式 (match()search()findall()sub())之后,让我们深入了解如何构建它们的细节。你制作的模式适用于这些函数中的任何一个。

你已经了解了基础知识:

  • 匹配所有非特殊字符的文字

  • \n 外的任何单个字符用 .

  • 任意数量的前一个字符(包括零)用 *

  • 前一个字符的可选(零次或一次)用 ?

首先,特殊字符显示在 表 12-2 中。

表 12-2. 特殊字符

模式 匹配项
\d 单个数字
\D 单个非数字字符
\w 字母数字字符
\W 非字母数字字符
\s 空白字符
\S 非空白字符
\b 单词边界(在 \w\W 之间,顺序不限)
\B 非单词边界

Python 的 string 模块有预定义的字符串常量,我们可以用它们进行测试。让我们使用 printable,其中包含 100 个可打印的 ASCII 字符,包括大小写字母、数字、空格字符和标点符号:

>>> import string
>>> printable = string.printable
>>> len(printable)
100
>>> printable[0:50]
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN'
>>> printable[50:]
'OPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'

printable 中哪些字符是数字?

>>> re.findall('\d', printable)
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

哪些字符是数字、字母或下划线?

>>> re.findall('\w', printable)
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b',
'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', '_']

哪些是空格?

>>> re.findall('\s', printable)
[' ', '\t', '\n', '\r', '\x0b', '\x0c']

按顺序,这些是:普通空格、制表符、换行符、回车符、垂直制表符和换页符。

正则表达式不仅限于 ASCII。\d 将匹配任何 Unicode 所谓的数字,而不仅仅是 ASCII 字符 '0''9'。让我们从 FileFormat.info 添加两个非 ASCII 小写字母:

在这个测试中,我们将加入以下内容:

  • 三个 ASCII 字母

  • 三个标点符号不应该与 \w 匹配

  • Unicode 带抑音的拉丁小写字母 E (\u00ea)

  • Unicode 带抑音的拉丁小写字母 E (\u0115)

>>> x = 'abc' + '-/*' + '\u00ea' + '\u0115'

如预期,这个模式仅找到了字母:

>>> re.findall('\w', x)
['a', 'b', 'c', 'ê', 'ĕ']

模式:使用限定符

现在让我们制作“标点披萨”,使用正则表达式的主要模式限定符,这些限定符在 表 12-3 中介绍。

在表中,expr和其他斜体字表示任何有效的正则表达式。

表 12-3。模式说明符

模式 匹配
abc 字面abc
( expr ) expr
expr1 &#124; expr2 expr1expr2
. \n外的任意字符
^ 源字符串的开头
$ 源字符串的结尾
prev ? 零或一个prev
prev * 零或多个prev,尽可能多地匹配
prev *? 零或多个prev,尽可能少地匹配
prev + 一个或多个prev,尽可能多地匹配
prev +? 一个或多个prev,尽可能少地匹配
prev { m } m个连续的prev
prev { m, n } mn个连续的prev,尽可能多地匹配
prev { m, n }? mn个连续的prev,尽可能少地匹配
[ abc ] abc(等同于a&#124;b&#124;c
[^ abc ] abc
prev (?= next ) 若紧随其后则prev
prev (?! next ) 若不紧随其后则prev
(?<= prev ) next 若之前有prevnext
(?<! prev ) next 若不紧随其前则next

当试图阅读这些示例时,你的眼睛可能永久地交叉了。首先,让我们定义我们的源字符串:

>>> source = '''I wish I may, I wish I might
... Have a dish of fish tonight.'''

现在我们应用不同的正则表达式模式字符串来尝试在source字符串中匹配某些内容。

注意

在以下示例中,我使用普通的引号字符串表示模式。在本节稍后,我将展示如何使用原始模式字符串(在初始引号前加上r)来避免 Python 正常字符串转义与正则表达式转义之间的冲突。因此,为了更安全,所有以下示例中的第一个参数实际上应该是原始字符串。

首先,在任意位置找到wish

>>> re.findall('wish', source)
['wish', 'wish']

接下来,在任意位置找到wishfish

>>> re.findall('wish|fish', source)
['wish', 'wish', 'fish']

查找开头的wish

>>> re.findall('^wish', source)
[]

查找开头的I wish

>>> re.findall('^I wish', source)
['I wish']

查找结尾的fish

>>> re.findall('fish$', source)
[]

最后,在结尾找到fish tonight.

>>> re.findall('fish tonight.$', source)
['fish tonight.']

字符^$称为锚点^锚定搜索到搜索字符串的开始,而$锚定到结尾。. $匹配行尾的任意字符,包括句号,所以它起作用了。为了更精确,我们应该转义句点以确实匹配它:

>>> re.findall('fish tonight\.$', source)
['fish tonight.']

从找到wf后面跟着ish开始:

>>> re.findall('[wf]ish', source)
['wish', 'wish', 'fish']

查找一个或多个wsh的连续序列:

>>> re.findall('[wsh]+', source)
['w', 'sh', 'w', 'sh', 'h', 'sh', 'sh', 'h']

找到以非字母数字字符跟随的ght

>>> re.findall('ght\W', source)
['ght\n', 'ght.']

找到以I开头的wish

>>> re.findall('I (?=wish)', source)
['I ', 'I ']

最后,wish之前有I

>>> re.findall('(?<=I) wish', source)
[' wish', ' wish']

我之前提到过,有几种情况下正则表达式模式规则与 Python 字符串规则相冲突。以下模式应匹配以fish开头的任何单词:

>>> re.findall('\bfish', source)
[]

为什么不这样做呢?如第五章中所述,Python 为字符串使用了一些特殊的转义字符。例如,\b 在字符串中表示退格,但在正则表达式的迷你语言中表示单词的开头。通过在定义正则表达式字符串时始终在其前加上 r 字符,可以避免意外使用转义字符,这样将禁用 Python 转义字符,如下所示:

>>> re.findall(r'\bfish', source)
['fish']

模式:指定 match() 输出

在使用 match()search() 时,所有匹配项都作为结果对象 mm.group() 返回。如果将模式括在括号中,则匹配将保存到自己的组中,并作为 m.groups() 的元组可用,如下所示:

>>> m = re.search(r'(. dish\b).*(\bfish)', source)
>>> m.group()
'a dish of fish'
>>> m.groups()
('a dish', 'fish')

如果使用此模式 (?P< name > expr ),它将匹配 expr,并将匹配保存在组 name 中:

>>> m = re.search(r'(?P<DISH>. dish\b).*(?P<FISH>\bfish)', source)
>>> m.group()
'a dish of fish'
>>> m.groups()
('a dish', 'fish')
>>> m.group('DISH')
'a dish'
>>> m.group('FISH')
'fish'

二进制数据

文本数据可能会有挑战,但二进制数据可能会更加有趣。您需要了解诸如字节顺序(计算机处理器如何将数据分解为字节)和整数的符号位等概念。您可能需要深入了解二进制文件格式或网络数据包,以提取甚至更改数据。本节向您展示了在 Python 中进行二进制数据处理的基础知识。

bytes 和 bytearray

Python 3 引入了以下八位整数序列,可能值为 0 到 255,有两种类型:

  • bytes 不可变,类似于字节元组

  • bytearray 可变,类似于字节列表

以名为 blist 的列表开始,下一个示例创建了名为 the_bytesbytes 变量和名为 the_byte_arraybytearray 变量:

>>> blist = [1, 2, 3, 255]
>>> the_bytes = bytes(blist)
>>> the_bytes
b'\x01\x02\x03\xff'
>>> the_byte_array = bytearray(blist)
>>> the_byte_array
bytearray(b'\x01\x02\x03\xff')
注意

bytes 值的表示以 b 和引号字符开头,后跟诸如 \x02 或 ASCII 字符的十六进制序列,并以匹配的引号字符结束。Python 将十六进制序列或 ASCII 字符转换为小整数,但显示也有效的 ASCII 编码的字节值作为 ASCII 字符:

>>> b'\x61'
b'a'
>>> b'\x01abc\xff'
b'\x01abc\xff'

下一个示例演示了您不能更改 bytes 变量:

>>> blist = [1, 2, 3, 255]
>>> the_bytes = bytes(blist)
>>> the_bytes[1] = 127
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment

bytearray 变量温和且可变:

>>> blist = [1, 2, 3, 255]
>>> the_byte_array = bytearray(blist)
>>> the_byte_array
bytearray(b'\x01\x02\x03\xff')
>>> the_byte_array[1] = 127
>>> the_byte_array
bytearray(b'\x01\x7f\x03\xff')

这些操作会生成一个包含 0 到 255 的 256 元素结果:

>>> the_bytes = bytes(range(0, 256))
>>> the_byte_array = bytearray(range(0, 256))

在打印 bytesbytearray 数据时,Python 使用 \xxx 表示不可打印字节及其 ASCII 等效字符,对于可打印字符则显示其 ASCII 值(以及一些常见的转义字符,例如 \n 而非 \x0a)。以下是手动重新格式化以显示每行 16 个字节的 the_bytes 的打印表示:

>>> the_bytes
b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f
\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f
!"#$%&\'()*+,-./
0123456789:;<=>?
@ABCDEFGHIJKLMNO
PQRSTUVWXYZ[\\]^_
`abcdefghijklmno
pqrstuvwxyz{|}~\x7f
\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f
\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f
\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf
\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf
\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf
\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf
\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef
\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff'

这可能会令人困惑,因为它们是字节(小整数),而不是字符。

使用 struct 转换二进制数据

正如您所见,Python 有许多用于操作文本的工具。用于二进制数据的工具则较少。标准库包含了处理类似于 C 和 C++ 中结构体的数据的 struct 模块。使用 struct,您可以将二进制数据转换为 Python 数据结构,反之亦然。

我们来看看如何处理来自 PNG 文件的数据——一种常见的图像格式,通常与 GIF 和 JPEG 文件一起出现。我们将编写一个小程序,从一些 PNG 数据中提取图像的宽度和高度。

我们将使用奥莱利的商标——在 图 12-1 中展示的小眼睛猫熊。

inp2 1201

图 12-1. 奥莱利猫熊

此图像的 PNG 文件可在 维基百科 上找到。在 第 14 章 我才会介绍如何读取文件,因此我下载了这个文件,编写了一个小程序将其值作为字节打印出来,并只在一个名为 data 的 Python bytes 变量中键入了前 30 个字节的值,用于接下来的示例中。(PNG 格式规范指出宽度和高度存储在前 24 字节中,因此我们现在不需要更多。)

>>> import struct
>>> valid_png_header = b'\x89PNG\r\n\x1a\n'
>>> data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR' + \
...     b'\x00\x00\x00\x9a\x00\x00\x00\x8d\x08\x02\x00\x00\x00\xc0'
>>> if data[:8] == valid_png_header:
...     width, height = struct.unpack('>LL', data[16:24])
...     print('Valid PNG, width', width, 'height', height)
... else:
...     print('Not a valid PNG')
...
Valid PNG, width 154 height 141

以下是此代码的功能:

  • data 包含来自 PNG 文件的前 30 个字节。为了适应页面,我用 + 和续行符(\)连接了两个字节字符串。

  • valid_png_header 包含标记有效 PNG 文件起始的八字节序列。

  • width 从第 16 至 19 字节提取,height 从第 20 至 23 字节提取。

>LL 是格式字符串,指示 unpack() 如何解释其输入字节序列并将其组装成 Python 数据类型。以下是详细说明:

  • > 表示整数以 大端 格式存储。

  • 每个 L 指定一个四字节无符号长整数。

你可以直接检查每个四字节值:

>>> data[16:20]
b'\x00\x00\x00\x9a'
>>> data[20:24]0x9a
b'\x00\x00\x00\x8d'

大端整数将最重要的字节放在左边。因为宽度和高度都小于 255,它们适合每个序列的最后一个字节。你可以验证这些十六进制值是否与预期的十进制值匹配:

>>> 0x9a
154
>>> 0x8d
141

当你想反向操作并将 Python 数据转换为字节时,请使用 struct pack() 函数:

>>> import struct
>>> struct.pack('>L', 154)
b'\x00\x00\x00\x9a'
>>> struct.pack('>L', 141)
b'\x00\x00\x00\x8d'

表 12-4 和 12-5 显示了 pack()unpack() 的格式说明符。

字节顺序说明符在格式字符串中优先。

表 12-4. 字节顺序说明符

格式说明符 字节顺序
< 小端
> 大端

表 12-5. 格式说明符

格式说明符 描述 字节
x 跳过一个字节 1
b 有符号字节 1
B 无符号字节 1
h 有符号短整数 2
H 无符号短整数 2
i 有符号整数 4
I 无符号整数 4
l 有符号长整数 4
L 无符号长整数 4
Q 无符号长长整数 8
f 单精度浮点数 4
d 双精度浮点数 8
p count 和字符 1 + count
s 字符 count

类型说明符跟在字节顺序字符之后。任何说明符前都可以加一个数字,表示 count5B 等同于 BBBBB

你可以使用 count 前缀代替 >LL

>>> struct.unpack('>2L', data[16:24])
(154, 141)

我们使用切片 data[16:24] 直接抓取感兴趣的字节。我们也可以使用 x 标识符来跳过不感兴趣的部分:

>>> struct.unpack('>16x2L6x', data)
(154, 141)

这意味着:

  • 使用大端整数格式 (>)

  • 跳过 16 个字节 (16x)

  • 读取八个字节——两个无符号长整数 (2L)

  • 跳过最后六个字节 (6x)

其他二进制数据工具

一些第三方开源软件包提供了以下更具声明性的方法来定义和提取二进制数据:

附录 B 中详细介绍了如何下载和安装外部包,例如这些。在下一个示例中,您需要安装 construct。这是您需要做的全部工作:

$ pip install construct

以下是如何通过使用 construct 从我们的 data 字节串中提取 PNG 尺寸的方法:

>>> from construct import Struct, Magic, UBInt32, Const, String
>>> # adapted from code at https://github.com/construct
>>> fmt = Struct('png',
...     Magic(b'\x89PNG\r\n\x1a\n'),
...     UBInt32('length'),
...     Const(String('type', 4), b'IHDR'),
...     UBInt32('width'),
...     UBInt32('height')
...     )
>>> data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR' + \
...     b'\x00\x00\x00\x9a\x00\x00\x00\x8d\x08\x02\x00\x00\x00\xc0'
>>> result = fmt.parse(data)
>>> print(result)
Container:
 length = 13
 type = b'IHDR'
 width = 154
 height = 141
>>> print(result.width, result.height)
154, 141

使用 binascii() 转换字节/字符串

标准的 binascii 模块具有将二进制数据与各种字符串表示形式(十六进制(基数 16)、Base64、uuencoded 等)之间转换的函数。例如,在下一段代码中,让我们将那八字节的 PNG 头部打印为一系列十六进制值,而不是 Python 用来显示 bytes 变量的混合 ASCII 和 \x xx 转义的方式:

>>> import binascii
>>> valid_png_header = b'\x89PNG\r\n\x1a\n'
>>> print(binascii.hexlify(valid_png_header))
b'89504e470d0a1a0a'

嘿,这个东西也可以反向操作:

>>> print(binascii.unhexlify(b'89504e470d0a1a0a'))
b'\x89PNG\r\n\x1a\n'

位运算符

Python 提供了类似 C 语言的位级整数操作符。表 12-6 总结了这些操作符,并包括对整数变量 x(十进制 5,二进制 0b0101)和 y(十进制 1,二进制 0b0001)的示例。

表 12-6. 位级整数运算符

操作符 描述 示例 十进制结果 二进制结果
& x & y 1 0b0001
&#124; x &#124; y 5 0b0101
^ 异或 x ^ y 4 0b0100
~ 反转位 ~x -6 二进制表示取决于整数大小
<< 左移 x << 1 10 0b1010
>> 右移 x >> 1 2 0b0010

这些操作符的工作方式类似于 第 8 章 中的集合操作符。& 操作符返回两个参数中相同的位,| 返回两个参数中设置的位。^ 操作符返回一个参数中的位,而不是两者都有的位。~ 操作符反转其单个参数中的所有位;这也反转了符号,因为整数的最高位在 二进制补码 算术中表示其符号(1 = 负数),这种算法在所有现代计算机中使用。<<>> 操作符只是将位向左或向右移动。向左移动一位与乘以二相同,向右移动相当于除以二。

一个珠宝类比

Unicode 字符串就像魅力手链,而字节则像串珠。

即将到来

接下来是另一个实用章节:如何处理日期和时间。

待办事项

12.1 创建一个名为mystery的 Unicode 字符串,并将其赋值为'\U0001f984'。打印mystery及其 Unicode 名称。

12.2 使用 UTF-8 对mystery进行编码,并将结果存入名为pop_bytesbytes变量中。打印pop_bytes

12.3 使用 UTF-8 将pop_bytes解码为字符串变量pop_string。打印pop_stringpop_string等于mystery吗?

12.4 当您处理文本时,正则表达式非常方便。我们将以多种方式应用它们到我们特色的文本样本中。这是一首名为“Ode on the Mammoth Cheese”的诗,由詹姆斯·麦金泰尔于 1866 年写作,致敬于一个重七千磅的奶酪,在安大略省制作并发送国际巡回展。如果您不想全部输入,请使用您喜爱的搜索引擎并将单词剪切并粘贴到 Python 程序中,或者直接从Project Gutenberg获取。将文本字符串命名为mammoth

例子 12-1. mammoth.txt
We have seen thee, queen of cheese,
Lying quietly at your ease,
Gently fanned by evening breeze,
Thy fair form no flies dare seize.

All gaily dressed soon you'll go
To the great Provincial show,
To be admired by many a beau
In the city of Toronto.

Cows numerous as a swarm of bees,
Or as the leaves upon the trees,
It did require to make thee please,
And stand unrivalled, queen of cheese.

May you not receive a scar as
We have heard that Mr. Harris
Intends to send you off as far as
The great world's show at Paris.

Of the youth beware of these,
For some of them might rudely squeeze
And bite your cheek, then songs or glees
We could not sing, oh! queen of cheese.

We'rt thou suspended from balloon,
You'd cast a shade even at noon,
Folks would think it was the moon
About to fall and crush them soon.

12.5 导入re模块以使用 Python 的正则表达式函数。使用re.findall()打印所有以c开头的单词。

12.6 找出所有以c开头的四字单词。

12.7 找出所有以r结尾的单词。

12.8 找出所有包含恰好三个连续元音字母的单词。

12.9 使用unhexlify将这个十六进制字符串(从两个字符串组合成一个以适应页面)转换为名为gifbytes变量:

'47494638396101000100800000000000ffffff21f9' +
'0401000000002c000000000100010000020144003b'

12.10 gif中的字节定义了一个像素的透明 GIF 文件,这是最常见的图形文件格式之一。合法的 GIF 以 ASCII 字符GIF89a开头。gif是否符合这个规范?

12.11 GIF 的像素宽度是从字节偏移量 6 开始的 16 位小端整数,高度也是相同大小,从偏移量 8 开始。提取并打印这些值以供gif使用。它们都是1吗?

¹ 这种酒在德国有一个分音符号,但在去法国的途中在阿尔萨斯地区失去了它。

² 基数 16,由字符0-9A-F指定。

³ 参见 HTML5 命名字符引用图表

第十三章:日历和钟表

“一!”在钟楼塔上的钟敲响,

前不久不过六十分钟前

十二点的钟声响起。

Frederick B. Needham,《钟的轮回》

我有日历但我从未准时过。

玛丽莲·梦露

程序员花费了令人惊讶的精力处理日期和时间。让我们谈谈他们遇到的一些问题,然后介绍一些最佳实践和技巧,使情况稍微不那么混乱。

日期可以用多种方式表示——实际上太多了。即使在使用罗马日历的英语中,你也会看到简单日期的许多变体:

  • July 21 1987

  • 21 Jul 1987

  • 21/7/1987

  • 7/21/1987

在其他问题中,日期表示可能会产生歧义。在前面的示例中,很容易确定 7 是月份,21 是月中的日期,因为月份不会达到 21 日。但是 1/6/2012 是指 1 月 6 日还是 6 月 1 日呢?

月份名称在罗马日历中在不同的语言中会有所变化。甚至年份和月份在其他文化中的定义也可能有所不同。

时间也有它们自己的烦恼来源,特别是由于时区和夏令时。如果你看一下时区地图,这些区域遵循的是政治和历史边界,而不是每 15 度(360 度 / 24)经度上的清晰分界线。并且各国在一年中开始和结束夏令时的日期也不同。南半球国家在北半球朋友结束夏令时时,他们自己的时钟也在前进,反之亦然。

Python 的标准库包括许多日期和时间模块,包括:datetimetimecalendardateutil 等。它们有些重叠,并且有些令人困惑。

闰年

闰年是时间的一个特殊问题。你可能知道每四年是一个闰年(夏季奥运会和美国总统选举)。你还知道每 100 年不是闰年,但每 400 年是吗?下面是测试各年份是否为闰年的代码:

>>> import calendar
>>> calendar.isleap(1900)
False
>>> calendar.isleap(1996)
True
>>> calendar.isleap(1999)
False
>>> calendar.isleap(2000)
True
>>> calendar.isleap(2002)
False
>>> calendar.isleap(2004)
True

对于好奇的人:

  • 一年有 365.242196 天(绕太阳一周后,地球大约从其轴上旋转四分之一)

  • 每四年增加一天。现在平均一年有 365.242196 - 0.25 = 364.992196 天

  • 每 100 年减少一天。现在平均一年有 364.992196 + 0.01 = 365.002196 天

  • 每 400 年增加一天。现在平均一年有 365.002196 - 0.0025 = 364.999696 天

暂且这样吧!我们不会谈论闰秒

datetime 模块

标准的 datetime 模块处理(这应该不会让你感到惊讶)日期和时间。它定义了四个主要的对象类,每个类都有许多方法:

  • date 用于年、月和日

  • time 用于小时、分钟、秒和小数

  • datetime 用于日期和时间的组合

  • timedelta 用于日期和/或时间间隔

你可以通过指定年、月和日来创建一个 date 对象。这些值随后可作为属性使用:

>>> from datetime import date
>>> halloween = date(2019, 10, 31)
>>> halloween
datetime.date(2019, 10, 31)
>>> halloween.day
31
>>> halloween.month
10
>>> halloween.year
2019

您可以使用其 isoformat() 方法打印一个 date

>>> halloween.isoformat()
'2019-10-31'

iso 指的是 ISO 8601,这是一个国际标准,用于表示日期和时间。它从最一般的(年)到最具体的(日)进行排序。因此,它也正确排序:按年、月、日排序。我通常选择这种格式来表示程序中的日期,并用于按日期保存数据的文件名。下一节描述了更复杂的 strptime()strftime() 方法,用于解析和格式化日期。

此示例使用 today() 方法生成今天的日期:

>>> from datetime import date
>>> now = date.today()
>>> now
datetime.date(2019, 4, 5)

这个示例利用了一个 timedelta 对象,将一些时间间隔添加到一个 date 中:

>>> from datetime import timedelta
>>> one_day = timedelta(days=1)
>>> tomorrow = now + one_day
>>> tomorrow
datetime.date(2019, 4, 6)
>>> now + 17*one_day
datetime.date(2019, 4, 22)
>>> yesterday = now - one_day
>>> yesterday
datetime.date(2019, 4, 4)

date 的范围是从 date.min(年=1,月=1,日=1)到 date.max(年=9999,月=12,日=31)。因此,您不能将其用于历史或天文计算。

datetime 模块的 time 对象用于表示一天中的时间:

>>> from datetime import time
>>> noon = time(12, 0, 0)
>>> noon
datetime.time(12, 0)
>>> noon.hour
12
>>> noon.minute
0
>>> noon.second
0
>>> noon.microsecond
0

参数从最大的时间单位(小时)到最小的(微秒)进行。如果您没有提供所有参数,time 将假定其余的都是零。顺便说一句,仅因为您可以存储和检索微秒,并不意味着您可以精确到微秒从计算机检索时间。次秒测量的准确性取决于硬件和操作系统中的许多因素。

datetime 对象包含日期和时间。您可以直接创建一个,例如接下来的一个,用于 2019 年 1 月 2 日凌晨 3 点 04 分,加上 5 秒和 6 微秒:

>>> from datetime import datetime
>>> some_day = datetime(2019, 1, 2, 3, 4, 5, 6)
>>> some_day
datetime.datetime(2019, 1, 2, 3, 4, 5, 6)

datetime 对象还有一个 isoformat() 方法:

>>> some_day.isoformat()
'2019-01-02T03:04:05.000006'

那个中间的 T 分隔了日期和时间部分。

datetime 有一个 now() 方法返回当前日期和时间:

>>> from datetime import datetime
>>> now = datetime.now()
>>> now
datetime.datetime(2019, 4, 5, 19, 53, 7, 580562)
>>> now.year
2019
>>> now.month
4
>>> now.day
5
>>> now.hour
19
>>> now.minute
53
>>> now.second
7
>>> now.microsecond
580562

您可以将 date 对象和 time 对象组合成 datetime

>>> from datetime import datetime, time, date
>>> noon = time(12)
>>> this_day = date.today()
>>> noon_today = datetime.combine(this_day, noon)
>>> noon_today
datetime.datetime(2019, 4, 5, 12, 0)

您可以使用 date()time() 方法从 datetime 中提取 datetime

>>> noon_today.date()
datetime.date(2019, 4, 5)
>>> noon_today.time()
datetime.time(12, 0)

使用时间模块

令人困惑的是,Python 有一个带有 time 对象的 datetime 模块,以及一个单独的 time 模块。此外,time 模块有一个名为——等待它——time() 的函数。

表示绝对时间的一种方法是计算自某个起始点以来的秒数。Unix 时间 使用自 1970 年 1 月 1 日午夜以来的秒数。这个值通常称为 时代,通常是在系统之间交换日期和时间的最简单方法。

time 模块的 time() 函数返回当前时间的时代值:

>>> import time
>>> now = time.time()
>>> now
1554512132.778233

自 1970 年元旦以来已经过去了超过十亿秒。时间都去哪了?

您可以通过使用 ctime() 将时代值转换为字符串:

>>> time.ctime(now)
'Fri Apr  5 19:55:32 2019'

在下一节中,您将看到如何生成更吸引人的日期和时间格式。

epoch 值是与不同系统交换日期和时间的有用的最小公分母,如 JavaScript。然而,有时候你需要实际的天数、小时等,time 提供了 struct_time 对象。localtime() 提供系统时区的时间,而 gmtime() 提供 UTC 的时间:

>>> time.localtime(now)
time.struct_time(tm_year=2019, tm_mon=4, tm_mday=5, tm_hour=19,
tm_min=55, tm_sec=32, tm_wday=4, tm_yday=95, tm_isdst=1)
>>> time.gmtime(now)
time.struct_time(tm_year=2019, tm_mon=4, tm_mday=6, tm_hour=0,
tm_min=55, tm_sec=32, tm_wday=5, tm_yday=96, tm_isdst=0)

我的 19:55(中部时区,夏令时)在 UTC 的下一天的 00:55(以前称为 格林威治时间Zulu 时间)。如果省略 localtime()gmtime() 的参数,它们将使用当前时间。

struct_time 中的一些 tm_... 值可能有些模糊,因此请查看 表 13-1 获取更多详细信息。

表格 13-1. struct_time 的值

索引 名称 意义
0 tm_year 年份 00009999
1 tm_mon 月份 112
2 tm_mday 月份的某一天 131
3 tm_hour 小时 023
4 tm_min 分钟 059
5 tm_sec 061
6 tm_wday 星期几 0(周一)到 6(周日)
7 tm_yday 年内天数 1366
8 tm_isdst 夏令时? 0 = 否,1 = 是,-1 = 不明

如果你不想输入所有 tm_... 的名称,struct_time 也像一个命名元组一样(见 “命名元组”),所以你可以使用前面表格中的索引:

>>> import time
>>> now = time.localtime()
>>> now
time.struct_time(tm_year=2019, tm_mon=6, tm_mday=23, tm_hour=12,
tm_min=12, tm_sec=24, tm_wday=6, tm_yday=174, tm_isdst=1)
>>> now[0]
2019
print(list(now[x] for x in range(9)))
[2019, 6, 23, 12, 12, 24, 6, 174, 1]

mktime() 则反过来,将 struct_time 对象转换为 epoch 秒数:

>>> tm = time.localtime(now)
>>> time.mktime(tm)
1554512132.0

这与我们之前的 now() 的 epoch 值不完全匹配,因为 struct_time 对象仅保留到秒。

注意

一些建议:在可能的情况下,使用协调世界时 (UTC) 而不是时区。UTC 是一个绝对时间,独立于时区。如果你有一个服务器,将其时间设置为 UTC;不要使用本地时间。

更多建议:尽量避免使用夏令时。如果可以避免使用夏令时,一个时期的一个小时会消失(“提前”),而在另一个时期会出现两次(“倒退”)。出于某种原因,许多组织在计算机系统中使用本地时间和夏令时,但每年两次都会被那个神秘的小时弄得迷惑不解。

读写日期和时间

isoformat() 并不是写入日期和时间的唯一方式。你已经在 time 模块中看到了 ctime() 函数,你可以用它来将 epoch 转换为字符串:

>>> import time
>>> now = time.time()
>>> time.ctime(now)
'Fri Apr  5 19:58:23 2019'

你还可以通过使用 strftime() 将日期和时间转换为字符串。这作为 datetimedatetime 对象的一个方法提供,也作为 time 模块的一个函数提供。strftime() 使用格式字符串来指定输出,你可以在 表 13-2 中看到。

表格 13-2. strftime() 的输出格式说明符

格式字符串 日期/时间单元 范围
%Y 年份 1900-…
%m 月份 01-12
%B 月份名称 January, …
%b 月份缩写 Jan, …
%d 日期 01-31
%A 星期名称 Sunday, …
a 缩写星期 Sun, …
%H 小时(24 小时制) 00-23
%I 小时(12 小时制) 01-12
%p 上午/下午 AM, PM
%M 分钟 00-59
%S 00-59

数字在左侧补零。

这是由time模块提供的strftime()函数。它将struct_time对象转换为字符串。我们首先定义格式字符串fmt,稍后再次使用它:

>>> import time
>>> fmt = "It's %A, %B %d, %Y, local time %I:%M:%S%p"
>>> t = time.localtime()
>>> t
time.struct_time(tm_year=2019, tm_mon=3, tm_mday=13, tm_hour=15,
tm_min=23, tm_sec=46, tm_wday=2, tm_yday=72, tm_isdst=1)
>>> time.strftime(fmt, t)
"It's Wednesday, March 13, 2019, local time 03:23:46PM"

如果我们尝试使用date对象,只有日期部分会生效,时间默认为午夜:

>>> from datetime import date
>>> some_day = date(2019, 7, 4)
>>> fmt = "It's %A, %B %d, %Y, local time %I:%M:%S%p"
>>> some_day.strftime(fmt)
"It's Thursday, July 04, 2019, local time 12:00:00AM"

对于time对象,只有时间部分会被转换:

>>> from datetime import time
>>> fmt = "It's %A, %B %d, %Y, local time %I:%M:%S%p"
>>> some_time = time(10, 35)
>>> some_time.strftime(fmt)
"It's Monday, January 01, 1900, local time 10:35:00AM"

您不会想使用time对象的日部分,因为它们没有意义。

要反向转换并将字符串转换为日期或时间,请使用具有相同格式字符串的strptime()。没有正则表达式模式匹配;字符串的非格式部分(没有%)需要完全匹配。让我们指定一个与--匹配的格式,例如2019-01-29。如果要解析的日期字符串中有空格而不是破折号会发生什么?

>>> import time
>>> fmt = "%Y-%m-%d"
>>> time.strptime("2019 01 29", fmt)
Traceback (most recent call last):
 File "<stdin>",
 line 1, in <module>
 File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/_strptime.py",
 line 571, in _strptime_time
 tt = _strptime(data_string, format)[0]
 File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/_strptime.py",
 line 359, in _strptime(data_string, format))
ValueError: time data '2019 01 29' does not match format '%Y-%m-%d

如果我们给strptime()输入一些破折号,它会开心吗?

>>> import time
>>> fmt = "%Y-%m-%d"
>>> time.strptime("2019-01-29", fmt)
time.struct_time(tm_year=2019, tm_mon=1, tm_mday=29, tm_hour=0,
tm_min=0, tm_sec=0, tm_wday=1, tm_yday=29, tm_isdst=-1)

或者修复fmt字符串以匹配日期字符串:

>>> import time
>>> fmt = "%Y %m %d"
>>> time.strptime("2019 01 29", fmt)
time.struct_time(tm_year=2019, tm_mon=1, tm_mday=29, tm_hour=0,
tm_min=0, tm_sec=0, tm_wday=1, tm_yday=29, tm_isdst=-1)

即使字符串似乎与其格式匹配,如果值超出范围,将引发异常(文件名因空间而被截断):

>>> time.strptime("2019-13-29", fmt)
Traceback (most recent call last):
 File "<stdin>",
 line 1, in <module>
 File ".../3.7/lib/python3.7/_strptime.py",
 line 571, in _strptime_time
 tt = _strptime(data_string, format)[0]
 File ".../3.7/lib/python3.7/_strptime.py",
 line 359, in _strptime(data_string, format))
ValueError: time data '2019-13-29' does not match format '%Y-%m-%d

名称特定于您的locale——操作系统的国际化设置。如果您需要打印不同的月份和日期名称,请通过使用setlocale()来更改您的 locale;它的第一个参数是用于日期和时间的locale.LC_TIME,第二个参数是一个字符串,结合语言和国家缩写。让我们邀请一些国际朋友参加万圣节聚会。我们将用美国英语、法语、德语、西班牙语和冰岛语(冰岛人真的有精灵)打印月份、日期和星期几:

>>> import locale
>>> from datetime import date
>>> halloween = date(2019, 10, 31)
>>> for lang_country in ['en_us', 'fr_fr', 'de_de', 'es_es', 'is_is',]:
...     locale.setlocale(locale.LC_TIME, lang_country)
...     halloween.strftime('%A, %B %d')
...
'en_us'
'Thursday, October 31'
'fr_fr'
'Jeudi, octobre 31'
'de_de'
'Donnerstag, Oktober 31'
'es_es'
'jueves, octubre 31'
'is_is'
'fimmtudagur, október 31'
>>>

您从哪里找到这些lang_country的魔法值?这有点奇怪,但您可以尝试这样做以获取所有这些值(有几百个):

>>> import locale
>>> names = locale.locale_alias.keys()

names中,让我们获取看起来可以与setlocale()一起使用的区域设置名称,例如我们在前面示例中使用的那些——两个字符的语言代码,后面跟着下划线和两个字符的国家代码

>>> good_names = [name for name in names if \
len(name) == 5 and name[2] == '_']

前五个长什么样?

>>> good_names[:5]
['sr_cs', 'de_at', 'nl_nl', 'es_ni', 'sp_yu']

因此,如果您想要所有的德语语言区域设置,请尝试这样做:

>>> de = [name for name in good_names if name.startswith('de')]
>>> de
['de_at', 'de_de', 'de_ch', 'de_lu', 'de_be']
注意

如果运行set_locale()并出现错误

locale.Error: unsupported locale setting

该 locale 不受您的操作系统支持。您需要弄清楚您的操作系统需要添加什么。即使 Python 告诉您(使用locale.locale_alias.keys())这是一个好的 locale,我在使用cy_gb(威尔士语,英国)locale 时在 macOS 上遇到过此错误,即使它之前接受了is_is(冰岛语)的情况也是如此。

所有的转换

图 13-1(来自 Python wiki)总结了所有标准 Python 时间转换。

inp2 1301

图 13-1. 日期和时间转换

替代模块

如果你觉得标准库模块混乱,或者缺少你想要的特定转换,那么有许多第三方替代方案。以下是其中几个:

arrow

将许多日期和时间函数与简单的 API 结合起来。

dateutil

几乎可以解析任何日期格式,并且良好处理相对日期和时间。

iso8601

填补了 ISO8601 格式在标准库中的空白。

fleming

许多时区功能。

maya

直观的日期、时间和间隔接口。

dateinfer

从日期/时间字符串中猜测正确的格式字符串。

即将到来

文件和目录也需要关爱。

要做的事情

13.1 将当前日期作为字符串写入文本文件 today.txt

13.2 将文本文件 today.txt 读入字符串 today_string 中。

13.3 解析来自 today_string 的日期。

13.4 创建你的生日的日期对象。

13.5 你的生日是星期几?

13.6 你将(或者已经)十千天岁时是哪天?

¹ 大约是 Unix 诞生的起点,忽略那些烦人的闰秒。

第十四章:文件和目录。

我有文件,我有电脑文件,你知道的,在纸上也有文件。但大部分都在我脑子里。所以如果我的脑子出了问题,上帝帮帮我!

乔治·R·R·马丁。

当你刚开始学习编程时,你会反复听到一些词,但不确定它们是否具有特定的技术含义还是随意的说法。文件目录就是这样的词,它们确实有实际的技术含义。文件是一系列字节,存储在某个文件系统中,并通过文件名访问。目录是文件和可能其他目录的集合。术语文件夹是目录的同义词。它出现在计算机获得图形用户界面时,模仿办公室概念,使事物看起来更加熟悉。

许多文件系统是分层的,通常被称为类似于树。真实的办公室里不会有树,文件夹类比只有在你能够想象出所有子文件夹的情况下才有效。

文件的输入和输出。

最简单的持久性形式是普通的文件,有时称为平面文件。你从文件中读取到内存中,然后从内存中写入到文件中。Python 使得这些工作变得容易。与许多语言一样,它的文件操作在很大程度上是模仿熟悉且受欢迎的 Unix 等效操作。

使用open()创建或打开。

在执行以下操作之前,您需要调用open函数:

  • 读取现有文件。

  • 写入到一个新文件。

  • 追加到现有文件。

  • 覆盖现有文件。

*`fileobj`* = open( *`filename`*, *`mode`* )

这里是对这个调用的各部分的简要解释:

  • *fileobj*是open()返回的文件对象。

  • *filename*是文件的字符串名称。

  • *mode*是一个表示文件类型及其操作的字符串。

*mode*的第一个字母表示操作:

  • r表示读取。

  • w表示写入。如果文件不存在,则创建该文件。如果文件存在,则覆盖它。

  • x表示写入,但只有在文件不存在时才会写入。

  • a表示追加(在末尾写入),如果文件存在。

mode的第二个字母表示文件的类型

  • t(或什么都不写)表示文本。

  • b表示二进制。

打开文件后,您可以调用函数来读取或写入数据;这些将在接下来的示例中展示。

最后,您需要关闭文件以确保任何写入操作都已完成,并且内存已被释放。稍后,您将看到如何使用with来自动化此过程。

此程序打开一个名为oops.txt的文件,并在不写入任何内容的情况下关闭它。这将创建一个空文件:

>>> fout = open('oops.txt', 'wt')
>>> fout.close()

使用print()写入文本文件。

让我们重新创建oops.txt,然后向其中写入一行内容,然后关闭它:

>>> fout = open('oops.txt', 'wt')
>>> print('Oops, I created a file.', file=fout)
>>> fout.close()

我们在上一节创建了一个空的oops.txt文件,所以这只是覆盖它。

我们使用了print函数的file参数。如果没有这个参数,print会将内容写入标准输出,也就是你的终端(除非你已经告诉你的 shell 程序使用>重定向输出到文件或使用|管道传输到另一个程序)。

使用write()写入文本文件。

我们刚刚使用print向文件中写入了一行。我们也可以使用write

对于我们的多行数据源,让我们使用这首关于狭义相对论的打油诗作为例子:¹

>>> poem = '''There was a young lady named Bright,
... Whose speed was far faster than light;
... She started one day
... In a relative way,
... And returned on the previous night.'''
>>> len(poem)
150

下面的代码一次性将整首诗写入到名为'relativity'的文件中:

>>> fout = open('relativity', 'wt')
>>> fout.write(poem)
150
>>> fout.close()

write函数返回写入的字节数。它不像print那样添加空格或换行符。同样,你也可以使用print将多行字符串写入文本文件:

>>> fout = open('relativity', 'wt')
>>> print(poem, file=fout)
>>> fout.close()

那么,应该使用write还是print?正如你所见,默认情况下,print在每个参数后添加一个空格,并在末尾添加换行符。在前一个示例中,它向relativity文件附加了一个换行符。要使printwrite一样工作,将以下两个参数传递给它:

  • sep(分隔符,默认为空格,' '

  • end(结束字符串,默认为换行符,'\n'

我们将使用空字符串来替换这些默认值:

>>> fout = open('relativity', 'wt')
>>> print(poem, file=fout, sep='', end='')
>>> fout.close()

如果你有一个大的源字符串,你也可以写入分片(使用切片),直到源字符串处理完毕:

>>> fout = open('relativity', 'wt')
>>> size = len(poem)
>>> offset = 0
>>> chunk = 100
>>> while True:
...     if offset > size:
...          break
...     fout.write(poem[offset:offset+chunk])
...     offset += chunk
...
100
50
>>> fout.close()

这一次在第一次尝试中写入了 100 个字符,下一次写入了最后 50 个字符。切片允许你“超过结尾”而不会引发异常。

如果对我们来说relativity文件很重要,让我们看看使用模式x是否真的保护我们免受覆盖:

>>> fout = open('relativity', 'xt')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
FileExistsError: [Errno 17] File exists: 'relativity'

你可以将其与异常处理器一起使用:

>>> try:
...     fout = open('relativity', 'xt')]
...     fout.write('stomp stomp stomp')
... except FileExistsError:
...     print('relativity already exists!. That was a close one.')
...
relativity already exists!. That was a close one.

使用read()readline()readlines()读取文本文件

你可以不带参数调用read()一次性读取整个文件,就像下面的示例一样(在处理大文件时要小心;一个 1GB 文件将消耗 1GB 内存):

>>> fin = open('relativity', 'rt' )
>>> poem = fin.read()
>>> fin.close()
>>> len(poem)
150

你可以提供一个最大字符数来限制read()一次返回多少内容。让我们一次读取 100 个字符,并将每个块追加到poem字符串以重建原始内容:

>>> poem = ''
>>> fin = open('relativity', 'rt' )
>>> chunk = 100
>>> while True:
...     fragment = fin.read(chunk)
...     if not fragment:
...         break
...     poem += fragment
...
>>> fin.close()
>>> len(poem)
150

当你读取到结尾后,进一步调用read()会返回一个空字符串(''),这在if not fragment中被视作False。这会跳出while True循环。

你也可以使用readline()一次读取一行。在下一个示例中,我们将每一行追加到poem字符串中以重建原始内容:

>>> poem = ''
>>> fin = open('relativity', 'rt' )
>>> while True:
...     line = fin.readline()
...     if not line:
...         break
...     poem += line
...
>>> fin.close()
>>> len(poem)
150

对于文本文件,即使是空行也有长度为一(换行符),并且被视作True。当文件被读取完毕时,readline()(和read()一样)也会返回一个空字符串,同样被视作False

读取文本文件的最简单方法是使用迭代器。它一次返回一行。与前面的示例类似,但代码更少:

>>> poem = ''
>>> fin = open('relativity', 'rt' )
>>> for line in fin:
...     poem += line
...
>>> fin.close()
>>> len(poem)
150

所有前述示例最终构建了单个字符串poemreadlines()方法逐行读取,返回一个包含每行字符串的列表:

>>> fin = open('relativity', 'rt' )
>>> lines = fin.readlines()
>>> fin.close()
>>> print(len(lines), 'lines read')
5 lines read
>>> for line in lines:
...     print(line, end='')
...
There was a young lady named Bright,
Whose speed was far faster than light;
She started one day
In a relative way,
And returned on the previous night.>>>

我们告诉print()不要自动换行,因为前四行已经有了换行。最后一行没有换行,导致交互提示符>>>出现在最后一行之后。

使用write()写入二进制文件

如果在模式字符串中包含 'b',文件将以二进制模式打开。在这种情况下,你读取和写入的是 bytes 而不是字符串。

我们手头没有二进制诗歌,所以我们只会生成从 0 到 255 的 256 个字节值:

>>> bdata = bytes(range(0, 256))
>>> len(bdata)
256

以二进制模式打开文件进行写入,并一次性写入所有数据:

>>> fout = open('bfile', 'wb')
>>> fout.write(bdata)
256
>>> fout.close()

同样,write() 返回写入的字节数。

和文本一样,你可以将二进制数据分块写入:

>>> fout = open('bfile', 'wb')
>>> size = len(bdata)
>>> offset = 0
>>> chunk = 100
>>> while True:
...     if offset > size:
...          break
...     fout.write(bdata[offset:offset+chunk])
...     offset += chunk
...
100
100
56
>>> fout.close()

使用 read() 读取二进制文件

这个很简单;你只需要用 'rb' 打开即可:

>>> fin = open('bfile', 'rb')
>>> bdata = fin.read()
>>> len(bdata)
256
>>> fin.close()

使用 with 自动关闭文件

如果你忘记关闭已打开的文件,在不再引用后 Python 会关闭它。这意味着如果你在函数中打开文件但没有显式关闭它,在函数结束时文件会被自动关闭。但你可能在长时间运行的函数或程序的主要部分中打开了文件。应该关闭文件以确保所有未完成的写入被完成。

Python 有上下文管理器来清理诸如打开的文件之类的资源。你可以使用形式 with 表达式 as 变量

>>> with open('relativity', 'wt') as fout:
...     fout.write(poem)
...

就是这样。在上下文管理器(在本例中就是一行代码块)完成(正常完成 通过抛出异常)后,文件会自动关闭。

使用 seek() 改变位置

当你读取和写入时,Python 会跟踪你在文件中的位置。tell() 函数返回你当前从文件开头的偏移量,以字节为单位。seek() 函数让你跳到文件中的另一个字节偏移量。这意味着你不必读取文件中的每个字节来读取最后一个字节;你可以 seek() 到最后一个字节并只读取一个字节。

对于这个示例,使用你之前写的 256 字节二进制文件 'bfile'

>>> fin = open('bfile', 'rb')
>>> fin.tell()
0

使用 seek() 跳转到文件末尾前一个字节:

>>> fin.seek(255)
255

读取直到文件末尾:

>>> bdata = fin.read()
>>> len(bdata)
1
>>> bdata[0]
255

seek() 也会返回当前偏移量。

你可以给 seek() 调用一个第二参数:seek(*`offset`*, *`origin`*)

  • 如果 origin0(默认值),就从文件开头向后 offset 字节

  • 如果 origin1,就从当前位置向后 offset 字节

  • 如果 origin2,就从文件末尾相对 offset 字节

这些值也在标准的 os 模块中定义:

>>> import os
>>> os.SEEK_SET
0
>>> os.SEEK_CUR
1
>>> os.SEEK_END
2

因此,我们可以用不同的方式读取最后一个字节:

>>> fin = open('bfile', 'rb')

文件末尾前一个字节:

>>> fin.seek(-1, 2)
255
>>> fin.tell()
255

读取直到文件末尾:

>>> bdata = fin.read()
>>> len(bdata)
1
>>> bdata[0]
255
注意

你不需要调用 tell() 来让 seek() 工作。我只是想展示它们报告相同的偏移量。

下面是从文件当前位置进行搜索的示例:

>>> fin = open('bfile', 'rb')

这个示例最终会在文件末尾前两个字节处结束:

>>> fin.seek(254, 0)
254
>>> fin.tell()
254

现在向前移动一个字节:

>>> fin.seek(1, 1)
255
>>> fin.tell()
255

最后,读取直到文件末尾:

>>> bdata = fin.read()
>>> len(bdata)
1
>>> bdata[0]
255

这些函数对于二进制文件最有用。你可以用它们处理文本文件,但除非文件是 ASCII(每个字符一个字节),否则计算偏移量会很困难。这将取决于文本编码,而最流行的编码(UTF-8)使用不同数量的字节表示每个字符。

内存映射

读取和写入文件的替代方法是使用标准mmap模块将其内存映射。 这使得文件内容在内存中看起来像一个bytearray。 有关详细信息,请参阅文档和一些示例

文件操作

Python,像许多其他语言一样,根据 Unix 模式化其文件操作。 一些函数,例如chown()chmod(),具有相同的名称,但还有一些新函数。

首先,我将展示 Python 如何使用os.path模块的函数以及使用较新的pathlib模块处理这些任务。

使用exists()检查存在性。

要验证文件或目录是否确实存在,或者您只是想象了它,您可以提供exists(),并提供相对或绝对路径名,如此示例所示:

>>> import os
>>> os.path.exists('oops.txt')
True
>>> os.path.exists('./oops.txt')
True
>>> os.path.exists('waffles')
False
>>> os.path.exists('.')
True
>>> os.path.exists('..')
True

使用isfile()检查类型。

此部分中的函数检查名称是否引用文件、目录或符号链接(有关链接讨论的示例,请参见后续内容)。

我们将首先查看的第一个函数isfile,它提出一个简单的问题:这是一个普通的老实文件吗?

>>> name = 'oops.txt'
>>> os.path.isfile(name)
True

下面是确定目录的方法:

>>> os.path.isdir(name)
False

单个点(.)是当前目录的简写,两个点(..)代表父目录。 这些始终存在,因此像以下语句将始终报告True

>>> os.path.isdir('.')
True

os模块包含许多处理路径名(完全合格的文件名,以/开头并包括所有父级)的函数。 其中一个函数isabs()确定其参数是否为绝对路径名。 参数不需要是真实文件的名称:

>>> os.path.isabs(name)
False
>>> os.path.isabs('/big/fake/name')
True
>>> os.path.isabs('big/fake/name/without/a/leading/slash')
False

使用copy()复制。

copy()函数来自另一个模块shutil。 例如,将文件oops.txt复制到文件ohno.txt

>>> import shutil
>>> shutil.copy('oops.txt', 'ohno.txt')

shutil.move()函数复制文件,然后删除原始文件。

使用rename()函数更改名称。

此函数正如其名称所示。 在此示例中,它将ohno.txt重命名为ohwell.txt

>>> import os
>>> os.rename('ohno.txt', 'ohwell.txt')

在 Unix 中,文件存在于一个位置,但可以有多个名称,称为链接。 在低级硬链接中,很难找到给定文件的所有名称。 符号链接是一种替代方法,它将新名称存储为自己的文件,使您可以同时获取原始名称和新名称。 link()调用创建硬链接,symlink()创建符号链接。 islink()函数检查文件是否是符号链接。

下面是如何为现有文件oops.txt创建硬链接到新文件yikes.txt的方法:

>>> os.link('oops.txt', 'yikes.txt')
>>> os.path.isfile('yikes.txt')
True
>>> os.path.islink('yikes.txt')
False

要为现有文件oops.txt创建到新文件jeepers.txt的符号链接,请使用以下命令:

>>> os.symlink('oops.txt', 'jeepers.txt')
>>> os.path.islink('jeepers.txt')
True

使用chmod()更改权限。

在 Unix 系统中,chmod() 改变文件权限。对于用户(通常是你,如果你创建了该文件)、用户所在的主要组和其余世界,有读、写和执行权限。该命令使用紧凑的八进制(基数 8)值,结合用户、组和其他权限。例如,要使 oops.txt 只能由其所有者读取,输入以下内容:

>>> os.chmod('oops.txt', 0o400)

如果你不想处理晦涩的八进制值,而宁愿处理(稍微不那么)晦涩的符号,可以从 stat 模块导入一些常量,并使用如下语句:

>>> import stat
>>> os.chmod('oops.txt', stat.S_IRUSR)

使用 chown() 更改所有权

这个函数同样适用于 Unix/Linux/Mac。你可以通过指定数值用户 ID (uid) 和组 ID (gid) 来改变文件的所有者和/或组所有权:

>>> uid = 5
>>> gid = 22
>>> os.chown('oops', uid, gid)

使用 remove() 删除文件

在这段代码中,我们使用 remove() 函数,告别 oops.txt

>>> os.remove('oops.txt')
>>> os.path.exists('oops.txt')
False

目录操作

在大多数操作系统中,文件存在于层级结构的目录(通常称为文件夹)中。所有这些文件和目录的容器是一个文件系统(有时称为)。标准的 os 模块处理这些操作系统的具体细节,并提供以下函数,用于对它们进行操作。

使用 mkdir() 创建

此示例展示了如何创建一个名为 poems 的目录来存储那些珍贵的诗句:

>>> os.mkdir('poems')
>>> os.path.exists('poems')
True

使用 rmdir() 删除目录

经过重新考虑²,你决定其实根本不需要那个目录。以下是如何删除它的方法:

>>> os.rmdir('poems')
>>> os.path.exists('poems')
False

使用 listdir() 列出内容

好的,重来一次;让我们再次创建 poems,并添加一些内容:

>>> os.mkdir('poems')

现在获取其内容列表(到目前为止还没有):

>>> os.listdir('poems')
[]

接下来,创建一个子目录:

>>> os.mkdir('poems/mcintyre')
>>> os.listdir('poems')
['mcintyre']

在这个子目录中创建一个文件(如果你真的感觉有诗意,才输入所有这些行;确保使用匹配的单引号或三重引号开头和结尾):

>>> fout = open('poems/mcintyre/the_good_man', 'wt')
>>> fout.write('''Cheerful and happy was his mood,
... He to the poor was kind and good,
... And he oft' times did find them food,
... Also supplies of coal and wood,
... He never spake a word was rude,
... And cheer'd those did o'er sorrows brood,
... He passed away not understood,
... Because no poet in his lays
... Had penned a sonnet in his praise,
... 'Tis sad, but such is world's ways.
... ''')
344
>>> fout.close()

最后,让我们看看我们有什么。它最好在那里:

>>> os.listdir('poems/mcintyre')
['the_good_man']

使用 chdir() 更改当前目录

使用此函数,你可以从一个目录切换到另一个目录。让我们离开当前目录,花一点时间在 poems 中:

>>> import os
>>> os.chdir('poems')
>>> os.listdir('.')
['mcintyre']

使用 glob() 列出匹配的文件

glob() 函数使用 Unix shell 规则而非更完整的正则表达式语法来匹配文件或目录名。以下是这些规则:

  • * 匹配任何内容(re 应该期望 .*

  • ? 匹配一个单字符

  • [abc] 匹配字符 abc

  • [!abc] 匹配除了 abc 之外的任何字符

尝试获取所有以 m 开头的文件或目录:

>>> import glob
>>> glob.glob('m*')
['mcintyre']

任何两个字母的文件或目录如何?

>>> glob.glob('??')
[]

我在想一个以 m 开头、以 e 结尾的八个字母的单词:

>>> glob.glob('m??????e')
['mcintyre']

那么任何以 klm 开头、以 e 结尾的内容呢?

>>> glob.glob('[klm]*e')
['mcintyre']

路径名

几乎所有的计算机都使用层次化文件系统,其中目录(“文件夹”)包含文件和其他目录,向下延伸到不同的层级。当您想引用特定的文件或目录时,您需要它的 路径名:到达那里所需的目录序列,可以是 绝对 从顶部()或 相对 到您当前目录。

当您指定名称时,您经常会听到人们混淆正斜杠('/',而不是 Guns N’ Roses 的家伙)和反斜杠('\')。³ Unix 和 Mac(以及 Web URL)使用正斜杠作为 路径分隔符,而 Windows 使用反斜杠。⁴

Python 允许您在指定名称时使用斜杠作为路径分隔符。在 Windows 上,您可以使用反斜杠,但是您知道反斜杠在 Python 中是普遍的转义字符,所以您必须在所有地方加倍使用它,或者使用 Python 的原始字符串:

>>> win_file = 'eek\\urk\\snort.txt'
>>> win_file2 = r'eek\urk\snort.txt'
>>> win_file
'eek\\urk\\snort.txt'
>>> win_file2
'eek\\urk\\snort.txt'

当您构建路径名时,您可以做以下操作:

  • 使用适当的路径分隔符 ('/''\')

  • 使用 os.path.join() 构建路径名(参见 “使用 os.path.join() 构建路径名”)

  • 使用 pathlib(参见 “使用 pathlib”)

通过 abspath() 获取路径名

这个函数将相对名扩展为绝对名。如果您的当前目录是 /usr/gaberlunzie 并且文件 oops.txt 就在那里,您可以输入以下内容:

>>> os.path.abspath('oops.txt')
'/usr/gaberlunzie/oops.txt'

使用 realpath() 获取符号链接路径名

在较早的某一部分中,我们从新文件 jeepers.txt 创造了对 oops.txt 的符号链接。在这种情况下,您可以使用 realpath() 函数从 jeepers.txt 获取 oops.txt 的名称,如下所示:

>>> os.path.realpath('jeepers.txt')
'/usr/gaberlunzie/oops.txt'

使用 os.path.join() 构建路径名

当您构建一个多部分的路径名时,您可以使用 os.path.join() 将它们成对地组合,使用适合您操作系统的正确路径分隔符:

>>> import os
>>> win_file = os.path.join("eek", "urk")
>>> win_file = os.path.join(win_file, "snort.txt")

如果我在 Mac 或 Linux 系统上运行这个程序,我会得到这个结果:

>>> win_file
'eek/urk/snort.txt'

在 Windows 上运行会产生这个结果:

>>> win_file
'eek\\urk\\snort.txt'

但是如果相同的代码在不同位置运行会产生不同的结果,这可能是一个问题。新的 pathlib 模块是这个问题的一个便携解决方案。

使用 pathlib

Python 在版本 3.4 中添加了 pathlib 模块。它是我刚刚描述的 os.path 模块的一个替代方案。但我们为什么需要另一个模块呢?

它不是把文件系统路径名当作字符串,而是引入了 Path 对象来在稍高一级处理它们。使用 Path() 类创建一个 Path,然后用裸斜线(而不是 '/' 字符)将您的路径编织在一起:

>>> from pathlib import Path
>>> file_path = Path('eek') / 'urk' / 'snort.txt'
>>> file_path
PosixPath('eek/urk/snort.txt')
>>> print(file_path)
eek/urk/snort.txt

这个斜杠技巧利用了 Python 的 “魔术方法”。一个 Path 可以告诉您关于自己的一些信息:

>>> file_path.name
'snort.txt'
>>> file_path.suffix
'.txt'
>>> file_path.stem
'snort'

您可以像对待任何文件名或路径名字符串一样将 file_path 提供给 open()

您还可以看到如果在另一个系统上运行此程序会发生什么,或者如果需要在您的计算机上生成外国路径名:

>>> from pathlib import PureWindowsPath
>>> PureWindowsPath(file_path)
PureWindowsPath('eek/urk/snort.txt')
>>> print(PureWindowsPath(file_path))
eek\urk\snort.txt

参见文档以获取所有细节。

BytesIO 和 StringIO

你已经学会了如何修改内存中的数据以及如何将数据读取到文件中和从文件中获取数据。如果你有内存中的数据,但想调用一个期望文件的函数(或者反过来),你会怎么做?你想修改数据并传递这些字节或字符,而不是读取和写入临时文件。

你可以使用 io.BytesIO 处理二进制数据(bytes)和 io.StringIO 处理文本数据(str)。使用其中任何一个都可以将数据包装为类文件对象,适用于本章中介绍的所有文件函数。

这种情况的一个用例是数据格式转换。让我们将其应用于 PIL 库(详细信息将在“PIL 和 Pillow”中介绍),该库读取和写入图像数据。其 Image 对象的 open()save() 方法的第一个参数是文件名类文件对象。示例 14-1 中的代码使用 BytesIO 在内存中读取 并且 写入数据。它从命令行读取一个或多个图像文件,将其图像数据转换为三种不同的格式,并打印这些输出的长度和前 10 个字节。

示例 14-1. convert_image.py
from io import BytesIO
from PIL import Image
import sys

def data_to_img(data):
    """Return PIL Image object, with data from in-memory <data>"""
    fp = BytesIO(data)
    return Image.open(fp)    # reads from memory

def img_to_data(img, fmt=None):
    """Return image data from PIL Image <img>, in <fmt> format"""
    fp = BytesIO()
    if not fmt:
        fmt = img.format     # keeps the original format
    img.save(fp, fmt)        # writes to memory
    return fp.getvalue()

def convert_image(data, fmt=None):
    """Convert image <data> to PIL <fmt> image data"""
    img = data_to_img(data)
    return img_to_data(img, fmt)

def get_file_data(name):
    """Return PIL Image object for image file <name>"""
    img = Image.open(name)
    print("img", img, img.format)
    return img_to_data(img)

if __name__ == "__main__":
    for name in sys.argv[1:]:
        data = get_file_data(name)
        print("in", len(data), data[:10])
        for fmt in ("gif", "png", "jpeg"):
            out_data = convert_image(data, fmt)
            print("out", len(out_data), out_data[:10])
注意

因为它的行为类似于文件,所以你可以像处理普通文件一样使用 seek()read()write() 方法处理 BytesIO 对象;如果你执行了 seek() 后跟着一个 read(),你将只获得从该 seek 位置到结尾的字节。getvalue() 返回 BytesIO 对象中的所有字节。

这是输出结果,使用了你将在第二十章中看到的输入图像文件:

$ python convert_image.py ch20_critter.png
img <PIL.PngImagePlugin.PngImageFile image mode=RGB size=154x141 at 0x10340CF28> PNG
in 24941 b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00'
out 14751 b'GIF87a\\x9a\\x00\\x8d\\x00'
out 24941 b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00'
out 5914 b'\\xff\xd8\\xff\\xe0\\x00\\x10JFIF'

即将到来

下一章内容稍微复杂一些。它涉及并发(即大约同时执行多个任务的方式)和进程(运行程序)。

要做的事情

14.1 列出当前目录中的文件。

14.2 列出父目录中的文件。

14.3 将字符串 'This is a test of the emergency text system' 赋值给变量 test1,并将 test1 写入名为 test.txt 的文件。

14.4 打开文件 test.txt 并将其内容读取到字符串 test2 中。test1test2 是否相同?

¹ 在本书的第一份手稿中,我说的是广义相对论,被一位物理学家审阅者友善地纠正了。

² 为什么它从来都不是第一个?

³ 一种记忆方法是:斜杠向倾斜,反斜杠向倾斜。

⁴ 当 IBM 联系比尔·盖茨,询问他们的第一台个人电脑时,他以 $50,000 购买了操作系统 QDOS,以获得“MS-DOS”。它模仿了使用斜杠作为命令行参数的 CP/M。当 MS-DOS 后来添加了文件夹时,它不得不使用反斜杠。

第十五章:时间上的数据:进程与并发

计算机可以做的一件事情是被密封在纸板箱中并坐在仓库里,这是大多数人类做不到的。

Jack Handey

这一章和接下来的两章比之前的内容稍微有些挑战。在这一章中,我们涵盖了时间上的数据(在单台计算机上的顺序访问和并发访问),接着我们将在第十六章中讨论盒子中的数据(特殊文件和数据库的存储和检索),然后在第十七章中讨论空间中的数据(网络)。

程序与进程

当您运行一个单独的程序时,操作系统会创建一个单独的进程。它使用系统资源(CPU、内存、磁盘空间)和操作系统内核中的数据结构(文件和网络连接、使用统计等)。进程与其他进程隔离——它不能看到其他进程在做什么或者干扰它们。

操作系统会跟踪所有正在运行的进程,为每个进程分配一点运行时间,然后切换到另一个进程,以实现公平地分配工作和对用户响应迅速的双重目标。您可以通过图形界面(如 macOS 的活动监视器、Windows 计算机上的任务管理器或 Linux 中的top命令)查看进程的状态。

您还可以从自己的程序中访问进程数据。标准库的os模块提供了一种常见的访问某些系统信息的方式。例如,以下函数获取运行中 Python 解释器的进程 ID当前工作目录

>>> import os
>>> os.getpid()
76051
>>> os.getcwd()
'/Users/williamlubanovic'

这些获取用户 ID组 ID

>>> os.getuid()
501
>>> os.getgid()
20

使用子进程创建进程

到目前为止,你所见过的所有程序都是单独的进程。您可以使用标准库的subprocess模块从 Python 启动和停止其他已经存在的程序。如果只想在 shell 中运行另一个程序并获取其生成的所有输出(标准输出和标准错误输出),请使用getoutput()函数。在这里,我们获取 Unix 的date程序的输出:

>>> import subprocess
>>> ret = subprocess.getoutput('date')
>>> ret
'Sun Mar 30 22:54:37 CDT 2014'

在进程结束之前,您将得不到任何返回。如果需要调用可能需要很多时间的内容,请参阅“并发”中关于并发的讨论。由于getoutput()的参数是表示完整 shell 命令的字符串,因此可以包括参数、管道、<> 的 I/O 重定向等:

>>> ret = subprocess.getoutput('date -u')
>>> ret
'Mon Mar 31 03:55:01 UTC 2014'

将该输出字符串管道传递给wc命令计数一行、六个“单词”和 29 个字符:

>>> ret = subprocess.getoutput('date -u | wc')
>>> ret
'       1       6      29'

变体方法称为check_output()接受命令和参数列表。默认情况下,它只返回标准输出作为字节类型而不是字符串,并且不使用 shell:

>>> ret = subprocess.check_output(['date', '-u'])
>>> ret
b'Mon Mar 31 04:01:50 UTC 2014\n'

要显示其他程序的退出状态,getstatusoutput()返回一个包含状态代码和输出的元组:

>>> ret = subprocess.getstatusoutput('date')
>>> ret
(0, 'Sat Jan 18 21:36:23 CST 2014')

如果您不想捕获输出但可能想知道其退出状态,请使用call()

>>> ret = subprocess.call('date')
Sat Jan 18 21:33:11 CST 2014
>>> ret
0

(在类 Unix 系统中,0 通常是成功的退出状态。)

那个日期和时间被打印到输出中,但没有在我们的程序中捕获。因此,我们将返回代码保存为 ret

你可以以两种方式运行带参数的程序。第一种是在一个字符串中指定它们。我们的示例命令是 date -u,它会打印当前的日期和时间(协调世界时):

>>> ret = subprocess.call('date -u', shell=True)
Tue Jan 21 04:40:04 UTC 2014

你需要 shell=True 来识别命令行 date -u,将其拆分为单独的字符串,并可能扩展任何通配符字符,比如 *(在这个示例中我们没有使用任何通配符)。

第二种方法是将参数列表化,因此不需要调用 shell:

>>> ret = subprocess.call(['date', '-u'])
Tue Jan 21 04:41:59 UTC 2014

使用 multiprocessing 创建一个进程

你可以将一个 Python 函数作为一个独立的进程运行,甚至使用 multiprocessing 模块创建多个独立的进程。示例 15-1 中的示例代码简短而简单;将其保存为 mp.py,然后通过输入 python mp.py 运行它:

示例 15-1. mp.py
import multiprocessing
import os

def whoami(what):
    print("Process %s says: %s" % (os.getpid(), what))

if __name__ == "__main__":
    whoami("I'm the main program")
    for n in range(4):
        p = multiprocessing.Process(target=whoami,
          args=("I'm function %s" % n,))
        p.start()

当我运行这个时,我的输出看起来像这样:

Process 6224 says: I'm the main program
Process 6225 says: I'm function 0
Process 6226 says: I'm function 1
Process 6227 says: I'm function 2
Process 6228 says: I'm function 3

Process() 函数生成了一个新进程,并在其中运行 do_this() 函数。因为我们在一个有四次循环的循环中执行了这个操作,所以我们生成了四个执行 do_this() 然后退出的新进程。

multiprocessing 模块比一个喜剧团的小丑还要多。它真的是为那些需要将某些任务分配给多个进程以节省总体时间的时候而设计的;例如,下载网页进行爬取,调整图像大小等。它包括了排队任务、启用进程间通信以及等待所有进程完成的方法。“并发性” 探讨了其中的一些细节。

使用 terminate() 杀死一个进程

如果你创建了一个或多个进程,并且想出于某种原因终止其中一个(也许它陷入了循环,或者你感到无聊,或者你想成为一个邪恶的霸主),使用 terminate()。在 示例 15-2 中,我们的进程会计数到一百万,每一步都会睡眠一秒,并打印一个恼人的消息。然而,我们的主程序在五秒内失去耐心,然后将其从轨道上摧毁。

示例 15-2. mp2.py
import multiprocessing
import time
import os

def whoami(name):
    print("I'm %s, in process %s" % (name, os.getpid()))

def loopy(name):
    whoami(name)
    start = 1
    stop = 1000000
    for num in range(start, stop):
        print("\tNumber %s of %s. Honk!" % (num, stop))
        time.sleep(1)

if __name__ == "__main__":
    whoami("main")
    p = multiprocessing.Process(target=loopy, args=("loopy",))
    p.start()
    time.sleep(5)
    p.terminate()

当我运行这个程序时,我得到了以下输出:

I'm main, in process 97080
I'm loopy, in process 97081
    Number 1 of 1000000\. Honk!
    Number 2 of 1000000\. Honk!
    Number 3 of 1000000\. Honk!
    Number 4 of 1000000\. Honk!
    Number 5 of 1000000\. Honk!

使用 os 获取系统信息

标准的 os 包提供了关于你的系统的许多详细信息,并且如果以特权用户(root 或管理员)身份运行你的 Python 脚本,还可以控制其中的一些内容。除了在 第十四章 中介绍的文件和目录函数外,它还有像这样的信息函数(在 iMac 上运行):

>>> import os
>>> os.uname()
posix.uname_result(sysname='Darwin',
nodename='iMac.local',
release='18.5.0',
version='Darwin Kernel Version 18.5.0: Mon Mar 11 20:40:32 PDT 2019;
 root:xnu-4903.251.3~3/RELEASE_X86_64',
machine='x86_64')
>>> os.getloadavg()
(1.794921875, 1.93115234375, 2.2587890625)
>>> os.cpu_count()
4

一个有用的函数是 system(),它会执行一个命令字符串,就像你在终端上输入一样:

>>> import os
>>> os.system('date -u')
Tue Apr 30 13:10:09 UTC 2019
0

这是一个大杂烩。查看 文档 以获取有趣的小知识。

使用 psutil 获取进程信息

第三方包 psutil 还为 Linux、Unix、macOS 和 Windows 系统提供了系统和进程信息。

你可以猜测如何安装它:

$ pip install psutil

覆盖范围包括以下内容:

系统

CPU、内存、磁盘、网络、传感器

进程

ID、父 ID、CPU、内存、打开的文件、线程

我们已经在前面的os讨论中看到,我的计算机有四个 CPU。它们已经使用了多少时间(以秒为单位)?

>>> import psutil
>>> psutil.cpu_times(True)
[scputimes(user=62306.49, nice=0.0, system=19872.71, idle=256097.64),
scputimes(user=19928.3, nice=0.0, system=6934.29, idle=311407.28),
scputimes(user=57311.41, nice=0.0, system=15472.99, idle=265485.56),
scputimes(user=14399.49, nice=0.0, system=4848.84, idle=319017.87)]

它们现在有多忙?

>>> import psutil
>>> psutil.cpu_percent(True)
26.1
>>> psutil.cpu_percent(percpu=True)
[39.7, 16.2, 50.5, 6.0]

也许你永远不需要这种类型的数据,但知道在哪里查找是很好的。

命令自动化

你经常从 shell 中运行命令(要么手动输入命令,要么使用 shell 脚本),但 Python 有多个良好的第三方管理工具。

一个相关的主题,任务队列,在“队列”中讨论。

Invoke

fabric工具的第一个版本允许您使用 Python 代码定义本地和远程(网络)任务。开发人员将此原始包拆分为fabric2(远程)和invoke(本地)。

通过运行以下命令安装invoke

$ pip install invoke

invoke的一个用途是将函数作为命令行参数提供。让我们创建一个tasks.py文件,其中包含示例 15-3 中显示的行。

示例 15-3. tasks.py
from invoke import task

@task
def mytime(ctx):
    import time
    now = time.time()
    time_str = time.asctime(time.localtime(now))
    print("Local time is", timestr)

(那个ctx参数是每个任务函数的第一个参数,但它仅在invoke内部使用。你可以随意命名它,但必须有一个参数在那里。)

$ invoke mytime
Local time is Thu May  2 13:16:23 2019

使用参数-l--list来查看可用的任务:

$ invoke -l
Available tasks:

  mytime

任务可以有参数,你可以从命令行同时调用多个任务(类似于 shell 脚本中的&&使用)。

其他用途包括:

  • 使用run()函数运行本地 shell 命令

  • 响应程序的字符串输出模式

这只是一个简短的一瞥。详细信息请参阅文档

其他命令助手

这些 Python 包在某种程度上类似于invoke,但在需要时可能有一个或多个更适合:

并发

官方 Python 网站总结了一般的并发概念以及标准库中的并发。这些页面包含许多链接到各种包和技术;在本章中,我们展示了最有用的一些链接。

在计算机中,如果你在等待什么东西,通常是有两个原因:

I/O 绑定

这是目前最常见的情况。计算机 CPU 速度非常快 - 比计算机内存快数百倍,比磁盘或网络快数千倍。

CPU 绑定

CPU 保持繁忙。这发生在像科学或图形计算这样的数字计算任务中。

另外两个与并发相关的术语是:

同步

事物紧随其后,就像一行幼鹅跟随它们的父母。

异步

任务是独立的,就像随机的鹅在池塘里溅水一样。

随着您从简单系统和任务逐渐过渡到现实生活中的问题,您在某个时候将需要处理并发性。以网站为例。您通常可以相当快地为 web 客户端提供静态和动态页面。一秒钟的时间被认为是交互式的,但如果显示或交互需要更长时间,人们会变得不耐烦。像 Google 和 Amazon 这样的公司进行的测试表明,如果页面加载速度稍慢,流量会迅速下降。

但是,如果有些事情花费很长时间,比如上传文件、调整图像大小或查询数据库,你又无能为力怎么办?你不能再在同步的 web 服务器代码中做了,因为有人在等待。

在单台计算机上,如果要尽可能快地执行多个任务,就需要使它们相互独立。慢任务不应该阻塞其他所有任务。

本章前面展示了如何利用多进程在单台计算机上重叠工作。如果您需要调整图像大小,您的 web 服务器代码可以调用一个单独的、专用的图像调整进程来异步和并发地运行。它可以通过调用多个调整大小的进程来扩展您的应用程序。

诀窍在于让它们彼此协同工作。任何共享的控制或状态意味着会有瓶颈。更大的诀窍是处理故障,因为并发计算比常规计算更难。许多事情可能会出错,你成功的几率会更低。

好的。什么方法可以帮助您应对这些复杂性?让我们从管理多个任务的好方法开始:队列

队列

队列类似于列表:东西从一端添加,从另一端取走。最常见的是所谓的FIFO(先进先出)。

假设你正在洗盘子。如果你被困在整个工作中,你需要洗每个盘子,擦干它,并把它收起来。你可以用多种方式做到这一点。你可能先洗第一只盘子,然后擦干,然后把它收起来。然后你重复第二只盘子,依此类推。或者,您可以批量操作,洗所有的盘子,擦干它们,然后把它们收起来;这意味着您在水槽和沥干架上有足够的空间来存放每一步积累的所有盘子。这些都是同步方法——一个工人,一次做一件事。

作为替代方案,您可以找一个或两个帮手。如果您是洗碗工,您可以把每个洗净的盘子交给擦干工,擦干工再把每个擦干的盘子交给收拾工。只要每个人的工作速度一样,你们应该比一个人做快得多。

然而,如果你洗碗比烘干快怎么办?湿碟子要么掉在地上,要么堆在你和烘干机之间,或者你只是走音哼着歌等待烘干机准备好。如果最后一个人比烘干机慢,干燥好的碟子最终可能会掉在地上,或者堆在一起,或者烘干机开始哼歌。你有多个工人,但整体任务仍然是同步的,只能按照最慢的工人的速度进行。

众人拾柴火焰高,古语如是说(我一直以为这是阿米什人的,因为它让我想到了建造谷仓)。增加工人可以建造谷仓或者更快地洗碗。这涉及到队列

通常,队列传输消息,可以是任何类型的信息。在这种情况下,我们对分布式任务管理的队列感兴趣,也称为工作队列作业队列任务队列。水池中的每个碟子都交给一个可用的洗碗机,洗碗机洗完后交给第一个可用的烘干机,烘干机烘干后交给一个放置者。这可以是同步的(工人等待处理一个碟子和另一个工人来接收它),也可以是异步的(碟子在不同速度的工人之间堆积)。只要你有足够的工人,并且他们跟得上碟子的速度,事情就会快得多。

进程

你可以用许多方法实现队列。对于单台机器,标准库的 multiprocessing 模块(前面你见过)包含一个 Queue 函数。让我们模拟只有一个洗碗机和多个烘干进程(稍后有人会把碟子放好),以及一个中间的 dish_queue。将这个程序称为 dishes.py(Example 15-4)。

Example 15-4. dishes.py
import multiprocessing as mp

def washer(dishes, output):
    for dish in dishes:
        print('Washing', dish, 'dish')
        output.put(dish)

def dryer(input):
    while True:
        dish = input.get()
        print('Drying', dish, 'dish')
        input.task_done()

dish_queue = mp.JoinableQueue()
dryer_proc = mp.Process(target=dryer, args=(dish_queue,))
dryer_proc.daemon = True
dryer_proc.start()

dishes = ['salad', 'bread', 'entree', 'dessert']
washer(dishes, dish_queue)
dish_queue.join()

运行你的新程序,像这样:

$ python dishes.py
Washing salad dish
Washing bread dish
Washing entree dish
Washing dessert dish
Drying salad dish
Drying bread dish
Drying entree dish
Drying dessert dish

这个队列看起来很像一个简单的 Python 迭代器,产生一系列的碟子。实际上,它启动了独立的进程以及洗碗机和烘干机之间的通信。我使用了 JoinableQueue 和最终的 join() 方法来告诉洗碗机所有的碟子已经干燥好了。在 multiprocessing 模块中还有其他的队列类型,你可以阅读 文档 获取更多例子。

线程

线程在一个进程中运行,并可以访问进程中的所有内容,类似于多重人格。multiprocessing 模块有一个名为 threading 的表兄弟,它使用线程而不是进程(实际上,multiprocessing 是它基于进程的对应物)。让我们用线程重新做我们的进程示例,如 Example 15-5 所示。

Example 15-5. thread1.py
import threading

def do_this(what):
    whoami(what)

def whoami(what):
    print("Thread %s says: %s" % (threading.current_thread(), what))

if __name__ == "__main__":
    whoami("I'm the main program")
    for n in range(4):
        p = threading.Thread(target=do_this,
          args=("I'm function %s" % n,))
        p.start()

这是我的打印输出:

Thread <_MainThread(MainThread, started 140735207346960)> says: I'm the main
program
Thread <Thread(Thread-1, started 4326629376)> says: I'm function 0
Thread <Thread(Thread-2, started 4342157312)> says: I'm function 1
Thread <Thread(Thread-3, started 4347412480)> says: I'm function 2
Thread <Thread(Thread-4, started 4342157312)> says: I'm function 3

我们可以通过线程重新复制我们基于进程的洗碟子示例,如 Example 15-6 所示。

Example 15-6. thread_dishes.py
import threading, queue
import time

def washer(dishes, dish_queue):
    for dish in dishes:
        print ("Washing", dish)
        time.sleep(5)
        dish_queue.put(dish)

def dryer(dish_queue):
    while True:
        dish = dish_queue.get()
        print ("Drying", dish)
        time.sleep(10)
        dish_queue.task_done()

dish_queue = queue.Queue()
for n in range(2):
    dryer_thread = threading.Thread(target=dryer, args=(dish_queue,))
    dryer_thread.start()

dishes = ['salad', 'bread', 'entree', 'dessert']
washer(dishes, dish_queue)
dish_queue.join()

multiprocessingthreading之间的一个区别是,threading没有terminate()函数。没有简单的方法来终止运行中的线程,因为它可能会在您的代码中引发各种问题,甚至可能影响时空连续体本身。

线程可能是危险的。就像 C 和 C++等语言中的手动内存管理一样,它们可能会导致极难发现,更不用说修复的错误。要使用线程,程序中的所有代码(以及它使用的外部库中的代码)都必须是线程安全的。在前面的示例代码中,线程没有共享任何全局变量,因此它们可以独立运行而不会出错。

假设你是一个在闹鬼的房子里进行超自然调查的调查员。鬼魂在走廊里游荡,但彼此并不知道对方的存在,随时都可以查看、添加、删除或移动房子里的任何物品。

你戒备地穿过房子,用你那令人印象深刻的仪器进行测量。突然间,你注意到你刚刚走过的烛台不见了。

房子里的内容就像程序中的变量一样。鬼魂是进程(房子)中的线程。如果鬼魂只是偶尔瞥一眼房子的内容,那就没有问题。就像一个线程读取常量或变量的值而不试图改变它一样。

然而,某些看不见的实体可能会拿走你的手电筒,往你的脖子上吹冷风,把弹珠放在楼梯上,或点燃壁炉。真正微妙的鬼魂会改变你可能永远不会注意到的其他房间里的东西。

尽管你有花哨的仪器,但你要弄清楚谁做了什么,怎么做的,什么时候做的,以及在哪里做的,是非常困难的。

如果您使用多个进程而不是线程,那就像每个房子只有一个(活着的)人一样。如果您把白兰地放在壁炉前,一个小时后它仍会在那里——有些会因蒸发而丢失,但位置不变。

当不涉及全局数据时,线程可能是有用且安全的。特别是,在等待某些 I/O 操作完成时,线程可节省时间。在这些情况下,它们不必争夺数据,因为每个线程都有完全独立的变量。

但线程有时确实有充分理由更改全局数据。事实上,启动多个线程的一个常见原因是让它们分配某些数据的工作,因此预期对数据进行一定程度的更改。

安全共享数据的通常方法是在修改线程中的变量之前应用软件锁定。这样在进行更改时可以阻止其他线程进入。这就像让一个捉鬼者守卫你想保持清静的房间一样。不过,诀窍在于你需要记得解锁它。而且,锁定可以嵌套:如果另一个捉鬼者也在监视同一个房间,或者是房子本身呢?锁的使用是传统的但难以做到完全正确。

注:

在 Python 中,由于标准 Python 系统中的一个实现细节,线程不会加速 CPU 密集型任务,这称为全局解释器锁(GIL)。这存在是为了避免 Python 解释器中的线程问题,但实际上可能使多线程程序比其单线程版本或甚至多进程版本更慢。

因此,对于 Python,建议如下:

  • 对于 I/O 密集型问题,请使用线程

  • 对于 CPU 密集型问题,请使用进程、网络或事件(在下一节中讨论)

concurrent.futures

正如您刚刚看到的,使用线程或多进程涉及许多细节。concurrent.futures 模块已添加到 Python 3.2 标准库中,以简化这些操作。它允许您调度异步工人池,使用线程(当 I/O 密集型时)或进程(当 CPU 密集型时)。您将得到一个 future 来跟踪它们的状态并收集结果。

示例 15-7 包含一个测试程序,您可以将其保存为 cf.py。任务函数 calc() 睡眠一秒钟(我们模拟忙于某事),计算其参数的平方根,并返回它。程序可以接受一个可选的命令行参数,表示要使用的工人数,默认为 3。它在线程池中启动此数量的工人,然后在进程池中启动,然后打印经过的时间。values 列表包含五个数字,逐个发送给 calc() 在工人线程或进程中。

示例 15-7. cf.py
from concurrent import futures
import math
import time
import sys

def calc(val):
    time.sleep(1)
    result = math.sqrt(float(val))
    return result

def use_threads(num, values):
    t1 = time.time()
    with futures.ThreadPoolExecutor(num) as tex:
        results = tex.map(calc, values)
    t2 = time.time()
    return t2 - t1

def use_processes(num, values):
    t1 = time.time()
    with futures.ProcessPoolExecutor(num) as pex:
        results = pex.map(calc, values)
    t2 = time.time()
    return t2 - t1

def main(workers, values):
    print(f"Using {workers} workers for {len(values)} values")
    t_sec = use_threads(workers, values)
    print(f"Threads took {t_sec:.4f} seconds")
    p_sec = use_processes(workers, values)
    print(f"Processes took {p_sec:.4f} seconds")

if __name__ == '__main__':
    workers = int(sys.argv[1])
    values = list(range(1, 6)) # 1 .. 5
    main(workers, values)

这里是我得到的一些结果:

$ python cf.py 1
Using 1 workers for 5 values
Threads took 5.0736 seconds
Processes took 5.5395 seconds
$ python cf.py 3
Using 3 workers for 5 values
Threads took 2.0040 seconds
Processes took 2.0351 seconds
$ python cf.py 5
Using 5 workers for 5 values
Threads took 1.0052 seconds
Processes took 1.0444 seconds

那一秒钟的 sleep() 强制每个工人对每个计算都花费一秒钟:

  • 只有一个工人同时工作,一切都是串行的,总时间超过五秒。

  • 五个工人与被测试值的大小匹配,所以经过的时间略多于一秒。

  • 使用三个工人,我们需要两次运行来处理所有五个值,所以经过了两秒。

在程序中,我忽略了实际的 results(我们计算的平方根),以突出显示经过的时间。此外,使用 map() 来定义池会导致我们在返回 results 之前等待所有工人完成。如果您希望在每次完成时获取每个结果,让我们尝试另一个测试(称为 cf2.py),在该测试中,每个工人在计算完值及其平方根后立即返回该值(示例 15-8)。

示例 15-8. cf2.py
from concurrent import futures
import math
import sys

def calc(val):
    result = math.sqrt(float(val))
    return val, result

def use_threads(num, values):
    with futures.ThreadPoolExecutor(num) as tex:
        tasks = [tex.submit(calc, value) for value in values]
        for f in futures.as_completed(tasks):
             yield f.result()

def use_processes(num, values):
    with futures.ProcessPoolExecutor(num) as pex:
        tasks = [pex.submit(calc, value) for value in values]
        for f in futures.as_completed(tasks):
             yield f.result()

def main(workers, values):
    print(f"Using {workers} workers for {len(values)} values")
    print("Using threads:")
    for val, result in use_threads(workers, values):
        print(f'{val} {result:.4f}')
    print("Using processes:")
    for val, result in use_processes(workers, values):
        print(f'{val} {result:.4f}')

if __name__ == '__main__':
    workers = 3
    if len(sys.argv) > 1:
        workers = int(sys.argv[1])
    values = list(range(1, 6)) # 1 .. 5
    main(workers, values)

我们的 use_threads()use_processes() 函数现在是生成器函数,每次迭代调用 yield 返回。在我的机器上运行一次,您可以看到工人不总是按顺序完成 15

$ python cf2.py 5
Using 5 workers for 5 values
Using threads:
3 1.7321
1 1.0000
2 1.4142
4 2.0000
5 2.2361
Using processes:
1 1.0000
2 1.4142
3 1.7321
4 2.0000
5 2.2361

您可以在任何时候使用 concurrent.futures 启动一堆并发任务,例如以下内容:

  • 爬取网页上的 URL

  • 处理文件,如调整图像大小

  • 调用服务 API

如往常一样,文档 提供了额外的详细信息,但更加技术性。

绿色线程和 gevent

正如你所见,开发者传统上通过将程序中的慢点运行在单独的线程或进程中来避免慢点。Apache 网络服务器就是这种设计的一个例子。

一个替代方案是基于事件的编程。一个基于事件的程序运行一个中央事件循环,分发任何任务,并重复该循环。NGINX 网络服务器遵循这种设计,并且通常比 Apache 更快。

gevent库是基于事件的,并完成了一个巧妙的技巧:你编写普通的命令式代码,它会神奇地将部分代码转换为协程。这些协程类似于可以相互通信并跟踪其位置的生成器。gevent修改了 Python 许多标准对象如socket,以使用其机制而不是阻塞。这不能与 Python 中用 C 编写的插件代码一起工作,比如一些数据库驱动。

你可以使用pip安装gevent

$ pip install gevent

这里是gevent网站的示例代码的变体。在即将到来的 DNS 部分中,你会看到socket模块的gethostbyname()函数。这个函数是同步的,所以你要等待(可能很多秒),而它在世界各地的名称服务器中查找地址。但你可以使用gevent版本来独立查找多个站点。将其保存为gevent_test.py(示例 15-9)。

示例 15-9. gevent_test.py
import gevent
from gevent import socket
hosts = ['www.crappytaxidermy.com', 'www.walterpottertaxidermy.com',
    'www.antique-taxidermy.com']
jobs = [gevent.spawn(gevent.socket.gethostbyname, host) for host in hosts]
gevent.joinall(jobs, timeout=5)
for job in jobs:
    print(job.value)

在前面的示例中有一个单行的 for 循环。每个主机名依次提交给gethostbyname()调用,但它们可以异步运行,因为这是gevent版本的gethostbyname()

运行gevent_test.py

$ python gevent_test.py 
66.6.44.4
74.125.142.121
78.136.12.50

gevent.spawn()创建一个greenlet(有时也称为绿色线程微线程)来执行每个gevent.socket.gethostbyname(url)

与普通线程的区别在于它不会阻塞。如果发生了本应该阻塞普通线程的事件,gevent会切换控制到其他的 greenlet。

gevent.joinall()方法等待所有生成的作业完成。最后,我们会输出这些主机名对应的 IP 地址。

你可以使用它的富有表现力的名为monkey-patching的函数,而不是gevent版本的socket。这些函数修改标准模块如socket,以使用绿色线程而不是调用模块的gevent版本。当你希望gevent被应用到所有代码,甚至是无法访问的代码时,这是非常有用的。

在你的程序顶部添加以下调用:

from gevent import monkey
monkey.patch_socket()

这里将gevent套接字插入到任何地方普通的socket被调用的地方,即使是在你的程序中的标准库中。再次强调,这仅适用于 Python 代码,而不适用于用 C 编写的库。

另一个函数 monkey-patches 更多的标准库模块:

from gevent import monkey
monkey.patch_all()

在你的程序顶部使用这个来获取尽可能多的gevent加速。

将此程序保存为gevent_monkey.py(示例 15-9)。

示例 15-10. gevent_monkey.py
import gevent
from gevent import monkey; monkey.patch_all()
import socket
hosts = ['www.crappytaxidermy.com', 'www.walterpottertaxidermy.com',
    'www.antique-taxidermy.com']
jobs = [gevent.spawn(socket.gethostbyname, host) for host in hosts]
gevent.joinall(jobs, timeout=5)
for job in jobs:
    print(job.value)

再次运行程序:

$ python gevent_monkey.py
66.6.44.4
74.125.192.121
78.136.12.50

使用gevent存在潜在风险。与任何基于事件的系统一样,每段执行的代码应该相对迅速。虽然它是非阻塞的,但执行大量工作的代码仍然慢。

monkey-patching 的概念使一些人感到不安。然而,像 Pinterest 这样的大型网站使用gevent显著加速他们的网站。就像药瓶上的小字一样,请按照指示使用gevent

欲知更多示例,请参阅这个详尽的gevent教程

注意

你可能也考虑使用tornado或者gunicorn,这两个流行的事件驱动框架提供了低级事件处理和快速的 Web 服务器。如果你想构建一个快速的网站而不想麻烦传统的 Web 服务器如 Apache,它们值得一试。

twisted

twisted是一个异步的、事件驱动的网络框架。你可以将函数连接到诸如数据接收或连接关闭等事件,当这些事件发生时,这些函数就会被调用。这是一种回调设计,如果你之前写过 JavaScript 代码,这种方式可能很熟悉。如果你还不熟悉,它可能看起来有些反直觉。对于一些开发者来说,基于回调的代码在应用程序增长时变得更难管理。

通过以下命令安装它:

$ pip install twisted

twisted是一个庞大的包,支持多种基于 TCP 和 UDP 的互联网协议。简单说,我们展示了一个从twisted 示例改编的小小的“敲门”服务器和客户端。首先,让我们看看服务器,knock_server.py:(示例 15-11)。

示例 15-11. knock_server.py
from twisted.internet import protocol, reactor

class Knock(protocol.Protocol):
    def dataReceived(self, data):
        print('Client:', data)
        if data.startswith("Knock knock"):
            response = "Who's there?"
        else:
            response = data + " who?"
        print('Server:', response)
        self.transport.write(response)

class KnockFactory(protocol.Factory):
    def buildProtocol(self, addr):
        return Knock()

reactor.listenTCP(8000, KnockFactory())
reactor.run()

现在让我们快速浏览它的可靠伴侣,knock_client.py(示例 15-12)。

示例 15-12. knock_client.py
from twisted.internet import reactor, protocol

class KnockClient(protocol.Protocol):
    def connectionMade(self):
        self.transport.write("Knock knock")

    def dataReceived(self, data):
        if data.startswith("Who's there?"):
            response = "Disappearing client"
            self.transport.write(response)
        else:
            self.transport.loseConnection()
            reactor.stop()

class KnockFactory(protocol.ClientFactory):
    protocol = KnockClient

def main():
    f = KnockFactory()
    reactor.connectTCP("localhost", 8000, f)
    reactor.run()

if __name__ == '__main__':
    main()

首先启动服务器:

$ python knock_server.py

然后,启动客户端:

$ python knock_client.py

服务器和客户端交换消息,服务器打印对话:

Client: Knock knock
Server: Who's there?
Client: Disappearing client
Server: Disappearing client who?

我们的恶作剧客户端然后结束,让服务器等待笑话的结尾。

如果你想进入twisted的世界,请尝试一些它文档中的其他示例。

asyncio

Python 在 3.4 版本中加入了asyncio库。它是使用新的asyncawait功能定义并发代码的一种方式。这是一个涉及许多细节的大课题。为了避免在本章节中过多涉及,我已将有关asyncio和相关主题的讨论移至附录 C。

Redis

我们之前关于洗碗的代码示例,使用进程或线程,在单台机器上运行。让我们再来看一种可以在单台机器或跨网络运行的队列方法。即使有多个歌唱进程和跳舞线程,有时一台机器还不够,你可以把这一节当作单台(一台机器)和多台并发之间的桥梁。

要尝试本节中的示例,您需要一个 Redis 服务器及其 Python 模块。您可以在“Redis”中找到获取它们的位置。在那一章中,Redis 的角色是数据库。在这里,我们展示它的并发性格。

使用 Redis 列表是快速创建队列的方法。Redis 服务器运行在一台机器上;这可以是与其客户端相同的机器,或者是客户端通过网络访问的另一台机器。无论哪种情况,客户端通过 TCP 与服务器通信,因此它们是网络化的。一个或多个提供者客户端将消息推送到列表的一端。一个或多个客户端工作进程使用阻塞弹出操作监视此列表。如果列表为空,它们就会坐在那里打牌。一旦有消息到达,第一个渴望的工作进程就会获取到它。

像我们早期基于进程和线程的示例一样,redis_washer.py生成一系列的菜品(示例 15-13)。

示例 15-13. redis_washer.py
import redis
conn = redis.Redis()
print('Washer is starting')
dishes = ['salad', 'bread', 'entree', 'dessert']
for dish in dishes:
    msg = dish.encode('utf-8')
    conn.rpush('dishes', msg)
    print('Washed', dish)
conn.rpush('dishes', 'quit')
print('Washer is done')

循环生成四条包含菜品名称的消息,然后是一条说“quit”的最终消息。它将每条消息追加到 Redis 服务器中的名为dishes的列表中,类似于追加到 Python 列表中。

一旦第一道菜准备好,redis_dryer.py就开始工作(示例 15-14)。

示例 15-14. redis_dryer.py
import redis
conn = redis.Redis()
print('Dryer is starting')
while True:
    msg = conn.blpop('dishes')
    if not msg:
        break
    val = msg[1].decode('utf-8')
    if val == 'quit':
        break
    print('Dried', val)
print('Dishes are dried')

该代码等待第一个令牌为“dishes”的消息,并打印每一个干燥。它通过结束循环遵循quit消息。

先启动烘干机,然后启动洗碗机。在命令末尾使用&将第一个程序置于后台;它会继续运行,但不再接受键盘输入。这适用于 Linux、macOS 和 Windows,尽管您可能在下一行看到不同的输出。在这种情况下(macOS),它是关于后台烘干机进程的一些信息。然后,我们正常启动洗碗机进程(前台)。您将看到两个进程的混合输出:

$ python redis_dryer.py & 
[2] 81691
Dryer is starting
$ python redis_washer.py
Washer is starting
Washed salad
Dried salad
Washed bread
Dried bread
Washed entree
Dried entree
Washed dessert
Washer is done
Dried dessert
Dishes are dried
[2]+  Done                    python redis_dryer.py

一旦从洗碗机进程到达 Redis 的菜品 ID 开始,我们勤劳的烘干机进程就开始将它们取回。每个菜品 ID 都是一个数字,除了最后的sentinel值,即字符串'quit'。当烘干机进程读取到quit菜品 ID 时,它就会退出,并且一些更多的后台进程信息会打印到终端(同样依赖系统)。您可以使用一个标志(一个否则无效的值)来指示数据流本身的某些特殊情况,例如,我们已经完成了。否则,我们需要添加更多的程序逻辑,比如以下内容:

  • 预先同意一些最大的菜品编号,这实际上会成为一个标志。

  • 进行一些特殊的out-of-band(不在数据流中的)进程间通信。

  • 在一段时间内没有新数据时,设置超时。

让我们做一些最后的更改:

  • 创建多个dryer进程。

  • 给每个烘干机添加超时,而不是寻找一个标志。

新的redis_dryer2.py显示在示例 15-15 中。

示例 15-15. redis_dryer2.py
def dryer():
    import redis
    import os
    import time
    conn = redis.Redis()
    pid = os.getpid()
    timeout = 20
    print('Dryer process %s is starting' % pid)
    while True:
        msg = conn.blpop('dishes', timeout)
        if not msg:
            break
        val = msg[1].decode('utf-8')
        if val == 'quit':
            break
        print('%s: dried %s' % (pid, val))
        time.sleep(0.1)
    print('Dryer process %s is done' % pid)

import multiprocessing
DRYERS=3
for num in range(DRYERS):
    p = multiprocessing.Process(target=dryer)
    p.start()

在后台启动烘干进程,然后在前台启动洗碗机进程:

$ python redis_dryer2.py &
Dryer process 44447 is starting
Dryer process 44448 is starting
Dryer process 44446 is starting
$ python redis_washer.py
Washer is starting
Washed salad
44447: dried salad
Washed bread
44448: dried bread
Washed entree
44446: dried entree
Washed dessert
Washer is done
44447: dried dessert

一个更干燥的过程读取quit ID 并退出:

Dryer process 44448 is done

20 秒后,其他烘干程序从它们的blpop调用中获取到None的返回值,表示它们已超时。它们说出它们的最后一句话并退出:

Dryer process 44447 is done
Dryer process 44446 is done

在最后一个烘干子进程退出后,主要的烘干程序就结束了:

[1]+  Done                    python redis_dryer2.py

超越队列

随着更多的零部件移动,我们可爱的装配线被打断的可能性也更多。如果我们需要洗一顿宴会的盘子,我们有足够的工人吗?如果烘干机喝醉了怎么办?如果水槽堵塞了怎么办?担心,担心!

如何应对这一切?常见的技术包括以下几种:

火而忘

只需传递事物,不要担心后果,即使没有人在那里。这就是盘子掉在地上的方法。

请求-响应

洗碗机收到烘干机的确认,烘干机收到收拾碗盘者的确认,每个管道中的盘子都会这样。

回压或节流

如果下游的某个人跟不上,这种技术会让快速工人放松点。

在实际系统中,您需要确保工人们能够跟上需求;否则,您会听到盘子掉在地上的声音。您可以将新任务添加到待处理列表中,而某些工作进程则会弹出最新消息并将其添加到正在处理列表中。当消息完成时,它将从正在处理列表中删除,并添加到已完成列表中。这让您知道哪些任务失败或花费太长时间。您可以自己使用 Redis 来完成这一过程,或者使用已经编写和测试过的系统。一些基于 Python 的队列包可以增加这种额外的管理水平,包括:

  • celery可以使用我们讨论过的方法(multiprocessinggevent等)同步或异步地执行分布式任务。

  • rq是一个基于 Redis 的 Python 作业队列库。

队列讨论了队列软件,基于 Python 和其他语言。

即将到来

在本章中,我们将数据流经过程。在下一章中,您将看到如何在各种文件格式和数据库中存储和检索数据。

要做的事情

15.1 使用multiprocessing创建三个单独的进程。使每个进程在零到一秒之间等待一个随机数,打印当前时间,然后退出。

第十六章:盒子中的数据:持久存储

在获得数据之前进行理论化是一个重大的错误。

亚瑟·柯南·道尔

活跃的程序访问存储在随机存取存储器(RAM)中的数据。RAM 非常快速,但价格昂贵,并且需要持续的电源供应;如果电源中断,内存中的所有数据都将丢失。磁盘驱动器比 RAM 慢,但容量更大,成本更低,并且即使有人绊倒电源线后,也可以保留数据。因此,计算机系统中的大量工作已经致力于在磁盘和 RAM 之间进行最佳权衡。作为程序员,我们需要持久性:使用非易失性介质(如磁盘)存储和检索数据。

本章讨论了为不同目的优化的数据存储的不同类型:平面文件、结构化文件和数据库。除了输入和输出之外的文件操作在第十四章中有所涵盖。

记录是指一块相关数据的术语,由各个字段组成。

平面文本文件

最简单的持久性是普通的平面文件。如果您的数据结构非常简单并且在磁盘和内存之间交换所有数据,则此方法非常有效。纯文本数据可能适合这种处理方式。

填充文本文件

在这种格式中,记录中的每个字段都有固定的宽度,并且在文件中(通常用空格字符)填充到该宽度,使得每行(记录)具有相同的宽度。程序员可以使用seek()来在文件中跳转,并且仅读取和写入需要的记录和字段。

表格式文本文件

对于简单的文本文件,唯一的组织级别是行。有时,您可能需要比这更多的结构。您可能希望将数据保存供程序稍后使用,或者将数据发送到另一个程序。

有许多格式,这里是如何区分它们的方式:

  • 分隔符分隔符字符,如制表符('\t')、逗号(',')或竖线('|')。这是逗号分隔值(CSV)格式的一个例子。

  • '<''>'围绕标签。例如 XML 和 HTML。

  • 标点符号。一个例子是 JavaScript 对象表示法(JSON)。

  • 缩进。一个例子是 YAML(递归定义为“YAML 不是标记语言”)。

  • 杂项,例如程序的配置文件。

这些结构化文件格式中的每一个都可以由至少一个 Python 模块读取和写入。

CSV

分隔文件通常用作电子表格和数据库的交换格式。您可以手动读取 CSV 文件,一次读取一行,在逗号分隔符处拆分每行成字段,并将结果添加到诸如列表和字典之类的数据结构中。但最好使用标准的csv模块,因为解析这些文件可能比您想象的要复杂得多。在处理 CSV 时需要记住以下几个重要特征:

  • 有些分隔符除了逗号以外还有:'|''\t'(制表符)是常见的。

  • 有些有转义序列。如果定界符字符可以出现在字段内,则整个字段可能用引号字符括起来或者在某些转义字符之前。

  • 文件具有不同的行尾字符。Unix 使用'\n',Microsoft 使用'\r\n',而 Apple 曾使用'\r',但现在使用'\n'

  • 第一行可能包含列名。

首先,我们看看如何读取和写入包含列列表的行列表:

>>> import csv
>>> villains = [
...     ['Doctor', 'No'],
...     ['Rosa', 'Klebb'],
...     ['Mister', 'Big'],
...     ['Auric', 'Goldfinger'],
...     ['Ernst', 'Blofeld'],
...     ]
>>> with open('villains', 'wt') as fout:  # a context manager
...     csvout = csv.writer(fout)
...     csvout.writerows(villains)

这将创建包含这些行的文件villains

Doctor,No
Rosa,Klebb
Mister,Big
Auric,Goldfinger
Ernst,Blofeld

现在,我们试着再次读取它:

>>> import csv
>>> with open('villains', 'rt') as fin:  # context manager
...     cin = csv.reader(fin)
...     villains = [row for row in cin]  # a list comprehension
...
>>> print(villains)
[['Doctor', 'No'], ['Rosa', 'Klebb'], ['Mister', 'Big'],
['Auric', 'Goldfinger'], ['Ernst', 'Blofeld']]

我们利用了reader()函数创建的结构。它在cin对象中创建了可以在for循环中提取的行。

使用reader()writer()及其默认选项,列由逗号分隔,行由换行符分隔。

数据可以是字典列表而不是列表列表。让我们再次使用新的DictReader()函数和指定的列名读取villains文件:

>>> import csv
>>> with open('villains', 'rt') as fin:
...     cin = csv.DictReader(fin, fieldnames=['first', 'last'])
...     villains = [row for row in cin]
...
>>> print(villains)
[OrderedDict([('first', 'Doctor'), ('last', 'No')]),
OrderedDict([('first', 'Rosa'), ('last', 'Klebb')]),
OrderedDict([('first', 'Mister'), ('last', 'Big')]),
OrderedDict([('first', 'Auric'), ('last', 'Goldfinger')]),
OrderedDict([('first', 'Ernst'), ('last', 'Blofeld')])]

那个 OrderedDict 用于兼容 Python 3.6 之前的版本,当时字典默认保持其顺序。

让我们使用新的 DictWriter() 函数重新编写 CSV 文件。我们还调用 writeheader() 来向 CSV 文件写入初始列名行:

import csv
villains = [
 {'first': 'Doctor', 'last': 'No'},
 {'first': 'Rosa', 'last': 'Klebb'},
 {'first': 'Mister', 'last': 'Big'},
 {'first': 'Auric', 'last': 'Goldfinger'},
 {'first': 'Ernst', 'last': 'Blofeld'},
 ]
with open('villains.txt', 'wt') as fout:
 cout = csv.DictWriter(fout, ['first', 'last'])
 cout.writeheader()
 cout.writerows(villains)

这创建了一个带有头行的villains.csv文件(示例 16-1)。

示例 16-1. villains.csv
first,last
Doctor,No
Rosa,Klebb
Mister,Big
Auric,Goldfinger
Ernst,Blofeld

现在,让我们读取它。在DictReader()调用中省略fieldnames参数,告诉它使用文件的第一行的值(first,last)作为列标签和匹配的字典键:

>>> import csv
>>> with open('villains.csv', 'rt') as fin:
...     cin = csv.DictReader(fin)
...     villains = [row for row in cin]
...
>>> print(villains)
[OrderedDict([('first', 'Doctor'), ('last', 'No')]),
OrderedDict([('first', 'Rosa'), ('last', 'Klebb')]),
OrderedDict([('first', 'Mister'), ('last', 'Big')]),
OrderedDict([('first', 'Auric'), ('last', 'Goldfinger')]),
OrderedDict([('first', 'Ernst'), ('last', 'Blofeld')])]

XML

分隔文件仅传达两个维度:行(行)和列(行内字段)。如果要在程序之间交换数据结构,需要一种方法将层次结构、序列、集合和其他结构编码为文本。

XML 是一种突出显示的标记格式,它使用标签来界定数据,就像这个样本menu.xml文件中所示。

<?xml version="1.0"?>
<menu>
  <breakfast hours="7-11">
    <item price="$6.00">breakfast burritos</item>
    <item price="$4.00">pancakes</item>
  </breakfast>
  <lunch hours="11-3">
    <item price="$5.00">hamburger</item>
  </lunch>
  <dinner hours="3-10">
    <item price="8.00">spaghetti</item>
  </dinner>
</menu>

以下是 XML 的一些重要特征:

  • 标签以 < 字符开始。这个示例中的标签是 menubreakfastlunchdinneritem

  • 空白符将被忽略。

  • 通常像<menu>这样的开始标签后面跟着其他内容,然后是最终匹配的结束标签,例如</menu>

  • 标签可以在其他标签内嵌套到任何级别。在此示例中,item标签是breakfastlunchdinner标签的子标签;它们反过来是menu的子标签。

  • 可选的属性可以出现在开始标签内。在这个例子中,priceitem 的一个属性。

  • 标签可以包含。在这个例子中,每个 item 都有一个值,比如第二个早餐项目的 pancakes

  • 如果名为thing的标签没有值或子项,则可以通过在闭合尖括号之前包含一个斜杠来表示为单个标签,例如<thing/>,而不是开始和结束标签,像<thing></thing>

  • 将数据放在哪里的选择——属性、值、子标签——有些是任意的。例如,我们可以将最后一个 item 标签写成 <item price="$8.00" food="spaghetti"/>

XML 通常用于数据 feedsmessages,并具有诸如 RSS 和 Atom 之类的子格式。一些行业有许多专门的 XML 格式,比如 金融领域

XML 的超级灵活性启发了多个 Python 库,这些库在方法和功能上有所不同。

在 Python 中解析 XML 的最简单方法是使用标准的 ElementTree 模块。下面是一个简单的程序,用于解析 menu.xml 文件并打印一些标签和属性:

>>> import xml.etree.ElementTree as et
>>> tree = et.ElementTree(file='menu.xml')
>>> root = tree.getroot()
>>> root.tag
'menu'
>>> for child in root:
...     print('tag:', child.tag, 'attributes:', child.attrib)
...     for grandchild in child:
...         print('\ttag:', grandchild.tag, 'attributes:', grandchild.attrib)
...
tag: breakfast attributes: {'hours': '7-11'}
 tag: item attributes: {'price': '$6.00'}
 tag: item attributes: {'price': '$4.00'}
tag: lunch attributes: {'hours': '11-3'}
 tag: item attributes: {'price': '$5.00'}
tag: dinner attributes: {'hours': '3-10'}
 tag: item attributes: {'price': '8.00'}
>>> len(root)     # number of menu sections
3
>>> len(root[0])  # number of breakfast items
2

对于嵌套列表中的每个元素,tag 是标签字符串,attrib 是其属性的字典。ElementTree 有很多其他搜索 XML 派生数据,修改它的方法,甚至编写 XML 文件的方法。ElementTree 文档 上有详细信息。

其他标准的 Python XML 库包括以下内容:

xml.dom

文件对象模型(DOM),对于 JavaScript 开发者来说很熟悉,它将 web 文档表示为分层结构。这个模块将整个 XML 文件加载到内存中,并允许您平等地访问所有部分。

xml.sax

简单的 XML API,或 SAX,可以实时解析 XML,因此它不必一次加载所有内容到内存中。因此,如果您需要处理非常大的 XML 流,它可能是一个不错的选择。

XML 安全注意事项

您可以使用本章中描述的所有格式将对象保存到文件中,然后再读取它们。可以利用这个过程来造成安全问题。

例如,下面来自十亿笑话维基百科页面的 XML 片段定义了 10 个嵌套实体,每个实体扩展了低一级 10 次,总扩展量达到了十亿:

<?xml version="1.0"?>
<!DOCTYPE lolz [
 <!ENTITY lol "lol">
 <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
 <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
 <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
 <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
 <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
 <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
 <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
 <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
 <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>

坏消息:十亿笑话将会使前面提到的所有 XML 库爆炸。Defused XML 列出了这个攻击和其他攻击,以及 Python 库的漏洞。

该链接展示了如何更改许多库的设置以避免这些问题。此外,您可以使用 defusedxml 库作为其他库的安全前端:

>>> # insecure:
>>> from xml.etree.ElementTree import parse
>>> et = parse(xmlfile)
>>> # protected:
>>> from defusedxml.ElementTree import parse
>>> et = parse(xmlfile)

标准的 Python 网站上也有关于 XML 漏洞 的页面。

HTML

庞大的数据以超文本标记语言(HTML)的形式保存,这是 web 的基本文档格式。问题是其中的大部分都不符合 HTML 规则,这可能会使解析变得困难。HTML 比数据交换格式更好地显示格式。因为本章旨在描述相当明确定义的数据格式,我已将有关 HTML 的讨论分离到 第十八章 中。

JSON

JavaScript Object Notation (JSON) 已成为非常流行的数据交换格式,超越了其 JavaScript 的起源。JSON 格式是 JavaScript 的子集,通常也是合法的 Python 语法。它与 Python 的密切配合使其成为程序之间数据交换的良好选择。您将在 第十八章 中看到许多用于 Web 开发的 JSON 示例。

与各种 XML 模块不同,主要的 JSON 模块只有一个,其名称令人难忘,即 json。此程序将数据编码(转储)为 JSON 字符串,并将 JSON 字符串解码(加载)回数据。在下面的示例中,让我们构建一个包含先前 XML 示例中的数据的 Python 数据结构:

>>> menu = \
... {
... "breakfast": {
...         "hours": "7-11",
...         "items": {
...                 "breakfast burritos": "$6.00",
...                 "pancakes": "$4.00"
...                 }
...         },
... "lunch" : {
...         "hours": "11-3",
...         "items": {
...                 "hamburger": "$5.00"
...                 }
...         },
... "dinner": {
...         "hours": "3-10",
...         "items": {
...                 "spaghetti": "$8.00"
...                 }
...         }
... }
.

接下来,使用 dumps() 将数据结构 (menu) 编码为 JSON 字符串 (menu_json):

>>> import json
>>> menu_json = json.dumps(menu)
>>> menu_json
'{"dinner": {"items": {"spaghetti": "$8.00"}, "hours": "3-10"},
"lunch": {"items": {"hamburger": "$5.00"}, "hours": "11-3"},
"breakfast": {"items": {"breakfast burritos": "$6.00", "pancakes":
"$4.00"}, "hours": "7-11"}}'

现在,让我们通过使用 loads() 将 JSON 字符串 menu_json 转换回 Python 数据结构 (menu2):

>>> menu2 = json.loads(menu_json)
>>> menu2
{'breakfast': {'items': {'breakfast burritos': '$6.00', 'pancakes':
'$4.00'}, 'hours': '7-11'}, 'lunch': {'items': {'hamburger': '$5.00'},
'hours': '11-3'}, 'dinner': {'items': {'spaghetti': '$8.00'}, 'hours': '3-10'}}

menumenu2 都是具有相同键和值的字典。

在尝试编码或解码某些对象时,可能会遇到异常,包括诸如 datetime 的对象(在 第十三章 中有详细说明),如下所示:

>>> import datetime
>>> import json
>>> now = datetime.datetime.utcnow()
>>> now
datetime.datetime(2013, 2, 22, 3, 49, 27, 483336)
>>> json.dumps(now)
Traceback (most recent call last):
# ... (deleted stack trace to save trees)
TypeError: datetime.datetime(2013, 2, 22, 3, 49, 27, 483336)
 is not JSON serializable
>>>

这可能是因为 JSON 标准未定义日期或时间类型;它期望您定义如何处理它们。您可以将 datetime 转换为 JSON 理解的内容,如字符串或 epoch 值(见 第十三章):

>>> now_str = str(now)
>>> json.dumps(now_str)
'"2013-02-22 03:49:27.483336"'
>>> from time import mktime
>>> now_epoch = int(mktime(now.timetuple()))
>>> json.dumps(now_epoch)
'1361526567'

如果 datetime 值可能出现在通常转换的数据类型中间,这些特殊转换可能会令人困扰。您可以通过使用继承修改 JSON 的编码方式来进行修改,详见 第十章。Python 的 JSON 文档 提供了关于复数的示例,这也使 JSON 看起来像是死的。我们修改它以适应 datetime

>>> import datetime
>>> now = datetime.datetime.utcnow()
>>> class DTEncoder(json.JSONEncoder):
...     def default(self, obj):
...         # isinstance() checks the type of obj
...         if isinstance(obj, datetime.datetime):
...             return int(mktime(obj.timetuple()))
...         # else it's something the normal decoder knows:
...         return json.JSONEncoder.default(self, obj)
...
>>> json.dumps(now, cls=DTEncoder)
'1361526567'

新的类 DTEncoderJSONEncoder 的子类或子类,我们需要重写其唯一的 default() 方法以添加 datetime 处理。继承确保所有其他内容都将由父类处理。

isinstance() 函数检查对象 obj 是否为 datetime.datetime 类的实例。因为 Python 中的所有东西都是对象,所以 isinstance() 在任何地方都适用:

>>> import datetime
>>> now = datetime.datetime.utcnow()
>>> type(now)
<class 'datetime.datetime'>
>>> isinstance(now, datetime.datetime)
True
>>> type(234)
<class 'int'>
>>> isinstance(234, int)
True
>>> type('hey')
<class 'str'>
>>> isinstance('hey', str)
True
注意

对于 JSON 和其他结构化文本格式,您可以从文件加载到数据结构中,而无需事先了解结构的任何信息。然后,您可以使用 isinstance() 和适当类型的方法遍历这些结构以检查其值。例如,如果其中一个项目是字典,您可以通过 keys()values()items() 提取内容。

在让您以困难的方式处理后,事实证明有一种更简单的方法可以将 datetime 对象转换为 JSON:

>>> import datetime
>>> import json
>>> now = datetime.datetime.utcnow()
>>> json.dumps(now, default=str)
'"2019-04-17 21:54:43.617337"'

default=str 告诉 json.dumps() 对于它不理解的数据类型应用 str() 转换函数。这是因为 datetime.datetime 类的定义包含一个 __str__() 方法。

YAML

类似于 JSON,YAML具有键和值,但处理更多数据类型,如日期和时间。标准的 Python 库尚未包含 YAML 处理,因此您需要安装名为yaml的第三方库来操作它。((("dump() function")))((("load() function")))load()将 YAML 字符串转换为 Python 数据,而dump()则相反。

以下的 YAML 文件,mcintyre.yaml,包含了加拿大诗人詹姆斯·麦金太尔(James McIntyre)的信息,包括他的两首诗:

name:
  first: James
  last: McIntyre
dates:
  birth: 1828-05-25
  death: 1906-03-31
details:
  bearded: true
  themes: [cheese, Canada]
books:
  url: http://www.gutenberg.org/files/36068/36068-h/36068-h.htm
poems:
  - title: 'Motto'
    text: |
      Politeness, perseverance and pluck,
      To their possessor will bring good luck.
  - title: 'Canadian Charms'
    text: |
      Here industry is not in vain,
      For we have bounteous crops of grain,
      And you behold on every field
      Of grass and roots abundant yield,
      But after all the greatest charm
      Is the snug home upon the farm,
      And stone walls now keep cattle warm.

值如truefalseonoff会转换为 Python 布尔值。整数和字符串会转换为它们的 Python 等效项。其他语法创建列表和字典:

>>> import yaml
>>> with open('mcintyre.yaml', 'rt') as fin:
>>>     text = fin.read()
>>> data = yaml.load(text)
>>> data['details']
{'themes': ['cheese', 'Canada'], 'bearded': True}
>>> len(data['poems'])
2

创建的数据结构与 YAML 文件中的相匹配,这在某些情况下是多层次的。您可以使用这个字典/列表/字典引用获取第二首诗的标题:

>>> data['poems'][1]['title']
'Canadian Charms'
警告

PyYAML 可以从字符串加载 Python 对象,这是危险的。如果您导入不信任的 YAML,请使用safe_load()而不是load()。更好的做法是,总是使用safe_load()。阅读 Ned Batchelder 的博客文章“War is Peace”了解未受保护的 YAML 加载如何危及 Ruby on Rails 平台。

Tablib

在阅读所有先前的章节之后,有一个第三方包可以让您导入、导出和编辑 CSV、JSON 或 YAML 格式的表格数据,¹还有 Microsoft Excel、Pandas DataFrame 和其他几个。您可以用熟悉的叠歌(pip install tablib)安装它,并查看文档

Pandas

这是介绍pandas的好地方——一个用于结构化数据的 Python 库。它是处理现实生活数据问题的优秀工具:

  • 读写许多文本和二进制文件格式:

    • 文本,字段由逗号(CSV)、制表符(TSV)或其他字符分隔

    • 固定宽度文本

    • Excel

    • JSON

    • HTML 表格

    • SQL

    • HDF5

    • 其他

  • 分组、拆分、合并、索引、切片、排序、选择、标记

  • 转换数据类型

  • 更改大小或形状

  • 处理丢失的数据

  • 生成随机值

  • 管理时间序列

读取函数返回一个DataFrame对象,Pandas 的标准表示形式,用于二维数据(行和列)。在某些方面类似于电子表格或关系数据库表。它的一维小兄弟是Series

示例 16-2 演示了一个简单的应用程序,从示例 16-1 中读取我们的villains.csv文件。

示例 16-2. 使用 Pandas 读取 CSV
>>> import pandas
>>>
>>> data = pandas.read_csv('villains.csv')
>>> print(data)
    first        last
0  Doctor          No
1    Rosa       Klebb
2  Mister         Big
3   Auric  Goldfinger
4   Ernst     Blofeld

变量data刚刚展示的是一个DataFrame。它比基本的 Python 字典有更多的技巧。它特别适用于使用 NumPy 进行大量数字工作和为机器学习准备数据。

参考 Pandas 文档的“入门指南”部分以及“10 分钟入门 Pandas”中的工作示例。

让我们举个小日历例子使用 Pandas——列出 2019 年前三个月的第一天的列表:

>>> import pandas
>>> dates = pandas.date_range('2019-01-01', periods=3, freq='MS')
>>> dates
DatetimeIndex(['2019-01-01', '2019-02-01', '2019-03-01'],
 dtype='datetime64[ns]', freq='MS')

你可以编写一些代码来实现这一点,使用第十三章中描述的时间和日期函数,但这需要更多的工作——特别是调试(日期和时间常常令人沮丧)。Pandas 还处理许多特殊的日期/时间细节,如业务月和年。

后面当我谈论映射时,Pandas 会再次出现(“Geopandas”)以及科学应用(“Pandas”)。

配置文件

大多数程序提供各种optionssettings。动态的可以作为程序参数提供,但长期的需要保存在某个地方。定义自己快速而脏的config file格式的诱惑力很强——但要抵制它。它经常变得脏乱,但并不快速。您需要维护编写程序和读取程序(有时称为parser)。有很多好的替代方案可以直接插入您的程序,包括前面的部分中提到的那些。

这里,我们将使用标准configparser模块,它处理 Windows 风格的*.ini文件。这些文件有key* = value定义的部分。这是一个最小的settings.cfg文件:

[english]
greeting = Hello

[french]
greeting = Bonjour

[files]
home = /usr/local
# simple interpolation:
bin = %(home)s/bin

这是将其读入 Python 数据结构的代码:

>>> import configparser
>>> cfg = configparser.ConfigParser()
>>> cfg.read('settings.cfg')
['settings.cfg']
>>> cfg
<configparser.ConfigParser object at 0x1006be4d0>
>>> cfg['french']
<Section: french>
>>> cfg['french']['greeting']
'Bonjour'
>>> cfg['files']['bin']
'/usr/local/bin'

还有其他选择,包括更高级的插值。参见configparser文档。如果需要比两级更深的嵌套,请尝试 YAML 或 JSON。

二进制文件

有些文件格式设计用于存储特定的数据结构,但既不是关系型数据库也不是 NoSQL 数据库。接下来的部分介绍了其中的一些。

填充的二进制文件和内存映射

这些类似于填充的文本文件,但内容可能是二进制的,填充字节可能是\x00而不是空格字符。每个记录和记录内的每个字段都有固定的大小。这使得在文件中通过seek()查找所需的记录和字段变得更简单。数据的每一项操作都是手动的,因此这种方法通常仅在非常低级别(例如接近硬件)的情况下使用。

这种形式的数据可以使用标准的mmap库进行memory mapped。参见一些示例和标准的文档

电子表格

电子表格,特别是 Microsoft Excel,是广泛使用的二进制数据格式。如果可以将电子表格保存为 CSV 文件,可以使用早期描述的标准csv模块进行读取。这对于二进制的xls文件、xlrdtablib(在“Tablib”早些时候提到)都适用。

HDF5

HDF5 是用于多维或层次化数值数据的二进制数据格式。它主要用于科学领域,其中快速访问大数据集(从几千兆字节到几太字节)是常见需求。尽管在某些情况下,HDF5 可能是数据库的良好替代品,但由于某些原因,HDF5 在商业界几乎不为人知。它最适合于WORM(写入一次/多次读取)应用程序,这种应用程序不需要数据库对抗冲突写入。以下是一些可能对你有用的模块:

  • h5py 是一个功能齐全的低级接口。阅读文档代码

  • PyTables 是一个稍微高级的库,具有类似数据库的功能。阅读文档代码

这两者都是讨论 Python 在第二十二章科学应用程序方面的应用。我在这里提到 HDF5 是因为你可能需要存储和检索大量数据,并愿意考虑除了传统数据库解决方案以外的其他选择。一个很好的例子是百万首歌数据集,其中包含以 HDF5 和 SQLite 格式提供的可下载歌曲数据。

TileDB

用于密集或稀疏数组存储的最新后继者是TileDB。通过运行 pip install tiledb 安装Python 接口(包括 TileDB 库本身)。这专为科学数据和应用程序设计。

关系数据库

关系数据库虽然只有大约 40 年的历史,但在计算世界中无处不在。你几乎肯定会在某个时候必须处理它们。当你这样做时,你会感谢它们提供的:

  • 多个同时用户访问数据

  • 受用户防止损坏

  • 高效存储和检索数据的方法

  • 模式定义的数据和受约束限制

  • 连接以找到跨多种类型数据的关系

  • 一种声明性(而不是命令性)查询语言:SQL(结构化查询语言)

这些被称为关系数据库,因为它们展示了不同类型数据之间的关系,以矩形表格的形式。例如,在我们之前的菜单示例中,每个项目与其价格之间存在关系。

表格是(数据字段)和(单个数据记录)的矩形网格,类似于电子表格。行和列的交集是表格的单元格。要创建表格,需要命名它并指定其列的顺序、名称和类型。每行都具有相同的列,尽管可以定义某列允许在单元格中包含缺失数据(称为nulls)。在菜单示例中,你可以为每个出售的项目创建一个包含价格等列的表格。

表的列或一组列通常是表的主键;其值在表中必须是唯一的。这样可以防止将相同的数据多次添加到表中。此键被索引以便在查询期间快速查找。索引的工作方式有点像书籍索引,使得快速找到特定行。

每个表都位于父数据库中,就像目录中的文件一样。两个层次的层次结构有助于使事物组织得更好一些。

注意

是的,单词数据库以多种方式使用:作为服务器,表容器和其中存储的数据。如果您同时提到它们所有,可能有助于将它们称为数据库服务器数据库数据

如果要通过某些非关键列值查找行,请在该列上定义二级索引。否则,数据库服务器必须执行表扫描—对每一行进行匹配列值的 brute-force 搜索。

表可以通过外键相互关联,并且列值可以受限于这些键。

SQL

SQL 不是 API 或协议,而是声明性语言:您说您想要什么而不是如何做。这是关系数据库的通用语言。SQL 查询是由客户端发送到数据库服务器的文本字符串,数据库服务器然后确定如何处理它们。

有各种各样的 SQL 标准定义,所有数据库供应商都添加了自己的调整和扩展,导致了许多 SQL 方言。如果您将数据存储在关系型数据库中,SQL 可以提供一些可移植性。然而,方言和操作差异可能会使您将数据移动到另一种类型的数据库变得困难。SQL 语句有两个主要类别:

DDL(数据定义语言)

处理表,数据库和用户的创建,删除,约束和权限。

DML(数据操纵语言)

处理数据插入,选择,更新和删除。

表 16-1 列出了基本的 SQL DDL 命令。

表 16-1. 基本的 SQL DDL 命令

操作 SQL 模式 SQL 示例
创建数据库 CREATE DATABASE dbname CREATE DATABASE d
选择当前数据库 USE dbname USE d
删除数据库及其表 DROP DATABASE dbname DROP DATABASE d
创建表 CREATE TABLE tbname ( coldefs ) CREATE TABLE t (id INT, count INT)
删除表 DROP TABLE tbname DROP TABLE t
从表中删除所有行 TRUNCATE TABLE tbname TRUNCATE TABLE t
注意

为什么所有的大写字母?SQL 不区分大小写,但在代码示例中大声喊出关键字是一种传统(不要问我为什么),以区分它们和列名。

关系数据库的主要 DML 操作通常以 CRUD 缩写而闻名:

  • 使用 SQL INSERT语句Create

  • R通过SELECT读取

  • U通过UPDATE更新

  • D通过DELETE删除

表 16-2 查看了可用于 SQL DML 的命令。

表 16-2. 基本 SQL DML 命令

操作 SQL 模式 SQL 示例
添加一行 INSERT INTO tbname VALUES() INSERT INTO t VALUES(7, 40)
选择所有行和列 SELECT * FROM tbname SELECT * FROM t
选择所有行,某些列 SELECT cols FROM tbname SELECT id, count FROM t
选择某些行,某些列 SELECT cols FROM tbname WHERE condition SELECT id, count from t WHERE count > 5 AND id = 9
更改某列中的一些行 UPDATE tbname SET col = value WHERE condition UPDATE t SET count=3 WHERE id=5
删除某些行 DELETE FROM tbname WHERE condition DELETE FROM t WHERE count <= 10 OR id = 16

DB-API

应用程序编程接口(API)是一组可以调用以访问某些服务的函数。DB-API 是 Python 访问关系型数据库的标准 API。使用它,你可以编写一个程序,可以与多种关系型数据库一起工作,而不是为每种数据库编写单独的程序。它类似于 Java 的 JDBC 或 Perl 的 dbi。

它的主要功能如下:

connect()

建立与数据库的连接;这可以包括用户名、密码、服务器地址等参数。

cursor()

创建一个 cursor 对象来管理查询。

execute()executemany()

对数据库运行一个或多个 SQL 命令。

fetchone()fetchmany()fetchall()

execute() 获取结果。

接下来的章节中的 Python 数据库模块符合 DB-API,通常具有扩展和一些细节上的差异。

SQLite

SQLite 是一个优秀、轻量、开源的关系型数据库。它作为标准的 Python 库实现,并且将数据库存储在普通文件中。这些文件可以跨机器和操作系统移植,使 SQLite 成为简单关系数据库应用程序的非常便携的解决方案。虽然不如 MySQL 或 PostgreSQL 功能全面,但它支持 SQL,并且能够管理多个同时用户。Web 浏览器、智能手机和其他应用程序都将 SQLite 用作嵌入式数据库。

首先通过 connect() 连接到你要使用或创建的本地 SQLite 数据库文件。这个文件相当于其他服务器中父表所在的类似目录的数据库。特殊字符串 ':memory:' 仅在内存中创建数据库;这对测试很快并且很有用,但在程序终止或计算机关机时会丢失数据。

作为下一个示例,让我们创建一个名为 enterprise.db 的数据库和一个名为 zoo 的表,以管理我们蓬勃发展的路边宠物动物园业务。表的列如下:

critter

变长字符串,以及我们的主键。

count

这种动物的当前库存的整数计数。

damages

我们当前因动物与人类互动而造成的损失金额。

>>> import sqlite3
>>> conn = sqlite3.connect('enterprise.db')
>>> curs = conn.cursor()
>>> curs.execute('''CREATE TABLE zoo
 (critter VARCHAR(20) PRIMARY KEY,
 count INT,
 damages FLOAT)''')
<sqlite3.Cursor object at 0x1006a22d0>

Python 的三引号在创建长字符串(如 SQL 查询)时很方便。

现在,向动物园添加一些动物:

>>> curs.execute('INSERT INTO zoo VALUES("duck", 5, 0.0)')
<sqlite3.Cursor object at 0x1006a22d0>
>>> curs.execute('INSERT INTO zoo VALUES("bear", 2, 1000.0)')
<sqlite3.Cursor object at 0x1006a22d0>

有一种更安全的方式可以插入数据,即使用占位符

>>> ins = 'INSERT INTO zoo (critter, count, damages) VALUES(?, ?, ?)'
>>> curs.execute(ins, ('weasel', 1, 2000.0))
<sqlite3.Cursor object at 0x1006a22d0>

这次,我们在 SQL 中使用了三个问号来表示我们打算插入三个值,然后将这三个值作为元组传递给 execute() 函数。占位符处理繁琐的细节,如引用。它们保护您免受SQL 注入的攻击,这是一种将恶意 SQL 命令插入系统的外部攻击(在网络上很常见)。

现在,让我们看看是否可以再次把我们所有的动物都放出来:

>>> curs.execute('SELECT * FROM zoo')
<sqlite3.Cursor object at 0x1006a22d0>
>>> rows = curs.fetchall()
>>> print(rows)
[('duck', 5, 0.0), ('bear', 2, 1000.0), ('weasel', 1, 2000.0)]

让我们再次获取它们,但按计数排序:

>>> curs.execute('SELECT * from zoo ORDER BY count')
<sqlite3.Cursor object at 0x1006a22d0>
>>> curs.fetchall()
[('weasel', 1, 2000.0), ('bear', 2, 1000.0), ('duck', 5, 0.0)]

嘿,我们希望它们按降序排列:

>>> curs.execute('SELECT * from zoo ORDER BY count DESC')
<sqlite3.Cursor object at 0x1006a22d0>
>>> curs.fetchall()
[('duck', 5, 0.0), ('bear', 2, 1000.0), ('weasel', 1, 2000.0)]

哪种类型的动物给我们花费最多?

>>> curs.execute('''SELECT * FROM zoo WHERE
...     damages = (SELECT MAX(damages) FROM zoo)''')
<sqlite3.Cursor object at 0x1006a22d0>
>>> curs.fetchall()
[('weasel', 1, 2000.0)]

你可能会认为是熊。最好检查一下实际数据。

在我们离开 SQLite 之前,我们需要清理一下。如果我们打开了连接和游标,那么在完成时我们需要关闭它们:

>>> curs.close()
>>> conn.close()

MySQL

MySQL 是一个非常流行的开源关系型数据库。与 SQLite 不同,它是一个实际的服务器,因此客户端可以从网络上的不同设备访问它。

表 16-3 列出了您可以使用的驱动程序,以从 Python 访问 MySQL。有关所有 Python MySQL 驱动程序的更多详细信息,请参见python.org wiki

表 16-3. MySQL 驱动程序

Name Link Pypi package Import as Notes
mysqlclient https://https://mysqlclient.readthedocs.io mysql-connector-python MySQLdb
MySQL Connector http://bit.ly/mysql-cpdg mysql-connector-python mysql.connector
PYMySQL https://github.com/petehunt/PyMySQL pymysql pymysql
oursql http://pythonhosted.org/oursql oursql oursql 需要 MySQL C 客户端库

PostgreSQL

PostgreSQL 是一个功能齐全的开源关系型数据库。事实上,在许多方面,它比 MySQL 更先进。表 16-4 列出了您可以用来访问它的 Python 驱动程序。

表 16-4. PostgreSQL 驱动程序

Name Link Pypi package Import as Notes
psycopg2 http://initd.org/psycopg psycopg2 psycopg2 需要来自 PostgreSQL 客户端工具的 pg_config
py-postgresql https://pypi.org/project/py-postgresql py-postgresql postgresql

最受欢迎的驱动程序是 psycopg2,但它的安装需要 PostgreSQL 客户端库。

SQLAlchemy

对于所有关系数据库,SQL 并不完全相同,而 DB-API 也只能带你走到这一步。每个数据库都实现了一个反映其特性和哲学的特定方言。许多库试图以一种或另一种方式弥合这些差异。最流行的跨数据库 Python 库是 SQLAlchemy

它不在标准库中,但它是众所周知的,并被许多人使用。你可以通过使用这个命令在你的系统上安装它:

$ pip install sqlalchemy

你可以在几个层面上使用 SQLAlchemy:

  • 最低级别管理数据库连接,执行 SQL 命令并返回结果。这是最接近 DB-API 的层次。

  • 接下来是 SQL 表达式语言,它允许你以更加面向 Python 的方式表达查询。

  • 最高级别是 ORM(对象关系模型)层,它使用 SQL 表达语言并将应用程序代码与关系数据结构绑定。

随着我们的进行,你会理解在这些层次中术语的含义。SQLAlchemy 与前面章节中记录的数据库驱动程序一起使用。你不需要导入驱动程序;你提供给 SQLAlchemy 的初始连接字符串将决定它。这个字符串看起来像这样:

*`dialect`* + *`driver`* :// *`user`* : *`password`* @ *`host`* : *`port`* / *`dbname`*

你在这个字符串中放入的值如下:

dialect

数据库类型

driver

你想要用于该数据库的特定驱动程序

userpassword

您的数据库认证字符串

hostport

数据库服务器的位置(仅在不是该服务器的标准端口时需要)

dbname

最初连接到服务器的数据库

表 16-5 列出了方言和驱动程序。

表 16-5. SQLAlchemy 连接

方言 驱动程序
sqlite pysqlite(或省略)
mysql mysqlconnector
mysql pymysql
mysql oursql
postgresql psycopg2
postgresql pypostgresql

另请参阅关于 MySQLSQLitePostgreSQL其他数据库 的 SQLAlchemy 详细信息。

引擎层

首先,让我们尝试 SQLAlchemy 的最低级别,它只比基本的 DB-API 函数多做一些事情。

我们尝试使用内置到 Python 中的 SQLite。SQLite 的连接字符串省略了 hostportuserpassworddbname 告诉 SQLite 要使用哪个文件来存储你的数据库。如果省略 dbname,SQLite 将在内存中构建一个数据库。如果 dbname 以斜杠 (/) 开头,它是计算机上的绝对文件名(如在 Linux 和 macOS 中;例如,在 Windows 中是 C:\)。否则,它相对于当前目录。

下面的段落都属于一个程序的一部分,这里分开说明。

要开始,您需要导入所需内容。以下是一个 导入别名 的示例,它允许我们使用字符串 sa 来引用 SQLAlchemy 方法。我主要这样做是因为 sasqlalchemy 更容易输入:

>>> import sqlalchemy as sa

连接到数据库并在内存中创建存储它的位置(参数字符串 'sqlite:///:memory:' 也适用):

>>> conn = sa.create_engine('sqlite://')

创建一个名为 zoo 的数据库表,包含三列:

>>> conn.execute('''CREATE TABLE zoo
...     (critter VARCHAR(20) PRIMARY KEY,
...      count INT,
...      damages FLOAT)''')
<sqlalchemy.engine.result.ResultProxy object at 0x1017efb10>

运行 conn.execute() 会返回一个称为 ResultProxy 的 SQLAlchemy 对象。您很快就会看到如何处理它。

顺便说一句,如果您以前从未制作过数据库表,请恭喜。在您的待办清单中打勾。

现在,将三组数据插入到您的新空表中:

>>> ins = 'INSERT INTO zoo (critter, count, damages) VALUES (?, ?, ?)'
>>> conn.execute(ins, 'duck', 10, 0.0)
<sqlalchemy.engine.result.ResultProxy object at 0x1017efb50>
>>> conn.execute(ins, 'bear', 2, 1000.0)
<sqlalchemy.engine.result.ResultProxy object at 0x1017ef090>
>>> conn.execute(ins, 'weasel', 1, 2000.0)
<sqlalchemy.engine.result.ResultProxy object at 0x1017ef450>

接下来,向数据库请求我们刚刚放入的所有内容:

>>> rows = conn.execute('SELECT * FROM zoo')

在 SQLAlchemy 中,rows 不是一个列表;它是我们无法直接打印的那个特殊的 ResultProxy 东西:

>>> print(rows)
<sqlalchemy.engine.result.ResultProxy object at 0x1017ef9d0>

但是,您可以像迭代列表一样迭代它,因此我们可以逐行获取:

>>> for row in rows:
...     print(row)
...
('duck', 10, 0.0)
('bear', 2, 1000.0)
('weasel', 1, 2000.0)

这几乎与您之前看到的 SQLite DB-API 示例相同。一个优点是我们不需要在顶部导入数据库驱动程序;SQLAlchemy 从连接字符串中找到了这一点。只需更改连接字符串,即可将此代码移植到另一种类型的数据库。另一个优点是 SQLAlchemy 的 连接池,您可以在其 文档站点 上阅读有关它的信息。

SQL 表达式语言

上一级是 SQLAlchemy 的 SQL 表达式语言。它引入了函数来创建各种操作的 SQL。表达式语言处理的 SQL 方言差异比底层引擎层更多。对于关系数据库应用程序来说,它可以是一个方便的中间途径。

下面是如何创建和填充 zoo 表的方法。同样,这些是单个程序的连续片段。

导入和连接与之前相同:

>>> import sqlalchemy as sa
>>> conn = sa.create_engine('sqlite://')

要定义 zoo 表,我们开始使用一些表达式语言,而不是 SQL:

>>> meta = sa.MetaData()
>>> zoo = sa.Table('zoo', meta,
...     sa.Column('critter', sa.String, primary_key=True),
...     sa.Column('count', sa.Integer),
...     sa.Column('damages', sa.Float)
...    )
>>> meta.create_all(conn)

查看前面示例中多行调用中的括号。Table() 方法的结构与表的结构匹配。正如我们的表包含三列一样,在 Table() 方法调用的括号内有三次对 Column() 的调用。

与此同时,zoo 是一个魔术对象,连接了 SQL 数据库世界和 Python 数据结构世界。

使用更多表达式语言函数插入数据:

... conn.execute(zoo.insert(('bear', 2, 1000.0)))
<sqlalchemy.engine.result.ResultProxy object at 0x1017ea910>
>>> conn.execute(zoo.insert(('weasel', 1, 2000.0)))
<sqlalchemy.engine.result.ResultProxy object at 0x1017eab10>
>>> conn.execute(zoo.insert(('duck', 10, 0)))
<sqlalchemy.engine.result.ResultProxy object at 0x1017eac50>

接下来,创建 SELECT 语句(zoo.select() 选择由 zoo 对象表示的表中的所有内容,就像在普通 SQL 中执行 SELECT * FROM zoo 一样):

>>> result = conn.execute(zoo.select())

最后,获取结果:

>>> rows = result.fetchall()
>>> print(rows)
[('bear', 2, 1000.0), ('weasel', 1, 2000.0), ('duck', 10, 0.0)]

对象关系映射器(ORM)

在前一节中,zoo对象是 SQL 和 Python 之间的中间连接。在 SQLAlchemy 的顶层,对象关系映射器(ORM)使用 SQL 表达语言,但尝试使实际的数据库机制变得不可见。您定义类,ORM 处理如何将它们的数据进出数据库。复杂短语“对象关系映射器”的基本思想是,您可以在代码中引用对象,从而保持接近 Python 喜欢操作的方式,同时仍然使用关系数据库。

我们将定义一个Zoo类,并将其与 ORM 连接起来。这次,我们让 SQLite 使用文件zoo.db,以便确认 ORM 的工作。

与前两节类似,接下来的片段实际上是一个程序,由解释分隔开。如果您对某些内容不理解也不要担心。SQLAlchemy 文档中有所有细节——这些内容可能会变得复杂。我只是希望您了解做这件事情需要多少工作,这样您可以决定本章讨论的哪种方法最适合您。

初始导入是相同的,但这次我们还需要另外一些东西:

>>> import sqlalchemy as sa
>>> from sqlalchemy.ext.declarative import declarative_base

在这里,我们建立连接:

>>> conn = sa.create_engine('sqlite:///zoo.db')

现在,我们进入 SQLAlchemy 的 ORM。我们定义Zoo类,并关联其属性与表列:

>>> Base = declarative_base()
>>> class Zoo(Base):
...     __tablename__ = 'zoo'
...     critter = sa.Column('critter', sa.String, primary_key=True)
...     count = sa.Column('count', sa.Integer)
...     damages = sa.Column('damages', sa.Float)
...     def __init__(self, critter, count, damages):
...         self.critter = critter
...         self.count = count
...         self.damages = damages
...     def __repr__(self):
...         return "<Zoo({}, {}, {})>".format(self.critter, self.count,
...           self.damages)

下面的代码神奇地创建了数据库和表:

>>> Base.metadata.create_all(conn)

您可以通过创建 Python 对象来插入数据。ORM 在内部管理这些数据:

>>> first = Zoo('duck', 10, 0.0)
>>> second = Zoo('bear', 2, 1000.0)
>>> third = Zoo('weasel', 1, 2000.0)
>>> first
<Zoo(duck, 10, 0.0)>

接下来,我们让 ORM 带我们进入 SQL 世界。我们创建一个会话来与数据库交互:

>>> from sqlalchemy.orm import sessionmaker
>>> Session = sessionmaker(bind=conn)
>>> session = Session()

在会话中,我们将创建的三个对象写入数据库。add()函数添加一个对象,而add_all()添加一个列表:

>>> session.add(first)
>>> session.add_all([second, third])

最后,我们需要强制完成所有操作:

>>> session.commit()

它是否起作用?好吧,它在当前目录下创建了一个zoo.db文件。您可以使用命令行sqlite3程序来检查:

$ sqlite3 zoo.db
SQLite version 3.6.12
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .tables
zoo
sqlite> select * from zoo;
duck|10|0.0
bear|2|1000.0
weasel|1|2000.0

本节的目的是展示 ORM 是什么以及它如何在高层次上工作。SQLAlchemy 的作者写了一个完整的教程。阅读后,请决定以下哪种层次最适合您的需求:

  • 就像之前的 SQLite 部分中的普通 DB-API 一样

  • SQLAlchemy 引擎

  • SQLAlchemy 表达语言

  • SQLAlchemy ORM

使用 ORM 似乎是避免 SQL 复杂性的自然选择。您应该使用 ORM 吗?有些人认为应该避免使用 ORM,但其他人认为这种批评是过度的。不管谁是对的,ORM 都是一种抽象,所有抽象都会泄漏,并在某些时候出现问题。当 ORM 不能按您的意愿工作时,您必须弄清楚它的工作原理以及如何在 SQL 中修复它。借用互联网迷因:

有些人面对问题时会想:“我知道了,我会使用 ORM。”现在他们有两个问题。

对于简单的应用程序或将数据相当直接映射到数据库表的应用程序,请使用 ORM。如果应用程序如此简单,您可以考虑使用纯 SQL 或 SQL 表达式语言。

其他数据库访问包

如果您正在寻找能处理多个数据库的 Python 工具,具有比纯 db-api 更多功能但少于 SQLAlchemy 的功能,那么这些工具值得一看:

  • dataset声称其目标是“懒惰人的数据库”。它构建在 SQLAlchemy 之上,并为 SQL、JSON 和 CSV 存储提供了一个简单的 ORM。

  • records自称为“人类的 SQL”。它仅支持 SQL 查询,内部使用 SQLAlchemy 处理 SQL 方言问题、连接池和其他细节。它与tablib的集成(在“Tablib”中提到)允许您将数据导出为 CSV、JSON 和其他格式。

NoSQL 数据存储

关系表是矩形的,但数据有多种形状,可能非常难以适应,需要进行大量努力和扭曲。这是一个方孔/圆孔问题。

一些非关系数据库已被编写,允许更灵活的数据定义,以及处理非常大的数据集或支持自定义数据操作。它们被统称为NoSQL(原意为no SQL,现在是更不具对抗性的not only SQL)。

最简单类型的 NoSQL 数据库是键值存储。一个流行的排名展示了我在以下章节中涵盖的一些数据库。

dbm 家族

dbm格式在NoSQL标签被创造之前就存在了。它们是简单的键值存储,通常嵌入在诸如 Web 浏览器之类的应用程序中,用于维护各种设置。dbm 数据库类似于 Python 字典的以下方面:

  • 您可以为键分配一个值,并且它会自动保存到数据库中。

  • 您可以查询键的值。

以下是一个快速示例。以下open()方法的第二个参数是'r'表示读取,'w'表示写入,'c'表示两者,如果文件不存在则创建:

>>> import dbm
>>> db = dbm.open('definitions', 'c')

要创建键值对,只需像创建字典一样将值分配给键:

>>> db['mustard'] = 'yellow'
>>> db['ketchup'] = 'red'
>>> db['pesto'] = 'green'

让我们停下来检查一下我们到目前为止所做的:

>>> len(db)
3
>>> db['pesto']
b'green'

现在关闭,然后重新打开以查看它是否实际保存了我们给予的内容:

>>> db.close()
>>> db = dbm.open('definitions', 'r')
>>> db['mustard']
b'yellow'

键和值被存储为bytes。您不能对数据库对象db进行迭代,但可以使用len()来获取键的数量。get()setdefault()的工作方式与字典相同。

Memcached

memcached是一个快速的内存中键值缓存服务器。它经常被放在数据库前面,或用于存储 Web 服务器会话数据。

您可以在Linux 和 macOS以及Windows下载版本。如果您想尝试本节,您需要一个正在运行的 memcached 服务器和 Python 驱动程序。

有许多 Python 驱动程序;与 Python 3 兼容的一个是 python3-memcached,您可以使用以下命令安装它:

$ pip install python-memcached

要使用它,请连接到 memcached 服务器,之后您可以执行以下操作:

  • 为键设置和获取值

  • 通过使用incrdecr增加或减少值

  • 删除一个键

数据键和值是持久的,之前写入的数据可能会消失。这是 memcached 的固有特性——它是一个缓存服务器,而不是数据库,并通过丢弃旧数据来避免内存耗尽。

您可以同时连接多个 memcached 服务器。在下一个示例中,我们仅与同一台计算机上的一个服务器通信:

>>> import memcache
>>> db = memcache.Client(['127.0.0.1:11211'])
>>> db.set('marco', 'polo')
True
>>> db.get('marco')
'polo'
>>> db.set('ducks', 0)
True
>>> db.get('ducks')
0
>>> db.incr('ducks', 2)
2
>>> db.get('ducks')
2

Redis

Redis是一个数据结构服务器。它处理键及其值,但是与其他键值存储中的值相比,值更丰富。与 memcached 一样,Redis 服务器中的所有数据都应该适合内存。不同于 memcached,Redis 可以执行以下操作:

  • 将数据保存到磁盘以保证可靠性和重新启动

  • 保留旧数据

  • 提供比简单字符串更多的数据结构

Redis 的数据类型与 Python 的非常接近,并且 Redis 服务器可以成为一个或多个 Python 应用程序共享数据的有用中介。我发现它非常有用,因此在这里额外进行一些覆盖是值得的。

Python 驱动程序 redis-pyGitHub 上有其源代码和测试,以及 文档。您可以使用以下命令安装它:

$ pip install redis

Redis 服务器有很好的文档。如果在本地计算机上安装并启动 Redis 服务器(使用网络别名localhost),您可以尝试以下部分的程序。

字符串

一个具有单个值的键是 Redis 字符串。简单的 Python 数据类型会自动转换。连接到某个主机上的 Redis 服务器(默认为localhost)和端口(默认为6379):

>>> import redis
>>> conn = redis.Redis()

连接到redis.Redis('localhost')redis.Redis('localhost', 6379)将给出相同的结果。

列出所有键(目前没有):

>>> conn.keys('*')
[]

设置一个简单的字符串(键'secret')、整数(键'carats')和浮点数(键'fever'):

>>> conn.set('secret', 'ni!')
True
>>> conn.set('carats', 24)
True
>>> conn.set('fever', '101.5')
True

通过键获取值(作为 Python byte 值):

>>> conn.get('secret')
b'ni!'
>>> conn.get('carats')
b'24'
>>> conn.get('fever')
b'101.5'

在这里,setnx()方法仅在键不存在时设置一个值:

>>> conn.setnx('secret', 'icky-icky-icky-ptang-zoop-boing!')
False

因为我们已经定义了'secret',所以失败了:

>>> conn.get('secret')
b'ni!'

getset()方法返回旧值,并同时设置为新值:

>>> conn.getset('secret', 'icky-icky-icky-ptang-zoop-boing!')
b'ni!'

不要急于前进。这有用吗?

>>> conn.get('secret')
b'icky-icky-icky-ptang-zoop-boing!'

现在,使用getrange()获取子字符串(与 Python 中一样,偏移量0表示起始,-1表示结尾):

>>> conn.getrange('secret', -6, -1)
b'boing!'

使用setrange()替换子字符串(使用从零开始的偏移量):

>>> conn.setrange('secret', 0, 'ICKY')
32
>>> conn.get('secret')
b'ICKY-icky-icky-ptang-zoop-boing!'

接下来,使用mset()一次设置多个键:

>>> conn.mset({'pie': 'cherry', 'cordial': 'sherry'})
True

通过使用mget()一次获取多个值:

>>> conn.mget(['fever', 'carats'])
[b'101.5', b'24']

使用delete()方法删除一个键:

>>> conn.delete('fever')
True

使用incr()incrbyfloat()命令进行增量,并使用decr()进行减量:

>>> conn.incr('carats')
25
>>> conn.incr('carats', 10)
35
>>> conn.decr('carats')
34
>>> conn.decr('carats', 15)
19
>>> conn.set('fever', '101.5')
True
>>> conn.incrbyfloat('fever')
102.5
>>> conn.incrbyfloat('fever', 0.5)
103.0

没有decrbyfloat()。使用负增量来减少发烧:

>>> conn.incrbyfloat('fever', -2.0)
101.0

列表

Redis 列表只能包含字符串。当您进行第一次插入时,列表被创建。通过使用lpush()在开头插入:

>>> conn.lpush('zoo', 'bear')
1

在开头插入多个项目:

>>> conn.lpush('zoo', 'alligator', 'duck')
3

通过使用linsert()在值之前或之后插入:

>>> conn.linsert('zoo', 'before', 'bear', 'beaver')
4
>>> conn.linsert('zoo', 'after', 'bear', 'cassowary')
5

通过使用lset()在偏移处插入(列表必须已经存在):

>>> conn.lset('zoo', 2, 'marmoset')
True

通过使用rpush()在末尾插入:

>>> conn.rpush('zoo', 'yak')
6

通过使用lindex()按偏移量获取值:

>>> conn.lindex('zoo', 3)
b'bear'

通过使用lrange()获取偏移范围内的值(0 到-1 获取所有):

>>> conn.lrange('zoo', 0, 2)
[b'duck', b'alligator', b'marmoset']

使用ltrim()修剪列表,仅保留偏移范围内的元素:

>>> conn.ltrim('zoo', 1, 4)
True

通过使用lrange()获取值的范围(使用0-1获取所有):

>>> conn.lrange('zoo', 0, -1)
[b'alligator', b'marmoset', b'bear', b'cassowary']

第十五章展示了如何使用 Redis 列表和发布-订阅来实现作业队列。

哈希

Redis 哈希类似于 Python 字典,但只能包含字符串。此外,您只能深入到一级,不能创建深度嵌套的结构。以下是创建和操作名为song的 Redis 哈希的示例:

通过使用hmset()一次设置哈希song中的字段dore

>>> conn.hmset('song', {'do': 'a deer', 're': 'about a deer'})
True

通过使用hset()在哈希中设置单个字段值:

>>> conn.hset('song', 'mi', 'a note to follow re')
1

通过使用hget()获取一个字段的值:

>>> conn.hget('song', 'mi')
b'a note to follow re'

通过使用hmget()获取多个字段值:

>>> conn.hmget('song', 're', 'do')
[b'about a deer', b'a deer']

通过使用hkeys()获取哈希的所有字段键:

>>> conn.hkeys('song')
[b'do', b're', b'mi']

通过使用hvals()获取哈希的所有字段值:

>>> conn.hvals('song')
[b'a deer', b'about a deer', b'a note to follow re']

通过使用hlen()获取哈希中字段的数量:

>>> conn.hlen('song')
3

通过使用hgetall()获取哈希中的所有字段键和值:

>>> conn.hgetall('song')
{b'do': b'a deer', b're': b'about a deer', b'mi': b'a note to follow re'}

如果其键不存在,则使用hsetnx()设置字段:

>>> conn.hsetnx('song', 'fa', 'a note that rhymes with la')
1

集合

Redis 集合与 Python 集合类似,您将在以下示例中看到。

向集合添加一个或多个值:

>>> conn.sadd('zoo', 'duck', 'goat', 'turkey')
3

获取集合的值的数量:

>>> conn.scard('zoo')
3

获取集合的所有值:

>>> conn.smembers('zoo')
{b'duck', b'goat', b'turkey'}

从集合中移除一个值:

>>> conn.srem('zoo', 'turkey')
True

让我们再建立一个集合来展示一些集合操作:

>>> conn.sadd('better_zoo', 'tiger', 'wolf', 'duck')
0

求交集(获取zoobetter_zoo集合的共同成员):

>>> conn.sinter('zoo', 'better_zoo')
{b'duck'}

获取zoobetter_zoo的交集,并将结果存储在集合fowl_zoo中:

>>> conn.sinterstore('fowl_zoo', 'zoo', 'better_zoo')
1

里面有谁?

>>> conn.smembers('fowl_zoo')
{b'duck'}

获取zoobetter_zoo的并集(所有成员):

>>> conn.sunion('zoo', 'better_zoo')
{b'duck', b'goat', b'wolf', b'tiger'}

将联合结果存储在集合fabulous_zoo中:

>>> conn.sunionstore('fabulous_zoo', 'zoo', 'better_zoo')
4
>>> conn.smembers('fabulous_zoo')
{b'duck', b'goat', b'wolf', b'tiger'}

zoo有什么,better_zoo没有?使用sdiff()获取集合的差集,并使用sdiffstore()将其保存在zoo_sale集合中:

>>> conn.sdiff('zoo', 'better_zoo')
{b'goat'}
>>> conn.sdiffstore('zoo_sale', 'zoo', 'better_zoo')
1
>>> conn.smembers('zoo_sale')
{b'goat'}

排序集合

最多用途的 Redis 数据类型之一是排序集合,或zset。它是一组唯一值,但每个值都有一个关联的浮点数分数。您可以通过其值或分数访问每个项目。排序集合有许多用途:

  • 排行榜

  • 二级索引

  • 时间序列,使用时间戳作为分数

我们展示了最后一个用例,通过时间戳跟踪用户登录。我们使用 Python time()函数返回的 Unix epoch值(更多详情请参见第十五章):

>>> import time
>>> now = time.time()
>>> now
1361857057.576483

让我们添加第一个看起来紧张的客人:

>>> conn.zadd('logins', 'smeagol', now)
1

五分钟后,另一位客人:

>>> conn.zadd('logins', 'sauron', now+(5*60))
1

两小时后:

>>> conn.zadd('logins', 'bilbo', now+(2*60*60))
1

一天后,不要着急:

>>> conn.zadd('logins', 'treebeard', now+(24*60*60))
1

bilbo以什么顺序到达?

>>> conn.zrank('logins', 'bilbo')
2

那是什么时候?

>>> conn.zscore('logins', 'bilbo')
1361864257.576483

让我们按登录顺序查看每个人:

>>> conn.zrange('logins', 0, -1)
[b'smeagol', b'sauron', b'bilbo', b'treebeard']

请附上它们的时间:

>>> conn.zrange('logins', 0, -1, withscores=True)
[(b'smeagol', 1361857057.576483), (b'sauron', 1361857357.576483),
(b'bilbo', 1361864257.576483), (b'treebeard', 1361943457.576483)]

缓存和过期

所有的 Redis 键都有一个到期时间,默认情况下是永久的。我们可以使用expire()函数来指示 Redis 保留键的时间长度。该值是以秒为单位的数字:

>>> import time
>>> key = 'now you see it'
>>> conn.set(key, 'but not for long')
True
>>> conn.expire(key, 5)
True
>>> conn.ttl(key)
5
>>> conn.get(key)
b'but not for long'
>>> time.sleep(6)
>>> conn.get(key)
>>>

expireat()命令在给定的纪元时间点过期键。键的过期对于保持缓存新鲜和限制登录会话非常有用。类比:在你的杂货店货架后面的冷藏室中,当牛奶达到保质期时,店员们就会将那些加仑装出货。

文档数据库

文档数据库是一种 NoSQL 数据库,它以不同的字段存储数据。与关系表(每行具有相同列的矩形表)相比,这样的数据是“参差不齐”的,每行具有不同的字段(列),甚至是嵌套字段。你可以使用 Python 字典和列表在内存中处理这样的数据,或者将其存储为 JSON 文件。要将这样的数据存储在关系数据库表中,你需要定义每个可能的列,并使用 null 来表示缺失的数据。

ODM可以代表对象数据管理器或对象文档映射器(至少它们同意“O”部分)。ODM 是文档数据库的关系数据库 ORM 对应物。一些流行的文档数据库和工具(驱动程序和 ODM)列在表 16-6 中。

表 16-6. 文档数据库

数据库 Python API
Mongo tools
DynamoDB boto3
CouchDB couchdb
注意

PostgreSQL 可以做一些文档数据库可以做的事情。它的一些扩展允许它逃离关系规范,同时保留事务、数据验证和外键等特性:1)多维数组 - 在表单元格中存储多个值;2)jsonb - 在单元格中存储 JSON 数据,并进行完整的索引和查询。

时间序列数据库

时间序列数据可以在固定间隔(如计算机性能指标)或随机时间收集,这导致了许多存储方法。其中许多的一些,一些具有 Python 支持的方法列在表 16-7 中。

表 16-7. 时间数据库

数据库 Python API
InfluxDB influx-client
kdb+ PyQ
Prometheus prometheus_client
TimescaleDB (PostgreSQL clients)
OpenTSDB potsdb
PyStore PyStore

图数据库

对于需要有自己数据库类别的最后一种数据案例,我们有 图形节点(数据)通过 边缘顶点(关系)相连。一个个体 Twitter 用户 可以是一个节点,与其他用户的关系如 关注被关注 为边。

随着社交媒体的增长,图形数据变得更加明显,价值在于连接而非内容本身。一些流行的图数据库在 表 16-8 中概述。

表 16-8. 图数据库

数据库 Python API
Neo4J py2neo
OrientDB pyorient
ArangoDB pyArango

其他 NoSQL

这里列出的 NoSQL 服务器处理比内存更大的数据,并且许多使用多台计算机。表 16-9 展示了显著的服务器及其 Python 库。

表 16-9. NoSQL 数据库

数据库 Python API
Cassandra pycassa
CouchDB couchdb-python
HBase happybase
Kyoto Cabinet kyotocabinet
MongoDB mongodb
Pilosa python-pilosa
Riak riak-python-client

全文搜索数据库

最后,还有一个专门用于 全文 搜索的数据库类别。它们索引所有内容,因此您可以找到那些讲述风车和巨大芝士轮的诗歌。您可以在 表 16-10 中看到一些流行的开源示例及其 Python API。

表 16-10. 全文搜索数据库

网站 Python API
Lucene pylucene
Solr SolPython
ElasticSearch elasticsearch
Sphinx sphinxapi
Xapian xappy
Whoosh (用 Python 编写,包含 API)

即将出现

前一章讨论了在时间上交错使用代码(并发)。接下来的章节将介绍如何在空间中移动数据(网络),这不仅可以用于并发,还有其他用途。

待办事项

16.1 将以下文本行保存到名为 books.csv 的文件中(注意,如果字段由逗号分隔,如果字段包含逗号,你需要用引号括起来):

author,book
J R R Tolkien,The Hobbit
Lynne Truss,"Eats, Shoots & Leaves"

16.2 使用 csv 模块及其 DictReader 方法读取 books.csv 并存入变量 books。打印 books 中的值。DictReader 是否处理了第二本书标题中的引号和逗号?

16.3 使用以下代码创建一个名为 books2.csv 的 CSV 文件:

title,author,year
The Weirdstone of Brisingamen,Alan Garner,1960
Perdido Street Station,China Miéville,2000
Thud!,Terry Pratchett,2005
The Spellman Files,Lisa Lutz,2007
Small Gods,Terry Pratchett,1992

16.4 使用 sqlite3 模块创建一个名为 books.db 的 SQLite 数据库,并创建一个名为 books 的表,具有以下字段:title(文本)、author(文本)和 year(整数)。

16.5 读取 books2.csv 并将其数据插入到 book 表中。

16.6 按字母顺序选择并打印 book 表中的 title 列。

16.7 按照出版顺序选择并打印 book 表中的所有列。

16.8 使用 sqlalchemy 模块连接到你在练习 16.4 中刚刚创建的 sqlite3 数据库 books.db。如同 16.6,按字母顺序选择并打印 book 表中的 title 列。

16.9 在你的计算机上安装 Redis 服务器和 Python redis 库(pip install redis)。创建一个名为 test 的 Redis 哈希,具有字段 count1)和 name'Fester Bestertester')。打印 test 的所有字段。

16.10 增加 testcount 字段并打印它。

¹ 啊,还没到 XML 的时候。

第十七章:空间中的数据:网络

时间是自然使一切都不同时发生的方式。空间则是防止一切同时发生于我身上的东西。

关于时间的引语

在 第十五章 中,您了解了 并发性:如何同时做多件事情。现在我们将尝试在多个地方做事情:分布式计算网络。有许多充分的理由来挑战时间和空间:

性能

您的目标是保持快速组件繁忙,而不是等待慢组件。

鲁棒性

数量越多越安全,因此您希望复制任务以解决硬件和软件故障。

简洁性

最佳实践是将复杂的任务分解成许多更容易创建、理解和修复的小任务。

可伸缩性

增加服务器以处理负载,减少以节省成本。

在本章中,我们从网络基元向更高级别的概念发展。让我们从 TCP/IP 和套接字开始。

TCP/IP

互联网基于关于如何建立连接、交换数据、终止连接、处理超时等规则。这些被称为 协议,它们按层次排列。层次的目的是允许创新和以不同方式做事情的替代方式;只要您在处理上层和下层约定时遵循惯例,您可以在任何一层上做任何您想做的事情。

最低层次管理诸如电信号等方面;每个更高的层次都建立在下面的层次之上。中间差不多是 IP(Internet Protocol)层,它指定网络位置的寻址方式以及数据包(数据块)的流动方式。在其上一层,有两个协议描述如何在位置之间传递字节:

UDP(用户数据报协议)

这用于短期交流。数据报 是一种以单次传送为单位的微小消息,就像明信片上的便签。

TCP(传输控制协议)

这个协议用于持久连接。它发送字节,确保它们按顺序到达且不重复。

UDP 消息不会收到确认,因此你永远不确定它们是否到达目的地。如果你想通过 UDP 讲笑话:

Here's a UDP joke. Get it?

TCP 在发送方和接收方之间建立秘密握手以确保良好的连接。一个 TCP 笑话会以这样开始:

Do you want to hear a TCP joke?
Yes, I want to hear a TCP joke.
Okay, I'll tell you a TCP joke.
Okay, I'll hear a TCP joke.
Okay, I'll send you a TCP joke now.
Okay, I'll receive the TCP joke now.
... (and so on)

您的本地机器始终具有 IP 地址 127.0.0.1 和名称 localhost。您可能会看到这被称为 环回接口。如果连接到互联网,您的机器还将有一个 公共 IP。如果您只是使用家用计算机,则它位于诸如电缆调制解调器或路由器等设备之后。您可以在同一台机器上的进程之间运行互联网协议。

我们与之交互的大多数互联网——网页、数据库服务器等——都是基于 TCP 协议运行在 IP 协议之上的;简称 TCP/IP。让我们先看一些基本的互联网服务。之后,我们探索通用的网络模式。

套接字

如果你想知道事情是如何工作的,一直到底层,这一部分是为你准备的。

网络编程的最底层使用了一个socket,从 C 语言和 Unix 操作系统中借用而来。套接字级别的编码很繁琐。使用像 ZeroMQ 这样的东西会更有趣,但看到底层是很有用的。例如,当发生网络错误时,关于套接字的消息经常出现。

让我们编写一个非常简单的客户端-服务器交换,一次使用 UDP,一次使用 TCP。在 UDP 示例中,客户端将字符串发送到服务器的 UDP 数据报中,服务器返回一个包含字符串的数据包。服务器需要在特定地址和端口上监听,就像一个邮局和一个邮箱一样。客户端需要知道这两个值以便发送消息并接收任何回复。

在以下客户端和服务器代码中,address是一个元组(地址端口)。address是一个字符串,可以是名称或IP 地址。当你的程序只是在同一台机器上相互交流时,你可以使用名称'localhost'或等效的地址字符串'127.0.0.1'

首先,让我们从一个进程向另一个进程发送一些数据,并将一些数据返回给发送者。第一个程序是客户端,第二个是服务器。在每个程序中,我们打印时间并打开一个套接字。服务器将监听其套接字的连接,客户端将写入其套接字,该套接字将向服务器传输一条消息。

示例 17-1 呈现了第一个程序,udp_server.py

示例 17-1。udp_server.py
from datetime import datetime
import socket

server_address = ('localhost', 6789)
max_size = 4096

print('Starting the server at', datetime.now())
print('Waiting for a client to call.')
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.bind(server_address)

data, client = server.recvfrom(max_size)

print('At', datetime.now(), client, 'said', data)
server.sendto(b'Are you talking to me?', client)
server.close()

服务器必须通过从socket包导入的两种方法来设置网络。第一个方法,socket.socket,创建一个套接字,第二个方法,bind绑定到它(监听到达该 IP 地址和端口的任何数据)。AF_INET表示我们将创建一个 IP 套接字。(还有另一种类型的Unix 域套接字,但这些仅在本地机器上工作。)SOCK_DGRAM表示我们将发送和接收数据报,换句话说,我们将使用 UDP。

此时,服务器坐在那里等待数据报的到来(recvfrom)。当数据报到达时,服务器将唤醒并获取数据以及关于客户端的信息。client变量包含了到达客户端所需的地址和端口组合。服务器最后通过发送回复并关闭连接来结束。

让我们看一下udp_client.py(示例 17-2)。

示例 17-2。udp_client.py
import socket
from datetime import datetime

server_address = ('localhost', 6789)
max_size = 4096

print('Starting the client at', datetime.now())
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client.sendto(b'Hey!', server_address)
data, server = client.recvfrom(max_size)
print('At', datetime.now(), server, 'said', data)
client.close()

客户端有大部分与服务器相同的方法(除了bind())。客户端先发送再接收,而服务器先接收。

首先,在它自己的窗口中启动服务器。它将打印问候语,然后静静地等待,直到客户端发送一些数据:

$ python udp_server.py
Starting the server at 2014-02-05 21:17:41.945649
Waiting for a client to call.

接下来,在另一个窗口中启动客户端。它将打印问候语,向服务器发送数据(字节值为'Hey'),打印回复,然后退出:

$ python udp_client.py
Starting the client at 2014-02-05 21:24:56.509682
At 2014-02-05 21:24:56.518670 ('127.0.0.1', 6789) said b'Are you talking to me?'

最后,服务器将打印接收到的消息,并退出:

At 2014-02-05 21:24:56.518473 ('127.0.0.1', 56267) said b'Hey!'

客户端需要知道服务器的地址和端口号,但不需要为自己指定端口号。系统会自动分配端口号——在这种情况下是 56267

注意

UDP 以单个数据块发送数据。它不保证传递。如果通过 UDP 发送多个消息,它们可能无序到达,或者根本不到达。UDP 快速、轻便、无连接且不可靠。UDP 在需要快速推送数据包并且可以偶尔容忍丢失数据包的情况下非常有用,比如 VoIP(互联网电话)。

这让我们来谈谈 TCP(传输控制协议)。TCP 用于更长时间的连接,比如 Web。TCP 按照发送顺序传递数据。如果出现任何问题,它会尝试重新发送。这使得 TCP 比 UDP 稍慢,但通常在需要所有数据包按正确顺序到达时更可靠。

注意

Web 协议 HTTP 的前两个版本基于 TCP,但 HTTP/3 基于一个称为 QUIC 的协议,QUIC 本身使用 UDP。因此,在 UDP 和 TCP 之间选择可能涉及许多因素。

让我们使用 TCP 从客户端到服务器再返回射击几个数据包。

tcp_client.py 的行为类似于之前的 UDP 客户端,只向服务器发送一个字符串,但套接字调用中有小差异,示例 17-3 中有所说明。

示例 17-3. tcp_client.py
import socket
from datetime import datetime

address = ('localhost', 6789)
max_size = 1000

print('Starting the client at', datetime.now())
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(address)
client.sendall(b'Hey!')
data = client.recv(max_size)
print('At', datetime.now(), 'someone replied', data)
client.close()

我们已经用 SOCK_STREAM 替换了 SOCK_DGRAM 以获取流协议 TCP。我们还添加了一个 connect() 调用来建立流。我们在 UDP 中不需要这样做,因为每个数据报都是独立的,存在于广阔而不受控制的互联网上。

正如示例 17-4 所示,tcp_server.py 与其 UDP 表兄弟也有所不同。

示例 17-4. tcp_server.py
from datetime import datetime
import socket

address = ('localhost', 6789)
max_size = 1000

print('Starting the server at', datetime.now())
print('Waiting for a client to call.')
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(address)
server.listen(5)

client, addr = server.accept()
data = client.recv(max_size)

print('At', datetime.now(), client, 'said', data)
client.sendall(b'Are you talking to me?')
client.close()
server.close()

server.listen(5) 被配置为在拒绝新连接之前排队最多五个客户端连接。server.accept() 获取到达的第一个可用消息。

client.recv(1000) 设置了最大可接受消息长度为 1,000 字节。

就像之前一样,先启动服务器,然后启动客户端,看看有趣的事情发生了。首先是服务器:

$ python tcp_server.py
Starting the server at 2014-02-06 22:45:13.306971
Waiting for a client to call.
At 2014-02-06 22:45:16.048865 <socket.socket object, fd=6, family=2, type=1,
    proto=0> said b'Hey!'

现在,启动客户端。它会将消息发送到服务器,接收响应,然后退出:

$ python tcp_client.py
Starting the client at 2014-02-06 22:45:16.038642
At 2014-02-06 22:45:16.049078 someone replied b'Are you talking to me?'

服务器收集消息,打印消息,响应消息,然后退出:

At 2014-02-06 22:45:16.048865 <socket.socket object, fd=6, family=2, type=1,
    proto=0> said b'Hey!'

注意到 TCP 服务器调用了 client.sendall() 来响应,而之前的 UDP 服务器调用了 client.sendto()。TCP 在多次套接字调用中维护客户端-服务器连接,并记住客户端的 IP 地址。

这看起来还不错,但如果你尝试写更复杂的内容,你会看到套接字在低层真正的操作方式:

  • UDP 发送消息,但其大小受限且不能保证到达目的地。

  • TCP 发送字节流,而不是消息。你不知道系统在每次调用中会发送或接收多少字节。

  • 要使用 TCP 交换整个消息,您需要一些额外的信息来从其段重新组装完整消息:固定的消息大小(字节),或完整消息的大小,或一些分隔字符。

  • 因为消息是字节而不是 Unicode 文本字符串,所以您需要使用 Python 的bytes类型。有关更多信息,请参阅第十二章。

在这一切之后,如果你对套接字编程感兴趣,请查看 Python 套接字编程HOWTO获取更多详细信息。

Scapy

有时候你需要深入网络流并观察数据字节的传递。您可能需要调试 Web API 或追踪某些安全问题。scapy库和程序提供了一个领域特定语言,用于在 Python 中创建和检查数据包,这比编写和调试等效的 C 程序要容易得多。

标准安装使用pip install scapy文档非常详尽。如果您使用tcpdumpwireshark等工具来调查 TCP 问题,您应该查看scapy。最后,请不要将scapyscrapy混淆,后者在“爬取和抓取”中有介绍。

Netcat

另一个测试网络和端口的工具是Netcat,通常缩写为nc。这里有一个连接到谷歌网站的 HTTP 示例,并请求其主页的一些基本信息:

$ $ nc www.google.com 80
HEAD / HTTP/1.1

HTTP/1.1 200 OK
Date: Sat, 27 Jul 2019 21:04:02 GMT
...

在下一章中,有一个示例使用“使用 telnet 进行测试”来做同样的事情。

网络模式

您可以从一些基本模式构建网络应用程序:

  • 最常见的模式是请求-回复,也称为请求-响应客户端-服务器。这种模式是同步的:客户端等待服务器响应。在本书中您已经看到了许多请求-响应的例子。您的 Web 浏览器也是一个客户端,向 Web 服务器发送 HTTP 请求,后者返回一个响应。

  • 另一种常见模式是推送扇出:您将数据发送到进程池中的任何可用工作进程。一个例子是负载均衡器后面的 Web 服务器。

  • 推送的反义是拉取扇入:您从一个或多个来源接受数据。一个例子是一个日志记录器,它从多个进程接收文本消息并将它们写入单个日志文件。

  • 一种模式类似于广播电台或电视广播:发布-订阅,或pub-sub。使用此模式,发布者发送数据。在简单的发布-订阅系统中,所有订阅者都会收到一份副本。更常见的是,订阅者可以指示他们仅对某些类型的数据感兴趣(通常称为主题),发布者将仅发送这些数据。因此,与推送模式不同,可能会有多个订阅者收到给定数据。如果没有主题的订阅者,则数据将被忽略。

让我们展示一些请求-响应的例子,稍后再展示一些发布-订阅的例子。

请求-响应模式

这是最熟悉的模式。您可以从适当的服务器请求 DNS、Web 或电子邮件数据,它们会回复,或告诉您是否有问题。

我刚刚向你展示了如何使用 UDP 或 TCP 进行一些基本的请求,但在套接字级别上构建网络应用程序很难。让我们看看 ZeroMQ 是否可以帮助解决这个问题。

ZeroMQ

ZeroMQ 是一个库,不是一个服务器。有时被描述为增强版套接字,ZeroMQ 套接字执行的是您预期普通套接字能够执行的操作:

  • 交换整个消息

  • 重试连接

  • 缓冲数据以在发件人和接收者之间的时间不匹配时保护它们

在线指南写得很好,富有幽默感,并且它提供了我见过的最佳网络模式描述。印刷版(ZeroMQ: Messaging for Many Applications,作者 Pieter Hintjens,出版商是 O'Reilly,上面有一条大鱼)有着良好的代码质量,但不是反过来。印刷指南中的所有示例都是用 C 语言编写的,但在线版本可以让您为每个代码示例选择多种语言,Python 的示例也可以查看。在本章中,我将向您展示一些基本的请求-回复 ZeroMQ 示例。

ZeroMQ 就像一个乐高积木套件,我们都知道您可以从几个乐高形状构建出各种惊人的东西。在这种情况下,您从几种套接字类型和模式构建网络。以下列出的基本“乐高积木”是 ZeroMQ 套接字类型,出于某种巧合,它们看起来像我们已经讨论过的网络模式:

  • REQ(同步请求)

  • REP(同步回复)

  • DEALER(异步请求)

  • ROUTER(异步回复)

  • PUB(发布)

  • SUB(订阅)

  • PUSH(扇出)

  • PULL(汇聚)

要自己尝试这些内容,您需要通过输入以下命令安装 Python ZeroMQ 库:

$ pip install pyzmq

最简单的模式是一个单一的请求-回复对。这是同步的:一个套接字发送请求,然后另一个套接字回复。首先是回复(服务器)的代码,zmq_server.py,如示例 17-5 所示。

示例 17-5. zmq_server.py
import zmq

host = '127.0.0.1'
port = 6789
context = zmq.Context()
server = context.socket(zmq.REP)
server.bind("tcp://%s:%s" % (host, port))
while True:
    #  Wait for next request from client
    request_bytes = server.recv()
    request_str = request_bytes.decode('utf-8')
    print("That voice in my head says: %s" % request_str)
    reply_str = "Stop saying: %s" % request_str
    reply_bytes = bytes(reply_str, 'utf-8')
    server.send(reply_bytes)

我们创建一个Context对象:这是一个维护状态的 ZeroMQ 对象。然后,我们创建一个类型为REP(代表回复)的 ZeroMQsocket。我们调用bind()方法使其监听特定的 IP 地址和端口。请注意,它们以字符串形式指定,如'tcp://localhost:6789',而不是像普通套接字示例中的元组。

此示例保持接收来自发送方的请求并发送响应。消息可能非常长,但 ZeroMQ 会处理这些细节。

示例 17-6 展示了相应请求(客户端)的代码,zmq_client.py。其类型为REQ(代表请求),并调用了connect()而不是bind()

示例 17-6. zmq_client.py
import zmq

host = '127.0.0.1'
port = 6789
context = zmq.Context()
client = context.socket(zmq.REQ)
client.connect("tcp://%s:%s" % (host, port))
for num in range(1, 6):
    request_str = "message #%s" % num
    request_bytes = request_str.encode('utf-8')
    client.send(request_bytes)
    reply_bytes = client.recv()
    reply_str = reply_bytes.decode('utf-8')
    print("Sent %s, received %s" % (request_str, reply_str))

现在是时候启动它们了。与普通套接字示例的一个有趣区别是,你可以按任意顺序启动服务器和客户端。请在一个窗口后台启动服务器:

$ python zmq_server.py &

在同一窗口中启动客户端:

$ python zmq_client.py

你会看到客户端和服务器交替输出的这些行:

That voice in my head says 'message #1'
Sent 'message #1', received 'Stop saying message #1'
That voice in my head says 'message #2'
Sent 'message #2', received 'Stop saying message #2'
That voice in my head says 'message #3'
Sent 'message #3', received 'Stop saying message #3'
That voice in my head says 'message #4'
Sent 'message #4', received 'Stop saying message #4'
That voice in my head says 'message #5'
Sent 'message #5', received 'Stop saying message #5'

我们的客户端在发送其第五条消息后结束,但我们没有告诉服务器退出,所以它仍然在等待另一条消息。如果您再次运行客户端,它将打印相同的五行,服务器也会打印它的五行。如果您不终止 zmq_server.py 进程并尝试运行另一个,Python 将投诉地址已经在使用中。

$ python zmq_server.py
[2] 356
Traceback (most recent call last):
  File "zmq_server.py", line 7, in <module>
    server.bind("tcp://%s:%s" % (host, port))
  File "socket.pyx", line 444, in zmq.backend.cython.socket.Socket.bind
      (zmq/backend/cython/socket.c:4076)
  File "checkrc.pxd", line 21, in zmq.backend.cython.checkrc._check_rc
      (zmq/backend/cython/socket.c:6032)
zmq.error.ZMQError: Address already in use

消息需要以字节字符串形式发送,因此我们以 UTF-8 格式对示例的文本字符串进行了编码。您可以发送任何类型的消息,只要将其转换为 bytes。我们使用简单的文本字符串作为消息的源,因此 encode()decode() 足以进行字节字符串的转换。如果您的消息具有其他数据类型,可以使用像 MessagePack 这样的库。

即使这种基本的 REQ-REP 模式也允许一些花式通信模式,因为任意数量的 REQ clients 可以 connect() 到单个 REP server。服务器一次处理一个请求,同步地,但不会丢弃同时到达的其他请求。ZeroMQ 缓冲消息,直到它们能够通过,这是它名字中 Q 的来源。Q 代表队列,M 代表消息,而 Zero 意味着不需要任何代理。

虽然 ZeroMQ 不强制使用任何中央代理(中介),但可以在需要时构建它们。例如,使用 DEALER 和 ROUTER sockets 异步连接多个源和/或目的地。

多个 REQ sockets 连接到单个 ROUTER,后者将每个请求传递给 DEALER,然后再联系连接到它的任何 REP sockets(Figure 17-1)。这类似于一堆浏览器联系位于 web 服务器群前面的代理服务器。它允许您根据需要添加多个客户端和服务器。

REQ sockets 只连接到 ROUTER socket;DEALER 则连接到它后面的多个 REP sockets。ZeroMQ 处理繁琐的细节,确保请求负载平衡,并且确保回复返回到正确的地方。

另一个网络模式称为ventilator,使用 PUSH sockets 分发异步任务,并使用 PULL sockets 收集结果。

ZeroMQ 的最后一个显著特点是,通过在创建时改变套接字的连接类型,它可以进行上下的扩展:

  • tcp 是在一台或多台机器上进程之间的通信方式。

  • ipc 是在同一台机器上进程之间的通信方式。

  • inproc 是单个进程内线程之间的通信方式。

最后一个,inproc,是一种在不使用锁的情况下在线程之间传递数据的方式,也是在“Threads”中 threading 示例的替代方式。

inp2 1701

图 17-1. 使用经纪人连接多个客户端和服务

使用了 ZeroMQ 后,您可能不再想编写原始套接字代码。

其他消息传递工具

ZeroMQ 绝对不是 Python 支持的唯一消息传递库。消息传递是网络中最流行的概念之一,而 Python 与其他语言保持同步:

  • Apache 项目,我们在“Apache”中看到其 Web 服务器,还维护 ActiveMQ 项目,包括使用简单文本 STOMP 协议的几个 Python 接口。

  • RabbitMQ 也很受欢迎,它有有用的在线 Python 教程

  • NATS 是一个快速的消息系统,使用 Go 编写。

发布-订阅模式

发布-订阅不是队列,而是广播。一个或多个进程发布消息。每个订阅进程指示它想接收哪些类型的消息。每条消息的副本都发送给与其类型匹配的每个订阅者。因此,给定消息可能会被处理一次,多次或根本不处理。就像一个孤独的无线电操作员一样,每个发布者只是广播,并不知道谁在听。

Redis

在第十六章中,您已经看过 Redis,主要作为数据结构服务器,但它也包含发布-订阅系统。发布者通过主题和值发送消息,订阅者指定它们想要接收的主题。

示例 17-7 包含一个发布者,redis_pub.py

示例 17-7. redis_pub.py
import redis
import random

conn = redis.Redis()
cats = ['siamese', 'persian', 'maine coon', 'norwegian forest']
hats = ['stovepipe', 'bowler', 'tam-o-shanter', 'fedora']
for msg in range(10):
    cat = random.choice(cats)
    hat = random.choice(hats)
    print('Publish: %s wears a %s' % (cat, hat))
    conn.publish(cat, hat)

每个主题都是一种猫的品种,伴随的消息是一种帽子的类型。

示例 17-8 展示了一个单一的订阅者,redis_sub.py

示例 17-8. redis_sub.py
import redis
conn = redis.Redis()

topics = ['maine coon', 'persian']
sub = conn.pubsub()
sub.subscribe(topics)
for msg in sub.listen():
    if msg['type'] == 'message':
        cat = msg['channel']
        hat = msg['data']
        print('Subscribe: %s wears a %s' % (cat, hat))

此订阅者希望接收所有 'maine coon''persian' 类型的消息,而不是其他类型。listen() 方法返回一个字典。如果其类型是 'message',则它是由发布者发送并符合我们的标准。 'channel' 键是主题(猫),'data' 键包含消息(帽子)。

如果您先启动发布者而没有人在监听,就像一位哑剧艺术家在森林中倒下(他会发出声音吗?),所以先启动订阅者:

$ python redis_sub.py

接下来启动发布者。它会发送 10 条消息然后退出:

$ python redis_pub.py
Publish: maine coon wears a stovepipe
Publish: norwegian forest wears a stovepipe
Publish: norwegian forest wears a tam-o-shanter
Publish: maine coon wears a bowler
Publish: siamese wears a stovepipe
Publish: norwegian forest wears a tam-o-shanter
Publish: maine coon wears a bowler
Publish: persian wears a bowler
Publish: norwegian forest wears a bowler
Publish: maine coon wears a stovepipe

订阅者只关心两种类型的猫:

$ python redis_sub.py
Subscribe: maine coon wears a stovepipe
Subscribe: maine coon wears a bowler
Subscribe: maine coon wears a bowler
Subscribe: persian wears a bowler
Subscribe: maine coon wears a stovepipe

我们没有告诉订阅者退出,所以它仍在等待消息。如果重新启动发布者,订阅者将抓取更多消息并打印它们。

您可以拥有任意数量的订阅者(和发布者)。如果没有消息的订阅者,消息会从 Redis 服务器中消失。但是,如果有订阅者,消息会一直留在服务器中,直到所有订阅者都检索到它们。

ZeroMQ

ZeroMQ 没有中央服务器,因此每个发布者都向所有订阅者写入。发布者 zmq_pub.py 提供在示例 17-9。

示例 17-9. zmq_pub.py
import zmq
import random
import time
host = '*'
port = 6789
ctx = zmq.Context()
pub = ctx.socket(zmq.PUB)
pub.bind('tcp://%s:%s' % (host, port))
cats = ['siamese', 'persian', 'maine coon', 'norwegian forest']
hats = ['stovepipe', 'bowler', 'tam-o-shanter', 'fedora']
time.sleep(1)
for msg in range(10):
    cat = random.choice(cats)
    cat_bytes = cat.encode('utf-8')
    hat = random.choice(hats)
    hat_bytes = hat.encode('utf-8')
    print('Publish: %s wears a %s' % (cat, hat))
    pub.send_multipart([cat_bytes, hat_bytes])

请注意,此代码如何使用 UTF-8 编码来处理主题和值字符串。

订阅者的文件名为 zmq_sub.py(例 17-10)。

例 17-10. zmq_sub.py
import zmq
host = '127.0.0.1'
port = 6789
ctx = zmq.Context()
sub = ctx.socket(zmq.SUB)
sub.connect('tcp://%s:%s' % (host, port))
topics = ['maine coon', 'persian']
for topic in topics:
    sub.setsockopt(zmq.SUBSCRIBE, topic.encode('utf-8'))
while True:
    cat_bytes, hat_bytes = sub.recv_multipart()
    cat = cat_bytes.decode('utf-8')
    hat = hat_bytes.decode('utf-8')
    print('Subscribe: %s wears a %s' % (cat, hat))

在此代码中,我们订阅了两个不同的字节值:topics 中的两个字符串,编码为 UTF-8。

注意

看起来有点反向,但如果您想订阅 所有 主题,则需要订阅空字节字符串 b'';如果不这样做,您将一无所获。

请注意,在发布者中我们称之为 send_multipart(),而在订阅者中称之为 recv_multipart()。这使我们能够发送多部分消息并将第一部分用作主题。我们也可以将主题和消息作为单个字符串或字节字符串发送,但将它们分开看起来更清晰。

启动订阅者:

$ python zmq_sub.py

启动发布者。它立即发送 10 条消息然后退出:

$ python zmq_pub.py
Publish: norwegian forest wears a stovepipe
Publish: siamese wears a bowler
Publish: persian wears a stovepipe
Publish: norwegian forest wears a fedora
Publish: maine coon wears a tam-o-shanter
Publish: maine coon wears a stovepipe
Publish: persian wears a stovepipe
Publish: norwegian forest wears a fedora
Publish: norwegian forest wears a bowler
Publish: maine coon wears a bowler

订阅者打印其请求和接收的内容:

Subscribe: persian wears a stovepipe
Subscribe: maine coon wears a tam-o-shanter
Subscribe: maine coon wears a stovepipe
Subscribe: persian wears a stovepipe
Subscribe: maine coon wears a bowler

其他 Pub-Sub 工具

您可能希望探索一些其他 Python 发布-订阅链接:

  • RabbitMQ 是一个著名的消息代理,pika 是其 Python API。参见 pika 文档发布-订阅教程

  • 转到 PyPi 搜索窗口,并键入 pubsub 以查找类似 pypubsub 的 Python 包。

  • PubSubHubbub 允许订阅者向发布者注册回调。

  • NATS 是一个快速、开源的消息系统,支持发布-订阅、请求-响应和排队。

互联网服务

Python 拥有广泛的网络工具集。在接下来的几节中,我们将探讨如何自动化一些最流行的互联网服务。官方、全面的 文档 在线可用。

域名系统

计算机具有如 85.2.101.94 这样的数字 IP 地址,但我们更容易记住名称而不是数字。域名系统(DNS)是一个关键的互联网服务,通过分布式数据库将 IP 地址与名称相互转换。每当您在使用 Web 浏览器时突然看到“looking up host”这样的消息时,您可能已经失去了互联网连接,而您的第一个线索是 DNS 失败。

一些 DNS 功能可以在低级别的 socket 模块中找到。gethostbyname() 返回域名的 IP 地址,而扩展版的 gethostbyname_ex() 返回名称、备用名称列表和地址列表:

>>> import socket
>>> socket.gethostbyname('www.crappytaxidermy.com')
'66.6.44.4'
>>> socket.gethostbyname_ex('www.crappytaxidermy.com')
('crappytaxidermy.com', ['www.crappytaxidermy.com'], ['66.6.44.4'])

getaddrinfo() 方法查找 IP 地址,但它还返回足够的信息以创建一个连接到该地址的套接字:

>>> socket.getaddrinfo('www.crappytaxidermy.com', 80)
[(2, 2, 17, '', ('66.6.44.4', 80)),
(2, 1, 6, '', ('66.6.44.4', 80))]

前面的调用返回了两个元组:第一个用于 UDP,第二个用于 TCP(2, 1, 6 中的 6 是 TCP 的值)。

您可以仅请求 TCP 或 UDP 信息:

>>> socket.getaddrinfo('www.crappytaxidermy.com', 80, socket.AF_INET,
socket.SOCK_STREAM)
[(2, 1, 6, '', ('66.6.44.4', 80))]

一些 TCP 和 UDP 端口号 由 IANA 保留,并与服务名称相关联。例如,HTTP 被命名为 http 并分配给 TCP 端口 80。

这些函数在服务名称和端口号之间进行转换:

>>> import socket
>>> socket.getservbyname('http')
80
>>> socket.getservbyport(80)
'http'

Python 电子邮件模块

标准库包含以下电子邮件模块:

  • smtplib 用于通过简单邮件传输协议 (SMTP) 发送电子邮件消息

  • email 用于创建和解析电子邮件消息

  • poplib 用于通过邮局协议 3 (POP3) 读取电子邮件

  • imaplib 用于通过互联网消息访问协议 (IMAP) 读取电子邮件

如果你想编写自己的 Python SMTP 服务器,请尝试smtpd,或者新的异步版本aiosmtpd

其他协议

使用标准的ftplib 模块,你可以通过文件传输协议 (FTP) 传输字节。尽管这是一个老协议,FTP 仍然表现非常好。

你在本书的多个地方看到了这些模块,但也尝试一下标准库支持的互联网协议的文档。

Web 服务和 API

信息提供者总是有网站,但这些网站面向的是人类眼睛,而不是自动化。如果数据仅在网站上发布,任何想要访问和结构化数据的人都需要编写爬虫(如“爬取和解析”中所示),并在页面格式更改时重新编写。这通常很乏味。相比之下,如果一个网站提供其数据的 API,数据就可以直接提供给客户端程序。API 的更改频率比网页布局低,因此客户端重写较少。一个快速、清晰的数据管道也使得构建未曾预见但有用的组合更加容易。

在许多方面,最简单的 API 是一个 Web 接口,但提供的数据是结构化格式,如 JSON 或 XML,而不是纯文本或 HTML。API 可能很简单,也可能是完整的 RESTful API(在“Web API 和 REST”中定义),但它为这些不安静的字节提供了另一个出口。

在本书的开头,你看到了一个 Web API 查询互联网档案馆以获取旧网站的副本。

API 尤其适用于挖掘像 Twitter、Facebook 和 LinkedIn 这样的知名社交媒体网站。所有这些网站都提供免费使用的 API,但需要注册并获取一个密钥(一个长生成的文本字符串,有时也称为令牌)以在连接时使用。密钥使得网站能够确定谁在访问其数据。它还可以作为限制请求流量到服务器的一种方式。

这里有一些有趣的服务 API:

你可以在第二十一章看到地图 API 的示例,以及在第二十二章看到其他内容。

数据序列化

如你在 第十六章 中看到的,像 XML、JSON 和 YAML 这样的格式是存储结构化文本数据的方式。网络应用程序需要与其他程序交换数据。在内存中和“在传输线上”之间的数据转换称为 序列化编组。JSON 是一种流行的序列化格式,特别适用于 Web RESTful 系统,但它不能直接表示所有 Python 数据类型。另外,作为文本格式,它往往比某些二进制序列化方法更冗长。让我们看看一些你可能会遇到的方法。

使用 pickle 进行序列化

Python 提供了 pickle 模块来保存和恢复任何对象到一个特殊的二进制格式。

还记得 JSON 在遇到 datetime 对象时变得混乱吗?对于 pickle 来说不是问题:

>>> import pickle
>>> import datetime
>>> now1 = datetime.datetime.utcnow()
>>> pickled = pickle.dumps(now1)
>>> now2 = pickle.loads(pickled)
>>> now1
datetime.datetime(2014, 6, 22, 23, 24, 19, 195722)
>>> now2
datetime.datetime(2014, 6, 22, 23, 24, 19, 195722)

pickle 也可以处理你自己定义的类和对象。让我们定义一个叫做 Tiny 的小类,在被当作字符串处理时返回字符串 'tiny'

>>> import pickle
>>> class Tiny():
...     def __str__(self):
...        return 'tiny'
...
>>> obj1 = Tiny()
>>> obj1
<__main__.Tiny object at 0x10076ed10>
>>> str(obj1)
'tiny'
>>> pickled = pickle.dumps(obj1)
>>> pickled
b'\x80\x03c__main__\nTiny\nq\x00)\x81q\x01.'
>>> obj2 = pickle.loads(pickled)
>>> obj2
<__main__.Tiny object at 0x10076e550>
>>> str(obj2)
'tiny'

pickled 是从对象 obj1 制作的 pickled 二进制字符串。我们将其转换回对象 obj2 来复制 obj1。使用 dump() 将 pickle 到文件,使用 load() 从文件中反序列化。

multiprocessing 模块使用 pickle 在进程之间交换数据。

如果 pickle 无法序列化你的数据格式,一个更新的第三方包叫做 dill 可能会有所帮助。

注意

因为 pickle 可以创建 Python 对象,所以同样适用于之前讨论过的安全警告。公共服务公告:不要反序列化你不信任的内容。

其他序列化格式

这些二进制数据交换格式通常比 XML 或 JSON 更紧凑且更快:

因为它们是二进制的,所以无法通过文本编辑器轻松编辑。

一些第三方包可以互相转换对象和基本的 Python 数据类型(允许进一步转换为/从 JSON 等格式),并提供以下的 验证

  • 数据类型

  • 值范围

  • 必需与可选数据

这些包括:

这些通常与 Web 服务器一起使用,以确保通过 HTTP 传输的字节最终进入正确的数据结构以供进一步处理。

远程过程调用

远程过程调用(RPC)看起来像是普通的函数,但是在网络上的远程机器上执行。与在 URL 或请求体中编码参数并调用 RESTful API 不同,您在自己的机器上调用 RPC 函数。你的本地机器:

  • 将你的函数参数序列化为字节。

  • 将编码的字节发送到远程机器。

远程机器:

  • 接收编码的请求字节。

  • 将字节反序列化回数据结构。

  • 找到并调用具有解码数据的服务函数。

  • 对函数结果进行编码。

  • 将编码的字节发送回调用者。

最后,启动一切的本地机器:

  • 解码字节以返回值。

RPC 是一种流行的技术,人们已经用许多方式实现了它。在服务器端,您启动一个服务器程序,将其连接到某些字节传输和编码/解码方法,定义一些服务函数,然后点亮您的RPC 已开放营业标志。客户端连接到服务器并通过 RPC 调用其中一个函数。

XML RPC

标准库包括一个使用 XML 作为交换格式的 RPC 实现:xmlrpc。您在服务器上定义和注册函数,客户端调用它们就像它们被导入一样。首先,让我们探索文件xmlrpc_server.py,如示例 17-11 所示。

示例 17-11. xmlrpc_server.py
from xmlrpc.server import SimpleXMLRPCServer

def double(num):
    return num * 2

server = SimpleXMLRPCServer(("localhost", 6789))
server.register_function(double, "double")
server.serve_forever()

我们在服务器上提供的函数称为double()。它期望一个数字作为参数,并返回该数字乘以 2 的值。服务器在地址和端口上启动。我们需要注册函数以使其通过 RPC 对客户端可用。最后,开始服务并继续。

现在——你猜对了——xmlrpc_client.py,自豪地呈现在示例 17-12 中。

示例 17-12. xmlrpc_client.py
import xmlrpc.client

proxy = xmlrpc.client.ServerProxy("http://localhost:6789/")
num = 7
result = proxy.double(num)
print("Double %s is %s" % (num, result))

客户端通过使用ServerProxy()连接到服务器。然后,它调用函数proxy.double()。这是从哪里来的?它是由服务器动态创建的。RPC 机制神奇地将此函数名连接到对远程服务器的调用中。

试一试——启动服务器,然后运行客户端:

$ python xmlrpc_server.py

再次运行客户端:

$ python xmlrpc_client.py
Double 7 is 14

然后服务器打印如下内容:

127.0.0.1 - - [13/Feb/2014 20:16:23] "POST / HTTP/1.1" 200 -

流行的传输方法包括 HTTP 和 ZeroMQ。

JSON RPC

JSON-RPC(版本1.02.0)类似于 XML-RPC,但使用 JSON。有许多 Python JSON-RPC 库,但我找到的最简单的一个分为两部分:客户端服务器端

安装这两者都很熟悉:pip install jsonrpcserverpip install jsonrpclient

这些库提供了许多写客户端服务器的替代方法。在示例 17-13 和示例 17-14 中,我使用了这个库的内置服务器,它使用端口 5000,是最简单的。

首先,服务器端。

示例 17-13. jsonrpc_server.py
from jsonrpcserver import method, serve

@method
def double(num):
    return num * 2

if __name__ == "__main__":
    serve()

其次,客户端。

示例 17-14. jsonrpc_client.py
from jsonrpcclient import request

num = 7
response = request("http://localhost:5000", "double", num=num)
print("Double", num, "is", response.data.result)

与本章中的大多数客户端-服务器示例一样,首先启动服务器(在其自己的终端窗口中,或者使用后面的&将其放入后台),然后运行客户端:

$ python jsonrpc_server.py &
[1] 10621
$ python jsonrpc_client.py
127.0.0.1 - - [23/Jun/2019 15:39:24] "POST / HTTP/1.1" 200 -
Double 7 is 14

如果将服务器放入后台,请在完成后将其关闭。

MessagePack RPC

编码库 MessagePack 有自己的Python RPC 实现。以下是如何安装它:

$ pip install msgpack-rpc-python

这也会安装tornado,这是一个 Python 基于事件的 Web 服务器,该库将其用作传输。和往常一样,首先启动服务器(msgpack_server.py)(示例 17-15)。

示例 17-15. msgpack_server.py
from msgpackrpc import Server, Address

class Services():
    def double(self, num):
        return num * 2

server = Server(Services())
server.listen(Address("localhost", 6789))
server.start()

Services类将其方法公开为 RPC 服务。继续启动客户端,msgpack_client.py(示例 17-16)。

示例 17-16. msgpack_client.py
from msgpackrpc import Client, Address

client = Client(Address("localhost", 6789))
num = 8
result =  client.call('double', num)
print("Double %s is %s" % (num, result))

要运行这些,请按照通常的步骤-在单独的终端窗口中启动服务器和客户端¹,并观察结果:

$ python msgpack_server.py
$ python msgpack_client.py
Double 8 is 16

Zerorpc

由 Docker 的开发人员(当时被称为 dotCloud)编写,zerorpc使用 ZeroMQ 和 MsgPack 连接客户端和服务器。它会将函数神奇地公开为 RPC 端点。

输入pip install zerorpc来安装它。示例 17-17 和示例 17-18 中的示例代码显示了一个请求-回复客户端和服务器。

示例 17-17. zerorpc_server.py
import zerorpc

class RPC():
    def double(self, num):
        return 2 * num

server = zerorpc.Server(RPC())
server.bind("tcp://0.0.0.0:4242")
server.run()
示例 17-18. zerorpc_client.py
import zerorpc

client = zerorpc.Client()
client.connect("tcp://127.0.0.1:4242")
num = 7
result = client.double(num)
print("Double", num, "is", result)

客户端调用client.double(),即使在其中没有定义:

$ python zerorpc_server &
[1] 55172
$ python zerorpc_client.py
Double 7 is 14

该网站有许多其他示例

gRPC

谷歌创建了gRPC,作为一种便捷快速定义和连接服务的方式。它将数据编码为协议缓冲区

安装 Python 部分:

$ pip install grpcio
$ pip install grpcio-tools

Python 客户端文档非常详细,所以这里我只是简要概述。你可能也喜欢这个单独的教程

要使用 gRPC,你需要编写一个 .proto 文件来定义一个service及其rpc方法。

一个rpc方法类似于一个函数定义(描述其参数和返回类型),并且可以指定以下其中一种网络模式:

  • 请求-响应(同步或异步)

  • 请求-流式响应

  • 流式请求-响应(同步或异步)

  • 流式请求-流式响应

单个响应可以是阻塞或异步的。流式响应会被迭代。

接下来,你将运行grpc_tools.protoc程序为客户端和服务器创建 Python 代码。gRPC 处理序列化和网络通信;你将应用特定的代码添加到客户端和服务器存根中。

gRPC 是 Web REST API 的顶级替代方案。它似乎比 REST 更适合服务间通信,而 REST 可能更适合公共 API。

Twirp

Twirp 类似于 gRPC,但声称更简单。你可以像使用 gRPC 一样定义一个.proto文件,twirp 可以生成处理客户端和服务器端的 Python 代码。

远程管理工具

  • Salt 是用 Python 编写的。它起初是一种实现远程执行的方法,但发展成为一个完整的系统管理平台。基于 ZeroMQ 而不是 SSH,它可以扩展到数千台服务器。

  • PuppetChef 非常流行,并与 Ruby 密切相关。

  • Ansible 软件包,像 Salt 一样是用 Python 编写的,也是类似的。它可以免费下载和使用,但支持和一些附加软件包需要商业许可证。它默认使用 SSH,不需要在要管理的机器上安装任何特殊软件。

SaltAnsible 都是 Fabric 的功能超集,处理初始配置、部署和远程执行。

大数据

随着谷歌和其他互联网公司的发展,他们发现传统的计算解决方案无法满足需求。对单台机器有效的软件,甚至只是几十台机器,无法跟上数千台机器。

数据库和文件的磁盘存储涉及太多寻址,这需要磁盘头的机械移动。(想象一下黑胶唱片,以及将唱针手动从一条曲目移动到另一条曲目所需的时间。再想想当你把它掉得太重时发出的尖叫声,更不用说唱片所有者发出的声音了。)但你可以更快地传统磁盘的连续片段。

开发人员发现,将数据分布式地分析在许多网络化的机器上比在单个机器上更快。他们可以使用听起来简单但实际上在处理海量分布式数据时效果更好的算法。其中之一是 MapReduce,它将计算分布在许多机器上,然后收集结果。这类似于使用队列。

Hadoop

谷歌在一篇论文中公布了其 MapReduce 的结果后,雅虎推出了一个名为Hadoop的开源基于 Java 的软件包(以主要程序员儿子的毛绒大象玩具命名)。

短语大数据适用于这里。通常它只是指“数据太大,无法放在我的机器上”:超过磁盘、内存、CPU 时间或以上所有的数据。对一些组织来说,如果问题中提到了大数据,答案总是 Hadoop。Hadoop 在机器之间复制数据,通过map(分散)和reduce(聚集)程序运行数据,并在每一步将结果保存在磁盘上。

这个批处理过程可能很慢。一个更快的方法叫做Hadoop streaming,类似于 Unix 管道,通过程序流式传输数据,而不需要在每一步都写入磁盘。你可以用任何语言编写 Hadoop 流式处理程序,包括 Python。

许多 Python 模块已经为 Hadoop 编写,并且一些在博文“Python 框架指南”中有所讨论。以流媒体音乐而闻名的 Spotify,开源了其 Hadoop 流处理的 Python 组件Luigi

Spark

一位名为Spark的竞争对手被设计成比 Hadoop 快 10 到 100 倍。它可以读取和处理任何 Hadoop 数据源和格式。Spark 包括 Python 等语言的 API。你可以在网上找到安装文档。

Disco

另一个替代 Hadoop 的选择是Disco,它使用 Python 进行 MapReduce 处理,并使用 Erlang 进行通信。不幸的是,你不能用pip安装它;请参阅文档

Dask

Dask类似于 Spark,尽管它是用 Python 编写的,并且主要与 NumPy、Pandas 和 scikit-learn 等科学 Python 包一起使用。它可以将任务分散到千台机器的集群中。

要获取 Dask 及其所有额外的帮助程序:

$ pip install dask[complete]

参见第二十二章,了解并行编程的相关示例,其中大型结构化计算分布在许多机器之间。

Clouds

我真的不了解云。

Joni Mitchell

不久以前,你会购买自己的服务器,将它们安装在数据中心的机架上,并在其上安装各种软件层:操作系统、设备驱动程序、文件系统、数据库、Web 服务器、电子邮件服务器、名称服务器、负载均衡器、监视器等等。任何最初的新奇感都会随着你试图保持多个系统的运行和响应而消失。而且你会持续担心安全问题。

许多托管服务提供了为你管理服务器的服务,但你仍然租用物理设备,并且必须始终支付峰值负载配置的费用。

随着个体机器的增多,故障不再是偶发的:它们非常普遍。你需要横向扩展服务并冗余存储数据。不能假设网络像单一机器一样运行。根据 Peter Deutsch,分布式计算的八个谬误如下:

  • 网络是可靠的。

  • 延迟是零。

  • 带宽是无限的。

  • 网络是安全的。

  • 拓扑结构不会改变。

  • 只有一个管理员。

  • 运输成本为零。

  • 网络是同质的。

你可以尝试构建这些复杂的分布式系统,但这是一项艰巨的工作,需要不同的工具集。借用一个类比,当你有一小群服务器时,你对待它们像宠物一样——给它们起名字,了解它们的个性,并在需要时照顾它们。但在规模化时,你更像对待牲畜一样对待服务器:它们看起来一样,有编号,有问题就直接更换。

而不是自己搭建,您可以在中租用服务器。通过采用这种模式,维护工作成为了别人的问题,您可以专注于您的服务、博客或者任何您想向世界展示的内容。使用 Web 仪表板和 API,您可以快速轻松地启动具有所需配置的服务器——它们弹性。您可以监视它们的状态,并在某些指标超过给定阈值时收到警报。云目前是一个非常热门的话题,企业在云组件上的支出激增。

大型云供应商包括:

  • 亚马逊 (AWS)

  • 谷歌

  • Microsoft Azure

亚马逊网络服务

当亚马逊从数百台服务器增长到数千台、数百万台时,开发人员遇到了所有分布式系统的可怕问题。大约在 2002 年某一天,CEO 杰夫·贝佐斯向亚马逊员工宣布,今后所有数据和功能都必须仅通过网络服务接口公开——而不是文件、数据库或本地函数调用。他们必须设计这些接口,就像它们是向公众提供的一样。备忘录以一句激励人心的话结束:“任何不这样做的人都会被解雇。”

没有什么奇怪的,开发人员开始行动,并随着时间的推移构建了一个庞大的面向服务的架构。他们借鉴或创新了许多解决方案,发展成为亚马逊网络服务 (AWS),目前主导市场。官方的 Python AWS 库是 boto3

使用以下命令安装:

$ pip install boto3

您可以使用 boto3 作为 AWS 的基于 Web 的管理页面的替代品。

谷歌云

谷歌在内部大量使用 Python,并雇佣了一些著名的 Python 开发者(甚至包括 Guido van Rossum 自己)。从其主页Python页面,您可以找到其许多服务的详细信息。

Microsoft Azure

Microsoft 在其云服务提供中赶上了亚马逊和谷歌,Azure。查看Python on Azure以了解如何开发和部署 Python 应用程序。

OpenStack

OpenStack 是一个 Python 服务和 REST API 的开源框架。许多服务类似于商业云中的服务。

Docker

随着一个简单的标准化集装箱彻底改变了国际贸易。仅仅几年前,Docker 将容器这个名字和类比应用于使用一些鲜为人知的 Linux 特性的虚拟化方法。容器比虚拟机轻得多,比 Python 的虚拟环境重一点。它们允许您将应用程序与同一台机器上的其他应用程序分开打包,只共享操作系统内核。

若要安装 Docker 的 Python 客户端

$ pip install docker

Kubernetes

容器在计算领域迅速流行开来。最终,人们需要管理多个容器的方法,并希望自动化一些在大型分布式系统中通常需要的手动步骤:

  • 故障转移

  • 负载均衡

  • 扩展和收缩

看起来 Kubernetes 在这个新领域的容器编排中处于领先地位。

要安装 Python 客户端

$ pip install kubernetes

即将到来

正如电视上所说,我们的下一个嘉宾无需介绍。了解为什么 Python 是驯服网络的最佳语言之一。

要做的事情

17.1 使用一个普通的 socket 来实现一个当前时间服务。当客户端向服务器发送字符串 time 时,返回当前日期和时间作为 ISO 字符串。

17.2 使用 ZeroMQ 的 REQ 和 REP sockets 来做同样的事情。

17.3 尝试使用 XMLRPC 来做同样的事情。

17.4 你可能看过经典的《我爱 Lucy》电视剧集,讲述了 Lucy 和 Ethel 在巧克力工厂工作的故事。由于给她们加工的传送带开始以越来越快的速度运转,二人落后了。编写一个模拟,将不同类型的巧克力推送到一个 Redis 列表中,Lucy 是一个执行阻塞弹出此列表的客户端。她需要 0.5 秒来处理一块巧克力。打印 Lucy 收到的每块巧克力的时间和类型,以及剩下需要处理的数量。

17.5 使用 ZeroMQ 逐字发布来自练习 12.4 的诗歌(来自 示例 12-1),一次一个字。编写一个 ZeroMQ 消费者,打印每个以元音开头的单词,以及打印每个包含五个字母的单词。忽略标点符号字符。

¹ 或使用最后的 & 将服务器放在后台。

第十八章:解开网络的秘密

哦,我们编织的网……

沃尔特·斯科特,《马尔米翁》

横跨法国和瑞士边境的是 CERN——一个粒子物理研究所,多次粉碎原子,以确保。

所有这些粉碎产生了大量数据。1989 年,英国科学家蒂姆·伯纳斯-李在 CERN 内部首次提出了一个提议,帮助在那里和研究界传播信息。他称之为万维网,并将其设计简化为三个简单的想法:

HTTP(超文本传输协议)

一种用于网络客户端和服务器交换请求和响应的协议。

HTML(超文本标记语言)

结果的演示格式。

URL(统一资源定位符)

表示服务器和该服务器上的资源的唯一方式。

在其最简单的用法中,Web 客户端(我认为伯纳斯-李是第一个使用术语浏览器的人)通过 HTTP 连接到 Web 服务器,请求 URL,并接收 HTML。

所有这一切都建立在来自互联网的网络基础之上,当时互联网是非商业化的,并且仅为少数大学和研究组织所知。

他在一台 NeXT¹电脑上写了第一款网页浏览器和服务器。1993 年,当伊利诺伊大学的一群学生发布了适用于 Windows、Macintosh 和 Unix 系统的 Mosaic 网页浏览器和 NCSA httpd服务器时,网络意识真正扩展开来。那个夏天我下载了 Mosaic 并开始建站时,我完全没有想到网络和互联网很快会成为日常生活的一部分。当时互联网²仍然正式非商业化;全球约有 500 台已知的网络服务器(详见)。到 1994 年底,网络服务器数量已增长到 10,000 台。互联网开放商业化,Mosaic 的作者们创立了 Netscape 公司,开发商业化的网络软件。Netscape 随着早期互联网狂热而上市,网络的爆炸性增长从未停止。

几乎每种计算机语言都被用来编写网络客户端和服务器。动态语言 Perl、PHP 和 Ruby 尤为流行。在本章中,我展示了为何 Python 是各个层次上进行网络工作的特别优秀语言:

  • 客户端,用于访问远程站点

  • 服务器,为网站和 Web API 提供数据

  • Web API 和服务,以其他方式交换数据,而不仅仅是可视化的网页

顺便说一句,我们将在本章末尾的练习中构建一个实际的交互式网站。

网络客户端

互联网的低级网络管道被称为传输控制协议/因特网协议,或更常见的 TCP/IP(“TCP/IP”详细介绍了这一点)。它在计算机之间传输字节,但不关心这些字节的含义。这是高级协议的工作——专门用于特定目的的语法定义。HTTP 是网络数据交换的标准协议。

Web 是一个客户端-服务器系统。客户端向服务器发送一个请求:它通过 HTTP 打开 TCP/IP 连接,发送 URL 和其他信息,并接收一个响应

响应的格式也由 HTTP 定义。它包括请求的状态,以及(如果请求成功)响应的数据和格式。

最著名的 Web 客户端是 Web 浏览器。它可以以多种方式发出 HTTP 请求。您可以通过在位置栏中键入 URL 或单击 Web 页面中的链接手动发出请求。返回的数据通常用于显示网站——HTML 文档、JavaScript 文件、CSS 文件和图像——但它可以是任何类型的数据,不仅仅是用于显示的数据。

HTTP 的一个重要特点是它是无状态的。您建立的每个 HTTP 连接都是彼此独立的。这简化了基本的 Web 操作,但也使其他操作变得复杂。这里只是一些挑战的样本:

缓存

不变的远程内容应该由 Web 客户端保存并使用,以避免再次从服务器下载。

会话

购物网站应该记住您购物车的内容。

认证

需要您的用户名和密码的网站在您登录时应该记住它们。

解决无状态性的方法包括cookies,服务器向客户端发送足够的特定信息,以便在客户端将 cookie 发送回来时能够唯一识别它。

使用 telnet 进行测试

HTTP 是基于文本的协议,因此您实际上可以自己输入以进行 Web 测试。古老的telnet程序允许您连接到任何运行服务的服务器和端口,并向任何正在运行的服务输入命令。对于与其他机器的安全(加密)连接,它已被ssh取代。

让我们询问每个人最喜欢的测试网站,Google,关于其主页的一些基本信息。输入以下内容:

$ telnet www.google.com 80

如果在 80 端口上有一个 Web 服务器(这是未加密的http通常运行的地方;加密的https使用 443 端口)在google.com上(我认为这是一个安全的赌注),telnet将打印一些令人放心的信息,然后显示一个最终的空行,这是您输入其他内容的信号:

Trying 74.125.225.177...
Connected to www.google.com.
Escape character is '^]'.

现在,键入一个实际的 HTTP 命令,让telnet发送给 Google Web 服务器。最常见的 HTTP 命令(当您在浏览器的位置栏中键入 URL 时使用的命令)是GET。这会检索指定资源的内容,例如 HTML 文件,并将其返回给客户端。对于我们的第一个测试,我们将使用 HTTP 命令HEAD,它只是检索资源的一些基本信息关于资源:

HEAD / HTTP/1.1

添加额外的换行符以发送空行,以便远程服务器知道您已经完成并希望得到响应。该 HEAD / 发送 HTTP HEAD 动词(命令)以获取关于首页(/)的信息。您将收到类似以下内容的响应(我使用...裁剪了一些长行,以防止其超出书籍):

HTTP/1.1 200 OK
Date: Mon, 10 Jun 2019 16:12:13 GMT
Expires: -1
Cache-Control: private, max-age=0
Content-Type: text/html; charset=ISO-8859-1
P3P: CP="This is not a P3P policy! See g.co/p3phelp for more info."
Server: gws
X-XSS-Protection: 0
X-Frame-Options: SAMEORIGIN
Set-Cookie: 1P_JAR=...; expires=... GMT; path=/; domain=.google.com
Set-Cookie: NID=...; expires=... GMT; path=/; domain=.google.com; HttpOnly
Transfer-Encoding: chunked
Accept-Ranges: none
Vary: Accept-Encoding

这些是 HTTP 响应头及其值。像 DateContent-Type 这样的头是必需的。其他头,如 Set-Cookie,用于跟踪您在多次访问中的活动(我们稍后在本章讨论状态管理)。当您发出 HTTP HEAD 请求时,您只会收到头信息。如果您使用了 HTTP GETPOST 命令,您还将从首页收到数据(其中包含 HTML、CSS、JavaScript 和 Google 决定加入首页的其他内容)。

我不想让你陷在 telnet 中。要关闭 telnet,请键入以下内容:

q

使用 curl 进行测试

使用 telnet 简单,但是这是一个完全手动的过程。curl 程序可能是最流行的命令行 Web 客户端。其文档包括书籍Everything Curl,提供 HTML、PDF 和电子书格式。一张表比较了 curl 与类似工具。下载页面包含了所有主要平台和许多不太常见的平台。

使用 curl 的最简单方式执行隐式 GET(此处截断了输出):

$ curl http://www.example.com
<!doctype html>
<html>
<head>
    <title>Example Domain</title>
    ...

这使用 HEAD

$ curl --head http://www.example.com
HTTP/1.1 200 OK
Content-Encoding: gzip
Accept-Ranges: bytes
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Date: Sun, 05 May 2019 16:14:30 GMT
Etag: "1541025663"
Expires: Sun, 12 May 2019 16:14:30 GMT
Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT
Server: ECS (agb/52B1)
X-Cache: HIT
Content-Length: 606

如果要传递参数,您可以在命令行或数据文件中包含它们。在这些示例中,我使用以下内容:

  • url 适用于任何网站

  • data.txt 是一个文本数据文件,内容如下:a=1&b=2

  • data.json 作为 JSON 数据文件,其内容为:{"a":1, "b": 2}

  • a=1&b=2 作为两个数据参数

使用默认的(form-encoded)参数:

$ curl -X POST -d "a=1&b=2" *url*
$ curl -X POST -d "@data.txt" *url*

对于 JSON 编码的参数:

$ curl -X POST -d "{'a':1,'b':2}" -H "Content-Type: application/json" *url*
$ curl -X POST -d "@data.json" *url*

使用 httpie 进行测试

比 curl 更 Pythonic 的选择是httpie

$ pip install httpie

要进行与上述 curl 方法类似的表单编码 POST,请使用 -f 作为 --form 的同义词:

$ http -f POST *url* a=1 b=2
$ http POST -f *url* < data.txt

默认编码是 JSON:

$ http POST *url* a=1 b=2
$ http POST *url* < data.json

httpie 还处理 HTTP 头部、cookie、文件上传、身份验证、重定向、SSL 等等。如往常一样,请参阅文档

使用 httpbin 进行测试

您可以针对网站httpbin测试您的 Web 查询,或者在本地 Docker 映像中下载并运行该站点:

$ docker run -p 80:80 kennethreitz/httpbin

Python 的标准 Web 库

在 Python 2 中,Web 客户端和服务器模块有点分散。Python 3 的目标之一是将这些模块打包成两个(请记住来自第十一章的定义,包只是包含模块文件的目录):

  • http 管理所有客户端-服务器 HTTP 细节:

    • client 处理客户端相关工作

    • server 帮助你编写 Python Web 服务器

    • cookiescookiejar 管理 cookie,用于在访问站点时保存数据

  • urllib 基于 http 运行:

    • request 处理客户端请求

    • response处理服务器响应

    • parse解析 URL 的各个部分

注意

如果你试图编写同时兼容 Python 2 和 Python 3 的代码,请记住urllib在这两个版本之间有很大的变化。查看更好的替代方案,请参考“超越标准库:requests”。

让我们使用标准库从网站获取一些内容。以下示例中的 URL 返回来自测试网站的信息:

>>> import urllib.request as ur
>>>
>>> url = 'http://www.example.com/'
>>> conn = ur.urlopen(url)

这段小小的 Python 代码打开了一个到远程 Web 服务器www.example.com的 TCP/IP 连接,发送了一个 HTTP 请求,并接收到了一个 HTTP 响应。响应中包含的不仅仅是页面数据。在官方文档中,我们发现conn是一个具有多个方法和属性的HTTPResponse对象。响应的一个重要部分是 HTTP 状态码

>>> print(conn.status)
200

200表示一切顺利。HTTP 状态码分为五个范围,根据其第一个(百位)数字进行分组:

1xx(信息)

服务器已接收请求,但有一些额外的信息要传递给客户端。

2xx(成功)

成功;除了 200 之外的所有成功代码都携带额外的细节。

3xx(重定向)

资源已移动,所以响应将新的 URL 返回给客户端。

4xx(客户端错误)

客户端出现了一些问题,比如众所周知的 404(未找到)。418(我是茶壶)是愚人节玩笑。

5xx(服务器错误)

500 是通用的“哎呀”错误;如果 Web 服务器和后端应用程序服务器之间存在断开连接,则可能会看到 502(坏网关)。

要获取网页的实际数据内容,请使用conn变量的read()方法。这将返回一个bytes值。让我们获取数据并打印前 50 个字节:

>>> data = conn.read()
>>> print(data[:50])
b'<!doctype html>\n<html>\n<head>\n    <title>Example D'

我们可以将这些字节转换为字符串,并打印其前 50 个字符:

>>> str_data = data.decode('utf8')
>>> print(str_data[:50])
<!doctype html>
<html>
<head>
    <title>Example D
>>>

其余是更多的 HTML 和 CSS。

纯粹出于好奇,我们收到了哪些 HTTP 头部回复?

>>> for key, value in conn.getheaders():
...     print(key, value)
...
Cache-Control max-age=604800
Content-Type text/html; charset=UTF-8
Date Sun, 05 May 2019 03:09:26 GMT
Etag "1541025663+ident"
Expires Sun, 12 May 2019 03:09:26 GMT
Last-Modified Fri, 09 Aug 2013 23:54:35 GMT
Server ECS (agb/5296)
Vary Accept-Encoding
X-Cache HIT
Content-Length 1270
Connection close

还记得之前的telnet示例吗?现在,我们的 Python 库正在解析所有这些 HTTP 响应头,并将它们以字典形式提供。DateServer似乎很直观;其他一些可能不那么直观。了解 HTTP 具有一组标准头部,如Content-Type和许多可选头部,这是很有帮助的。

超越标准库:requests

在第一章的开头,有一个程序通过使用标准库urllib.requestjson访问了 Wayback Machine API。接下来是一个使用第三方模块requests的版本。requests版本更简短,更易于理解。

对于大多数情况,我认为使用requests进行 Web 客户端开发更容易。你可以浏览文档(非常好),了解所有细节。我将在本节中展示requests的基础知识,并在整本书中用它来处理 Web 客户端任务。

首先,安装requests库:

$ pip install requests

现在,让我们用 requests 重新执行我们的 example.com 查询:

>>> import requests
>>> resp = requests.get('http://example.com')
>>> resp
<Response [200]>
>>> resp.status_code
200
>>> resp.text[:50]
'<!doctype html>\n<html>\n<head>\n    <title>Example D'

要显示一个 JSON 查询,这是一个本章末尾出现的程序的最小版本。你提供一个字符串,它使用互联网档案馆搜索 API 来搜索那里保存的数十亿多媒体项目的标题。请注意,在 示例 18-1 中显示的 requests.get() 调用中,你只需要传递一个 params 字典,requests 将处理所有的查询构造和字符转义。

示例 18-1. ia.py
import json
import sys

import requests

def search(title):
     url = "http://archive.org/advancedsearch.php"
     params = {"q": f"title:({title})",
               "output": "json",
               "fields": "identifier,title",
               "rows": 50,
               "page": 1,}
     resp = requests.get(url, params=params)
     return resp.json()

if __name__ == "__main__":
     title = sys.argv[1]
     data = search(title)
     docs = data["response"]["docs"]
     print(f"Found {len(docs)} items, showing first 10")
     print("identifier\ttitle")
     for row in docs[:10]:
         print(row["identifier"], row["title"], sep="\t")

他们关于食人魔物品的库存如何?

$ python ia.py wendigo
Found 24 items, showing first 10
identifier  title
cd_wendigo_penny-sparrow  Wendigo
Wendigo1  Wendigo 1
wendigo_ag_librivox The Wendigo
thewendigo10897gut  The Wendigo
isbn_9780843944792  Wendigo mountain ; Death camp
jamendo-060508  Wendigo - Audio Leash
fav-lady_wendigo  lady_wendigo Favorites
011bFearTheWendigo  011b Fear The Wendigo
CharmedChats112 Episode 112 - The Wendigo
jamendo-076964  Wendigo - Tomame o Dejame>

第一列(标识符)可以用来实际查看存档网站上的项目。你将在本章末尾看到如何做到这一点。

Web 服务器

Web 开发人员发现 Python 是编写 Web 服务器和服务器端程序的优秀语言。这导致了许多基于 Python 的 Web 框架 的出现,以至于在它们中间和做选择时可能很难导航——更不用说决定哪些应该写进一本书了。

一个 Web 框架提供了一些功能,你可以用它来构建网站,所以它不仅仅是一个简单的 Web(HTTP)服务器。你会看到一些功能,比如路由(将 URL 映射到服务器功能)、模板(包含动态内容的 HTML)、调试等等。

我不打算在这里涵盖所有的框架,只介绍那些我发现相对简单易用且适合真实网站的。我还会展示如何用 Python 运行网站的动态部分以及用传统 Web 服务器运行其他部分。

最简单的 Python Web 服务器

你可以通过输入一行 Python 来运行一个简单的 Web 服务器:

$ python -m http.server

这实现了一个简单的 Python HTTP 服务器。如果没有问题,它将打印初始状态消息:

Serving HTTP on 0.0.0.0 port 8000 ...

那个 0.0.0.0 意味着 任何 TCP 地址,所以 Web 客户端无论服务器有什么地址都可以访问它。有关 TCP 和其他网络底层细节,你可以在第十七章中进一步阅读。

现在,你可以请求文件,路径相对于你当前的目录,它们将被返回。如果在你的 Web 浏览器中输入 http://localhost:8000,你应该能看到那里的目录列表,并且服务器将打印类似这样的访问日志行:

127.0.0.1 - - [20/Feb/2013 22:02:37] "GET / HTTP/1.1" 200 -

localhost127.0.0.1你的本地计算机 的 TCP 同义词,所以这在无论你是否连接互联网时都能工作。你可以这样解释这一行:

  • 127.0.0.1 是客户端的 IP 地址

  • 第一个 - 是远程用户名(如果有的话)

  • 第二个 - 是登录用户名(如果需要的话)

  • [20/Feb/2013 22:02:37] 是访问的日期和时间

  • "GET / HTTP/1.1" 是发送给 Web 服务器的命令:

    • HTTP 方法(GET

    • 所请求的资源(/,顶层)

    • HTTP 版本(HTTP/1.1

  • 最后的 200 是 Web 服务器返回的 HTTP 状态码

点击任何文件。如果你的浏览器能够识别格式(HTML、PNG、GIF、JPEG 等),它应该会显示它,并且服务器将记录该请求。例如,如果你的当前目录中有 oreilly.png 文件,请求 http://localhost:8000/oreilly.png 应该返回 图 20-2 中那个怪异家伙的图像,并且日志应该显示类似于这样的内容:

127.0.0.1 - - [20/Feb/2013 22:03:48] "GET /oreilly.png HTTP/1.1" 200 -

如果你的电脑相同目录下还有其他文件,它们应该显示在你的显示器上,你可以点击任何一个来下载。如果你的浏览器配置了显示该文件的格式,你会在屏幕上看到结果;否则,你的浏览器会询问你是否要下载并保存该文件。

默认使用的端口号是 8000,但你可以指定其他端口号:

$ python -m http.server 9999

你应该看到这个:

Serving HTTP on 0.0.0.0 port 9999 ...

这个仅支持 Python 的服务器最适合进行快速测试。你可以通过结束其进程来停止它;在大多数终端中,按 Ctrl+C。

你不应该将这个基本服务器用于繁忙的生产网站。传统的 Web 服务器如 Apache 和 NGINX 对于提供静态文件要快得多。此外,这个简单的服务器无法处理动态内容,而更复杂的服务器可以通过接受参数来实现这一点。

Web 服务器网关接口(WSGI)

很快,只提供简单文件的吸引力就消失了,我们需要一个能够动态运行程序的 Web 服务器。在 Web 的早期,通用网关接口(CGI)是为客户端执行 Web 服务器运行外部程序并返回结果设计的。CGI 也处理通过服务器从客户端获取输入参数传递到外部程序。然而,这些程序会为 每个 客户端访问重新启动。这种方式不能很好地扩展,因为即使是小型程序也有可观的启动时间。

为了避免这种启动延迟,人们开始将语言解释器合并到 Web 服务器中。Apache 在其 mod_php 模块中运行 PHP,Perl 在 mod_perl 中,Python 在 mod_python 中。然后,这些动态语言中的代码可以在长时间运行的 Apache 进程中执行,而不是在外部程序中执行。

另一种方法是在单独的长期运行程序中运行动态语言,并让它与 Web 服务器通信。FastCGI 和 SCGI 就是其中的例子。

Python Web 开发通过定义 Web 服务器网关接口(WSGI)迈出了一大步,这是 Python Web 应用程序与 Web 服务器之间的通用 API。本章剩余部分介绍的所有 Python Web 框架和 Web 服务器都使用 WSGI。通常情况下你不需要知道 WSGI 如何工作(其实它并不复杂),但了解一些底层组成部分的名称有助于你的理解。这是一个 同步 连接,一步接着一步。

ASGI

迄今为止,在几个地方,我已经提到 Python 正在引入像asyncawaitasyncio这样的异步语言特性。ASGI(异步服务器网关接口)是 WSGI 的一个对应项,它使用这些新特性。在附录 C 中,您将看到更多讨论,并且会有使用 ASGI 的新 Web 框架示例。

Apache

apache Web 服务器最好的 WSGI 模块是mod_wsgi。这可以在 Apache 进程内或与 Apache 通信的独立进程中运行 Python 代码。

如果您的系统是 Linux 或 macOS,您应该已经有apache。对于 Windows,您需要安装apache

最后,安装您喜欢的基于 WSGI 的 Python Web 框架。我们在这里尝试bottle。几乎所有的工作都涉及配置 Apache,这可能是一门黑暗的艺术。

创建如示例 18-2 所示的测试文件,并将其保存为*/var/www/test/home.wsgi*。

示例 18-2. home.wsgi
import bottle

application = bottle.default_app()

@bottle.route('/')
def home():
    return "apache and wsgi, sitting in a tree"

这次不要调用run(),因为它会启动内置的 Python Web 服务器。我们需要将变量application赋值,因为这是mod_wsgi用来连接 Web 服务器和 Python 代码的地方。

如果apache和其mod_wsgi模块正常工作,我们只需将它们连接到我们的 Python 脚本即可。我们希望在定义此apache服务器默认网站的文件中添加一行,但找到该文件本身就是一个任务。它可能是*/etc/apache2/httpd.conf*,也可能是*/etc/apache2/sites-available/default*,或者是某人宠物蝾螈的拉丁名。

现在假设您已经理解了apache并找到了那个文件。在管理默认网站的部分内添加此行:

    WSGIScriptAlias / /var/www/test/home.wsgi

那一节可能看起来像这样:

<VirtualHost *:80>
    DocumentRoot /var/www

    WSGIScriptAlias / /var/www/test/home.wsgi

    <Directory /var/www/test>
    Order allow,deny
    Allow from all
    </Directory>
</VirtualHost>

启动apache,或者如果它已经运行,则重新启动以使用这个新配置。然后,如果您浏览到http://localhost/,您应该会看到:

apache and wsgi, sitting in a tree

这将在嵌入模式下运行mod_wsgi,作为apache本身的一部分。

您也可以将其以守护程序模式运行,作为一个或多个与apache分开的进程。为此,请在您的apache配置文件中添加两行新的指令行:

WSGIDaemonProcess *domain-name* user=*user-name* group=*group-name* threads=25
WSGIProcessGroup *domain-name*

在上述示例中,*user-namegroup-name是操作系统用户和组名,domain-name*是您的互联网域名。一个最小的apache配置可能如下所示:

<VirtualHost *:80>
    DocumentRoot /var/www

    WSGIScriptAlias / /var/www/test/home.wsgi

    WSGIDaemonProcess mydomain.com user=myuser group=mygroup threads=25
    WSGIProcessGroup mydomain.com

    <Directory /var/www/test>
    Order allow,deny
    Allow from all
    </Directory>
</VirtualHost>

NGINX

NGINX Web 服务器没有内置的 Python 模块。相反,它是一个前端,用于连接到诸如 uWSGI 或 gUnicorn 之类的独立 WSGI 服务器。它们共同构成了一个非常快速和可配置的 Python Web 开发平台。

您可以从其网站安装nginx。有关使用 NGINX 和 WSGI 服务器设置 Flask 的示例,请参阅此处

其他 Python Web 服务器

以下是一些独立的基于 Python 的 WSGI 服务器,类似于 apachenginx,使用多进程和/或线程(见“并发”)处理并发请求:

这里有一些基于事件的服务器,它们使用单一进程但避免在任何单个请求上阻塞:

在关于第十五章中关于并发的讨论中,我还有更多要说的。

Web 服务器框架

Web 服务器处理 HTTP 和 WSGI 的细节,但是你使用 Web 框架来实际编写驱动站点的 Python 代码。因此,让我们先谈谈框架,然后再回到实际使用它们的网站服务的替代方法。

如果你想用 Python 写一个网站,有许多(有人说太多)Python Web 框架。Web 框架至少处理客户端请求和服务器响应。大多数主要的 Web 框架包括以下任务:

  • HTTP 协议处理

  • 认证(authn,或者你是谁?)

  • 认证(authz,或者你可以做什么?)

  • 建立一个会话

  • 获取参数

  • 验证参数(必需/可选、类型、范围)

  • 处理 HTTP 动词

  • 路由(函数/类)

  • 提供静态文件(HTML、JS、CSS、图像)

  • 提供动态数据(数据库、服务)

  • 返回值和 HTTP 状态

可选功能包括:

  • 后端模板

  • 数据库连接、ORMs

  • 速率限制

  • 异步任务

在接下来的章节中,我们将为两个框架(bottleflask)编写示例代码。这些是同步的。稍后,我将讨论特别是用于数据库支持网站的替代方案。你可以找到一个 Python 框架来支持你可以想到的任何网站。

Bottle

Bottle 由单个 Python 文件组成,因此非常容易尝试,并且稍后易于部署。Bottle 不是标准 Python 的一部分,因此要安装它,请键入以下命令:

$ pip install bottle

这里是一个将运行一个测试 Web 服务器并在你的浏览器访问 URL http://localhost:9999/ 时返回一行文本的代码。将其保存为 bottle1.py(示例 18-3)。

示例 18-3. bottle1.py
from bottle import route, run

@route('/')
def home():
  return "It isn't fancy, but it's my home page"

run(host='localhost', port=9999)

Bottle 使用 route 装饰器将 URL 关联到以下函数;在这种情况下,/(主页)由 home() 函数处理。通过键入以下命令使 Python 运行此服务器脚本:

$ python bottle1.py

当你访问http://localhost:9999/时,你应该在你的浏览器上看到这个:

It isn't fancy, but it's my home page

run() 函数执行 bottle 内置的 Python 测试 Web 服务器。对于 bottle 程序,你不需要使用它,但在初始开发和测试时很有用。

现在,不再在代码中创建主页文本,让我们创建一个名为 index.html 的单独 HTML 文件,其中包含这行文本:

My <b>new</b> and <i>improved</i> home page!!!

使bottle在请求主页时返回此文件的内容。将此脚本保存为bottle2.py(示例 18-4)。

示例 18-4. bottle2.py
from bottle import route, run, static_file

@route('/')
def main():
    return static_file('index.html', root='.')

run(host='localhost', port=9999)

在调用static_file()时,我们想要的是root所指示的目录中的文件index.html(在本例中,为 '.',即当前目录)。如果你之前的服务器示例代码仍在运行,请停止它。现在,运行新的服务器:

$ python bottle2.py

当你请求浏览器获取*http:/localhost:9999/*时,你应该看到这个:

My `new` and *`improved`* home page!!!

让我们最后添加一个示例,展示如何向 URL 传递参数并使用它们。当然,这将是bottle3.py,你可以在示例 18-5 中看到它。

示例 18-5. bottle3.py
from bottle import route, run, static_file

@route('/')
def home():
    return static_file('index.html', root='.')

@route('/echo/<thing>')
def echo(thing):
    return "Say hello to my little friend: %s!" % thing

run(host='localhost', port=9999)

我们有一个名为echo()的新函数,并希望在 URL 中传递一个字符串参数。这就是前面示例中的@route('/echo/<thing>')一行所做的事情。路由中的<thing>意味着在/echo/之后的 URL 中的任何内容都将被分配给字符串参数thing,然后传递给echo函数。要查看发生了什么,请停止旧服务器(如果仍在运行)然后使用新代码启动它:

$ python bottle3.py

然后,在你的网络浏览器中访问*http://localhost:9999/echo/Mothra*。你应该看到以下内容:

Say hello to my little friend: Mothra!

现在,让bottle3.py运行一分钟,这样我们可以尝试其他东西。你一直通过在浏览器中输入 URL 并查看显示的页面来验证这些示例是否有效。你也可以使用诸如requests之类的客户端库来为你完成工作。将此保存为bottle_test.py(示例 18-6)。

示例 18-6. bottle_test.py
import requests

resp = requests.get('http://localhost:9999/echo/Mothra')
if resp.status_code == 200 and \
  resp.text == 'Say hello to my little friend: Mothra!':
    print('It worked! That almost never happens!')
else:
    print('Argh, got this:', resp.text)

太棒了!现在,运行它:

$ python bottle_test.py

你应该在终端上看到这个:

It worked! That almost never happens!

这是一个单元测试的小例子。第十九章详细介绍了为什么测试很重要以及如何在 Python 中编写测试。

Bottle 还有更多功能,这里没有展示出来。特别是,你可以在调用run()时添加这些参数:

  • debug=True如果出现 HTTP 错误,则会创建一个调试页面;

  • reloader=True如果更改任何 Python 代码,它会在浏览器中重新加载页面。

这在开发者网站上有详细文档。

Flask

Bottle 是一个不错的初始 Web 框架。如果你需要更多功能,可以尝试 Flask。它于 2010 年愚人节笑话开始,但是热烈的反响鼓励了作者阿尔明·罗纳赫将其制作成一个真正的框架。他将结果命名为 Flask,这是对 Bottle 的一种文字游戏。

Flask 的使用方法与 Bottle 差不多,但它支持许多在专业 Web 开发中有用的扩展,比如 Facebook 认证和数据库集成。它是我个人在 Python Web 框架中的首选,因为它在易用性和丰富功能集之间取得了平衡。

flask包含werkzeug WSGI 库和jinja2模板库。你可以从终端安装它:

$ pip install flask

让我们在 Flask 中复制最终的 Bottle 示例代码。不过,在此之前,我们需要做一些更改:

  • Flask 的默认静态文件目录主目录是static,那里的文件 URL 也以/static开头。我们将文件夹更改为'.'(当前目录),URL 前缀更改为''(空),以便将 URL/映射到文件index.html

  • run()函数中,设置debug=True还会激活自动重新加载器;bottle为调试和重新加载使用了单独的参数。

将此文件保存为flask1.py(示例 18-7)。

示例 18-7. flask1.py
from flask import Flask

app = Flask(__name__, static_folder='.', static_url_path='')

@app.route('/')
def home():
    return app.send_static_file('index.html')

@app.route('/echo/<thing>')
def echo(thing):
    return "Say hello to my little friend: %s" % thing

app.run(port=9999, debug=True)

然后,在终端或窗口中运行服务器:

$ python flask1.py

通过将此网址输入浏览器中来测试主页:

http://localhost:9999/

您应该看到以下内容(就像对bottle一样):

My `new` and *`improved`* home page!!!

尝试/echo端点:

http://localhost:9999/echo/Godzilla

您应该看到这个:

Say hello to my little friend: Godzilla

当调用run时将debug设置为True还有另一个好处。如果服务器代码出现异常,Flask 将返回一个格式特殊的页面,其中包含有关发生了什么错误以及在哪里的有用详细信息。更好的是,您可以输入一些命令来查看服务器程序中变量的值。

警告

不要在生产 Web 服务器中设置debug = True。这会向潜在入侵者暴露关于您服务器的过多信息。

到目前为止,Flask 示例只是复制了我们在 Bottle 中所做的事情。Flask 可以做到 Bottle 不能做到的是什么?Flask 包含jinja2,一个更广泛的模板系统。这里是如何将jinja2和 Flask 结合使用的一个小例子。

创建一个名为templates的目录,并在其中创建一个名为flask2.html的文件(示例 18-8)。

示例 18-8. flask2.html
<html>
<head>
<title>Flask2 Example</title>
</head>
<body>
Say hello to my little friend: {{ thing }}
</body>
</html>

接下来,我们编写服务器代码来获取此模板,填充我们传递给它的thing的值,并将其呈现为 HTML(我在这里省略了home()函数以节省空间)。将其保存为flask2.py(示例 18-9)。

示例 18-9. flask2.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/echo/<thing>')
def echo(thing):
    return render_template('flask2.html', thing=thing)

app.run(port=9999, debug=True)

那个thing = thing参数意味着将一个名为thing的变量传递给模板,其值为字符串thing

确保flask1.py没有在运行,并启动flask2.py

$ python flask2.py

现在,输入此网址:

http://localhost:9999/echo/Gamera

您应该看到以下内容:

Say hello to my little friend: Gamera

让我们修改我们的模板,并将其保存在templates目录中,文件名为flask3.html

<html>
<head>
<title>Flask3 Example</title>
</head>
<body>
Say hello to my little friend: {{ thing }}.
Alas, it just destroyed {{ place }}!
</body>
</html>

您可以通过多种方式将第二个参数传递给echo网址。

作为 URL 路径的一部分传递一个参数

使用此方法,您只需扩展 URL 本身。将示例 18-10 中显示的代码保存为flask3a.py

示例 18-10. flask3a.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/echo/<thing>/<place>')
def echo(thing, place):
    return render_template('flask3.html', thing=thing, place=place)

app.run(port=9999, debug=True)

通常情况下,如果之前的测试服务器脚本仍在运行,请先停止它,然后尝试这个新的:

$ python flask3a.py

URL 将如下所示:

http://localhost:9999/echo/Rodan/McKeesport

而且您应该看到以下内容:

Say hello to my little friend: Rodan. Alas, it just destroyed McKeesport!

或者,您可以按照示例 18-11 中显示的方式将参数作为GET参数提供;将其保存为flask3b.py

示例 18-11. flask3b.py
from flask import Flask, render_template, request

app = Flask(__name__)

@app.route('/echo/')
def echo():
    thing = request.args.get('thing')
    place = request.args.get('place')
    return render_template('flask3.html', thing=thing, place=place)

app.run(port=9999, debug=True)

运行新的服务器脚本:

$ python flask3b.py

这一次,使用此网址:

http://localhost:9999/echo?thing=Gorgo&place=Wilmerding

您应该得到与这里看到的相同的结果:

Say hello to my little friend: Gorgo. Alas, it just destroyed Wilmerding!

当使用GET命令访问 URL 时,任何参数都以&*`key1`*=*`val1`*&*`key2`*=*`val2`*&...的形式传递。

你还可以使用字典 ** 运算符将多个参数从单个字典传递到模板(称为 flask3c.py),如 示例 18-12 所示。

示例 18-12. flask3c.py
from flask import Flask, render_template, request

app = Flask(__name__)

@app.route('/echo/')
def echo():
    kwargs = {}
    kwargs['thing'] = request.args.get('thing')
    kwargs['place'] = request.args.get('place')
    return render_template('flask3.html', **kwargs)

app.run(port=9999, debug=True)

**kwargs 就像 thing=thing, place=place。如果有很多输入参数,这样可以节省一些输入。

jinja2 模板语言比这个做的更多。如果你用过 PHP 编程,你会看到很多相似之处。

Django

Django 是一个非常流行的 Python web 框架,特别适用于大型网站。有很多理由值得学习它,包括 Python 工作广告中对 django 经验的频繁需求。它包括 ORM 代码(我们在 “对象关系映射器(ORM)” 中讨论了 ORM)来为典型数据库的 CRUD 功能(创建、替换、更新、删除)自动生成网页,我们在 第十六章 中看过。它还包括一些自动管理页面,但这些页面设计用于程序员内部使用,而不是公共网页使用。如果你喜欢其他 ORM,比如 SQLAlchemy 或直接 SQL 查询,你也不必使用 Django 的 ORM。

其他框架

你可以通过查看这个 在线表格 来比较这些框架:

  • fastapi 处理同步(WSGI)和异步(ASGI)调用,使用类型提示,生成测试页面,文档齐全。推荐使用。

  • web2py 覆盖了与 django 类似的大部分领域,但风格不同。

  • pyramid 从早期的 pylons 项目发展而来,与 django 在范围上类似。

  • turbogears 支持 ORM、多个数据库和多个模板语言。

  • wheezy.web 是一个性能优化的较新框架。它在最近的测试中比其他框架都要

  • molten 也使用类型提示,但仅支持 WSGI。

  • apistar 类似于 fastapi,但更像是 API 验证工具而不是 web 框架。

  • masonite 是 Python 版本的 Ruby on Rails 或 PHP 的 Laravel。

数据库框架

在计算中,网络和数据库就像花生酱和果冻一样,一个地方会找到另一个地方。在现实生活中的 Python 应用程序中,你可能需要为关系数据库提供网页界面(网站和/或 API)。

你可以用以下方法构建自己的:

  • 像 Bottle 或 Flask 这样的 web 框架

  • 一个数据库包,比如 db-api 或 SQLAlchemy

  • 数据库驱动,比如 pymysql

相反,你可以使用像这些之一的 web/数据库包:

或者,你可以使用一个具有内置数据库支持的框架,比如 Django。

你的数据库可能不是关系型的。如果你的数据模式差异显著——不同行之间的列差异较大——考虑使用一个无模式数据库可能会更有价值,比如第十六章中讨论的某些NoSQL数据库。我曾在一个网站工作过,最初将其数据存储在一个 NoSQL 数据库中,然后切换到关系型数据库,再到另一个关系型数据库,然后再到另一个不同的 NoSQL 数据库,最后又回到一个关系型数据库。

Web Services and Automation

我们刚刚看了传统的网络客户端和服务器应用程序,消费和生成 HTML 页面。然而,网络已经被证明是将应用程序和数据粘合在一起的强大方式,支持比 HTML 更多格式的数据。

webbrowser

让我们开始一个小小的惊喜。在终端窗口中启动一个 Python 会话,并输入以下内容:

>>> import antigravity

这会秘密调用标准库的 webbrowser 模块,并将你的浏览器导向一个启发性的 Python 链接。³

你可以直接使用这个模块。这个程序会在你的浏览器中加载主 Python 网站的页面:

>>> import webbrowser
>>> url = 'http://www.python.org/'
>>> webbrowser.open(url)
True

这会在一个新窗口中打开:

>>> webbrowser.open_new(url)
True

这会在一个新标签页中打开,如果你的浏览器支持标签页的话:

>>> webbrowser.open_new_tab('http://www.python.org/')
True

webbrowser 让你的浏览器做所有的工作。

webview

webviewwebbrowser 不同,它在自己的窗口中显示页面,使用您计算机的本地图形用户界面。要在 Linux 或 macOS 上安装:

$ pip install pywebview[qt]

对于 Windows:

$ pip install pywebview[cef]

如果你遇到问题,请查看安装说明

这是一个示例,我访问了美国政府官方当前时间的网站:

>>> import webview
>>> url = input("URL? ")
URL? http://time.gov
>>> webview.create_window(f"webview display of {url}", url)

图 18-1 显示了我得到的结果。

inp2 1801

图 18-1. webview 显示窗口

要停止程序,请关闭显示窗口。

Web API 和 REST

数据通常仅在网页中可用。如果你想要访问这些数据,就需要通过网络浏览器访问页面并读取它。如果网站的作者自你上次访问以来进行了任何更改,数据的位置和样式可能已经改变。

你可以通过网络应用程序编程接口(API)提供数据,而不是发布网页。客户端通过向 URL 发送请求并获取包含状态和数据的响应来访问你的服务。数据不是 HTML 页面,而是更易于程序消费的格式,比如 JSON 或 XML(关于这些格式的更多信息请参考第十六章)。

表述性状态转移(REST)由罗伊·菲尔丁在他的博士论文中定义。许多产品声称拥有REST 接口RESTful 接口。实际上,这通常意味着它们拥有一个Web 接口——用于访问 Web 服务的 URL 定义。

一个RESTful服务以特定方式使用 HTTP 动词

  • HEAD 获取资源的信息,但不获取其数据。

  • GET 从服务器检索资源的数据。这是你的浏览器使用的标准方法。GET 不应该用于创建、更改或删除数据。

  • POST 创建一个新的资源。

  • PUT 替换现有资源,如果不存在则创建。

  • PATCH 部分更新资源。

  • DELETE 删除。广告里面的真相!

一个 RESTful 客户端也可以通过使用 HTTP 请求头从服务器请求一个或多个内容类型。例如,具有 REST 接口的复杂服务可能更喜欢其输入和输出为 JSON 字符串。

爬取和抓取

有时,你可能只想要一点信息——电影评分、股价或产品可用性,但你需要的信息只在由广告和无关内容环绕的 HTML 页面中。

你可以通过以下步骤手动提取你要查找的信息:

  1. 将 URL 输入到你的浏览器中。

  2. 等待远程页面加载。

  3. 在显示的页面中查找你想要的信息。

  4. 把它写在某个地方。

  5. 可能要重复处理相关的网址。

然而,自动化一些或所有这些步骤会更加令人满足。自动网络抓取器称为爬虫蜘蛛。⁴ 在从远程 Web 服务器检索内容后,抓取器会解析它以在海量信息中找到所需的信息。

Scrapy

如果你需要一个强大的联合爬虫抓取器,Scrapy值得下载:

$ pip install scrapy

这将安装模块和一个独立的命令行scrapy程序。

Scrapy 是一个框架,不仅仅是一个模块,比如BeautifulSoup。它功能更强大,但设置更复杂。想了解更多关于 Scrapy 的信息,请阅读“Scrapy 简介”教程

BeautifulSoup

如果你已经从网站获得了 HTML 数据,只想从中提取数据,BeautifulSoup是一个不错的选择。HTML 解析比听起来要难。这是因为公共网页上的大部分 HTML 在技术上都是无效的:未闭合的标签,不正确的嵌套以及其他复杂情况。如果你试图通过使用正则表达式(在“文本字符串:正则表达式”中讨论)编写自己的 HTML 解析器,很快就会遇到这些混乱。

要安装BeautifulSoup,输入以下命令(不要忘记最后的4,否则pip会尝试安装旧版本并且可能失败):

$ pip install beautifulsoup4

现在,让我们用它来获取网页上的所有链接。HTML a 元素表示一个链接,href 是表示链接目标的属性。在例子 18-13 中,我们将定义函数get_links()来完成这项繁重的工作,并且一个主程序来获取一个或多个 URL 作为命令行参数。

例子 18-13. links.py
def get_links(url):
    import requests
    from bs4 import BeautifulSoup as soup
    result = requests.get(url)
    page = result.text
    doc = soup(page)
    links = [element.get('href') for element in doc.find_all('a')]
    return links

if __name__ == '__main__':
    import sys
    for url in sys.argv[1:]:
        print('Links in', url)
        for num, link in enumerate(get_links(url), start=1):
            print(num, link)
        print()

我把这个程序保存为links.py,然后运行这个命令:

$ python links.py http://boingboing.net

这是它打印的前几行:

Links in http://boingboing.net/
1 http://boingboing.net/suggest.html
2 http://boingboing.net/category/feature/
3 http://boingboing.net/category/review/
4 http://boingboing.net/category/podcasts
5 http://boingboing.net/category/video/
6 http://bbs.boingboing.net/
7 javascript:void(0)
8 http://shop.boingboing.net/
9 http://boingboing.net/about
10 http://boingboing.net/contact

Requests-HTML

Kenneth Reitz,流行的 Web 客户端包requests的作者,已经编写了一个名为requests-html的新抓取库(适用于 Python 3.6 及更新版本)。

它获取一个页面并处理其元素,因此你可以查找例如其所有链接或任何 HTML 元素的所有内容或属性。

它具有干净的设计,类似于requests和其他同一作者的包。总体而言,它可能比beautifulsoup或 Scrapy 更容易使用。

让我们看电影

让我们构建一个完整的程序。

它使用 Internet Archive 的 API 搜索视频。⁵ 这是为数不多允许匿名访问在本书印刷后仍应存在的 API 之一。

注意

大多数网络 API 要求你首先获取一个API 密钥,并在每次访问该 API 时提供它。为什么?这是公地悲剧:匿名访问的免费资源经常被过度使用或滥用。这就是为什么我们不能有好东西。

在示例 18-14 中显示的以下程序执行以下操作:

  • 提示你输入电影或视频标题的一部分

  • 在 Internet Archive 搜索它

  • 返回标识符、名称和描述的列表

  • 列出它们并要求你选择其中一个

  • 在你的 Web 浏览器中显示该视频

将此保存为iamovies.py

search()函数使用requests访问 URL,获取结果并将其转换为 JSON。其他函数处理其他所有事务。你会看到列表推导、字符串切片和你在之前章节中看到的其他内容。(行号不是源代码的一部分;它们将用于练习中定位代码片段。)

示例 18-14. iamovies.py
1 """Find a video at the Internet Archive
2 by a partial title match and display it."""
3 
4 import sys
5 import webbrowser
6 import requests
7 
8 def search(title):
9     """Return a list of 3-item tuples (identifier,
10        title, description) about videos
11        whose titles partially match :title."""
12     search_url = "https://archive.org/advancedsearch.php"
13     params = {
14         "q": "title:({}) AND mediatype:(movies)".format(title),
15         "fl": "identifier,title,description",
16         "output": "json",
17         "rows": 10,
18         "page": 1,
19         }
20     resp = requests.get(search_url, params=params)
21     data = resp.json()
22     docs = [(doc["identifier"], doc["title"], doc["description"])
23             for doc in data["response"]["docs"]]
24     return docs
25 
26 def choose(docs):
27     """Print line number, title and truncated description for
28        each tuple in :docs. Get the user to pick a line
29        number. If it's valid, return the first item in the
30        chosen tuple (the "identifier"). Otherwise, return None."""
31     last = len(docs) - 1
32     for num, doc in enumerate(docs):
33         print(f"{num}: ({doc[1]}) {doc[2][:30]}...")
34     index = input(f"Which would you like to see (0 to {last})? ")
35     try:
36         return docs[int(index)][0]
37     except:
38         return None
39 
40 def display(identifier):
41     """Display the Archive video with :identifier in the browser"""
42     details_url = "https://archive.org/details/{}".format(identifier)
43     print("Loading", details_url)
44     webbrowser.open(details_url)
45 
46 def main(title):
47     """Find any movies that match :title.
48        Get the user's choice and display it in the browser."""
49     identifiers = search(title)
50     if identifiers:
51         identifier = choose(identifiers)
52         if identifier:
53             display(identifier)
54         else:
55             print("Nothing selected")
56     else:
57         print("Nothing found for", title)
58 
59 if __name__ == "__main__":
60     main(sys.argv[1])

这是我运行此程序并搜索**eegah**时得到的结果:⁶

$ python iamovies.py eegah
0: (Eegah) From IMDb : While driving thro...
1: (Eegah) This film has fallen into the ...
2: (Eegah) A caveman is discovered out in...
3: (Eegah (1962)) While driving through the dese...
4: (It's "Eegah" - Part 2) Wait till you see how this end...
5: (EEGAH trailer) The infamous modern-day cavema...
6: (It's "Eegah" - Part 1) Count Gore De Vol shares some ...
7: (Midnight Movie show: eegah) Arch Hall Jr...
Which would you like to see (0 to 7)? 2
Loading https://archive.org/details/Eegah

它在我的浏览器中显示了页面,准备运行(图 18-2)。

inp2 1802

图 18-2. 电影搜索结果

即将发生的事情

下一章是一个非常实用的章节,涵盖现代 Python 开发的方方面面。学习如何成为一个眼明手快、持有 Python 会员卡的 Pythonista。

要做的事情

18.1 如果你还没有安装flask,请立即安装。这也会安装werkzeugjinja2和可能的其他包。

18.2 使用 Flask 的调试/重新加载开发 Web 服务器构建一个骨架网站。确保服务器在默认端口5000上为主机名localhost启动。如果你的计算机已经在使用端口 5000 进行其他操作,请使用其他端口号。

18.3 添加一个home()函数来处理对主页的请求。设置它返回字符串It's alive!

18.4 创建一个名为home.html的 Jinja2 模板文件,内容如下:

<html>
<head>
<title>It's alive!</title>
<body>
I'm of course referring to {{thing}}, which is {{height}} feet tall and {{color}}.
</body>
</html>

18.5 修改你的服务器的home()函数,使用home.html模板。为其提供三个GET参数:thingheightcolor

¹ 史蒂夫·乔布斯在被迫离开苹果期间创立的一家公司。

² 让我们在这里揭穿一个僵尸谎言。参议员(后来是副总统)阿尔·戈尔倡导了大力推动早期互联网发展的两党立法和合作,包括为编写 Mosaic 的团队提供资金。他从未声称自己“发明了互联网”;这个短语是在 2000 年他开始竞选总统时由政治对手错误地归因给他的。

³ 如果出现某种原因没有看到它,请访问xkcd

⁴ 对恐蜘蛛症患者来说,这些术语可能不吸引人。

⁵ 如果你还记得,在我们在第一章中看到的主要示例程序中,我使用了另一个档案 API。

⁶ 由理查德·基尔饰演穴居人,这是他在邦德电影中扮演钢牙前的多年。

第十九章:成为 Pythonista

一直想回到过去试试和年轻时的自己斗智斗勇?软件开发就是你的职业选择!

Elliot Loh

本章专注于 Python 开发的艺术和科学,提供“最佳实践”建议。吸收它们,你也能成为一名 Pythonista。

关于编程

首先,根据个人经验谈一些关于编程的注意事项。

我最初的职业路径是科学,我自学编程来分析和展示实验数据。我曾以为计算机编程会像我对会计的印象一样——精确但乏味。我惊讶地发现我很喜欢它。其中一部分乐趣来自逻辑方面——像解谜题一样——但部分也来自创造性。你必须正确地编写程序才能得到正确的结果,但你有自由按照自己的方式编写。这是左右脑思维的一种不寻常的平衡。

在我迷失在编程职业的道路后,我也发现这个领域有很多不同的小众,有着截然不同的任务和人群。你可以深入研究计算机图形学、操作系统、商业应用甚至科学领域。

如果你是一名程序员,你可能也有类似的经历。如果不是,你可以尝试一点编程,看看它是否适合你的个性,或者至少能帮助你完成一些任务。正如我在这本书的早期可能提到的那样,数学技能并不是那么重要。似乎最重要的是逻辑思维能力,语言天赋也有所帮助。最后,耐心也很重要,特别是当你在代码中追踪一个难以捉摸的 bug 时。

查找 Python 代码

当你需要开发一些代码时,最快的解决方案是从允许的来源“借”一些代码。

Python 标准库非常广泛、深入且大部分清晰。深入其中,寻找那些珍珠吧。

就像各种体育名人堂一样,一个模块要进入标准库需要时间。新的包不断出现在外面,本书中我突出了一些新功能或者更好的旧功能。Python 被宣传为“内置电池”,但你可能需要一种新型电池。

那么,在标准库之外,你应该去哪里寻找好的 Python 代码?

首先要查找的地方是Python Package Index (PyPI)。这个网站之前被称为蒙提·派森小品《奶酪商店》(Cheese Shop),我写作时这个网站上不断更新着 Python 的包——超过 113,000 个。当你使用 pip(见下一节)时,它会搜索 PyPI。主 PyPI 页面显示了最近添加的包。你还可以通过在 PyPI 主页中间的搜索框中输入内容进行直接搜索。例如,genealogy 返回 21 个匹配项,movies 返回 528 个。

另一个流行的存储库是 GitHub。查看当前哪些 Python 包正在流行

热门 Python 示例拥有超过四千个短的 Python 程序,涵盖各个主题。

安装包

有许多安装 Python 包的方法:

  • 如果可以的话,请使用pip。这是目前最常见的方法。你可以使用pip安装大多数你可能遇到的 Python 包。

  • 使用pipenv,它结合了pipvirtualenv

  • 有时,你可以使用操作系统的包管理器。

  • 如果你经常从事科学工作并想使用 Anaconda Python 发行版,请使用conda。有关详情,请参阅“安装 Anaconda”。

  • 从源码安装。

如果你对同一领域的几个包感兴趣,你可以找到一个已经包含它们的 Python 发行版。例如,在第二十二章中,你可以尝试一些数字和科学程序,这些程序单独安装会很繁琐,但在 Anaconda 等发行版中已经包含在内。

使用 pip

Python 包装存在一些限制。一个早期的安装工具叫做easy_install已经被一个叫做pip的工具取代,但它们都没有被包含在标准的 Python 安装中。如果你应该使用pip来安装东西,那你从哪里获取pip?从 Python 3.4 开始,pip最终将包含在 Python 的其他部分中,以避免这种存在危机。如果你正在使用较早版本的 Python 3 并且没有pip,你可以从http://www.pip-installer.org获取它。

使用pip的最简单方法是使用以下命令安装最新版本的单个包:

$ pip install flask

你将看到它正在做什么,这样你就不会认为它在闲着:下载、运行 setup.py、在你的磁盘上安装文件等细节。

你也可以要求pip安装特定版本:

$ pip install flask==0.9.0

或者,一个最小版本(当你不能没有某个特定版本的功能时,这是很有用的):

$ pip install 'flask≥0.9.0'

在上面的示例中,那些单引号防止了>被 Shell 解释为将输出重定向到一个名为=0.9.0的文件。

如果你想安装多个 Python 包,你可以使用一个requirements 文件。虽然它有许多选项,但最简单的用法是一个包的列表,每行一个,可选地带有特定或相对版本:

$ pip -r requirements.txt

你的示例 requirements.txt 文件可能包含这样的内容:

flask==0.9.0
django
psycopg2

更多示例:

  • 安装最新版本:pip install --upgrade *package*

  • 删除一个包:pip uninstall *package*

使用 virtualenv

安装第三方 Python 包的标准方式是使用pipvirtual``env。我将展示如何在“安装 virtualenv”中安装virtualenv

虚拟环境只是一个包含 Python 解释器、像pip这样的其他程序和一些包的目录。你可以通过运行虚拟环境中bin目录下的activate脚本来激活它。这会设置环境变量$PATH,你的 shell 用它来查找程序。通过激活虚拟环境,你将其bin目录放在通常的目录如/bin/usr/bin/usr/local/bin之前。结果是,当你输入像pippython这样的命令时,你的 shell 首先在虚拟环境中找到它们,而不是系统目录中的版本。

你不想将软件安装到这些系统目录中,因为:

  • 你没有写入它们的权限。

  • 即使可以,覆盖系统的标准程序(如python)可能会引起问题。

使用 pipenv

最近有一个叫做pipenv的包结合了我们的朋友pipvirtualenv。它还解决了在不同环境中(例如本地开发机器、测试和生产环境)使用pip时可能出现的依赖问题。

$ pip install pipenv

Python Packaging Authority推荐使用它——一个致力于改进 Python 打包工作流程的工作组。这不同于定义核心 Python 本身的组织,所以pipenv不是标准库的一部分。

使用包管理器

Apple 的 macOS 包括第三方包管理器homebrewbrew)和ports。它们有点像pip,但不限于 Python 包。

Linux 对于每个发行版有不同的管理器。最流行的是apt-getyumdpkgzypper

Windows 有 Windows Installer 和带有*.msi*后缀的包文件。如果你在 Windows 上安装 Python,可能是以 MSI 格式。

从源代码安装

有时,一个 Python 包是新的,或者作者还没有通过pip提供它。要构建这个包,通常需要执行以下步骤:

  • 下载代码。

  • 如果文件已经被归档或压缩,使用ziptar或其他适当的工具来提取它们。

  • 在包含setup.py文件的目录中运行python setup.py install

注意

一如既往,请注意你下载和安装的内容。在 Python 程序中隐藏恶意软件稍微困难些,因为它们是可读文本,但确实发生过。

集成开发环境

我在本书中的程序都用纯文本界面,但这并不意味着你需要在控制台或文本窗口中运行所有内容。有许多免费和商业的集成开发环境(IDE),它们是带有文本编辑器、调试器、库搜索等工具支持的图形界面。

IDLE

IDLE是唯一包含在标准发行版中的 Python IDE。它基于 tkinter,其 GUI 界面简单。

PyCharm

PyCharm 是一个具有许多功能的新型图形 IDE。社区版是免费的,你可以获得专业版的免费许可证,用于课堂或开源项目中。Figure 19-1 显示了它的初始显示。

inp2 1901

图 19-1. PyCharm 的启动界面

IPython

iPython 最初是一个增强的文本 Python IDE 终端,但随后演变成具有类似“笔记本”的图形界面。它集成了许多本书讨论的包,包括 Matplotlib 和 NumPy,并成为科学计算中的热门工具。

你可以使用 pip install ipython 安装基本的文本版本。当你启动它时,你会看到类似于这样的界面:

$ ipython
Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 16:39:00)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.3.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]:

正如你所知,标准的 Python 解释器使用输入提示符 >>>... 来指示何时何地应该输入代码。IPython 会将你输入的所有内容记录在一个名为 In 的列表中,将所有的输出记录在 Out 中。每个输入可能包含多行,因此,你可以按住 Shift 键并同时按 Enter 键来提交它。

这是一个单行示例:

In [1]: print("Hello? World?")
Hello? World?

In [2]:

InOut 是自动编号的列表,让你可以访问你输入的任何输入或收到的输出。

如果你在一个变量后面输入 ?,IPython 将告诉你它的类型、值、创建该类型变量的方法以及一些解释:

In [4]: answer = 42

In [5]: answer?
Type:       int
String Form:42
Docstring:
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
given base.  The literal can be preceded by '+' or '-' and be surrounded
by whitespace.  The base defaults to 10\.  Valid bases are 0 and 2-36.
Base 0 means to interpret the base from the string as an integer literal.
>>> int('0b100', base=0)
4

名称查找是像 IPython 这样的 IDE 的一个流行特性。如果你在一些字符后面立即按 Tab 键,IPython 将显示所有以这些字符开头的变量、关键字和函数。让我们定义一些变量,然后找出所有以字母 f 开头的内容:

In [6]: fee = 1

In [7]: fie = 2

In [8]: fo = 3

In [9]: fum = 4

In [10]: f*tab*
%%file    fie       finally   fo        format    frozenset
fee       filter    float     for       from      fum

如果你输入 fe,然后按 Tab 键,它会展开为变量 fee,在这个程序中,它是以 fe 开头的唯一内容:

In [11]: fee
Out[11]: 1

IPython 还有更多功能。看看它的 教程,了解它的特性。

Jupyter Notebook

Jupyter 是 IPython 的一个进化。它的名称结合了数据科学和科学计算中流行的 Julia、Python 和 R 语言。Jupyter Notebooks 是一种现代的方式,可以用于开发和发布你的代码,并包含任何这些语言的文档。

如果你想在计算机上安装任何东西之前先玩一下,你可以首先在你的网络浏览器中 试试

要在本地安装 Jupyter Notebook,请输入 pip install jupyter。使用 jupyter notebook 命令来运行它。

JupyterLab

JupyterLab 是 Jupyter Notebook 的下一代,并最终将取代它。和 Notebook 一样,你可以在浏览器中 试用 JupyterLab。你可以通过 pip install jupyterlab 在本地安装它,然后用 jupyter lab 运行它。

名称和文档

你不会记得自己写了什么。有时候,我看着自己最近写的代码,想知道它到底是从哪儿来的。这就是为什么文档化你的代码很有帮助。文档可以包括注释和文档字符串,但也可以包括对变量、函数、模块和类进行信息丰富的命名。不要过度追求,就像这个例子中一样:

>>> # I'm going to assign 10 to the variable "num" here:
... num = 10
>>> # I hope that worked
... print(num)
10
>>> # Whew.

相反,说清楚为什么你给变量赋值10。指出为什么叫变量名为num。如果你正在写经典的华氏度到摄氏度转换器,你可能会命名变量来解释它们的作用,而不是一团神奇的代码。而且一点测试代码也不会有什么坏处(示例 19-1)。

示例 19-1. ftoc1.py
def ftoc(f_temp):
    "Convert Fahrenheit temperature <f_temp> to Celsius and return it."
    f_boil_temp = 212.0
    f_freeze_temp = 32.0
    c_boil_temp = 100.0
    c_freeze_temp = 0.0
    f_range = f_boil_temp - f_freeze_temp
    c_range = c_boil_temp - c_freeze_temp
    f_c_ratio = c_range / f_range
    c_temp = (f_temp - f_freeze_temp) * f_c_ratio + c_freeze_temp
    return c_temp

if __name__ == '__main__':
    for f_temp in [-40.0, 0.0, 32.0, 100.0, 212.0]:
        c_temp = ftoc(f_temp)
        print('%f F => %f C' % (f_temp, c_temp))

让我们运行测试:

$ python ftoc1.py
-40.000000 F => -40.000000 C
0.000000 F => -17.777778 C
32.000000 F => 0.000000 C
100.000000 F => 37.777778 C
212.000000 F => 100.000000 C

我们可以做(至少)两个改进:

  • Python 没有常量,但 PEP8 样式表 建议 在命名变量时使用大写字母和下划线(例如 ALL_CAPS)。让我们在我们的示例中将那些看起来像常量的变量重命名。

  • 因为我们基于常量值预计算值,让我们将它们移到模块的顶层。然后,它们将只在每次调用ftoc()函数时计算一次,而不是每次调用时都计算。

示例 19-2 展示了我们重做的结果。

示例 19-2. ftoc2.py
F_BOIL_TEMP = 212.0
F_FREEZE_TEMP = 32.0
C_BOIL_TEMP = 100.0
C_FREEZE_TEMP = 0.0
F_RANGE = F_BOIL_TEMP - F_FREEZE_TEMP
C_RANGE = C_BOIL_TEMP - C_FREEZE_TEMP
F_C_RATIO = C_RANGE / F_RANGE

def ftoc(f_temp):
    "Convert Fahrenheit temperature <f_temp> to Celsius and return it."
    c_temp = (f_temp - F_FREEZE_TEMP) * F_C_RATIO + C_FREEZE_TEMP
    return c_temp

if __name__ == '__main__':
    for f_temp in [-40.0, 0.0, 32.0, 100.0, 212.0]:
        c_temp = ftoc(f_temp)
        print('%f F => %f C' % (f_temp, c_temp))

添加类型提示

静态语言要求您定义变量的类型,并且它们可以在编译时捕获一些错误。如您所知,Python 不会这样做,只有在运行代码时才会遇到错误。Python 变量是名称,只是引用实际对象。对象具有严格的类型,但名称可以随时指向任何对象。

然而,在实际代码中(无论是 Python 还是其他语言),名称往往指向特定的对象。至少在文档中,如果我们能够注释事物(变量、函数返回等)与我们期望它们引用的对象类型,则会有所帮助。然后,开发人员就不需要查看那么多代码来了解特定变量应该如何操作。

Python 3.x 添加了类型提示(或类型注释)来解决这个问题。这完全是可选的,并且不会强制变量的类型。它帮助那些习惯于静态语言的开发人员,其中变量类型必须声明。

一个将数字转换为字符串的函数的提示看起来是这样的:

def num_to_str(num: int) -> str:
    return str(num)

这些只是提示,并不会改变 Python 的运行方式。它们的主要用途是用于文档,但人们正在找到更多用途。例如,FastAPI web 框架使用提示生成带有实时表单用于测试的 web 文档。

测试

你可能已经知道了,但如果不知道的话:即使是微不足道的代码更改也可能会破坏你的程序。Python 缺乏静态语言的类型检查,这使得某些事情变得更容易,但也让不良结果通过了大门。测试是必不可少的。

测试 Python 程序的最简单方法是添加print()语句。Python 交互式解释器的 REPL(Read-Evaluate-Print Loop)允许您快速编辑和测试更改。但是,在生产代码中您不希望有print()语句,因此需要记住全部删除它们。

使用 pylint、pyflakes、flake8 或 pep8 进行检查

在创建实际测试程序之前的下一步是运行 Python 代码检查器。最流行的是pylintpyflakes。您可以使用pip安装其中一个或两个:

$ pip install pylint
$ pip install pyflakes

它们检查实际的代码错误(例如在给变量赋值之前引用它)和风格上的失误(代码版的搭配格格不入)。示例 19-3 是一个有错误和风格问题的相当无意义的程序。

示例 19-3. style1.py
a = 1
b = 2
print(a)
print(b)
print(c)

这是pylint的初始输出:

$ pylint style1.py
No config file found, using default configuration
************* Module style1
C:  1,0: Missing docstring
C:  1,0: Invalid name "a" for type constant
  (should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
C:  2,0: Invalid name "b" for type constant
  (should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
E:  5,6: Undefined variable 'c'

在“全局评估”下,我们的分数(10.0 是完美的)进一步下降了:

Your code has been rated at -3.33/10

糟糕了。让我们先修复错误。那个以E开头的pylint输出行表示一个Error,因为我们在打印它之前没有给c赋值。查看 示例 19-4 看看我们如何修复它。

示例 19-4. style2.py
a = 1
b = 2
c = 3
print(a)
print(b)
print(c)
$ pylint style2.py
No config file found, using default configuration
************* Module style2
C:  1,0: Missing docstring
C:  1,0: Invalid name "a" for type constant
  (should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
C:  2,0: Invalid name "b" for type constant
  (should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
C:  3,0: Invalid name "c" for type constant
  (should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)

很好,不再有E行了。我们的分数也从-3.33 上升到了 4.29:

Your code has been rated at 4.29/10

pylint希望有一个文档字符串(一个简短的文本,位于模块或函数顶部,描述代码),并认为像abc这样的短变量名是俗气的。让我们让pylint更开心,并将 style2.py 改进为 style3.py (示例 19-5)。

示例 19-5. style3.py
"Module docstring goes here"

def func():
    "Function docstring goes here. Hi, Mom!"
    first = 1
    second = 2
    third = 3
    print(first)
    print(second)
    print(third)

func()
$ pylint style3.py
No config file found, using default configuration

嘿,没有抱怨。我们的分数呢?

Your code has been rated at 10.00/10

大家都很高兴。

另一个风格检查器是pep8,您现在可能已经可以在睡觉时安装它了:

$ pip install pep8

这对我们的风格改头换面有何建议?

$ pep8 style3.py
style3.py:3:1: E302 expected 2 blank lines, found 1

要真正时尚,它建议我在初始模块文档字符串后添加一个空行。

使用 unittest 进行测试

我们已经验证我们不再侮辱代码之神的审美感,所以让我们转向测试您程序逻辑的实际测试。

在提交代码到任何源代码管理系统之前,先编写独立的测试程序是一个良好的实践。最初编写测试可能看起来很乏味,但它们确实能帮助您更快地发现问题,尤其是回归(破坏了曾经工作的东西)。痛苦的经验教会所有开发者,即使是他们发誓不可能影响其他任何东西的微小改变,实际上也会如此。看看写得好的 Python 包,它们总是包含一个测试套件。

标准库中不止一个测试包。让我们从 unittest 开始。我们将编写一个将单词首字母大写的模块。我们的第一个版本仅使用标准字符串函数 capitalize(),结果如我们所见有些意外。将其保存为 cap.py (示例 19-6)。

示例 19-6. cap.py
def just_do_it(text):
    return text.capitalize()

测试的基础是决定从特定输入中得到什么结果(这里,您希望输入的任何文本的大写版本),将输入提交给您正在测试的函数,然后检查它是否返回了预期的结果。预期的结果称为断言,因此在unittest中,您通过使用以assert开头的方法(例如在 Example 19-7 中显示的assertEqual方法)来检查结果。

将此测试脚本保存为test_cap.py

Example 19-7. test_cap.py
import unittest
import cap

class TestCap(unittest.TestCase):

    def setUp(self):
        pass

    def tearDown(self):
        pass

    def test_one_word(self):
        text = 'duck'
        result = cap.just_do_it(text)
        self.assertEqual(result, 'Duck')

    def test_multiple_words(self):
        text = 'a veritable flock of ducks'
        result = cap.just_do_it(text)
        self.assertEqual(result, 'A Veritable Flock Of Ducks')

if __name__ == '__main__':
    unittest.main()

setUp()方法在每个测试方法之前调用,而tearDown()方法在每个测试方法之后调用。它们的目的是为测试分配和释放外部资源,例如数据库连接或某些测试数据。在这种情况下,我们的测试是独立的,甚至不需要定义setUp()tearDown(),但留有空版本也无妨。在我们的测试核心是两个名为test_one_word()test_multiple_words()的函数。每个函数都使用不同的输入运行我们定义的just_do_it()函数,并检查我们是否得到了预期的结果。好的,让我们运行它。这将调用我们的两个测试方法:

$ python test_cap.py
 F.
 ======================================================================
 FAIL: test_multiple_words (__main__.TestCap)
 ----------------------------------------------------------------------
 Traceback (most recent call last):
   File "test_cap.py", line 20, in test_multiple_words
  self.assertEqual(result, 'A Veritable Flock Of Ducks')
 AssertionError: 'A veritable flock of ducks' != 'A Veritable Flock Of Ducks'
 - A veritable flock of ducks
 ?   ^         ^     ^  ^
 + A Veritable Flock Of Ducks
 ?   ^         ^     ^  ^

 ----------------------------------------------------------------------
 Ran 2 tests in 0.001s

 FAILED (failures=1)

它喜欢第一个测试(test_one_word),但不喜欢第二个(test_multiple_words)。上箭头(^)显示了字符串实际上有所不同的地方。

多个单词有什么特别之处?阅读string capitalize函数的文档提供了一个重要线索:它仅大写第一个单词的第一个字母。也许我们应该先读这个文档。

因此,我们需要另一个函数。在页面底部略微望去,我们找到了title()。因此,让我们更改cap.py以使用title()而不是capitalize()(Example 19-8)。

Example 19-8. cap.py, revised
def just_do_it(text):
    return text.title()

重新运行测试,看看会发生什么:

$ python test_cap.py
 ..
 ----------------------------------------------------------------------
 Ran 2 tests in 0.000s

 OK

一切都很好。好吧,实际上并不是。我们需要在test_cap.py中至少再添加一个方法来测试(Example 19-9)。

Example 19-9. test_cap.py, revised
    def test_words_with_apostrophes(self):
        text = "I'm fresh out of ideas"
        result = cap.just_do_it(text)
        self.assertEqual(result, "I'm Fresh Out Of Ideas")

继续尝试一下:

$ python test_cap.py
 ..F
 ======================================================================
 FAIL: test_words_with_apostrophes (__main__.TestCap)
 ----------------------------------------------------------------------
 Traceback (most recent call last):
   File "test_cap.py", line 25, in test_words_with_apostrophes
     self.assertEqual(result, "I'm Fresh Out Of Ideas")
 AssertionError: "I'M Fresh Out Of Ideas" != "I'm Fresh Out Of Ideas"
 - I'M Fresh Out Of Ideas
 ?   ^
 + I'm Fresh Out Of Ideas
 ?   ^

 ----------------------------------------------------------------------
 Ran 3 tests in 0.001s

 FAILED (failures=1)

我们的函数将I'm中的m大写化了。快速查阅title()的文档后显示,它不能很好地处理撇号。我们真的应该先完整地阅读整个文本。

string文档的底部还有另一个候选:一个名为capwords()的辅助函数。让我们在cap.py中使用它(Example 19-10)。

Example 19-10. cap.py, re-revised
def just_do_it(text):
    from string import capwords
    return capwords(text)
$ python test_cap.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.004s

OK

终于,我们终于完成了!嗯,不对。我们还需要在test_cap.py中添加另一个测试(Example 19-11)。

Example 19-11. test_cap.py, re-revised
    def test_words_with_quotes(self):
        text = "\"You're despicable,\" said Daffy Duck"
        result = cap.just_do_it(text)
        self.assertEqual(result, "\"You're Despicable,\" Said Daffy Duck")

它工作了吗?

$ python test_cap.py
...F
======================================================================
FAIL: test_words_with_quotes (__main__.TestCap)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_cap.py", line 30, in test_words_with_quotes
    self.assertEqual(result, "\"You're
    Despicable,\" Said Daffy Duck") AssertionError: '"you\'re Despicable,"
        Said Daffy Duck'
 != '"You\'re Despicable," Said Daffy Duck' - "you're Despicable,"
        Said Daffy Duck
?  ^ + "You're Despicable," Said Daffy Duck
?  ^
----------------------------------------------------------------------
Ran 4 tests in 0.004s

FAILED (failures=1)

看起来第一个双引号甚至搞混了我们到目前为止最喜欢的大写功能capwords。它尝试大写",并小写了其余部分(You're)。我们还应该测试我们的大写器是否会保留字符串的其余部分。

以职业测试为生的人对于发现这些边缘情况有一种天赋,但开发人员在自己的代码中往往有盲点。

unittest提供了一组小而强大的断言,让你可以检查值,确认是否有你想要的类,确定是否引发了错误等等。

使用 doctest 进行测试

标准库中的第二个测试包是doctest。使用这个包,你可以在文档字符串内部编写测试,同时作为文档。它看起来像交互式解释器:字符>>>,然后是调用,然后是下一行的结果。你可以在交互式解释器中运行一些测试,然后将结果粘贴到你的测试文件中。让我们将我们旧的cap.py修改为cap2.py(去掉那个带引号的麻烦的最后一个测试),如示例 19-12 所示。

示例 19-12. cap2.py
def just_do_it(text):
    """
 >>> just_do_it('duck')
 'Duck'
 >>> just_do_it('a veritable flock of ducks')
 'A Veritable Flock Of Ducks'
 >>> just_do_it("I'm fresh out of ideas")
 "I'm Fresh Out Of Ideas"
 """
    from string import capwords
    return capwords(text)

if __name__ == '__main__':
    import doctest
    doctest.testmod()

当你运行它时,如果所有测试都通过,它不会打印任何内容:

$ python cap2.py

给它加上详细选项(-v),看看到底发生了什么:

$ python cap2.py -v
Trying:
    just_do_it('duck')
    Expecting:
    'Duck'
ok
Trying:
     just_do_it('a veritable flock of ducks')
Expecting:
     'A Veritable Flock Of Ducks'
ok
Trying:
     just_do_it("I'm fresh out of ideas")
Expecting:
     "I'm Fresh Out Of Ideas"
ok
1 items had no tests:
     __main__
1 items passed all tests:
    3 tests in __main__.just_do_it
3 tests in 2 items.
3 passed and 0 failed.
Test passed.

使用 nose 进行测试

第三方包叫做noseunittest的另一种选择。这里是安装它的命令:

$ pip install nose

你不需要创建一个包含测试方法的类,就像我们在unittest中所做的那样。任何函数名中包含test的函数都会被运行。让我们修改我们unittest的最新版本,并保存为test_cap_nose.py(示例 19-13)。

示例 19-13. test_cap_nose.py
import cap2
from nose.tools import eq_

def test_one_word():
    text = 'duck'
    result = cap.just_do_it(text)
    eq_(result, 'Duck')

def test_multiple_words():
    text = 'a veritable flock of ducks'
    result = cap.just_do_it(text)
    eq_(result, 'A Veritable Flock Of Ducks')

def test_words_with_apostrophes():
    text = "I'm fresh out of ideas"
    result = cap.just_do_it(text)
    eq_(result, "I'm Fresh Out Of Ideas")

def test_words_with_quotes():
    text = "\"You're despicable,\" said Daffy Duck"
    result = cap.just_do_it(text)
    eq_(result, "\"You're Despicable,\" Said Daffy Duck")

运行测试:

$ nosetests test_cap_nose.py
...F
======================================================================
FAIL: test_cap_nose.test_words_with_quotes
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/.../site-packages/nose/case.py", line 198, in runTest
    self.test(*self.arg)
  File "/Users/.../book/test_cap_nose.py", line 23, in test_words_with_quotes
    eq_(result, "\"You're Despicable,\" Said Daffy Duck")
AssertionError: '"you\'re Despicable," Said Daffy Duck'      !=
'"You\'re Despicable," Said Daffy Duck'
----------------------------------------------------------------------
Ran 4 tests in 0.005s

FAILED (failures=1)

这是我们在使用unittest进行测试时发现的同样的 bug;幸运的是,本章末尾有一个修复它的练习。

其他测试框架

由于某些原因,人们喜欢编写 Python 测试框架。如果你感兴趣,可以看看其他流行的一些:

持续集成

当你的团队每天输出大量代码时,自动化测试在变更到来时会很有帮助。你可以自动化源代码控制系统以在检入时对所有代码运行测试。这样,每个人都知道是否有人破坏了构建,然后提前吃午餐——或者找新工作。

这些都是庞大的系统,我这里不会详细讨论安装和使用细节。万一有一天你需要它们,你会知道在哪里找到它们:

buildbot

这个源代码控制系统用 Python 编写,自动化了构建、测试和发布。

jenkins

这是用 Java 编写的,似乎是目前首选的 CI 工具。

travis-ci

这个自动化项目托管在 GitHub 上,并且对开源项目是免费的。

circleci

这个是商业的,但对于开源项目和私有项目是免费的。

调试 Python 代码

调试就像是一部侦探电影中你既是侦探又是凶手的情节。

菲利普·福尔特斯

每个人都知道调试比一开始编写程序要难两倍。所以如果你在编写时尽可能聪明,你将如何调试呢?

Brian Kernighan

先测试。你的测试越好,后面就需要修复的 bug 就越少。然而,bug 会发生,需要在稍后找到并修复。

当代码出错时,通常是因为你刚刚做的某事。因此,你通常会“自下而上”调试,从最近的更改开始。¹

但有时候原因在别处,可能是你信任并认为有效的某些东西。你会认为如果有问题是由很多人使用的东西引起的,现在应该已经有人注意到了。但事实并不总是如此。我遇到的最棘手的 bug,每一个都花了一周以上的时间来修复,都是由外部原因引起的。所以,在责怪镜子中的那个人之后,要质疑你的假设。这是一种“自上而下”的方法,而且需要更长的时间。

以下是一些调试技术,从快速且脏到更慢但通常同样脏的技术。

在 Python 中调试的最简单方法是打印字符串。一些有用的要打印的东西包括 vars(),它提取你的局部变量的值,包括函数参数:

>>> def func(*args, **kwargs):
...     print(vars())
...
>>> func(1, 2, 3)
{'args': (1, 2, 3), 'kwargs': {}}
>>> func(['a', 'b', 'argh'])
{'args': (['a', 'b', 'argh'],), 'kwargs': {}}

通常值得打印的其他东西包括 locals()globals()

如果你的代码还包括普通的打印输出,你可以使用 print(*stuff*, file=sys.stderr) 将你的调试信息写入标准错误输出。

使用装饰器

正如你在“装饰器”中所读到的,装饰器可以在函数之前或之后调用代码,而不修改函数本身的代码。这意味着你可以在任何 Python 函数之前或之后使用装饰器来执行某些操作,而不仅仅是你编写的函数。让我们定义一个装饰器 dump 来打印任何函数在被调用时的输入参数和输出值(设计者知道装饰时经常需要装饰),如 示例 19-14 所示。

示例 19-14. dump.py
def dump(func):
    "Print input arguments and output value(s)"
    def wrapped(*args, **kwargs):
        print("Function name:", func.__name__)
        print("Input arguments:", ' '.join(map(str, args)))
        print("Input keyword arguments:", kwargs.items())
        output = func(*args, **kwargs)
        print("Output:", output)
        return output
    return wrapped

现在是被装饰的函数。这是一个名为 double() 的函数,它期望数值参数,可以是命名的或未命名的,并将它们以列表形式返回,其中包含它们的值加倍(参见 示例 19-15)。

示例 19-15. test_dump.py
from dump import dump

@dump
def double(*args, **kwargs):
    "Double every argument"
    output_list = [ 2 * arg for arg in args ]
    output_dict =  { k:2*v for k,v in kwargs.items() }
    return output_list, output_dict

if __name__ == '__main__':
    output = double(3, 5, first=100, next=98.6, last=-40)

花一点时间运行它:

$ python test_dump.py
Function name: double
Input arguments: 3 5
Input keyword arguments: dict_items([('first', 100), ('next', 98.6),
    ('last', -40)])
Output: ([6, 10], {'first': 200, 'next': 197.2, 'last': -80})

使用 pdb

这些技术有所帮助,但有时候没有真正调试器来得有效。大多数集成开发环境都包含了调试器,具有各种功能和用户界面。在这里,我描述了如何使用标准的 Python 调试器,pdb

注意

如果你用 -i 标志运行你的程序,如果程序失败,Python 就会将你带入它的交互式解释器中。

这是一个依赖数据的 bug 的程序,这种 bug 特别难以找到。这是早期计算机的一个真正的 bug,它让程序员们困惑了很长时间。

我们将读取一个国家和其首都的文件,用逗号分隔,然后将它们写成 capital, country 的形式。在打印时,它们可能有错误的大写,所以我们也要修正。哦,这里可能会有额外的空格,你会想把它们去掉。最后,尽管程序读到文件末尾会更合理,但由于某种原因,我们的经理告诉我们遇到 quit 这个词时要停止(不区分大小写)。示例 19-16 展示了一个样本数据文件。

示例 19-16. cities.csv
France, Paris
venuzuela,caracas
  LithuaniA,vilnius
     quit

让我们设计我们的 算法(解决问题的方法)。这是 伪代码 ——它看起来像一个程序,但只是一种在转换为实际程序之前用普通语言解释逻辑的方法。程序员喜欢 Python 的一个原因是因为它 看起来很像伪代码,所以在转换为工作程序时工作量较少:

for each line in the text file:
    read the line
    strip leading and trailing spaces
    if `quit` occurs in the lower-case copy of the line:
        stop
    else:
        split the country and capital by the comma character
        trim any leading and trailing spaces
        convert the country and capital to titlecase
        print the capital, a comma, and the country

我们需要从名称中去掉开头和结尾的空格,因为那是一个要求。同样对于与 quit 的小写比较和将城市和国家名称转换为首字母大写格式也是如此。既然如此,让我们拿出 capitals.py,它肯定能完美运行(示例 19-17)。

示例 19-17. capitals.py
def process_cities(filename):
    with open(filename, 'rt') as file:
        for line in file:
            line = line.strip()
            if 'quit' in line.lower():
                return
            country, city = line.split(',')
            city = city.strip()
            country = country.strip()
            print(city.title(), country.title(), sep=',')

if __name__ == '__main__':
    import sys
    process_cities(sys.argv[1])

让我们尝试使用之前制作的样本数据文件。准备好了,开火:

$ python capitals.py cities.csv
Paris,France
Caracas,Venuzuela
Vilnius,Lithuania

看起来很不错!它通过了一个测试,所以让我们把它投入到生产中,处理来自世界各地的首都和国家——直到它失败,但仅限于这个数据文件(示例 19-18)。

示例 19-18. cities2.csv
argentina,buenos aires
bolivia,la paz
brazil,brasilia
chile,santiago
colombia,Bogotá
ecuador,quito
falkland islands,stanley
french guiana,cayenne
guyana,georgetown
paraguay,Asunción
peru,lima
suriname,paramaribo
uruguay,montevideo
venezuela,caracas
quit

程序在打印了数据文件中的 15 行中的 5 行后结束,如下所示:

$ python capitals.py cities2.csv
Buenos Aires,Argentina
La Paz,Bolivia
Brazilia,Brazil
Santiago,Chile
Bogotá,Colombia

发生了什么?我们可以继续编辑 capitals.py,在可能的地方加入 print() 语句,但让我们看看调试器能否帮助我们。

要使用调试器,在命令行中导入 pdb 模块,输入 -m pdb,像这样:

$ python -m pdb capitals.py cities2.csv
> /Users/williamlubanovic/book/capitals.py(1)<module>()
-> def process_cities(filename):
(Pdb)

这将启动程序并将您放在第一行。如果您输入 c继续),程序将运行直到正常结束或出现错误:

(Pdb) c
Buenos Aires,Argentina
La Paz,Bolivia
Brazilia,Brazil
Santiago,Chile
Bogotá,Colombia
The program finished and will be restarted
> /Users/williamlubanovic/book/capitals.py(1)<module>()
-> def process_cities(filename):

它完成了正常的运行,就像我们之前在调试器之外运行时一样。让我们再试一次,使用一些命令来缩小问题所在的范围。它似乎是一个逻辑错误,而不是语法问题或异常(这会打印错误消息)。

输入**sstep)逐行步进 Python 代码。这将逐行执行所有 Python 代码:您的代码、标准库的代码以及您可能使用的任何其他模块。当您使用s时,还会进入函数并在其中逐步执行。输入n**(next)逐行步进,但不会进入函数内部;当您到达函数时,单个n将导致整个函数执行并将您带到程序的下一行。因此,当您不确定问题所在时,请使用s;当您确信特定函数不是问题的原因时,尤其是如果它是一个长函数,请使用n。通常情况下,您会逐步执行自己的代码并跳过库代码,因为这些代码可能已经经过充分测试。让我们使用s从程序的开始步进到函数process_cities()

(Pdb) s
> /Users/williamlubanovic/book/capitals.py(12)<module>()
 -> if __name__ == '__main__':</pre>
(Pdb) s
 > /Users/williamlubanovic/book/capitals.py(13)<module>()
 -> import sys
(Pdb) s
 > /Users/williamlubanovic/book/capitals.py(14)<module>()
 -> process_cities(sys.argv[1])
(Pdb) s
 --Call--
 > /Users/williamlubanovic/book/capitals.py(1)process_cities()
 -> def process_cities(filename):
(Pdb) s
 > /Users/williamlubanovic/book/capitals.py(2)process_cities()
 -> with open(filename, 'rt') as file:

输入**l**(list)以查看程序的下几行:

(Pdb) l
  1      def process_cities(filename):
  2  ->     with open(filename, 'rt') as file:
  3             for line in file:
  4                 line = line.strip()
  5                 if 'quit' in line.lower():
  6                     return
  7                 country, city = line.split(',')
  8                 city = city.strip()
  9                 country = country.strip()
 10                 print(city.title(), country.title(), sep=',')
 11
 (Pdb)

箭头(->)表示当前行。

我们可以继续使用sn,希望找到一些东西,但让我们使用调试器的一个主要功能:断点。断点将在您指定的行停止执行。在我们的情况下,我们想知道为什么在process_cities()在读取所有输入行之前退出。第 3 行(for line in file:)将读取输入文件中的每一行,因此看起来很无辜。我们可以在函数返回所有数据之前的唯一其他地方是在第 6 行(return)。让我们在第 6 行设置一个断点:

(Pdb) b 6
Breakpoint 1 at /Users/williamlubanovic/book/capitals.py:6

接下来,让我们继续执行程序,直到它命中断点或正常读取所有输入行并完成:

(Pdb) c
 Buenos Aires,Argentina
 La Paz,Bolivia
 Brasilia,Brazil
 Santiago,Chile
 Bogotá,Colombia
 > /Users/williamlubanovic/book/capitals.py(6)process_cities()
 -> return

啊哈!它在我们的第 6 行断点处停下来了。这表明程序在读取哥伦比亚后的国家后想要早些返回。让我们打印line的值来查看我们刚刚读到的内容:

(Pdb) p line
'ecuador,quito'

有什么特别之处——哦,算了。

真的吗?我们经理从未预料到字符串quit会出现在正常数据中,因此将其用作标记(结束指示)值的想法真是愚蠢至极。你直接去那里告诉他这一点,而我在这里等着。

如果此时您仍然有工作,您可以使用普通的b命令查看所有断点:

(Pdb) b
Num Type         Disp Enb   Where
1   breakpoint   keep yes   at /Users/williamlubanovic/book/capitals.py:6
    breakpoint already hit 1 time

输入l将显示您的代码行、当前行(->)和任何断点(B)。普通的l将从您上次调用l的末尾开始列出,因此包括可选的起始行(这里,让我们从第1行开始):

(Pdb) l 1
  1      def process_cities(filename):
  2         with open(filename, 'rt') as file:
  3             for line in file:
  4                 line = line.strip()
  5                 if 'quit' in line.lower():
  6 B->                 return
  7                 country, city = line.split(',')
  8                 city = city.strip()
  9                 country = country.strip()
 10                 print(city.title(), country.title(), sep=',')
 11

好了,如示例 19-19 所示,让我们修复quit测试以仅匹配完整行,而不是其他字符中间。

示例 19-19. capitals2.py
def process_cities(filename):
    with open(filename, 'rt') as file:
        for line in file:
            line = line.strip()
            if 'quit' == line.lower():
                return
            country, city = line.split(',')
            city = city.strip()
            country = country.strip()
            print(city.title(), country.title(), sep=',')

if __name__ == '__main__':
    import sys
    process_cities(sys.argv[1])

再来,带点感情:

$ python capitals2.py cities2.csv
Buenos Aires,Argentina
La Paz,Bolivia
Brasilia,Brazil
Santiago,Chile
Bogotá,Colombia
Quito,Ecuador
Stanley,Falkland Islands
Cayenne,French Guiana
Georgetown,Guyana
Asunción,Paraguay
Lima,Peru
Paramaribo,Suriname
Montevideo,Uruguay
Caracas,Venezuela

这只是调试器的简要概述,足以展示它的功能及您在大部分时间内会使用的命令。

记住:多测试,少调试。

使用breakpoint()

在 Python 3.7 中,有一个名为 breakpoint() 的新内置函数。如果将其添加到您的代码中,调试器将自动启动并在每个位置暂停。如果没有这个函数,您将需要启动一个像 pdb 这样的调试器,并像您之前看到的那样手动设置断点。

默认调试器是您刚刚见过的(pdb),但可以通过设置环境变量 PYTHONBREAKPOINT 来更改。例如,您可以指定使用基于 Web 的远程调试器 web-pdb

$ export PYTHONBREAKPOINT='web_pdb.set_trace'

官方文档有点枯燥,但这里和那里都有很好的概述 herethere

记录错误消息

在某些时候,您可能需要从使用 print() 语句转向日志消息。日志通常是累积消息的系统文件,通常会插入有用的信息,例如时间戳或运行程序的用户名称。通常日志会按日进行“轮换”(重命名)并进行压缩;通过这样做,它们不会填满您的磁盘并引发问题。当程序出现问题时,您可以查看适当的日志文件以了解发生了什么。异常的内容在日志中特别有用,因为它们显示了程序实际崩溃的行和原因。

标准的 Python 库模块是 logging。我发现大多数对它的描述有点令人困惑。过了一会儿就会清楚些,但一开始确实看起来过于复杂。logging 模块包括这些概念:

  • 您要保存到日志的消息

  • 排名优先级级别及其匹配功能:debug()info()warn()error()critical()

  • 一个或多个作为与模块的主要连接的记录器对象

  • 将消息定向到您的终端、文件、数据库或其他位置的处理程序

  • 生成输出的格式化程序

  • 根据输入进行决策的过滤器

对于最简单的日志示例,只需导入模块并使用其中的一些函数:

>>> import logging
>>> logging.debug("Looks like rain")
>>> logging.info("And hail")
>>> logging.warn("Did I hear thunder?")
WARNING:root:Did I hear thunder?
>>> logging.error("Was that lightning?")
ERROR:root:Was that lightning?
>>> logging.critical("Stop fencing and get inside!")
CRITICAL:root:Stop fencing and get inside!

您是否注意到 debug()info() 没有执行任何操作,而其他两个在每条消息之前打印了LEVEL:root?到目前为止,它就像具有多重人格的 print() 语句,其中一些是敌对的。

但它很有用。您可以在日志文件中扫描特定级别的特定消息,比较时间戳以查看服务器崩溃前发生了什么等等。

大量查阅文档解答了第一个谜团(我们在一页或两页内到达第二个谜团):默认优先级级别WARNING,并且在我们调用第一个函数(logging.debug())时已经锁定。我们可以使用 basicConfig() 设置默认级别。DEBUG 是最低级别,因此启用它将使所有更高级别的消息都可以流经:

>>> import logging
>>> logging.basicConfig(level=logging.DEBUG)
>>> logging.debug("It's raining again")
DEBUG:root:It's raining again
>>> logging.info("With hail the size of hailstones")
INFO:root:With hail the size of hailstones

我们在没有实际创建 logger 对象的情况下使用了默认的 logging 函数。每个日志记录器都有一个名称。让我们创建一个名为 bunyan 的日志记录器:

>>> import logging
>>> logging.basicConfig(level='DEBUG')
>>> logger = logging.getLogger('bunyan')
>>> logger.debug('Timber!')
DEBUG:bunyan:Timber!

如果日志记录器名称包含任何点字符,它们会分隔成一个层次结构的日志记录器,每个可能具有不同的属性。这意味着命名为 quark 的日志记录器高于命名为 quark.charmed 的日志记录器。特殊的 root logger 处于顶层,称为 ''

到目前为止,我们只是打印了一些消息,这与 print() 没有太大的改进。我们使用 handlers 将消息定向到不同的位置。最常见的是 log file,下面是如何做的:

>>> import logging
>>> logging.basicConfig(level='DEBUG', filename='blue_ox.log')
>>> logger = logging.getLogger('bunyan')
>>> logger.debug("Where's my axe?")
>>> logger.warn("I need my axe")
>>>

啊哈!现在屏幕上没有这些行了;它们都在名为 blue_ox.log 的文件中:

DEBUG:bunyan:Where's my axe?
WARNING:bunyan:I need my axe

使用 filename 参数调用 basicConfig() 为您创建了一个 FileHandler 并使其对您的日志记录器可用。logging 模块至少包含 15 个处理程序,可将消息发送到诸如电子邮件和 Web 服务器以及屏幕和文件等位置。

最后,您可以控制所记录的消息的 format。在我们的第一个例子中,默认值给了我们类似于这样的东西:

WARNING:root:Message...

如果您向 basicConfig() 提供了一个 format 字符串,您可以更改为自己喜欢的格式:

>>> import logging
>>> fmt = '%(asctime)s %(levelname)s %(lineno)s %(message)s'
>>> logging.basicConfig(level='DEBUG', format=fmt)
>>> logger = logging.getLogger('bunyan')
>>> logger.error("Where's my other plaid shirt?")
2014-04-08 23:13:59,899 ERROR 1 Where's my other plaid shirt?

我们让日志记录器再次将输出发送到屏幕,但是更改了格式。

logging 模块在 fmt 格式字符串中识别多个变量名。我们使用了 asctime(日期和时间作为 ISO 8601 字符串)、levelnamelineno(行号)和消息本身。还有其他内置的变量,并且您可以提供自己的变量。

logging 的内容远远不止这个小概述所能提供的。您可以同时记录到多个位置,具有不同的优先级和格式。该包具有很大的灵活性,但有时会牺牲简单性。

优化

Python 通常足够快——直到它不快。在许多情况下,您可以通过使用更好的算法或数据结构来提高速度。关键在于知道在哪里做这些优化。即使是经验丰富的程序员也会出错得惊人。您需要像细心的被子裁缝一样,在裁剪之前进行测量。这也引出了 timers

测量时间

time() 函数位于 time 模块中,返回当前的时间戳作为一个浮点数秒数。要快速计时某事物,可以获取当前时间,执行某些操作,获取新的时间,然后用新时间减去原始时间。我们将按照 Example 19-20 中的方式编写它,并命名为(当然是)time1.py

Example 19-20. time1.py
from time import time

t1 = time()
num = 5
num *= 2
print(time() - t1)

在这个例子中,我们测量将值 5 赋给名称 num 并将其乘以 2 所需的时间。这并不是一个现实的基准测试,只是展示如何测量任意 Python 代码的示例。试着运行几次,看看它的变化:

$ python time1.py
2.1457672119140625e-06
$ python time1.py
2.1457672119140625e-06
$ python time1.py
2.1457672119140625e-06
$ python time1.py
1.9073486328125e-06
$ python time1.py
3.0994415283203125e-06

那大约是两三百万分之一秒。让我们尝试一些更慢的东西,比如sleep()。² 如果我们睡一秒,我们的计时器应该需要超过一秒钟的微小时间。示例 19-21 展示了代码;将其保存为time2.py

示例 19-21. time2.py
from time import time, sleep

t1 = time()
sleep(1.0)
print(time() - t1)

让我们确定我们的结果,所以运行几次:

$ python time2.py
1.000797986984253
$ python time2.py
1.0010130405426025
$ python time2.py
1.0010390281677246

如预期的那样,它大约需要一秒钟才能运行。如果没有,要么我们的计时器,要么sleep()应该感到尴尬。

有一种更方便的方法来测量像这样的代码片段:使用标准模块timeit。它有一个名为(你猜对了)timeit() 的函数,它将对你的测试代码进行count次运行并打印一些结果。语法是:timeit.timeit(*`code`*, number=*`count`*)

在本节的示例中,code需要放在引号中,这样在按下 Return 键后它不会被执行,但是会在timeit()内执行。(在下一节中,您将看到如何通过将其名称传递给timeit()来计时一个函数。)让我们只运行一次我们之前的示例并计时它。将此文件命名为timeit1.py(示例 19-22)。

示例 19-22. timeit1.py
from timeit import timeit
print(timeit('num = 5; num *= 2', number=1))

运行几次:

$ python timeit1.py
2.5600020308047533e-06
$ python timeit1.py
1.9020008039660752e-06
$ python timeit1.py
1.7380007193423808e-06

再次,这两行代码大约运行了两百万分之一秒。我们可以使用timeit模块的repeat()函数的repeat参数来运行更多的集合。将其保存为timeit2.py(示例 19-23)。

示例 19-23. timeit2.py
from timeit import repeat
print(repeat('num = 5; num *= 2', number=1, repeat=3))

尝试运行它看看会发生什么:

$ python timeit2.py
[1.691998477326706e-06, 4.070025170221925e-07, 2.4700057110749185e-07]

第一次运行花费了两百万分之一秒,第二次和第三次运行更快。为什么?原因可能有很多。首先,我们正在测试一小段代码,它的速度可能取决于计算机在这些瞬间正在做什么,Python 系统如何优化计算以及许多其他因素。

使用timeit()意味着将要测量的代码包装为字符串。如果您有多行代码怎么办?您可以传递一个三引号的多行字符串,但这可能很难阅读。

让我们定义一个懒惰的snooze()函数,偶尔打个盹,就像我们所有人偶尔做的那样。

首先,我们可以包装snooze()函数本身。我们需要包括参数globals=globals()(这有助于 Python 找到snooze)和number=1(仅运行一次;默认为 1000000,我们没有那么多时间):

>>> import time
>>> from timeit import timeit
>>>
>>> def snooze():
...     time.sleep(1)
...
>>> seconds = timeit('snooze()', globals=globals(), number=1)
>>> print("%.4f" % seconds)
1.0035

或者,我们可以使用一个装饰器:

>>> import time
>>>
>>> def snooze():
...     time.sleep(1)
...
>>> def time_decorator(func):
...     def inner(*args, **kwargs):
...         t1 = time.time()
...         result = func(*args, **kwargs)
...         t2 = time.time()
...         print(f"{(t2-t1):.4f}")
...         return result
...     return inner
...
>>> @time_decorator
... def naptime():
...     snooze()
...
>>> naptime()
1.0015

另一种方法是使用上下文管理器:

>>> import time
>>>
>>> def snooze():
...     time.sleep(1)
...
>>> class TimeContextManager:
...     def __enter__(self):
...         self.t1 = time.time()
...         return self
...     def __exit__(self, type, value, traceback):
...         t2 = time.time()
...         print(f"{(t2-self.t1):.4f}")
...
>>>
>>> with TimeContextManager():
...     snooze()
...
1.0019

__exit()__ 方法需要三个我们在这里不使用的额外参数;我们可以使用 *args 来代替它们。

好的,我们已经看到了许多计时的方法。现在,让我们计时一些代码,以比较不同算法(程序逻辑)和数据结构(存储机制)的效率。

算法和数据结构

Python 之禅提出做一件事应该有一种——最好只有一种——显而易见的方法。不幸的是,有时候这并不明显,你需要比较不同的选择。例如,使用for循环还是列表推导来构建列表更好?而更好的意思是什么?是更快,更容易理解,使用更少的内存,还是更符合 Python 风格?

在下一个练习中,我们以不同的方式构建列表,比较速度、可读性和 Python 风格。这是time_lists.py的例子(示例 19-24)。

示例 19-24. time_lists.py
from timeit import timeit

def make_list_1():
    result = []
    for value in range(1000):
        result.append(value)
    return result

def make_list_2():
    result = [value for value in range(1000)]
    return result

print('make_list_1 takes', timeit(make_list_1, number=1000), 'seconds')
print('make_list_2 takes', timeit(make_list_2, number=1000), 'seconds')

在每个函数中,我们向列表添加 1,000 个项目,并调用每个函数 1,000 次。请注意,在这个测试中,我们将timeit()的第一个参数作为函数名而不是作为字符串代码来调用。让我们运行它:

$ python time_lists.py
make_list_1 takes 0.14117428699682932 seconds
make_list_2 takes 0.06174145900149597 seconds

列表推导至少比使用append()添加项到列表快两倍。总体上,列表推导比手动构建更快。

利用这些想法使你的代码更快。

Cython、NumPy 和 C 扩展

如果你尽了最大努力仍然无法获得想要的性能,你还有更多的选择。

Cython是 Python 和 C 的混合体,旨在通过一些性能注释将 Python 代码翻译成编译的 C 代码。这些注释相对较少,比如声明一些变量的类型、函数参数或函数返回类型。对于科学计算中的数值计算循环,添加这些提示将使它们快得多——高达一千倍。详见Cython wiki获取文档和示例。

你可以在第二十二章中深入了解 NumPy。它是一个用 C 语言编写以提高速度的 Python 数学库。

Python 及其标准库的许多部分为了速度而用 C 编写,并包装在 Python 中供方便使用。这些挂钩可以用于你的应用程序。如果你了解 C 和 Python,并且真的想让你的代码快速运行,编写 C 扩展虽然更难,但改进可能是值得的。

PyPy

当 Java 大约 20 年前首次出现时,它慢得像一只关节炎的雪纳瑞。然而,当它开始给 Sun 等公司带来真正的利润时,他们投入了数百万美元来优化 Java 解释器和底层的 Java 虚拟机(JVM),借鉴了 Smalltalk 和 LISP 等早期语言的技术。微软同样致力于优化其竞争对手 C#语言和.NET 虚拟机。

没有人拥有 Python,所以没有人努力使其更快。你可能正在使用标准的 Python 实现。它是用 C 语言编写的,通常称为 CPython(不同于 Cython)。

像 PHP、Perl 甚至 Java 一样,Python 并不会被编译成机器语言,而是被翻译成一个中间语言(如字节码或 p-码),然后在虚拟机中解释。

PyPy是一个新的 Python 解释器,应用了一些加速 Java 的技巧。其基准测试显示,PyPy 在每个测试中都比 CPython 快——平均快六倍以上,在某些情况下甚至快 20 倍。它适用于 Python 2 和 3。你可以下载并使用它来替代 CPython。PyPy 在不断改进,甚至有可能取代 CPython。请查看网站上的最新发布说明,了解它是否适合您的需求。

Numba

您可以使用Numba来即时将您的 Python 代码编译成机器码并加速它。

使用通常的安装方法:

$ pip install numba

让我们首先看一个普通的 Python 函数,计算斜边:

>>> import math
>>> from timeit import timeit
>>> from numba import jit
>>>
>>> def hypot(a, b):
...     return math.sqrt(a**2 + b**2)
...
>>> timeit('hypot(5, 6)', globals=globals())
0.6349189280000189
>>> timeit('hypot(5, 6)', globals=globals())
0.6348589239999853

在首次调用后使用@jit装饰器加速调用。

>>> @jit
... def hypot_jit(a, b):
...     return math.sqrt(a**2 + b**2)
...
>>> timeit('hypot_jit(5, 6)', globals=globals())
0.5396156099999985
>>> timeit('hypot_jit(5, 6)', globals=globals())
0.1534771130000081

使用@jit(nopython=True)可以避免普通 Python 解释器的开销:

>>> @jit(nopython=True)
... def hypot_jit_nopy(a, b):
...     return math.sqrt(a**2 + b**2)
...
>>> timeit('hypot_jit_nopy(5, 6)', globals=globals())
0.18343535700000757
>>> timeit('hypot_jit_nopy(5, 6)', globals=globals())
0.15387067300002855

Numba 在 NumPy 和其他数学需求严格的包中特别有用。

源代码管理

当您处理一小组程序时,通常可以跟踪您的更改,直到您犯了一个愚蠢的错误并且破坏了几天的工作。源代码管理系统有助于保护您的代码免受像您这样的危险力量的侵害。如果您与一组开发人员合作,源代码管理就成为必需品。在 Python 生态中,最流行的开源世界中,Mercurial 和 Git 是最受欢迎的示例。它们都是分布式版本控制系统的例子,可以生成代码仓库的多个副本。早期的系统如 Subversion 在单一服务器上运行。

Mercurial

Mercurial是用 Python 编写的。它相当容易学习,有一些子命令可以从 Mercurial 仓库下载代码,添加文件,提交更改,以及合并来自不同来源的更改。bitbucket其他网站提供免费或商业托管服务。

Git

Git最初是为 Linux 内核开发编写的,但现在在开源社区中普遍存在。它类似于 Mercurial,尽管有些人觉得它稍微难以掌握。GitHub是最大的 git 主机,拥有超过一百万个仓库,但也有许多其他主机

本书中的独立程序示例可以在GitHub的公共 Git 仓库中找到。如果您的计算机上有git程序,可以使用以下命令下载这些程序:

$ git clone https://github.com/madscheme/introducing-python

您也可以从 GitHub 页面下载代码:

  • 单击“在桌面上克隆”以打开安装了的git的计算机版本。

  • 单击“下载 ZIP”获取程序的压缩归档文件。

如果你没有git但想尝试一下,可以阅读安装指南。我在这里讨论的是命令行版本,但你可能对 GitHub 等网站感兴趣,它们提供额外的服务,在某些情况下可能更容易使用;git有许多功能,但并不总是直观的。

让我们试驾一下git。我们不会走得很远,但这次旅行将展示一些命令及其输出。

创建一个新的目录并切换到该目录:

$ mkdir newdir
$ cd newdir

在当前目录newdir中创建一个本地的 Git 仓库:

$ git init
Initialized empty Git repository in /Users/williamlubanovic/newdir/.git/

newdir中创建一个名为test.py的 Python 文件,如示例 19-25 所示,其内容如下。

示例 19-25. test.py
print('Oops')

将文件添加到 Git 仓库中:

$ git add test.py

你觉得怎么样,Git 先生?

$ git status
On branch master

Initial commit

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

    new file:   test.py

这意味着test.py是本地仓库的一部分,但其更改尚未提交。让我们来提交它:

$ git commit -m "simple print program"
 [master (root-commit) 52d60d7] my first commit
  1 file changed, 1 insertion(+)
  create mode 100644 test.py

那个-m "my first commit"是你的提交消息。如果你省略了这一点,git将弹出一个编辑器,引导你以这种方式输入消息。这成为该文件的git更改历史的一部分。

让我们看看我们当前的状态:

$ git status
 On branch master
 nothing to commit, working directory clean

好的,所有当前的更改已经提交。这意味着我们可以更改东西而不必担心丢失原始版本。现在调整一下test.py—将Oops改为Ops!并保存文件(示例 19-26)。

示例 19-26. test.py,修订版
print('Ops!')

让我们来看看现在git的想法:

$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   test.py

no changes added to commit (use "git add" and/or "git commit -a")

使用git diff查看自上次提交以来更改了哪些行:

$ git diff
diff --git a/test.py b/test.py
index 76b8c39..62782b2 100644
--- a/test.py
+++ b/test.py
@@ -1 +1 @@
-print('Oops')
+print('Ops!')

如果现在尝试提交这个更改,git会抱怨:

$ git commit -m "change the print string"
On branch master
Changes not staged for commit:
    modified:   test.py

no changes added to commit

那个staged for commit短语意味着你需要add这个文件,大致翻译过来就是嘿,git,看这里

$ git add test.py

你也可以键入git add .来添加当前目录中所有已更改的文件;当你实际上已经编辑了多个文件并希望确保检查所有更改时,这非常方便。现在我们可以提交更改:

$ git commit -m "my first change"
 [master e1e11ec] my first change
  1 file changed, 1 insertion(&plus;), 1 deletion(-)

如果你想要查看所有对test.py做过的可怕的事情(最近的在前),使用git log命令:

$ git log test.py
 commit e1e11ecf802ae1a78debe6193c552dcd15ca160a
 Author: William Lubanovic <bill@madscheme.com>
 Date:   Tue May 13 23:34:59 2014 -0500

     change the print string

 commit 52d60d76594a62299f6fd561b2446c8b1227cfe1
 Author: William Lubanovic <bill@madscheme.com>
 Date:   Tue May 13 23:26:14 2014 -0500

     simple print program

分发你的程序

你知道你的 Python 文件可以安装在文件和目录中,你知道可以使用python解释器运行 Python 程序文件。

较少人知道 Python 解释器还可以执行打包在 ZIP 文件中的 Python 代码。甚至更少人知道特殊的 ZIP 文件(称为pex 文件)也可以使用。

克隆本书

你可以获取本书中所有程序的副本。访问Git 仓库并按照说明将其复制到本地计算机。如果你有git,运行命令git clone https://github.com/madscheme/introducing-python在你的计算机上创建一个 Git 仓库。你也可以以 ZIP 格式下载这些文件。

如何进一步学习

这是一个简介。它几乎肯定对一些你不关心的事情说得太多,对一些你关心的事情说得不够。让我推荐一些我发现有用的 Python 资源。

书籍

我发现以下列表中的书籍特别有用。这些书籍从入门到高级,混合了 Python 2 和 3:

  • Barry, Paul. Head First Python (2nd Edition) O’Reilly, 2016.

  • Beazley, David M. Python Essential Reference (5th Edition). Addison-Wesley, 2019.

  • Beazley, David M. and Brian K. Jones. Python Cookbook (3rd Edition). O’Reilly, 2013.

  • Gorelick, Micha and Ian Ozsvald. High Performance Python. O’Reilly, 2014.

  • Maxwell, Aaron. Powerful Python. Powerful Python Press, 2017.

  • McKinney, Wes. Python for Data Analysis: Data Wrangling with Pandas, NumPy, and IPython. O’Reilly, 2012.

  • Ramalho, Luciano. Fluent Python. O’Reilly, 2015.

  • Reitz, Kenneth and Tanya Schlusser. The Hitchhiker’s Guide to Python. O’Reilly, 2016.

  • Slatkin, Brett. Effective Python. Addison-Wesley, 2015.

  • Summerfield, Mark. Python in Practice: Create Better Programs Using Concurrency, Libraries, and Patterns. Addison-Wesley, 2013.

当然,还有更多

网站

这里有一些你可以找到有用教程的网站:

如果你对跟上 Python 世界发展感兴趣,可以查看这些新闻网站:

最后,这里是一些寻找和下载包的好网站:

Groups

计算机社区有各种各样的个性:热情洋溢、好争辩、沉闷、时髦、正式等等。Python 社区友好且文明。你可以根据地理位置找到基于 Python 的小组—meetups 和世界各地的本地用户组LocalUserGroups。其他群体是分布式的,基于共同的兴趣。例如,PyLadies 是一个支持对 Python 和开源感兴趣的女性的网络。

Conferences

在许多会议世界各地的研讨会中,最大的年度活动分别在北美欧洲举行。

获取 Python 工作

有用的搜索网站包括:

对于大多数这些网站,请在第一个框中键入python,在另一个框中输入您的位置。像这样的好地方包括像这个链接一样的 Craigslist,用于西雅图。只需将seattle部分更改为sfbaybostonnyc或其他 Craigslist 网站前缀以搜索这些地区。对于远程(远程工作或“在家工作”)Python 工作,特殊网站包括:

即将到来

但是等等,还有更多!接下来的三章将介绍 Python 在艺术、商业和科学领域的应用。你会发现至少一个你想要探索的包。网络上充满了闪亮的物体。只有你能分辨出哪些是假的珠宝,哪些是银弹。即使你目前没有被狼人困扰,你可能也希望口袋里有些银弹。以防万一。

最后,我们有了那些让人烦恼的章节末尾练习的答案,Python 及其伙伴安装的详细信息,以及一些常常需要查找的备忘单。你的大脑几乎肯定更精通,但如果需要,它们在那里。

要做的事情

(Python 爱好者今天没有家庭作业。)

¹ 你作为侦探:“我知道我在里面!如果我不举手投降,我就要自己出来!”

² 许多计算机书籍在定时示例中使用 Fibonacci 数字计算,但我宁愿睡觉也不愿意计算 Fibonacci 数字。

第二十章:Python 艺术

嗯,艺术就是艺术,不是吗?但另一方面,水还是水!东方是东方,西方是西方,如果你像炖苹果酱那样炖蔓越橘,它们的味道更像李子而不是大黄。

格劳乔·马尔克斯

本章及接下来的两章讨论了将 Python 应用于一些常见的人类活动:艺术、商业和科学。如果你对这些领域感兴趣,你可能会得到一些有用的想法或者试图尝试一些新的东西。

2-D 图形

所有的计算机语言在一定程度上都应用于计算机图形。本章的许多重型平台都是用 C 或 C++ 编写的,以追求速度,但增加了 Python 库以提高生产力。让我们从查看一些 2-D 成像库开始。

标准库

仅标准库中有几个与图像相关的模块:

imghdr

检测一些图像文件的文件类型。

colorsys

在各种系统之间转换颜色:RGB、YIQ、HSV 和 HLS。

如果您将 O’Reilly 标志下载到名为 oreilly.png 的本地文件中,您可以运行以下命令:

>>> import imghdr
>>> imghdr.what('oreilly.png')
'png'

另一个标准库是 turtle——“乌龟图形”,有时用于教授年轻人编程。您可以使用此命令运行演示:

$ python -m turtledemo

图 20-1 展示了其玫瑰花示例的屏幕截图。

inp2 2001

图 20-1. 来自 turtledemo 的图片

要在 Python 中进行严肃的图形处理,我们需要获取一些第三方包。让我们看看有什么可用的。

PIL 和 Pillow

多年来,尽管不在标准库中,Python 图像库(PIL)一直是 Python 最著名的 2-D 图像处理库。在像 pip 这样的安装程序出现之前,它的“友好分支”被称为 Pillow。Pillow 的图像代码与 PIL 兼容,并且其文档非常好,所以让我们在这里使用它。

安装很简单;只需输入以下命令:

$ pip install Pillow

如果您已安装操作系统包如 libjpeglibfreetypezlib,它们将被 Pillow 检测并使用。有关详细信息,请参阅安装页面

打开图像文件:

>>> from PIL import Image
>>> img = Image.open('oreilly.png')
>>> img.format
'PNG'
>>> img.size
(154, 141)
>>> img.mode
'RGB'

尽管该包称为 Pillow,但为了与旧版 PIL 兼容,您将其导入为 PIL

要使用 Image 对象的 show() 方法在屏幕上显示图像,您首先需要安装下一节中描述的 ImageMagick 包,然后尝试以下操作:

>>> img.show()

图 20-2 中显示的图像将在另一个窗口中打开。(此屏幕截图是在 Mac 上捕获的,show() 函数使用了预览应用程序。您的窗口外观可能会有所不同。)

inp2 2002

图 20-2. 使用 Python 图像库显示的图像

让我们在内存中裁剪图像,将结果保存为一个名为img2的新对象,并显示它。图像总是通过水平(x)值和垂直(y)值来测量,图像的一个角被称为原点,并且被任意地分配了一个 x 和 y 的值。在这个库中,原点(0, 0)位于图像的左上角,x 向右增加,y 向下增加。我们想要将左 x(55)、上 y(70)、右 x(85)和下 y(100)的值传递给crop()方法,因此传递一个按照这些顺序的元组:

>>> crop = (55, 70, 85, 100)
>>> img2 = img.crop(crop)
>>> img2.show()

结果显示在图 20-3 中。

inp2 2003

图 20-3. 裁剪后的图像

使用save方法保存图像文件。它接受一个文件名和一个可选的类型。如果文件名有后缀,库将使用该后缀确定类型。但是你也可以显式指定类型。要将我们裁剪后的图像保存为 GIF 文件,请执行以下操作:

>>> img2.save('cropped.gif', 'GIF')
>>> img3 = Image.open('cropped.gif')
>>> img3.format
'GIF'
>>> img3.size
(30, 30)

作为我们最后的例子,让我们“改进”一下我们的小吉祥物。首先下载我们原始生物的副本,显示在图 20-4 中。

inp2 2004

图 20-4. 可爱的原始生物

他有点粗糙的五点胡须,所以让我们获取一张图像来改善他的形象;参见图 20-5。

inp2 2005

图 20-5. 外星技术

让我们结合起来,加上一些alpha通道的魔法,使重叠部分半透明,如示例 20-1 所示。

示例 20-1. ch20_critter.py
from PIL import Image

critter = Image.open('ch20_critter.png')
stache = Image.open('ch20_stache.png')
stache.putalpha(100)
img = Image.new('RGBA', critter.size, (255, 255, 255, 0))
img.paste(critter, (0, 0))
img.paste(stache, (45, 90), mask=stache)
img.show()

图 20-6 展示了他的改头换面。

inp2 2006

图 20-6. 我们新的、时髦的吉祥物

ImageMagick

ImageMagick是一套用于转换、修改和显示 2D 位图图像的程序。已经存在超过 20 年。各种 Python 库已连接到 ImageMagick 的 C 库。最近支持 Python 3 的一个是wand。要安装它,请键入以下命令:

$ pip install Wand

你可以使用wand做很多和 Pillow 一样的事情:

>>> from wand.image import Image
>>> from wand.display import display
>>>
>>> img = Image(filename='oreilly.png')
>>> img.size
(154, 141)
>>> img.format
'PNG'

与 Pillow 类似,这将在屏幕上显示图像:

>>> display(img)

wand包括旋转、缩放、文本和线条绘制、格式转换等功能,这些功能也可以在 Pillow 中找到。两者都有良好的 API 和文档。

3D 图形

一些基本的 Python 包括以下内容:

  • VPython有一些示例,可以在你的浏览器中运行。

  • pi3d适用于树莓派、Windows、Linux 和 Android。

  • Open3D是一个功能齐全的 3D 库。

3D 动画

观看几乎任何现代电影的长片尾部字幕,你会看到大量从事特效和动画制作的人。大多数大型工作室——如华特迪士尼动画、工业光魔、维塔数码、梦工厂和皮克斯——都雇佣具备 Python 经验的人员。搜索“python 动画工作”以了解当前的职位空缺。

一些 Python 3D 包括:

Panda3D

它是开源的,免费使用,即使是商业应用也可以。您可以从 Panda3D 网站 下载版本。

VPython

配备了许多 示例

Blender

Blender 是一个免费的 3D 动画和游戏创作软件。当您 下载 并安装它时,它会捆绑自带其自己的 Python 3 版本。

Maya

这是一个商业的 3D 动画和图形系统。它还捆绑了一个版本的 Python,目前是 2.7。Chad Vernon 编写了一本免费下载的书籍,Python Scripting for Maya Artists。如果您在网上搜索 Python 和 Maya,您会找到许多其他资源,包括视频,既有免费也有商业的。

Houdini

Houdini 是商业软件,但您可以下载一个名为 Apprentice 的免费版本。与其他动画软件包一样,它配备了一个 Python 绑定

图形用户界面

名字中包含“graphic”,但图形用户界面(GUI)更注重用户界面:用于呈现数据的小部件、输入方式、菜单、按钮以及用来框架所有内容的窗口。

GUI 编程 wiki 页面和 FAQ 列出了许多由 Python 驱动的 GUI。让我们从唯一内置到标准库中的 GUI 开始:Tkinter。它很简单,但可以在所有平台上生成本地外观的窗口和小部件。

这里是一个微小的 Tkinter 程序,用于在窗口中显示我们喜爱的大眼睛吉祥物:

>>> import tkinter
>>> from PIL import Image, ImageTk
>>>
>>> main = tkinter.Tk()
>>> img = Image.open('oreilly.png')
>>> tkimg = ImageTk.PhotoImage(img)
>>> tkinter.Label(main, image=tkimg).pack()
>>> main.mainloop()

请注意,它使用了来自 PIL/Pillow 的一些模块。如图 Figure 20-7 中所示,您应该能看到 O’Reilly 的标志。

inp2 2007

Figure 20-7. 使用 Tkinter 显示的图像

要关闭窗口,点击其关闭按钮或离开您的 Python 解释器。

您可以在tkinter wiki上了解更多关于 Tkinter 的信息。现在来看一下标准库中没有的 GUI:

Qt

这是一个专业的 GUI 和应用程序工具包,大约 20 年前由挪威的 Trolltech 创立。它被用来构建应用程序,如 Google Earth、Maya 和 Skype。它还被用作 KDE 的基础,这是一个 Linux 桌面环境。有两个主要的 Python 库用于 Qt:PySide 是免费的(LGPL 许可),而 PyQt 可以通过 GPL 或商业许可获得。Qt 开发人员对这些 差异 有所了解。从 PyPIQt 下载 PySide 并阅读 教程。您可以在 免费在线下载 Qt

GTK+

GTK+ 是 Qt 的竞争对手,它也被用于创建许多应用程序,包括 GIMP 和 Linux 的 Gnome 桌面。Python 绑定是 PyGTK。要下载代码,请转到 PyGTK 站点,您还可以阅读 文档

WxPython

这是WxWidgets的 Python 绑定。这是另一个庞大的包,可以在在线免费下载。

Kivy

Kivy 是一个免费的现代化库,可跨平台(桌面:Windows、macOS、Linux;移动:Android、iOS)构建多媒体用户界面。它包括多点触控支持。您可以在 Kivy 网站 上为所有平台下载。Kivy 包括应用程序开发 教程

PySimpleGUI

使用一个库编写本机或基于 Web 的 GUI。PySimpleGUI 是本节提到的其他 GUI 的包装器,包括 Tk、Kivy 和 Qt。

网页

框架如 Qt 使用本机组件,但其他一些使用 Web。毕竟,Web 是一个通用的 GUI,它具有图形(SVG)、文本(HTML)甚至现在还有多媒体(在 HTML5 中)。您可以使用任何组合的前端(基于浏览器)和后端(Web 服务器)工具构建 Web 应用程序。瘦客户端让后端大部分工作。如果前端占主导地位,它就是丰富客户端;最后一个形容词听起来更加让人欣赏。双方通常通过 RESTful API、Ajax 和 JSON 进行通信。

绘图、图表和可视化

Python 已成为绘制、图表和数据可视化的主要解决方案。它在科学领域特别受欢迎,这在 第二十二章 中有所涉及。有用的概述和示例包括官方 Python wikiPython Graph Gallery

让我们看看最受欢迎的一些库。在下一章中,您将再次看到其中一些,但用于创建地图。

Matplotlib

Matplotlib 是一个二维绘图库,可以通过以下命令安装:

$ pip install matplotlib

gallery 中的示例展示了 Matplotlib 的广泛性。

让我们首先尝试相同的图像显示应用程序(结果显示在 图 20-8 中),只是看看代码和演示的效果如何:

import matplotlib.pyplot as plot
import matplotlib.image as image

img = image.imread('oreilly.png')
plot.imshow(img)
plot.show()

inp2 2008

图 20-8. 使用 Matplotlib 显示的图像

Matplotlib 的真正强大之处在于绘图,这毕竟是它的中间名。让我们生成两个包含 20 个整数的列表,一个从 1 增加到 20,另一个与第一个类似,但偶尔会有轻微的波动(示例 20-2)。

示例 20-2. ch20_matplotlib.py
import matplotlib.pyplot as plt
from random import randint

linear = list(range(1, 21))
wiggly = list(num + randint(-1, 1) for num in linear)

fig, plots = plt.subplots(nrows=1, ncols=3)

ticks = list(range(0, 21, 5))
for plot in plots:
    plot.set_xticks(ticks)
    plot.set_yticks(ticks)

plots[0].scatter(linear, wiggly)
plots[1].plot(linear, wiggly)
plots[2].plot(linear, wiggly, 'o-')

plt.show()

如果你运行这个程序,你会看到类似于图 20-9 所示的内容(不完全一样,因为randint()调用会使其有随机波动)。

inp2 2009

图 20-9. 基本的 Matplotlib 散点图和线图

此示例显示了散点图、线图以及带数据标记的线图。所有样式和颜色都使用了 Matplotlib 的默认设置,但可以进行非常广泛的定制。详细信息请参阅 Matplotlib 站点或类似的概述,如Python 绘图与 Matplotlib 指南

你可以在第 22 章中看到更多 Matplotlib;它与 NumPy 和其他科学应用有着紧密联系。

Seaborn

Seaborn是一个数据可视化库(参见图 20-10),构建在 Matplotlib 之上,并与 Pandas 连接紧密。通常的安装方法(pip install seaborn)可行。

inp2 2010

图 20-10. 基本的 Seaborn 散点图和线性回归

示例 20-3 中的代码基于 Seaborn 的一个示例;它访问了有关餐厅小费的测试数据,并绘制了小费与总账单金额的线性回归拟合线。

示例 20-3. ch20_seaborn.py
import seaborn as sns
import matplotlib.pyplot as plt

tips = sns.load_dataset("tips")
sns.regplot(x="total_bill", y="tip", data=tips);

plt.show()
注意

如果你用标准 Python 解释器运行上述代码,需要初始导入行(import matplotlib.pyplot as plt)和最后一行(plt.show()),如示例 20-3 所示,否则图形将无法显示。如果使用 Jupyter,则 Matplotlib 已内置,无需键入这些行。阅读 Python 映射工具的代码示例时请记住这一点。

和 Matplotlib 一样,Seaborn 在数据处理和显示方面有大量选项。

Bokeh

在旧的 Web 时代,开发人员会在服务器上生成图形,并提供一些 URL 供 Web 浏览器访问。最近,JavaScript 已经获得了性能提升,并且像 D3 这样的客户端图形生成工具也在增多。前面一页或两页,我提到了使用 Python 作为图形和 GUI 的前端后端架构的可能性。一种名为Bokeh的新工具结合了 Python(大数据集、易用性)和 JavaScript(交互性、较少的图形延迟)的优势。它强调快速可视化大数据集。

如果你已经安装了其前提条件(NumPy、Pandas 和 Redis),可以通过键入以下命令安装 Bokeh:

$ pip install bokeh

(你可以在第 22 章看到 NumPy 和 Pandas 的实际应用。)

或者,你可以从 Bokeh 网站一次性安装所有内容。虽然 Matplotlib 在服务器上运行,但 Bokeh 主要在浏览器中运行,并且可以利用客户端的最新进展。点击画廊中的任何图像,可以查看交互式显示及其 Python 代码。

游戏

Python 是一个如此出色的游戏开发平台,以至于人们已经写了很多关于它的书籍:

Python wiki 上有一般讨论,还有更多链接。

最著名的 Python 游戏平台可能是 pygame。你可以从 Pygame 网站 上为你的平台下载一个可执行安装程序,并逐行阅读一个“pummel the chimp”游戏的示例。

音频和音乐

我寻求衬线体

但那不适合克劳德·德彪西。

当之无愧的匿名者

关于声音、音乐和猫唱“Jingle Bells”,正如 Meatloaf 所说,三者中有二就不错了。

在印刷书籍中很难表现声音,因此这里有一些关于声音和音乐的最新链接到 Python 包,但 Google 上还有更多:

  • 标准库 音频 模块

  • 第三方 音频 工具

  • 几十种第三方 音乐 应用程序:图形和命令行播放器、转换器、记谱、分析、播放列表、MIDI 等等

最后,如何获取一些在线音乐资源呢?你已经在本书中看到了访问互联网档案的代码示例。以下是链接到一些其音频档案的链接:

即将推出

忙碌!这是 Python 在商业中的应用。

待办事项

20.1 安装 matplotlib。绘制这些 (x, y) 对的散点图:((0, 0), (3, 5), (6, 2), (9, 8), (14, 10))

20.2 绘制相同数据的折线图。

20.3 绘制一个带有标记的图(折线图)。

第二十一章:Py 在工作中

“商务!”幽灵再次搓着双手叫道。“人类是我的事业……”

查尔斯·狄更斯,《圣诞颂歌》

商人的制服是西装和领带。但是在他能着手做生意之前,他把夹克抛在椅子上,解开领带,卷起袖子,倒一些咖啡。与此同时,商务女士已经开始工作了。也许她是在喝拿铁。

在商业和政府中,我们使用了早期章节中的所有技术——数据库、网络、系统。Python 的高效率正在使其在企业初创公司中越来越受欢迎。

多年来,组织一直在与不兼容的文件格式、复杂的网络协议、语言锁定以及普遍缺乏准确文档进行斗争。通过使用这些工具,他们可以创建更快、更便宜、更灵活的应用程序:

  • Python 等动态语言

  • Web 作为通用的图形用户界面

  • RESTful API 作为语言无关的服务接口

  • 关系型数据库和 NoSQL 数据库

  • “大数据”和分析

  • 云用于部署和资本节约

Microsoft Office 套件

商业活动严重依赖于 Microsoft Office 应用程序和文件格式。虽然它们不是很出名,并且在某些情况下文档不完善,但是有一些 Python 库可以提供帮助。以下是一些处理 Microsoft Office 文档的库:

docx

此库可以创建、读取和写入 Microsoft Office Word 2007 .docx文件。

python-excel

这篇文章讨论了xlrdxlwtxlutils模块,通过 PDF 的教程。Excel 也可以读取和写入逗号分隔值(CSV)文件,你可以使用标准的csv模块来处理它们。

oletools

此库从 Office 格式中提取数据。

开放办公 是 Office 的开源替代品。它可以在 Linux、Unix、Windows 和 macOS 上运行,并且能够读取和写入 Office 文件格式。它还为自己安装了 Python 3 版本。你可以使用PyUNO库在Python中编程 OpenOffice。

OpenOffice 曾由 Sun Microsystems 拥有,后来 Oracle 收购了 Sun,一些人担心其未来的可用性。作为结果,LibreOffice被分拆出来。DocumentHacker描述了如何使用 Python UNO 库与 LibreOffice。

OpenOffice 和 LibreOffice 不得不对 Microsoft 文件格式进行逆向工程,这并不容易。Universal Office Converter模块依赖于 OpenOffice 或 LibreOffice 中的 UNO 库。它可以转换许多文件格式:文档、电子表格、图形和演示文稿。

如果你有一个神秘的文件,python-magic 可以通过分析特定的字节序列来猜测其格式。

python open document 库允许您在模板内提供 Python 代码,以创建动态文档。

虽然不是微软格式,但 Adobe 的 PDF 在商业中非常常见。ReportLab 提供了基于 Python 的 PDF 生成器的开源和商业版本。如果需要编辑 PDF,您可能会在 StackOverflow 找到一些帮助。

执行业务任务

你几乎可以找到任何 Python 模块。访问 PyPI 并在搜索框中输入内容。许多模块是各种服务的公共 API 的接口。你可能对一些与业务任务相关的示例感兴趣:

  • 通过 FedexUPS 进行发货。

  • 使用 stamps.com API 发送邮件。

  • 阅读关于Python 用于商业智能的讨论。

  • 如果在 Anoka,Aeropress 空中出售,是客户活动还是捣蛋鬼?Cubes 是一个在线分析处理(OLAP)Web 服务器和数据浏览器。

  • OpenERP 是一个大型的商业企业资源规划(ERP)系统,用 Python 和 JavaScript 编写,拥有成千上万的附加模块。

处理商业数据

企业对数据有特别的喜爱。可悲的是,他们中的许多人想出了让数据更难使用的反常方法。

电子表格是一个很好的发明,随着时间的推移,企业变得对它们上瘾。许多非程序员因为称为而不是程序而被诱导进编程。但宇宙正在扩展,数据正在试图跟上。早期版本的 Excel 限制为 65,536 行,甚至更新版本在百万左右时也会崩溃。当一个组织的数据超过单台计算机的限制时,就像人数超过一百人一样突然需要新的层次、中介和沟通。

过多的数据程序不是由单个桌面上的数据大小引起的;相反,它们是企业中涌入的数据总和的结果。关系数据库可以处理数百万行而不会爆炸,但一次只能写入或更新那么多。一个普通的文本或二进制文件可能会增长到几个 GB 的大小,但如果你需要一次性处理它们,就需要足够的内存。传统的桌面软件并不为这一切设计。像 Google 和 Amazon 这样的公司不得不发明解决方案来处理如此多的数据规模。Netflix 就是一个建立在 Amazon AWS 云上的例子,使用 Python 来粘合 RESTful API、安全性、部署和数据库。

提取、转换和加载

数据冰山的水下部分包括首先获取数据的所有工作。如果你熟悉企业术语,常见术语是提取、转换、加载,或ETL。同义词如数据整理数据处理给人一种驯服难以控制的野兽的印象,这可能是恰当的隐喻。这似乎现在应该是一个已解决的工程问题,但它仍然主要是一门艺术。我在第十二章中谈到了这个问题。我们在第二十二章中更广泛地讨论数据科学,因为这是大多数开发者花费大部分时间的地方。

如果你看过绿野仙踪,你可能记得(除了飞猴子以外)最后那部分——好女巫告诉多萝西她只需点击红宝石鞋子就能回到堪萨斯的一幕。即使在我年轻的时候我也想,“现在她告诉她了!”尽管事后我意识到如果她早点分享这个秘诀,电影会短得多。

但这不是一部电影;我们谈论的是商业世界,在这里,缩短任务时间是一件好事。所以,现在让我和你分享一些技巧。你在日常业务数据工作中需要的大多数工具都是你已经在这里读到的。这些包括高级数据结构,如字典和对象,成千上万的标准和第三方库,以及一个距离你只有一步之遥的专家社区。

如果你是一名为某个企业工作的计算机程序员,你的工作流程几乎总是包括以下步骤:

  1. 从奇怪的文件格式或数据库中提取数据

  2. “清理”数据,这涵盖了很多领域,所有这些领域都布满了尖锐的物体

  3. 转换日期、时间和字符集等内容

  4. 实际上对数据进行操作

  5. 将结果数据存储在文件或数据库中

  6. 再次回到步骤 1;重复上述步骤

这里有一个例子:你想将数据从电子表格移动到数据库。你可以将电子表格保存为 CSV 格式,并使用第十六章中的 Python 库。或者,你可以寻找一个直接读取二进制电子表格格式的模块。你的手指知道如何在谷歌中输入python excel并找到网站,比如Python 处理 Excel 文件。你可以使用pip安装其中一个包,并找到用于最后一部分任务的 Python 数据库驱动程序。我在同一章中提到了 SQLAlchemy 和直接的低级数据库驱动程序。现在你需要一些中间代码,这就是 Python 的数据结构和库可以节省你时间的地方。

让我们在这里尝试一个例子,然后我们再试试一个节省几个步骤的库。我们将读取一个 CSV 文件,根据另一个列中的唯一值聚合计数,并打印结果。如果我们在 SQL 中执行此操作,我们将使用 SELECT、JOIN 和 GROUP BY。

首先是文件zoo.csv,它有列来标识动物的类型、它咬过游客的次数、需要多少针线以及我们支付给游客的不让当地电视台报道的钱数:

animal,bites,stitches,hush
bear,1,35,300
marmoset,1,2,250
bear,2,42,500
elk,1,30,100
weasel,4,7,50
duck,2,0,10

我们想知道哪种动物花费最多,因此我们按动物类型汇总了总的口油钱数。(让我们把咬伤和缝针的工作留给一个实习生。)我们使用来自“CSV”的csv模块和来自“使用 Counter()计数项目”的Counter。将此代码保存为zoo_counts.py

import csv
from collections import Counter

counts = Counter()
with open('zoo.csv', 'rt') as fin:
    cin = csv.reader(fin)
    for num, row in enumerate(cin):
        if num > 0:
            counts[row[0]] += int(row[-1])
for animal, hush in counts.items():
    print("%10s %10s" % (animal, hush))

我们跳过了第一行,因为它只包含列名。counts是一个Counter对象,并负责将每种动物的总和初始化为零。我们还对输出进行了一些格式化,使其右对齐。让我们试一试:

$ python zoo_counts.py
      duck         10
       elk        100
      bear        800
    weasel         50
  marmoset        250

哈哈!是熊。他一直是我们的主要嫌疑人,现在我们有了数字。

接下来,让我们使用一个称为Bubbles的数据处理工具包来复制这个过程。您可以通过输入以下命令来安装它:

$ pip install bubbles

它需要 SQLAlchemy;如果没有,可以通过pip install sqlalchemy来安装。这是测试程序(称为bubbles1.py),改编自文档

import bubbles

p = bubbles.Pipeline()
p.source(bubbles.data_object('csv_source', 'zoo.csv', infer_fields=True))
p.aggregate('animal', 'hush')
p.pretty_print()

现在,关键时刻到了:

$ python bubbles1.py
2014-03-11 19:46:36,806 DEBUG calling aggregate(rows)
2014-03-11 19:46:36,807 INFO called aggregate(rows)
2014-03-11 19:46:36,807 DEBUG calling pretty_print(records)
+--------+--------+------------+
|animal  |hush_sum|record_count|
+--------+--------+------------+
|duck    |      10|           1|
|weasel  |      50|           1|
|bear    |     800|           2|
|elk     |     100|           1|
|marmoset|     250|           1|
+--------+--------+------------+
2014-03-11 19:46:36,807 INFO called pretty_print(records)

如果您阅读了文档,就可以避免这些调试打印行,并可能改变表格的格式。

看两个例子,我们看到bubbles示例使用了单个函数调用(aggregate)来替代我们手动读取和计数 CSV 格式。根据您的需求,数据工具包可以节省大量工作。

在一个更现实的例子中,我们的动物园文件可能有数千行(这是一个危险的地方),包括诸如bare、数字中的逗号等拼写错误。关于实际数据问题的 Python 代码的好例子,我还推荐以下内容:

数据清理工具可以节省大量时间,Python 有很多这样的工具。举个例子,PETL 可以进行行和列的提取和重命名。其相关工作页面列出了许多有用的模块和产品。第二十二章详细讨论了一些特别有用的数据工具:Pandas、NumPy 和 IPython。虽然它们目前在科学家中最为人知,但它们正在逐渐流行于金融和数据开发人员中。在 2012 年 Pydata 会议上,AppData讨论了这三个工具及其他 Python 工具如何每天处理 15TB 的数据。Python 能够处理非常大的现实世界数据负载。

您还可以回顾讨论的数据序列化和验证工具,如“数据序列化”。

数据验证

在清理数据时,您经常需要检查:

  • 数据类型,如整数、浮点数或字符串

  • 值的范围

  • 正确的值,如有效的电话号码或电子邮件地址

  • Duplicates

  • 缺失数据

在处理网络请求和响应时尤其常见。

特定数据类型的有用 Python 包括:

一些有用的通用工具包括:

Additional Sources of Information

有时,您需要来自其他地方的数据。一些商业和政府数据源包括:

data.gov

成千上万数据集和工具的入口。其 APIs 建立在 CKAN 上,这是一个 Python 数据管理系统。

使用 Python 的开放政府

查看 视频幻灯片

python-sunlight

用于访问 Sunlight APIs 的库。

froide

一个基于 Django 的平台,用于管理信息自由请求。

30 places to find open data on the web

一些方便的链接。

开源 Python 商业包

Odoo

广泛的企业资源规划(ERP)平台

Tryton

另一个广泛的业务平台

Oscar

Django 的电子商务框架

Grid Studio

基于 Python 的电子表格,可在本地或云中运行

Python 在金融中的应用

最近,金融行业对 Python 的兴趣日益增加。通过适应第二十二章的软件以及一些自己的软件,量化分析师 正在构建新一代金融工具:

Quantitative economics

一个经济建模工具,具有大量数学和 Python 代码

Python for finance

以 Yves Hilpisch(Wiley)所著的书籍 Derivatives Analytics with Python: Data Analytics, Models, Simulation, Calibration, and Hedging 为特色

Quantopian

一个互动网站,您可以在其中编写自己的 Python 代码,并针对历史股票数据运行它,以查看其表现如何

PyAlgoTrade

另一个可以用于股票回测的工具,但需要在您自己的计算机上运行

Quandl

搜索数百万金融数据集

Ultra-finance

实时股票收集库

Python for Finance(O’Reilly)

Yves Hilpisch 撰写的一本带有 Python 示例的金融建模书籍。

业务数据安全

安全对于企业来说是一个特别关注的问题。有整本书专门讨论这个话题,因此我们在这里只提到了一些与 Python 相关的技巧。

  • “Scapy”讨论了scapy,一个用于数据包取证的基于 Python 的语言。它已被用来解释一些主要的网络攻击。

  • Python 安全网站讨论了安全主题,详细介绍了一些 Python 模块和备忘单。

  • TJ O’Connor(Syngress)撰写的书籍Violent Python(副标题为黑客、法医分析员、渗透测试人员和安全工程师的烹饪书)是对 Python 和计算机安全的广泛审查。

地图

对许多企业来说,地图已经变得非常有价值。Python 非常擅长制作地图,因此我们将在这个领域多花一点时间。管理人员喜欢图形,如果你能为组织的网站快速制作一个漂亮的地图,那肯定是有好处的。

在 Web 的早期,我曾经访问过施乐的一个实验性地图制作网站。当像 Google Maps 这样的大型网站出现时,它们是一个启示(类似于“为什么我没想到那个并赚了几百万?”)。现在映射和基于位置的服务随处可见,特别是在移动设备中非常有用。

许多术语在这里重叠:映射、制图学、GIS(地理信息系统)、GPS(全球定位系统)、地理空间分析等等。在地理空间 Python的博客上有一个“800 磅大猩猩”系统的图像—GDAL/OGR、GEOS 和 PROJ.4(投影)—及其周边系统,这些系统许多都有 Python 接口。让我们从一些最简单的格式开始讨论一些内容。

格式

映射世界有许多格式:矢量(线)、栅格(图像)、元数据(文字)以及各种组合。

Esri,一个地理系统的先驱,20 多年前发明了shapefile格式。一个 shapefile 实际上包含了多个文件,至少包括以下文件:

.shp

“shape”(矢量)信息

.shx

形状索引

.dbf

一个属性数据库

让我们为下一个示例获取一个 shapefile——访问 Natural Earth 1:110m Cultural Vectors 页面。在“Admin 1 - States and Provinces”下,点击绿色的download states and provinces方框以下载一个 zip 文件。下载完成后,解压缩它到你的计算机上,你应该看到以下生成的文件:

ne_110m_admin_1_states_provinces_shp.README.html
ne_110m_admin_1_states_provinces_shp.sbn
ne_110m_admin_1_states_provinces_shp.VERSION.txt
ne_110m_admin_1_states_provinces_shp.sbx
ne_110m_admin_1_states_provinces_shp.dbf
ne_110m_admin_1_states_provinces_shp.shp
ne_110m_admin_1_states_provinces_shp.prj
ne_110m_admin_1_states_provinces_shp.shx

我们将在我们的示例中使用这些。

从 Shapefile 绘制地图

这一节是一个过于简化的演示,展示了如何读取和显示一个 shapefile。你会看到结果存在问题,你最好使用更高级的地图包,比如接下来介绍的那些。

你将需要这个库来读取一个 shapefile:

$ pip install pyshp

现在是程序map1.py,我从 Geospatial Python 博客文章修改而来:

def display_shapefile(name, iwidth=500, iheight=500):
    import shapefile
    from PIL import Image, ImageDraw
    r = shapefile.Reader(name)
    mleft, mbottom, mright, mtop = r.bbox
    # map units
    mwidth = mright - mleft
    mheight = mtop - mbottom
    # scale map units to image units
    hscale = iwidth/mwidth
    vscale = iheight/mheight
    img = Image.new("RGB", (iwidth, iheight), "white")
    draw = ImageDraw.Draw(img)
    for shape in r.shapes():
        pixels = [
            (int(iwidth - ((mright - x) * hscale)), int((mtop - y) * vscale))
            for x, y in shape.points]
        if shape.shapeType == shapefile.POLYGON:
            draw.polygon(pixels, outline='black')
        elif shape.shapeType == shapefile.POLYLINE:
            draw.line(pixels, fill='black')
    img.show()

if __name__ == '__main__':
    import sys
    display_shapefile(sys.argv[1], 700, 700)

这段代码读取 shapefile 并迭代其各个形状。我只检查两种形状类型:多边形,将最后一个点连接到第一个点,和折线,不会连接。我的逻辑基于原始帖子和对pyshp文档的快速查阅,所以我不太确定它将如何工作。有时,我们只需开始并在发现问题时解决它们。

那么,让我们运行它。参数是 shapefile 文件的基本名称,没有任何扩展名:

$ python map1.py ne_110m_admin_1_states_provinces_shp

你应该看到类似于图 21-1 的东西。

它画了一幅类似于美国的地图,但:

  • 看起来像一只猫拖着毛线穿过阿拉斯加和夏威夷;这是一个bug

  • 这个国家被挤压了;我需要一个投影

  • 这幅图不好看;我需要更好的样式控制。

inp2 2101

图 21-1. 初步地图

要解决第一点:我的逻辑中有问题,但我该怎么办?第十九章讨论了开发技巧,包括调试,但在这里我们可以考虑其他选择。我可以编写一些测试,并坚持直到解决这个问题,或者我可以尝试其他地图库。也许某种更高级别的东西会解决我的三个问题(偏离的线条,压缩的外观和原始的样式)。

就我所知,没有纯 Python 的基本地图包。幸运的是,有一些更高级的选择,所以让我们来看一看。

Geopandas

Geopandasmatplotlibpandas和其他 Python 库集成到地理空间数据平台中。

基础包可以使用熟悉的pip install geopandas安装,但它依赖于其他包,如果你没有安装它们,也需要用pip安装:

  • numpy

  • pandas(版本 0.23.4 或更高)

  • shapely(GEOS 接口)

  • fiona(GDAL 接口)

  • pyproj(PROJ 接口)

  • six

Geopandas 可以读取 shapefile(包括上一节的文件),并便利地包含来自自然地球的两种数据:国家/大陆轮廓和国家首都。示例 21-1 是一个简单的演示,同时使用了这两者。

示例 21-1. geopandas.py
import geopandas
import matplotlib.pyplot as plt

world_file = geopandas.datasets.get_path('naturalearth_lowres')
world = geopandas.read_file(world_file)
cities_file = geopandas.datasets.get_path('naturalearth_cities')
cities = geopandas.read_file(cities_file)
base = world.plot(color='orchid')
cities.plot(ax=base, color='black', markersize=2)
plt.show()

运行它,你应该看到显示在图 21-2 中的地图。

inp2 2102

图 21-2. Geopandas 地图

对我来说,geopandas 目前看起来是地理数据管理和显示的最佳组合。但还有许多值得考虑的竞争对手,我们将在下一节中讨论。

其他地图包

这里是一些链接,指向其他 Python 地图软件;许多不能完全使用pip安装,但有些可以使用conda(特别适用于科学软件的替代 Python 包安装器):

pyshp

一个纯 Python shapefile 库,前面提到过 “从 shapefile 绘制地图”。

kartograph

在服务器或客户端将 shapefile 渲染为 SVG 地图。

shapely

解答几何问题,比如“这个城镇中哪些建筑物在 50 年洪水轮廓线内?”

basemap

基于 matplotlib,绘制地图和数据叠加。不幸的是,它已被 Cartopy 废弃。

cartopy

成功的 Basemap,可以做一些 geopandas 做的事情。

folium

与 leaflet.js 一起使用,被 geopandas 使用。

plotly

另一个包含地图功能的绘图包。

dash

使用 Plotly、Flask 和 JavaScript 创建交互式可视化,包括地图。

fiona

封装了处理 shapefile 和其他矢量格式的 OGR 库。

Open Street Map

访问广阔的 OpenStreetMap 世界地图。

mapnik

一个具有 Python 绑定的 C++ 库,用于矢量(线)和栅格(图像)地图。

Vincent

转换为 Vega,一个 JavaScript 可视化工具;参见教程:使用 pandas 和 vincent 在 Python 中绘制数据地图

Python for ArcGIS

链接到 Esri 商业 ArcGIS 产品的 Python 资源。

使用 Python 处理地理空间数据

视频演示。

想用 Python 制作地图

使用 pandas, matplotlib, shapely 和其他 Python 模块创建历史牌匾位置的地图。

Python 地理空间开发(Packt 出版)

由 Eric Westra 编写的书籍,示例使用 mapnik 和其他工具。

学习 Python 地理空间分析(Packt 出版)

Joel Lawhead 的另一本书,回顾格式和库,具有地理空间算法。

geomancer

地理空间工程,例如从点到最近的爱尔兰酒吧的距离。

如果你对地图感兴趣,可以尝试下载并安装其中一个软件包,看看你能做什么。或者,你可以避免安装软件,尝试连接到远程 Web 服务 API;第十八章 展示了如何连接到 Web 服务器并解码 JSON 响应。

应用和数据

我一直在谈论绘制地图,但是你可以用地图数据做很多其他事情。地理编码 可以在地址和地理坐标之间转换。有许多地理编码 APIs(参见 ProgrammableWeb 的比较)和 Python 库:

如果你使用 Google 或其他来源注册 API 密钥,你可以访问其他服务,比如逐步旅行指南或本地搜索。

这里有几个地图数据的来源:

美国人口普查局地图数据

美国人口普查局地图文件概述

美国人口普查局的 TIGER 数据

大量的地理和人口统计地图数据

开放街道地图潜在数据源

全球资源

自然地球数据

三种比例尺下的矢量和栅格地图数据

我应该在这里提一下数据科学工具包。它包括免费的双向地理编码、坐标到政治边界和统计数据等功能。你也可以下载所有数据和软件作为虚拟机(VM),在自己的计算机上独立运行。

即将到来

我们参加了一场科学展,看到了所有的 Python 展品。

待办事项

21.1 安装geopandas并运行示例 21-1。尝试修改颜色和标记大小等内容。

第二十二章:Py Sci

在她统治期间,蒸汽的力量

在陆地和海上变得无与伦比,

现在所有都有强烈的依赖

在科学的新胜利中。

詹姆斯·麦金泰尔,《女王 1887 年的银禧颂歌》

近几年来,主要因为你将在本章看到的软件,Python 在科学家中变得非常流行。如果你是科学家或学生,你可能已经使用过类似 MATLAB 和 R 的工具,或者传统语言如 Java、C 或 C++。现在你将看到 Python 如何成为科学分析和出版的优秀平台。

标准库中的数学和统计

首先,让我们回到标准库,看看我们忽略了的一些特性和模块。

数学函数

Python 在标准math库中拥有丰富的数学函数。只需输入**import math**即可从你的程序中访问它们。

它还有一些常量,如pie

>>> import math
>>> math.pi
>>> 3.141592653589793
>>> math.e
2.718281828459045

大部分由函数组成,让我们看看最有用的几个。

fabs()返回其参数的绝对值:

>>> math.fabs(98.6)
98.6
>>> math.fabs(-271.1)
271.1

获取某个数字下方(floor())和上方(ceil())的整数:

>>> math.floor(98.6)
98
>>> math.floor(-271.1)
-272
>>> math.ceil(98.6)
99
>>> math.ceil(-271.1)
-271

使用factorial()来计算阶乘(数学中的n!):

>>> math.factorial(0)
1
>>> math.factorial(1)
1
>>> math.factorial(2)
2
>>> math.factorial(3)
6
>>> math.factorial(10)
3628800

使用log()在自然对数e的基础上获取参数的对数:

>>> math.log(1.0)
0.0
>>> math.log(math.e)
1.0

如果你想要不同基数的对数,可以提供第二个参数:

>>> math.log(8, 2)
3.0

函数pow()执行相反操作,将一个数提升为某个幂次方:

>>> math.pow(2, 3)
8.0

Python 还有内置的指数运算符**来完成相同的功能,但如果底数和指数都是整数,结果不会自动转换为浮点数:

>>> 2**3
8
>>> 2.0**3
8.0

使用sqrt()获取平方根:

>>> math.sqrt(100.0)
10.0

不要试图愚弄这个函数;它以前见过所有的东西:

>>> math.sqrt(-100.0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: math domain error

所有常见的三角函数都在这里,我会在这里列出它们的名字:sin()cos()tan()asin()acos()atan(),和atan2()。如果你记得毕达哥拉斯定理(或者能快速连续三次说出来而不喷出口水),数学库还有一个hypot()函数用于计算两边的斜边:

>>> x = 3.0
>>> y = 4.0
>>> math.hypot(x, y)
5.0

如果你不信任所有这些花哨的函数,你可以自己解决:

>>> math.sqrt(x*x + y*y)
5.0
>>> math.sqrt(x**2 + y**2)
5.0

一组最后的函数转换角坐标:

>>> math.radians(180.0)
3.141592653589793
>>> math.degrees(math.pi)
180.0

处理复数

复数在基本的 Python 语言中得到了完全支持,具有熟悉的实部虚部的表示法:

>>> # a real number
... 5
5
>>> # an imaginary number
... 8j
8j
>>> # an imaginary number
... 3 + 2j
(3+2j)

因为虚数i(Python 中的1j)定义为–1 的平方根,我们可以执行以下操作:

>>> 1j * 1j
(-1+0j)
>>> (7 + 1j) * 1j
(-1+7j)

一些复杂的数学函数在标准cmath模块中。

使用十进制计算准确的浮点数

计算机中的浮点数不像我们在学校学到的实数那样:

>>> x = 10.0 / 3.0
>>> x
3.3333333333333335

嘿,末尾的5是什么?应该一路都是3。这是因为计算机 CPU 寄存器中只有有限的位数,而不是 2 的整数次幂的数字无法被精确表示。

使用 Python 的decimal模块,您可以将数字表示为您所需的显著位数。这对涉及货币的计算特别重要。美元货币的最小单位是一分(美元的百分之一),因此如果我们将金额表示为美元和分,我们希望准确到分。如果我们尝试使用浮点值(如 19.99 和 0.06)表示美元和分,我们将在开始计算之前失去一些显著性。我们如何处理这个问题?很简单,我们使用decimal模块:

>>> from decimal import Decimal
>>> price = Decimal('19.99')
>>> tax = Decimal('0.06')
>>> total = price + (price * tax)
>>> total
Decimal('21.1894')

我们创建了价格和税费的字符串值以保留它们的意义。total的计算保留了所有显著的分数部分,但我们希望获得最接近的分数:

>>> penny = Decimal('0.01')
>>> total.quantize(penny)
Decimal('21.19')

可能会使用普通的浮点数和四舍五入得到相同的结果,但并非总是如此。您也可以将所有内容乘以 100,并在计算中使用整数分(cents),但最终这也会给您带来麻烦。

使用 fractions 执行有理算术

您可以通过标准 Python fractions模块将数字表示为分子除以分母。这里是一个简单的操作,将三分之一乘以三分之二:

>>> from fractions import Fraction
>>> Fraction(1,  3) * Fraction(2, 3)
Fraction(2, 9)

浮点数参数可能不精确,因此您可以在Fraction内使用Decimal

>>> Fraction(1.0/3.0)
Fraction(6004799503160661, 18014398509481984)
>>> Fraction(Decimal('1.0')/Decimal('3.0'))
Fraction(3333333333333333333333333333, 10000000000000000000000000000)

使用gcd函数获取两个数字的最大公约数:

>>> import fractions
>>> fractions.gcd(24, 16)
8

使用array处理打包的序列

Python 列表更像是链表而不是数组。如果您想要相同类型的一维序列,请使用array类型。它比列表使用更少的空间,并支持许多列表方法。使用array( typecode , initializer )来创建一个。typecode指定数据类型(如intfloat),可选的*initializer*包含初始值,可以指定为列表、字符串或可迭代对象。

我从未真正使用这个包进行实际工作。它是一个低级数据结构,适用于诸如图像数据之类的事物。如果您确实需要一个数组——尤其是带有多个维度——来进行数值计算,那么使用 NumPy 要好得多,稍后我们会讨论它。

使用statistics处理简单的统计数据

从 Python 3.4 开始,statistics是一个标准模块。它具有常见的函数:均值、中位数、众数、标准差、方差等等。输入参数是各种数值数据类型的序列(列表或元组)或迭代器:int、float、decimal 和 fraction。一个函数,mode,还接受字符串。许多更多的统计函数可以在诸如 SciPy 和 Pandas 等包中找到,稍后在本章中介绍。

矩阵乘法

从 Python 3.5 开始,你会看到 @ 字符有些不同寻常的用法。它仍然用于装饰器,但也有一个新的用法用于 矩阵乘法。如果你正在使用较旧版本的 Python,NumPy(即将介绍)是你的最佳选择。

科学 Python

本章的其余部分涵盖了用于科学和数学的第三方 Python 包。虽然你可以单独安装它们,但考虑作为科学 Python 发行版的一部分同时下载所有这些包会更好。以下是你的主要选择:

Anaconda

免费、全面、最新,支持 Python 2 和 3,并且不会破坏现有的系统 Python。

Enthought Canopy

提供免费和商业版本

Python(x,y)

仅适用于 Windows 版本

Pyzo

基于 Anaconda 的一些工具,以及其他一些工具

我建议安装 Anaconda。它很大,但本章的所有示例都包含在其中。本章的其余示例将假定你已安装了所需的包,无论是单独安装还是作为 Anaconda 的一部分。

NumPy

NumPy 是 Python 在科学家中流行的主要原因之一。你听说过动态语言如 Python 通常比编译语言如 C 或甚至其他解释语言如 Java 慢。NumPy 的出现是为了提供快速的多维数值数组,类似于科学语言如 FORTRAN。你既得到了 C 的速度,又保留了 Python 的开发者友好性。

如果你已经下载了其中一个科学 Python 发行版,那么你已经拥有了 NumPy。如果没有,请按照 NumPy 的 下载页面 上的说明操作。

要开始使用 NumPy,你应该了解一个核心数据结构,称为 ndarrayN-dimensional array 的缩写)或简称 array 的多维数组。与 Python 的列表和元组不同,每个元素都需要是相同类型的。NumPy 将数组的维数称为其 rank。一维数组类似于值的行,二维数组类似于行和列的表,三维数组类似于魔方。各维度的长度不必相同。

注意

NumPy 的 array 和标准的 Python array 不是一回事。本章的其余部分中,当我说 array 时,我指的是 NumPy 数组。

但是为什么你需要一个数组?

  • 科学数据通常包含大量的数据序列。

  • 在这些数据上进行科学计算通常使用矩阵数学、回归、模拟以及其他处理大量数据点的技术。

  • NumPy 处理数组比标准的 Python 列表或元组快得多。

有许多方法可以创建一个 NumPy 数组。

使用 array() 创建一个数组

你可以从普通列表或元组创建一个数组:

>>> b = np.array( [2, 4, 6, 8] )
>>> b
array([2, 4, 6, 8])

属性 ndim 返回数组的维数:

>>> b.ndim
1

数组中的值的总数由size给出:

>>> b.size
4

每个秩中的值的数量由shape返回:

>>> b.shape
(4,)

使用arange()创建一个数组

NumPy 的arange()方法类似于 Python 的标准range()。如果你用一个整数参数num调用arange(),它会返回一个从0num-1ndarray

>>> import numpy as np
>>> a = np.arange(10)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a.ndim
1
>>> a.shape
(10,)
>>> a.size
10

用两个值,它创建一个从第一个到最后一个的数组,减去一个:

>>> a = np.arange(7, 11)
>>> a
array([ 7,  8,  9, 10])

你也可以提供一个步长作为第三个参数来使用:

>>> a = np.arange(7, 11, 2)
>>> a
array([7, 9])

到目前为止,我们的例子都使用整数,但浮点数也完全可以:

>>> f = np.arange(2.0, 9.8, 0.3)
>>> f
array([ 2\. ,  2.3,  2.6,  2.9,  3.2,  3.5,  3.8,  4.1,  4.4,  4.7,  5\. ,
 5.3,  5.6,  5.9,  6.2,  6.5,  6.8,  7.1,  7.4,  7.7,  8\. ,  8.3,
 8.6,  8.9,  9.2,  9.5,  9.8])

还有一个小技巧:dtype参数告诉arange要产生什么类型的值:

>>> g = np.arange(10, 4, -1.5, dtype=np.float)
>>> g
array([ 10\. ,   8.5,   7\. ,   5.5])

使用 zeros()、ones() 或 random() 创建一个数组

zeros()方法返回一个所有值都为零的数组。你提供的参数是一个你想要的形状的元组。这是一个一维数组:

>>> a = np.zeros((3,))
>>> a
array([ 0.,  0.,  0.])
>>> a.ndim
1
>>> a.shape
(3,)
>>> a.size
3

这个是二阶的:

>>> b = np.zeros((2, 4))
>>> b
array([[ 0.,  0.,  0.,  0.],
 [ 0.,  0.,  0.,  0.]])
>>> b.ndim
2
>>> b.shape
(2, 4)
>>> b.size
8

用相同值填充数组的另一个特殊函数是ones()

>>> import numpy as np
>>> k = np.ones((3, 5))
>>> k
array([[ 1.,  1.,  1.,  1.,  1.],
 [ 1.,  1.,  1.,  1.,  1.],
 [ 1.,  1.,  1.,  1.,  1.]])

最后一个初始化器创建一个具有介于 0.0 和 1.0 之间的随机值的数组:

>>> m = np.random.random((3, 5))
>>> m
array([[  1.92415699e-01,   4.43131404e-01,   7.99226773e-01,
 1.14301942e-01,   2.85383430e-04],
 [  6.53705749e-01,   7.48034559e-01,   4.49463241e-01,
 4.87906915e-01,   9.34341118e-01],
 [  9.47575562e-01,   2.21152583e-01,   2.49031209e-01,
 3.46190961e-01,   8.94842676e-01]])

使用reshape()改变数组的形状

到目前为止,数组似乎并不比列表或元组不同。一个区别是你可以让它执行诸如使用reshape()改变其形状的技巧:

>>> a = np.arange(10)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a = a.reshape(2, 5)
>>> a
array([[0, 1, 2, 3, 4],
 [5, 6, 7, 8, 9]])
>>> a.ndim
2
>>> a.shape
(2, 5)
>>> a.size
10

你可以以不同的方式重塑相同的数组:

>>> a = a.reshape(5, 2)
>>> a
array([[0, 1],
 [2, 3],
 [4, 5],
 [6, 7],
 [8, 9]])
>>> a.ndim
2
>>> a.shape
(5, 2)
>>> a.size
10

将一个具有形状的元组分配给shape会做同样的事情:

>>> a.shape = (2, 5)
>>> a
array([[0, 1, 2, 3, 4],
 [5, 6, 7, 8, 9]])

形状的唯一限制是秩大小的乘积需要等于值的总数(在本例中为 10):

>>> a = a.reshape(3, 4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: total size of new array must be unchanged

使用[]获取一个元素

一维数组的工作方式类似于列表:

>>> a = np.arange(10)
>>> a[7]
7
>>> a[-1]
9

但是,如果数组的形状不同,请在方括号内使用逗号分隔的索引:

>>> a.shape = (2, 5)
>>> a
array([[0, 1, 2, 3, 4],
 [5, 6, 7, 8, 9]])
>>> a[1,2]
7

这与二维 Python 列表不同,后者的索引位于单独的方括号中:

>>> l = [ [0, 1, 2, 3, 4], [5, 6, 7, 8, 9] ]
>>> l
[[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]]
>>> l[1,2]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: list indices must be integers, not tuple
>>> l[1][2]
7

最后一件事:切片是有效的,但再次强调,只能在一组方括号内。让我们再次创建我们熟悉的测试数组:

>>> a = np.arange(10)
>>> a = a.reshape(2, 5)
>>> a
array([[0, 1, 2, 3, 4],
       [5, 6, 7, 8, 9]])

使用切片获取第一行,从偏移量2到末尾的元素:

>>> a[0, 2:]
array([2, 3, 4])

现在,获取最后一行,从末尾倒数第三个元素开始:

>>> a[-1, :3]
array([5, 6, 7])

你也可以使用切片为多个元素赋值。以下语句将值1000赋给所有行的列(偏移量)23

>>> a[:, 2:4] = 1000
>>> a
array([[   0,    1, 1000, 1000,    4],
 [   5,    6, 1000, 1000,    9]])

数组数学

制作和重塑数组是如此有趣,以至于我们几乎忘记了实际上用它们做些什么。作为我们的第一个技巧,我们使用 NumPy 的重新定义的乘法(*)运算符一次性将 NumPy 数组中的所有值相乘:

>>> from numpy import *
>>> a = arange(4)
>>> a
array([0, 1, 2, 3])
>>> a *= 3
>>> a
array([0, 3, 6, 9])

如果你尝试将普通的 Python 列表中的每个元素乘以一个数字,你将需要一个循环或列表推导:

>>> plain_list = list(range(4))
>>> plain_list
[0, 1, 2, 3]
>>> plain_list = [num * 3 for num in plain_list]
>>> plain_list
[0, 3, 6, 9]

这种一次性行为也适用于 NumPy 库中的加法、减法、除法和其他函数。例如,你可以使用zeros()+将数组的所有成员初始化为任何值:

>>> from numpy import *
>>> a = zeros((2, 5)) + 17.0
>>> a
array([[ 17.,  17.,  17.,  17.,  17.],
 [ 17.,  17.,  17.,  17.,  17.]])

线性代数

NumPy 包含许多线性代数函数。例如,让我们定义这个线性方程组:

4x + 5y = 20
 x + 2y = 13

我们如何解出xy?我们构建两个数组:

  • 系数xy的乘数)

  • 依赖变量(方程的右侧)

>>> import numpy as np
>>> coefficients = np.array([ [4, 5], [1, 2] ])
>>> dependents = np.array( [20, 13] )

现在,在linalg模块中使用solve()函数:

>>> answers = np.linalg.solve(coefficients, dependents)
>>> answers
array([ -8.33333333,  10.66666667])

结果显示x约为–8.3,y约为 10.6。这些数字是否解出了方程?

>>> 4 * answers[0] + 5 * answers[1]
20.0
>>> 1 * answers[0] + 2 * answers[1]
13.0

太棒了。为了避免输入所有内容,您还可以要求 NumPy 为您获取数组的点积

>>> product = np.dot(coefficients, answers)
>>> product
array([ 20.,  13.])

如果此解决方案正确,则product数组中的值应接近dependents中的值。您可以使用allclose()函数来检查数组是否大致相等(由于浮点数精度问题,它们可能并不完全相等):

>>> np.allclose(product, dependents)
True

NumPy 还包括用于多项式、傅里叶变换、统计以及一些概率分布的模块。

SciPy

在 NumPy 之上构建的数学和统计函数库还有更多:SciPy。SciPy 的发布包括 NumPy、SciPy、Pandas(本章后续会讨论)、以及其他库。

SciPy 包括许多模块,包括以下任务:

  • 优化

  • 统计

  • 插值

  • 线性回归

  • 积分

  • 图像处理

  • 信号处理

如果您曾经使用过其他科学计算工具,您会发现 Python、NumPy 和 SciPy 与商业软件MATLAB或开源软件R涵盖了一些相同的领域。

SciKit

在类似之前软件的模式下,SciKit是基于 SciPy 构建的一组科学软件包。SciKit-Learn是一个重要的机器学习软件包:它支持建模、分类、聚类以及各种算法。

Pandas

最近,“数据科学”这个术语变得很常见。我见过一些定义,包括“在 Mac 上进行的统计分析”或“在旧金山进行的统计分析”。不管您如何定义它,本章讨论的工具——NumPy、SciPy 以及本节的主题 Pandas——都是一个日益流行的数据科学工具包的组成部分。(Mac 和旧金山是可选的。)

Pandas是一种用于交互式数据分析的新软件包。它特别适用于真实世界的数据处理,结合了 NumPy 的矩阵数学和电子表格以及关系数据库的处理能力。Wes McKinney(O’Reilly)的书籍Python for Data Analysis: Data Wrangling with Pandas, NumPy, and IPython介绍了使用 NumPy、IPython 和 Pandas 进行数据整理。

NumPy 致力于传统的科学计算,通常用于处理单一类型的多维数据集,通常是浮点数。而 Pandas 更像是一个数据库编辑器,可以处理多种数据类型的组合。在某些语言中,这些组合被称为记录结构。Pandas 定义了一个名为DataFrame的基本数据结构。它是具有名称和类型的列的有序集合。它与数据库表、Python 的命名元组和嵌套字典有些相似。其目的在于简化您可能会遇到的数据处理,不仅仅局限于科学领域,还包括商业领域。事实上,Pandas 最初是为了处理金融数据而设计的,而其最常见的替代品是电子表格。

Pandas 是一个处理现实世界中混乱数据(缺失值、奇怪的格式、零散的测量)的 ETL 工具,支持各种数据类型。您可以分割、连接、扩展、填充、转换、重塑、切片、加载和保存文件。它与我们刚讨论过的工具集成在一起——NumPy、SciPy、iPython——用于计算统计数据、将数据拟合到模型、绘制图表、发布等等。

大多数科学家只想完成他们的工作,而不是花数月时间成为奇特计算机语言或应用的专家。使用 Python,他们可以更快地变得高效。

Python 与科学领域

我们已经看过几乎适用于任何科学领域的 Python 工具。那么针对特定科学领域的软件和文档呢?以下是 Python 在特定问题和一些特殊用途库中的应用示例:

一般

物理学

生物学和医学

Python 和科学数据的国际会议包括以下内容:

即将到来

我们已经触及了可观察的 Python 宇宙的尽头,除了多元宇宙附录。

要做的事情

安装 Pandas。获取 示例 16-1 中的 CSV 文件。运行 示例 16-2 中的程序。尝试一些 Pandas 命令。

附录 A. 初学者的硬件和软件

有些东西是直观的。有些我们在自然界中看到,而其他的是人类的发明,如轮子或披萨。

其他需要更大的信仰跳跃。电视是如何将空中的看不见的摇晃转换成声音和动态图像的?

计算机是一个难以接受的想法之一。你怎么能够输入东西,然后让机器按照你的意愿去做呢?

当我学习编程时,有些基本问题的答案很难找到。例如:一些书籍用图书馆书架上的书的类比来解释计算机存储器。我曾想知道,如果你从内存中读取,类比意味着你从书架上取出一本书。那么,这会把它从内存中删除吗?实际上不会。更像是从书架上复制一本书。

如果你是相对新手,本附录简要回顾了计算机硬件和软件。我尝试解释那些最终变得“显而易见”的事物,但可能在一开始时会成为障碍。

硬件

穴居人的计算机

当穴居人 Og 和 Thog 从狩猎归来时,他们每杀死一头猛犸象,就会在自己的堆里加一块石头。但除了比谁的堆显著更大,他们不能做什么。

Og 的远裔(Thog 有一天被猛犸象踩死,试图增加他的堆),学会了数数、写字和使用算盘。但要超越这些工具,进入计算机的概念,需要一些想象力和技术的飞跃。第一个必要的技术是电力。

电力

本杰明·富兰克林认为电是一种从流体量较多的地方(正极)流向量较少的地方(负极)的流动的某种看不见的液体。他是对的,但术语搞反了。电子从他的“负极”流向“正极”,但电子直到后来才被发现 —— 太迟了,不能改变术语。因此,自那时起,我们一直需要记住电子流动的方向和电流定义的流动方式是相反的。

我们都熟悉自然电现象,如静电和闪电。在人们发现如何通过导线推动电子形成电路后,我们离制造计算机又近了一步。

我曾经以为电线中的电流是由兴奋的电子在轨道上做圈圈引起的。实际情况大不相同。电子会从一个原子跳到另一个原子。它们的行为有点像管子中的球轴承(或者珍珠奶茶吸管中的珍珠)。当你在一端推动一颗球时,它会推动它的邻居,依此类推,直到另一端的球被推出来。虽然一个普通电子的移动速度很慢(电线中的漂移速度仅约为每小时三英寸),但这种几乎同时的撞击使得生成的电磁波传播非常迅速:速度达到光速的 50%到 99%,取决于导体。

发明

我们仍然需要:

  • 记忆事物的一种方法

  • 一种方法是利用我们记住的东西进行操作

一个存储概念是开关:一种只能处于开或关状态,并且保持不变直到有东西将其翻转到另一状态的物品。电开关通过打开或关闭电路来工作,允许电子流动或阻止电子流动。我们经常使用开关来控制灯和其他电气设备。需要的是一种能够通过电来控制开关本身的方法。

最早的计算机(和电视)使用真空管来实现这一目的,但它们又大又经常烧坏。导致现代计算机出现的单一关键发明是晶体管:它更小、更高效、更可靠。最后的关键步骤是将晶体管做得更小,并将它们连接成集成电路。多年来,随着计算机变得越来越小,它们变得越来越快速和价格越来越低廉。当组件越接近时,信号传输速度更快。

但是我们能够将东西放得越来越小的极限是有限的。这种电子的调皮会遇到电阻,产生热量。我们在超过 10 年前达到了这个下限,制造商通过在同一板上放置多个“芯片”来补偿。这增加了分布式计算的需求,我稍后会讨论。

不管这些细节如何,有了这些发明,我们已经能够构建计算机:能够记住事物并对其进行处理的机器。

理想化的计算机

真正的计算机有许多复杂的功能。让我们专注于关键部分。

电路“板”包含 CPU、存储器和连接它们彼此以及外部设备插头的电线。

CPU

CPU(中央处理单元)或“芯片”执行实际的“计算”:

  • 数学任务如加法

  • 比较值

存储器和高速缓存

RAM(随机存取存储器)进行“记忆”。它速度快,但是易失性(如果断电则会丢失数据)。

CPU 比内存速度更快,因此计算机设计者一直在添加缓存:位于 CPU 和主内存之间的较小、更快的内存。当 CPU 尝试从内存读取一些字节时,它首先尝试最接近的缓存(称为L1缓存),然后是下一个(L2),最后到主 RAM。

存储

因为主存储器会丢失数据,我们还需要非易失性存储。这些设备比内存便宜,能够存储更多数据,但速度也慢得多。

传统的存储方法是“旋转锈”:磁盘(或硬盘HDD)带有可移动读写头,有点像黑胶唱片和唱针。

一种称为固态硬盘(Solid State Drive,SSD)的混合技术是由类似 RAM 的半导体组成,但像磁盘一样是非易失性的。价格和速度介于这两者之间。

输入

如何将数据输入计算机?对于人类来说,主要选择是键盘、鼠标和触摸屏。

输出

人们通常通过显示器和打印机看到计算机的输出。

相对访问时间

从任何这些组件获取数据并将数据传输出去所需的时间差异巨大。这对实际应用有着重大的影响,例如,软件需要在内存中运行并访问数据,但它还需要将数据安全地存储在像磁盘这样的非易失性设备上。问题在于磁盘慢了成千上万倍,网络甚至更慢。这意味着程序员花费了大量时间来在速度和成本之间进行最佳权衡。

《人类尺度下的计算机延迟》 中,大卫·杰普森对它们进行了比较。我从他的数据和其他数据推导出了 表 A-1。最后三列——比率、相对时间(CPU = 一秒)和相对距离(CPU = 一英寸)——比具体的时间更容易理解。

表 A-1. 相对访问时间

地点 时间 比率 相对时间 相对距离
CPU 0.4 纳秒 1 1 秒 1 英寸
L1 缓存 0.9 纳秒 2 2 秒 2 英寸
L2 缓存 2.8 纳秒 7 7 秒 7 英寸
L3 缓存 28 纳秒 70 1 分钟 6 英尺
RAM 100 纳秒 250 4 分钟 20 英尺
SSD 100 微秒 250,000 3 天 4 英里
磁盘 10 毫秒 25,000,000 9 个月 400 英里
互联网:SF→NY 65 毫秒 162,500,000 5 年 2,500 英里

庆幸的是,CPU 指令实际上只需要不到一纳秒的时间,而不是整整一秒,否则你可能在访问磁盘的时间里生个孩子。因为磁盘和网络的时间比 CPU 和 RAM 慢得多,所以尽可能多地在内存中进行工作是有帮助的。而且由于 CPU 本身比 RAM 快得多,所以将数据保持连续是有意义的,这样字节就可以由更快(但更小)的缓存服务,靠近 CPU。

软件

鉴于所有这些计算机硬件,我们将如何控制它?首先,我们既有指令(告诉 CPU 要做什么的东西),也有数据(指令的输入和输出)。在存储程序计算机中,一切都可以被视为数据,这简化了设计。但是如何表示指令和数据呢?什么是你在一个地方保存并在另一个地方处理的东西?遥远的原始人 Og 的后代们想知道。

起初是比特

让我们重新思考一下开关的概念:维持两个值之一的东西。这些值可以是开或关、高或低电压、正或负——只要是可以设置的东西,不会忘记,并且以后可以向任何要求它的人提供其值的东西。集成电路给了我们一种将数十亿个小开关集成和连接到小芯片中的方法。

如果一个开关只能有两个值,它可以用来表示一个比特,或二进制位。这可以被视为微小的整数 01,是和否、真和假,或者任何我们想要的东西。

然而,对于 01 以外的任何东西来说,比特都太小了。我们怎么能说服比特表示更大的事物呢?

要获得答案,请看您的手指。我们在日常生活中只使用 10 个数字(0 到 9),但通过位的位置表示法可以创建比 9 更大的数字。如果我给数字381,则8变成9,整个值现在是39。如果我再加1,则9变成0,我向左进位,将3增加到4,得到最终的数字40。最右边的数字在“个位”,左边的在“十位”,依此类推向左乘以 10。使用三个十进制数字,可以表示一千(10 * 10 * 10)个数字,从000999

我们可以使用位的位置表示法来组合更大的位集合。字节由八位组成,有 2⁸(256)种可能的位组合。例如,您可以使用一个字节存储小整数0255(在位的位置表示法中需要为零保留空间)。

一个字节看起来像是一排八个位,每个位的值要么是0(关闭或假),要么是1(打开或真)。最右边的位是最低有效位,最左边的是最高有效位

机器语言

每台计算机的 CPU 都设计有一组位模式(也称为操作码)的指令集,它理解这些指令。每个操作码执行特定的功能,从一个地方获取输入值并将输出值传输到另一个地方。CPU 有称为寄存器的特殊内部位置来存储这些操作码和值。

让我们使用一个仅处理字节的简化计算机,它有四个字节大小的寄存器,分别称为ABCD。假设:

  • 命令的操作码放入寄存器A中。

  • 该指令从寄存器BC获取其字节输入。

  • 该指令将其字节结果存储在寄存器D中。

(两个字节相加可能会溢出到一个字节的结果,但这里我忽略了这一点,以展示发生了什么。)

假设

  • 寄存器A包含加两个整数的操作码:十进制1(二进制00000001)。

  • 寄存器B中有十进制值5(二进制00000101)。

  • 寄存器C中有十进制值3(二进制00000011)。

CPU 发现指令已经到达寄存器A。它解码并执行该指令,从寄存器BC读取值,并将它们传递给内部硬件电路,可以添加字节。完成后,我们应该在寄存器D中看到十进制值8(二进制00001000)。

CPU 使用寄存器进行加法和其他数学功能。它解码操作码并将控制传递给 CPU 内特定的电路。它还可以比较事物,比如“寄存器B中的值是否大于寄存器C中的值?”重要的是,它还从内存中获取值到 CPU,从 CPU 中存储值到内存。

计算机将程序(机器语言指令和数据)存储在内存中,并处理将指令和数据输入到 CPU,并从 CPU 中读取指令和数据。

汇编器

用机器语言编程很难。你必须精确指定每一位,这非常耗时。因此,人们提出了一种稍微可读性更高的语言级别,称为汇编语言,或者简称汇编器。这些语言特定于 CPU 设计,并允许你使用变量名来定义指令流和数据。

高级语言

汇编仍然是一项繁琐的工作,所以人们设计了更高级别的语言,更容易供人们使用。这些语言将由称为编译器的程序转换为汇编语言,或者由解释器直接运行。其中最早的语言包括 FORTRAN、LISP 和 C——在设计和预期使用方面大相径庭,但在计算机架构中的位置相似。

在实际工作中,你往往会看到不同的软件“堆栈”:

大型计算机

IBM,COBOL,FORTRAN 和其他语言

微软

Windows,ASP,C#,SQL Server

JVM

Java,Scala,Groovy

开源软件

Linux,语言(Python,PHP,Perl,C,C++,Go),数据库(MySQL,PostgreSQL),Web(apache,nginx)

程序员倾向于留在这些世界中的一个,使用其中的语言和工具。一些技术,如 TCP/IP 和 Web,允许不同堆栈之间的互联。

操作系统

每一个创新都是在前面的基础上构建的,通常我们不知道也不关心更底层的工作原理。工具构建工具,进而构建更多工具,我们习以为常。

主要的操作系统有:

Windows(微软)

商业软件,多个版本

macOS(苹果)

商业软件

Linux

开源软件

Unix

许多商业版本,大部分已被 Linux 取代

操作系统包含:

内核

调度和控制程序和 I/O

设备驱动程序

被内核用于访问 RAM、磁盘和其他设备

开发人员使用的源代码和二进制文件

应用程序

独立程序

同一台计算机硬件可以支持多个操作系统,但一次只能运行一个。当一个操作系统启动时,称为启动,¹ 因此重新启动是重新启动它。这些术语甚至出现在电影营销中,因为制片厂会“重启”之前未成功的尝试。你可以通过安装多个操作系统并排运行来双重启动你的计算机,但一次只能启动和运行一个。

如果你看到术语裸金属,它指的是单台运行操作系统的计算机。在接下来的几节中,我们将从裸金属中逐步升级。

虚拟机

操作系统有点像一个大程序,所以最终有人想出了如何在主机机器上作为虚拟机(客户程序)运行外部操作系统。所以你可以在你的 PC 上运行 Microsoft Windows,同时又在其上启动一个 Linux 虚拟机,而无需购买第二台计算机或双重启动。

容器

一个更近期的概念是容器——一种同时运行多个操作系统的方式,只要它们共享相同的内核。这个想法是由Docker推广的,它利用了一些鲜为人知的 Linux 内核功能并增加了有用的管理功能。他们对运输集装箱的类比(它们彻底改变了运输业,并为我们所有人节省了金钱)是清晰而吸引人的。通过将代码作为开源发布,Docker 使容器能够在整个计算机行业中迅速被采用。

谷歌和其他云提供商多年来一直悄悄地在 Linux 中添加基础内核支持,并在他们的数据中心中使用容器。容器使用的资源比虚拟机少,让你能够在每个物理计算机箱中装更多的程序。

分布式计算与网络

当企业开始使用个人电脑时,它们需要一种使它们互相交流以及与打印机等设备通信的方式。最初使用专有的网络软件,如诺维尔(Novell)的,但随着因特网在 90 年代中期到 90 年代末的出现,最终被 TCP/IP 所取代。微软从一种名为BSD的免费 Unix 变体中获取了它的 TCP/IP 堆栈。²

因互联网繁荣而带来的一个影响是对服务器的需求:运行所有那些网络、聊天和电子邮件服务的机器和软件。旧式的系统管理员(系统管理)是手动安装和管理所有硬件和软件。不久之后,很明显地对每个人来说都需要自动化。2006 年,微软的比尔·贝克尔提出了关于服务器管理的宠物与牛的类比,自那时起它已经成为一个行业迷因(有时更通用地称为宠物与牲畜);见表 A-2。

表 A-2. 宠物与牲畜

宠物 牲畜
分别命名 自动编号
定制关怀 标准化
护理至健康 替换

作为“系统管理员”的继任者,您经常会看到术语DevOps:开发加运维,一种支持对服务进行快速更改而不会炸掉的技术混合。云服务非常庞大和复杂,即使像亚马逊和谷歌这样的大公司也偶尔会出现故障。

多年来,人们一直在建立计算机集群,使用许多技术。一个早期的概念是贝奥武夫集群:由相同的商用计算机(戴尔或类似的,而不是太阳或惠普等工作站)通过本地网络连接起来。

术语云计算意味着使用数据中心中的计算机来执行计算任务和存储数据——但不仅仅是为拥有这些后端资源的公司。这些服务向任何人提供,费用基于 CPU 时间、磁盘存储量等。亚马逊及其AWS(亚马逊网络服务)是最突出的,但Azure(微软)和Google Cloud也是大公司。

在幕后,这些云使用裸金属、虚拟机和容器——全部被视为家畜,而非宠物。

Kubernetes

需要管理多个数据中心中的大型计算机集群的公司,如谷歌、亚马逊和 Facebook,都借用或构建了帮助它们扩展的解决方案:

部署

如何使新的计算硬件和软件可用?它们失败时如何替换?

配置

这些系统应该如何运行?它们需要诸如其他计算机的名称和地址、密码和安全设置等内容。

编排

如何管理所有这些计算机、虚拟机和容器?您能根据负载变化进行扩展或收缩吗?

服务发现

您如何找出谁在做什么,以及它在哪里?

一些竞争解决方案由 Docker 和其他公司开发。但在过去几年中,看起来Kubernetes已经获胜。

谷歌开发了大型内部管理框架,代号为 Borg 和 Omega。当员工们提出开源这些“皇家珍宝”的想法时,管理层需要考虑一下,但他们迈出了这一步。谷歌在 2015 年发布了 Kubernetes 1.0 版本,其生态系统和影响力自那时以来不断增长。

¹ 这指的是“通过自己的靴带提升自己”,这似乎和一台计算机一样不可能。

² 您仍然可以在一些微软文件中看到加州大学的版权声明。

附录 B. 安装 Python 3

本书中大多数示例都是使用 Python 3.7 编写和测试的,这是当时最新的稳定版本。Python 的新功能页面介绍了每个版本的新增内容。有许多 Python 的来源和安装方式。在本附录中,我描述了其中的一些方式:

  • 标准安装会从python.org下载 Python,并添加辅助程序pipvirtualenv

  • 如果你的工作涉及科学领域,你可能更喜欢从 Anaconda 获取预装有许多科学包的 Python,并使用其包安装程序conda而不是pip

Windows 根本没有安装 Python,而 macOS、Linux 和 Unix 则倾向于安装旧版本。在它们赶上之前,你可能需要自行安装 Python 3。

检查你的 Python 版本

在终端或终端窗口中输入python -V

$ python -V
Python 3.7.2

根据你的操作系统,如果没有安装 Python 或操作系统找不到它,你将会收到一些类似command not found的错误消息。

如果你已经安装了 Python 且是版本 2,你可能想要安装 Python 3 —— 可以是系统范围的,也可以只安装在虚拟环境中(见“使用 virtualenv”,或者“安装 virtualenv”)。在本附录中,我展示了如何在系统范围内安装 Python 3。

安装标准 Python

打开官方 Python下载页面。它会尝试猜测你的操作系统并显示适当的选择,但如果猜错了,你可以使用以下信息:

你将看到一个类似于图 B-1 所示的页面。

inp2 ab01

B-1. 示例下载页面

如果你点击黄色的 Download Python 3.7.3 按钮,它将为你的操作系统下载该版本。如果你想先了解一些关于它的信息,点击表格底部第一列中蓝色链接文字 Python 3.7.3,这会带你到一个类似于图 B-2 所示的信息页面。

inp2 ab02

B-2. 下载详细页面

你需要向下滚动页面才能看到实际的下载链接(图 B-3)。

inp2 ab03

B-3. 页面底部提供下载

macOS

点击macOS 64 位/32 位安装程序链接来下载一个 Mac 的*.pkg*文件。双击打开它,你会看到一个介绍性的对话框(图 B-4)。

inp2 ab04

B-4. Mac 安装对话框 1

点击继续。你将会通过一系列其他对话框。

当一切完成时,您应该看到 图 B-5 中显示的对话框。

inp2 ab05

图 B-5. Mac 安装对话框 9

Python 3 将安装为 /usr/local/bin/python3,不会更改计算机上的任何现有 Python 2。

Windows

Windows 从未包含 Python,但最近已经更容易安装。Windows 10 的 2019 年 5 月更新 包括 python.exepython3.exe 文件。这些不是 Python 解释器,而是指向 Microsoft Store 的新 Python 3.7 页面 的链接。您可以使用此链接下载和安装 Python,方式与获取其他 Windows 软件相同。

或者您可以从官方 Python 网站下载并安装 Python:

要确定您的 Windows 版本是 32 位还是 64 位:

  • 单击“开始”按钮。

  • 右键单击“计算机”。

  • 单击属性并查找位数值。

单击适当的安装程序(.msi 文件)。下载完成后,双击并按照安装程序的指示操作。

Linux 或 Unix

Linux 和 Unix 用户可以选择压缩的源代码格式:

选择其中一个进行下载。通过使用 tar xJ (.xz 文件) 或 tar xz (.tgz 文件) 解压缩它,然后运行生成的 shell 脚本。

安装 pip 包管理器

除了标准 Python 安装外,对于 Python 开发,两个工具几乎是必不可少的:pipvirtualenv

pip 包是安装非标准 Python 包的最流行方式。一个如此有用的工具没有成为标准 Python 的一部分,需要你自己下载和安装,实在令人恼火。正如我的一个朋友曾经说过的,这是一种残酷的入门仪式。好消息是,从 3.4 版本开始,pip 已经成为 Python 的标准部分。

如果您安装了 Python 3,但只有 Python 2 版本的 pip,那么在 Linux 或 macOS 上获取 Python 3 版本的方法如下:

$ curl -O http://python-distribute.org/distribute_setup.py
$ sudo python3 distribute_setup.py
$ curl -O https://raw.github.com/pypa/pip/master/contrib/get-pip.py
$ sudo python3 get-pip.py

这将在您的 Python 3 安装的 bin 目录中安装 pip-3.3。然后,使用 pip-3.3 安装第三方 Python 包,而不是 Python 2 的 pip

安装 virtualenv

经常与 pip 一起使用,virtualenv 程序是在指定目录(文件夹)中安装 Python 包的一种方式,以避免与任何现有系统 Python 包的交互。这使您可以使用任何您想要的 Python 工具,即使您没有更改现有安装的权限。

一些关于 pipvirtualenv 的好指南包括:

其他打包解决方案

你已经看到,Python 的打包技术各有千秋,没有一种方法适用于所有问题。PyPA(Python 打包管理权威组织)是一个志愿工作团队(并非官方 Python 开发核心组的一部分),旨在简化 Python 的打包过程。该团队编写了Python 打包用户指南,讨论了相关问题及其解决方案。

最流行的工具是 pipvirtualenv,我在本书中一直使用它们。如果它们对你来说不够用,或者你喜欢尝试新事物,这里还有一些替代方案:

  • pipenv 结合了 pipvirtualenv 并添加了更多功能。还可以参考一些 批评 和相关的 讨论

  • poetry 是一个竞争对手,解决了 pipenv 的一些问题。

但是最流行的打包工具,尤其是在科学和数据密集型应用中,是 conda。你可以作为 Anaconda Python 发行版的一部分获得它,下面我将详细介绍,或者单独获取它(“安装 Anaconda 的包管理器 conda”)。

安装 Anaconda

Anaconda 是一个以科学为重点的一体化发行版。最新版本 Anaconda3 包括 Python 3.7 及其标准库,以及用于数据科学的 R 语言。其他有用的库包括我们在本书中讨论过的:beautifulsoup4, flask, ipython, matplotlib, nose, numpy, pandas, pillow, pip, scipy, tables, zmq 等。它还有一个跨平台的安装程序称为 conda,我将在下一节介绍。

要安装 Anaconda3,请访问下载页面获取 Python 3 版本。点击适合你平台的链接(版本号可能会有所变化,但你可以找到对应的版本):

  • macOS 安装程序将所有内容安装到你主目录下的 anaconda 目录。

  • 对于 Windows,在下载完成后,双击 .exe 文件。

  • 对于 Linux,选择 32 位或 64 位版本。下载完成后,执行它(这是一个大型的 Shell 脚本)。

注意

确保你下载的文件名以 Anaconda3 开头。如果只以 Anaconda 开头,那就是 Python 2 版本。

Anaconda 将所有内容安装在自己的目录下(在你的主目录下的 anaconda 目录)。这意味着它不会干扰你计算机上可能已安装的任何 Python 版本。同时也意味着你无需特殊权限(如 adminroot 账户)来安装它。

Anaconda 现在包含超过 1500 个开源包。访问 Anaconda 文档 页面,点击适合你平台和 Python 版本的链接。

安装完 Anaconda3 后,你可以输入命令 conda list 查看圣诞老人给你装了什么东西在你的电脑上。

安装 Anaconda 的包管理器 conda

Anaconda 的开发者们构建了 conda 来解决他们在使用 pip 和其他工具时遇到的问题。pip 是一个 Python 包管理器,但 conda 可以与任何软件和语言一起使用。conda 还避免了像 virtualenv 这样的工具,以避免安装互相冲突的问题。

如果你安装了 Anaconda 发行版,你已经拥有 conda 程序了。如果没有,你可以从 miniconda 页面获取 Python 3 和 conda。和 Anaconda 一样,确保你下载的文件以 Miniconda3 开头;如果只是以 Miniconda 开头,那就是 Python 2 版本。

conda 可以和 pip 一起工作。虽然它有自己的公共 包仓库,但像 conda search 这样的命令也会搜索 PyPi 仓库。如果你在使用 pip 时遇到问题,conda 可能是一个不错的选择。

附录 C. 完全不同的东西:异步

我们的前两个附录是为初学者编写的,但这个是为那些有些进阶的人准备的。

和大多数编程语言一样,Python 一直以来都是同步的。它按顺序线性地运行代码,逐行从头到尾。当你调用一个函数时,Python 跳到其代码中,并且调用者会等待函数返回后才能恢复其原来的工作。

你的 CPU 一次只能做一件事情,因此同步执行是完全合理的。但事实证明,一个程序实际上并不总是在运行代码,而是在等待某些东西,比如来自文件或网络服务的数据。这就像我们盯着浏览器屏幕等待网站加载一样。如果我们能避免这种“忙等待”,我们可能会缩短程序的总运行时间。这也被称为提高吞吐量

在第十五章中,你看到如果需要并发处理,你可以选择线程、进程或者像geventtwisted这样的第三方解决方案。但现在有越来越多的异步答案,既内置于 Python 中,也有第三方解决方案。这些与通常的同步 Python 代码并存,但借用一句《捉鬼敢死队》中的警告,不能混用。我会向你展示如何避免任何幽灵般的副作用。

协程和事件循环

在 Python 3.4 中,Python 加入了一个标准的异步模块,名为asyncio。Python 3.5 随后添加了关键字asyncawait。这些实现了一些新概念:

  • 协程是在各种点暂停的函数

  • 一个事件循环,用于调度和运行协程

这些让我们编写看起来像我们习惯的普通同步代码的异步代码。否则,我们需要使用第十五章和第十七章中提到的方法,并在后面总结为“异步与…”。

正常的多任务处理是你的操作系统对你的进程所做的事情。它决定什么是公平的,谁是 CPU 食量巨大者,何时打开 I/O 水龙头等等。然而,事件循环提供了协作式多任务处理,其中协程指示何时能够开始和停止。它们在单个线程中运行,因此你不会遇到我在“线程”中提到的潜在问题。

通过在初始的def前加上async定义一个协程。你通过以下方式调用一个协程:

  • await放在前面,悄悄地将协程添加到现有的事件循环中。你只能在另一个协程内部这样做。

  • 或者通过asyncio.run()来使用,它显式地启动一个事件循环。

  • 或者使用asyncio.create_task()asyncio.ensure_future()

这个例子使用了前两种调用方法:

>>> import asyncio
>>>
>>> async def wicked():
...     print("Surrender,")
...     await asyncio.sleep(2)
...     print("Dorothy!")
...
>>> asyncio.run(wicked())
Surrender,
Dorothy!

这里有一个戏剧性的两秒等待,在打印页上你看不到。为了证明我们没有作弊(详见第十九章中的timeit详细信息):

>>> from timeit import timeit
>>> timeit("asyncio.run(wicked())", globals=globals(), number=1)
Surrender,
Dorothy!
2.005701574998966

asyncio.sleep(2) 的调用本身就是一个协程,在这里只是一个示例,模拟像 API 调用这样的耗时操作。

asyncio.run(wicked()) 这一行是从同步 Python 代码中运行协程的一种方式(这里是程序的顶层)。

与标准同步对应的区别(使用time.sleep())是,在运行wicked()时,调用者不会被阻塞两秒钟。

运行协程的第三种方式是创建一个任务await它。此示例展示了任务方法以及前两种方法:

>>> import asyncio
>>>
>>> async def say(phrase, seconds):
...     print(phrase)
...     await asyncio.sleep(seconds)
...
>>> async def wicked():
...     task_1 = asyncio.create_task(say("Surrender,", 2))
...     task_2 = asyncio.create_task(say("Dorothy!", 0))
...     await task_1
...     await task_2
...
>>> asyncio.run(wicked())
Surrender,
Dorothy!

如果你运行这个代码,你会发现这次两行打印之间没有延迟。这是因为它们是独立的任务。task_1在打印Surrender后暂停了两秒,但这并不影响task_2

await类似于生成器中的yield,但是它不返回值,而是标记一个在需要时事件循环可以暂停它的位置。

文档中可以找到更多相关信息。同步和异步代码可以在同一个程序中共存。只需记住在函数定义前加上async,在调用异步函数前加上await即可。

一些更多信息:

Asyncio 替代方案

虽然asyncio是标准的 Python 包,但是你可以在没有它的情况下使用asyncawait。协程和事件循环是独立的。asyncio的设计有时会受到批评,并且出现了第三方替代品:

让我们展示一个使用trioasks(一个类似于requestsAPI 的异步 Web 框架)的真实例子。示例 C-1 展示了一个使用trioasks进行并发 Web 爬虫的例子,这个例子改编自 stackoverflow 的一个答案。要运行这个例子,首先要安装trioasks

示例 C-1. trio_asks_sites.py
import time

import asks
import trio

asks.init("trio")

urls = [
    'https://boredomtherapy.com/bad-taxidermy/',
    'http://www.badtaxidermy.com/',
    'https://crappytaxidermy.com/',
    'https://www.ranker.com/list/bad-taxidermy-pictures/ashley-reign',
]

async def get_one(url, t1):
    r = await asks.get(url)
    t2 = time.time()
    print(f"{(t2-t1):.04}\t{len(r.content)}\t{url}")

async def get_sites(sites):
    t1 = time.time()
    async with trio.open_nursery() as nursery:
        for url in sites:
            nursery.start_soon(get_one, url, t1)

if __name__ == "__main__":
    print("seconds\tbytes\turl")
    trio.run(get_sites, urls)

下面是我得到的:

$ python trio_asks_sites.py
seconds bytes   url
0.1287  5735    https://boredomtherapy.com/bad-taxidermy/
0.2134  146082  https://www.ranker.com/list/bad-taxidermy-pictures/ashley-reign
0.215   11029   http://www.badtaxidermy.com/
0.3813  52385   https://crappytaxidermy.com/

注意到trio没有使用asyncio.run(),而是使用了自己的trio.open_nursery()。如果你感兴趣,可以阅读关于trio背后设计决策的文章讨论

一个名为AnyIO的新包提供了对asynciocuriotrio的单一接口。

未来,你可以期待更多的异步方法,无论是在标准 Python 中还是来自第三方开发者。

异步对比…

如你在本书中许多地方看到的那样,有许多并发技术。异步处理与它们相比如何?

进程

如果你想要使用机器上的所有 CPU 核心或多台机器,这是一个很好的解决方案。但是进程比较重,启动时间长,并且需要序列化进行进程间通信。

线程

虽然线程被设计为进程的“轻量级”替代方案,但每个线程使用大量内存。协程比线程轻得多;在仅支持几千个线程的机器上,可以创建数十万个协程。

绿色线程

绿色线程像gevent一样表现良好,并且看起来像是同步代码,但是需要猴子补丁标准的 Python 函数,例如套接字库。

回调

twisted这样的库依赖于回调:当某些事件发生时调用的函数。这对 GUI 和 JavaScript 程序员来说很熟悉。

队列—当你的数据或进程确实需要多台机器时,这些通常是一个大规模的解决方案。

异步框架和服务器

Python 的异步新增功能是最近才有的,开发者们需要时间来创建像 Flask 这样的异步框架版本。

ASGI标准是 WSGI 的异步版本,详细讨论请参见这里

这里有一些 ASGI Web 服务器:

还有一些异步 Web 框架:

最后,一些异步数据库接口:

附录 D. 练习答案

1. Python 的味道

1.1 如果你的计算机上还没有安装 Python 3,请立即安装。参见附录 B 获取详细的安装信息。

1.2 启动 Python 3 交互式解释器。关于如何做的详细信息在附录 B 中。它应该打印几行关于自身的信息,然后一行以>>>开头。这是你输入 Python 命令的提示符。

1.3 用解释器玩一会儿。像计算器一样使用它,输入8 * 9。按 Enter 键查看结果。Python 应该打印72

1.4 输入数字47并按 Enter 键。它是否在下一行为你打印了 47?

1.5 现在输入print(47)并按 Enter 键。这会在下一行也打印出 47 吗?

2. 数据:类型、值、变量和名称

2.1 将整数值99赋给变量prince,并打印出来。

2.2 值5的类型是什么?

2.3 值2.0的类型是什么?

2.4 表达式5 + 2.0的类型是什么?

3. 数字

3.1 一个小时有多少秒?将秒钟数(60)乘以小时数(同样是60),使用交互式解释器作为计算器。

3.2 将前一个任务(一个小时的秒数)的结果赋给名为seconds_per_hour的变量。

3.3 一天有多少秒?使用你的seconds_per_hour变量。

3.4 再次计算一天的秒数,但这次将结果保存在名为seconds_per_day的变量中。

3.5 将seconds_per_day除以seconds_per_hour。使用浮点数(/)除法。

3.6 使用整数(//)除法将seconds_per_day除以seconds_per_hour。这个数值与前一个问题中的浮点数值是否一致,除了最后的.0

4. 选择if

4.1 选择一个介于 1 到 10 之间的数字,并将其赋给变量secret。然后,再选择另一个介于 1 到 10 之间的数字,并赋给变量guess。接下来,编写条件测试(ifelseelif)来打印字符串'too low'(如果guess小于secret)、'too high'(如果大于secret)和'just right'(如果等于secret)。

4.2 将TrueFalse分配给变量smallgreen。编写一些if/else语句来打印与这些选择匹配的水果:cherry、pea、watermelon、pumpkin。

5. 文本字符串

5.1 将以m开头的单词大写:

>>> song = """When an eel grabs your arm,
... And it causes great harm,
... That's - a moray!"""

5.2 按正确的匹配打印每个列表问题和它们的答案,格式如下:

Q: 问题

A: 答案

>>> questions = [
...     "We don't serve strings around here. Are you a string?",
...     "What is said on Father's Day in the forest?",
...     "What makes the sound 'Sis! Boom! Bah!'?"
...     ]
>>> answers = [
...     "An exploding sheep.",
...     "No, I'm a frayed knot.",
...     "'Pop!' goes the weasel."
...     ]

5.3 使用旧式格式编写以下诗歌。将字符串'roast beef''ham''head''clam'替换到这个字符串中:

My kitty cat likes %s,
My kitty cat likes %s,
My kitty cat fell on his %s
And now thinks he's a %s.

5.4 使用新式格式编写一封表格信。将以下字符串保存为letter(你将在下一个练习中使用它):

Dear {salutation} {name},

Thank you for your letter. We are sorry that our {product}
{verbed} in your {room}. Please note that it should never
be used in a {room}, especially near any {animals}.

Send us your receipt and {amount} for shipping and handling.
We will send you another {product} that, in our tests,
is {percent}% less likely to have {verbed}.

Thank you for your support.

Sincerely,
{spokesman}
{job_title}

5.5 为名为'salutation''name''product''verbed'(过去式动词)、'room''animals''percent''spokesman''job_title'的字符串变量分配值。使用letter.format()打印letter与这些值。

5.6 在公众投票中命名事物后,出现了一种模式:英国潜艇(Boaty McBoatface)、澳大利亚赛马(Horsey McHorseface)和瑞典火车(Trainy McTrainface)。使用%格式化打印国家博览会上的获奖名字,用于鸭子、葫芦和斯皮茨。

5.7 使用format()格式化相同。

5.8 再来一次,使用f 字符串

6. 使用 while 和 for 循环

6.1 使用for循环打印列表[3, 2, 1, 0]的值。

6.2 将值7赋给变量guess_me,将值1赋给变量number。编写一个while循环,将numberguess_me进行比较。如果number小于guess_me,则打印'too low'。如果number等于guess_me,则打印'found it!',然后退出循环。如果number大于guess_me,则打印'oops',然后退出循环。在循环结束时增加number

6.3 将值5赋给变量guess_me。使用for循环迭代名为number的变量,范围为range(10)。如果number小于guess_me,则打印'too low'。如果等于guess_me,则打印'found it!',然后退出循环。如果number大于guess_me,则打印'oops',然后退出循环。

7. 元组和列表

7.1 创建名为years_list的列表,以你的出生年份开始,直到你五岁生日的年份。例如,如果你是 1980 年出生,列表将是years_list = [1980, 1981, 1982, 1983, 1984, 1985]

7.2 在这些年份中,哪一年是你的第三个生日?请记住,你的第一年是 0 岁。

7.3 在years_list中哪一年你最年长?

7.4 创建并打印一个名为things的列表,其中包含这三个字符串作为元素:"mozzarella""cinderella""salmonella"

7.5 将things中指向人的元素大写,然后打印列表。它改变了列表中的元素吗?

7.6 将things中的奶酪元素全部大写,然后打印列表。

7.7 删除疾病元素,领取你的诺贝尔奖,然后打印列表。

7.8 创建名为surprise的列表,元素为"Groucho""Chico""Harpo"

7.9 将surprise列表的最后一个元素小写,反转它,然后大写它。

7.10 使用列表推导创建一个名为even的列表,其中包含range(10)中的偶数。

7.11 让我们创建一个跳绳童谣生成器。你将打印一系列两行押韵的诗句。从这个程序片段开始:

start1 = ["fee", "fie", "foe"]
rhymes = [
    ("flop", "get a mop"),
    ("fope", "turn the rope"),
    ("fa", "get your ma"),
    ("fudge", "call the judge"),
    ("fat", "pet the cat"),
    ("fog", "walk the dog"),
    ("fun", "say we're done"),
    ]
start2 = "Someone better"

对于rhymes中的每对字符串(firstsecond):

对于第一行:

  • 打印start1中的每个字符串,大写并后跟感叹号和空格。

  • 打印大写的first,后跟感叹号。

对于第二行:

  • 打印start2和一个空格。

  • 打印second和一个句号。

8. 字典

8.1 创建一个英语到法语的字典e2f并打印它。这是你的起始词汇:dogchiencatchatwalrusmorse

8.2 使用你的三词字典e2f,打印walrus的法语单词。

8.3 从e2f创建一个法语到英语的字典f2e。使用items方法。

8.4 打印法语单词chien的英语对应词。

8.5 打印e2f中的英语单词集合。

8.6 创建一个名为life的多级字典。使用以下字符串作为顶层键:'animals''plants''other'。使'animals'键参考另一个字典,其中包含键'cats''octopi''emus'。使'cats'键参考一个包含值'Henri''Grumpy''Lucy'的字符串列表。使所有其他键参考空字典。

8.7 打印life的顶层键。

8.8 打印life['animals']的键。

8.9 打印life['animals']['cats']的值。

8.10 使用字典推导式创建字典squares。使用range(10)返回键,并使用每个键的平方作为其值。

8.11 使用集合推导式从range(10)中的奇数创建集合odd

8.12 使用生成器推导式返回字符串'Got 'range(10)中的数字。通过使用for循环来迭代这个。

8.13 使用zip()从键元组('optimist', 'pessimist', 'troll')和值元组('The glass is half full', 'The glass is half empty', 'How did you get a glass?')创建一个字典。

8.14 使用zip()创建一个名为movies的字典,将这些列表配对:titles = ['Creature of Habit', 'Crewel Fate', 'Sharks On a Plane']plots = ['A nun turns into a monster', 'A haunted yarn shop', 'Check your exits']

9. 函数

9.1 定义一个名为good()的函数,返回以下列表:['Harry', 'Ron', 'Hermione']

9.2 定义一个生成器函数get_odds(),返回range(10)中的奇数。使用for循环找到并打印第三个返回的值。

9.3 定义一个名为test的装饰器。当调用函数时,打印'start',当函数完成时,打印'end'

9.4 定义一个名为OopsException的异常。引发此异常以查看发生了什么。然后编写代码来捕获此异常并打印'Caught an oops'

10. Oh Oh:对象和类

10.1 创建一个名为Thing的类,没有内容并打印它。然后,从这个类创建一个名为example的对象并打印它。打印出的值是相同的还是不同的?

10.2 创建一个名为Thing2的新类,并将值'abc'分配给一个名为letters的类变量。打印letters

10.3 再次创建一个名为Thing3的类。这次,将值'xyz'赋给一个名为letters的实例(对象)变量。打印letters。你需要创建一个类的对象来执行这个操作吗?

10.4 创建一个名为Element的类,具有实例属性namesymbolnumber。使用值'Hydrogen''H'1创建该类的对象hydrogen

10.5 创建一个具有这些键和值的字典:'name': 'Hydrogen''symbol': 'H''number': 1。然后,使用这个字典从Element类创建一个名为hydrogen的对象。

10.6 对于Element类,定义一个名为dump()的方法,打印对象属性namesymbolnumber的值。从这个新定义创建hydrogen对象,并使用dump()打印其属性。

10.7 调用print(hydrogen)。在Element的定义中,将方法dump的名称更改为__str__,创建一个新的hydrogen对象,并再次调用print(hydrogen)

10.8 将Element修改为使namesymbolnumber属性私有化。为每个属性定义一个 getter 属性以返回其值。

10.9 定义三个类:BearRabbitOctothorpe。对于每个类,只定义一个方法:eats()。分别返回'berries'Bear)、'clover'Rabbit)和'campers'Octothorpe)。分别创建一个对象并打印它吃的东西。

10.10 定义这些类:LaserClawSmartPhone。每个类只有一个方法:does()。分别返回'disintegrate'Laser)、'crush'Claw)或'ring'SmartPhone)。然后定义一个Robot类,该类包含这三个组件对象的一个实例。为Robot定义一个does()方法,打印其组件对象的功能。

11. 模块、包和好东西

11.1 创建一个名为zoo.py的文件。在其中定义一个名为hours的函数,打印字符串'Open 9-5 daily'。然后,在交互式解释器中导入zoo模块并调用其hours函数。

11.2 在交互式解释器中,将zoo模块作为menagerie导入,并调用其hours()函数。

11.3 在解释器中保持不变,直接从zoo导入hours()函数并调用它。

11.4 将hours()函数作为info导入并调用。

11.6 创建一个名为fancyOrderedDict,使用相同的键值对并打印它。它和plain打印出来的顺序一样吗?

11.7 创建一个名为dict_of_listsdefaultdict,并传递list作为参数。创建列表dict_of_lists['a']并通过一次赋值将值'something for a'附加到其中。打印dict_of_lists['a']

12. 数据处理与管理

12.1 创建一个名为mystery的 Unicode 字符串,并将其赋值为'\U0001f984'。打印mystery及其 Unicode 名称。

12.2 使用 UTF-8 对mystery进行编码,将结果存入名为popbytesbytes变量中。打印pop_bytes

12.3 使用 UTF-8 解码popbytes,将结果存入名为pop_string的字符串变量中。打印pop_stringpop_string等于mystery吗?

12.4 当你处理文本时,正则表达式非常方便。我们将以多种方式应用它们到我们特选的文本样本中。这是一首名为《关于庞大奶酪的颂歌》的诗,由詹姆斯·麦金泰尔在 1866 年创作,以向安大略州制造的一块七千磅重的奶酪致敬,并送往国际巡回演出。如果你不想全部输入,可以使用你喜欢的搜索引擎复制并粘贴这些词到你的 Python 程序中,或者直接从Project Gutenberg中获取。将文本字符串命名为 mammoth

12.5 导入re模块以使用 Python 的正则表达式功能。使用re.findall()打印所有以c开头的单词。

12.6 查找所有以c开头且长度为四个字母的单词。

12.7 找出所有以r结尾的单词。

12.8 找出所有包含恰好三个连续元音字母的单词。

12.9 使用unhexlify()将这个十六进制字符串(由两个字符串组合以适应一页)转换为名为gifbytes变量:

'47494638396101000100800000000000ffffff21f9' +
'0401000000002c000000000100010000020144003b'

12.10 gif中的字节定义了一个像素的透明 GIF 文件,这是最常见的图形文件格式之一。一个合法的 GIF 以字符串GIF89a开始。gif是否匹配这个?

12.11 GIF 的像素宽度是从字节偏移 6 开始的 16 位小端整数,高度也是同样大小,从偏移 8 开始。提取并打印这些值到变量gif中。它们都是1吗?

13. 日历与时钟

13.1 将当前日期作为字符串写入名为today.txt的文本文件。

13.2 将名为today.txt的文本文件读入字符串today_string

13.3 从today_string中解析日期。

13.4 创建一个你出生日期的日期对象。

13.5 你出生的那一天是星期几?

13.6 你什么时候会(或者你何时)满 10000 天?

14. 文件与目录

14.1 列出当前目录中的文件。

14.2 列出你的父目录中的文件。

14.3 将字符串'This is a test of the emergency text system'赋给变量test1,并将test1写入名为test.txt的文件。

14.4 打开名为test.txt的文件并将其内容读入字符串test2test1test2相同吗?

15. 时间中的数据:进程与并发

15.1 使用multiprocessing创建三个单独的进程。每个进程在 0 到 1 秒之间等待一个随机数,打印当前时间,然后退出。

16. 数据盒子:持久存储

16.1 将以下文本行保存到名为books.csv的文件中(注意,如果字段用逗号分隔,如果包含逗号,则需要用引号括起来):

author,book
J R R Tolkien,The Hobbit
Lynne Truss,"Eats, Shoots & Leaves"

16.2 使用csv模块及其DictReader方法将books.csv读取到变量books中。打印books的值。DictReader处理了第二本书标题中的引号和逗号吗?

16.3 通过以下行创建名为books2.csv的 CSV 文件:

title,author,year
The Weirdstone of Brisingamen,Alan Garner,1960
Perdido Street Station,China Miéville,2000
Thud!,Terry Pratchett,2005
The Spellman Files,Lisa Lutz,2007
Small Gods,Terry Pratchett,1992

16.4 使用sqlite3模块创建一个名为books.db的 SQLite 数据库,并创建一个名为books的表,包含以下字段:title(文本)、author(文本)和year(整数)。

16.5 读取books2.csv并将其数据插入book表中。

16.6 按字母顺序选择并打印book表中的title列。

16.7 按出版顺序选择并打印book表中的所有列。

16.8 使用sqlalchemy模块连接到您刚刚在练习 8.6 中创建的 sqlite3 数据库books.db。像 8.8 中一样,按字母顺序选择并打印book表中的title列。

16.9 在您的机器上安装 Redis 服务器(参见附录 B)和 Python 的redis库(pip install redis)。创建一个名为test的 Redis 哈希,具有字段count1)和name'Fester Bestertester')。打印test的所有字段。

16.10 增加testcount字段并打印它。

17. 空间数据:网络

17.1 使用普通的socket实现一个当前时间服务。当客户端向服务器发送字符串'time'时,返回当前日期和时间的 ISO 字符串。

17.2 使用 ZeroMQ 的 REQ 和 REP 套接字来执行相同的操作。

17.3. 尝试使用 XMLRPC 做同样的事情。

17.4 你可能看过经典的I Love Lucy电视剧集,其中 Lucy 和 Ethel 在巧克力工厂工作。随着供应他们加工的糖果的传送带速度越来越快,二人开始落后。编写一个模拟程序,将不同类型的巧克力推送到 Redis 列表中,Lucy 作为客户端进行阻塞弹出这个列表。她需要 0.5 秒处理一块巧克力。打印每块巧克力到达 Lucy 手中的时间和类型,以及剩余待处理的数量。

17.5 使用 ZeroMQ 发布从练习 12.4(来自示例 12-1)的诗歌,逐字发布。编写一个 ZeroMQ 消费者,打印以元音字母开头的每个单词,以及包含五个字母的每个单词。忽略标点符号字符。

18. 解析 Web

18.1 如果您还没有安装flask,请立即安装。这也将安装werkzeugjinja2和可能其他包。

18.2 使用 Flask 的调试/重载开发 Web 服务器构建一个骨架网站。确保服务器在默认端口5000上启动,主机名为localhost。如果您的机器已经在使用端口 5000 做其他事情,请使用其他端口号。

18.3 添加一个home()函数来处理对主页的请求。设置它返回字符串It's alive!

18.4 创建一个名为home.html的 Jinja2 模板文件,内容如下:

I'm of course referring to {{thing}},
which is {{height}} feet tall and {{color}}.

创建一个名为templates的目录,并创建文件home.html,其内容如上所示。如果您之前的 Flask 服务器仍在运行,它将检测到新内容并重新启动自身。

18.5 修改您服务器的home()函数以使用home.html模板。为其提供三个GET参数:thingheightcolor

19. 成为 Pythonista

(Pythonistas 今天没有作业。)

20. Py 艺术

20.1 安装matplotlib。绘制这些(x, y)对的散点图:( (0, 0), (3, 5), (6, 2), (9, 8), (14, 10) )

20.2 绘制相同数据的折线图。

20.3 绘制相同数据的图表(带有标记的折线图)。

21. Py at Work

21.1 安装geopandas并运行 示例 21-1。尝试修改颜色和标记大小。

22. PySci

22.1 安装 Pandas。获取 示例 16-1 中的 CSV 文件。运行 示例 16-2 中的程序。尝试使用一些 Pandas 命令。

附录 E. 快速参考表

我发现自己经常查阅某些内容。这里有些希望您会发现有用的表格。

运算符优先级

此表格是 Python 3 中运算符优先级的官方文档改编版,优先级最高的运算符位于顶部。

运算符 描述和示例
[*`v`*, …], {*`v1`*, …}, {*`k1`*:*`v1`*, …}, (…) 列表/集合/字典/生成器的创建或推导,括号表达式
*`seq`*[*`n`*], *`seq`*[*`n`*:*`m`*], *`func`*(*`args`*…), *`obj`*.*`attr`* 索引、切片、函数调用、属性引用
** 指数运算
+n, n, ~n 正数、负数、位运算 not
*, /, //, % 乘法、浮点除法、整数除法、取余
+, - 加法、减法
<<, >> 位左移、右移
& 位运算 and
&#124; 位运算 or
in, not in, is, is not, <, <=, >, >=, !=, == 成员关系和相等性测试
not x 布尔(逻辑)not
and 布尔 and
or 布尔 or
ifelse 条件表达式
lambda lambda 表达式

字符串方法

Python 提供了字符串 方法(可用于任何 str 对象)和一个 string 模块,其中包含一些有用的定义。让我们使用这些测试变量:

>>> s = "OH, my paws and whiskers!"
>>> t = "I'm late!"

在以下示例中,Python shell 打印方法调用的结果,但原始变量 st 并未更改。

更改大小写

>>> s.capitalize()
'Oh, my paws and whiskers!'
>>> s.lower()
'oh, my paws and whiskers!'
>>> s.swapcase()
'oh, MY PAWS AND WHISKERS!'
>>> s.title()
'Oh, My Paws And Whiskers!'
>>> s.upper()
'OH, MY PAWS AND WHISKERS!'

搜索

>>> s.count('w')
2
>>> s.find('w')
9
>>> s.index('w')
9
>>> s.rfind('w')
16
>>> s.rindex('w')
16
>>> s.startswith('OH')
True

修改

>>> ''.join(s)
'OH, my paws and whiskers!'
>>> ' '.join(s)
'O H ,   m y   p a w s   a n d   w h i s k e r s !'
>>> ' '.join((s, t))
"OH, my paws and whiskers! I'm late!"
>>> s.lstrip('HO')
', my paws and whiskers!'
>>> s.replace('H', 'MG')
'OMG, my paws and whiskers!'
>>> s.rsplit()
['OH,', 'my', 'paws', 'and', 'whiskers!']
>>> s.rsplit(' ', 1)
['OH, my paws and', 'whiskers!']
>>> s.split(' ', 1)
['OH,', 'my paws and whiskers!']
>>> s.split(' ')
['OH,', 'my', 'paws', 'and', 'whiskers!']
>>> s.splitlines()
['OH, my paws and whiskers!']
>>> s.strip()
'OH, my paws and whiskers!'
>>> s.strip('s!')
'OH, my paws and whisker'

格式

>>> s.center(30)
'  OH, my paws and whiskers!   '
>>> s.expandtabs()
'OH, my paws and whiskers!'
>>> s.ljust(30)
'OH, my paws and whiskers!     '
>>> s.rjust(30)
'     OH, my paws and whiskers!'

字符串类型

>>> s.isalnum()
False
>>> s.isalpha()
False
>>> s.isprintable()
True
>>> s.istitle()
False
>>> s.isupper()
False
>>> s.isdecimal()
False
>>> s.isnumeric()
False

字符串模块属性

这些是用作常量定义的类属性。

| 属性 | 示例 |
| --- | --- |
| ascii_letters | 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' |
| ascii_lowercase | 'abcdefghijklmnopqrstuvwxyz' |
| ascii_uppercase | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' |
| digits | '0123456789' |
| hexdigits | '0123456789abcdefABCDEF' |
| octdigits | '01234567' |
| punctuation | '!"#$%&\'()*+,-./:;<=>?@[\\]^_\{|}~'| |printable| digits + ascii_letters + punctuation + whitespace | |whitespace|' \t\n\r\x0b\x0c'` |

结尾

切斯特希望表达对你勤奋工作的赞赏。如果你需要他,他正在午休中…

inp2 ae01

图 E-1. 切斯特¹

但露西可以回答任何问题。

inp2 ae02

图 E-2. 露西

¹ 自 图 3-1 以来,他向右移动了大约一英尺。

posted @ 2024-06-17 19:07  绝不原创的飞龙  阅读(9)  评论(0编辑  收藏  举报