原文:
zh.annas-archive.org/md5/406733548F67B770B962DA4756270D5F
译者:飞龙
前言
当我开始写这本书的第一版时,我对预期的内容知之甚少。渐渐地,我学会了如何将每个主题转化为一个故事。我想通过提供有用、简单、易于理解的例子来谈论 Python,但与此同时,我也想将自己的经验倾注到书页中,我在这些年学到的任何我认为对读者有价值的东西——一些值得思考、反思,并且希望能够吸收的东西。读者可能会持不同意见,提出不同的做事方式,但希望是更好的方式。
我希望这本书不仅仅是关于语言,而是关于编程。事实上,编程的艺术包括许多方面,语言只是其中之一。
编程的另一个关键方面是独立性。当你遇到难题,不知道如何解决面临的问题时,能够自我解锁的能力。没有一本书可以教会这一点,所以我想,我不会试图教授这一方面,而是尝试训练读者。因此,我在整本书中留下了评论、问题和备注,希望能激发读者的灵感。我希望他们能花时间浏览网页或官方文档,深入挖掘,学到更多,发现自己独立解决问题的乐趣。
最后,我希望写一本即使在呈现方式上也稍有不同的书。因此,我决定与我的编辑一起,以理论的方式写第一部分,介绍描述 Python 特性的主题,并且有第二部分由各种真实项目组成,向读者展示使用这种语言可以实现多少。
在考虑了所有这些目标之后,我不得不面对最艰巨的挑战:将我想写的所有内容放入允许的页数范围内。这是很困难的,也做出了牺牲。
我的努力得到了回报:至今,近 3 年过去了,我仍然不时地收到读者的可爱留言,感谢我并告诉我诸如“你的书让我变得更有力量”。对我来说,这是最美的赞美。我知道语言可能会改变和消逝,但我已经成功地与读者分享了一些我的知识,这些知识将与他们保持在一起。
现在,我已经写了这本书的第二版,这一次,我有了更多的空间。所以我决定增加一章关于 IO,这是非常需要的,我甚至有机会增加两章,一章关于秘密,一章关于并发执行。后者绝对是整本书中最具挑战性的章节,其目的是激励读者达到一个能够轻松消化其中代码并理解其概念的水平。
我保留了所有原始章节,除了最后一个略显多余的章节。它们都已经得到更新,符合最新版本的 Python,即写作时的 3.7 版本。
当我看着这本书时,我看到了一个更加成熟的产品。有更多的章节,内容已重新组织以更好地适应叙述,但书的灵魂仍在那里。最重要的一点,赋予读者力量,仍然完整无缺。
我希望这个版本会比上一个版本更成功,并且能帮助读者成为优秀的程序员。我希望帮助他们发展批判性思维、优秀的技能,并且能够随着时间的推移适应,这都要归功于他们从这本书中获得的坚实基础。
这本书是为谁写的
Python 是美国顶尖计算机科学大学最受欢迎的入门教学语言,因此如果你是软件开发新手,或者经验不足并希望从正确的角度开始,那么这门语言和这本书就是你需要的。它惊人的设计和可移植性将帮助你在选择工作环境时变得更加高效。
如果你已经使用过 Python 或其他任何语言,这本书仍然对你有用,既可以作为 Python 基础知识的参考,也可以提供二十年经验积累的各种考虑和建议。
本书涵盖的内容
第一章,“Python 的初步介绍”,向你介绍了基本的编程概念。它指导你如何在计算机上运行 Python,并向你介绍了一些构造。
第二章,“内置数据类型”,向你介绍了 Python 的内置数据类型。Python 拥有非常丰富的本地数据类型,本章将为你介绍每种类型的描述和简短示例。
第三章,“迭代和决策”,教你如何通过检查条件、应用逻辑和执行循环来控制代码流程。
第四章,“函数,代码的构建块”,教会你如何编写函数。函数是重用代码的关键,可以减少调试时间,并且通常可以编写更好的代码。
第五章,“节省时间和内存”,向你介绍了 Python 编程的功能方面。本章教你如何编写理解和生成器,这是你可以用来加快代码速度和节省内存的强大工具。
第六章,“面向对象编程、装饰器和迭代器”,教你使用 Python 的面向对象编程基础知识。它向你展示了这种范式的关键概念和所有潜力。它还向你展示了 Python 最受喜爱的特性之一:装饰器。最后,它还涵盖了迭代器的概念。
第七章,“文件和数据持久性”,教你如何处理文件、流、数据交换格式和数据库等内容。
第八章,“测试、性能分析和处理异常”,教你如何使用测试和性能分析等技术使你的代码更加健壮、快速和稳定。它还正式定义了异常的概念。
第九章,“加密和令牌”,涉及安全、哈希、加密和令牌等概念,这些是当今日常编程的一部分。
第十章,“并发执行”,是一个具有挑战性的章节,描述了如何同时做很多事情。它介绍了这个主题的理论方面,然后提供了三个不同技术开发的好练习,从而使读者能够理解所呈现的范式之间的差异。
第十一章,“调试和故障排除”,向你展示了调试代码的主要方法,以及如何应用这些方法的一些示例。
第十二章,“GUI 和脚本”,从两个不同的角度指导你完成一个示例。它们处于光谱的两端:一个实现是脚本,另一个是一个合适的图形用户界面应用程序。
第十三章,数据科学,介绍了一些关键概念和一个非常特殊的工具,Jupyter Notebook。
第十四章,Web 开发,介绍了 Web 开发的基础知识,并使用 Django Web 框架提供了一个项目。示例将基于正则表达式。
为了充分利用本书
鼓励您按照本书中的示例进行操作。为此,您需要一台计算机,一个互联网连接和一个浏览器。本书是用 Python 3.7 编写的,但在很大程度上,它也适用于任何最近的 Python 3.*版本。我已经给出了如何在您的操作系统上安装 Python 的指南。这些程序一直在变化,因此您需要参考网络上最新的指南,以找到精确的设置说明。我还解释了如何安装各种示例中使用的所有额外库,并在读者在安装它们时遇到任何问题时提供了建议。不需要特定的编辑器来输入代码;但是,我建议那些有兴趣跟随示例的人考虑采用适当的编码环境。我在第一章中对此问题提出了建议。
下载示例代码文件
您可以从您的帐户在www.packtpub.com下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。
您可以按照以下步骤下载代码文件:
-
登录或注册www.packtpub.com。
-
选择“支持”选项卡。
-
点击“代码下载和勘误”。
-
在搜索框中输入书名,然后按照屏幕上的说明操作。
下载文件后,请确保使用最新版本的解压缩或提取文件夹:
-
WinRAR/7-Zip 适用于 Windows
-
Zipeg/iZip/UnRarX 适用于 Mac
-
7-Zip/PeaZip 适用于 Linux
该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Learn-Python-Programming-Second-Edition
。如果代码有更新,将在现有的 GitHub 存储库上进行更新。
我们还有其他代码包来自我们丰富的图书和视频目录,可在github.com/PacktPublishing/
上找到。去看看吧!
使用的约定
本书中使用了许多文本约定。
CodeInText
:表示文本中的代码词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄。这是一个例子:“在learn.pp
文件夹中,我们将创建一个名为learnpp
的虚拟环境。”
代码块设置如下:
# we define a function, called local
def local():
m = 7
print(m)
当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体设置:
# key.points.mutable.assignment.py
x = [1, 2, 3]
def func(x):
x[1] = 42 # this changes the caller!
x = 'something else' # this points x to a new string object
任何命令行输入或输出都以以下方式编写:
>>> import sys
>>> print(sys.version)
粗体:表示新术语,重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。这是一个例子:“要在 Windows 中打开控制台,转到“开始”菜单,选择“运行”,然后输入cmd
。”
警告或重要说明会出现在这样的地方。提示和技巧会出现在这样的地方。
第一章:Python 的简介
“授人以鱼不如授人以渔”-中国谚语
根据维基百科,计算机编程是:
“……从最初的计算问题表述到可执行的计算机程序的过程。编程涉及活动,如分析、开发理解、生成算法、验证算法的要求,包括它们的正确性和资源消耗,以及在目标编程语言中实现(通常称为编码)算法。”
简而言之,编码就是用计算机能理解的语言告诉计算机做某事。
计算机是非常强大的工具,但不幸的是,它们无法自行思考。它们需要被告知一切:如何执行任务,如何评估条件以决定要遵循哪条路径,如何处理来自设备的数据,比如网络或磁盘,以及在发生意外情况时如何做出反应,比如,某物坏了或丢失了。
你可以用许多不同的风格和语言编写代码。难吗?我会说是和不是。这有点像写作。每个人都可以学会写作,你也可以。但是,如果你想成为一名诗人呢?那么仅仅写作是不够的。你还需要掌握一整套其他技能,这将需要更长时间和更大的努力。
最终,一切都取决于你想走多远。编码不仅仅是将一些有效的指令组合在一起。它远不止如此!
良好的代码是简短、快速、优雅、易于阅读和理解、简单、易于修改和扩展、易于扩展和重构、易于测试。要能够同时具备所有这些品质的代码需要时间,但好消息是,通过阅读这本书,你正在迈出迈向这个目标的第一步。我毫不怀疑你能做到。任何人都可以;事实上,我们都在不知不觉中一直在编程。
你想要一个例子吗?
假设你想泡速溶咖啡。你需要一个杯子,速溶咖啡罐,一茶匙,水和水壶。即使你没有意识到,你正在评估大量的数据。你要确保水壶里有水,水壶已经插上电,杯子是干净的,罐子里有足够的咖啡。然后,你煮水,也许在此期间,你把一些咖啡放在杯子里。当水煮好时,你把它倒进杯子里,然后搅拌。
那么,这和编程有什么关系呢?
嗯,我们收集了资源(水壶、咖啡、水、茶匙和杯子),并验证了一些关于它们的条件(水壶已插上电,杯子是干净的,罐子里有足够的咖啡)。然后我们开始了两个动作(煮水和把咖啡放在杯子里),当它们都完成时,我们最终通过把水倒进杯子里并搅拌来结束了这个过程。
你能看到吗?我刚刚描述了一个咖啡程序的高级功能。这并不难,因为这就是大脑整天在做的事情:评估条件,决定采取行动,执行任务,重复其中一些,并在某个时刻停下来。清理物品,放回去,等等。
现在你所需要做的就是学会如何分解你在现实生活中自动执行的所有这些动作,以便计算机实际上能够理解它们。你还需要学习一种语言,来指导它。
这就是这本书的目的。我会告诉你如何做,我会尝试通过许多简单但专注的例子来做到这一点(我最喜欢的类型)。
在本章中,我们将涵盖以下内容:
-
Python 的特点和生态系统
-
如何开始并运行 Python 和虚拟环境的指南
-
如何运行 Python 程序
-
如何组织 Python 代码和 Python 的执行模型
一个适当的介绍
我喜欢在教编码时引用现实世界;我相信这有助于人们更好地记住概念。然而,现在是时候更严谨地从技术角度看待编码是什么了。
当我们编写代码时,我们正在指示计算机要做的事情。动作发生在哪里?在许多地方:计算机内存、硬盘、网络电缆、CPU 等等。这是一个完整的世界,大多数时候是真实世界的一个子集的表示。
如果您编写了一个允许人们在线购买衣服的软件,那么您将不得不在程序的范围内代表真实的人、真实的衣服、真实的品牌、尺寸等等。
为了做到这一点,您需要在编写的程序中创建和处理对象。一个人可以是一个对象。一辆汽车是一个对象。一双袜子是一个对象。幸运的是,Python 非常了解对象。
任何对象具有的两个主要特征是属性和方法。让我们以一个人对象为例。在计算机程序中,您通常将人表示为顾客或员工。您存储在他们身上的属性是姓名、社会安全号码、年龄、是否有驾照、电子邮件、性别等等。在计算机程序中,您存储了您需要的所有数据,以便使用对象来实现您的目的。如果您正在编写一个销售服装的网站,您可能还想存储客户的身高和体重以及其他测量数据,以便为他们推荐合适的衣服。因此,属性是对象的特征。我们一直在使用它们:你能把那支笔递给我吗?—哪一支?—黑色的那支。在这里,我们使用了笔的黑色属性来识别它(很可能是在蓝色和红色中)。
方法是对象可以执行的操作。作为一个人,我有诸如说话、走路、睡觉、醒来、吃饭、做梦、写作、阅读等方法。我能做的所有事情都可以看作是代表我的对象的方法。
所以,现在您知道对象是什么,它们公开了可以运行的方法和可以检查的属性,您已经准备好开始编码了。实际上,编码只是简单地管理我们在软件中再现的世界子集中生活的那些对象。您可以随意创建、使用、重用和删除对象。
根据官方 Python 文档上的数据模型章节(docs.python.org/3/reference/datamodel.html
):
“对象是 Python 程序中数据的抽象。Python 程序中的所有数据都由对象或对象之间的关系表示。”
我们将在第六章中更仔细地研究 Python 对象,OOP、装饰器和迭代器。目前,我们需要知道的是 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 Index(PyPI)上免费访问。当你编写 Python 代码时,当你意识到你需要某个特定功能时,大多数情况下,至少有一个库已经为你实现了这个功能。
软件质量
Python 非常注重可读性、连贯性和质量。语言的统一性使得高可读性成为可能,这在当今编码更多是集体努力而不是个人努力的情况下至关重要。Python 的另一个重要方面是其固有的多范式特性。你可以将它用作脚本语言,但也可以利用面向对象、命令式和函数式编程风格。它是多才多艺的。
软件集成
Python 有一个重要的方面是它可以扩展和与许多其他语言集成,这意味着即使一家公司正在使用不同的语言作为他们的主流工具,Python 可以作为一个粘合剂在复杂的应用程序之间起到沟通的作用。这是一个高级话题,但在现实世界中,这个特性非常重要。
满足和享受
最后,但同样重要的是,这很有趣!使用 Python 很有趣。我可以编码 8 个小时,离开办公室时感到快乐和满意,对于其他程序员必须忍受的挣扎来说,他们使用的语言没有提供同样数量的精心设计的数据结构和构造。毫无疑问,Python 让编码变得有趣。有趣促进了动力和生产力。
这些是我为什么会向每个人推荐 Python 的主要方面。当然,还有许多其他技术和高级特性,我本可以谈论,但它们并不真正属于像这样的入门部分。它们会在这本书的每一章中自然地出现。
有什么缺点?
也许,唯一的缺点是 Python 的执行速度,这不是由于个人偏好造成的。通常情况下,Python 比它的编译兄弟慢。Python 的标准实现在运行应用程序时会产生一个称为字节码的源代码的编译版本(扩展名为.pyc
),然后由 Python 解释器运行。这种方法的优势是可移植性,但由于 Python 没有像其他语言那样编译到机器级别,我们付出了速度减慢的代价。
然而,Python 的速度在今天很少是一个问题,因此它被广泛使用,尽管有这个次优特性。实际上,硬件成本已经不再是一个问题,通常很容易通过并行化任务来提高速度。此外,许多程序花费大部分时间等待 IO 操作完成;因此,原始执行速度通常是整体性能的次要因素。不过,当涉及到大量计算时,人们可以切换到更快的 Python 实现,比如 PyPy,通过实现先进的编译技术,它提供了平均五倍的加速(参考pypy.org/
)。
在进行数据科学时,你很可能会发现你使用的 Python 库,如Pandas和NumPy,由于它们的实现方式,实现了本地速度。
如果这不是一个足够好的论点,你可以考虑 Python 已经被用来驱动 Spotify 和 Instagram 等服务的后端,其中性能是一个问题。尽管如此,Python 已经完美地完成了它的工作。
今天谁在使用 Python?
还不确定?让我们简要看一下今天正在使用 Python 的公司:Google、YouTube、Dropbox、Yahoo!、Zope Corporation、Industrial Light & Magic、Walt Disney Feature Animation、Blender 3D、Pixar、NASA、NSA、Red Hat、Nokia、IBM、Netflix、Yelp、Intel、Cisco、HP、Qualcomm 和 JPMorgan Chase 等等。
甚至像Battlefield 2、Civilization IV和QuArK这样的游戏也是用 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 和所需的库并使其运行需要一些技巧。对于 Python 程序员来说,Linux 和 macOS 似乎是最用户友好的操作系统;另一方面,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 交互式 shell简单地称为Python 控制台。
要在 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 框架,我们将在第十四章 Web Development中深入了解。假设你为 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 版本,您需要正确运行命令。当您想要使用 virtualenv 时,另一件需要做的事情是激活它。激活 virtualenv 基本上在幕后进行一些路径操作,这样当您调用 Python 解释器时,实际上调用的是活动的虚拟环境,而不是单纯的系统环境。
我将在我的 Macintosh 控制台上展示一个完整的示例。我们将:
-
在您的项目根目录下创建一个名为
learn.pp
的文件夹(在我的情况下是一个名为srv
的文件夹,在我的主文件夹中)。请根据您在系统上喜欢的设置调整路径。 -
在
learn.pp
文件夹中,我们将创建一个名为learnpp
的虚拟环境。
一些开发人员更喜欢使用相同的名称来调用所有虚拟环境(例如.venv
)。这样他们就可以通过知道项目名称来运行脚本来针对任何虚拟环境。.venv
中的点是因为在 Linux/macOS 中,用点作为名称的前缀会使该文件或文件夹变为不可见。
-
创建虚拟环境后,我们将激活它。在 Linux、macOS 和 Windows 之间的方法略有不同。
-
然后,我们将通过运行 Python 交互式 shell 来确保我们正在运行所需的 Python 版本(3.7.*)。
-
最后,我们将使用
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 脚本来完成所有那些如果手动完成会花费我几分钟甚至更长时间的琐碎任务,最终我决定自动化。我们将在第十二章 GUIs and Scripts中的 Python 脚本部分进行讨论。
运行 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
最后的操作向你展示了一些令人难以置信的东西。我们计算2
的1024
次方,Python 毫不费力地完成了这个任务。试着在 Java、C++或 C#中做这个,不会成功,除非你使用特殊的库来处理这么大的数字。
我每天都使用交互式外壳。它非常有用,可以快速调试,例如,检查数据结构是否支持某个操作。或者检查或运行一段代码。
当您使用 Django(一个 Web 框架)时,交互式外壳与之耦合,并允许您通过框架工具,检查数据库中的数据等方式来工作。您会发现交互式外壳很快会成为您在即将开始的旅程中最亲密的朋友之一。
另一个解决方案,以更美观的图形布局呈现,是使用集成开发环境(IDE)。这是一个相当简单的 IDE,主要面向初学者。它具有比控制台中获得的裸交互式外壳更多的功能,因此您可能希望探索它。它在 Windows Python 安装程序中免费提供,并且您可以轻松在任何其他系统中安装它。您可以在 Python 网站上找到有关它的信息。
Guido Van Rossum 以英国喜剧团体 Monty Python 的名字命名了 Python,因此有传言称 IDLE 的名字是为了纪念 Monty Python 的创始成员之一 Eric Idle 而选择的。
作为服务运行 Python
除了作为脚本运行,并且在 shell 的边界内,Python 还可以编码并作为应用程序运行。我们将在本书中看到许多关于这种模式的示例。我们将在稍后讨论 Python 代码是如何组织和运行的时候更多地了解它。
作为 GUI 应用程序运行的 Python
Python 也可以作为图形用户界面(GUI)运行。有几个可用的框架,其中一些是跨平台的,另一些是特定于平台的。在第十二章《GUI 和脚本》中,我们将看到使用 Tkinter 创建的 GUI 应用程序的示例,Tkinter 是一个面向对象的层,位于 Tk(Tkinter 表示 Tk 界面)之上。
Tk 是一个 GUI 工具包,它将桌面应用程序开发提升到比传统方法更高的水平。它是Tool Command Language(Tcl)的标准 GUI,但也是许多其他动态语言的标准 GUI,并且可以生成在 Windows、Linux、macOS X 等系统下无缝运行的丰富本机应用程序。
Tkinter 与 Python 捆绑在一起;因此,它为程序员提供了轻松访问 GUI 世界的便利,并且出于这些原因,我选择它作为我将在本书中呈现的 GUI 示例的框架。
在其他 GUI 框架中,我们发现以下是最广泛使用的:
-
PyQt
-
wxPython
-
PyGTK
详细描述它们超出了本书的范围,但您可以在 Python 网站上找到所有您需要的信息(https://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.py
和run.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.py
,run.py
,util
等等的例子。
在我们的例子中,包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 的定义时,提示从>>>
变成了...
?那是因为定义跨越了多行。
那么,n
,address
和employee
是什么?它们是名称。我们可以使用这些名称在我们的代码中检索数据。它们需要被保存在某个地方,这样每当我们需要检索这些对象时,我们可以使用它们的名称来获取它们。我们需要一些空间来保存它们,因此:命名空间!
命名空间因此是从名称到对象的映射。例如内置名称集(包含在任何 Python 程序中始终可访问的函数)、模块中的全局名称和函数中的局部名称。甚至对象的属性集也可以被视为命名空间。
命名空间的美妙之处在于它们允许您以清晰的方式定义和组织您的名称,而不会重叠或干扰。例如,与我们在图书馆寻找的那本书相关联的命名空间可以用来导入书本本身,就像这样:
from library.second_floor.section_x.row_three import book
我们从library
命名空间开始,通过点(.
)运算符,我们进入该命名空间。在这个命名空间中,我们寻找second_floor
,再次使用.
运算符进入它。然后我们进入section_x
,最后在最后一个命名空间row_three
中,我们找到了我们要找的名称:book
。
当我们处理真实的代码示例时,通过命名空间的遍历将更加清晰。现在,只需记住命名空间是名称与对象相关联的地方。
还有一个概念,与命名空间的概念密切相关,我想简要谈谈:作用域。
作用域
根据 Python 的文档:
“作用域是 Python 程序的文本区域,其中命名空间是直接可访问的。”
直接可访问意味着当你寻找一个未经修饰的名称引用时,Python 会尝试在命名空间中找到它。
作用域是静态确定的,但实际上在运行时是动态使用的。这意味着通过检查源代码,你可以知道对象的作用域是什么,但这并不阻止软件在运行时改变它。Python 提供了四种不同的作用域(当然不一定同时都存在):
-
local作用域,是最内层的作用域,包含局部名称。
-
enclosing作用域,即任何封闭函数的作用域。它包含非局部名称和非全局名称。
-
global作用域包含全局名称。
-
built-in作用域包含内置名称。Python 带有一组函数,可以直接使用,例如
print
、all
、abs
等。它们存在于内置作用域中。
规则是:当我们引用一个名称时,Python 会从当前命名空间开始查找它。如果找不到该名称,Python 会继续搜索封闭作用域,直到搜索内置作用域。如果在搜索内置作用域后仍然找不到名称,那么 Python 会引发一个NameError
异常,这基本上意味着该名称未被定义(你在前面的例子中看到了这一点)。
因此,在查找名称时,命名空间的扫描顺序是:local,enclosing,global,built-in(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
我们在控制台上看到两个数字打印出来:5
和7
。
发生的情况是,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
指令仍然是之前的m
。m
仍然没有在函数本身中定义,所以 Python 按照 LEGB 顺序遍历作用域。这次m
在封闭作用域中找到了。
现在如果这还不是很清楚,不要担心。随着我们在书中的例子,你会慢慢理解的。Python 教程的Classes部分(docs.python.org/3/tutorial/classes.html
)有一段有趣的关于作用域和命名空间的段落。如果你想更深入地理解这个主题,一定要在某个时候阅读它。
在我们结束本章之前,我想再多谈谈对象。毕竟,基本上 Python 中的一切都是对象,所以我认为它们值得更多的关注。
对象和类
当我在本章的A proper introduction部分介绍对象时,我说我们用它们来代表现实生活中的对象。例如,我们现在在网上销售各种商品,我们需要能够适当地处理、存储和表示它们。但对象实际上远不止于此。在 Python 中,你将要做的大部分事情都与操作对象有关。
因此,不要深入细节(我们将在第六章中做),我想给你一个关于类和对象的简明扼要的解释。
我们已经看到对象是 Python 对数据的抽象。事实上,Python 中的一切都是对象,包括数字,字符串(保存文本的数据结构),容器,集合,甚至函数。你可以把它们想象成至少具有三个特征的盒子:一个 ID(唯一),一个类型和一个值。
但是它们是如何产生的呢?我们如何创建它们?我们如何编写我们自己的自定义对象?答案就在一个简单的词中:classes。
事实上,对象是类的实例。Python 的美妙之处在于类本身也是对象,但我们不要深入研究这一点。这会导致这种语言中最先进的概念之一:元类。现在,你理解类和对象之间的区别最好的方法是通过一个例子。
比如一个朋友告诉你,我买了一辆新自行车! 你立刻明白她在说什么。你看到了自行车吗?没有。你知道它是什么颜色吗?不知道。品牌呢?也不知道。你对它了解多少?一无所知。但与此同时,你知道你需要了解的一切,以理解你的朋友告诉你她买了一辆新自行车的意思。你知道自行车有两个轮子连接在一个框架上,有一个鞍座,脚踏板,把手,刹车等等。换句话说,即使你没有看到自行车本身,你知道自行车的概念。一组抽象的特征和特性共同形成了所谓的自行车。
在计算机编程中,这就是所谓的class。就是这么简单。类用于创建对象。实际上,对象被称为类的实例。
换句话说,我们都知道自行车是什么;我们知道这个类。但是我有自己的自行车,它是自行车类的一个实例。我的自行车是一个具有自己特征和方法的对象。你也有自己的自行车。同一个类,但不同的实例。世界上制造的每一辆自行车都是自行车类的一个实例。
让我们看一个例子。我们将编写一个定义自行车的类,然后我们将创建两辆自行车,一辆红色,一辆蓝色。我会保持代码非常简单,但如果你不完全理解它,不要担心;你现在需要关心的是理解类和对象(或类的实例)之间的区别:
# 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_bike
的brake
方法。
最后要注意的一件事。你还记得我告诉过你对象的属性集被认为是一个命名空间吗?我希望现在我说的更清楚了。你可以看到通过不同的命名空间(red_bike
,blue_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。你可以在 Python 网站上找到一份全面的开源 IDE 列表(只需谷歌 Python IDEs)。我个人使用 Sublime 文本编辑器。它是免费试用的,只需几美元。我一生中尝试过许多 IDE,但这是让我最有效率的一个。
两个重要的建议:
-
无论你选择使用哪种 IDE,都要努力学会它,以便能够充分利用它的优势,但不要依赖它。偶尔练习使用 VIM(或任何其他文本编辑器);学会在任何平台上使用任何一套工具进行工作。
-
无论你使用什么文本编辑器/集成开发环境,在编写 Python 时,缩进是四个空格。不要使用制表符,也不要将它们与空格混合使用。使用四个空格,不是两个,不是三个,也不是五个。就使用四个。整个世界都是这样工作的,你不想因为你喜欢三个空格的布局而成为局外人。
总结
在本章中,我们开始探索编程和 Python 的世界。我们只是略微触及了一些概念,这些概念将在本书的后面更详细地讨论。
我们谈到了 Python 的主要特性,谁在使用它以及为什么使用它,以及我们可以用哪些不同的方式编写 Python 程序。
在本章的最后部分,我们简要介绍了命名空间、作用域、类和对象的基本概念。我们还看到了如何使用模块和包组织 Python 代码。
在实际操作中,我们学会了如何在系统上安装 Python,如何确保我们拥有所需的工具pip
和 virtualenv,并创建并激活了我们的第一个虚拟环境。这将使我们能够在一个独立的环境中工作,而不会危及 Python 系统安装。
现在你已经准备好和我一起开始这段旅程了。你所需要的只是热情、一个激活的虚拟环境、这本书、你的手指和一些咖啡。
尝试跟着示例走;我会让它们简单而简短。如果你把它们放在指尖下,你会比仅仅阅读它们更好地记住它们。
在下一章中,我们将探索 Python 丰富的内置数据类型。有很多内容需要涵盖和学习!
第二章:内置数据类型
“数据!数据!数据!”他不耐烦地喊道。“没有黏土,我就无法制造砖块。”– 福尔摩斯 – 铜山毛榉的冒险
你在计算机上所做的一切都是在管理数据。数据有许多不同的形状和风味。这是你听的音乐,你流媒体的电影,你打开的 PDF。甚至你正在阅读的本章的来源只是一个文件,也就是数据。
数据可以是简单的,比如用整数表示年龄,也可以是复杂的,比如在网站上下的订单。它可以是关于单个对象或关于它们的集合。数据甚至可以是关于数据的,也就是元数据。描述其他数据结构的设计或描述应用程序数据或其上下文的数据。在 Python 中,对象是数据的抽象,Python 有各种各样的数据结构,你可以用它们来表示数据,或者组合它们来创建自己的自定义数据。
在这一章中,我们将涵盖以下内容:
-
Python 对象的结构
-
可变性和不可变性
-
内置数据类型:数字、字符串、序列、集合和映射类型
-
集合模块
-
枚举
一切都是对象
在我们深入具体内容之前,我希望你对 Python 中的对象非常清楚,所以让我们再多谈一点关于它们。正如我们已经说过的,Python 中的一切都是对象。但是当你在 Python 模块中输入age = 42
这样的指令时,真正发生了什么呢?
如果你去pythontutor.com/
,你可以在文本框中输入该指令并获得其可视化表示。记住这个网站;它对于巩固你对幕后发生的事情的理解非常有用。
因此,创建了一个对象。它得到了一个id
,type
设置为int
(整数),value
设置为42
。一个名为age
的名称被放置在全局命名空间中,指向该对象。因此,每当我们在全局命名空间中,在执行该行之后,我们可以通过简单地通过其名称访问它来检索该对象:age
。
如果你要搬家,你会把所有的刀、叉和勺子放在一个盒子里,贴上“餐具”的标签。你能看到这正是相同的概念吗?这是一个可能看起来像这样的屏幕截图(你可能需要调整设置以获得相同的视图):
因此,在本章的其余部分,每当你读到诸如name = some_value
这样的内容时,想象一下一个放置在命名空间中的名称,它与写入该指令的范围相关联,并且有一个漂亮的箭头指向具有id
、type
和value
的对象。关于这个机制还有一些要说的,但是通过一个例子来谈论它要容易得多,所以我们稍后再回到这个问题。
可变还是不可变?这是个问题
Python 对数据所做的第一个基本区分是关于对象的值是否会改变。如果值可以改变,对象称为可变,而如果值不能改变,对象称为不可变。
非常重要的是,你要理解可变和不可变之间的区别,因为它会影响你编写的代码,所以这里有一个问题:
>>> age = 42
>>> age
42
>>> age = 43 #A
>>> age
43
在前面的代码中,在#A
行,我改变了 age 的值吗?嗯,没有。但现在是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
在这种情况下,我设置了一个fab
对象,它的type
是Person
(一个自定义类)。在创建时,对象被赋予42
的age
。我打印它,以及对象的id
,以及age
的 ID。请注意,即使我将age
更改为25
,fab
的 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 中,True
和False
是两个关键字,用于表示真值。布尔值是整数的一个子类,分别像1
和0
一样行为。布尔值的等价于int
类的是bool
类,它返回True
或False
。每个内置的 Python 对象在布尔上下文中都有一个值,这意味着当它们被传递给bool
函数时,它们基本上会被评估为True
或False
。我们将在第三章中详细了解这一切,迭代和做决定。
布尔值可以使用逻辑运算符and
、or
和not
组合成布尔表达式。我们将在下一章中详细介绍它们,所以现在让我们看一个简单的例子:
>>> 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
你可以看到,当你尝试将True
和False
相加时,它们是整数的子类。Python 将它们提升为整数并执行加法:
>>> 1 + True
2
>>> False + 42
42
>>> 7 - True
6
向上转型是一种从子类到其父类的类型转换操作。在这里介绍的例子中,True
和False
属于从整数类派生的类,当需要时会转换回整数。这个主题涉及继承,将在第六章 OOP, Decorators, and Iterators中详细解释。
实数
实数,或浮点数,根据 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
个数字来表示这些位数。查看浮点数的max
和epsilon
值,你会意识到不可能表示它们所有。空间不够,因此它们被近似到最接近的可表示数字。你可能认为只有极大或极小的数字才会受到这个问题的影响。好吧,再想一想,尝试在你的控制台上输入以下内容:
>>> 0.3 - 0.1 * 3 # this should be 0!!!
-5.551115123125783e-17
这告诉你什么?它告诉你,双精度数甚至在处理简单的数字如0.1
或0.3
时也会受到近似问题的影响。为什么这很重要?如果你处理价格、金融计算或任何不需要近似的数据,这可能是一个大问题。不用担心,Python 给了你decimal类型,它不会受到这些问题的影响;我们马上就会看到它们。
复数
Python 为您提供了复数支持。如果你不知道什么是复数,它们是可以用a + ib的形式表示的数字,其中a和b是实数,i(或者如果你是工程师,是j)是虚数单位,即*-1的平方根。a和b分别被称为数字的实部和虚部*。
实际上,你可能不会经常使用它们,除非你在编写科学代码。让我们看一个小例子:
>>> 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
数字时,它会带有所有可能出现的近似问题。另一方面,当Decimal
没有近似问题时(例如,当我们将int
或string
表示传递给构造函数时),则计算没有古怪的行为。在涉及货币时,请使用小数。
这结束了我们对内置数字类型的介绍。现在让我们来看看序列。
不可变序列
让我们从不可变序列开始:字符串、元组和字节。
字符串和字节
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
,我们正在创建一个字节对象:
>>> 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'>
索引和切片字符串
在操作序列时,非常常见的是必须在一个精确的位置访问它们(索引),或者从它们中获取一个子序列(切片)。在处理不可变序列时,这两种操作都是只读的。
虽然索引以一种形式出现,即零为基础的访问序列中的任何位置,但切片以不同的形式出现。当您获取序列的一部分时,可以指定start
和stop
位置以及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
。
注意greet_positional_idx
通过向format
调用提供不同的数据而呈现出不同的方式。显然,我喜欢 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, 3
和my_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 程序通常只有等价的 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,它是merge和insertion 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 之间的所有整数不包括(因此 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
行,我创建了一个名为name
的bytearray
,从字节文字b'Lina'
中显示了bytearray
对象如何公开来自序列和字符串的方法,这非常方便。 如果您仔细考虑,它们可以被视为可变字符串。
集合类型
Python 还提供了两种集合类型,set
和frozenset
。 set
类型是可变的,而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)]
在前面的例子中,我以两种不同的方式创建了相同的列表,一种更加明确,另一种稍微更加 Pythonic。暂时忘记我不得不在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 一致性的又一个例子。
现在让我们来看看三个特殊的对象,称为字典视图:keys
、values
和items
。这些对象提供了字典条目的动态视图,并且随着字典的更改而更改。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]
的压缩版本来创建字典的。字符串'hello'
里有两个'l'
字符,它们分别与zip
函数的值2
和3
配对。请注意,在字典中,'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
经常用于表示值的缺失,并且在函数声明的参数中经常用作默认值。一些经验不足的编程人员有时会编写返回False
或None
的代码。False
和None
在布尔上下文中都会评估为False
,因此它们之间似乎没有太大的区别。但实际上,我认为它们之间有一个非常重要的区别:False
表示我们有信息,而我们拥有的信息是False
。None
表示没有信息。没有信息与信息为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 通用内置容器(tuple
,list
,set
和dict
)不够用时,我们可以在collections
模块中找到专门的容器数据类型。它们是:
数据类型 | 描述 |
---|---|
namedtuple() | 用于创建具有命名字段的元组子类的工厂函数 |
deque | 具有快速附加和弹出的类似列表的容器 |
ChainMap | 用于创建多个映射的单个视图的类似字典的类 |
Counter | 用于计算可散列对象的字典子类 |
OrderedDict | 记住条目添加顺序的字典子类 |
defaultdict | 调用工厂函数以提供缺失值的字典子类 |
UserDict | 用于更轻松地对字典子类进行封装的字典对象 |
UserList | 用于更轻松地对列表子类进行封装的列表对象 |
UserString | 用于更轻松地对字符串子类进行封装的字符串对象 |
我们没有空间来涵盖所有这些,但您可以在官方文档中找到大量的例子,所以在这里我只给出一个小例子,向您展示namedtuple
、defaultdict
和ChainMap
。
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.left
和vision.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
在前面的例子中,a
和b
被分配给了两个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
,这是预期的。
关于名称
您可能已经注意到,为了使示例尽可能简短,我使用了简单的字母来调用许多对象,如a
、b
、c
、d
等。当您在控制台上调试或显示a + b == 7
时,这是完全可以的,但是在专业编码(或任何类型的编码)中是不好的做法。如果我有时这样做,希望您能谅解;原因是为了以更紧凑的方式呈现代码。
在真实环境中,当您为数据选择名称时,您应该仔细选择名称,并且名称应该反映数据的内容。因此,如果您有一组Customer
对象,customers
是一个非常好的名称。customers_list
、customers_tuple
或customers_collection
也可以吗?想一想。将集合的名称与数据类型绑定在一起好吗?我认为大多数情况下不好。因此,我会说如果您有充分的理由这样做,请继续;否则,不要这样做。原因是,一旦customers_tuple
开始在代码的不同位置使用,并且您意识到实际上想要使用列表而不是元组,您将需要进行一些有趣的重构(也称为浪费时间)。数据的名称应该是名词,函数的名称应该是动词。名称应该尽可能具有表现力。实际上,Python 在命名方面是一个非常好的例子。大多数情况下,如果您知道函数的作用,您可以猜出函数的名称。疯狂,对吧?
*《代码整洁之道》的《有意义的命名》*第二章,Robert C. Martin,Prentice Hall完全致力于名称。这是一本了不起的书,它帮助我以许多不同的方式改进了我的编码风格,如果您想将编码提升到下一个水平,这是一本必读的书。
总结
在本章中,我们探讨了 Python 的内置数据类型。我们已经看到了有多少种类型,以及仅仅通过不同的组合就可以实现多少。
我们已经看到了数字类型、序列、集合、映射、集合(以及Enum
的特别客串),我们已经知道了一切都是对象,我们已经学会了可变和不可变的区别,我们还学会了切片和索引(以及自豪地学会了负索引)。
我们提供了简单的示例,但是您可以学到更多关于这个主题的知识,所以请深入研究官方文档并探索。
最重要的是,我鼓励你自己尝试所有的练习,让你的手指使用那些代码,建立一些肌肉记忆,并且不断尝试,实验,实验。学习当你除以零时会发生什么,当你将不同的数字类型组合成一个表达式时会发生什么,当你处理字符串时会发生什么。玩转所有的数据类型。锻炼它们,打破它们,发现它们所有的方法,享受它们,并且非常非常好地学习它们。
如果你的基础不是非常牢固的,你的代码能有多好呢?而数据是一切的基础。数据塑造了其周围的一切。
随着你在书中的进展,很可能会发现我的代码(或你的代码)中偶尔会出现一些不一致或小错误。你会收到错误消息,有些东西会出错。这很棒!编码时,事情经常会出错,你会一直进行调试和修复,所以把错误视为学习有关你正在使用的语言的新知识的有用练习,而不是失败或问题。错误会一直出现,直到你的最后一行代码,这是肯定的,所以最好现在就开始接受它们。
下一章是关于迭代和做决策的。我们将看到如何实际运用这些集合,并根据我们所提供的数据做出决策。现在你的知识正在积累,我们将开始加快速度,所以在你进入下一章之前,请确保你对本章的内容感到舒适。再次强调,玩得开心,探索,打破一切。这是学习的好方法。