Python编程入门与实战

978-7-115-54812-2
作者: 法布里奇奥·罗马诺(Fabrizio Romano)
译者: 徐波
编辑: 武晓燕
分类: Python

图书目录:

详情

这是一本Python入门书,本书的特色之一是在介绍Python编程的基础知识之后,通过具体编程范例,分不同的主题来阐释如何用Python语言高效编程,在帮助读者夯实基础的同时找到最佳解决方案,达到学以致用的目的。 本书内容由浅入深,从理论到实践,首先介绍Python编程的基本知识和编程范例,然后介绍如何进行性能优化、有效调试以及如何控制程序的流程。此外,本书还讲解了Python中的加密服务和安全令牌等知识。通过学习本书,读者将对Python语言有一定的了解。本书能够帮助读者掌握如何编写程序、构建网站以及利用Python著名的数据科学库来处理数据等内容。本书涵盖各种类型的应用程序,可帮助读者根据所学的知识解决真实世界中的问题。

图书摘要

版权信息

书名:Python编程入门与实战

ISBN:978-7-115-54812-2

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。


著    [意] 法布里奇奥•罗马诺(Fabrizio Romano)

译    徐 波

责任编辑 武晓燕

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


Copyright ©2018 Packt Publishing. First published in the English language under the title Learn Python Programming, 2nd edition.

All rights reserved.

本书由英国Packt Publishing公司授权人民邮电出版社有限公司出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。

版权所有,侵权必究。


这是一本Python入门书,本书的特色之一是在介绍Python编程的基础知识之后,通过具体编程范例,分不同的主题来阐释如何用Python语言高效编程,在帮助读者夯实基础的同时找到最佳解决方案,达到学以致用的目的。

本书内容由浅入深,从理论到实践,首先介绍Python编程的基本知识和编程范例,然后介绍如何进行性能优化、有效调试以及如何控制程序的流程。此外,本书还讲解了Python中的加密服务和安全令牌等知识。通过学习本书,读者将对Python语言有一定的了解。本书能够帮助读者掌握如何编写程序、构建网站以及利用Python著名的数据科学库来处理数据等内容。本书涵盖各种类型的应用程序,可帮助读者根据所学的知识解决真实世界中的问题。

本书适合对Python编程技能感兴趣的初学者及IT从业者阅读。


献给我最亲爱的朋友和导师托尔斯滕•亚历山大•兰格(Torsten Alexander Lange),感谢你们所有的支持和爱。


法布里奇奥•罗马诺(Fabrizio Romano)于1975年出生于意大利。他获得了帕多瓦大学计算机科学系的硕士学位。他是一位项目经理和教师,并且是 CNHC的成员。

他于2011年移居伦敦,曾在Glasses Direct、TBG/Sprinklr和学旅家等公司就职。他目前就职于Sohonet,担任首席工程师和项目领导人。

他曾经在EuroPython的Teaching Python和TDD上发言,也曾在伦敦的Skillsmatter和ProgSCon上发言。


衷心感谢所有帮助我编写本书的人们。特别感谢Naomi Ceder博士为本书作序。感谢Heinrich Kruger和Julio Trigo对本书的审核。感谢我的朋友和家庭,他们让我随时感受到温暖和支持。感谢Petra Lange,感谢他一直所表现的友善,谢谢!


我第一次知道法布里奇奥是在几年前他成为我公司的首席开发人员的时候。不管是在设计系统、结对编写代码、执行代码审查时,还是在午餐间隙组织扑克游戏时,法布里奇奥所思考的都不仅仅是完成任务的最佳方法,还包括如何提高整支队伍的技能、如何激励他人最大限度地发挥他们的能力。

读者会在书中感受到作者的睿智和细心。每个章节、每个例子、每段讲解都经过了精心的构思,目的是把他对这项技术出色、准确的理解以非常恰当的方式呈现给读者。法布里奇奥将带领读者在他的帮助下学习Python的语法和最佳实践方法。

我对本书所涵盖的内容之广印象深刻。Python在这些年得到了长足的成长和发展,现在它已经成长为一个庞大的生态系统,不仅适用于Web开发、日常数据处理和ETL,而且在数据科学方面的应用也越来越广。如果读者不熟悉Python的生态系统,将很难知道要学习什么才能实现自己的目标。在本书中,读者会发现很多实用的例子,它们展示了Python的许多不同用法,可以帮助读者更好地领悟Python。

我希望读者能够享受本书的学习之旅,并成为我们的全球社区的一员。我非常自豪受邀为本书作序,更重要的是,我非常高兴法布里奇奥能够为读者提供帮助。

Python软件基金会成员

娜奥米•塞德(Naomi Ceder)


当我刚开始编写这本书时,我对读者的期望知之甚少。后来,我逐渐学会了如何把每个话题转换为一个故事。我希望通过一些简单、实用、容易理解的例子来讨论Python,同时又把自己的经验倾注到文字当中去,把我这些年对Python的感悟传递给读者。这些有价值的感悟值得读者思考、回顾和消化。读者可能会不同意我的某些方法,而是采用了其他方法,我衷心希望他们所找到的是一种更好的方法。

本书并不仅讨论语言本身,还涵盖了编程的技能。事实上,编程的艺术是由许多方面所组成的,语言仅仅是其中的一个方面。

编程的另一个关键是要具有独立性,也就是当我们遇到阻碍不知道如何解决时能够放飞自己的能力。没有任何书籍可以传授这个能力,因此我认为在这个方面,不应该采用“教”的方式,而是应该让读者自己去领悟。我在全书中设置了一些说明、问题和评论,希望能够启发读者。同时,我还希望读者能够花点时间浏览网站或其他官方文档,更深入地挖掘和学习,感受自己解决问题所带来的乐趣。

最后,我想编写一本甚至在形式上也与众不同的书。因此我和编辑商量后决定以理论的方式编写本书的第一部分,介绍一些描述Python各方面特性的话题。本书的第二部分则由各种现实生活中的项目所组成,向读者展示这种语言能够实现多么出色的成就。

有了这些目标之后,接着我迎接了最困难的挑战:对我想要编写的所有内容进行规划,把它们压缩到本书允许的篇幅中。这个挑战很困难,我不得不做出一些取舍。

我的努力得到了读者的良好反馈。时至今日,距离这本书第一版问世差不多已有3年,我仍然会不断地收到来自读者的愉快信息。他们向我表示感谢,告诉我这本书为他们提供了巨大的帮助。对我来说,这是最好的赞美。我知道Python语言可能会发生变化甚至会过时,但我仍然设法把我的知识与读者共享,希望这些知识可以让读者长久受益。

现在,我已经完成了本书的写作,这次我有了更大的空间。因此,我决定增加一章关于IO的内容,这部分内容是读者极其需要的。我甚至有机会又增加了两章,一章关于加密,另一章关于并发执行。后者无疑是全书难度最大的一章,它的目标是激励读者达到能够轻松消化Python中的代码并理解其概念的层次。

除了有些冗长的最后一章外,我保留了第一版中其余的所有章节,并根据最新版本的Python进行了更新。在本书写作时,最新的Python版本是3.7。

当我看到本书时,我便看到了一个更加成熟的产品。本书章节的数量更多,内容也进行了重组并更偏向描述,但本书的灵魂仍然不变。本书最主要和最重要的出发点,始终是提高读者的能力,这一点在新版本中仍然非常显著。

我希望本书能够帮助读者开发关键的思维、获得强大的技能,并培养随着时间变化不断适应新技术的能力。相信学习本书所获得的稳固基础可以帮助读者更好地实现这个目标。

Python是美国顶级大学的计算机科学系所使用的非常流行的入门级教学语言,因此如果读者是软件开发的新手,那么这种语言和本书正是这些读者所需要的。不管读者所选择的工作环境是什么,Python的精彩设计和优秀的可移植性都将会帮助读者提高工作效率。

如果读者已经使用过Python或其他任何语言,本书仍然是非常实用的,它不仅可以作为Python基础知识的参考书,而且还提供了作者20年的编程经验及大量观点和建议。

第1章“Python概述”,介绍了基本的编程概念。它能够指导读者获取并在自己的计算机上运行Python,同时还介绍了Python的一些结构。

第2章“内置的数据类型”,介绍了Python的内置数据类型。Python具有丰富的本地数据类型,本章对每种内置类型进行了描述并各提供了一个简单的例子。

第3章“迭代和决策”,讲述了如何通过检查条件、应用逻辑和执行循环来控制代码的执行流。

第4章“函数,代码的基本构件”,讲述了如何编写函数。函数是代码复用的关键,可以减少调试时间。按照更广泛的说法,它能够帮助我们编写更好的代码。

第5章“节省时间和内存”,介绍了Python编程的一些功能特性。本章讲述了如何编写解析和生成器,它们是功能强大的工具,可以帮助我们加快代码的执行速度并节省内存。

第6章“面向对象编程、装饰器和迭代器”,讲述了使用Python进行面向对象编程的基础知识。本章描述了这种编程模式的一些关键概念和所有潜在优点;介绍了Python最受人喜爱的特性之一,即装饰器;还介绍了迭代器的概念。

第7章“文件和数据持久化”,讲述了如何处理文件、流、数据交换格式和数据库等内容。

第8章“测试、性能分析和异常处理”,讲述了如何使用诸如测试和性能分析这样的技巧使我们的代码更健壮、快速和稳定。本章还正式定义了异常的概念。

第9章“加密与标记”,讲述了安全、散列、加密和标记的概念,它们是当今日常编程的重要组成部分。

第10章“并发执行”,这是难度颇大的一章,描述了如何同时完成多件事情。本章提供了这个主题的一些理论概念,然后展示了分别用不同的技巧实现这个功能的3个优秀例子,帮助读者理解本章所介绍的编程模式之间的区别。

第11章“调试和故障排除”,讲述了对代码进行调试的主要方法,并展示了使用这些方法的一些例子。

第12章“GUI和脚本”,通过两个不同的角度指导读者完成一个例子。这两个角度就好像光谱的对立双方:一种实现是个脚本,另一种实现是个图形用户接口应用程序。

第 13 章“数据科学”,介绍了一些关键的概念和一种非常特殊的工具,即Jupyter Notebook。

第14章“Web开发”,介绍了Web开发的基础知识,并使用Django Web框架开发了一个项目,这个例子将建立在正则表达式的基础之上。

我鼓励读者自己实现本书中的例子。为此,读者需要一台计算机、Internet连接和一个浏览器。本书中的例子是用Python 3.7编写的,但对于任何最近的Python 3.*版本都应该适用。我在书中讲述了如何在各个操作系统中安装Python,这个任务的具体过程会不断发生变化,因此读者需要从网络上寻找最新的精确安装指南。我还解释了如何安装本书的各个例子所需要的所有额外的程序库,并对读者在安装过程中可能会遇到的问题提供了相关建议。在输入代码时并不要求读者使用某种特定的编辑器,但是,我建议对本书中的例子感兴趣的读者采用一种合适的编程环境,对此我在第1章中提供了一些建议。

本书使用了一些体例约定。

代码体例:表示文本中的一些代码词汇,包括数据库名称、文件夹名称、文件名、文件扩展名、路径名、URL地址、用户的输入以及Twitter用户名。例如:“在learn.pp文件夹中,我们将创建一个称为learnpp的虚拟环境。”

代码块是按下面的方式设置的:

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

所有命令行的输入或输出都是采用下面的书写形式:

>>> import sys
>>> print(sys.version)

粗体:表示一个新项、一个重要词汇或者在屏幕上所看到的词。例如,菜单或对话框中的单词在文本中就会用粗体显示。例如:“为了在Windows中打开控制台,选择开始菜单,选择运行,并输入cmd。”

 

警告或重要说明以这种形式出现。

 

提示和技巧以这种形式出现。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书提供如下资源:

要获得以上配套资源,请在异步社区本书页面中点击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线投稿(直接访问www.epubit.com/ selfpublish/submission即可)。

如果您所在的是学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


“授人以鱼不如授之以渔。”

——中国古话

根据维基百科的定义,计算机编程的含义是:

“计算机编程是指从计算问题的原始形式产生可执行计算机程序的过程。编程所涉及的活动包括分析问题、深入理解问题、生成算法、对算法需求进行验证,包括它们的正确性以及所消耗的资源,另外还包括怎样用一种目标编程语言实现该算法(此过程一般称为编程)。”

概括地说,编程就是用一种计算机所能理解的语言来告诉它完成某个任务。

计算机是一种功能非常强大的工具,但遗憾的是,它本身并不具备思考能力。我们需要告诉它所有的任务细节,如怎样执行一个任务、怎样评估一个条件以决定采取哪条路径、怎样处理来自某个设备(如网络或磁盘)的数据以及当某件不可预见的事情(如什么东西坏了或者不见了)发生时应该采取什么操作。

我们在编写代码时可以选择许多不同的风格和不同的语言。编程是不是很难?可以说是,也可以说不是。它有点像写作,每个人都知道怎样写作。但是,如果我们想成为一名诗人呢?要想成为诗人,光知道怎样写作是远远不够的。我们还需要掌握一整套的其他技巧,而这需要大量的时间和精力。

最后,一切都取决于我们想要在编程的道路上走得多远。编程绝不仅仅是把一些指令组合在一起使之能够运行,它所意味的东西要多得多。

优秀的代码短小、快速、优雅、易于阅读和理解、简单、易于修改和扩展、易于缩放和重构,并且容易进行测试。想要编写同时具备上述特点的代码需要时间的积累,不过有个好消息是当读者阅读本书的时候,就已经朝着这个目标迈出了可喜的一步。我毫不怀疑读者能够做到这一点。事实上,每个人随时都在进行编程,只不过我们并没有意识到这一点。

想不想要看个例子?

假设我们想泡一杯速溶咖啡。我们必须要有咖啡杯、速溶咖啡罐、茶匙、水和水壶。即使我们并没有意识到,实际上我们已经对大量的数据进行了评估。我们需要确保水壶中有水并且水壶已经通上了电、咖啡杯必须已经洗干净了并且咖啡罐里有足够的咖啡。然后,我们烧好开水,同时在咖啡杯里加入一些咖啡。水烧开之后,就可以把开水倒入咖啡杯中并进行搅拌。

那么,这个过程中的编程体现在什么地方呢?

没错,我们收集资源(水壶、咖啡、水、茶匙和咖啡杯)并验证一些与它们有关的条件(水壶已经通上电、咖啡杯已经洗干净、咖啡的数量足够)。然后我们开始进行两项活动(烧开水以及把咖啡倒入咖啡杯中),当这两个活动都完成之后,我们把开水倒入咖啡杯中并进行搅拌,从而完成了整个过程。

能不能理解?我只是描述了泡咖啡程序的高层功能。这并不是很难,因为这正是我们的大脑每天所做的事情:对条件进行评估以决定采取什么活动、执行活动、重复其中一些活动并在某个时刻终止、清理物品、把它们放回原处等。

现在,我们所需要的就是学习怎样把自己在现实生活中自动完成的那些活动进行结构分解,使计算机能够理解它们。另外,我们还需要学习一种能够指导计算机执行任务的语言。

因此,这就是本书的目的所在。我将告诉读者怎样做到这一点,并将通过许多简单但目标明确的例子(我所喜欢的类型)来帮助读者实现这个目标。

在本章中,我们将讨论下面这些内容。

我在讲授编程的时候喜欢引用现实世界的例子。我相信它们可以帮助读者更好地理解相关的概念。但是,现在我们需要采取更严格的方式,更多地从技术的角度观察什么是编程。

当我们编写代码时,我们指示计算机完成一些必须完成的事情。这些活动是在哪里发生的?在计算机的许多地方都有可能,如计算机内存、硬盘、网线、CPU等。这是一个完整的世界,在大多数情况下可以看成是现实世界的一个子集。

如果我们编写一个软件,允许人们在线购买服装,那么就必须在程序的边界之内表示现实的人们、现实的衣服、现实的品牌、现实的尺寸等概念。

为此,我们需要在自己所编写的程序中创建和处理对象。一个人是一个对象,一辆汽车也是一个对象,一双袜子也是一个对象。幸运的是,Python能够很好地理解对象这个概念。

任何对象都具有的两个主要特性是属性和方法。我们以人这个对象为例。一般情况下,在计算机程序中,人这个对象是以顾客或员工的形式出现的。我们在这种对象中所存储的属性包括姓名、社会保障号码、年龄、是否拥有驾照、电子邮件、性别等。在计算机程序中,我们需要存储所有必要的数据,以便计算机能够按照预期的方式使用这种对象。如果我们为一个销售服装的网站编写代码,除了顾客的其他信息之外,很可能还需要存储身高和体重数据,以便向他们提供适当的服装尺码建议。因此,属性就是对象的特征。实际上我们一直在使用对象的属性,例如,“可以把那支笔递给我吗?”“哪一支?”“黑色的那支”。在这里,我们使用了笔的颜色属性(黑色)来标识这个对象(和其他红色或蓝色的笔进行区别)。

方法就是对象可以做的事情。作为一个人,我可以说话、走路、睡觉、起床、吃东西、做梦、写字、阅读等。我可以做的任何事情都可以看成是表示我的那个对象的方法。

现在,我们知道了什么是对象,并且知道了它提供了一些可以运行的方法和一些可以检查的属性,这样我们就可以开始编写代码了。编写代码实际上就是简单地对我们的软件所复刻的世界子集中所存在的对象进行管理。我们可以按照自己的意愿创建、使用、复用和删除对象。

根据Python官方文档的“数据模型”这一章的说法:

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

我们将在第6章中更深入地讨论Python对象。现在,我们只需要知道Python中的每个对象都有一个ID(或称为标识)、一种类型和一个值。

一旦创建了一个对象之后,它的ID就不会改变。每个ID都是一个独一无二的标识符,当我们需要使用这个对象时,Python就会在后台用ID来提取这个对象。

同样,对象的类型也不会改变。类型决定了对象所支持的操作以及我们可以给对象赋什么样的值。

我们将在第2章中讨论Python中大多数的重要数据类型。

对象的值有些能够改变,也有些不能改变。如果可以改变,这种对象就称为 mutable(可变)对象。如果不能改变,这种对象就称为immutable(不可变)对象。

我们该怎样使用对象呢?当然,我们需要为它提供一个名称。当我们为一个对象提供一个名称后,就可以用这个名称来提取这个对象并使用它。

从更一般的意义上说,诸如数值、字符串(文本)、集合这样的对象都是与一个名称相关联的。我们通常把这种名称称为变量名。我们可以把变量看成是可以装纳数据的盒子。

现在,有了我们所需要的所有对象之后,接下来应该怎么做呢?不错,我们需要使用它们。我们可能需要通过网络连接发送它们或者把它们存储在数据库中。也许我们想把它们显示在一个网页上或者把它们写入一个文件中。为此,我们需要对用户填写一个表单、点击一个按钮或者打开一个网页并进行搜索的行为做出响应。我们通过运行自己的代码来对这些行为做出响应,对条件进行评估以选择需要执行的路径、确定需要执行多少次以及在什么样的情况下执行。

为了实现这个目的,我们一般需要一种语言,Python就适合这种用途。Python是我们在本书中指示计算机为我们执行任务时所使用的语言。

现在,我们已经了解了足够的理论背景,可以进入正式的学习之旅了!

Python是荷兰计算机科学家、数学家Guido Van Rossum的杰出作品,这是他在1989年圣诞节期间参与一个项目时为全世界所送上的一份礼物。Python在1991年前后出现在公众视野中,在此之后不断发展,逐渐成为当今世界广泛使用的主流编程语言之一。

我从7岁开始在一台Commodore VIC-20计算机上学习编程,这台机器后来被它的进化版本Commodore 64所取代。它所使用的语言是BASIC。后来,我学习过Pascal、汇编语言、C、C++、Java、JavaScript、Visual Basic、PHP、ASP、ASP.NET、C#以及其他一些我甚至已经想不起名字的非主流语言。但是,直到我接触Python后,我才意识到它才是最适合我的语言。

我由衷地发出这样的呐喊:“就是它了!它对我来说是最完美的!”我花了一天的时间就适应了它。它的语法与我所习惯的语法有些差异,但在克服了最初的不适应(就像刚刚穿上新鞋时)之后,我发现自己深深地喜欢上了它。下面我会详细说明为什么Python是一种完美的语言。

在讨论冷冰冰的细节之前,我们首先要体会为什么需要使用Python(我推荐读者阅读维基百科的Python页面,了解更详细的介绍)。对我而言,Python具有下面这些优点。

Python的运行范围很广,把一个程序从Linux移植到Windows或Mac通常只需要修改路径和设置就可以了。Python在设计时充分考虑了可移植性,能够处理特定操作系统(Operating System,OS)接口背后的特定特性,从而避免了在编写代码时不得不进行剪裁以适应某个特定平台的麻烦。

Python具有极强的逻辑性和一致性。我们可以看到它是由一位卓越的计算机科学家所设计的。大多数情况下,即使我们并不熟悉某个方法,也可以猜出它是怎么被调用的。

现在,我们可能还无法意识到这个特点的重要性,尤其在我们初学编程的时候。但是,这是Python的一个主要特性。它意味着我们大脑中不会有太多混乱的东西,并且需要阅读的文档也很少,所以当我们编写代码时大脑里所需要的映射也就很少。

根据马克·卢茨(Learning Python第5版,O'Reilly)的说法,Python程序的长度一般只有对应的Java或C++程序的五分之一到三分之一。这意味着使用Python可以更快速地完成工作。快速显然是个很好的优点,意味着在市场上能够得到更快的响应。更少的代码不仅意味着需要编写的代码更少,同时也意味着需要阅读的代码更少(专业程序员所阅读的代码数量要远远多于他们所编写的代码),并且还意味着需要维护、调试和重构的代码也更少。

Python的另一个重要优点是它在运行时不需要冗长耗时的编译和链接步骤,因此我们不需要等待就可以看到自己的工作成果。

Python提供了一个标准库(就像手机的随机电池),其有着令人难以置信的、广阔的涵盖范围。如果觉得这还不够,遍布全球的Python社区还维护了一个第三方的程序库主体,设计人员可以通过裁剪以适应具体的需要。我们可以很方便地通过Python程序包索引PyPI)获取它们。在大多数情况下,当我们编写Python代码并需要某个特性时,至少会有一个程序库已经为我们实现了这个特性。

Python专注于可读性、一致性和质量。语言的一致性提供了极高的可读性,这在如今是一个至关重要的优点,因为现在的代码往往是多人合作的成果,而不是一个人的单独工作。Python的另一个重要特点是它在本质上的多范式性质。我们可以把它当作一种脚本语言,也可以采用面向对象式的、命令式的和函数式的编程风格。它是一种极为全能的语言。

Python的另一个重要特点是它可以扩展并可以与其他许多语言进行集成,这意味着即使一家公司使用另一种不同的语言作为它的主流语言,仍然可以使用Python作为复杂应用程序之间的黏合剂,使它们可以按照某种方式彼此通信。这个话题比较高级,但是在现实世界中,这个特性是非常重要的。

最后一个但绝非不重要的特点是它的乐趣。用Python编写代码是一件快乐的事情。我可以编写8个小时的代码,然后兴高采烈、心满意足地离开办公室。其他程序员可能就没有这么惬意,因为他们所使用的语言并没有提供同等数量的设计良好的数据结构和代码结构。Python能够使编程充满乐趣,这点是毫无疑问的。编程的乐趣能够提升工作动力和工作效率。

上面这些优点是我向每个人推荐Python的主要原因。当然,我还可以举出其他许多技术特点和高级特性,但在入门章节中并不适合讨论这些话题。它们将会在本书的后面自然而然地呈现在读者面前。

我们在Python中唯一可能找到的与个人偏好无关的缺点就是它的执行速度。一般而言,Python代码的执行速度要慢于经过编译的代码。Python的标准实现在我们运行程序时会生成源代码的编译版本,称为字节码(扩展名为.pyc),然后由Python解释器运行。这种方法的优点是具有可移植性,其代价是速度较慢,因为Python不像其他语言一样编译到机器层次。

但是,Python的运行速度在当今这个时代并不是什么问题,因此这个并不重要的缺点并不会影响它的广泛应用。在现实生活中,硬件成本不再是什么问题,并且并行化能够很容易实现速度的提升。而且,许多程序的大部分运行时间花在等待IO操作的完成上。因此,原始运行速度只是总体性能的一个次要因素。而且,在遇到数据处理密集的情况时,我们可以切换为更快速的Python实现,例如PyPy,它能够通过实现一些高级编程技巧提升5倍的速度(详细信息可参考PyPy官网)。

涉及数据科学时,我们很可能会发现Python所使用的程序库(如PandaNumPy)由于实现方式的缘故已经实现了本地速度。如果觉得说服力还不够,可以再看一下Python已经用于驱动像Spotify和Instagram这样非常重视性能的后端服务了。无论如何,Python已经能够足够完美地完成各种任务。

觉得说服力还不够吗?我们简单地观察一下,可以发现当前使用Python的公司和机构有Google、YouTube、Dropbox、Yahoo!、Zope Corporation、Industrial Light & Magic、Walt Disney Feature Animation、Blender 3D、Pixar、NASA、the NSA、Red Hat、Nokia、IBM、Netflix、Yelp、Intel、Cisco、HP、Qualcomm和JPMorgan Chase,这些仅仅是其中的一部分。甚至像《Battlefield 2》《Civilization IV》和《QuArK》这样的游戏也是用Python实现的。

Python可用于许多不同的环境,如系统编程、网页编程、图形用户界面(Graphical User Interface,GUI)应用程序、游戏和机器人、快速原型、系统集成、数据科学、数据库应用程序等。一些声名卓著的大学已经采用Python作为计算机科学课程的主要语言。

在讨论如何在系统中安装Python之前,我们先说明一下本书所使用的Python版本。

Python有两个主要的版本:以前的Python 2和现在的Python 3。这两个版本尽管非常相似,但是有几个方面是不兼容的。

在现实世界中,Python 2还远远谈不上过时。简而言之,尽管Python 3早在2008年就出现了,但是Python 2的过渡阶段还远远没有结束。这很大程度上是由于Python 2在行业中被广泛使用,公司的系统一般不会仅仅为了更新而更新,而是恪守“如果还没坏,就不用修”的原则。我们可以在网络上了解这两个版本之间的过渡。

妨碍Python版本过渡的另一个因素是第三方程序库的可用性。一个Python项目通常要依赖数十个外部程序库,因此当我们启动一个新项目时,需要确保已经存在一个兼容版本3的程序库,以满足这个项目所需要的所有业务需求。如果不满足这种情况,用Python 3开发一个全新的项目就存在潜在的风险,而这种风险是许多公司不乐意承担的。

不过,在写作本书时,大多数广泛使用的程序库已经移植到了Python 3,因此在大多数情况下用Python 3开发一个新项目是相当安全的。许多程序库进行了重写以同时兼容这两个版本,这里主要是利用了six程序库的功能(这个名称来自2×3,表示从版本2到版本3的移植),它可以帮助我们根据所使用的版本对程序库的行为进行自查和变更。根据PEP 373,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,这个版本是在2018年6月发布的。上面的文本与我们在控制台所输入的Python代码有点相似。稍后我们就将对此进行讨论。

本书的所有例子将使用Python 3.7运行。尽管目前的最终版本与我所使用的版本存在细微的差别,但我将保证所有的代码和例子在本书出版时都已经更新到了3.7。

有些代码仍可以在Python 2.7下运行,有些会完全按原样运行,有些则存在细微的差别。但在现在这个时刻,我觉得最好还是直接学习Python 3。如果读者需要,也可以了解一下它与Python 2的区别,而不是先学习Python 2。

不必过于担心版本的问题,因为在实际使用中这并不是什么大的问题。

我并没有想到要在本书中专门安排一个介绍安装的章节,尽管读者确实需要安装一些东西。大多数情况下,作者编写书中的代码与读者实际试验这些代码之间存在几个月的时间差。过了这么长时间,很可能已经发生了版本的变化,本书所描述的方法也很可能已经不再适用。幸运的是,我们现在有了网络。因此,为了帮助读者完成安装和运行,我将提供一些指导和目标。

我注意到本书的大多数读者希望书中能够提供一些关于安装Python的指导,我并不认为这能够为读者提供真正的帮助。我非常坚定地认为,如果读者想要学习使用Python编程,一开始花点时间熟悉它的生态系统是极有帮助的,而且是非常重要的。当读者阅读以后的章节时,会极大地提升信心。如果在这个过程中遇到困难,我们可以借助搜索引擎。

我们先来讨论一下操作系统。Python已经完全集成并很可能已经自动安装于几乎每个Linux系统中。对于macOS,很可能也已经安装了Python(但是,很可能只支持Python 2.7)。如果我们所使用的是Windows系统,则很可能需要自行安装。

获取Python以及所需的程序库并使之能够运行,需要进行一些手动操作。对于Python程序员来说,Linux和macOS是相当友好的操作系统。反之,使用Windows系统的程序员就需要花费一些精力。

我当前的系统是 macOS,这也是我在本书中一直所使用的系统,它所使用的版本是Python 3.7。

我们需要关注的是Python的官方网站。这个网站提供了Python的官方文档以及其他很多非常实用的资源。我们应该花点时间探索这个网站。

另一个提供了Python及其生态系统方面的丰富资源的优秀网站是The Hitchhiker’s Guide to Python。我们可以在这个网站中找到在不同的操作系统中安装Python的不同方法。

在这个网站中找到安装部分,并选择适合自己的操作系统的安装程序。如果操作系统是Windows,要确保在运行安装程序时安装了pip工具(实际上,我建议完整安装Python,为了安全起见,最好安装所有的组件)。我们将在后面讨论pip工具。

在操作系统中安装了Python之后,接下来的目标就是打开一个控制台并输入python,运行Python交互性shell。

注意,我通常简单地用Python控制台表示Python交互性shell

为了在Windows中打开控制台,进入“开始”菜单,选择“运行”并输入cmd。如果在运行本书的例子时遇到类似权限这样的问题,请确保以管理员身份运行控制台。

在macOS X系统中,我们可以进入“应用程序”>“工具”>“终端”启动一个终端窗口。如果是在Linux操作系统中,就不需要知道什么是控制台。

我将使用控制台这个术语表示Linux的控制台、Windows的命令行窗口以及Macintosh的终端。我还将用Linux默认格式表示命令行的输入提示,就像下面这样:

$ sudo apt-get update

如果读者对此并不熟悉,请花一点时间学习控制台的工作方式的基础知识。概括地说,在$符号之后,我们一般会发现一条必须输入的指令。注意大小写和空格,它们是非常重要的。

不管打开的是哪个控制台,都需要在提示符后面输入python,以确保显示Python交互性shell。输入exit()退出这个窗口。记住,如果操作系统预安装了Python 2.*,我们可能需要指定Python 3。

下面大概就是我们在运行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是关于虚拟环境的。我将解释它是什么,并将通过一个简单的例子来说明为什么需要它。

我们在系统中安装Python,并在一个网站上为顾客X开始工作。我们创建一个项目文件夹,并开始编写代码。在这个过程中,我们还安装了一些程序库,如Django框架。我们将在第14章Web开发中深入讨论这个程序库。假设我们为项目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版本中,可以用下面的命令安装virtualenv:

$ sudo apt-get install python-virtualenv

最简单的方法很可能是遵循virtualenv的官方网站上的指令。

我们将会发现安装virtualenv最常见的方法之一是使用pip工具。这是一个程序包管理工具,用于安装和管理用Python所编写的软件包。

在Python 3.5中,创建虚拟环境的推荐方法是使用venv模块。关于这方面的详细信息,可以查阅官方文档。但是,在写作本书之时,virtualenv仍然是创建虚拟环境最常用的工具。

创建虚拟环境是非常简单的。但是,根据系统的配置以及需要在虚拟环境中所运行的Python版本,我们需要正确地运行命令。我们使用virtualenv时还需要做的另一件事情就是将其激活。激活virtualenv基本上就是在后台生成一些路径,这样当我们调用Python解释器时,实际上所调用的是那个活动的虚拟环境,而不是单纯的系统环境。

我将展示在我的Macintosh控制台上完成的一个完整例子。我们将进行以下操作。

1.在项目的根目录(对我而言,是home文件夹中的一个称为srv的文件夹)中创建一个名为learn.pp的文件夹。我们可以根据自己的喜好设置路径的名称。

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

有些开发人员喜欢用相同的名称表示所有的虚拟环境(如.venv)。采用这种方法时,他们只需要知道他们所关注的项目名称就可以针对任何虚拟环境运行脚本。.venv中的点号是存在的,因为在Linux操作系统macOS中,在名称前加上一个点号可以使该文件或文件夹不可见。

3.在创建了虚拟环境之后,我们需要将其激活。Linux、macOS和Windows操作系统所采用的方法稍有不同。

4.然后,我们运行Python交互性shell以确保所运行的是我们所需要的Python版本(3.7.*)。

5.最后,我们将使用deactivate命令取消虚拟环境的激活。

这5个简单的步骤包括了启动和使用一个项目时所需要的所有操作。下面是这些步骤在macOS(以#开始的命令是注释,为了便于阅读加上了空格,→表示上一行的空间不足导致的换行)上的大致样子(操作系统、Python版本以及其他因素不同,结果可能会有细微的差别):

fabmp:srv fab$         # 步骤1——创建文件夹
fabmp:srv fab$ mkdir learn.pp
fabmp:srv fab$ cd learn.pp

fabmp:learn.pp fab$    # 步骤2——创建虚拟环境
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$            # 步骤3——激活虚拟环境
fabmp:learn.pp fab$ source learnpp/bin/activate

(learnpp) fabmp:learn.pp fab$  # 步骤4——验证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$  # 步骤5——取消激活
(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脚本,而该脚本又需要通过source命令才能导入当前环境中。当一个脚本运行了source命令之后,意味着它可以在当前的命令窗口中执行,并且在执行之后它的效果仍然会持续,这是非常重要的。另外,注意在激活虚拟窗口之后命令提示符所发生的变化,它在左边显示了虚拟环境的名称(当我们取消虚拟环境的激活之后,这个名称就会消失)。

在Linux操作系统中,这些步骤是相同的,因此不再赘述。在Windows操作系统中,步骤略有变化,但概念是一致的。我们可以在virtualenv的官方网站阅读相关的指南。

现在,我们应该能够创建并激活一个虚拟环境了。读者可以尝试在没有指导的情况下自己创建另一个虚拟环境。我们需要熟悉这个过程,因为这是我们一直都需要做的事情,而且我们绝不会用Python进行整个系统的操作,这是极为重要的。

做好了相关的准备工作之后,我们便可以更多地讨论Python以及它的用法了。不过在此之前,我们先简单地讨论一下控制台。

在GUI和触摸屏的时代,所有的操作都可以通过点击或触碰来完成,而使用一个像控制台这样的工具听上去可能有些荒谬。

但是,事实上每次当我们把自己的右手从键盘上移开(如果是左撇子,则是左手)并抓住鼠标,把鼠标指针移动到自己想要点击的位置,我们都会浪费一些时间。而如果用控制台完成相同的操作,虽然看上去有点不太直观,但是它的效率更高、速度更快。作为程序员,我们必须要坚信这一点。

就我个人而言,速度和效率是非常重要的。我并不反对使用鼠标,但另外还有一个非常重要的原因需要我们熟悉控制台的操作:当我们开发在服务器上运行的代码时,控制台可能是唯一可用的工具。如果我们熟练掌握了控制台的操作,在紧急状况下我们就不会陷入手足无措的困境(例如,当网站崩溃,我们必须快速找出原因时)。

因此,我们应该努力熟悉用控制台进行操作。如果读者还没有决定,请相信我的建议并进行尝试。它比我们想象的要容易得多,我们绝不会对此感到后悔。对于优秀的开发人员而言,没有什么事情比迷失于一个与某台服务器的SSH连接更为痛心了,因为他们已经习惯了自己的工具集,而且只熟悉这些工具。

现在,让我们回到Python本身。

我们可以用一些不同的方法运行Python程序。

Python可以作为脚本语言使用。事实上,它一直在证明自己是一种非常实用的脚本语言。脚本一般是在完成某个任务时所执行的文件(通常较小)。许多开发人员随着时间的积累会创建他们自己的工具集,并在需要执行一个任务时进行使用。例如,我们可以使用脚本解析某种格式的数据并把它保存为另一种不同的格式,或者我们可以使用脚本对文件和文件夹进行操作,我们还可以创建或修改配置文件。脚本可以完成的任务还有很多。从技术上说,一个脚本所做的事情并不会太多。

让脚本在一台服务器上在某个精确的时间运行是相当常见的做法。例如,如果我们的网站数据库需要每隔24小时清理一次(例如,清理存储了用户会话的表,它们很快就会过期,但并不会被自动清理),我们可以设置一个Cron任务在每天的凌晨3点触发脚本的运行。

根据维基百科的说法,软件工具Cron是一个在类似UNIX的操作系统中运行的基于时间的任务调度工具。人们在设置和维护软件环境时可以使用Cron对任务(命令或shell脚本)进行调度,使其在某个固定时间、日期或间隔定期运行。

Python脚本可以完成所有需要手动操作几分钟甚至更多的时间才能完成的杂务。从某一时刻起,我决定采用自动化。第12章GUI和脚本有一半的篇幅描述Python脚本。

运行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
179769313486231590772930519078902473361797697894230657273430081157732675805
500963132708477322407536021120113879871393357658789768814416622492847430639
474124377767893424865485276302219601246094119453082952085005768838150682342
462881473913110540827237163350510684586298239947245938479716304835356329624
224137216

最后一个操作显示了令人难以置信的结果。我们要求Python计算2的1024次方的结果,而Python非常轻松地完成了这个任务。如果在Java、C++或C#中尝试这样的做法,肯定会失败,除非使用能够处理这类巨大数值的特殊程序库。

我每天都在使用交互性shell。它在快速调试方面极为实用,如检查一种数据结构是否支持某种操作,或者我们可以用它来检查或运行一段代码。

当我们使用Django(一种Web框架)时,它附带了交互性shell,允许我们按照自己的方式使用这个框架的工具,对数据库中的数据进行检查或者进行其他许多操作。在学习Python的过程中,我们会发现交互性shell很快就会成为我们亲密的伙伴之一。

另一种解决方案具有更漂亮的图形外观,称为集成开发环境IDLE)。这是一种简单的IDE,主要面向初学者。它的功能比控制台中的原始交互性shell稍微强大一点,因此我们也可以对它进行探索。Windows操作系统的Python安装程序是免费的,我们也可以很容易地把它安装在任何系统中。我们可以在Python的网站中找到与此有关的信息。

Guido Van Rossum是根据英国喜剧团Monty Python为他所发明的这种语言命名的,因此有一种传说,选择IDLE这个名称是为了向Eric Idle致敬,他是喜剧团Monty Python的创立者之一。

除了作为脚本运行或者在shell中运行之外,Python也可以编成代码以应用程序的形式运行。我们在本书中将会看到与这个模式有关的很多例子。稍后当我们讨论如何组织和运行Python代码时,将会对此有更深刻的理解。

Python也可以以图形用户界面GUI)的形式运行。我们可以使用几种框架,有些框架是跨平台的,有些框架是特定于某个平台的。在第12章GUI和脚本中,我们将看到一个使用Tkinter创建的GUI应用程序的例子。Tkinter是一个面向对象层,生存在Tk(Tkinter的含义是TK接口)的顶部。

Tk是一个GUI工具包,它把桌面应用程序的开发带入较之传统方法更高的层次。它是工具命令语言Tcl)的标准GUI,但也可用于许多其他动态语言。它可以生成丰富的本地应用程序,能够无缝地在Windows、Linux、macOS X及其他操作系统中运行。

Tkinter是与Python捆绑的,因此Python程序员可以很方便地进入GUI世界。出于这个原因,我选择它作为本书所讨论的GUI例子的框架。

在其他GUI框架中,我们发现下面这些框架是最为常用的。

对它们进行详细的描述将超出本书的范围,但我们可以在Python网站的“What platform-independent GUI toolkits exist for Python?”(Python存在哪些独立于平台的GUI工具包?)一节中找到我们所需要的信息。如果读者想要寻找一些GUI框架,记住要根据一些原则选择最适合的框架。

要确保它们有以下特点。

我们对Python代码的组织方式稍做讨论。在本书中,我们将稍稍“深入兔子洞”,介绍一些更具技术性的名称和概念。

先来介绍最基本的概念,Python代码是如何组织的?当然是我们把代码编写在文件中。当我们用.py扩展名保存一个文件时,这个文件就成了一个Python模块。

如果是在Windows或macOS这种一般会隐藏扩展名的操作系统中,要确保对配置进行修改,以便看到完整的文件名。这并不是严格的要求,而是一个建议。

把软件所需要的所有代码保存在一个文件中是不切实际的。这种方法只适用于脚本,它们的长度最多不会超过几百行(而且通常都比较短)。

一个完整的Python应用程序可能由数十万行代码所组成,因此我们不得不把它们划分到不同的模块中。这种做法要好一点,但还不够好。事实证明,就算采用了这种做法,我们在操作代码时仍然是极为麻烦的。

因此,Python提供了另一种称为package程序包)的结构,它允许我们把模块组合在一起。一个程序包就是一个简单的文件夹,但它必须包含一个特殊的文件__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的作用就是告诉Python:util是一个程序包,而不仅仅是个简单的文件夹。

如果这个软件只是在模块内进行组织,那么要想推断它的结构是非常困难的。我把一个只含模块的例子放在ch1/files_only文件夹中,读者可以自行查阅:

$ tree -v files_only

这将展示一幅完全不同的画面:

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

要想猜测每个模块的作用要困难一些,是不是?现在,考虑到它只是一个简单的例子,我们可以想象,如果我们不采用程序包和模块的方法,要想理解一个真实的应用程序将是一件多么困难的事情。

当一位开发人员在编写一个应用程序时,很可能需要把同一段逻辑应用于程序的不同部分。例如,为用户可能在网页上填写的数据编写解析器时,应用程序必须验证某个字段是否包含了数字。不管这种验证的逻辑是如何编写的,很可能有多个地方都需要使用这种逻辑。

例如,在一个投票应用程序中,用户需要回答多个问题,很可能有几个问题要求答案是数值形式的。例如:

在每个需要数值答案的地方复制粘贴验证逻辑是一个非常糟糕的做法。这违反了“不要做重复劳动”的原则。这个原则表示我们在应用程序中不应该把相同片段的代码重复使用超过一次。我觉得需要强调这个原则的重要性:绝对不要在应用程序中把同一段代码重复多次

把同一段逻辑重复多次的做法之所以极为糟糕出于很多原因,其中最重要的几个原因如下。

Python是一种出色的语言,为我们提供了实现最佳的编程实践所需要的所有工具。在这个特定的例子中,我们需要能够复用一段代码。为了能够复用一段代码,我们需要使用一种结构保存这段代码,这样我们在每次需要复制它所蕴含的逻辑时就可以调用这个结构。这样的结构确实存在,它就是函数

我不打算在此深入介绍函数的特定细节,我们只需要记住函数是一段有组织的、可复用的代码,用于完成一个任务。根据函数所属的环境类型的不同,它们可能呈现不同的形式和名称,但现在我们还不需要详细了解这一点。我们将在本书的后面真正领会函数的作用并看到这些细节。函数是应用程序模块化的基础构件,几乎是不可或缺的。除非我们所编写的是一个超级简单的脚本,否则肯定会用到函数。我们将在第4章函数,代码的基础构件中详细讨论函数。

正如我之前所说的那样,Python提供了一个非常全面的程序库。现在就是对程序库进行定义的良好时机:程序库是一些函数和对象的集合,提供了一些功能,从而丰富了语言的功能。

例如,在Python的math库中,我们可以发现大量的函数,其中一个是阶乘函数,它可以用于计算一个数的阶乘。

在数学中,一个非负整数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是工具程序库。我们的自定义工具belt包含了我们的应用程序所需要的所有可复用的工具(即函数)。其中有些用于处理数据库(db.py),有些用于处理网络(network.py),有些用于执行一些超出了Python的标准math库范畴的数学计算(math.py),因此我们不得不自己编写代码来实现这些功能。

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

在本节中,我们将介绍一些非常重要的概念,如名称和名字空间、作用域。当然,我们可以阅读官方语言参考,了解与Python的执行模型有关的所有信息。但是,我觉得它的介绍技术性太强并且过于抽象,因此我们在这里提供一个非正式的解释。

假设我们正在寻找一本书,因此来到图书馆并询问管理员自己想要借的书。管理员告诉我们类似“二楼,X区域,第3排”这样的信息。因此我们上楼,找到X区域并继续进行寻找。

如果一家图书馆的所有书籍都随机堆在一个大房间里,情况就大不相同。没有楼层、没有区域、没有书架、没有顺序,在这样的图书馆里寻找一本书是件极为困难的事情。

当我们编写代码时,我们面临着相同的问题:我们必须对代码进行组织,这样以前并不了解这些代码的人也可以很方便地找到他们所寻找的东西。软件具有正确的结构可以提升代码的复用率。另外,组织形式糟糕的软件很可能散布着大量的重复逻辑的代码。

首先,我们以书为例子。我们用书名表示一本书,用Python的行话表示就是名称。Python的名称最接近于其他语言所称的变量。名称一般表示对象,是通过名称绑定操作引入的。我们先来观察一个简单的例子(注意,#后面的内容是注释):

>>> n = 3   # 整数
>>> address = "221b Baker Street, NW1 6XE, London"  # 歇洛克·福尔摩斯的住址
>>> employee = {
...     'age': 45,
...     'role': 'CTO',
...     'SSN': 'AB1234567',
... } 
>>>  # 让我们输出它们
>>> 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

在上面的代码中,我们定义了如下3个对象(还记得每个Python对象所具有的3个特性吗?)。

不要担心,现在不知道什么是字典是完全正常的。我们将在第2章内置的数据类型中讨论Python的这个数据结构之王。

有没有注意到,当我们输入employee的定义之后,输入提示符从>>>变成了…?这是因为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提供了以下4个可以访问的不同作用域(当前,它们并不一定同时存在)。

规则如下:当我们引用一个名称时,Python会先在当前名字空间中寻找它;如果没有找到这个名称,Python就继续在外层作用域中寻找;这个过程将一直持续,直到搜索完内置作用域;如果一个名称在搜索了内置作用域之后仍未找到,则Python就会触发一个NameError异常,表示这个名称未被定义(在前面的例子中可以看到这个结果)。

因此,在寻找一个名称时,名字空间的搜索顺序是:局部外层全局内置即LEGB规则)。

这个描述有点过于抽象,因此让我们观察一个例子。为了展示局部作用域和外层作用域,我们必须定义一个新函数。现在没有必要担心不熟悉定义新函数的语法。我们将在第4章中研究函数。现在只要记住,在下面的代码中,当我们看到def时,它表示定义了一个函数。

# scopes1.py
# 局部和全局

# 我们定义了一个名为local的函数
def local():
    m = 7
    print(m)

m = 5
print(m)

# 我们调用(或执行)local函数
local()

在上面这个例子中,我们定义了相同的名称m,它们分别位于全局作用域和局部作用域(local函数所定义的那个)中。当我们用下面的命令执行这个程序时(要记得激活虚拟环境):

$ python scopes1.py

我们可以看到控制台所输出的两个数字:5和7。

Python解释器将从上而下解析这个文件。首先,它会找到几个注释行并将其忽略。然后,它会解析local函数的定义。当local函数被调用时,它会执行两项任务:设置一个表示数字5的对象的名称并输出。Python解释器将继续执行自己的任务,并找到另一个名称绑定。这次的绑定发生在全局作用域,其值是5。下一行是调用被执行的print函数(我们将在控制台上看到第一个输出的值:5)。

在此之后,我们调用了local函数。此时,Python执行这个函数,因此现在发生了m = 7的绑定,并且这个值被输出。

一个非常重要的事实是属于local函数定义的那部分代码向右缩进了4个空格。事实上,Python是通过缩进代码来定义作用域的。我们通过缩进进入一个作用域,并通过取消缩进退出这个作用域。有些程序员使用2个空格的缩进,有些则使用3个空格的缩进,但建议使用4个空格的缩进。这是一种很好的最大限度地提高可读性的措施。我们以后会更多地讨论在编写Python代码时应该遵循的所有约定。

如果我们删除m = 7这一行会发生什么呢?记住LEGB规则。Python会先在局部作用域(local函数)中寻找m。如果没有找到,它会进入下一个外层作用域。在这个例子中,这个外层作用域是全局作用域,因为local函数并不是出现在其他函数调用的内部。因此,我们将在控制台上看到两个输出的数字都是5。让我们实际观察这种情况下的代码是怎么样的:

# scopes2.py
# 局部和全局

def local():
    # m并不属于local函数所定义的作用域
    # 因此Python将在下一个外层作用域中寻找
    # m最终在全局作用域中被找到
    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函数被调用时,Python并没有在这个函数的作用域中找到m。因此Python会按照LEGB规则继续寻找,直到在全局作用域中找到m。

让我们观察一个具有额外层的例子,也就是局部作用域和全局作用域之间有一个外层作用域:

# scopes3.py
# 局部、外层和全局

def enclosing_func():
    m = 13

    def local():
        # m并不属于local函数所定义的作用域
        # 因此Python将在外层作用域中寻找m。这次m是在
        # 外层作用域中找到的
        print(m, 'printing from the local scope')

    # 调用local函数
    local()

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

enclosing_func()

运行scopes3.py,Python将在控制台上输出:

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

我们可以看到,local函数中的print指令像以前一样引用了m。m仍然不是在这个函数内部定义的,因此Python按照LEGB规则开始在外层作用域中寻找。这次,它在外层作用域中找到了m。

如果读者对这个过程仍然不是非常清楚,不必心怀忧虑。当我们更多地讨论本书的例子时,迟早会彻底弄清这个概念。Python教程的“类”一节对作用域和名字空间有一段有趣的描述。如果读者希望更深入地理解这个主题,可以在某个时候阅读这段内容。

在结束本章之前,我们稍微讨论一下对象。不管怎样,Python中的所有东西都是对象,因此值得我们花一些时间对它进行一些关注。

本章的1.1节编程预备知识在介绍对象时,提到过我们用对象表示现实世界的物品。例如,我们如今通过网络销售任何种类的商品,所以需要我们能够适当地处理、存储和表示它们。但是,对象实际上的含义要丰富得多。我们在Python中所完成的绝大部分工作都是对对象进行操作。

因此,我们在此不打算太深入地讨论对象(我们将在第6章面向对象编程、装饰器和迭代器中深入讨论对象),只是对类和对象进行一些概括的介绍。我们已经知道了对象是Python对数据的抽象。事实上,Python中的所有东西都是对象。数值、字符串(保存文本的数据结构)、容器、集合甚至函数都是对象。我们可以把对象看成是至少具有3个特征的盒子:ID(独一无二的)、类型和值。

但它们是怎么进入生活的?我们是怎样创建它们的?我们应该如何编写自己的自定义对象?答案藏在一个简单的词中:

事实上,对象是类的实例。Python的优点在于类本身也是对象,我们现在不会深究这个概念,但要知道它是Python语言高级的概念之一:元类。现在,我们最好通过一个例子来弄明白类和对象之间的区别。

假设有一位朋友说“我买了一辆自行车”,我们会立即就明白对方的意思。我们看到过这辆自行车吗?不。我们知道它的颜色吗?不。知道牌子吗?不。知道其他细节吗?不。但是,我们已经知道了足够的信息,足以理解朋友所表示的“我买了一辆自行车”是什么意思。我们知道自行车是有两个轮子装在车架上,还有车座、踏板、车把、刹车等配件的物品。换句话说,即使我们没有看到过这辆自行车,我们却知道自行车这个概念。特性和特征的一组抽象集合组合在一起就形成了一种称为“自行车”的东西。

在计算机编程中,这种抽象称为。这个概念非常简单。类用于创建对象。事实上,对象被称为类的实例

换句话说,我们知道自行车是什么东西,即我们知道这个类。当我们有了自己的自行车时,该自行车就是自行车类的一个实例。我的自行车具有它自己的特征,其他人也有自己的自行车,它们属于同一个类,但是是不同的实例。这个世界上所制造的每辆自行车都是自行车类的一个实例。

让我们观察一个例子。我们将编写一个定义自行车的类,然后创建两辆自行车,一辆红色的和一辆蓝色的。我会尽量使代码保持简单,但是如果读者还不能完全理解这些代码,也不必气馁。现在我们只需要理解类和对象(或类的实例)之间的区别:

# bike.py
# 让我们定义Bike类
class Bike:

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

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

# 让我们创建几个实例
red_bike = Bike('Red', 'Carbon fiber')
blue_bike = Bike('Blue', 'Steel')

# 让我们检查自己所拥有的对象,也就是Bike类的实例
print(red_bike.colour)     # 输出:Red(红色)
print(red_bike.frame_material)     # 输出:Carbon fiber(碳纤维)
print(blue_bike.colour)    # 输出:Blue(蓝色)
print(blue_bike.frame_material)    # 输出:Steel(钢)

# 刹车!
red_bike.brake()  # 输出:Braking!(刹车!)

现在,我希望读者已经掌握了怎么运行这个文件。代码块的第1行指定了文件名。只要运行$ python文件名,一切就没问题了。但是,要记得先激活虚拟环境。

这里有很多值得注意的有趣事情。首先,类的定义是用class语句创建的。然后,class语句后面的代码都被缩进了,称为类体。在这个类中,属于类定义的最后一行是print("Braking!")。

定义了这个类之后,我们就可以创建它的实例了。我们可以看到,这个类的类体包含了两个方法的定义。简单地说,方法就是属于某个类的函数。

第一个方法init是这个类的初始化方法。它使用了一些“Python魔术”,使用我们在创建对象时所传递的值来设置这个对象。

在Python中,每个具有前缀和后缀双下划线的方法称为魔术方法。Python所使用的魔术方法具有很多不同的用途,因此,在创建自定义的方法时,不应该使用双下划线作为方法名的前缀和后缀。这个命名约定最好保留给Python使用。

我们所定义的另一个方法brake是我们在刹车时想要调用的一个其他方法的例子。当然,它只包含了一个print语句。它仅仅是一个例子而已。

然后,我们创建了两个Bike对象。一个为红色,采用了碳纤维的车架;另一个为蓝色,采用了钢制的车架。我们在创建对象时向它们传递这些值。在创建之后,我们输出红色自行车的颜色属性和车架类型,然后输出蓝色自行车的颜色属性和车架类型。我们还调用了red_bike对象的brake方法。

最后还有一点值得注意。还记得我说过一个对象的属性集合也是一个名字空间吗?我希望读者现在对这个概念已经有了更进一步的理解。我们看到可以通过不同的名字空间(red_bike、blue_bike)来获取frame_type属性,从而得到不同的值。它们不会出现重叠,也不会发生冲突。

当然,这里的点号操作符(.)用于进入一个名字空间,它作用于对象时也是如此。

编写良好的代码并不像看上去那么容易。正如我之前所说的那样,良好的代码具有很多很难同时具备的品质。在某种程度上,编写良好的代码是一门艺术。不管我们打算采用什么样的学习之路,有一样东西能够让我们的代码得到即刻的提升:PEP 8

根据维基百科的说法:

“Python的开发很大程度上是通过PEP(Python Enhancement Proposal,Python增强建议书)过程所指导的。PEP过程是提议重要新特性的主要机制,它用于收集Python社区对某个问题的输入,并记录Python已经做出的设计决策。”

PEP 8可能是所有PEP中最著名的一个。它提出了一个简单但非常有效的指导方针集合,并且把Python定义为一种美学,使我们可以编写出优美的Python代码。如果我们只能从本章中吸取一个建议,那就是:使用它,拥抱它。很快我们就会庆幸这个决定。

如今的编程不再是一种个人的登记或退出业务,它是一项更需要社交能力的活动。几个开发人员共同协作,使用类似Git和Mercurial这样的工具一起开发一段代码,其结果就是一段代码很可能出自很多不同的开发人员之手。

Git和Mercurial很可能是如今非常常用的分布式修订控制系统。它们是重要的工具,作用是帮助开发人员团队在同一个软件项目上实现协作。

现如今,我们较之以往更需要一种一致的方法编写代码,因此愈发强调了代码可读性的重要性。当一家公司的所有开发人员都遵循PEP 8时,非常可能出现的情况就是:任何人在接触一段代码时,都觉得它就像是自己所编写的。对我来说,情况一直都是如此(我总是忘了哪些代码是自己编写的)。

这就产生了一个巨大的优点:当我们阅读符合自己编写习惯的代码时,可以很容易地理解它。如果没有这样的约定,每个程序员都按自己喜欢的方式编写代码,或者简单地采取他们所学习或习惯的方式编写代码,很可能会导致其他程序员在理解每段代码时都必须了解编写者的个人风格。感谢PEP 8,让我们可以避免这种情况。我是PEP 8的推崇者,我在评价代码时,如果它不符合PEP 8的精神,就不会给予它太高的评价。因此,我们应该花点时间研究这个建议书,它是非常重要的。

在本书的例子中,我会尽量符合它的精神。遗憾的是,我没有阔气到每行代码都使用79个字符(这是PEP 8所推荐的每行代码的最大长度),因此不得不削减一些空行和其他东西,但我保证尽最大努力完善代码的布局,尽可能地使它们容易阅读。

Python在所有的编程行业中都得到了广泛的应用。许多不同的公司使用Python完成许多不同的工作,而且Python也广泛应用于教育领域(它是一种优秀的教学语言,因为它的许多特性非常容易学习)。

Python在如今非常流行的一个原因是它的社区庞大、富有活力,并且聚集了大量优秀的人才。Python社区在全世界范围内组织了许多事件,这些事件大部分是围绕Python或它的Web主框架Django进行的。

Python是开放的,它的支持者的思维也常常是非常开放的。关于这方面的更多信息,可以访问Python网站的社区页面,并参与其中。

与Python有关的另一个现象是Python心理惯性。事实上,Python允许我们使用一些其他语言所没有的惯用法,这些惯用法至少在形式上明显不同或者在其他语言中不容易使用(现在,当我用一种非Python语言编写代码时就会产生恐慌感)。

不管怎样,在过去的几年里,很多人确实出现了Python心理惯性。按照我的理解,它有点类似于“按照Python所建议的方式做其他任何事情”。

为了帮助读者更多地理解Python的文化以及Python心理惯性,我特意展示了《禅意Python》。打开控制台并输入import this,就会出现下面的输出:

>>> import this
The Zen of Python, by Tim Peters(禅意Python,作者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 IDE)。我个人使用的是Sublime文件编辑器,它可以免费试用,正式版也只需要几美元。在我的编程生涯中,我试过很多IDE,但唯有这个IDE让我觉得能够极大地提高工作效率。

下面有两个重要的建议。

在本章中,我们开始探索编程世界并初步了解了Python。在学习之旅中,我们才刚刚出发,只是稍稍接触了一些概念,我们将在本书的后面详细讨论这些概念。

我们讨论了Python的主要特性、它的使用者以及它的用途,并讨论了编写Python程序的不同的方法。

在本章的最后部分,我们简单讨论了名字空间、作用域、类和对象的基本概念。我们还了解了使用模块和程序包对Python的代码进行组织的方法。

在实践的层次上,我们学习了怎样在自己的系统中安装Python、怎样保证自己所需要的工具已经就绪,并了解了pip和virtualenv,然后创建并激活了我们的第一个虚拟环境。它们允许我们在一个自包含的环境中进行工作,而不用冒着与Python的系统安装发生冲突的风险进行工作。

现在,大家已经准备好了跟随我进行本书的学习之旅。我们所需要的就是热情、一个激活的虚拟环境、本书、自己的手指以及一杯咖啡。

尝试运行本章的例子。我已经尽量使它们简单和短小。如果亲手输入这些例子,而不是仅仅阅读它们,无疑能够留下更深的印象。

在下一章中,我们将探索Python丰富的内置数据类型集合。对于这个主题,我们有大量的东西需要学习!


“数据!数据!数据!”他不耐烦地叫喊道,“没有黏土,我可做不出砖头。”

——《福尔摩斯探案集》

我们用计算机所做的每件事情都是在管理数据。数据有许多不同的形态和风格。我们所聆听的音乐、所观赏的电影、所打开的PDF文件都是数据。甚至读者此刻正在阅读的这一章,其来源是一个文件,它也是数据的一种。

数据可以非常简单,如表示年龄的整数。数据也可以非常复杂,如一个网站中所显示的订单。数据可以是单个对象,也可以是一组对象的集合。数据的内容甚至可以是关于数据的,这种数据称为元数据。这种数据用于描述其他数据结构的设计或者描述应用数据及它的上下文环境。在Python中,对象是数据的抽象。Python所提供的数据结构种类多到令人吃惊,我们可以用它们表示数据,或者将它们组合在一起创建自定义数据。

在本章中,我们将讨论下面这些内容。

在深入探究细节之前,我们需要对Python中的对象有一个清晰的理解。因此,我们对这个概念稍做讨论。正如本节的标题所述,Python中的所有东西都是对象。但是,当我们在一个Python模块中输入像age = 42这样的指令时,会发生什么呢?

如果访问Python Tutor网站,我们可以在一个文本框中输入这条指令并看到它的可视化表示形式。记住这个网站,它非常有助于我们巩固对后台所发生事情的理解。

因此,实际所发生的事情就是有一个对象被创建。它获得了一个ID,它的类型被设置为int(整数),它的值为42。全局名字空间中出现了一个名为age的名称,它指向这个对象。此后,当我们位于这个全局名字空间时,执行这行代码就可以简单地通过名称age来访问这个对象。

打个比方,如果我们打算搬家,可能会把所有的餐刀、叉子和汤匙放在一个盒子里,并贴上“餐具”的标签。是不是可以理解这种做法其实表达了相同的概念?图 2-1 所示的是这个网页可能出现的一个屏幕截图(为了获得相同的视图,可能需要调整一些设置)。

图2-1

因此,在本章的剩余部分,当我们看到像name = some_value这样的代码时,可以认为有一个名称出现在与该指令所在的作用域相关联的名字空间中,用一个箭头指向一个具有ID、类型和值的对象。对于这种机制,还有更多需要解释的细节,但对于一个简单例子来说,这些已经足够了。我们将在以后再来讨论这些细节。

Python中数据之间的第一个基本区别就是对象的值是否可以改变。如果对象的值可以改变,该对象就称为可变对象。如果对象的值不能改变,该对象就称为不可变对象

理解可变对象和不可变对象之间的区别是非常重要的,因为它会影响我们所编写的代码。因此,下面就有这样一个问题:

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

在上面这段代码中,#A行是不是改变了age的值?答案是否定的,但现在它是43了(我可以感觉到读者的疑问)。是的,它是43,但42是个int类型的整数,它是不可变对象。事实的真相出现在第1行,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。我们可以看到,与预期的一样,两个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   # 我希望如此
>>> id(fab)        # 将是相同的
4380878496
>>> id(fab.age)    # 将是不同的
4377552624

在这个例子中,我们设置了一个Person类型(一个自定义类)的对象fab。在创建这个对象时,age的值是42。我们输出这个对象的值和它的对象ID,另外还输出age的ID。注意,即使把age改为25,fab的ID仍然不变(当然,age的ID发生了变化)。Python中的自定义对象是可变的(除非把它设置为不可变的)。记住这个概念,它是极为重要的。在本章的剩余部分,我会反复向读者灌输这个概念。

我们现在来讨论Python表示数值的内置数据类型。Python的设计者具有数学和计算机科学双硕士学位,因此Python对数值提供了强大的支持是完全符合逻辑的。

数值是不可变对象。

Python中的整数并没有范围限制,仅仅受限于可用的虚拟内存。这意味着我们并不需要担心数值过大这个问题,只要它能够被计算机内存所容纳,Python就能够处理妥当。整数可以是正数、负数或0。它们支持所有的基本数学操作(运算),如下面的例子所示:

>>> a = 14
>>> b = 3
>>> a + b   #  加法
17
>>> a - b   #  减法
11
>>> a * b   #  乘法
42
>>> a / b   #  真正的除法
4.666666666666667
>>> a // b  # 整数除法
4
>>> a % b   # 取模操作(取余数)
2
>>> a ** b  # 乘方操作
2744

上面的代码应该很容易理解,不过要注意一个重要的地方:Python提供了两种除法操作符,一种是真正的除法(/),它将返回操作数的商。另一种是所谓的整数除法(//),它将返回操作数的商向下取整的整数。值得注意的是,Python 2的除法操作符/的行为与Python 3不同。我们可以通过下面的例子观察它对于正数和负数操作的区别所在:

>>> 7 / 4   # 真正的除法
1.75
>>> 7 // 4  # 整数除法,截断后返回1
1
>>> -7 / 4  # 同样是真正的除法,返回结果与上面相反
-1.75
>>> -7 // 4 # 整数除法,其结果并不是前面的相反数
-2

这是个有趣的例子。我们可能预期的是最后一行返回−1,不要对实际结果感觉糟糕,它只是Python的工作方式而已。在Python中,整数除法的结果总是向无限小取整。如果我们不想向下取整,而是想把一个数截取为整数,可以使用内置的int函数,如下面的例子所示:

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

注意,截取操作是向0的方向进行的。

另外还有一个操作符用于计算余数,称为求模操作符,用百分符号(%)表示:

>>> 10 % 3  # 取10 // 3的余数
1
>>> 10 % 4  # 取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是两个用于表示真值的关键字。布尔值是整数的一个子类,True和False的行为分别类似于1和0。布尔值对应的int类是bool类,它要么返回True,要么返回False。每个内置的Python对象在布尔值语境中都有一个值,意味着当它们输入到bool函数时其结果要么为True,要么为False。我们将在第3章迭代和决策中讨论关于这方面的例子。

布尔值可以在布尔表达式中使用逻辑操作符and、or和not进行组合。同样,我们将在第3章迭代和决策中详细讨论这种方法,现在我们来观察一个简单的例子:

>>> int(True)   # True的行为类似于1
1
>>> int(False)  # False的行为类似于0
0
>>> bool(1)     # 1在布尔值语境中求值为True
True
>>> bool(-42)   # 所有非零的数值都是如此
True
>>> bool(0)     # 0求值为False
False
>>> # 逻辑操作符(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属于一个从整数类派生的类,它们会根据需要转换为整数。这个话题与继承有关,将在第6章面向对象编程、装饰器和迭代器中详细讨论。

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

如果读者对这种数据格式的细节感兴趣,可以访问维基百科。

编程语言通常会向程序员提供两种不同的浮点格式:单精度和双精度。前者占据 32位的内存,后者则是64位。Python只支持双精度格式。我们观察一个简单的例子:

>>> pi = 3.1415926536  # 我们可以背出pi的多少位小数?
>>> 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位表示浮点数,这将意味着我们最多可以表示264 = 18 446 744 073 709 551 616个数。只要观察浮点数的最大值和最小值,我们就会意识到要想表示所有的浮点数是不可能的。64位的空间并不足够,因此需要近似地取最接近的可表示值。读者很可能会觉得只有极端大值或极端小值才会遇到这种问题。实际上并不是这样,读者可以细加思量,并在控制台中试验下面的代码:

>>> 0.3 - 0.1 * 3  # 它的结果应该是0!!!
-5.551115123125783e-17

这个例子说明了什么?它告诉我们,双精度浮点数即使在表示像0.1或0.3这样的简单数值时也会遇到精确性问题。这一点为什么非常重要?如果我们所处理的是价格、金融计算或任何不能取近似值的数据,那么就会遇到很大的问题。不要担心,Python还提供了decimal类型,它就不存在这个问题。我们将在稍后讨论这种类型。

Python创造性地提供了对复数的支持。有些读者可能不明白复数是什么,复数就是用a + ib形式表示的数,其中ab都是实数,而i(工程师可能会用j)是虚数单位,也就是−1的平方根。ab分别称为复数的实部和虚部。

除非我们所编写的代码涉及科学方面,否则用到复数的可能性微乎其微。我们观察一个简单的例子:

>>> c = 3.14 + 2.73j
>>> c.real      # 实部
3.14
>>> c.imag      # 虚部
2.73
>>> c.conjugate()   # A + Bj的共轭是A - Bj
(3.14-2.73j)
>>> c * 2       #  允许乘法
(6.28+5.46j)
>>> c ** 2      #  也允许乘方操作
(2.4067000000000007+17.1444j)
>>> d = 1 + 1j  #  也允许加法和减法
>>> c - d
(2.14+1.73j)

我们对数值部分的探索之旅的最后一站是分数和小数。分数可以保存一个最简形式的有理数分子和分母。我们来观察一个简单的例子:

>>> from fractions import Fraction
>>> Fraction(10, 6)   
Fraction(5, 3)       # 注意它已经被简化
>>> 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      # 简单起见进行了重命名
>>> D(3.14)    # 来自浮点值的pi,因此存在近似值问题
Decimal('3.140000000000000124344978758017532527446746826171875')
>>> D('3.14')  # 来自字符串的pi,因此不存在近似值问题
Decimal('3.14')
>>> D(0.1) * D(3) - D(0.3)   # 来自浮点值,仍然存在近似值问题
Decimal('2.775557561565156540423631668E-17')
>>> D('0.1') * D(3) - D('0.3')  #  来自字符串,一切完美
Decimal('0.0')
>>> D('1.4').as_integer_ratio() # 7/5 == 1.4 (是不是很酷?)
(7, 5)

注意,当我们从一个浮点数构建一个Decimal(小数)时,小数会接收浮点数可能存在的所有近似值问题。另外,当小数不存在近似值问题时(例如,我们把一个整数或字符串表示形式传递到Decimal的构造函数中),则计算结果不会表现出古怪的行为。在涉及金钱的计算时,就需要使用小数。

现在我们就完成了内置的数值类型的介绍。下面让我们观察序列。

我们首先讨论不可变序列:字符串、字节和元组。

Python中的文本数据是用str对象处理的,它的更常见名称是字符串。它们是Unicode代码点的不可变序列。Unicode代码点可以表示字符,但也可以具有其他含义,如格式化数据。和其他语言不同,Python并不支持char类型,因此单个字符会被简单地认为是长度为1的字符串。

Unicode 是一种优秀的数据处理方式,任何应用程序都应该在内部使用这种方式。但是,当我们存储文本数据或者通过网络发送它们时,可能想要使用一种适合当前所使用媒介的编码方式对它们进行编码。编码过程的结果产生了一个bytes(字节)对象,它的语法和行为与字符串类似。字符值字面值在Python中是用单引号、双引号或三引号(同时包括单引号和双引号)界定的。如果字符串出现在一对三引号内部,那么它可以跨越多行。下面这个例子清晰地说明了这一点:

>>> # 创建字符串的4种方式
>>> 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

1.字符串的编码和解码

使用encode和decode方法可以对Unicode字符串进行编码以及对bytes对象进行解码。UTF-8是一种可变长度的字符编码方式,几乎能够对所有的Unicode代码点进行编码。它是网络上占据主导地位的编码方式。另外,如果在一个字符串声明之前添加了一个字母b,那么系统就会创建一个bytes对象:

>>> s = "This is üŋíc0de"  # unicode字符串:代码点
>>> type(s)
<class 'str'>
>>> encoded_s = s.encode('utf-8')  # utf-8编码版本的 s
>>> encoded_s
b'This is \xc3\xbc\xc5\x8b\xc3\xadc0de'  # 结果:bytes对象
>>> type(encoded_s)  # 另一种验证它的方式
<class 'bytes'>
>>> encoded_s.decode('utf-8')  # 把它恢复到原始形式
'This is üŋíc0de'
>>> bytes_obj = b"A bytes object"  # 一个bytes对象
>>> type(bytes_obj)
<class 'bytes'>

2.字符串的索引和截取

对序列进行操作时,访问一个精确位置或者获取该序列的一个子序列(截取)都是极为常见的操作。处理不可变对象的时候,这两个操作都处于只读模式。

索引操作所采用的形式是以0为基数访问序列中的任何位置,截取操作的形式又有所不同。当我们获取一个序列的一个片段时,可以指定起始位置、终止位置和步长。它们是像下面这样用冒号(:)分隔的:my_sequence[start:stop:step]。所有的参数都是可选的,start是含指定位置的,stop则不含指定位置。用一个例子来说明截取操作要远比语言描述清楚得多:

>>> s = "The trouble is you think you have time."
>>> s[0]  # 取位置0的索引,即第1个字符
'T'
>>> s[5]  # 取位置5的索引,即第6个字符
'r'
>>> s[:4] # 截取,只指定终止位置
'The '
>>> s[4:] # 截取,只指定起始位置
'trouble is you think you have time.'
>>> s[2:14]   # 截取,同时指定起始位置和终止位置
'e trouble is'
>>> s[2:14:3] # 截取,指定了起始位置、终止位置和步长(每3个字符)
'erb '
>>> s[:]      # 创建一份副本的快速方式
'The trouble is you think you have time.'

在上面所有的行中,最后一行可能是最有趣的。如果我们不指定任何参数,Python会自动为我们指定默认参数。在这种情况下,start将是字符串的起始位置,stop将是字符串的末尾,step将是默认的1。这是获取字符串s的一份副本(相同的值,但不同的对象)的一种方便而快捷的方法。我们能不能通过截取操作获取一个字符串的一份反向副本呢?(不要查答案,想办法自己完成。)

3.字符串的格式化

字符串所具有的一个特性是它可以作为模板使用。Python中有几种不同的方式可以对字符串进行格式化,我鼓励读者通过查阅文档来寻找所有可能的格式化方式。下面是一些常见的例子:

>>> 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'

在上面这些例子中,我们可以看到对字符串进行格式化的4种不同方法。第一种方法依赖于%操作符,该方法已经被摒弃,不应该再使用。当前所使用的对字符进行格式化的方法是使用字符串的format方法。我们可以从不同的例子中看到,一对花括号作为字符串内部的一个占位符。当我们调用format时,系统就会向它传递数据替换这些占位符。我们可以在花括号内指定索引(以及其他更多信息),甚至可以在其中指定名称,以表示我们在调用format时所使用的是关键字参数而不是位置参数。

注意greet_positional_idx是怎样在调用format时通过传递不同的数据产生不同的结果的。从表面上看,我输入了Python和Coffee,结果却令人惊异!

我想介绍的最后一个特性是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..."

读者可以参阅官方文档,了解字符串格式化的所有细节,领略它的强大功能。

我们将要讨论的最后一种不可变序列类型是tuple(元组)。元组是任意Python对象的序列。在元组中,元素是由逗号分隔的。元组在Python中使用得极为广泛,因为它允许使用模式,这个功能很难被其他语言所复制。有时候元组是被隐式地使用的。例如,在一行中设置多个变量或者允许函数返回多个不同的对象(在许多其他语言中,一个函数通常只返回一个对象),甚至在Python控制台中,我们可以隐式地使用元组通过一条指令来输出多个元素。下面是一个包含所有这些情况的例子:

>>> t = ()   # 空元组
>>> type(t)
<class 'tuple'>
>>> one_element_tuple = (42, )  #  需要逗号!
>>> three_elements_tuple = (1, 3, 5)  # 括号在这里是可选的
>>> a, b, c = 1, 2, 3   # 元组用于多个赋值
>>> a, b, c  # 隐式的元组,用一条指令进行输出
(1, 2, 3)
>>> 3 in three_elements_tuple   # 成员测试
True

注意,成员操作符in也可以在列表、字符串和字典中使用。一般而言,它适用于集合和序列对象。

注意,为了创建一个包含一个元素的元组,我们需要在这个元素后面添加一个逗号。原因是如果没有这个逗号,而只有这个元素出现在括号中,那么该元素就会被当作一个冗余的数学表达式。另外还要注意在赋值时,括号是可选的,因此my_tuple = 1, 2, 3和my_tuple = (1, 2, 3)是相同的。

元组赋值允许我们实现的一个功能是单行交换,不需要第三个临时变量。我们先来观察完成这个任务的一种更为传统的方法:

>>> a, b = 1, 2
>>> c = a  # 我们需要3行代码和一个临时变量c
>>> a = b
>>> b = c
>>> a, b   # a和b已经被交换
(2, 1)

现在我们观察如何在Python中完成这个任务:

>>> a, b = 0, 1
>>> a, b = b, a  # 这是可以在Python中所采用的方法
>>> a, b
(1, 0)

我们可以观察Python所采用的交换两个值的方法。还记得我在第1章Python概述中所说的吗?Python程序的长度一般只有对应的Java或C++代码的五分之一到三分之一,像单行交换这样的特性就是一个很好的证明。Python是优雅的,而这个场景下的优雅同时也意味着节约。

由于元组是不可变的,因此它们可以作为字典的键(稍后讨论)。对我而言,元组是Python的一种内置数据,它最接近于数学上的向量概念。不过,这并不意味着这就是创建它们的原因。

元组通常包含了元素的混合序列。反之,列表的元素大多是同种类型的。而且,元组一般是通过解包或索引访问的,而列表一般是通过迭代访问的。

可变序列与不可变序列的区别在于它们在创建之后是否可以改变。Python提供了两种可变序列类型:列表和字节数组。我之前提到过字典是Python中的数据类型之王,我觉得列表就是它的“正宫王后”。

Python的列表是可变序列。它与元组非常相似,但没有不可变这个限制。列表一般用于存储相同类型的对象,但是在列表中存储不同类型的对象也是可行的。我们可以通过许多不同的方式来创建列表。让我们观察一个例子:

>>> []        # 空列表
[]
>>> list()    # 与[]相同
[]
>>> [1, 2, 3] # 和元组一样,元素之间是用逗号分隔的
[1, 2, 3]
>>> [x + 5 for x in [2, 3, 4]]  # Python很神奇
[7, 8, 9]
>>> list((1, 3, 5, 7, 9))  # 列表的元素来自一个元组
[1, 3, 5, 7, 9]
>>> list('hello')          # 列表的元素来自一个字符串
['h', 'e', 'l', 'l', 'o']

在上面这个例子中,我展示了怎样使用不同的技巧创建列表。我希望读者仔细看一下注释了“Python很神奇”的这一行,现在我并不指望读者完全看懂(除非读者并不是初学者)。这个方法称为列表解析,这是Python所提供的一个非常强大的功能特性。我们将在第5章节省时间和内存中详细讨论这个特性。现在,我只是希望读者对这个功能抱有期待。

创建列表自然很好,但真正的乐趣来自列表的使用,因此让我们观察列表向我们提供的主要方法:

>>> a = [1, 2, 1, 3]
>>> a.append(13)     # 我们可以在列表的末尾追加任何对象
>>> a
[1, 2, 1, 3, 13]
>>> a.count(1)       # 统计列表中有多少个1
2
>>> a.extend([5, 7]) # 用另一个列表(或序列)扩展这个列表
>>> a
[1, 2, 1, 3, 13, 5, 7]
>>> a.index(13)      # 找到13在列表中的位置(基于0的索引)
4
>>> a.insert(0, 17)  # 在位置0处插入17
>>> a
[17, 1, 2, 1, 3, 13, 5, 7]
>>> a.pop()  # 弹出(移除并返回)最后一个元素
7
>>> a.pop(3) # 弹出位置3处的元素
1
>>> a
[17, 1, 2, 3, 13, 5]
>>> a.remove(17) # 从列表中移除17
>>> a
[1, 2, 3, 13, 5]
>>> a.reverse()  # 反转列表中的元素顺序
>>> a
[5, 13, 3, 2, 1]
>>> a.sort()     # 对列表中的元素进行排序
>>> a
[1, 2, 3, 5, 13]
>>> a.clear()    # 从列表中移除所有的元素
>>> a
[]

上面的代码显示了可以对列表使用的主要方法。我想以extend为例来说明它们的功能有多么强大。我们可以使用任何序列类型对列表进行扩展:

>>> a = list('hello')   # 根据一个字符串创建一个列表
>>> a
['h', 'e', 'l', 'l', 'o']
>>> a.append(100)       # 追加100这个值,与其他元素的类型不同
>>> a
['h', 'e', 'l', 'l', 'o', 100]
>>> a.extend((1, 2, 3)) # 用一个元组扩展这个列表
>>> a
['h', 'e', 'l', 'l', 'o', 100, 1, 2, 3]
>>> a.extend('...')     # 用一个字符串扩展这个列表
>>> a
['h', 'e', 'l', 'l', 'o', 100, 1, 2, 3, '.', '.', '.']

现在,让我们观察列表最常见的操作有哪些:

>>> a = [1, 3, 5, 7]
>>> min(a)  # 列表中的最小值
1
>>> max(a)  # 列表中的最大值
7
>>> sum(a)  # 列表中所有元素之和
16
>>> len(a)  # 列表中元素的数量
4
>>> b = [6, 7, 8]
>>> a + b  # 列表的+操作符表示连接列表
[1, 3, 5, 7, 6, 7, 8]
>>> a * 2  # *具有一种特殊的含义
[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中的每个元素都是一个元组(精确起见,每个元素都是二元组)。然后,当我们调用sorted(some_list)时,就得到了some_list的排序版本。在这个例子中,对二元组列表进行排序的原则是:首先根据元组中的第1个元素进行排序,如果第1个元素相同,则根据第2个元素进行排序。

我们可以从sorted(a)的结果中看到这个行为,它产生的结果是[(1, 2), (1, 3), (2,−1), (4,9), (5,3)]。Python还允许我们指定sorted函数根据元组的第几个元素进行排序。注意,当我们指定sorted函数根据每个元组的第1个元素进行排序时(通过key = itemgetter(0)指定),其结果是不同的:[(1, 3), (1, 2), (2,−1), (4,9), (5,3)]。排序只针对每个元组的第1个元素(位置0处的元素)。如果我们想要重复简单的sorted(a)调用的默认行为,需要指定key=itemgetter(0, 1),告诉Python首先根据元组位置0处的元素进行排序,然后根据位置1处的元素进行排序。对结果进行比较,我们将会发现它们是符合预期的。

为了完整,我添加了一个只根据位置1处的元素进行排序的例子,然后是一个其余相同但采用反序的例子。如果读者对Java中的排序有所了解,相信此刻会留下深刻的印象。

Python的排序算法的功能极为强大,是由Tim Peters(我们已经看到过这个名字,还记得是在什么时候吗?)所编写的。它被恰如其分地命名为Timsort,是归并排序插入排序的一种混合方式,比主流编程语言所使用的大多数其他算法具有更好的时间性能。Timsort算法是一种稳定的排序算法,意味着当多条记录具有相同的键时,它们的原先顺序会被保留。我们已经在sorted(a, key=itemgetter(0))的结果中看到了这一点,它所产生的结果是[(1, 3), (1, 2), (2,−1), (4,9), (5,3)],这两个元组的原先顺序得到了保留,因为它们位置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, k1)+[k1, k2)+[k2, k3)+...+[kN−1, b]

中间点(ki)被排除在一端之外,但被包括在另一端之内,这样在代码中处理范围时可以很方便地进行连接和分隔。

让我们观察一个使用bytearray类型的简单例子:

>>> bytearray()     # 空的bytearray对象
bytearray(b' ')
>>> bytearray(10)   # 给定长度的实例,用0填充
bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
>>> bytearray(range(5))   # bytearray的元素为可迭代的整数
bytearray(b'\x00\x01\x02\x03\x04')
>>> name = bytearray(b'Lina')  #A  bytearray的元素来自一个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对象。它们可用于许多场合。例如,通过一个套接字接收数据时,它们可以消除在执行poll命令时连接数据的需要,因此极为便利。在#A这一行,我创建了一个名为name的bytearray对象,它的元素来自bytes字面值b'Lina',说明了bytearray对象同时提供了序列类型和字符串类型的方法,具有非常好的便利性。如果细加思量,我们可以把它们看成是可变字符串。

Python还提供了两种集合类型:set和frozenset。set类型是可变的,而frozenset类型是不可变的。它们都是不可变对象的一个无序集合。散列性是它们的一个特性,它允许把一个对象作为集合的成员和字典的键,稍后我们将对此进行讨论。

根据官方文档的说法:如果一个对象具有一个在它的生命期内绝不会改变的散列值,并且可以与其他对象进行比较,那么它就是可散列的。具备散列性的对象可以作为字典的键和集合的成员使用,因为这两种数据结构在内部都使用了散列值。Python的所有不可变内置对象都是可散列的,而可变容器则是不可散列的。

比较结果相同的对象必然具有相同的散列值。在集合中,成员测试是极为常用的,因此我们在下面的例子中引入了in操作符:

>>> small_primes = set()    # 空的set对象
>>> small_primes.add(2)     # 一次添加一个元素
>>> small_primes.add(3)
>>> small_primes.add(5)
>>> small_primes
{2, 3, 5}
>>> small_primes.add(1)     # 观察我的做法,1并不是质数
>>> small_primes
{1, 2, 3, 5}
>>> small_primes.remove(1)  #  因此将它移除
>>> 3 in small_primes       #  成员测试
True
>>> 4 in small_primes
False
>>> 4 not in small_primes   #  非成员测试
True
>>> small_primes.add(3)     #  再次尝试添加3
>>> small_primes
{2, 3, 5}       # 没有变化,不允许重复的值
>>> bigger_primes = set([5, 7, 11, 13])  #  更快速地创建
>>> small_primes | bigger_primes  # 合集操作符 |
{2, 3, 5, 7, 11, 13}
>>> small_primes & bigger_primes  # 交集操作符 &
{5}
>>> small_primes - bigger_primes  # 差集操作符-
{2, 3}

在上面的代码中,我们可以看到创建set对象的两种不同方法。一种方法是创建一个空set对象然后一次向它添加一个元素。另一种创建set对象的方法是把一个数值列表作为参数传递给set的构造函数,后者会为我们完成所有的工作。当然,我们可以根据一个列表或一个元组(或任何可迭代的类型)创建一个set对象,然后根据自己的需要从集合中添加或删除成员。

我们将在下一章讨论可迭代对象和迭代。现在,我们只需要知道可迭代对象就是可以从一个方向进行迭代的对象。

创建set对象的另一种方法是简单地使用花括号,如下所示:

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

注意,这里我添加了一些重复的值,而最终结果并不会出现重复的值。下面我们观察set类型的不可变版本frozenset类型的一个例子:

>>> small_primes = frozenset([2, 3, 5, 7])
>>> bigger_primes = frozenset([5, 7, 11])
>>> small_primes.add(11)    # 不能向frozenset对象添加元素
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'frozenset' object has no attribute 'add'
>>> small_primes.remove(2)  # 也不能移除元素
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'frozenset' object has no attribute 'remove'
>>> small_primes & bigger_primes  # 允许交集、并集等操作
frozenset({5, 7})

我们可以看到,frozenset对象与可变的set对象相比是相当受限制的。但是它们在成员测试、并集操作、交集操作和差集操作方面还是被证明是极其有效的,并且由于性能方面的原因,frozenset对象存在着一定的用武之地。

在Python的所有内置数据类型中,字典是其中最有趣的一种。它是唯一的一种标准映射类型,是每个Python对象的脊梁。

字典把键映射到值。键必须是可散列对象,而值可以是任意类型。字典是可变对象。我们可以采用几种不同的方式创建字典对象。因此,在下面这个简单的例子中,我们用 5种不同的方式创建了一个等于{'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  # 它们都相同吗?
True  # 它们确实相同

有没有注意到双等于符号?赋值是用单等于符号完成的,而为了检查一个对象是否与另一个对象相同(或者像这个例子一样,检查5个对象是否相同),我们使用双等于符号。还有另一种方法可以用于对象之间的比较,它涉及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)))  # 等效的操作,但这个更具Python风格
[('h', 1), ('e', 2), ('l', 3), ('l', 4), ('o', 5)]

在上面这个例子中,我用两种不同的方法创建了相同的列表,一种方法更为明确,另一种方法更偏向Python风格。暂时忽略我围绕zip调用包装list构造函数的做法(原因是zip返回的是一个迭代器而不是列表,因此如果我想观察其结果,必须耗尽这个迭代器,在此例中它的结果是一个列表),把注意力集中在它的结果上。观察zip函数是怎样把它的两个参数进行配对的:首先是第1个元素,然后是第2个元素,再接着是第3个元素,依次类推。观察裤子或钱包上的拉链,可以看到实际拉链的行为是与之相同的。但是,现在我们回到字典对象,观察它们提供了哪些出色的方法允许我们根据需要对它们进行操作。

让我们从基本的操作开始讨论:

>>> d = {}
>>> d['a'] = 1  # 设置一些(键,值)对
>>> d['b'] = 2
>>> len(d)  # 统计有多少对
2
>>> d['a']  # 找到a的值是什么
1
>>> d   # d现在看上去是什么样子?
{'a': 1, 'b': 2}
>>> del d['a']  # 移除a
>>> d
{'b': 2}
>>> d['c'] = 3  # 添加c
>>> 'c' in d    # 根据键测试是否为成员
True
>>> 3 in d      # 不能根据值来测试
False
>>> 'e' in d
False
>>> d.clear()   # 清除字典中的所有元素
>>> d
{}

注意,不管我们所操作的类型是什么,对字典中的键进行访问总是通过方括号来完成的。还记得字符串、列表和元组吗?我们也是通过方括号来访问某个位置的元素,这是Python一致性的另一个例子。

现在,让我们观察3个称为字典视图的特殊对象:keys、values和items。这些对象提供了字典中的元素的动态视图,并在字典发生变化时随之变化。keys()返回字典中所有的键,values()返回字典中所有的值,而items()返回字典中所有的(键,值)对。

根据Python文档:“键和值是以一种非随机的任意顺序迭代的,其顺序因不同的Python实现而异,并且依赖于字典的插入和删除历史。如果keys、values和items视图在迭代期间没有对字典进行修改,那么元素的顺序将是直接对应的。”

言归正传,我们把上面的理论落实到代码中:

>>> 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的内部有2个l字符,zip函数分别将它们与2和3这两个值配对。注意在这个字典中,第2个l键(与3配对的那个)覆盖了第1个l键(与2配对的那个)。另外值得注意的一点是,不管我们怎么查看字典,它的原先顺序都会得到保留。但是在Python 3.6版本之前,还没有办法保证这一点。

在Python 3.6中,dict类型进行了重新实现,使用了一种更为紧凑的表示形式。这就导致字典所使用的内存数量与Python 3.5相比下降了20%到25%。而且,在Python 3.6中,作为一种副作用,字典是天然排序的。这个特性受到了Python社区的欢迎,因此在Python 3.7中,它成为语言的一个合法特性,不再被认为是副作用。如果一个dict对象能够记住键的插入顺序,那么它就是已排序的。

当我们讨论对集合进行迭代时,将会发现这些观图都是基本工具。现在,让我们观察Python的字典所提供的一些其他方法。字典所提供的方法非常多,并且它们都非常实用:

>>> d
{'e': 1, 'h': 0, 'o': 4, 'l': 3}
>>> d.popitem()  # 移除一个随机的元素(在算法中很实用)
('o', 4)
>>> d
{'h': 0, 'e': 1, 'l': 3}
>>> d.pop('l')   # 移除键l的那个元素
3
>>> d.pop('not-a-key')  # 移除一个字典中并不存在的元素:KeyError
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'not-a-key'
>>> d.pop('not-a-key', 'default-value')  # 测试是否具有默认值
'default-value'     # 获得默认值
>>> d.update({'another': 'value'})       # 可以按照这种方式更新dict
>>> d.update(a=13)  # 或者按照这种方式(就像函数调用)
>>> d
{'h': 0, 'e': 1, 'another': 'value', 'a': 13}
>>> d.get('a')      # 与d['a']相同,但如果不存在这个键,就不会出现KeyError
13
>>> d.get('a', 177) # 如果不存在该键时所使用的默认值
13
>>> d.get('b', 177) # 就像这种情况一样
177
>>> d.get('b')      # 键不存在,因此返回None

这些方法都很简单,很容易理解,但是值得讨论一下返回值None。Python中的每个函数默认返回None,除非明确使用return语句返回其他对象。我们将在讨论函数时看到这种做法。None常用于表示不存在值,常常作为函数声明中参数的默认值使用。有些经验不足的程序员有时候会编写返回False或None的代码。False和None在布尔值语境中的结果都是False,因此它们之间看上去并没有太大的区别。但实际上,我认为两者之间存在一个重要的区别:False表示具有信息,该信息的内容是False;None表示没有信息。没有信息与信息内容为False存在巨大的差别。用外行的话说,如果问你的机师“我的车修好了吗?”,回答“不,还没有”(False)和“不清楚”(None)是存在巨大差别的。

我想讨论的最后一个关于字典的方法是setdefault方法。它的行为与get方法相似,但是如果不存在这个键,它就会为这个键设置一个特定的默认值。让我们观察一个例子:

>>> d = {}
>>> d.setdefault('a', 1)  # a不存在,得到默认值1
1
>>> d
{'a': 1}  # 另外,现在键值对 ('a', 1)已经被添加到字典中
>>> d.setdefault('a', 5)  # 让我们试图覆盖这个值
1
>>> d
{'a': 1}  # 没有被覆盖,如预期的一样

至此,我们已经到了本节内容的尾声。读者可以预测一下在下面的代码执行之后d会是什么样子,从而检测一下自己对字典的理解:

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

如果无法立即给出答案,也不必担心。我只是想鼓励读者对字典进行测试。

现在,我们已经完成了内置数据类型的讨论。在对本章所讨论的内容提出一些注意事项之前,我想简单地讨论一下collections模块。

如果觉得Python的通用容器(元组、列表、集合和字典)还不够用,我们可以在collections模块中找到专业的容器数据类型,如表2-1所示。

表2-1

数据类型

描述

namedtuple()

工厂函数,用命名字段创建元组子类

deque

类似列表的容器,可以在任一端快速添加和弹出元素

ChainMap

类似字典的类,用于创建多重映射的一个简单视图

Counter

字典的一个子类,用于对可散列对象进行计数

OrderedDict

字典的一个子类,能够记住元素的添加顺序

defaultdict

字典的一个子类,调用一个工厂函数以支持缺失的值

UserDict

字典对象的包装器,用于方便地创建字典子类

UserList

列表对象的包装器,用于方便地创建列表子类

UserString

字符串对象的包装器,用于方便地创建字符串子类

本书没有太多的篇幅讨论以上所有专业容器,但读者可以在官方文档中找到大量的例子。因此我在这里只提供一些简单的例子,对namedtuple、defaultdict和ChainMap略做展示。

namedtuple是一种类似元组的对象,除了可以按照索引和迭代方式使用它之外,我们还可以通过属性查找的方式来访问它的字段(它实际上是元组的一个子类)。这是功能完整的对象和元组之间所形成的某种类型的约定。我们有时候并不需要一个自定义对象的完整功能,但同时又希望自己的代码可以避免奇怪的索引访问,从而实现更佳的可读性。此时,像这样的对象就非常实用。这种对象的另一个适用场合是元组中的元素可能会在重构之后改变它们的位置,从而迫使程序员对相关的逻辑都进行重构,而这是相当麻烦的。和往常一样,一个例子胜过千言万语。假设我们正在处理与一位病人的左眼视力和右眼视力有关的数据。我们在一个常规的元组中为左眼视力保存一个值(位置0),并为右眼视力保存一个值(位置1)。下面显示了可能的做法:

>>> vision = (9.5, 8.8)
>>> vision
(9.5, 8.8)
>>> vision[0]  # 左眼视力(隐式的位置引用)
9.5
>>> vision[1]  # 右眼视力(隐式的位置引用)
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   # 与vision[0]相同,但明确指定
9.5
>>> vision.right  # 与vision[1]相同,但明确指定
8.8

如果在代码中,我们使用vision.left和vision.right表示左眼视力和右眼视力,现在为了修正设计问题,我们只需要修改对象工厂以及创建实例的方式,剩余的代码不需要修改:

>>> Vision = namedtuple('Vision', ['left', 'combined', 'right'])
>>> vision = Vision(9.5, 9.2, 8.8)
>>> vision.left      # 仍然正确
9.5
>>> vision.right     # 仍然正确(尽管现在是vision[2])
8.8
>>> vision.combined  # 新的vision[1]
9.2

我们可以看到,通过名称来引用值要比通过位置来引用值方便得多。不管怎样,一位智者曾经写过,“明确指定胜过隐含表示(还记得《禅意Python》吗?)”。当然,这个例子可能有点极端,代码设计者不太可能有机会做这样的事情。但是在专业的环境中,我们常常可以看到与此类似的问题,对这样的代码进行重构是一件痛苦的事情。

defaultdict数据类型是我最爱的数据类型之一。它允许我们在第一次访问字典的一个键时简单地将它插入字典中,从而避免了检查这个键是否在字典中的麻烦。与这个插入的键相关联的值是在创建时以默认值的形式传递的。在有些情况下,这个工具极为实用,可以有效地缩短我们的代码。让我们观察一个简单的例子。假设我们正在更新年龄的值,如果年龄存在,则将它加上1年;如果年龄不存在,我们就假设它原先是0,并把它更新为1:

>>> d = {}
>>> d['age'] = d.get('age', 0) + 1  # age不存在,结果是0 + 1
>>> d
{'age': 1}
>>> d = {'age': 39}
>>> d['age'] = d.get('age', 0) + 1  # age存在,结果是40
>>> d
{'age': 40}

现在,让我们观察怎么用defaultdict数据类型完成上面的操作。第2行实际上是4行长度的if子句的精简版本,是在字典不存在get方法时必须编写的代码(我们将在第3章迭代和决策中详细讨论if子句):

>>> from collections import defaultdict
>>> dd = defaultdict(int)  # int是默认类型(值为0)
>>> dd['age'] += 1         # dd['age'] = dd['age'] + 1的精简形式
>>> dd
defaultdict(<class 'int'>, {'age': 1})  # 1,和预期的一样

注意,我们只需要指示defaultdict工厂如果这个键不存在就使用一个int值(结果为0,这是int类型的默认值)。

另外注意,尽管在这个例子中,代码中的行数并没有变化,但代码显然更容易理解了,这是非常重要的。我们还可以使用一种不同的技巧实例化一个defaultdict类型,它涉及创建一个工厂对象。如果想更深入地探索这个类型,可以参阅官方文档。

ChainMap是一种极为优秀的数据类型,它是在Python 3.3中所引入的。它的行为与常规的字典相似。但是,根据Python文档的说法:“它用于快速链接一个映射成员,使它们可以按照一个独立的单元进行处理。”这种方法比创建一个字典并在它上面运行多个更新调用要快速得多。ChainMap可用于模拟嵌套的作用域,在模板方面非常实用。底层的映射存储在一个列表中。这个列表是公共的,可以使用map属性进行访问或更新。它的查找操作是对底层的映射进行连续的搜索,直到找到一个键。与此形成对照的是,它的写入、更新和删除操作只对第一对映射进行。

它的一种极为常见的用法是提供默认值,下面我们观察一个例子:

>>> from collections import ChainMap
>>> default_connection = {'host': 'localhost', 'port': 4567}
>>> connection = {'port': 5678}
>>> conn = ChainMap(connection, default_connection)  # 映射的创建
>>> conn['port']  # port在第一个字典中找到
5678
>>> conn['host']  # host是从第二个字典中提取的
'localhost'
>>> conn.maps     # 我们可以看到映射对象
[{'port': 5678}, {'host': 'localhost', 'port': 4567}]
>>> conn['host'] = 'packtpub.com'  # 让我们添加host
>>> conn.maps
[{'port': 5678, 'host': 'packtpub.com'},
{'host': 'localhost', 'port': 4567}]
>>> del conn['port']  # 让我们删除port
>>> conn.maps
[{'host': 'packtpub.com'}, {'host': 'localhost', 'port': 4567}]
>>> conn['port']  # 现在port是从第二个字典提取的
4567
>>> dict(conn)    # 很容易归并和转换为常规的字典
{'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)
>>> # 或使用一个字典
>>> 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文档,并对我们在本章所看到的每一种数据类型进行试验。相信我,这种做法是非常值得的。我们将要编写的所有代码都与处理数据有关,因此要确保自己对数据结构的理解犹如岩石般坚固。

在进入第3章迭代和决策之前,我想和读者分享一些不同方面的注意事项。我觉得它们是非常重要的,不应该被忽略。

当我们在本章开头讨论对象时,我们看到了当我们把一个名称分配给一个对象时,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会对短字符串和小数值进行缓存,避免它们的许多份副本簇集在系统内存中。Python会在后台适当地处理此事,因此我们不必为此担心。但是,如果我们的代码需要对ID进行操作,就要记住这个行为。

正如我们所看到的那样,Python向我们提供了一些内置的数据类型。如果我们的经验不够丰富,可能并不容易选择出最适合的数据类型,尤其是在和集合有关的时候。例如,假设我们有许多字典用于存储,每个字典表示一位顾客。在每个顾客的字典中,存在一个ID(即“code”),表示独一无二的标识码。我们应该在什么类型的集合中放置它们呢?说实话,如果对这些顾客的信息不够了解,那么将很难给出正确的答案。我们需要进行哪些类型的访问?我们必须对每位顾客进行哪些类型的操作?操作的次数是否频繁?这个集合是否会随着时间的变化而发生变化?我们是否可以按照某种方法修改顾客字典?我们对这个集合所执行的最频繁的操作是什么?

如果我们可以回答上述这些问题,就会知道怎样进行选择。如果集合不会收缩或增长(换言之,它在创建之后不需要删除和添加任何顾客对象),也不会打乱顺序,那么元组就是一个很好的选择。否则,列表可能更为合适。每个顾客字典都具有一个独一无二的标识符,因此即使选择一个字典作为顾客对象的容器也是可行的。下面我把这些选项聚集在一起:

# 示例顾客对象
customer1 = {'id': 'abc123', 'full_name': 'Master Yoda'}
customer2 = {'id': 'def456', 'full_name': 'Obi-Wan Kenobi'}
customer3 = {'id': 'ghi789', 'full_name': 'Anakin Skywalker'}
# 在一个元组中收集它们
customers = (customer1, customer2, customer3)
# 或者在一个列表中收集它们
customers = [customer1, customer2, customer3]
# 或者可以在一个字典中,不管怎么说,它们具有唯一标识符
customers = {
    'abc123': customer1,
    'def456': customer2,
    'ghi789': customer3,
}

已经有一些顾客在里面了,是不是?我很可能不会选择元组,除非我特别强调这个集合不会被修改。我认为列表通常会是更好的选择,因为它有更大的灵活性。

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

那么性能呢?例如,在列表中,像插入和成员测试这样的操作所需要的时间复杂度是O(n),而字典则是O(1)。但字典并不是总是能够适用的。如果无法保证集合中的每个元素都能用其中一个属性唯一地进行标识,并且这个属性是可散列的(使它可以作为字典中的键),那么就不适合使用字典。

如果读者不明白 O(n)和 O(1)的含义,可以通过搜索引擎搜索大 O 表示法。现在,我们简单地对它进行描述,内容如下:在一个数据结构上执行一个操作Op的时间复杂度是O(f(n)),它的意思是Op需要的时间上限,其中c是某个正数常量,n是输入的长度,f是某个函数。因此,可以把O(...)看成是一个操作的运行时间的上限(当然,它也可用于对其他可测量的数据进行衡量)。

理解是否选择了正确的数据结构的另一种方法是观察我们为了操作这种数据结构所编写的代码。如果所有的代码都很容易阅读并且非常自然,我们就很可能做出了正确的选择。如果觉得自己的代码变得不必要地复杂,就很有必要重新思考自己的选择。但是,如果没有实际的例子,很难提出实用的建议。因此,当我们为自己的数据选择了一种数据结构时,要设法使之容易使用和操作,把优先级放在当前的上下文环境中最重要的地方。

在本章开头的地方,我们了解了如何对字符串进行截取。一般而言,截取作用于序列,即元组、列表和字符串等。对于列表,截取还可以用于赋值。我几乎没有在专业的代码中看到这种做法,但这种做法至少在理论上是成立的。我们能不能对字典或集合进行截取?我想我们应该毫不犹豫地给出否定的答案。下面让我们讨论索引。

Python有一个与索引有关的特征是我之前没有提到的,我将通过一个例子来说明。我们应该怎样处理集合的最后一个元素?我们先来观察下面的代码:

>>> a = list(range(10))  # 列表a有10个元素,最后一个是9
>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> len(a)  # 它的长度是10
10
>>> a[len(a) - 1]  # 最后一个元素的位置是len(a) - 1
9
>>> a[-1]   # 但我们并不需要len(a),Python会报错!
9
>>> a[-2]   # 相当于len(a) - 2
8
>>> a[-3]   # 相当于len(a) - 3
7

如果列表a有10个元素,由于Python的索引是从0开始的,因此第1个元素的位置是0,最后一个元素的位置是9。在上面这个例子中,列表a中的元素被方便地放在与它们的值相等的位置上:0位于位置0处,1位于位置1处,接下来以此类推。

因此,为了提取最后一个元素,我们需要知道整个列表(或元组、字符串等)的长度,然后把它减去1。因此,最后一个元素的位置是len(a) – 1。这是一种相当常见的操作,因此Python向我们提供了一种使用负索引提取元素的方法。当我们对数据进行操作时,这被证明是一种极为有效的方法。图2-2清楚地描述了如何在字符串HelloThere上进行索引操作。

图2-2

我们可以尝试使用大于9或小于−10的索引值,它将如预期的那样产生一个IndexError。

读者可能已经注意到了,为了使例子尽可能地保持简单,我使用简单字母(如a、b、c、d等)作为许多对象的名称。当我们在控制台中进行调试或者进行a + b == 7这样的表达时,这种做法是非常合适的。但是,它并不适用于专业的代码(因此也不适合任何类型的编程)。我希望读者不介意我有时采用的这种做法,因为我的目的是用一种更紧凑的方式展示代码。

但是,在现实的环境中,当我们为自己的数据选择名称时,应该进行精心的选择,使这些名称能够反映它们所表示的数据。因此,如果有一个包含顾客对象的集合,customers就是一个完美的名称。customers_list、customers_tuple或customers_collection是不是适合使用呢?这个问题值得三思。

它们是不是很好地把集合的名称与数据类型进行了关联?我不觉得,至少在大多数情况下并不如此。如果读者觉得自己有非常充分的理由采用这类名称,当然也可以这样做。否则,我不推荐这种方法。我的理由是:一旦在代码中的不同地方使用customers_tuple,后来又意识到自己实际想使用的是列表而不是元组,就得对代码进行重构(这会浪费时间)。数据的名称应该是名词,函数的名称应该是动词。名称应该尽可能地具有表述性。Python在名称设置方面实际上是一个很好的例子。大多数时候,我们只要知道一个函数能够执行什么操作,就可以猜出它的名称是什么。

《Meaningful Names of Clean Code》(作者Robert C. Martin,由Prentice Hall出版)的第2章就专门讲述了名称。这是一本非常出色的书,在许多不同的方面帮助我完善了自己的编程风格。如果读者想把自己的编程水平向上提升一级,这本书可以说是必读的。

在本章中,我们探索了Python的内置数据类型。我们了解了大量的内置数据类型,并了解了只是通过不同的组合用法,就可以实现非常广泛的用途。

我们了解了数值类型、序列、集合、映射(以及Enum这位特殊的嘉宾)。我们了解了在Python中一切都是对象。我们明白了可变对象和不可变对象之间的区别,还了解了截取和索引(还非常自豪地学习了负索引)。

我们讨论了一些简单的例子。但是关于这个主题,还有很多值得学习的东西,因此我们应该认真阅读官方文档,对这个主题继续进行探索。

最重要的是,我鼓励读者自己尝试所有的练习,亲手输入这些代码,建立坚固的记忆并不断地进行试验。了解在除零时、把不同的数值类型组合在一个表达式中时、对字符串进行管理时会发生什么情况。尽情地对所有的数据类型进行试验、练习和分解,发现它们的所有方法,享受其中的乐趣,并最终熟练地掌握它们。

如果我们的基础不够扎实,我们所编写的代码的质量也就可想而知。数据是一切的基础。数据能够反映我们对它所进行的操作。

当我们不断深入本书的时候,很可能会发现我(或读者)的代码中的一些差异或者微小的输入错误。我们会得到错误信息,有时候程序就会无法工作。这是非常好的!当我们编写代码时,总是在不断地出错,我们总是在不断地调试和纠错。因此,我们可以把错误看成是一个非常实用的练习,能够让我们更深地理解自己所使用的语言,而不要把它们看成是失败或问题。在我们完成代码之前,会不断地出现错误,这是必然的。因此,我们要学会心平气和地看待错误。

第 3 章是关于迭代和决策的。我们将了解如何实际使用集合,并根据我们得到的数据做出决策。既然我们已经开始建立自己的知识体系,我们的节奏也会加快一些,因此在学习第3章之前要确保已经理解了本章的内容。再次强调,要学会寻找乐趣、勇于探索并分解事物,这是非常好的学习方式。


相关图书

深度学习的数学——使用Python语言
深度学习的数学——使用Python语言
动手学自然语言处理
动手学自然语言处理
Web应用安全
Web应用安全
Python高性能编程(第2版)
Python高性能编程(第2版)
图像处理与计算机视觉实践——基于OpenCV和Python
图像处理与计算机视觉实践——基于OpenCV和Python
Python数据科学实战
Python数据科学实战

相关文章

相关课程