Python 入门指南(一)

原文:zh.annas-archive.org/md5/97bc15629f1b51a0671040c56db61b92

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

这个学习路径帮助你在 Python 的世界中感到舒适。它从对 Python 的全面和实用的介绍开始。你将很快开始在学习路径的第一部分编写程序。借助链表、二分搜索和排序算法的力量,你将轻松地创建复杂的数据结构,如图、栈和队列。在理解了协同继承之后,你将熟练地引发、处理和操纵异常。你将轻松地整合 Python 的面向对象和非面向对象的方面,并使用更高级的设计模式创建可维护的应用程序。一旦你掌握了核心主题,你将理解单元测试的乐趣,以及创建单元测试有多么容易。

通过这个学习路径,你将构建易于理解、调试并可用于不同应用的组件。

这个学习路径包括以下 Packt 产品的内容:

  • 《学习 Python 编程-第二版》作者:法布里齐奥·罗马诺

  • 《Python 数据结构和算法》作者:本杰明·巴卡

  • 《Python 3 面向对象编程》作者:达斯蒂·菲利普斯

这本书适合谁

如果你相对新手,并希望使用 Python 编写脚本或程序来完成任务,或者如果你是其他语言的面向对象程序员,并希望在 Python 的世界中有所提升,那么这个学习路径适合你。虽然不是必需的,但具有编程和面向对象编程的基本知识会对你有所帮助。

这本书涵盖了什么

第一章《Python 的初步介绍》向你介绍了基本的编程概念。它指导你如何在计算机上运行 Python,并向你介绍了一些构造。

第二章《内置数据类型》向你介绍了 Python 的内置数据类型。Python 拥有非常丰富的本地数据类型,本章将为你介绍每种类型的描述和简短示例。

第三章《迭代和决策》教你如何通过检查条件、应用逻辑和执行循环来控制代码的流程。

第四章《函数,代码的构建块》教你如何编写函数。函数是重用代码、减少调试时间以及编写更好代码的关键。

第五章《文件和数据持久性》教你如何处理文件、流、数据交换格式和数据库等内容。

第六章《算法设计原则》涵盖了如何使用现有的 Python 数据结构构建具有特定功能的结构。一般来说,我们创建的数据结构需要符合一些原则。这些原则包括健壮性、适应性、可重用性和将结构与功能分离。我们将探讨迭代的作用,并介绍递归数据结构。

第七章《列表和指针结构》涵盖了链表,这是最常见的数据结构之一,通常用于实现其他结构,如栈和队列。在本章中,我们描述了它们的操作和实现。我们比较它们与数组的行为,并讨论了各自的相对优势和劣势。

第八章,“栈和队列”,讨论了这些线性数据结构的行为,并演示了一些实现。我们给出了典型应用的例子。

第九章,“树”,将讨论如何实现二叉树。树是许多最重要的高级数据结构的基础。我们将研究如何遍历树、检索和插入值。我们还将看看如何创建堆等结构。

第十章,“哈希和符号表”,描述了符号表,给出了一些典型的实现,并讨论了各种应用。我们将研究哈希的过程,给出哈希表的实现,并讨论各种设计考虑因素。

第十一章,“图和其他算法”,介绍了一些更专业的结构,包括图和空间结构。将数据表示为一组节点和顶点在许多应用中很方便,从中我们可以创建结构,如有向图和无向图。我们还将介绍一些其他结构和概念,如优先队列、堆和选择算法。

第十二章,“搜索”,讨论了最常见的搜索算法,并举例说明它们在各种数据结构中的使用。搜索数据结构是一项基本任务,有许多方法。

第十三章,“排序”,探讨了最常见的排序方法。这将包括冒泡排序、插入排序和选择排序。

第十四章,“选择算法”,涵盖了涉及查找统计数据的算法,例如列表中的最小值、最大值或中位数元素。有许多方法,其中最常见的方法之一是首先应用排序操作。其他方法包括分区和线性选择。

第十五章,“面向对象设计”,涵盖了重要的面向对象概念。主要涉及抽象、类、封装和继承等术语。我们还简要介绍了使用 UML 来建模我们的类和对象。

第十六章,“Python 中的对象”,讨论了在 Python 中使用的类和对象。我们将了解 Python 对象的属性和行为,以及类组织成包和模块。最后,我们将看到如何保护我们的数据。

第十七章,“当对象相似时”,更深入地介绍了继承。它涵盖了多重继承,并向我们展示了如何扩展内置。本章还涵盖了多态性和鸭子类型在 Python 中的工作原理。

第十八章,“意料之外”,深入研究了异常和异常处理。我们将学习如何创建自己的异常,以及如何利用异常来控制程序流程。

第十九章,“何时使用面向对象编程”,涉及创建和使用对象。我们将看到如何使用属性包装数据并限制数据访问。本章还讨论了 DRY 原则以及如何避免重复代码。

第二十章,“Python 面向对象的快捷方式”,顾名思义,涉及 Python 中的节省时间的方法。我们将研究许多有用的内置函数,例如使用默认参数进行方法重载。我们还将看到函数本身是对象,以及这如何有用。

第二十一章,迭代器模式,介绍了设计模式的概念,并涵盖了 Python 对迭代器模式的标志性实现。我们将学习列表、集合和字典推导式。我们还将揭开生成器和协程的神秘面纱。

第二十二章,Python 设计模式 I,涵盖了几种设计模式,包括装饰器、观察者、策略、状态、单例和模板模式。每种模式都有适当的示例和 Python 中的程序实现。

第二十三章,Python 设计模式 II,以适配器、外观、享元、命令、抽象和组合模式结束了我们对设计模式的讨论。提供了更多关于惯用 Python 代码与规范实现的示例。

第二十四章,测试面向对象的程序,从为什么在 Python 应用程序中进行测试如此重要开始。它专注于测试驱动开发,并介绍了两种不同的测试套件:unittest 和 py.test。最后,它讨论了模拟测试对象和代码覆盖。

为了充分利用本书

本书中的代码将需要您运行 Python 2.7.x 或更高版本。Python 的默认交互环境也可以用于运行代码片段。

本书中的一些示例依赖于不随 Python 一起发布的第三方库。它们在使用时会在书中介绍,因此您不需要提前安装它们。

下载示例代码文件

您可以从www.packt.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,可以访问www.packt.com/support并注册,文件将直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. 登录或注册www.packt.com

  2. 选择“支持”选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

下载文件后,请确保使用以下最新版本解压或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Getting-Started-with-Python。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有其他代码包,来自我们丰富的图书和视频目录,可在github.com/PacktPublishing/上查看!

使用的约定

本书中使用了一些文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。例如:"ifelseelif语句控制语句的条件执行。"

代码块设置如下:

a=10; b=20
def my_function(): 

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:

if "WARNING" in l:
 **yield l.replace("\tWARNING", "")**

任何命令行输入或输出都是这样写的:

>>> print(warnings_filter([])) 

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。例如:"然后,如果标签与颜色匹配,您必须手动点击是或否。"

警告或重要说明会这样显示。提示和技巧会这样显示。

第一章:Python 的初步介绍

“授人以鱼不如授人以渔。”- 中国谚语

根据维基百科的说法,计算机编程是:

“...从计算问题的原始表述到可执行的计算机程序的过程。编程涉及活动,如分析,开发理解,生成算法,验证算法的要求,包括它们的正确性和资源消耗,以及在目标编程语言中实现(通常称为编码)算法。”

简而言之,编码就是用计算机能理解的语言告诉计算机做某事。

计算机是非常强大的工具,但不幸的是,它们无法自行思考。它们需要被告知一切:如何执行任务,如何评估条件以决定要遵循哪条路径,如何处理来自设备的数据,比如网络或磁盘,以及在发生意外情况时如何做出反应,比如,某物坏了或丢失了。

你可以用许多不同的风格和语言编写代码。难吗?我会说不是。这有点像写作。每个人都可以学会写作,你也可以。但是,如果你想成为一名诗人呢?那么仅仅写作是不够的。你必须获得一整套其他技能,这需要更长时间和更大的努力。

最终,一切都取决于你想走多远。编码不仅仅是将一些有效的指令组合在一起。它远不止如此!

良好的代码是简短、快速、优雅、易于阅读和理解、简单、易于修改和扩展、易于扩展和重构、易于测试。要能够同时具备所有这些品质的代码需要时间,但好消息是,通过阅读这本书,你正在迈出迈向这个目标的第一步。我毫无疑问你能做到。任何人都可以;事实上,我们都在不知不觉中一直在编程。

你想要一个例子吗?

假设你想泡速溶咖啡。你需要准备一个杯子,速溶咖啡罐,一把茶匙,水和水壶。即使你没有意识到,你正在评估大量的数据。你要确保水壶里有水,水壶已插好电,杯子干净,并且咖啡罐里有足够的咖啡。然后,你烧开水,也许在此期间,你把一些咖啡放进杯子里。当水准备好时,你把它倒进杯子里,然后搅拌。

那么,这就是编程吗?

好吧,我们收集了资源(水壶,咖啡,水,茶匙和杯子),并验证了一些关于它们的条件(水壶已插好电,杯子干净,并且有足够的咖啡)。然后我们开始了两个动作(烧水和把咖啡放进杯子里),当它们都完成时,我们最终通过把水倒进杯子里并搅拌来结束了这个过程。

你能看到吗?我刚刚描述了一个咖啡程序的高级功能。这并不难,因为这就是大脑整天在做的事情:评估条件,决定采取行动,执行任务,重复其中一些,并在某个时候停下来。清理物品,把它们放回去,等等。

现在你需要学会如何分解你在现实生活中自动完成的所有这些动作,以便计算机实际上能够理解它们。而且你还需要学习一种语言,来指导它。

所以这本书就是为此而写的。我会告诉你如何做,我会尝试通过许多简单但专注的例子来做到这一点(这是我最喜欢的一种)。

在这一章中,我们将涵盖以下内容:

  • Python 的特点和生态系统

  • 关于如何开始并运行 Python 和虚拟环境的指南

  • 如何运行 Python 程序

  • 如何组织 Python 代码和 Python 的执行模型

一个合适的介绍

我喜欢在教编码时引用现实世界;我相信这有助于人们更好地理解概念。然而,现在是时候更严谨地从技术角度看待编码是什么了。

当我们编写代码时,我们在指示计算机要做的事情。行动发生在哪里?在很多地方:计算机内存、硬盘、网络电缆、CPU 等等。这是一个完整的世界,大部分时间是真实世界的一个子集的表示。

如果你写了一个允许人们在线购买服装的软件,你将不得不在程序的范围内代表真实的人、真实的服装、真实的品牌、尺寸等等。

为了做到这一点,你需要在你编写的程序中创建和处理对象。一个人可以是一个对象。一辆车是一个对象。一双袜子是一个对象。幸运的是,Python 非常了解对象。

任何对象具有的两个主要特征是属性和方法。让我们以一个人的对象为例。在计算机程序中,你通常将人表示为顾客或员工。你存储在他们身上的属性是姓名、社会安全号码、年龄、是否有驾照、电子邮件、性别等等。在计算机程序中,你存储了所有你需要的数据,以便使用对象来服务你的目的。如果你正在编写一个销售服装的网站,你可能还想存储顾客的身高、体重以及其他尺寸,以便为他们推荐合适的服装。因此,属性是对象的特征。我们经常使用它们:你能把那支笔递给我吗?哪一支?黑色的那支。在这里,我们使用了笔的黑色属性来识别它(很可能是在蓝色和红色中)。

方法是对象可以做的事情。作为一个人,我有诸如说话走路睡觉醒来吃饭做梦等方法。我能做的所有事情都可以看作是代表我的对象的方法。

现在你知道了对象是什么,它们公开了可以运行的方法和可以检查的属性,你已经准备好开始编码了。实际上,编码就是简单地管理我们在软件中再现的世界子集中的那些对象。你可以随意创建、使用、重用和删除对象。

根据官方 Python 文档的数据模型章节(docs.python.org/3/reference/datamodel.html):

“对象是 Python 对数据的抽象。Python 程序中的所有数据都由对象或对象之间的关系表示。”

我们将在后面的章节中更仔细地看 Python 对象。目前,我们需要知道的是 Python 中的每个对象都有一个 ID(或身份)、一个类型和一个值。

一旦创建,对象的 ID 就永远不会改变。这是它的唯一标识符,并且在幕后被 Python 用来在我们想要使用它时检索对象。

类型也永远不会改变。类型告诉对象支持哪些操作以及可以分配给它的可能值。

我们将在第二章中看到 Python 最重要的数据类型,内置数据类型

值可以改变,也可以不改变。如果可以改变,对象被称为可变,而当它不能改变时,对象被称为不可变

我们如何使用一个对象?当然是给它一个名字!当你给一个对象一个名字,然后你可以使用这个名字来检索对象并使用它。

在更一般的意义上,诸如数字、字符串(文本)、集合等对象都与一个名称相关联。通常,我们说这个名称是变量的名称。你可以把变量看作是一个盒子,你可以用它来存储数据。

所以,你拥有了所有你需要的对象;现在呢?好吧,我们需要使用它们,对吧?我们可能想要通过网络连接发送它们,或者将它们存储在数据库中。也许在网页上显示它们,或者将它们写入文件。为了做到这一点,我们需要对用户填写表单、按下按钮、打开网页并执行搜索做出反应。我们通过运行我们的代码来做出反应,评估条件以选择执行哪些部分,多少次,以及在什么情况下。

为了做到所有这些,基本上我们需要一种语言。这就是 Python 的用途。在本书中,我们将一起使用 Python 来指导计算机为我们做一些事情。

现在,够了这些理论的东西;让我们开始吧。

进入 Python

Python 是 Guido Van Rossum 的奇迹创造,他是一位荷兰计算机科学家和数学家,决定在 1989 年圣诞节期间向世界赠送他正在玩耍的项目。这种语言大约在 1991 年左右出现在公众面前,从那时起,它已经发展成为全球范围内使用的主要编程语言之一。

我 7 岁开始编程,用的是 Commodore VIC-20,后来换成了它的大哥 Commodore 64。它的语言是 BASIC。后来,我接触了 Pascal、Assembly、C、C++、Java、JavaScript、Visual Basic、PHP、ASP、ASP .NET、C#,以及其他一些我甚至都记不起来的小语言,但直到我接触到 Python,我才有了那种在商店里找到合适的沙发时的感觉。当你的所有身体部位都在喊着,“买这个!这个对我们来说完美!”

我大约花了一天时间适应它。它的语法与我习惯的有点不同,但在克服了最初的不适感之后(就像穿上新鞋一样),我就深深地爱上了它。让我们看看为什么。

关于 Python

在我们深入了解细节之前,让我们了解一下为什么有人会想要使用 Python(我建议你阅读维基百科上的 Python 页面,以获得更详细的介绍)。

在我看来,Python 体现了以下特质。

可移植性

Python 可以在任何地方运行,将程序从 Linux 移植到 Windows 或 Mac 通常只是修复路径和设置的问题。Python 被设计用于可移植性,并且它会处理特定操作系统(OS)的怪癖,这些接口会让你免于编写针对特定平台的代码的痛苦。

连贯性

Python 非常逻辑和连贯。你可以看出它是由一位杰出的计算机科学家设计的。大多数时候,如果你不知道一个方法的调用方式,你可以猜出来。

你现在可能没有意识到这一点有多重要,特别是如果你还处在起步阶段,但这是一个重要的特性。这意味着你的头脑中没有那么多杂乱,也不需要在编码时浏览文档,也不需要在大脑中进行映射。

开发者生产力

根据 Mark Lutz(《学习 Python,第 5 版》,O'Reilly Media)的说法,Python 程序通常只有等效的 Java 或 C++代码的五分之一到三分之一大小。这意味着工作可以更快地完成。更快是好的。更快意味着市场上更快的反应。更少的代码不仅意味着写的代码更少,而且意味着阅读的代码更少(专业程序员阅读的比写的要多得多),维护的代码更少,调试的代码更少,重构的代码更少。

另一个重要的方面是,Python 可以在不需要冗长和耗时的编译和链接步骤的情况下运行,因此你不必等待看到你的工作成果。

一个广泛的库

Python 拥有一个非常广泛的标准库(据说它是带有内置电池)。如果这还不够,全世界的 Python 社区维护着一系列第三方库,专门针对特定需求定制,你可以在Python Package IndexPyPI)上自由获取。当你编写 Python 代码时,你意识到你需要某个特定功能时,在大多数情况下,至少有一个库已经为你实现了该功能。

软件质量

Python 非常注重可读性、连贯性和质量。语言的统一性使得它具有很高的可读性,这在当今编码更多是集体努力而不是个人努力的情况下至关重要。Python 的另一个重要方面是其固有的多范式特性。你可以将其用作脚本语言,但也可以利用面向对象、命令式和函数式编程风格。它是多才多艺的。

软件集成

另一个重要的方面是 Python 可以扩展和集成许多其他语言,这意味着即使一家公司使用不同的语言作为他们的主流工具,Python 也可以作为一个粘合剂在某种程度上连接需要相互通信的复杂应用程序。这是一个比较高级的话题,但在现实世界中,这个特性非常重要。

满意和享受

最后但并非最不重要的是,这很有趣!使用 Python 很有趣。我可以编写 8 小时的代码,离开办公室时感到快乐和满足,对于其他使用不提供同样数量精心设计的数据结构和构造的编码人员所遭受的挣扎毫不知情。毫无疑问,Python 让编码变得有趣。而有趣则促进了动力和生产力。

有什么缺点吗?

也许,人们在 Python 中唯一可能找到的缺点(不是由于个人偏好)是它的执行速度。通常情况下,Python 比它的编译兄弟慢。Python 的标准实现在运行应用程序时会生成源代码的编译版本,称为字节码(扩展名为.pyc),然后由 Python 解释器运行。这种方法的优势是可移植性,但由于 Python 没有像其他语言那样编译到机器级别,我们需要付出减速的代价。

然而,Python 的速度在今天很少是一个问题,因此它被广泛使用,尽管有这个次优特性。在现实生活中,硬件成本不再是一个问题,通常很容易通过并行化任务来提高速度。此外,许多程序花费大部分时间等待 IO 操作完成;因此,原始执行速度通常是整体性能的次要因素。不过,当涉及到大量计算时,人们可以切换到更快的 Python 实现,比如 PyPy,通过实现先进的编译技术,它提供了平均五倍的加速(参考pypy.org/)。

在进行数据科学时,你很可能会发现,你使用的 Python 库,如PandasNumPy,由于它们的实现方式,实现了本地速度。

如果这还不足以说服你,你可以考虑 Python 已被用于驱动 Spotify 和 Instagram 等服务的后端,其中性能是一个问题。尽管如此,Python 已经完全胜任了它的工作。

今天谁在使用 Python?

还不够说服?让我们简要看一下今天正在使用 Python 的公司:谷歌、YouTube、Dropbox、雅虎、Zope 公司、工业光与魔法、华特迪士尼特效动画、Blender 3D、皮克斯、NASA、NSA、红帽、诺基亚、IBM、Netflix、Yelp、英特尔、思科、惠普、高通和摩根大通,仅举几例。

甚至像战地 2文明 4QuArK这样的游戏都是用 Python 实现的。

Python 在许多不同的环境中使用,如系统编程、Web 编程、GUI 应用程序、游戏和机器人技术、快速原型设计、系统集成、数据科学、数据库应用等。一些知名的大学也已经将 Python 作为他们计算机科学课程的主要语言。

设置环境

在谈论如何在你的系统上安装 Python 之前,让我告诉你我在本书中将使用的 Python 版本。

Python 2 与 Python 3

Python 有两个主要版本:Python 2 是过去,Python 3 是现在。这两个版本虽然非常相似,但在某些方面是不兼容的。

在现实世界中,Python 2 实际上离过去还相当遥远。简而言之,尽管 Python 3 自 2008 年以来就已经发布,但从版本 2 过渡到版本 3 的阶段仍然远未结束。这主要是因为 Python 2 在工业界被广泛使用,当然,公司并不急于仅仅为了更新而更新他们的系统,遵循“如果它没坏,就不要修理”的理念。你可以在网络上阅读关于这两个版本之间过渡的所有信息。

另一个妨碍过渡的问题是第三方库的可用性。通常,Python 项目依赖于数十个外部库,当你开始一个新项目时,你需要确保已经有一个兼容 Version-3 的库来满足任何可能出现的业务需求。如果不是这样,那么在 Python 3 中启动一个全新的项目意味着引入潜在的风险,而许多公司并不愿意冒这种风险。

在撰写本文时,大多数最广泛使用的库已经移植到 Python 3,并且对于大多数情况来说,在 Python 3 中启动项目是相当安全的。许多库已经重写,以便与两个版本兼容,主要利用了six库的功能(名称来源于 2 x 3 的乘法,因为从版本 2 到 3 的移植),它有助于根据使用的版本进行内省和行为调整。根据 PEP 373(legacy.python.org/dev/peps/pep-0373/),Python 2.7 的生命周期结束EOL)已经设定为 2020 年,不会有 Python 2.8,因此对于在 Python 2 中运行项目的公司来说,现在是需要开始制定升级策略并在太迟之前转移到 Python 3 的时候了。

在我的电脑上(MacBook Pro),这是我拥有的最新 Python 版本:

>>> import sys
>>> print(sys.version)
3.7.0a3 (default, Jan 27 2018, 00:46:45)
[Clang 9.0.0 (clang-900.0.39.2)]

因此,你可以看到这个版本是 Python 3.7 的 alpha 版本,将于 2018 年 6 月发布。前面的文本是我在控制台中输入的一小段 Python 代码。我们稍后会谈论它。

本书中的所有示例都将使用 Python 3.7 运行。尽管目前最终版本可能与我所拥有的略有不同,但我会确保所有的代码和示例在书籍出版时都是最新的 3.7 版本。

一些代码也可以在 Python 2.7 中运行,要么就是原样,要么稍作调整,但在这个时候,我认为最好先学习 Python 3,然后再了解它与 Python 2 的区别,而不是反过来。

不过,不要担心这个版本的问题;实际上并不是那么大的问题。

安装 Python

我从来没有真正理解为什么书中需要有一个设置部分,不管你需要设置什么。大多数情况下,作者写下指示的时间和你实际尝试它们的时间之间已经过去了几个月。如果你幸运的话。一旦版本发生变化,书中描述的方法可能就不再奏效了。幸运的是,现在我们有了网络,所以为了帮助你启动和运行,我只会给你一些指引和目标。

我知道大多数读者可能更喜欢在书中获得指导。但我怀疑这是否会让他们的生活变得更容易,因为我坚信,如果你想开始学习 Python,你必须付出最初的努力,以熟悉这个生态系统。这非常重要,它将增强你面对接下来章节中的材料的信心。如果遇到困难,请记住,谷歌是你的朋友。

设置 Python 解释器

首先,让我们谈谈你的操作系统。Python 已经完全集成,并且很可能已经安装在几乎每个 Linux 发行版中。如果你使用 macOS,很可能 Python 也已经安装好了(但可能只有 Python 2.7),而如果你使用 Windows,你可能需要安装它。

获取 Python 和你需要的库并使其运行需要一些技巧。Linux 和 macOS 似乎是最适合 Python 程序员的用户友好操作系统;而 Windows 则需要更大的努力。

我的当前系统是 MacBook Pro,这也是我在整本书中将使用的系统,搭配 Python 3.7。

你要开始的地方是官方 Python 网站:www.python.org。这个网站托管了官方 Python 文档和许多其他资源,你会发现非常有用。花点时间去探索一下。

另一个关于 Python 及其生态系统的优秀、富有资源的网站是docs.python-guide.org。你可以找到使用不同方法在不同操作系统上设置 Python 的说明。

找到下载部分,选择适合你操作系统的安装程序。如果你使用 Windows,在运行安装程序时,请确保勾选“安装 pip”选项(实际上,我建议进行完整安装,以确保安装程序包含的所有组件都安装了)。我们稍后会讨论pip

现在 Python 已经安装在你的系统中,目标是能够打开控制台并通过键入python运行 Python 交互式 shell。

请注意,我通常简称Python 交互式 shellPython 控制台

在 Windows 中打开控制台,转到“开始”菜单,选择“运行”,然后键入cmd。如果在本书的示例中遇到类似权限问题的情况,请确保以管理员权限运行控制台。

在 macOS X 上,你可以通过转到“应用程序”|“实用工具”|“终端”来启动终端。

如果你使用 Linux,你对控制台的了解就足够了。

我将使用术语控制台来交替指代 Linux 控制台、Windows 命令提示符和 Macintosh 终端。我还将用 Linux 默认格式指示命令行提示符,就像这样:

$ sudo apt-get update

如果你对此不熟悉,请花些时间学习控制台的基础知识。简而言之,在$符号后,通常会有一个你需要输入的指令。注意大小写和空格,它们非常重要。

无论你打开哪个控制台,都在提示符处键入python,确保 Python 交互式 shell 显示出来。键入exit()退出。请记住,如果你的操作系统预装了 Python 2.*,可能需要指定python3

这大致是你运行 Python 时应该看到的情况(根据版本和操作系统的不同,细节可能会有所变化):

$ python3.7
Python 3.7.0a3 (default, Jan 27 2018, 00:46:45)
[Clang 9.0.0 (clang-900.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

现在 Python 已经设置好,你可以运行它了,是时候确保你还有另一个工具,这个工具将是跟随本书示例不可或缺的:virtualenv。

关于 virtualenv

正如你可能已经猜到的那样,virtualenv关乎虚拟环境。让我通过一个简单的例子来解释它们是什么,为什么我们需要它们。

你在系统上安装了 Python,并开始为 X 客户工作。你创建了一个项目文件夹并开始编码。在此过程中,你还安装了一些库,例如 Django 框架。假设你为 X 项目安装的 Django 版本是 1.7.1。

现在,你的网站做得很好,你又得到了另一个客户 Y。她希望你为她建立另一个网站,所以你开始了 Y 项目,但在此过程中,你需要再次安装 Django。唯一的问题是现在 Django 的版本是 1.8,你不能在系统上安装它,因为这会替换你为 X 项目安装的版本。你不想冒险引入不兼容的问题,所以你有两个选择:要么你继续使用你当前机器上的版本,要么你升级它,并确保第一个项目仍然能够正确地使用新版本。

说实话,这些选项都不是很吸引人,对吧?绝对不是。所以,这里有解决方案:virtualenv!

virtualenv 是一个允许你创建虚拟环境的工具。换句话说,它是一个用于创建隔离的 Python 环境的工具,每个环境都是一个包含所有必要可执行文件的文件夹,以便使用 Python 项目所需的包(暂时将包视为库)。

所以你为 X 项目创建一个虚拟环境,安装所有依赖项,然后为 Y 项目创建一个虚拟环境,安装所有依赖项,而不用担心,因为你安装的每个库最终都会在适当的虚拟环境范围内。在我们的例子中,X 项目将使用 Django 1.7.1,而 Y 项目将使用 Django 1.8。

绝对不能直接在系统级别安装库非常重要。例如,Linux 依赖于 Python 执行许多不同的任务和操作,如果你在系统安装的 Python 上进行操作,就会有破坏整个系统完整性的风险(猜猜这是发生在谁身上的...)。所以把这当作一条规则,就像睡前刷牙一样重要:每次开始新项目时,一定要创建虚拟环境

要在系统上安装 virtualenv,有几种不同的方法。例如,在基于 Debian 的 Linux 发行版上,你可以使用以下命令安装:

$ sudo apt-get install python-virtualenv

可能最简单的方法是按照 virtualenv 官方网站上的说明进行操作:virtualenv.pypa.io

你会发现,安装 virtualenv 最常见的方法之一是使用pip,这是一个用于安装和管理用 Python 编写的软件包的软件包管理系统。

从 Python 3.5 开始,创建虚拟环境的建议方式是使用venv模块。更多信息请参阅官方文档。然而,在撰写本文时,virtualenv 仍然是创建虚拟环境最常用的工具。

你的第一个虚拟环境

创建虚拟环境非常容易,但根据系统配置和想要虚拟环境运行的 Python 版本,你需要正确运行命令。另一件你需要做的事情是激活虚拟环境。激活虚拟环境基本上是在后台进行一些路径操作,这样当你调用 Python 解释器时,实际上是调用激活的虚拟环境,而不是系统的。

我将在我的 Macintosh 控制台上展示一个完整的示例。我们将:

  1. 在你的项目根目录下创建一个名为learn.pp的文件夹(在我的情况下是一个名为srv的文件夹,在我的主目录下)。请根据你在计算机上喜欢的设置调整路径。

  2. learn.pp文件夹中,我们将创建一个名为learnpp的虚拟环境。

一些开发人员更喜欢使用相同的名称来称呼所有虚拟环境(例如,.venv)。这样他们就可以通过知道项目所在的名称来运行脚本。在 Linux/macOS 中,以点开头的名称会使文件或文件夹变得不可见。

  1. 创建虚拟环境后,我们将激活它。在 Linux、macOS 和 Windows 之间的方法略有不同。

  2. 然后,我们将确保我们正在运行所需的 Python 版本(3.7.*),通过运行 Python 交互式 shell。

  3. 最后,我们将使用deactivate命令来停用虚拟环境。

这五个简单的步骤将向您展示开始和使用项目所需做的一切。

以下是这些步骤可能看起来的一个示例(请注意,根据您的操作系统、Python 版本等,可能会得到略有不同的结果)在 macOS 上(以#开头的命令是注释,空格是为了可读性引入的,表示由于空间不足而换行):

fabmp:srv fab$ # step 1 - create folder
fabmp:srv fab$ mkdir learn.pp
fabmp:srv fab$ cd learn.pp

fabmp:learn.pp fab$ # step 2 - create virtual environment
fabmp:learn.pp fab$ which python3.7
/Users/fab/.pyenv/shims/python3.7
fabmp:learn.pp fab$ virtualenv -p
⇢ /Users/fab/.pyenv/shims/python3.7 learnpp
Running virtualenv with interpreter /Users/fab/.pyenv/shims/python3.7
Using base prefix '/Users/fab/.pyenv/versions/3.7.0a3'
New python executable in /Users/fab/srv/learn.pp/learnpp/bin/python3.7
Also creating executable in /Users/fab/srv/learn.pp/learnpp/bin/python
Installing setuptools, pip, wheel...done.

fabmp:learn.pp fab$ # step 3 - activate virtual environment
fabmp:learn.pp fab$ source learnpp/bin/activate

(learnpp) fabmp:learn.pp fab$ # step 4 - verify which python
(learnpp) fabmp:learn.pp fab$ which python
/Users/fab/srv/learn.pp/learnpp/bin/python

(learnpp) fabmp:learn.pp fab$ python
Python 3.7.0a3 (default, Jan 27 2018, 00:46:45)
[Clang 9.0.0 (clang-900.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> exit()

(learnpp) fabmp:learn.pp fab$ # step 5 - deactivate
(learnpp) fabmp:learn.pp fab$ deactivate
fabmp:learn.pp fab$

请注意,我不得不明确告诉 virtualenv 使用 Python 3.7 解释器,因为在我的系统上 Python 2.7 是默认的。如果我没有这样做,我将得到一个带有 Python 2.7 而不是 Python 3.7 的虚拟环境。

你可以将第 2 步的两条指令合并成一条命令,就像这样:

$ virtualenv -p $( which python3.7 ) learnpp

在这种情况下,我选择明确详细,以帮助您理解每个步骤。

另一件需要注意的事情是,为了激活虚拟环境,我们需要运行/bin/activate脚本,这需要被源化。当脚本被源化时,意味着它在当前 shell 中执行,因此其效果会在执行后持续存在。这非常重要。还要注意,在我们激活虚拟环境后,提示符会发生变化,左边会显示其名称(在我们停用它时会消失)。在 Linux 上,步骤是相同的,所以我不会在这里重复。在 Windows 上,事情略有不同,但概念是相同的。请参考官方 virtualenv 网站获取指导。

在这一点上,您应该能够创建和激活一个虚拟环境。请尝试创建另一个虚拟环境,而无需我的指导。熟悉这个过程,因为这是您将始终在做的事情:我们永远不会在系统范围内使用 Python,记住吗?这非常重要。

所以,一旦搭建好,我们就准备好更多地谈谈 Python 以及如何使用它。在我们开始之前,让我稍微谈谈控制台。

你的朋友,控制台

在这个 GUI 和触摸屏设备的时代,当一切都只是一个点击之遥时,不得不求助于控制台这样的工具似乎有点荒谬。

但事实是,每当你把右手(或者如果你是左撇子,是左手)从键盘上移开,去抓鼠标并将光标移动到你想点击的位置时,你都会浪费时间。用控制台完成任务,尽管可能有些违反直觉,但会提高生产力和速度。我知道,你必须相信我。

速度和生产力很重要,个人而言,我对鼠标没有任何意见,但还有另一个非常好的理由,你可能希望熟悉控制台:当你开发的代码最终部署到服务器上时,控制台可能是唯一可用的工具。如果你和它交朋友,我向你保证,在最重要的时候你永远不会迷失方向(通常是在网站宕机时,你需要迅速调查发生了什么)。

所以这真的取决于你。如果你还没有决定,请给我一点怀疑的好处,试一试。这比你想象的要容易,你永远不会后悔。没有什么比一个优秀的开发者因为习惯了自己定制的工具而在 SSH 连接到服务器中迷失更可悲的了。

现在,让我们回到 Python。

如何运行 Python 程序

有几种不同的方法可以运行 Python 程序。

运行 Python 脚本

Python 可以用作脚本语言。事实上,它总是非常有用的。脚本是文件(通常很小),通常用来执行一些任务。许多开发人员最终都会拥有自己的工具库,当他们需要执行任务时就会使用。例如,你可以有脚本来解析数据格式并将其呈现为另一种不同的格式。或者你可以使用脚本来处理文件和文件夹。你可以创建或修改配置文件,等等。从技术上讲,几乎没有什么是不能在脚本中完成的。

在服务器上定时运行脚本是非常常见的。例如,如果你的网站数据库每 24 小时需要清理一次(例如,存储用户会话的表,这些会话很快就会过期,但不会自动清理),你可以设置一个 Cron 作业,每天凌晨 3 点运行你的脚本。

根据维基百科,软件实用程序 Cron 是 Unix 类计算机操作系统中基于时间的作业调度程序。设置和维护软件环境的人使用 Cron 定期安排作业(命令或 shell 脚本)在固定的时间、日期或间隔运行。

运行 Python 交互式 shell

另一种运行 Python 的方法是调用交互式 shell。这是我们在控制台命令行中输入 python 时已经看到的东西。

所以,打开控制台,激活你的虚拟环境(现在应该已经成为你的第二天性了,对吧?),然后输入 python。你会看到几行文字,应该是这样的:

$ python
Python 3.7.0a3 (default, Jan 27 2018, 00:46:45)
[Clang 9.0.0 (clang-900.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

那些 >>> 是 shell 的提示符。它们告诉你 Python 正在等待你输入。如果你输入一个简单的指令,一个适合一行的东西,那就是你会看到的。然而,如果你输入需要多于一行代码的东西,shell 会把提示符改成 ...,给你一个视觉线索,告诉你正在输入一个多行语句(或者任何需要多于一行代码的东西)。

来吧,试一试;让我们做一些基本的数学:

>>> 2 + 4
6
>>> 10 / 4
2.5
>>> 2 ** 1024 
179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216

最后的操作向你展示了一些不可思议的东西。我们将21024次方,Python 毫无困难地处理了这个任务。试着在 Java、C++或 C#中做这个操作。除非你使用特殊的库来处理这样大的数字,否则是行不通的。

我每天都使用交互式 shell。它非常有用,可以快速调试,例如,检查数据结构是否支持某个操作。或者检查或运行一段代码。

当你使用 Django(一个 Web 框架)时,交互式 shell 与之相结合,允许你通过框架工具来检查数据库中的数据,以及其他许多事情。你会发现交互式 shell 很快会成为你在这段旅程中最亲密的朋友之一。

另一种解决方案,以更好的图形布局呈现,是使用集成开发环境IDLE)。这是一个相当简单的集成开发环境,主要面向初学者。它比你在控制台中得到的裸交互式 shell 具有稍微更多的功能,所以你可能想要探索一下。它在 Windows Python 安装程序中免费提供,你也可以轻松地在任何其他系统中安装它。你可以在 Python 网站上找到有关它的信息。

Guido Van Rossum 将 Python 命名为英国喜剧团体 Monty Python,因此有传言称 IDLE 的名称是为了纪念 Monty Python 的创始成员之一 Eric Idle 而选择的。

将 Python 作为服务运行

除了作为脚本运行之外,在 shell 的边界内,Python 还可以编码并作为应用程序运行。我们将在整本书中看到许多关于这种模式的例子。当我们谈论 Python 代码是如何组织和运行的时候,我们将更多地了解它。

将 Python 作为 GUI 应用程序运行

Python 也可以作为图形用户界面GUI)运行。有几个可用的框架,其中一些是跨平台的,另一些是特定于平台的。

在其他 GUI 框架中,我们发现以下是最广泛使用的:

  • PyQt

  • Tkinter

  • wxPython

  • PyGTK

详细描述它们超出了本书的范围,但您可以在 Python 网站上找到所有您需要的信息(docs.python.org/3/faq/gui.html),在Python 存在哪些平台无关的 GUI 工具包?部分。如果您正在寻找 GUI,请记住根据一些原则选择您想要的。确保它们:

  • 提供您可能需要开发项目的所有功能

  • 在您可能需要支持的所有平台上运行

  • 依靠尽可能广泛和活跃的社区

  • 包装图形驱动程序/工具,您可以轻松安装/访问

Python 代码是如何组织的?

让我们谈一下 Python 代码是如何组织的。在本节中,我们将更深入地探讨一些技术名称和概念。

从基础知识开始,Python 代码是如何组织的?当然,您将代码写入文件中。当您使用扩展名.py保存文件时,该文件被称为 Python 模块。

如果您使用的是 Windows 或 macOS,通常会向用户隐藏文件扩展名,请确保更改配置,以便您可以看到文件的完整名称。这不是严格的要求,而是一个建议。

将所有软件所需的代码保存在一个文件中是不切实际的。这种解决方案适用于脚本,通常不超过几百行(通常比这短得多)。

一个完整的 Python 应用程序可能由数十万行代码组成,因此您将不得不将其分散到不同的模块中,这是更好的,但还不够好。事实证明,即使这样,使用代码仍然是不切实际的。因此,Python 为您提供了另一种结构,称为,它允许您将模块组合在一起。包只是一个文件夹,必须包含一个特殊文件__init__.py,它不需要包含任何代码,但其存在是为了告诉 Python 该文件夹不仅仅是一个文件夹,而实际上是一个包(请注意,从 Python 3.3 开始,__init__.py模块不再严格需要)。

像往常一样,一个例子将使所有这些更加清晰。我在我的书项目中创建了一个示例结构,当我在控制台中输入时:

$ tree -v example

我得到了ch1/example文件夹内容的树形表示,其中包含本章示例的代码。这是一个真正简单应用程序结构的样子:

example
├── core.py
├── run.py
└── util
 ├── __init__.py
 ├── db.py
 ├── math.py
 └── network.py

您可以看到,在这个例子的根目录中,我们有两个模块,core.pyrun.py,以及一个包:util。在core.py中,可能有我们应用程序的核心逻辑。另一方面,在run.py模块中,我们可能会找到启动应用程序的逻辑。在util包中,我希望找到各种实用工具,实际上,我们可以猜测那里的模块是根据它们所持有的工具类型命名的:db.py将持有与数据库工作相关的工具,math.py当然将持有数学工具(也许我们的应用程序涉及财务数据),network.py可能将持有在网络上发送/接收数据的工具。

如前所述,__init__.py文件只是告诉 Pythonutil是一个包,而不仅仅是一个普通的文件夹。

如果这个软件只是组织在模块中,推断其结构将会更加困难。我在ch1/files_only文件夹下放了一个仅模块的例子;自己看看:

$ tree -v files_only

这给我们展示了一个完全不同的画面:

files_only/
├── core.py
├── db.py
├── math.py
├── network.py
└── run.py

猜测每个模块的功能可能有点困难,对吧?现在,考虑这只是一个简单的例子,所以您可以猜想如果我们无法将代码组织成包和模块,要理解一个真实应用程序会有多困难。

我们如何使用模块和包?

当开发人员编写应用程序时,很可能需要在不同的部分应用相同的逻辑。例如,当编写一个解析器来解析用户可以在网页上填写的表单数据时,应用程序将需要验证某个字段是否包含数字。无论这种验证逻辑如何编写,很可能会在多个地方需要使用。

例如,在一个调查应用程序中,用户被问及许多问题,很可能其中几个问题需要数字答案。例如:

  • 你多大了?

  • 你有多少宠物?

  • 你有多少孩子?

  • 你结婚多少次了?

在每个期望得到数字答案的地方复制/粘贴(或者更恰当地说:重复)验证逻辑将是非常糟糕的做法。这将违反不要重复自己DRY)原则,该原则规定您在应用程序中不应该重复相同的代码。我感到有必要强调这个原则的重要性:您在应用程序中不应该重复相同的代码(有意思的双关语)。

重复相同的逻辑可能非常糟糕的原因有几个,其中最重要的原因是:

  • 逻辑中可能存在错误,因此,您将不得不在应用逻辑的每个地方进行更正。

  • 您可能希望修改验证的方式,然后您将不得不在应用的每个地方进行更改。

  • 您可能会忘记修复/修改某个逻辑,因为在搜索所有出现的时候错过了它。这将在您的应用程序中留下错误/不一致的行为。

  • 您的代码会比需要的更长,没有好的理由。

Python 是一种很棒的语言,并为您提供了应用所有编码最佳实践所需的所有工具。对于这个特定的例子,我们需要能够重用一段代码。为了能够重用一段代码,我们需要有一个构造,可以为我们保存代码,以便我们可以在需要重复其中的逻辑时调用该构造。这个构造存在,它被称为函数

我在这里不会深入讨论具体细节,所以请记住,函数是一块有组织的可重用代码,用于执行任务。函数可以根据它们所属的环境的不同形式和名称,但现在这并不重要。我们将在书的后面能够欣赏到它们时,再看到细节。函数是应用程序中模块化的构建块,几乎是不可或缺的。除非你在写一个非常简单的脚本,否则你会一直使用函数。我们将在第四章中探讨函数,函数,代码的构建块

Python 自带一个非常庞大的库,就像我几页前已经说过的那样。现在,也许是定义库的好时机:是提供丰富功能的函数和对象的集合,丰富了语言的能力。

例如,在 Python 的math库中,我们可以找到大量的函数,其中之一就是factorial函数,它当然可以计算一个数的阶乘。

在数学中,非负整数N阶乘,表示为N!,被定义为小于或等于N的所有正整数的乘积。例如,计算 5 的阶乘如下:

5!= 5 * 4 * 3 * 2 * 1 = 120

0 的阶乘是 0!= 1,以遵守空乘积的约定。

因此,如果你想在你的代码中使用这个函数,你只需要导入它并用正确的输入值调用它。如果现在输入值和调用的概念不太清楚,不要担心,只需专注于导入部分。我们通过从中导入需要的内容来使用库,然后使用它。

在 Python 中,要计算数字 5 的阶乘,我们只需要以下代码:

>>> from math import factorial
>>> factorial(5)
120

无论我们在 shell 中输入什么,只要它有可打印的表示,就会在控制台上打印出来(在这种情况下,是函数调用的结果:120)。

所以,让我们回到我们的例子,那个有core.pyrun.pyutil等的例子。

在我们的例子中,包util是我们的实用库。我们的自定义实用工具包,其中包含我们应用程序中需要的所有可重用工具(即函数)。其中一些将处理数据库(db.py),一些将处理网络(network.py),一些将执行数学计算(math.py),这些都超出了 Python 标准math库的范围,因此我们必须自己编写它们。

我们将在专门的章节详细讨论如何导入函数并在其中使用。现在让我们谈谈另一个非常重要的概念:Python 的执行模型

Python 的执行模型

在这一部分,我想向你介绍一些非常重要的概念,比如作用域、名称和命名空间。当然,你可以在官方语言参考中阅读关于 Python 执行模型的所有内容,但我认为那相当技术化和抽象,所以让我先给你一个不太正式的解释。

名称和命名空间

假设你正在找一本书,所以你去图书馆问有人要找的书。他们告诉你类似二楼,X 区,第三排这样的信息。于是你上楼,找到 X 区,依此类推。

进入一个图书馆,所有的书都堆在一起,没有楼层,没有区域,没有排列,没有秩序,那将是非常不同的。找一本书将会非常困难。

当我们编写代码时,我们也会遇到同样的问题:我们必须尝试组织它,以便对于没有先验知识的人来说,能够轻松找到他们要找的东西。当软件结构正确时,它也促进了代码重用。另一方面,杂乱无章的软件更有可能暴露出分散的重复逻辑片段。

首先,让我们从书开始。我们通过书的标题来引用一本书,在 Python 术语中,那将是一个名称。Python 名称是其他语言称为变量的最接近的抽象。名称基本上是指向对象的,并且是通过名称绑定操作引入的。让我们举个快速的例子(注意,跟在#后面的任何内容都是注释):

>>> n = 3  # integer number
>>> address = "221b Baker Street, NW1 6XE, London"  # Sherlock Holmes' address
>>> employee = {
...     'age': 45,
...     'role': 'CTO',
...     'SSN': 'AB1234567',
... }
>>> # let's print them
>>> n
3
>>> address
'221b Baker Street, NW1 6XE, London'
>>> employee
{'age': 45, 'role': 'CTO', 'SSN': 'AB1234567'}
>>> other_name
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'other_name' is not defined 

我们在前面的代码中定义了三个对象(你还记得每个 Python 对象具有的三个特征是什么吗?):

  • 一个整数n(类型:int,值:3

  • 一个字符串address(类型:str,值:福尔摩斯的地址)

  • 一个字典employee(类型:dict,值:包含三个键/值对的字典)

别担心,我知道你不应该知道什么是字典。我们将在第二章“内置数据类型”中看到,它是 Python 数据结构的王者。

你有没有注意到,当我输入 employee 的定义时,提示从>>>变成了...?这是因为定义跨越了多行。

那么,naddressemployee是什么?它们是名称。我们可以使用这些名称在我们的代码中检索数据。它们需要被保存在某个地方,这样每当我们需要检索这些对象时,我们可以使用它们的名称来获取它们。我们需要一些空间来保存它们,因此:命名空间!

命名空间因此是名称到对象的映射。例如内置名称集(包含在任何 Python 程序中始终可访问的函数)、模块中的全局名称和函数中的局部名称。甚至对象的属性集也可以被视为命名空间。

命名空间的美妙之处在于它们允许你以清晰的方式定义和组织你的名称,而不会重叠或干扰。例如,与我们在图书馆中寻找的书相关联的命名空间可以用来导入书本本身,就像这样:

from library.second_floor.section_x.row_three import book 

我们从library命名空间开始,通过点(.)运算符,我们进入该命名空间。在这个命名空间中,我们寻找second_floor,再次使用.运算符进入它。然后我们进入section_x,最后在最后一个命名空间row_three中找到了我们正在寻找的名称:book

当我们处理真实的代码示例时,通过命名空间的遍历将更加清晰。现在,只需记住命名空间是名称与对象相关联的地方。

还有一个概念,与命名空间的概念密切相关,我想简要谈谈:作用域

作用域

根据 Python 的文档:

作用域是 Python 程序的文本区域,其中命名空间是直接可访问的。”

直接可访问意味着当你寻找一个未经修饰的名称引用时,Python 会尝试在命名空间中找到它。

作用域是静态确定的,但实际上,在运行时,它们是动态使用的。这意味着通过检查源代码,你可以知道对象的作用域是什么,但这并不阻止软件在运行时改变它。Python 提供了四种不同的作用域(当然,不一定同时都存在):

  • 局部作用域,它是最内层的作用域,包含局部名称。

  • 封闭作用域,也就是任何封闭函数的作用域。它包含非局部名称和非全局名称。

  • 全局作用域包含全局名称。

  • 内置作用域包含内置名称。Python 带有一组函数,你可以直接使用,比如printallabs等。它们存在于内置作用域中。

规则如下:当我们引用一个名称时,Python 会从当前命名空间开始查找它。如果找不到该名称,Python 会继续搜索封闭范围,直到搜索内置范围。如果在搜索内置范围后没有找到名称,则 Python 会引发一个NameError异常,这基本上意味着该名称尚未定义(您在前面的示例中看到了这一点)。

当查找名称时,命名空间的扫描顺序是:本地封闭全局内置LEGB)。

这都是非常理论的,所以让我们看一个例子。为了向您展示本地和封闭命名空间,我将不得不定义一些函数。如果您暂时不熟悉它们的语法,不要担心。我们将在第四章中学习函数,代码的构建块。只需记住,在下面的代码中,当您看到def时,这意味着我正在定义一个函数:

# scopes1.py
# Local versus Global

# we define a function, called local
def local():
    m = 7
    print(m)

m = 5
print(m)

# we call, or `execute` the function local
local()

在前面的示例中,我们在全局范围和本地范围(由local函数定义)中定义了相同的名称m。当我们使用以下命令执行此程序时(您已激活了您的虚拟环境吗?):

$ python scopes1.py

我们在控制台上看到两个数字打印出来:57

Python 解释器解析文件时,从上到下。首先找到一对注释行,然后解析函数local的定义。调用时,此函数执行两件事:它设置一个名称到代表数字7的对象,并将其打印出来。Python 解释器继续前进,找到另一个名称绑定。这次绑定发生在全局范围中,值为5。下一行是对print函数的调用,它被执行(因此我们在控制台上得到了第一个打印出的值:5)。

在此之后,调用函数local。此时,Python 执行该函数,因此此时发生绑定m = 7并打印出来。

非常重要的一点是代码的一部分属于local函数的定义,右侧缩进了四个空格。事实上,Python 通过缩进代码来定义作用域。通过缩进进入作用域,通过取消缩进退出作用域。一些编码人员使用两个空格,其他人使用三个空格,但建议使用的空格数是四个。这是最大化可读性的一个很好的措施。我们稍后会更多地讨论编写 Python 代码时应该采用的所有惯例。

如果我们删除m = 7这一行会发生什么?记住 LEGB 规则。Python 会开始在本地范围(函数local)中查找m,找不到,它会转到下一个封闭范围。在这种情况下,下一个是全局范围,因为没有包裹在local周围的封闭函数。因此,我们会在控制台上看到两个数字5。让我们实际看一下代码会是什么样子:

# scopes2.py
# Local versus Global

def local():
    # m doesn't belong to the scope defined by the local function
    # so Python will keep looking into the next enclosing scope.
    # m is finally found in the global scope
    print(m, 'printing from the local scope')

m = 5
print(m, 'printing from the global scope')

local()

运行scopes2.py将打印出:

$ python scopes2.py
5 printing from the global scope
5 printing from the local scope

正如预期的那样,Python 首次打印m,然后当调用函数local时,m在其范围内找不到,因此 Python 按照 LEGB 链继续寻找,直到在全局范围中找到m

让我们看一个带有额外层次的示例,封闭范围:

# scopes3.py
# Local, Enclosing and Global

def enclosing_func():
    m = 13

    def local():
        # m doesn't belong to the scope defined by the local
        # function so Python will keep looking into the next
        # enclosing scope. This time m is found in the enclosing
        # scope
        print(m, 'printing from the local scope')

    # calling the function local
    local()

m = 5
print(m, 'printing from the global scope')

enclosing_func()

运行scopes3.py将在控制台上打印:

$ python scopes3.py
(5, 'printing from the global scope')
(13, 'printing from the local scope')

正如您所看到的,函数local中的print指令与以前一样引用mm在函数本身中仍未定义,因此 Python 按照 LEGB 顺序开始遍历范围。这次m在封闭范围中找到。

现在如果这还不是很清楚,不要担心。随着我们在书中的例子,你会慢慢理解的。Python 教程的Classes部分(docs.python.org/3/tutorial/classes.html)有一段有趣的关于作用域和命名空间的段落。如果你想更深入地理解这个主题,一定要在某个时候阅读一下。

在我们结束这一章之前,我想再多谈谈对象。毕竟,基本上 Python 中的一切都是对象,所以我认为它们值得更多的关注。

对象和类

当我之前在章节A proper introduction中介绍对象时,我说我们用它们来代表现实生活中的对象。例如,现在我们在网上销售各种商品,我们需要能够正确地处理、存储和表示它们。但对象实际上远不止于此。在 Python 中,你将要做的大部分事情都与操作对象有关。

因此,不要深入细节(我们将在以后的章节中做到这一点),我想给你一个关于类和对象的简而言之的解释。

我们已经看到对象是 Python 对数据的抽象。事实上,Python 中的一切都是对象,包括数字,字符串(保存文本的数据结构),容器,集合,甚至函数。你可以把它们想象成至少有三个特征的盒子:一个 ID(唯一),一个类型和一个值。

但它们是如何产生的?我们如何创建它们?我们如何编写我们自己的自定义对象?答案就在一个简单的词中:

事实上,对象是类的实例。Python 的美妙之处在于类本身也是对象,但我们不要深入这个领域。这会导致这种语言中最高级的概念之一:元类。现在,你理解类和对象之间的区别最好的方法是通过一个例子。

比如一个朋友告诉你,我买了一辆新自行车! 你立刻明白她在说什么。你看到了这辆自行车吗?没有。你知道它是什么颜色吗?不知道。品牌?不知道。你对它了解多少?不知道。但与此同时,你知道你需要了解的一切,以理解你的朋友告诉你她买了一辆新自行车。你知道自行车有两个轮子连接到一个框架上,有一个鞍座,踏板,把手,刹车等等。换句话说,即使你没有看到自行车本身,你知道自行车的概念。一组抽象的特征和特性共同形成了一个叫做自行车的东西。

在计算机编程中,这就是所谓的。就是这么简单。类用于创建对象。事实上,对象被称为类的实例

换句话说,我们都知道自行车是什么;我们知道这个类。但是我有自己的自行车,它是自行车类的一个实例。我的自行车是一个具有自己特征和方法的对象。你也有自己的自行车。同样的类,但不同的实例。世界上制造的每辆自行车都是自行车类的一个实例。

让我们看一个例子。我们将编写一个定义自行车的类,然后我们将创建两辆自行车,一辆红色的,一辆蓝色的。我会保持代码非常简单,但如果你不完全理解它,不要担心;你现在需要关心的是理解类和对象(或类的实例)之间的区别:

# bike.py
# let's define the class Bike
class Bike:

    def __init__(self, colour, frame_material):
        self.colour = colour
        self.frame_material = frame_material

    def brake(self):
        print("Braking!")

# let's create a couple of instances
red_bike = Bike('Red', 'Carbon fiber')
blue_bike = Bike('Blue', 'Steel')

# let's inspect the objects we have, instances of the Bike class.
print(red_bike.colour)  # prints: Red
print(red_bike.frame_material)  # prints: Carbon fiber
print(blue_bike.colour)  # prints: Blue
print(blue_bike.frame_material)  # prints: Steel

# let's brake!
red_bike.brake()  # prints: Braking!

我希望现在我不需要再告诉你每次都要运行文件了,对吧?文件名在代码块的第一行中指定。只需运行$ python filename,一切都会很好。但记得要激活你的虚拟环境!

这里有很多有趣的事情要注意。首先,类的定义是通过class语句完成的。无论class语句之后的代码是什么,并且缩进,都被称为类的主体。在我们的例子中,属于类定义的最后一行是print("Braking!")

在定义了类之后,我们准备创建实例。你可以看到类主体承载了两个方法的定义。方法基本上(和简单地)是属于类的函数。

第一个方法__init__是一个初始化器。它使用一些 Python 魔术来使用我们在创建时传递的值设置对象。

在 Python 中,每个具有前导和尾随双下划线的方法都被称为魔术方法。魔术方法被 Python 用于多种不同的目的;因此,使用两个前导和尾随下划线命名自定义方法从来都不是一个好主意。这种命名约定最好留给 Python。

我们定义的另一种方法brake只是一个额外方法的示例,如果我们想要刹车自行车,我们可以调用它。当然,它只包含一个print语句;这只是一个例子。

我们创建了两辆自行车。一辆是红色的,有碳纤维框架,另一辆是蓝色的,有钢框架。我们在创建时传递这些值。创建后,我们打印出红色自行车的颜色属性和框架类型,以及蓝色自行车的框架类型,这只是一个例子。我们还调用了red_bikebrake方法。

最后要注意的一件事。你还记得我告诉过你对象的属性集被认为是一个命名空间吗?我希望现在我说的更清楚了。你可以看到通过不同的命名空间(red_bikeblue_bike)获取frame_type属性,我们得到不同的值。没有重叠,没有混淆。

点(.)运算符当然是我们用来进入命名空间的手段,在对象的情况下也是如此。

如何编写良好代码的指南

编写良好的代码并不像看起来那么容易。正如我之前所说,良好的代码展示了一长串相当难以组合的特质。编写良好的代码在某种程度上是一种艺术。无论你愿意在哪个阶段停下来,有一件事你可以接受,那就是让你的代码立即变得更好的东西:PEP 8

根据维基百科:

"Python 的发展主要通过 Python Enhancement Proposal (PEP) 过程进行。PEP 过程是提出主要新功能、收集社区对问题的意见和记录 Python 设计决策的主要机制。"

PEP 8 可能是所有 PEP 中最著名的。它提出了一套简单但有效的指南,定义了 Python 的美学,以便我们编写优美的 Python 代码。如果你从这一章中得到一个建议,请让它成为这个:使用它。拥抱它。你以后会感谢我的。

今天的编码不再是一个签入/签出的业务。相反,它更多的是一种社会努力。几个开发人员通过 Git 和 Mercurial 等工具共同协作一段代码,结果是由许多不同的手所创造的代码。

Git 和 Mercurial 可能是今天使用最多的分布式版本控制系统。它们是设计帮助开发团队共同协作在同一软件上的基本工具。

如今,我们更需要有一种一致的编写代码的方式,以便最大限度地提高可读性。当公司的所有开发人员遵守 PEP 8 时,他们中的任何一个人落在一段代码上,都不会觉得他们自己写的。这实际上经常发生在我身上(我总是忘记我写的代码)。

这有一个巨大的优势:当您阅读自己可以写出的代码时,您会轻松阅读。没有约定,每个编码人员都会按照他们最喜欢的方式或者他们被教导或者习惯的方式来构建代码,这意味着必须根据别人的风格来解释每一行。这意味着要花费更多的时间来理解它。由于 PEP 8,我们可以避免这种情况。我是它的忠实粉丝,如果代码不遵守它,我就不会签署代码审查。因此,请花时间学习它;这非常重要。

在本书的例子中,我会尽量尊重它。不幸的是,我没有 79 个字符的奢侈(这是 PEP 8 建议的最大行长度),我将不得不减少空行和其他东西,但我向你保证我会尽量布置我的代码,使其尽可能可读。

Python 文化

Python 已经被广泛应用于所有编码行业。它被许多不同的公司用于许多不同的目的,也被用于教育(因为它有许多优点,而且易于学习,所以它是一个非常出色的语言)。

Python 今天如此受欢迎的一个原因是,围绕它的社区是庞大、充满活力且充满杰出人才的。世界各地都组织了许多活动,主要是围绕 Python 或其主要的 Web 框架 Django。

Python 是开放的,而且通常拥抱它的人的思想也是开放的。在 Python 网站的社区页面上查看更多信息并参与其中!

Python 还有另一个方面,围绕着“Pythonic”的概念。这与 Python 允许您使用一些在其他地方找不到的习语有关,至少不是以相同的形式或者不是那么容易使用(现在我在使用不是 Python 的语言编码时感到相当幽闭恐惧)。

无论如何,多年来,这种 Pythonic 的概念已经出现了,我理解的方式是按照 Python 应该完成的方式来做事

为了帮助您更好地了解 Python 的文化和 Pythonic,我将向您展示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。从有趣的层次开始,然后深入挖掘。始终深入挖掘。

关于 IDE 的说明

关于 IDE 的一些话。要遵循本书中的示例,您不需要 IDE;任何文本编辑器都可以。如果您想要更多高级功能,比如语法着色和自动完成,您就需要获取一个 IDE。您可以在 Python 网站上找到一份全面的开源 IDE 列表(只需搜索 Python IDEs)。我个人使用 Sublime Text 编辑器。它是免费试用的,成本只需几美元。我一生中尝试过许多 IDE,但这是让我最有效率的一个。

两个重要的建议:

  • 无论您选择使用什么 IDE,都要努力学会它,以便能够充分利用它的优势,但不要依赖它。偶尔练习使用 VIM(或任何其他文本编辑器);学会能够在任何平台上使用任何一套工具进行工作。

  • 无论您使用什么文本编辑器/IDE,写 Python 时,缩进是四个空格。不要使用制表符,也不要将它们与空格混合使用。使用四个空格,不是两个,不是三个,也不是五个。就使用四个。整个世界都是这样的,你不想因为你喜欢三个空格的布局而成为局外人。

总结

在这一章中,我们开始探索编程和 Python 的世界。我们只是初步了解了一点点,涉及到的概念将在本书的后面更详细地讨论。

我们谈到了 Python 的主要特性,谁在使用它以及为什么,以及我们可以用哪些不同的方式编写 Python 程序。

在本章的最后部分,我们简要介绍了命名空间、作用域、类和对象的基本概念。我们还看到了 Python 代码如何使用模块和包进行组织。

在实际操作中,我们学会了如何在系统上安装 Python,如何确保我们拥有所需的工具pip和 virtualenv,并创建并激活了我们的第一个虚拟环境。这将使我们能够在一个独立的环境中工作,而不会危及 Python 系统的安装。

现在你已经准备好和我一起开始这段旅程了。你所需要的只是热情、激活的虚拟环境、这本书、你的手指和一些咖啡。

尝试跟着例子走;我会让它们简单而简短。如果你能熟练掌握它们,你会比仅仅阅读更好地记住它们。

在下一章中,我们将探索 Python 丰富的内置数据类型。有很多内容需要涵盖和学习!

第二章:内置数据类型

“数据!数据!数据!”他不耐烦地喊道。“没有黏土,我无法制砖。”– 福尔摩斯 – 铜山丛林的冒险

您使用计算机的一切都是在管理数据。数据有许多不同的形状和口味。这是您听的音乐,您流媒体的电影,您打开的 PDF。甚至您正在阅读的本章的来源只是一个文件,即数据。

数据可以是简单的,用整数表示年龄,也可以是复杂的,比如在网站上下的订单。它可以是关于单个对象或关于它们的集合。数据甚至可以是关于数据,即元数据。描述其他数据结构的设计或描述应用程序数据或其上下文的数据。在 Python 中,对象是数据的抽象,Python 有各种各样的数据结构,您可以使用它们来表示数据,或者将它们组合起来创建自己的自定义数据。

在本章中,我们将涵盖以下内容:

  • Python 对象的结构

  • 可变性和不可变性

  • 内置数据类型:数字、字符串、序列、集合和映射类型

  • 集合模块

  • 枚举

一切皆为对象

在我们深入具体内容之前,我希望您对 Python 中的对象非常清楚,所以让我们再多谈谈它们。正如我们已经说过的,Python 中的一切都是对象。但是当您在 Python 模块中键入age = 42这样的指令时,实际上发生了什么?

如果您转到pythontutor.com/,您可以在文本框中键入该指令并获得其可视化表示。记住这个网站;它对于巩固您对幕后发生的事情的理解非常有用。

因此,创建了一个对象。它获得了idtype设置为int(整数),value设置为42。一个名为age的名称被放置在全局命名空间中,指向该对象。因此,每当我们在全局命名空间中,在执行该行之后,我们可以通过简单地通过其名称访问它来检索该对象:age

如果你要搬家,你会把所有的刀、叉和勺子放在一个箱子里,贴上标签餐具。你能看到这完全是相同的概念吗?这是一个可能看起来像的屏幕截图(你可能需要调整设置以获得相同的视图):

因此,在本章的其余部分,每当您读到诸如name = some_value之类的内容时,都要想象一个放置在命名空间中并与编写该指令的范围相关联的名称,指向具有idtypevalue的对象的漂亮箭头。关于这个机制还有一些要说的地方,但是通过一个例子来谈论它要容易得多,所以我们稍后会回到这个问题。

可变还是不可变?这是个问题

Python 对数据做出的第一个基本区分是关于对象的值是否会改变。如果值可以改变,则对象称为可变,而如果值不能改变,则对象称为不可变

非常重要的是,您理解可变和不可变之间的区别,因为它会影响您编写的代码,所以这里有一个问题:

>>> age = 42
>>> age
42
>>> age = 43  #A
>>> age
43

在前面的代码中,在#A行,我改变了年龄的值吗?嗯,没有。但现在是43(我听到你说...)。是的,是43,但42是一个整数,类型为int,是不可变的。所以,真正发生的是,在第一行,age是一个名称,它被设置为指向一个int对象,其值为42。当我们键入age = 43时,发生的是创建另一个对象,类型为int,值为43(另外,id将不同),并且名称age被设置为指向它。因此,我们没有将42更改为43。实际上,我们只是将age指向了一个不同的位置:值为43的新int对象。让我们看看相同的代码也打印出 ID:

>>> age = 42
>>> id(age)
4377553168
>>> age = 43
>>> id(age)
4377553200

请注意,我们通过调用内置的id函数打印 ID。如你所见,它们是不同的,正如预期的那样。请记住,age一次只指向一个对象:首先是42,然后是43。从不同时。

现在,让我们看一个使用可变对象的相同示例。对于这个示例,让我们只使用一个Person对象,该对象具有一个age属性(现在不用担心类声明;它只是为了完整性):

>>> class Person():
...     def __init__(self, age):
...         self.age = age
...
>>> fab = Person(age=42)
>>> fab.age
42
>>> id(fab)
4380878496
>>> id(fab.age)
4377553168
>>> fab.age = 25  # I wish!
>>> id(fab)  # will be the same
4380878496
>>> id(fab.age)  # will be different
4377552624

在这种情况下,我设置了一个typePerson(自定义类)的对象fab。在创建时,对象被赋予age42。我打印它,以及对象id,以及age的 ID。请注意,即使我将age更改为25fab的 ID 仍然保持不变(当然,age的 ID 已经改变了)。Python 中的自定义对象是可变的(除非你编码使它们不可变)。记住这个概念;这是非常重要的。我会在本章的其余部分提醒你。

数字

让我们从探索 Python 的内置数字数据类型开始。Python 是由一位数学和计算机科学硕士设计的,所以它对数字有很好的支持是很合理的。

数字是不可变对象。

整数

Python 整数具有无限范围,仅受可用虚拟内存的限制。这意味着实际上并不重要要存储多大的数字:只要它能够适应计算机的内存,Python 就会处理它。整数可以是正数、负数和 0(零)。它们支持所有基本的数学运算,如下例所示:

>>> a = 14
>>> b = 3
>>> a + b  # addition
17
>>> a - b  # subtraction
11
>>> a * b  # multiplication
42
>>> a / b  # true division
4.666666666666667
>>> a // b  # integer division
4
>>> a % b  # modulo operation (reminder of division)
2
>>> a ** b  # power operation
2744

前面的代码应该很容易理解。只要注意一件重要的事情:Python 有两个除法运算符,一个执行所谓的真除法/),它返回操作数的商,另一个是所谓的整数除法//),它返回操作数的向下取整商。值得注意的是,在 Python 2 中,除法运算符/的行为与 Python 3 中不同。看看对于正数和负数是如何不同的:

>>> 7 / 4  # true division
1.75
>>> 7 // 4  # integer division, truncation returns 1
1
>>> -7 / 4  # true division again, result is opposite of previous
-1.75
>>> -7 // 4  # integer div., result not the opposite of previous
-2

这是一个有趣的例子。如果你在最后一行期望得到-1,不要感到难过,这只是 Python 的工作方式。在 Python 中,整数除法的结果总是朝负无穷大方向舍入。如果你想将一个数字截断为整数,而不是向下取整,你可以使用内置的int函数,如下例所示:

>>> int(1.75)
1
>>> int(-1.75)
-1

注意截断是朝0方向进行的。

还有一个运算符可以计算除法的余数。它被称为模运算符,用百分号(%)表示:

>>> 10 % 3  # remainder of the division 10 // 3
1
>>> 10 % 4  # remainder of the division 10 // 4
2

Python 3.6 中引入的一个很好的特性是能够在数字文字中添加下划线(在数字或基数说明符之间,但不能在前导或尾随)。其目的是帮助使一些数字更易读,例如1_000_000_000

>>> n = 1_024
>>> n
1024
>>> hex_n = 0x_4_0_0  # 0x400 == 1024
>>> hex_n
1024 

布尔值

布尔代数是代数的一个子集,在其中变量的值是真值:真和假。在 Python 中,TrueFalse是两个关键字,用于表示真值。布尔值是整数的一个子类,分别像10一样行为。布尔值的等价int类是bool类,它返回TrueFalse。在布尔上下文中,每个内置的 Python 对象都有一个值,这意味着当它们被传递给bool函数时,它们基本上会评估为TrueFalse。我们将在第三章中详细了解这一切,迭代和做决定

布尔值可以使用逻辑运算符andornot在布尔表达式中组合。再次,我们将在下一章中全面了解它们,所以现在让我们只看一个简单的例子:

>>> int(True)  # True behaves like 1
1
>>> int(False)  # False behaves like 0
0
>>> bool(1)  # 1 evaluates to True in a boolean context
True
>>> bool(-42)  # and so does every non-zero number
True
>>> bool(0)  # 0 evaluates to False
False
>>> # quick peak at the operators (and, or, not)
>>> not True
False
>>> not False
True
>>> True and True
True
>>> False or True
True

当你尝试将TrueFalse相加时,你会发现它们是整数的子类。Python 将它们向上转换为整数并执行加法:

>>> 1 + True
2
>>> False + 42
42
>>> 7 - True
6 

Upcasting是一种从子类到其父类的类型转换操作。在这里呈现的例子中,TrueFalse属于从整数类派生的类,当需要时会转换回整数。

实数

实数,或浮点数,根据 IEEE 754 双精度二进制浮点格式在 Python 中表示,它存储在 64 位信息中,分为三个部分:符号、指数和尾数。

满足你对这种格式的知识渴望:en.wikipedia.org/wiki/Double-precision_floating-point_format

通常,编程语言给程序员提供两种不同的格式:单精度和双精度。前者占用 32 位内存,后者占用 64 位。Python 只支持双精度格式。让我们看一个简单的例子:

>>> pi = 3.1415926536  # how many digits of PI can you remember?
>>> radius = 4.5
>>> area = pi * (radius ** 2)
>>> area
63.617251235400005 

在计算面积时,我在括号中包裹了radius ** 2。尽管这并不是必要的,因为幂运算符的优先级高于乘法运算符,但我认为这样公式读起来更容易。此外,如果你得到的面积稍有不同,不要担心。这可能取决于你的操作系统、Python 的编译方式等等。只要前几位小数正确,你就知道这是正确的结果。

sys.float_info结构序列包含有关浮点数在你的系统上的行为的信息。这是我在我的电脑上看到的:

>>> import sys
>>> sys.float_info
sys.float_info(max=1.7976931348623157e+308, max_exp=1024, max_10_exp=308, min=2.2250738585072014e-308, min_exp=-1021, min_10_exp=-307, dig=15, mant_dig=53, epsilon=2.220446049250313e-16, radix=2, rounds=1)

在这里我们需要考虑一些事情:我们有 64 位来表示浮点数。这意味着我们最多可以用2 ** 64 == 18,446,744,073,709,551,616个位来表示这些数字。看一下浮点数的maxepsilon值,你会意识到不可能表示它们所有。空间不够,所以它们被近似到最接近的可表示的数字。你可能认为只有极大或极小的数字才会受到这个问题的影响。好吧,再想一想,然后在你的控制台上尝试一下:

>>> 0.3 - 0.1 * 3  # this should be 0!!!
-5.551115123125783e-17 

这告诉你什么?它告诉你,双精度数字即使在处理简单的数字如0.10.3时也会受到近似问题的影响。为什么这很重要?如果你处理价格、金融计算或任何不需要近似的数据,这可能是一个大问题。不用担心,Python 给你decimal类型,它不会受到这些问题的影响;我们马上会看到它们。

复数

Python 为你提供了复数支持。如果你不知道复数是什么,它们是可以用形式a + ib表示的数字,其中ab是实数,i(或者如果你是工程师,j)是虚数单位,即-1的平方根。ab分别被称为数字的实部虚部

实际上,你可能不太会用到它们,除非你在编写科学代码。让我们看一个小例子:

>>> c = 3.14 + 2.73j
>>> c.real  # real part
3.14
>>> c.imag  # imaginary part
2.73
>>> c.conjugate()  # conjugate of A + Bj is A - Bj
(3.14-2.73j)
>>> c * 2  # multiplication is allowed
(6.28+5.46j)
>>> c ** 2  # power operation as well
(2.4067000000000007+17.1444j)
>>> d = 1 + 1j  # addition and subtraction as well
>>> c - d
(2.14+1.73j)

分数和小数

让我们以分数和小数结束数字部分的介绍。分数以最简形式包含有理数的分子和分母。让我们看一个快速的例子:

>>> from fractions import Fraction
>>> Fraction(10, 6)  # mad hatter?
Fraction(5, 3)  # notice it's been simplified
>>> Fraction(1, 3) + Fraction(2, 3)  # 1/3 + 2/3 == 3/3 == 1/1
Fraction(1, 1)
>>> f = Fraction(10, 6)
>>> f.numerator
5
>>> f.denominator
3

尽管它们有时可能非常有用,但在商业软件中并不常见。更容易的是,在所有需要精度的情况下使用小数;例如,在科学和金融计算中。

重要的是要记住,任意精度的十进制数当然会带来性能上的代价。每个数字存储的数据量远远大于分数或浮点数,以及它们的处理方式,这使得 Python 解释器在幕后做了更多的工作。另一个有趣的事情是,你可以通过访问decimal.getcontext().prec来获取和设置精度。

让我们看一个十进制数的快速例子:

>>> from decimal import Decimal as D  # rename for brevity
>>> D(3.14)  # pi, from float, so approximation issues
Decimal('3.140000000000000124344978758017532527446746826171875')
>>> D('3.14')  # pi, from a string, so no approximation issues
Decimal('3.14')
>>> D(0.1) * D(3) - D(0.3)  # from float, we still have the issue
Decimal('2.775557561565156540423631668E-17')
>>> D('0.1') * D(3) - D('0.3')  # from string, all perfect
Decimal('0.0')
>>> D('1.4').as_integer_ratio()  # 7/5 = 1.4 (isn't this cool?!)
(7, 5)

请注意,当我们从float构造一个Decimal数字时,它会继承所有可能来自float的近似问题。另一方面,当Decimal没有近似问题时(例如,当我们将intstring表示传递给构造函数时),计算就没有奇怪的行为。在涉及到货币时,请使用小数。

这就结束了我们对内置数值类型的介绍。现在让我们来看看序列。

不可变序列

让我们从不可变序列开始:字符串、元组和字节。

字符串和字节

Python 中的文本数据是通过str对象处理的,更常见的是字符串。它们是Unicode 代码点的不可变序列。Unicode 代码点可以表示一个字符,但也可以有其他含义,比如格式化数据。与其他语言不同,Python 没有char类型,因此一个单个字符只是一个长度为1的字符串。

Unicode 是处理数据的一个很好的方式,应该用于任何应用程序的内部。但是,当涉及到存储文本数据或在网络上传输文本数据时,您可能希望对其进行编码,使用适合您使用的介质的适当编码。编码的结果会产生一个bytes对象,其语法和行为类似于字符串。Python 中的字符串字面量使用单引号、双引号或三引号(单引号或双引号)编写。如果使用三引号构建,字符串可以跨多行。一个例子将澄清这一点:

>>> # 4 ways to make a string
>>> str1 = 'This is a string. We built it with single quotes.'
>>> str2 = "This is also a string, but built with double quotes."
>>> str3 = '''This is built using triple quotes,
... so it can span multiple lines.'''
>>> str4 = """This too
... is a multiline one
... built with triple double-quotes."""
>>> str4  #A
'This too\nis a multiline one\nbuilt with triple double-quotes.'
>>> print(str4)  #B
This too
is a multiline one
built with triple double-quotes.

#A#B中,我们打印str4,首先是隐式地,然后是显式地,使用print函数。一个很好的练习是找出它们为什么不同。您敢挑战吗?(提示:查找str函数。)

字符串,就像任何序列一样,都有一个长度。您可以通过调用len函数来获得这个长度:

>>> len(str1)
49

编码和解码字符串

使用encode/decode方法,我们可以对 Unicode 字符串进行编码和解码字节对象。UTF-8是一种可变长度的字符编码,能够编码所有可能的 Unicode 代码点。它是网络的主要编码。还要注意,通过在字符串声明前添加一个字面量b,我们创建了一个bytes对象:

>>> s = "This is üŋíc0de"  # unicode string: code points
>>> type(s)
<class 'str'>
>>> encoded_s = s.encode('utf-8')  # utf-8 encoded version of s
>>> encoded_s
b'This is \xc3\xbc\xc5\x8b\xc3\xadc0de'  # result: bytes object
>>> type(encoded_s)  # another way to verify it
<class 'bytes'>
>>> encoded_s.decode('utf-8')  # let's revert to the original
'This is üŋíc0de'
>>> bytes_obj = b"A bytes object"  # a bytes object
>>> type(bytes_obj)
<class 'bytes'>

索引和切片字符串

在操作序列时,很常见需要在一个精确的位置(索引)访问它们,或者从中获取一个子序列(切片)。在处理不可变序列时,这两个操作都是只读的。

虽然索引以一种形式出现,即在序列中的任何位置进行基于零的访问,但切片以不同的形式出现。当您获取序列的一个切片时,您可以指定startstop位置以及step。它们用冒号(:)分隔,就像这样:my_sequence[start:stop:step]。所有参数都是可选的,start是包含的,stop是不包含的。最好通过示例来展示,而不是用更多的文字来解释它们:

>>> s = "The trouble is you think you have time."
>>> s[0]  # indexing at position 0, which is the first char
'T'
>>> s[5]  # indexing at position 5, which is the sixth char
'r'
>>> s[:4]  # slicing, we specify only the stop position
'The '
>>> s[4:]  # slicing, we specify only the start position
'trouble is you think you have time.'
>>> s[2:14]  # slicing, both start and stop positions
'e trouble is'
>>> s[2:14:3]  # slicing, start, stop and step (every 3 chars)
'erb '
>>> s[:]  # quick way of making a copy
'The trouble is you think you have time.'

在所有行中,最后一行可能是最有趣的。如果您不指定参数,Python 会为您填充默认值。在这种情况下,start将是字符串的开始,stop将是字符串的结束,step将是默认的1。这是一种简单快捷的方法来获取字符串s的副本(相同的值,但不同的对象)。您能找到一种使用切片获取字符串的反向副本的方法吗(不要查找,自己找到)?

字符串格式化

字符串的一个特性是能够用作模板。有几种不同的格式化字符串的方法,对于所有可能性的完整列表,我鼓励您查阅文档。以下是一些常见的例子:

>>> greet_old = 'Hello %s!'
>>> greet_old % 'Fabrizio'
'Hello Fabrizio!'
>>> greet_positional = 'Hello {} {}!'
>>> greet_positional.format('Fabrizio', 'Romano')
'Hello Fabrizio Romano!' 
>>> greet_positional_idx = 'This is {0}! {1} loves {0}!'
>>> greet_positional_idx.format('Python', 'Fabrizio')
'This is Python! Fabrizio loves Python!'
>>> greet_positional_idx.format('Coffee', 'Fab')
'This is Coffee! Fab loves Coffee!'
 >>> keyword = 'Hello, my name is {name} {last_name}'
>>> keyword.format(name='Fabrizio', last_name='Romano')
'Hello, my name is Fabrizio Romano'

在前面的示例中,您可以看到四种不同的格式化字符串的方式。第一种依赖于%运算符的方式已被弃用,不应再使用。当前,格式化字符串的现代方式是使用format字符串方法。您可以从不同的示例中看到,一对大括号在字符串中充当占位符。当我们调用format时,我们向其提供替换占位符的数据。我们可以在大括号内指定索引(以及更多内容),甚至名称,这意味着我们将不得不使用关键字参数而不是位置参数来调用format

注意通过向format调用提供不同数据来呈现greet_positional_idx的方式不同。显然,我喜欢 Python 和咖啡...大惊喜!

我想向您展示的最后一个功能是 Python 的一个相对较新的添加(版本 3.6),它被称为格式化字符串文字。这个功能非常酷:字符串以f为前缀,并包含用大括号括起来的替换字段。替换字段是在运行时评估的表达式,然后使用format协议进行格式化:

>>> name = 'Fab'
>>> age = 42
>>> f"Hello! My name is {name} and I'm {age}"
"Hello! My name is Fab and I'm 42"
>>> from math import pi
>>> f"No arguing with {pi}, it's irrational..."
"No arguing with 3.141592653589793, it's irrational..."

查看官方文档,了解有关字符串格式和其强大功能的一切。

元组

我们将要看到的最后一个不可变序列类型是元组。元组是任意 Python 对象的序列。在元组中,项目用逗号分隔。它们在 Python 中随处可见,因为它们允许在其他语言中难以复制的模式。有时元组会被隐式使用;例如,一次设置多个变量,或者允许函数返回多个不同的对象(通常函数只返回一个对象,在许多其他语言中),甚至在 Python 控制台中,您可以隐式使用元组以一条指令打印多个元素。我们将为所有这些情况看到示例:

>>> t = ()  # empty tuple
>>> type(t)
<class 'tuple'>
>>> one_element_tuple = (42, )  # you need the comma!
>>> three_elements_tuple = (1, 3, 5)  # braces are optional here
>>> a, b, c = 1, 2, 3  # tuple for multiple assignment
>>> a, b, c  # implicit tuple to print with one instruction
(1, 2, 3)
>>> 3 in three_elements_tuple  # membership test
True

请注意,成员运算符in也可以与列表、字符串、字典以及一般的集合和序列对象一起使用。

请注意,要创建一个只有一个项目的元组,我们需要在项目后面加上逗号。原因是没有逗号,该项目就是自己包裹在括号中,有点像冗余的数学表达式。还要注意,在赋值时,括号是可选的,因此my_tuple = 1, 2, 3my_tuple = (1, 2, 3)是相同的。

元组赋值允许我们做的一件事是一行交换,不需要第三个临时变量。让我们首先看一下更传统的做法:

>>> a, b = 1, 2
>>> c = a  # we need three lines and a temporary var c
>>> a = b
>>> b = c
>>> a, b  # a and b have been swapped
(2, 1)

现在让我们看看我们如何在 Python 中完成它:

>>> a, b = 0, 1
>>> a, b = b, a  # this is the Pythonic way to do it
>>> a, b
(1, 0)

看一下显示 Python 交换两个值的 Pythonic 方式。您还记得我在第一章中写的吗,Python 的初步介绍?Python 程序通常是等效 Java 或 C++代码大小的五分之一到三分之一,并且像一行交换这样的功能有助于实现这一点。Python 是优雅的,这里的优雅也意味着经济。

由于它们是不可变的,元组可以用作字典的键(我们很快会看到这一点)。对我来说,元组是 Python 中最接近数学向量的内置数据。这并不意味着这就是它们被创建的原因。元组通常包含异构元素序列,而另一方面,列表大多数情况下是同构的。此外,元组通常通过解包或索引访问,而列表通常是迭代访问。

可变序列

可变序列与其不可变的姐妹们不同,它们可以在创建后进行更改。Python 中有两种可变序列类型:列表和字节数组。我之前说过字典是 Python 中数据结构的王者。我想这使得列表成为它合法的女王。

列表

Python 列表是可变序列。它们与元组非常相似,但没有不可变性的限制。列表通常用于存储同类对象的集合,但没有什么阻止你存储异类集合。列表可以以许多不同的方式创建。让我们看一个例子:

>>> []  # empty list
[]
>>> list()  # same as []
[]
>>> [1, 2, 3]  # as with tuples, items are comma separated
[1, 2, 3]
>>> [x + 5 for x in [2, 3, 4]]  # Python is magic
[7, 8, 9]
>>> list((1, 3, 5, 7, 9))  # list from a tuple
[1, 3, 5, 7, 9]
>>> list('hello')  # list from a string
['h', 'e', 'l', 'l', 'o']

在前面的例子中,我向你展示了如何使用不同的技术创建列表。我希望你仔细看一下那一行,它说Python is magic,我不指望你在这一点上完全理解它(除非你作弊了,你不是新手!)。这被称为列表推导,是 Python 的一个非常强大的函数特性。

创建列表是好的,但真正的乐趣是在使用它们时,所以让我们看看它们赋予我们的主要方法:

>>> a = [1, 2, 1, 3]
>>> a.append(13)  # we can append anything at the end
>>> a
[1, 2, 1, 3, 13]
>>> a.count(1)  # how many `1` are there in the list?
2
>>> a.extend([5, 7])  # extend the list by another (or sequence)
>>> a
[1, 2, 1, 3, 13, 5, 7]
>>> a.index(13)  # position of `13` in the list (0-based indexing)
4
>>> a.insert(0, 17)  # insert `17` at position 0
>>> a
[17, 1, 2, 1, 3, 13, 5, 7]
>>> a.pop()  # pop (remove and return) last element
7
>>> a.pop(3)  # pop element at position 3
1
>>> a
[17, 1, 2, 3, 13, 5]
>>> a.remove(17)  # remove `17` from the list
>>> a
[1, 2, 3, 13, 5]
>>> a.reverse()  # reverse the order of the elements in the list
>>> a
[5, 13, 3, 2, 1]
>>> a.sort()  # sort the list
>>> a
[1, 2, 3, 5, 13]
>>> a.clear()  # remove all elements from the list
>>> a
[]

前面的代码为你总结了列表的主要方法。我想向你展示它们有多么强大,以extend为例。你可以使用任何序列类型来扩展列表:

>>> a = list('hello')  # makes a list from a string
>>> a
['h', 'e', 'l', 'l', 'o']
>>> a.append(100)  # append 100, heterogeneous type
>>> a
['h', 'e', 'l', 'l', 'o', 100]
>>> a.extend((1, 2, 3))  # extend using tuple
>>> a
['h', 'e', 'l', 'l', 'o', 100, 1, 2, 3]
>>> a.extend('...')  # extend using string
>>> a
['h', 'e', 'l', 'l', 'o', 100, 1, 2, 3, '.', '.', '.']

现在,让我们看看你可以用列表做哪些最常见的操作:

>>> a = [1, 3, 5, 7]
>>> min(a)  # minimum value in the list
1
>>> max(a)  # maximum value in the list
7
>>> sum(a)  # sum of all values in the list
16
>>> len(a)  # number of elements in the list
4
>>> b = [6, 7, 8]
>>> a + b  # `+` with list means concatenation
[1, 3, 5, 7, 6, 7, 8]
>>> a * 2  # `*` has also a special meaning
[1, 3, 5, 7, 1, 3, 5, 7]

前面代码中的最后两行非常有趣,因为它们向我们介绍了一个叫做运算符重载的概念。简而言之,它意味着诸如+-*%等运算符,根据它们所用的上下文,可能代表不同的操作。对两个列表求和没有任何意义,对吧?因此,+号用于将它们连接起来。因此,*号用于根据右操作数将列表连接到自身。

现在,让我们再进一步,看一些更有趣的东西。我想向你展示sorted方法有多么强大,以及在 Python 中实现需要在其他语言中付出很大努力才能实现的结果有多容易:

>>> from operator import itemgetter
>>> a = [(5, 3), (1, 3), (1, 2), (2, -1), (4, 9)]
>>> sorted(a)
[(1, 2), (1, 3), (2, -1), (4, 9), (5, 3)]
>>> sorted(a, key=itemgetter(0))
[(1, 3), (1, 2), (2, -1), (4, 9), (5, 3)]
>>> sorted(a, key=itemgetter(0, 1))
[(1, 2), (1, 3), (2, -1), (4, 9), (5, 3)]
>>> sorted(a, key=itemgetter(1))
[(2, -1), (1, 2), (5, 3), (1, 3), (4, 9)]
>>> sorted(a, key=itemgetter(1), reverse=True)
[(4, 9), (5, 3), (1, 3), (1, 2), (2, -1)]

前面的代码值得解释一下。首先,a是一个元组的列表。这意味着a中的每个元素都是一个元组(准确地说是一个 2 元组)。当我们调用sorted(some_list)时,我们得到了some_list的排序版本。在这种情况下,对 2 元组的排序是通过对元组中的第一个项目进行排序,当第一个项目相同时,再对第二个项目进行排序。你可以在sorted(a)的结果中看到这种行为,得到了[(1, 2), (1, 3), ...]。Python 还给了我们控制排序必须运行的元组元素的能力。注意,当我们指示sorted函数在每个元组的第一个元素上工作(通过key=itemgetter(0))时,结果是不同的:[(1, 3), (1, 2), ...]。排序仅在每个元组的第一个元素上进行(即在位置 0 上)。如果我们想复制简单的sorted(a)调用的默认行为,我们需要使用key=itemgetter(0, 1),这告诉 Python 首先对元组中位置 0 的元素进行排序,然后对位置 1 的元素进行排序。比较结果,你会看到它们是匹配的。

为了完整起见,我包括了一个仅在位置 1 的元素上进行排序的示例,以及相同的示例,但是按相反的顺序。如果你曾经见过 Java 中的排序,我希望你此刻会感到非常印象深刻。

Python 的排序算法非常强大,由 Tim Peters 编写(我们已经见过这个名字,你还记得是在什么时候吗?)。它被称为Timsort,它是mergeinsertion sort的混合体,比大多数其他用于主流编程语言的算法具有更好的时间性能。Timsort 是一种稳定的排序算法,这意味着当多个记录具有相同的键时,它们的原始顺序被保留。我们在sorted(a, key=itemgetter(0))的结果中看到了这一点,它产生了[(1, 3), (1, 2), ...],其中这两个元组的顺序被保留,因为它们在位置 0 上具有相同的值。

字节数组

总结可变序列类型的概述,让我们花几分钟时间来了解bytearray类型。基本上,它们代表了bytes对象的可变版本。它们公开了大多数可变序列的常规方法,以及bytes类型的大多数方法。项目是范围[0, 256)内的整数。

关于间隔,我将使用开/闭范围的标准表示法。一端的方括号表示该值包括在内,而圆括号表示该值不包括在内。通常根据边缘元素的类型来推断粒度,例如,区间[3, 7]表示 3 和 7 之间的所有整数,包括 3 和 7。另一方面,(3, 7)表示 3 和 7 之间的所有整数,不包括 3 和 7(因此为 4、5 和 6)。bytearray类型中的项目是 0 到 256 之间的整数;0 包括在内,256 不包括在内。通常以这种方式表达间隔的一个原因是为了便于编码。如果我们将范围[a, b)分成N个连续范围,我们可以轻松地表示原始范围的连接,如下所示:

[a,k[1])+[k[1],k[2])+[k[2],k[3])+...+[k[N-1],b)

中间点(k[i])在一端被排除,在另一端被包括,这样在处理代码中处理间隔时就可以轻松进行连接和拆分。

让我们看一个bytearray类型的快速示例:

>>> bytearray()  # empty bytearray object
bytearray(b'')
>>> bytearray(10)  # zero-filled instance with given length
bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
>>> bytearray(range(5)) # bytearray from iterable of integers
bytearray(b'\x00\x01\x02\x03\x04')
>>> name = bytearray(b'Lina')  #A - bytearray from bytes
>>> name.replace(b'L', b'l')
bytearray(b'lina')
>>> name.endswith(b'na')
True
>>> name.upper()
bytearray(b'LINA')
>>> name.count(b'L')
1

如您在前面的代码中所看到的,有几种方法可以创建bytearray对象。它们在许多情况下都很有用;例如,当通过套接字接收数据时,它们消除了在轮询时连接数据的需要,因此它们可能非常方便。在#A行,我创建了一个名为namebytearray,从字节文字b'Lina'中显示了bytearray对象如何公开来自序列和字符串的方法,这非常方便。如果您仔细考虑,它们可以被认为是可变字符串。

集合类型

Python 还提供了两种集合类型,setfrozensetset类型是可变的,而frozenset是不可变的。它们是不可变对象的无序集合。可哈希性是一个特性,允许对象用作字典键和集合成员,正如我们很快将看到的。

从官方文档中:如果对象具有在其生命周期内永远不会更改的哈希值,并且可以与其他对象进行比较,则对象是可哈希的。可哈希性使对象可用作字典键和集合成员,因为这些数据结构在内部使用哈希值。所有 Python 的不可变内置对象都是可哈希的,而可变容器则不是。

比较相等的对象必须具有相同的哈希值。集合非常常用于测试成员资格,因此让我们在以下示例中介绍in运算符:

>>> small_primes = set()  # empty set
>>> small_primes.add(2)  # adding one element at a time
>>> small_primes.add(3)
>>> small_primes.add(5)
>>> small_primes
{2, 3, 5}
>>> small_primes.add(1)  # Look what I've done, 1 is not a prime!
>>> small_primes
{1, 2, 3, 5}
>>> small_primes.remove(1)  # so let's remove it
>>> 3 in small_primes # membership test
True
>>> 4 in small_primes
False
>>> 4 not in small_primes  # negated membership test
True
>>> small_primes.add(3)  # trying to add 3 again
>>> small_primes
{2, 3, 5}  # no change, duplication is not allowed
>>> bigger_primes = set([5, 7, 11, 13])  # faster creation
>>> small_primes | bigger_primes # union operator `|`
{2, 3, 5, 7, 11, 13}
>>> small_primes & bigger_primes  # intersection operator `&`
{5}
>>> small_primes - bigger_primes  # difference operator `-`
{2, 3}

在前面的代码中,您可以看到创建集合的两种不同方法。一种是创建一个空集合,然后逐个添加元素。另一种是使用数字列表作为构造函数的参数来创建集合,这样就可以为我们完成所有工作。当然,您可以从列表或元组(或任何可迭代对象)创建集合,然后随意添加和删除集合成员。

我们将在下一章中查看可迭代对象和迭代。现在,只需知道可迭代对象是可以按方向迭代的对象。

创建集合的另一种方法是简单地使用花括号表示法,如下所示:

>>> small_primes = {2, 3, 5, 5, 3}
>>> small_primes
{2, 3, 5}

请注意,我添加了一些重复以强调结果集不会有任何重复。让我们看一个关于集合类型的不可变对应物frozenset的示例:

>>> small_primes = frozenset([2, 3, 5, 7])
>>> bigger_primes = frozenset([5, 7, 11])
>>> small_primes.add(11)  # we cannot add to a frozenset
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
AttributeError: 'frozenset' object has no attribute 'add'
>>> small_primes.remove(2)  # neither we can remove
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
AttributeError: 'frozenset' object has no attribute 'remove'
>>> small_primes & bigger_primes  # intersect, union, etc. allowed
frozenset({5, 7})

如您所见,frozenset对象在其可变对应物方面相当有限。它们仍然非常有效,用于成员测试、并集、交集和差集操作,以及出于性能原因。

映射类型-字典

在所有内置的 Python 数据类型中,字典很容易是最有趣的一个。它是唯一的标准映射类型,也是每个 Python 对象的支柱。

字典将键映射到值。键需要是可哈希的对象,而值可以是任意类型。字典是可变对象。有很多不同的方法来创建字典,所以让我给你一个简单的例子,演示如何以五种不同的方式创建一个等于{'A': 1, 'Z': -1}的字典:

>>> a = dict(A=1, Z=-1)
>>> b = {'A': 1, 'Z': -1}
>>> c = dict(zip(['A', 'Z'], [1, -1]))
>>> d = dict([('A', 1), ('Z', -1)])
>>> e = dict({'Z': -1, 'A': 1})
>>> a == b == c == d == e  # are they all the same?
True  # They are indeed

你注意到那些双等号了吗?赋值是用一个等号完成的,而要检查一个对象是否与另一个对象相同(或者在这种情况下一次检查五个对象),我们使用双等号。还有另一种比较对象的方法,涉及到is运算符,并检查两个对象是否相同(如果它们具有相同的 ID,而不仅仅是值),但除非你有充分的理由使用它,否则应该使用双等号。在前面的代码中,我还使用了一个很好的函数:zip。它是以现实生活中的拉链命名的,它将两个东西粘合在一起,每次从每个东西中取一个元素。让我给你举个例子:

>>> list(zip(['h', 'e', 'l', 'l', 'o'], [1, 2, 3, 4, 5]))
[('h', 1), ('e', 2), ('l', 3), ('l', 4), ('o', 5)]
>>> list(zip('hello', range(1, 6)))  # equivalent, more Pythonic
[('h', 1), ('e', 2), ('l', 3), ('l', 4), ('o', 5)]

在前面的例子中,我以两种不同的方式创建了相同的列表,一种更明确,另一种更符合 Python 的风格。暂时忘记我不得不在zip调用周围包装list构造函数(原因是因为zip返回一个迭代器,而不是list,所以如果我想看到结果,我需要将该迭代器耗尽到某个东西中——在这种情况下是一个列表),并专注于结果。看看zip是如何将其两个参数的第一个元素配对在一起,然后是第二个元素,然后是第三个元素,依此类推?看看你的裤子(或者你的钱包,如果你是女士),你会看到你的拉链也有相同的行为。但让我们回到字典,看看它们暴露了多少精彩的方法,让我们可以按照我们的意愿来操作它们。

让我们从基本操作开始:

>>> d = {}
>>> d['a'] = 1  # let's set a couple of (key, value) pairs
>>> d['b'] = 2
>>> len(d)  # how many pairs?
2
>>> d['a']  # what is the value of 'a'?
1
>>> d  # how does `d` look now?
{'a': 1, 'b': 2}
>>> del d['a']  # let's remove `a`
>>> d
{'b': 2}
>>> d['c'] = 3  # let's add 'c': 3
>>> 'c' in d  # membership is checked against the keys
True
>>> 3 in d  # not the values
False
>>> 'e' in d
False
>>> d.clear()  # let's clean everything from this dictionary
>>> d
{}

注意访问字典的键,无论我们执行的操作类型如何,都是通过方括号完成的。你还记得字符串、列表和元组吗?我们也是通过方括号访问某个位置的元素,这是 Python 一致性的又一个例子。

现在让我们看看三个特殊的对象,称为字典视图:keysvaluesitems。这些对象提供了字典条目的动态视图,并且在字典更改时会发生变化。keys()返回字典中的所有键,values()返回字典中的所有值,items()返回字典中的所有(键,值)对。

根据 Python 文档:“键和值以任意顺序进行迭代,这个顺序是非随机的,在 Python 的不同实现中会有所不同,并且取决于字典插入和删除的历史。如果在没有对字典进行干预修改的情况下对键、值和项视图进行迭代,那么项的顺序将直接对应”。

够了这些废话;让我们把这些都写成代码:

>>> d = dict(zip('hello', range(5)))
>>> d
{'h': 0, 'e': 1, 'l': 3, 'o': 4}
>>> d.keys()
dict_keys(['h', 'e', 'l', 'o'])
>>> d.values()
dict_values([0, 1, 3, 4])
>>> d.items()
dict_items([('h', 0), ('e', 1), ('l', 3), ('o', 4)])
>>> 3 in d.values()
True
>>> ('o', 4) in d.items()
True

在前面的代码中有几件事情需要注意。首先,注意我们是通过迭代字符串'hello'和列表[0, 1, 2, 3, 4]的 zip 版本来创建字典的。字符串'hello'中有两个'l'字符,它们分别与23的值配对。请注意,在字典中,'l'键的第二次出现(值为3)覆盖了第一次出现(值为2)。另一个需要注意的是,当要求任何视图时,原始顺序现在被保留,而在 3.6 版本之前是不能保证的。

从 Python 3.6 开始,dict类型已经重新实现以使用更紧凑的表示。这导致与 Python 3.5 相比,字典使用的内存减少了 20%到 25%。此外,在 Python 3.6 中,作为一个副作用,字典是本地有序的。这个特性受到社区的热烈欢迎,以至于在 3.7 中它已经成为语言的合法特性,而不再是实现的副作用。如果一个dict记住了键首次插入的顺序,那么它就是有序的。

当我们讨论对集合进行迭代时,我们将看到这些视图是基本工具。现在让我们来看看 Python 字典暴露的一些其他方法;它们有很多,并且非常有用:

>>> d
{'e': 1, 'h': 0, 'o': 4, 'l': 3}
>>> d.popitem()  # removes a random item (useful in algorithms)
('o', 4)
>>> d
{'h': 0, 'e': 1, 'l': 3}
>>> d.pop('l')  # remove item with key `l`
3
>>> d.pop('not-a-key')  # remove a key not in dictionary: KeyError
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
KeyError: 'not-a-key'
>>> d.pop('not-a-key', 'default-value')  # with a default value?
'default-value'  # we get the default value
>>> d.update({'another': 'value'})  # we can update dict this way
>>> d.update(a=13)  # or this way (like a function call)
>>> d
{'h': 0, 'e': 1, 'another': 'value', 'a': 13}
>>> d.get('a')  # same as d['a'] but if key is missing no KeyError
13
>>> d.get('a', 177)  # default value used if key is missing
13
>>> d.get('b', 177)  # like in this case
177
>>> d.get('b')  # key is not there, so None is returned

所有这些方法都很简单易懂,但值得一提的是None。Python 中的每个函数都返回None,除非明确使用return语句返回其他内容,但我们将在探索函数时看到这一点。None经常用于表示值的缺失,并且它经常用作函数声明中参数的默认值。一些经验不足的编程人员有时会编写返回FalseNone的代码。FalseNone在布尔上下文中都会被评估为False,因此它们之间似乎没有太大的区别。但实际上,我认为它们之间有一个非常重要的区别:False表示我们有信息,而我们拥有的信息是FalseNone表示没有信息。没有信息与信息为False是非常不同的。通俗地说,如果你问你的机械师,“我的车准备好了吗?”,答案“不,还没有”(False)和“我不知道”(None)之间有很大的区别。

我非常喜欢字典的最后一个方法是setdefault。它的行为类似于get,但如果键不存在,它还会设置具有给定值的键。让我们看一个例子:

>>> d = {}
>>> d.setdefault('a', 1)  # 'a' is missing, we get default value
1
>>> d
{'a': 1}  # also, the key/value pair ('a', 1) has now been added
>>> d.setdefault('a', 5)  # let's try to override the value
1
>>> d
{'a': 1}  # no override, as expected

所以,我们现在已经到了这次旅行的尽头。通过尝试预测这行代码之后d的样子来测试你对字典的了解:

>>> d = {}
>>> d.setdefault('a', {}).setdefault('b', []).append(1)

如果你不能立刻理解,不要担心。我只是想鼓励你尝试使用字典。

这就结束了我们对内置数据类型的介绍。在我讨论本章中所见内容的一些考虑之前,我想简要地看一下collections模块。

collections 模块

当 Python 通用内置容器(tuplelistsetdict)不够用时,我们可以在collections模块中找到专门的容器数据类型。它们是:

数据类型 描述
namedtuple() 用于创建具有命名字段的元组子类的工厂函数
deque 具有快速追加和弹出的类似列表的容器
ChainMap 用于创建多个映射的单个视图的类似字典的类
Counter 用于计算可散列对象的字典子类
OrderedDict 记住条目添加顺序的字典子类
defaultdict 调用工厂函数以提供缺失值的字典子类
UserDict 用于更容易地对字典进行子类化的字典对象包装器
UserList 用于更容易地对列表进行子类化的列表对象包装器
UserString 用于更容易地对字符串进行子类化的字符串对象包装器

我们没有足够的空间来涵盖所有这些内容,但你可以在官方文档中找到大量的例子,所以在这里我只给出一个小例子来展示namedtupledefaultdictChainMap

namedtuple

namedtuple是一个类似元组的对象,可以通过属性查找访问字段,也可以通过索引和迭代(实际上是tuple的子类)。这在完全自定义对象和元组之间达成了一种妥协,可以在不需要完全自定义对象的情况下使用,但又希望通过避免奇怪的索引使代码更易读的情况下使用。另一个用例是在重构后元组中的项目可能需要改变位置的情况下,这会迫使编码人员也重构所有涉及的逻辑,这可能非常棘手。通常情况下,例子胜过千言万语(还是图片?)。假设我们正在处理关于患者左眼和右眼的数据。我们在常规元组中保存左眼(位置 0)和右眼(位置 1)的一个值。如下所示:

>>> vision = (9.5, 8.8)
>>> vision
(9.5, 8.8)
>>> vision[0]  # left eye (implicit positional reference)
9.5
>>> vision[1]  # right eye (implicit positional reference)
8.8

现在假设我们一直处理vision对象,并且在某个时候设计师决定通过添加组合视觉信息来增强它们,以便vision对象以这种格式存储数据:(左眼,组合,右眼)

现在您看到我们遇到的问题了吗?我们可能有很多依赖于vision[0]是左眼信息(它仍然是)和vision[1]是右眼信息(这不再是情况)的代码。我们必须在处理这些对象的任何地方重构我们的代码,将vision[1]更改为vision[2],这可能很痛苦。我们可能最初可以更好地处理这个问题,使用namedtuple来解决。让我告诉你我的意思:

>>> from collections import namedtuple
>>> Vision = namedtuple('Vision', ['left', 'right'])
>>> vision = Vision(9.5, 8.8)
>>> vision[0]
9.5
>>> vision.left  # same as vision[0], but explicit
9.5
>>> vision.right  # same as vision[1], but explicit
8.8

如果在我们的代码中,我们使用vision.leftvision.right来引用左眼和右眼,那么要解决新的设计问题,我们只需要更改我们的工厂和创建实例的方式。代码的其余部分不需要更改:

>>> Vision = namedtuple('Vision', ['left', 'combined', 'right'])
>>> vision = Vision(9.5, 9.2, 8.8)
>>> vision.left  # still correct
9.5
>>> vision.right  # still correct (though now is vision[2])
8.8
>>> vision.combined  # the new vision[1]
9.2

您可以看到按名称引用这些值比按位置引用更方便。毕竟,有人曾经写道,明确胜于隐晦(你能想起在哪里吗?如果你想不起来,想想)。这个例子可能有点极端;当然,我们的代码设计师不太可能做出这样的改变,但您会惊讶地看到在专业环境中经常发生类似这种问题,以及重构它们是多么痛苦。

defaultdict

defaultdict数据类型是我最喜欢的之一。它允许您通过在第一次访问时插入一个默认值来避免检查字典中是否存在键,其类型是您在创建时传递的默认值。在某些情况下,这个工具可能非常方便,并且可以稍微缩短您的代码。让我们看一个快速的例子。假设我们正在更新age的值,增加一岁。如果age不存在,我们假设它是0,并将其更新为1

>>> d = {}
>>> d['age'] = d.get('age', 0) + 1  # age not there, we get 0 + 1
>>> d
{'age': 1}
>>> d = {'age': 39}
>>> d['age'] = d.get('age', 0) + 1  # age is there, we get 40
>>> d
{'age': 40}

现在让我们看看它如何与defaultdict数据类型一起工作。第二行实际上是一个四行长的if子句的简短版本,如果字典没有get方法,我们将不得不编写它(我们将在第三章中看到所有关于if子句的内容,迭代和做决定):

>>> from collections import defaultdict
>>> dd = defaultdict(int)  # int is the default type (0 the value)
>>> dd['age'] += 1  # short for dd['age'] = dd['age'] + 1
>>> dd
defaultdict(<class 'int'>, {'age': 1})  # 1, as expected

请注意,我们只需要指示defaultdict工厂我们希望在键丢失时使用int数字(我们将得到0,这是int类型的默认值)。还要注意,即使在这个例子中行数没有增加,可读性确实有所提高,这非常重要。您还可以使用不同的技术来实例化defaultdict数据类型,这涉及创建一个工厂对象。要深入了解,请参考官方文档。

ChainMap

ChainMap是 Python 3.3 中引入的一个非常好的数据类型。它的行为类似于普通的字典,但根据 Python 文档的说法:“用于快速链接多个映射,以便它们可以被视为单个单元。” 这通常比创建一个字典并在其上运行多个更新调用要快得多。ChainMap可以用来模拟嵌套作用域,在模板中很有用。底层映射存储在一个列表中。该列表是公共的,可以使用 maps 属性访问或更新。查找依次搜索底层映射,直到找到一个键。相比之下,写入、更新和删除只对第一个映射进行操作。

一个非常常见的用例是提供默认值,让我们看一个例子:

>>> from collections import ChainMap
>>> default_connection = {'host': 'localhost', 'port': 4567}
>>> connection = {'port': 5678}
>>> conn = ChainMap(connection, default_connection)  # map creation
>>> conn['port']  # port is found in the first dictionary
5678
>>> conn['host']  # host is fetched from the second dictionary
'localhost'
>>> conn.maps  # we can see the mapping objects
[{'port': 5678}, {'host': 'localhost', 'port': 4567}]
>>> conn['host'] = 'packtpub.com'  # let's add host
>>> conn.maps
[{'port': 5678, 'host': 'packtpub.com'},
 {'host': 'localhost', 'port': 4567}]
>>> del conn['port']  # let's remove the port information
>>> conn.maps
[{'host': 'packtpub.com'}, {'host': 'localhost', 'port': 4567}]
>>> conn['port']  # now port is fetched from the second dictionary
4567
>>> dict(conn)  # easy to merge and convert to regular dictionary
{'host': 'packtpub.com', 'port': 4567}

我只是喜欢 Python 是如何让你的生活变得轻松。你在一个ChainMap对象上工作,配置第一个映射,当你需要一个包含所有默认项以及自定义项的完整字典时,你只需将ChainMap对象提供给dict构造函数。如果你从未在其他语言(如 Java 或 C++)中编写过代码,你可能无法完全欣赏到这一点,以及 Python 是如何让你的生活变得更加轻松。我可以,每当我不得不在其他语言中编写代码时,我都感到有一种幽闭恐惧症。

枚举

从技术上讲,枚举不是一个内置的数据类型,因为你必须从enum模块中导入它们,但绝对值得一提。它们是在 Python 3.4 中引入的,尽管在专业代码中看到它们并不那么常见(但),我还是想给你举个例子。

官方定义是这样的:“枚举是一组 符号 名称(成员)绑定到唯一的、常量值。在枚举中,成员可以通过标识进行比较,并且枚举本身可以被迭代。”

假设你需要表示交通信号灯。在你的代码中,你可能会这样做:

>>> GREEN = 1
>>> YELLOW = 2
>>> RED = 4
>>> TRAFFIC_LIGHTS = (GREEN, YELLOW, RED)
>>> # or with a dict
>>> traffic_lights = {'GREEN': 1, 'YELLOW': 2, 'RED': 4}

前面的代码没有什么特别之处。实际上,这是非常常见的。但是,考虑改用这种方式:

>>> from enum import Enum
>>> class TrafficLight(Enum):
...     GREEN = 1
...     YELLOW = 2
...     RED = 4
...
>>> TrafficLight.GREEN
<TrafficLight.GREEN: 1>
>>> TrafficLight.GREEN.name
'GREEN'
>>> TrafficLight.GREEN.value
1
>>> TrafficLight(1)
<TrafficLight.GREEN: 1>
>>> TrafficLight(4)
<TrafficLight.RED: 4>

暂时忽略类定义的(相对)复杂性,你可以欣赏到这可能更有优势。数据结构更清晰,提供的 API 更强大。我鼓励你查看官方文档,探索在enum模块中可以找到的所有出色功能。我认为值得探索,至少一次。

最后的考虑

就是这样。现在你已经看到了 Python 中将要使用的数据结构的很大一部分。我鼓励你深入研究 Python 文档,并进一步尝试本章中所见的每一种数据类型。相信我,这是值得的。你将写的一切都与处理数据有关,所以确保你对它的了解是非常牢固的。

在我们跳入第三章 迭代和做决定之前,我想分享一些关于我认为重要且不容忽视的不同方面的最终考虑。

小值缓存

当我们在本章的开头讨论对象时,我们看到当我们将一个名称分配给一个对象时,Python 会创建对象,设置它的值,然后将名称指向它。我们可以将不同的名称分配给相同的值,并且我们期望创建不同的对象,就像这样:

>>> a = 1000000
>>> b = 1000000
>>> id(a) == id(b)
False

在前面的例子中,ab被分配给两个int对象,它们具有相同的值,但它们不是相同的对象,正如你所看到的,它们的id不相同。所以让我们再做一次:

>>> a = 5
>>> b = 5
>>> id(a) == id(b)
True

哦,哦!Python 出问题了吗?为什么现在两个对象是相同的?我们没有做a = b = 5,我们分别设置它们。嗯,答案是性能。Python 缓存短字符串和小数字,以避免它们的副本堵塞系统内存。一切都在幕后正确处理,所以你不需要担心,但确保你记住这种行为,如果你的代码需要处理 ID 的话。

如何选择数据结构

正如我们所见,Python 为你提供了几种内置数据类型,有时,如果你不是很有经验,选择最适合你的可能会很棘手,特别是当涉及到集合时。例如,假设你有许多字典要存储,每个字典代表一个客户。在每个客户字典中,有一个'id': 'code'唯一标识代码。你会把它们放在什么样的集合中?嗯,除非我更多地了解这些客户,很难回答。我需要什么样的访问?我将对每个客户执行什么样的操作,以及多少次?集合会随时间改变吗?我需要以任何方式修改客户字典吗?我将在集合上执行的最频繁的操作是什么?

如果你能回答上述问题,那么你就会知道该选择什么。如果集合从不缩小或增长(换句话说,在创建后不需要添加/删除任何客户对象)或洗牌,那么元组是一个可能的选择。否则,列表是一个很好的选择。不过,每个客户字典都有一个唯一标识符,所以甚至字典也可以工作。让我为你列出这些选项:

# example customer objects 
customer1 = {'id': 'abc123', 'full_name': 'Master Yoda'} 
customer2 = {'id': 'def456', 'full_name': 'Obi-Wan Kenobi'} 
customer3 = {'id': 'ghi789', 'full_name': 'Anakin Skywalker'} 
# collect them in a tuple 
customers = (customer1, customer2, customer3) 
# or collect them in a list 
customers = [customer1, customer2, customer3] 
# or maybe within a dictionary, they have a unique id after all 
customers = { 
    'abc123': customer1, 
    'def456': customer2, 
    'ghi789': customer3, 
} 

我们有一些客户在那里,对吧?我可能不会选择元组选项,除非我想强调集合不会改变。我会说通常列表更好,因为它更灵活。

另一个要记住的因素是元组和列表是有序集合。如果使用字典(Python 3.6 之前)或集合,就会失去顺序,因此需要知道在你的应用程序中是否重要。

那性能呢?例如,在列表中,插入和成员操作可能需要O(n)的时间,而在字典中则为O(1)。不过,如果我们不能保证可以通过其属性之一唯一标识集合中的每个项目,并且该属性是可散列的(因此可以成为dict中的键),则不一定总是能使用字典。

如果你想知道O(n)O(1)是什么意思,请谷歌大 O 符号。在这个语境中,让我们说,如果在数据结构上执行一个操作Op需要O(f(n))的时间,那么意味着Op最多需要时间t ≤ c * f(n)来完成,其中c是某个正常数,n是输入的大小,f是某个函数。所以,把O(...)看作是操作运行时间的上限(当然也可以用来衡量其他可测量的数量)。

另一种了解你是否选择了正确的数据结构的方法是看你必须编写的代码。如果一切都很容易并且自然流畅,那么你可能已经选择正确了,但如果你发现自己认为你的代码变得不必要地复杂,那么你可能应该尝试并决定是否需要重新考虑你的选择。不过,没有实际案例很难给出建议,所以当你为你的数据选择数据结构时,试着记住易用性和性能,并优先考虑你所处环境中最重要的事情。

关于索引和切片

在本章的开头,我们看到了字符串上应用的切片。切片通常适用于序列:元组、列表、字符串等。对于列表,切片也可以用于赋值。我几乎从未在专业代码中看到过这种用法,但是,您知道您可以这样做。您能对字典或集合进行切片吗?我听到您尖叫,当然不能!。太棒了;我看到我们在同一个页面上,所以让我们谈谈索引。

关于 Python 索引的一个特点我之前没有提到。我将通过一个例子来向您展示。如何解决集合的最后一个元素?让我们看看:

>>> a = list(range(10))  # `a` has 10 elements. Last one is 9.
>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> len(a)  # its length is 10 elements
10
>>> a[len(a) - 1]  # position of last one is len(a) - 1
9
>>> a[-1]  # but we don't need len(a)! Python rocks!
9
>>> a[-2]  # equivalent to len(a) - 2
8
>>> a[-3]  # equivalent to len(a) - 3
7

如果列表a有 10 个元素,由于 Python 的 0 索引定位系统,第一个元素位于位置 0,最后一个元素位于位置 9。在前面的例子中,元素方便地放置在与其值相等的位置:0位于位置 0,1位于位置 1,依此类推。

因此,为了获取最后一个元素,我们需要知道整个列表(或元组,或字符串等)的长度,然后减去1。因此:len(a) - 1。这是一个如此常见的操作,Python 为您提供了一种使用负索引检索元素的方法。这在进行数据操作时非常有用。这是关于字符串"HelloThere"(这是 Obi-Wan Kenobi 讽刺地向 Grievous 将军问候)索引工作的一个很好的图表:

尝试解决大于9或小于-10的索引将引发IndexError,这是预期的。

关于名称

您可能已经注意到,为了使示例尽可能简短,我使用了简单的字母来调用许多对象,如abcd等。当您在控制台上调试或显示a + b == 7时,这是完全可以的,但是当涉及专业编码(或任何类型的编码)时,这是不好的做法。我希望您能原谅我有时这样做;原因是为了以更紧凑的方式呈现代码。

然而,在真实环境中,当您为数据选择名称时,您应该仔细选择,并且它们应该反映数据的内容。因此,如果您有一组Customer对象,customers是一个完全合适的名称。customers_listcustomers_tuplecustomers_collection也能起作用吗?请考虑一下。将集合的名称与数据类型绑定是好的吗?我认为在大多数情况下不是。因此,我会说,如果您有充分的理由这样做,那就去做;否则,不要这样做。原因是,一旦customers_tuple开始在代码的不同位置使用,并且您意识到实际上您想要使用列表而不是元组,您将需要进行一些有趣的重构(也称为浪费时间)。数据的名称应该是名词,函数的名称应该是动词。名称应该尽可能具有表现力。实际上,Python 在命名方面是一个很好的例子。大多数情况下,如果您知道函数的作用,您可以猜到函数的名称。疯狂,对吧?

《代码整洁之道》的第二章《有意义的命名》Robert C. MartinPrentice Hall*完全致力于命名。这是一本了不起的书,它以许多不同的方式帮助我改进了我的编码风格,如果您想将编码提升到更高的水平,这是一本必读的书。

总结

在本章中,我们已经探讨了 Python 的内置数据类型。我们已经看到了有多少种类型,并且只需使用它们的不同组合就可以实现多少。

我们已经看到了数字类型、序列、集合、映射、集合(以及Enum的特别嘉宾亮相),我们已经看到了一切都是对象,我们已经学会了可变和不可变之间的区别,我们还学会了切片和索引(以及自豪地学会了负索引)。

我们提供了简单的例子,但是关于这个主题,您还可以学到更多,所以请查阅官方文档并进行探索。

最重要的是,我鼓励你自己尝试所有的练习,让你的手指使用那些代码,建立一些肌肉记忆,并进行实验,实验,实验。学习当你除以零时会发生什么,当你将不同的数字类型组合成一个表达式时会发生什么,当你处理字符串时会发生什么。玩转所有的数据类型。锻炼它们,打破它们,发现它们所有的方法,享受它们,并且非常非常好地学习它们。

如果你的基础不是非常牢固的,你的代码能有多好呢?数据是一切的基础。数据塑造了周围的一切。

随着你在书中的进展,很可能会发现我的代码(或你的代码)中有一些不一致或可能是一些小的拼写错误。你会收到一个错误消息,某些东西会出错。这太棒了!编码时,事情总是会出错,你会一直进行调试和修复,所以把错误视为学习有关你正在使用的语言的新知识的有用练习,而不是失败或问题。错误会一直出现,直到你的最后一行代码,这是肯定的,所以最好现在就开始接受它们。

下一章是关于迭代和做决策的。我们将看到如何实际利用这些集合,并根据我们所提供的数据做出决策。现在你的知识正在积累,我们将开始加快速度,所以在你进入下一章之前,请确保你对本章的内容感到舒适。再一次,玩得开心,探索,打破事物。这是学习的一个非常好的方式。

posted @ 2024-04-16 16:14  绝不原创的飞龙  阅读(23)  评论(0编辑  收藏  举报