MIT-6-0001-Python-计算和编程入门第三版-一-

MIT 6.0001:Python 计算和编程入门第三版(一)

原文:zh.z-lib.gs/md5/b81f9400901fb07c6e4e456605c4cd1f

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书基于自 2006 年起在 MIT 开设的课程,并自 2012 年起通过 edX 和 MITx 作为“大规模在线开放课程”(MOOCs)提供。第一版基于一个学期的课程。然而,随着时间的推移,我无法抗拒添加超出一个学期内容的材料。目前版本适合两学期或三学季的计算机科学导论课程。

本书面向 1) 希望理解计算方法解决问题的初学者,几乎没有或没有编程经验,2) 想学习如何利用计算来建模或探索数据的更有经验的程序员。

我们强调广度而非深度。目标是为读者提供对多个主题的简要介绍,以便他们在思考如何利用计算来实现目标时,能了解可能性。也就是说,这不是一本“计算欣赏”书籍。它具有挑战性和严谨性。希望真正学习这些材料的读者需要花费大量时间和精力,让计算机顺应他们的意志。

本书的主要目标是帮助读者熟练地有效使用计算技术。他们应该学会利用计算思维来框定问题,建立计算模型,并指导从数据中提取信息的过程。他们从本书中获取的主要知识是计算问题解决的艺术。

我们选择不在章节末尾附上问题,而是在章节中适时插入“手指练习”。有些相当简短,旨在让读者确认他们理解了刚读过的材料。有些则更具挑战性,适合考试问题。而其他一些则足够有挑战性,适合作为家庭作业。

第 1-13 章包含通常在计算机科学导论课程中包含的材料,但呈现方式并不传统。我们将四个领域的材料交织在一起:

  • 编程基础,

  • Python 3 编程语言,

  • 计算问题解决技术,以及

  • 计算复杂性。

我们涵盖了 Python 的大多数特性,但重点在于人们可以用编程语言做什么,而不是语言本身。例如,到第三章结束时,书中只涵盖了 Python 的一小部分,但已经介绍了穷举枚举、猜测与检查算法、二分查找和高效*似算法的概念。我们在整本书中引入了 Python 的特性。同样,我们在整本书中引入了编程方法的各个方面。我们的想法是帮助读者在使用计算解决有趣问题的过程中学习 Python 并成为优秀的程序员。这些章节已被修订,以更温和的方式进行,并比本书第二版的相应章节包含更多的练习。

第十三章介绍了在 Python 中绘图。这一主题在入门课程中通常不涉及,但我们认为,学习如何制作信息可视化是一个重要技能,应纳入入门计算机科学课程。这一章包括第二版未涵盖的内容。

第 14-26 章讨论了如何使用计算帮助理解现实世界。它们涵盖了我们认为应成为计算机科学课程的常规第二门课程的材料。这些章节不假设有超出高中代数的数学知识,但假设读者对严谨的思维感到舒适,并且不被数学概念所吓倒。

本书的这一部分专门讨论在大多数入门教材中找不到的主题:数据可视化与分析、随机程序、仿真模型、概率与统计思维,以及机器学习。我们认为,这些内容对于大多数学生来说,远比通常在第二门计算机科学课程中涵盖的材料更为相关。除了第二十三章,本部分的内容侧重于概念问题,而非编程。第二十三章是对 Pandas 的介绍,这是早期版本未涉及的主题。

本书有三个贯穿的主题:系统问题解决、抽象的力量以及计算作为思考世界的一种方式。完成本书后,你应该能够:

  • 学习了一种语言,Python,用于表达计算,

  • 学会了系统化的方法来组织、编写和调试中等规模的程序,

  • 对计算复杂性有了非正式的理解,

  • 对于如何从模糊的问题陈述转变为计算方法的形成,获得了一些见解,

  • 学到一套有用的算法和问题简化技巧,

  • 学会了如何使用随机性和模拟来阐明那些不易用封闭形式解决的问题,和

  • 学会使用计算工具(包括简单的统计、可视化和机器学习工具)来建模和理解数据。

编程是一项本质上困难的活动。正如“没有通往几何的捷径”, ¹ 编程也没有捷径。如果你真的想学习这些材料,仅仅阅读书籍是不够的。至少你应该完成一些涉及编码的指法练习。如果你愿意尝试更有挑战性的任务,可以尝试一些可通过

`[`ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-0001-introduction-to-computer-science-and-programming-in-python-fall-2016/`](https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-0001-introduction-to-computer-science-and-programming-in-python-fall-2016/)`

`[`ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-0002-introduction-to-computational-thinking-and-data-science-fall-2016/`](https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-0002-introduction-to-computational-thinking-and-data-science-fall-2016/).`

致谢

本书的第一版源于我在麻省理工学院教授本科课程时准备的一套讲义。这个课程,因此本书,受益于来自教职员工(特别是Ana BellEric GrimsonSrinivas DevadasFredo DurandRon RivestChris Terman)的建议、助教和选修该课程的学生们的反馈。David Guttag克服了他对计算机科学的厌恶,并校对了第一版的多个章节。

像所有成功的教授一样,我非常感谢我的研究生们。除了进行出色的研究(并让我为此获得一些功劳)外,Guha BalakrishnanDavis BlalockJoel BrooksGaneshapillai GartheebanJen GongKatie LewisYun LiuJose Javier Gonzalez OrtizAnima SinghDivya ShanmugamJenna WiensAmy Zhao都对本手稿的不同版本提供了有用的意见。

我特别感谢Julie Sussman(P.P.A.),她编辑了本书的前两版,以及Lisa Ruffolo,她编辑了本版。JulieLisa都是以学生的视角阅读本书的合作者,告诉我需要做什么、应该做什么以及如果我有时间和精力可以做什么。他们给我提供了太多不容忽视的“建议”。

最后,感谢我的妻子奥尔加,感谢她鼓励我完成这本书,并免除我承担各种家务,以便我能够专心写作。

第一章:开始

计算机只做两件事,而且仅仅这两件事:执行计算并记住计算结果。但它在这两方面做得极其出色。典型的桌面或背包里的计算机每秒可以执行大约 1000 亿次计算。很难想象这是多么快。想象一下把一个球放在离地面一米高的地方,然后放手。在它落地的那一刻,你的计算机可能已经执行了超过十亿条指令。至于内存,小型计算机可能有数百 GB 的存储。那有多大?如果一个字节(表示一个字符所需的位数,通常是八个)重一克(实际上并不是),100GB 将重达 10 万吨。相比之下,这大约是 16000 头非洲大象的总重量。²

在人类历史的大部分时间里,计算受到人脑计算速度和人手记录计算结果的能力的限制。这意味着只有最小的问题可以通过计算方法解决。即使在现代计算机的速度下,一些问题仍然超出了现代计算模型(例如,全面理解气候变化),但越来越多的问题证明可以通过计算解决。我们希望,当你完成本书时,能够自如地将计算思维应用于解决你在学习、工作乃至日常生活中遇到的许多问题。

我们所说的计算思维是什么?

所有知识都可以被视为陈述性知识命令性知识陈述性知识由事实陈述组成。例如,“x的*方根是一个数字y,使得y*y = x,”以及“可以通过火车从巴黎到罗马旅行。”这些都是事实陈述。不幸的是,它们并没有告诉我们如何找到*方根或如何从巴黎乘火车到罗马。

命令性知识是“如何”知识,或推导信息的食谱。亚历山大的希罗尼斯是第一个³记录计算*方根的方法的人。他找到*方根的步骤,可以总结为:

  1. 1. 从一个猜测g开始。

  2. 2. 如果g*g足够接*x,则停止并说g是答案。

  3. 3. 否则,通过*均gx/g来创建一个新的猜测,即(g + x/g)/2

  4. 4. 使用这个新的猜测,我们再次称之为 g,重复这个过程,直到g*g足够接*x

考虑找到25的*方根。

  1. 1. 将g设为某个任意值,例如3

  2. 2. 我们决定3*3 = 9不够接*25

  3. 3. 将g设为(3 + 25/3)/2 = 5.67。⁴

  4. 4. 我们决定5.67*5.67 = 32.15仍然没有足够接*25

  5. 5. 将g设为(5.67 + 25/5.67)/2 = 5.04

  6. 6. 我们决定5.04*5.04 = 25.4足够接*,所以我们停止并宣布5.0425的*方根的足够*似值。

注意,该方法的描述是一系列简单步骤,以及指定何时执行每个步骤的控制流。这种描述称为算法。⁵ 我们用来*似*方根的算法是猜测和检查算法的一个例子。

更正式地说,算法是一系列有限的指令,描述了一组计算,当应用于一组输入时,将按照一系列明确定义的状态序列进行,并最终产生一个输出。

算法就像食谱书中的配方:

  1. 1. 将奶油混合物加热。

  2. 2. 搅拌。

  3. 3. 将勺子浸入奶油中。

  4. 4. 取出勺子并在勺背上划过手指。

  5. 5. 如果留下清晰的路径,将奶油从热源上取下并让其冷却。

  6. 6. 否则重复。

配方包括一些测试,用于确定过程何时完成,以及关于执行顺序的指示,有时基于测试跳转到特定指令。

那么如何将食谱的概念转化为机械过程?一种方法是设计一台专门用于计算*方根的机器。尽管听起来很奇怪,但最早的计算机确实是固定程序计算机,意味着它们设计用来解决特定的数学问题,比如计算炮弹的轨迹。首台计算机之一(由阿塔纳索夫和贝里于 1941 年建造)解决了线性方程组,但其他问题则无法处理。艾伦·图灵在二战期间开发的波美机被设计用来破解德国恩尼格玛密码。一些简单的计算机仍然沿用这种方法。例如,四则运算计算器⁶就是一种固定程序计算机。它可以进行基本算术运算,但不能用作文字处理器或运行视频游戏。要更改这类机器的程序,必须更换电路。

第一台真正现代化的计算机是曼彻斯特 Mark 1.⁷ 与其前身的显著区别在于它是存储程序计算机。这种计算机存储(和操作)一系列指令,并具有能够执行该序列中任何指令的组件。这种计算机的核心是一个解释器,可以执行任何合法的指令集,因此可用于计算任何可以用这些指令描述的东西。计算的结果甚至可以是一系列新的指令,随后可以由生成它们的计算机执行。换句话说,计算机可以对自己进行编程。⁸

程序及其操作的数据都驻留在内存中。通常,一个程序计数器指向内存中的特定位置,计算从执行该点的指令开始。大多数情况下,解释器会简单地转向序列中的下一条指令,但并不总是如此。在某些情况下,它会执行一个测试,并根据该测试,执行可能跳转到指令序列中的另一个点。这被称为控制流,对于让我们编写执行复杂任务的程序至关重要。

人们有时使用流程图来描述控制流。根据惯例,我们用矩形框表示处理步骤,用菱形表示测试,用箭头指示执行的顺序。图 1-1 包含了一个展示如何准备晚餐的流程图。

c1-fig-0001.jpg

图 1-1 获取晚餐的流程图

回到食谱的比喻,给定一组固定的原料,一个好的厨师可以通过不同的组合制作出无数美味的菜肴。同样,给定一小组固定的基本特征,一个优秀的程序员可以生成无数有用的程序。这就是编程如此惊人之处。

要创建食谱或指令序列,我们需要一种编程语言来描述它们,给计算机下达指令的方式。

在 1936 年,英国数学家阿兰·图灵描述了一种假设的计算设备,后来被称为通用图灵机。该机器具有无限的内存,表现为可以写入零和一的“磁带”,以及一小部分简单的原始指令用于移动、读取和写入磁带。教会-图灵理论指出,如果一个函数是可计算的,那么可以编写一个图灵机来计算它。

教会-图灵理论中的“如果”是重要的。并非所有问题都有计算解决方案。例如,图灵证明了,写一个程序,使其能够接受任意程序作为输入,并且仅当输入程序会永远运行时输出true,这是不可能的。这被称为停机问题

教会-图灵理论直接引出了图灵完备性的概念。如果一种编程语言可以用于模拟通用图灵机,则称其为图灵完备。所有现代编程语言都是图灵完备的。因此,任何可以用一种编程语言(例如 Python)编程的东西,都可以用任何其他编程语言(例如 Java)编程。当然,在特定语言中,有些东西可能更容易编程,但所有语言在计算能力上基本上是*等的。

幸运的是,没有程序员需要使用图灵的原始指令来构建程序。相反,现代编程语言提供了更大、更方便的原语集。然而,编程作为组装一系列操作的过程这一基本思想仍然是核心。

不论你拥有哪一组原语,以及你有何种组装它们的方法,编程的最好和最糟的地方是一样的:计算机将完全按照你所指示的方式执行——没有更多,也没有更少。这是好事,因为这意味着你可以让计算机做各种有趣和有用的事情。这也是坏事,因为当它没有按照你的意图运行时,你通常只能责怪自己。

世界上有数百种编程语言。没有最好的语言。不同的语言适用于不同类型的应用。MATLAB 例如,是一种适合操作向量和矩阵的良好语言。C 是一种适合编写控制数据网络程序的良好语言。PHP 是一种适合构建网站的良好语言。而 Python 是一种出色的通用语言。

每种编程语言都有一组原始构造、语法、静态语义和语义。类比于自然语言,例如英语,原始构造是单词,语法描述哪些单词序列构成良好格式的句子,静态语义定义哪些句子有意义,而语义定义这些句子的意义。Python 中的原始构造包括文字(例如数字3.2和字符串‘abc’)以及**中缀运算符**(例如+/`)。

一种语言的语法定义了哪些字符和符号的字符串是良好格式的。例如,在英语中,字符串“Cat dog boy.”不是语法上有效的句子,因为英语语法不接受<名词> <名词> <名词>形式的句子。在 Python 中,原语序列3.2 + 3.2是语法上良好格式的,但序列3.2 3.2则不是。

静态语义定义了哪些语法上有效的字符串具有意义。例如,考虑字符串“He run quickly”和“I runs quickly。”每个字符串的形式为 ,这是一个语法上可接受的序列。然而,这两者都不是有效的英语,因为有一个相当特殊的规则:对于规则动词,当句子的主语是第一或第二人称时,动词不以“s”结尾,而当主语是第三人称单数时,则以“s”结尾。这些都是静态语义错误的例子。

一种语言的语义将含义与每个没有静态语义错误的语法正确的符号串关联。在自然语言中,一个句子的语义可能是模糊的。例如,“我无法过于赞扬这个学生”这句话可以是恭维或谴责。编程语言的设计是确保每个合法程序只有一个确切的含义。

虽然语法错误是最常见的错误类型(尤其是对于学习新编程语言的人来说),但它们是最不危险的错误。每种严肃的编程语言都会检测所有语法错误,并不允许用户执行即使只有一个语法错误的程序。此外,在大多数情况下,语言系统会清楚地指示错误的位置,使程序员能够在不费太多脑筋的情况下修复它。

识别和解决静态语义错误更为复杂。一些编程语言,例如 Java,在允许程序执行之前会进行大量的静态语义检查。其他语言,例如 C 和 Python(可悲的是),在执行程序之前相对较少进行静态语义检查。Python 在运行程序时确实会进行相当多的语义检查。

如果程序没有语法错误和静态语义错误,它就有意义,即具有语义。当然,它可能没有其创建者所意图的语义。当一个程序意味着与其创建者所认为的不同的东西时,坏事可能会发生。

如果程序有错误,并且以意想不到的方式运行,会发生什么?

  • 它可能会崩溃,即停止运行并明显指示其已崩溃。在一个设计良好的计算系统中,当程序崩溃时,它不会损坏整体系统。可悲的是,一些非常流行的计算机系统没有这种良好特性。几乎每个使用个人计算机的人都运行过需要重新启动整个系统的程序。

  • 它可能会不断运行,运行,再运行,永远不会停止。如果你对程序完成工作的时间没有大致的概念,这种情况可能很难被识别。

  • 它可能会运行到完成并产生一个可能正确或可能不正确的答案。

每一个结果都不好,但最后一个无疑是最糟糕的。当一个程序看似在做正确的事情但实际上并非如此时,坏事可能随之而来:财富可能会丧失,患者可能会收到致命剂量的放射治疗,飞机可能会坠毁。

在可能的情况下,程序应编写得如此,当它们运行不正常时,显而易见。我们将在本书中讨论如何做到这一点。

指法练习:计算机往往会字面理解你的指令。如果你没有准确告诉它们你想要做什么,它们可能会做错事情。试着为从一个目的地开车到另一个目的地写一个算法。按照你给人的方式来写,然后想象如果那个人像计算机一样愚蠢,并严格执行算法会发生什么。(想看有趣的例子,请查看视频 www.youtube.com/watch?v=FN2RM-CHkuI&t=24s。)

1.1 章节中引入的术语

  • 声明式知识

  • 命令式知识

  • 算法

  • 计算

  • 固定程序计算机

  • 存储程序计算机

  • 解释器

  • 程序计数器

  • 控制流

  • 流程图

  • 编程语言

  • 通用图灵机

  • 丘奇-图灵论题

  • 停止问题

  • 图灵完备性

  • 字面量

  • 中缀运算符

  • 语法

  • 静态语义

  • 语义

第二章:Python 简介

尽管每种编程语言都是不同的(虽然没有设计者们所认为的那样不同),但它们可以在某些维度上关联。

  • 低级与高级指的是我们是否使用机器级别的指令和数据对象进行编程(例如,将 64 位数据从这个位置移动到那个位置),或者我们是否使用语言设计者提供的更抽象的操作(例如,在屏幕上弹出菜单)进行编程。

  • 通用与特定于应用领域指的是编程语言的原始操作是广泛适用还是针对某一领域进行细化。例如,SQL 旨在从关系数据库中提取信息,但你不希望用它来构建操作系统。

  • 解释执行与编译执行指的是程序员编写的指令序列(称为源代码)是否被直接执行(通过解释器),或者是否首先被转换(通过编译器)为一系列机器级原始操作。(在计算机的早期,人们必须用接*机器代码的语言编写源代码,以便计算机硬件可以直接解释。)这两种方法各有优劣。通常,调试那些设计为解释执行的语言编写的程序更容易,因为解释器能够产生易于与源代码关联的错误信息。编译语言通常生成运行更快且占用更少空间的程序。

在本书中,我们使用Python。然而,这本书并不是关于 Python 的。它当然会帮助你学习 Python,这很好。然而,更重要的是,你将学习如何编写解决问题的程序。你可以将这种技能转移到任何编程语言上。

Python 是一种通用编程语言,你可以有效地使用它构建几乎任何不需要直接访问计算机硬件的程序。对于具有高可靠性约束的程序(由于其弱静态语义检查)或由多人或在长时间内构建和维护的程序(同样因为弱静态语义检查),Python 并不是最佳选择。

Python 确实比许多其他语言具有多个优势。它是一种相对简单、易于学习的语言。由于 Python 被设计为解释执行,它能提供对初学者程序员特别有帮助的运行时反馈。大量不断增长的可自由获取的库与 Python 接口,提供了有用的扩展功能。本书中使用了其中的多个库。

我们准备介绍一些 Python 的基本元素。这些在概念上几乎所有编程语言都是共同的,尽管在细节上有所不同。

本书不仅仅是 Python 的介绍。它使用 Python 作为工具来呈现与计算问题解决和思维相关的概念。语言的呈现是按需逐步进行的。我们不需要的 Python 特性则完全不予展示。我们对没有涵盖每个细节感到放心,因为优秀的在线资源描述了语言的每一个方面。我们建议根据需要使用这些免费的在线资源。

Python 是一种活语言。自 1990 年由 Guido van Rossum 引入以来,它经历了许多变化。在其生命的头十年,Python 是一种鲜为人知且使用不多的语言。2000 年 Python 2.0 的到来改变了这一现状。除了对语言本身的重要改进外,它还标志着语言演变路径的转变。许多团体开始开发与 Python 无缝接口的库,Python 生态系统的持续支持和开发成为了一项社区活动。

Python 3.0 于 2008 年底发布。这个版本的 Python 清理了 Python 2 设计中的许多不一致性。然而,Python 3 不向后兼容。这意味着为早期版本的 Python 编写的大多数程序和库无法使用 Python 3 的实现运行。

到现在,所有重要的公共领域 Python 库都已移植到 Python 3。今天,没有理由使用 Python 2。

2.1 安装 Python 和 Python IDE

曾几何时,程序员使用通用文本编辑器输入他们的程序。如今,大多数程序员更喜欢使用集成开发环境IDE)中的文本编辑器。

第一个 Python IDE,IDLE,作为标准 Python 安装包的一部分而出现。随着 Python 的普及,其他 IDE 应运而生。这些较新的 IDE 通常集成了一些更流行的 Python 库,并提供 IDLE 未提供的功能。Anaconda和 Canopy 是这些 IDE 中更受欢迎的选择。本书中的代码是在 Anaconda 中创建和测试的。

IDE 是应用程序,就像计算机上的任何其他应用程序一样。启动一个 IDE 的方式与启动其他应用程序相同,例如,通过双击图标。

  • 提供具有语法高亮、自动补全和智能缩进的文本编辑器,

  • 具有语法高亮的命令行,

  • 一个集成调试器,目前可以安全地忽略它。

现在是安装 Anaconda(或其他 IDE)的好时机,以便你可以运行书中的示例,更重要的是,尝试编程练习。要安装 Anaconda,请访问

[`www.anaconda.com/distribution/`](https://www.anaconda.com/distribution/)

并遵循说明。

安装完成后,启动应用程序Anaconda-Navigator。将出现一个包含 Python 工具集合的窗口。该窗口看起来类似于图 2-1。目前,我们唯一要使用的工具是**Spyder**。当你启动 Spyder(通过点击它的Launch按钮)时,将打开一个类似于图 2-2 的窗口。

c2-fig-0001.jpg

图 2-1 Anaconda 启动窗口

c2-fig-0002.jpg

图 2-2 Spyder 窗口

图 2-2 右下角的窗格是一个运行交互式 Python shellIPython 控制台。你可以在这个窗口中输入并执行 Python 命令。右上角的窗格是一个帮助窗口。通常,关闭该窗口(通过点击 x)会更方便,这样可以为 IPython 控制台提供更多空间。左侧的窗格是一个编辑窗口,你可以在其中输入可以保存和运行的程序。窗口顶部的工具栏使打开文件和打印程序等各种任务变得容易。¹¹ Spyder 的文档可以在 www.spyder-ide.org/ 找到。

2.2  Python 的基本元素

Python 程序有时称为 脚本,是一系列定义和命令的序列。Shell 中的 Python 解释器评估定义并执行命令。

我们建议你现在启动一个 Python shell(例如,通过启动Spyder),并使用它尝试本章其余部分中的示例。事实上,在本书的其余部分中也可以这样做。

命令,通常称为 语句,指示解释器执行某项操作。例如,语句 print('Yankees rule!') 指示解释器调用函数¹² print,该函数将字符串 Yankees rule! 输出到与 shell 关联的窗口。

命令的序列

print('Yankees rule!')
print('But not in Boston!')
print('Yankees rule,', 'but not in Boston!')

导致解释器产生输出

Yankees rule!
But not in Boston!
Yankees rule, but not in Boston!

请注意,在第三条语句中传递了两个值给 printprint 函数接受由逗号分隔的可变数量的参数,并按出现顺序以空格字符分隔输出。

2.2.1 对象、表达式和数值类型

对象是 Python 程序操作的核心事物。每个对象都有一个 类型,定义了程序可以对该对象执行的操作。

类型分为标量和非标量。标量对象是不可分割的。可以将它们视为语言的原子。¹³ 非标量对象,例如字符串,具有内部结构。

许多类型的对象可以通过程序文本中的 字面量 来表示。例如,文本 2 是表示数字的字面量,而文本 'abc' 是表示字符串的字面量。

Python 有四种类型的标量对象:

  • **int** 用于表示整数。类型为 int 的字面量是以我们通常表示整数的方式书写的(例如,-3510002)。

  • **float** 用于表示实数。类型为 float 的字面量总是包含小数点(例如,3.03.17-28.72)。 (也可以使用科学记数法来书写类型为 float 的字面量。例如,字面量 1.6E3 代表 1.6*10³,即它与 1600.0 相同。)你可能会想知道为什么这个类型不叫 real。在计算机内部,类型为 float 的值以 浮点数 的形式存储。这种表示法被所有现代编程语言采用,具有许多优点。然而,在某些情况下,它会导致浮点运算的行为与实数运算略有不同。我们将在第 3.3 节中讨论这个问题。

  • **bool** 用于表示布尔值 TrueFalse

  • **None** 是一种只有一个值的类型。我们将在第四部分中详细讨论 None

对象和 运算符 可以组合形成 表达式,每个表达式的值为某种类型的对象。这称为表达式的 。例如,表达式 3 + 2 表示类型为 int 的对象 5,而表达式 3.0 + 2.0 表示类型为 float 的对象 5.0

== 运算符用于测试两个表达式是否评估为相同的值,而 != 运算符用于测试两个表达式是否评估为不同的值。单个 = 意味着完全不同的东西,正如我们将在第 2.2.2 节中看到的。请提前警告——你会犯下将“=”输入成“==”的错误。注意这个错误。

在 Spyder 控制台中,类似于 In [1]: 的内容是 shell 提示符,表明解释器正在等待用户输入一些 Python 代码。提示符下方的行是在解释器评估用户输入的 Python 代码时产生的,下面是与解释器的一个交互示例:

3
Out[1]: 3

3+2
Out[2]: 5

3.0+2.0
Out[3]: 5.0

3!=2
Out[4]: True

内置的 Python 函数 type 可用于查找对象的类型:

type(3)
Out[5]: int

type(3.0)
Out[6]: float

类型为 intfloat 的对象的运算符列在 图 2-3 中。算术运算符具有通常的优先级。例如,* 的优先级高于 +,因此表达式 x+y*2 会先计算 y 乘以 2,然后再将结果加到 x 上。可以通过使用括号来改变评估顺序,例如 (x+y)*2 会先将 xy 相加,然后再将结果乘以 2

c2-fig-0003.jpg

图 2-3 类型 intfloat 的运算符

类型 bool 的基本运算符是 andornot

  • **a 和 b**True 当且仅当 ab 都为 True,否则为 False

  • **a 或 b**True 当至少一个 abTrue,否则为 False

  • **not a**aFalse时为True,而在aTrue时为False

2.2.2 变量与赋值

变量提供了一种将名称与对象关联的方法。考虑以下代码

pi = 3
radius = 11
area = pi * (radius**2)
radius = 14

代码首先绑定了名称piradius到不同的int类型对象上。¹⁴ 然后将名称area绑定到第三个int类型的对象。这在图 2-4 的左侧进行了描述。

c2-fig-0004.jpg

图 2-4 变量与对象的绑定

如果程序接着执行radius = 14,那么名称radius将重新绑定到不同的int类型对象上,如图 2-4 右侧所示。请注意,这个赋值对area绑定的值没有影响。它仍然绑定于表达式3*(11**2)所表示的对象。

在 Python 中,变量仅仅是一个名称没有更多的意义。记住这一点——这很重要。一个赋值语句将=符号左侧的名称与=符号右侧表达式所表示的对象关联起来。也请记住:一个对象可以有一个、多个或没有与之关联的名称。

也许我们不应该说,“变量仅仅是一个名称。”尽管朱丽叶这么说过,¹⁵ 名称是重要的。编程语言让我们描述计算,以便计算机可以执行它们。这并不意味着只有计算机在阅读程序。

正如你很快会发现的,编写正确工作的程序并不总是容易。经验丰富的程序员会确认,他们花费了大量时间阅读程序,以尝试理解它们为何以这样的方式运行。因此,编写易于阅读的程序是至关重要的。恰当选择变量名称在增强可读性方面起着重要作用。

考虑这两个代码片段

a = 3.14159    pi = 3.14159
b = 11.2    diameter = 11.2
c = a*(b**2)   area = pi*(diameter**2)

就 Python 而言,这些代码片段没有区别。当执行时,它们将执行相同的操作。然而,对于人类读者来说,它们却截然不同。当我们阅读左侧的片段时,没有先验的理由怀疑有什么问题。然而,快速看一下右侧的代码应该会让我们怀疑某些地方不对。要么变量应该命名为radius而不是diameter,要么在计算面积时应该将diameter除以2.0

在 Python 中,变量名可以包含大写和小写字母、数字(虽然不能以数字开头)以及特殊字符_(下划线)。Python 的变量名区分大小写,例如,Romeoromeo是不同的名称。最后,Python 中有一些保留字(有时称为关键字),这些字有内置的意义,不能用作变量名。不同版本的 Python 有略微不同的保留字列表。Python 3.8 中的保留字包括:

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

提高代码可读性的另一个好方法是添加 注释。符号 # 后面的文本不会被 Python 解释。例如,我们可能会写

side = 1 #length of sides of a unit square
radius = 1 #radius of a unit circle
#subtract area of unit circle from area of unit square
area_circle = pi*radius**2
area_square = side*side
difference = area_square – area_circle

Python 允许多重赋值。语句

`x, y = 2, 3`

x 绑定到 2,将 y 绑定到 3。赋值右侧的所有表达式在任何绑定改变之前都会被评估。这很方便,因为它允许你使用多重赋值来交换两个变量的绑定。

例如,代码

x, y = 2, 3
x, y = y, x
print('x =', x)
print('y =', y)

将打印

x = 3
y = 2

2.3 分支程序

到目前为止,我们正在查看的计算类型称为 直线程序。它们按照出现的顺序一个接一个地执行语句,并在用尽语句时停止。我们可以用直线程序描述的计算类型并不是很有趣。事实上,它们实在是太无聊了。

分支程序更有趣。最简单的分支语句是 条件。如图 2-5 所示,条件语句有三个部分:

  • 一个测试,即评估为 TrueFalse 的表达式

  • 如果测试评估为 True,则执行的代码块

  • 如果测试评估为 False,则执行的可选代码块

在条件语句执行后,执行将继续在语句后的代码处。

c2-fig-0005.jpg

图 2-5 条件语句的流程图

在 Python 中,条件语句的形式是

`if` *Boolean expression*`:              if` *Boolean expression*`:`
    *block of code         * or  *         block of code*
`else:`
    *block of code*

在描述 Python 语句的形式时,我们使用斜体来标识在程序的某一点可能出现的代码类型。例如,布尔表达式表示任何评估为 TrueFalse 的表达式可以跟在保留字 if 后,而 代码块 表示任何 Python 语句序列可以跟在 else: 后。

考虑以下程序,如果变量 x 的值为偶数,则打印“Even”,否则打印“Odd”:

if x%2 == 0:
    print('Even')
else:
    print('Odd')
print('Done with conditional')

x 除以 2 的余数为 0 时,表达式 x%2 == 0 评估为 True,否则为 False。请记住,== 用于比较,而 = 是用于赋值的。

缩进 在 Python 中具有语义意义。例如,如果上述代码中的最后一条语句被缩进,它将属于与 else 相关的代码块,而不是条件语句后的代码块。

Python 在使用缩进方面是非常特殊的。大多数其他编程语言使用括号符号来划分代码块,例如,C 语言使用花括号{ }来封闭代码块。Python 方法的一个优点是,它确保程序的视觉结构准确地代表其语义结构。由于缩进在语义上很重要,行的概念也很重要。过长的代码行可以通过在屏幕上每一行的末尾(除了最后一行)加上反斜杠(\)来拆分为多行。比如,

 x = 1111111111111111111111111111111 + 222222222222333222222222 +\
    3333333333333333333333333333333

py`Long lines can also be wrapped using Python's implied line continuation. This is done with bracketing, i.e., parentheses, square brackets, and braces. For example,  x = 1111111111111111111111111111111 + 222222222222333222222222 + 3333333333333333333333333333333 py is interpreted as two lines (and therefore produces an “unexpected indent” syntax error whereas  x = (1111111111111111111111111111111 + 222222222222333222222222 + 3333333333333333333333333333333) py is interpreted as a single line because of the parentheses. Many Python programmers prefer using implied line continuations to using a backslash. Most commonly, programmers break long lines at commas or operators. Returning to conditionals, when either the true block or the false block of a conditional contains another conditional, the conditional statements are said to be **nested**. The following code contains nested conditionals in both branches of the top-level `if` statement. 如果 x%2 == 0: 如果 x%3 == 0: print('能被 2 和 3 整除') 否则: print('能被 2 整除但不能被 3 整除') 否则如果 x%3 == 0: print('能被 3 整除但不能被 2 整除') py The `elif` in the above code stands for “else if.” It is often convenient to use a **compound Boolean expression** in the test of a conditional, for example, 如果 x < y 且 x < z: print('x 是最小的') 否则如果 y < z: print('y 是最小的') 否则: print('z 是最小的') py **Finger exercise:** Write a program that examines three variables—`x`, `y`, and `z`—and prints the largest odd number among them. If none of them are odd, it should print the smallest value of the three. You can attack this exercise in a number of ways. There are eight separate cases to consider: they are all odd (one case), exactly two of them are odd (three cases), exactly one of them is odd (three cases), or none of them is odd (one case). So, a simple solution would involve a sequence of eight `if` statements, each with a single `print` statement: 如果 x%2 != 0 且 y%2 != 0 且 z%2 != 0: print(max(x, y, z)) 如果 x%2 != 0 且 y%2 != 0 且 z%2 == 0: print(max(x, y)) 如果 x%2 != 0 且 y%2 == 0 且 z%2 != 0: print(max(x, z)) 如果 x%2 == 0 且 y%2 != 0 且 z%2 != 0: print(max(y, z)) 如果 x%2 != 0 且 y%2 == 0 且 z%2 == 0: print(x) 如果 x%2 == 0 且 y%2 != 0 且 z%2 == 0: print(y) 如果 x%2 == 0 且 y%2 == 0 且 z%2 != 0: print(z) 如果 x%2 == 0 且 y%2 == 0 且 z%2 == 0: print(min(x, y, z)) py That gets the job done, but is rather cumbersome. Not only is it 16 lines of code, but the variables are repeatedly tested for oddness. The following code is both more elegant and more efficient: answer = min(x, y, z) 如果 x%2 != 0: answer = x 如果 y%2 != 0 且 y > answer: answer = y 如果 z%2 != 0 且 z > answer: answer = z print(answer) py The code is based on a common programming paradigm. It starts by assigning a provisional value to a variable (`answer`), updating it when appropriate, and then printing the final value of the variable. Notice that it tests whether each variable is odd exactly once, and contains only a single print statement. This code is pretty much as well as we can do, since any correct program must check each variable for oddness and compare the values of the odd variables to find the largest of them. Python supports **conditional expressions** as well as conditional statements. Conditional expressions are of the form expr1 如果 condition 否则 expr2 py If the condition evaluates to `True`, the value of the entire expression is `*expr1*`; otherwise it is `*expr2*`. For example, the statement x = y 如果 y > z 否则 z py sets `x` to the maximum of `y` and `z`. A conditional expression can appear any place an ordinary expression can appear, including within conditional expressions. So, for example, print((x 如果 x > z 否则 z) 如果 x > y 否则 (y 如果 y > z 否则 z)) py prints the maximum of `x`, `y`, and `z`. Conditionals allow us to write programs that are more interesting than straight-line programs, but the class of branching programs is still quite limited. One way to think about the power of a class of programs is in terms of how long they can take to run. Assume that each line of code takes one unit of time to execute. If a straight-line program has `n` lines of code, it will take `n` units of time to run. What about a branching program with `n` lines of code? It might take less than `n` units of time to run, but it cannot take more, since each line of code is executed at most once. A program for which the maximum running time is bounded by the length of the program is said to run in **constant time**. This does not mean that each time the program is run it executes the same number of steps. It means that there exists a constant, `k`, such that the program is guaranteed to take no more than `k` steps to run. This implies that the running time does not grow with the size of the input to the program. Constant-time programs are limited in what they can do. Consider writing a program to tally the votes in an election. It would be truly surprising if one could write a program that could do this in a time that was independent of the number of votes cast. In fact, it is impossible to do so. The study of the intrinsic difficulty of problems is the topic of **computational complexity**. We will return to this topic several times in this book. Fortunately, we need only one more programming language construct, iteration, to allow us write programs of arbitrary complexity. We get to that in Section 2.5.```` py## 2.4 Strings and Input Objects of type strare used to represent characters.¹⁶ Literals of typestrcan be written using either single or double quotes, e.g.,'abc'or"abc". The literal '123'denotes a string of three characters, not the number 123. Try typing the following expressions in to the Python interpreter. ```'a'3*43'a'3+4'a'+'a'```py The operator+is said to be **overloaded** because it has different meanings depending upon the types of the objects to which it is applied. For example, the operator+means addition when applied to two numbers and concatenation when applied to two strings. The operatoris also over­loaded. It means what you expect it to mean when its operands are both numbers. When applied to anintand astr, it is a **repetition operator**—the expression ns, where nis anintandsis astr, evaluates to a strwithnrepeats ofs. For example, the expression 2'John'has the value'JohnJohn'. There is a logic to this. Just as the mathematical expression 32is equivalent to2+2+2, the expression 3'a'is equivalent to'a'+'a'+'a'. Now try typing ``` new_id 'a''a'```py Each of these lines generates an error message. The first line produces the message ``` NameError: name 'new_id' is not defined ```py Becausenew_idis not a literal of any type, the interpreter treats it as a name. However, since that name is not bound to any object, attempting to use it causes a runtime error. The code'a''a'produces the error message ``` TypeError: can't multiply sequence by non-int of type ‘str' ```py That **type checking** exists is a good thing. It turns careless (and sometimes subtle) mistakes into errors that stop execution, rather than errors that lead programs to behave in mysterious ways. The type checking in Python is not as strong as in some other programming languages (e.g., Java), but it is better in Python 3 than in Python 2\. For example, it is clear what<should mean when it is used to compare two strings or two numbers. But what should the value of'4' < 3 be?Rather arbitrarily, the designers of Python 2 decided that it should beFalse, because all numeric values should be less than all values of type str. The designers of Python 3, and most other modern languages, decided that since such expressions don't have an obvious meaning, they should generate an error message. Strings are one of several sequence types in Python. They share the following operations with all sequence types. * The **length** of a string can be found using the lenfunction. For example, the value oflen('abc')is3. * **Indexing** can be used to extract individual characters from a string. In Python, all indexing is zero-based. For example, typing 'abc'[0]into the interpreter will cause it to display the string'a'. Typing 'abc'[3]will produce the error messageIndexError: string index out of range. Since Python uses 0to indicate the first element of a string, the last element of a string of length 3 is accessed using the index2. Negative numbers are used to index from the end of a string. For example, the value of 'abc'[‑1]is'c'. * **Slicing** is used to extract substrings of arbitrary length. If sis a string, the expressions[start:end]denotes the substring ofsthat starts at indexstartand ends at indexend-1. For example, 'abc'[1:3]evaluates to'bc'. Why does it end at index end-1rather thanend? So that expressions such as 'abc'[0:len('abc')]have the value you might expect. If the value before the colon is omitted, it defaults to0. If the value after the colon is omitted, it defaults to the length of the string. Consequently, the expression 'abc'[:]is semantically equivalent to the more verbose'abc'[0:len('abc')]. It is also possible to supply a third argument to select a non-contiguous slice of a string. For example, the value of the expression '123456789'[0:8:2] is the string '1357'. It is often convenient to convert objects of other types to strings using the strfunction. Consider, for example, the code ``` num = 30000000 fraction = 1/2 print(num*fraction, '是', fraction*100, '%', '的', num) print(num*fraction, '是', str(fraction*100) + '%', '的', num) ```py which prints ``` 15000000.0 是 50.0 % 的 30000000 15000000.0 是 50.0% 的 30000000 ```py The first print statement inserts a space between50and%because Python automatically inserts a space between the arguments toprint. The second print statement produces a more appropriate output by combining the 50and the%into a single argument of typestr. **Type conversions** (also called **type casts**) are used often in Python code. We use the name of a type to convert values to that type. So, for example, the value of int('3')4is12. When a floatis converted to anint, the number is truncated (not rounded), e.g., the value of int(3.9)is theint 3. Returning to the output of our print statements, you might be wondering about that .0at the end of the first number printed. That appears because1/2is a floating-point number, and the product of anintand afloatis afloat. It can be avoided by converting numfractionto anint. The code ``` print(int(num*fraction), '是', str(fraction*100) + '%', '的', num) ```py prints 15000000 is 50.0% of 30000000. Python 3.6 introduced an alternative, more compact, way to build string expressions. An **f-string** consists of the character f(orF) following by a special kind of string literal called a **formatted string literal**. Formatted string literals contain both sequences of characters (like other string literals) and expressions bracketed by curly braces. These expressions are evaluated at runtime and automatically converted to strings. The code ```  print(f'{int(num*fraction)} 是 {fraction*100}% 的 {num}') ```py produces the same output as the previous, more verbose, print statement. If you want to include a curly brace in the string denoted by an f-string, use two braces. E.g., print(f'{{{35}}}')prints{15}. The expression inside an f-string can contain modifiers that control the appearance of the output string.¹⁷ These modifiers are separated from the expression denoting the value to be modified by a colon. For example, the f-string  f'{3.14159:.2f}'evaluates to the string'3.14'because the modifier.2f instructs Python to truncate the string representation of a floating-point number to two digits after the decimal point. And the statement ``` print(f'{num*fraction:,.0f} 是 {fraction*100}% 的 {num:,}') ```py prints 15,000,000 is 50.0% of 30,000,000because the,modifier instructs Python to use commas as thousands separators. We will introduce other modifiers as convenient later in the book. ### 2.4.1 Input Python 3 has a function,input, that can be used to get input directly from a user. The inputfunction takes a string as an argument and displays it as a prompt in the shell. The function then waits for the user to type something and press theenterkey. The line typed by the user is treated as a string and becomes the value returned by the function. Executing the codename = input('Enter your name: ')will display the line ```请输入你的名字:```py in the console window. If you then typeGeorge Washingtonand pressenter, the string 'George Washington'will be assigned to the variablename. If you then execute print('Are you really', name, '?'), the line ``` 你真的是乔治·华盛顿吗? ```py would be displayed. Notice that the print statement introduces a space before the “?”. It does this because when printis given multiple arguments, it places a space between the values associated with the arguments. The space could be avoided by executingprint('Are you really ' + name + '?') or print(f'Are you really {name}?'), each of which produces a single string and passes that string as the only argument to print. Now consider the code ``` n = input('请输入一个整数: ') print(type(n)) ```py This code will always print ``` <class ‘str'> ```py because inputalways returns an object of typestr, even if the user has entered something that looks like an integer. For example, if the user had entered 3, nwould be bound to the str'3'not the int3. So, the value of the expression n4would be'3333'rather than12. The good news is that whenever a string is a valid literal of some type, a type conversion can be applied to it. **Finger exercise:** Write code that asks the user to enter their birthday in the form mm/dd/yyyy, and then prints a string of the form ‘You were born in the year yyyy.’ ### 2.4.2 A Digression about Character Encoding For many years most programming languages used a standard called ASCII for the internal representation of characters. This standard included128characters, plenty for representing the usual set of characters appearing in English-language text—but not enough to cover the characters and accents appearing in all the world's languages. The **Unicode** standard is a character coding system designed to support the digital processing and display of the written texts of all languages. The standard contains more than120,000characters—covering129modern and historic scripts and multiple symbol sets. The Unicode standard can be implemented using different internal character encodings. You can tell Python which encoding to use by inserting a comment of the form ```# -- coding: encoding name -- ```py as the first or second line of your program. For example, ``` # -*- coding: utf-8 -*- ```py instructs Python to use UTF-8, the most frequently used character encoding for webpages.¹⁸ If you don't have such a comment in your program, most Python implementations will default to UTF-8. When using UTF-8, you can, text editor permitting, directly enter code like ``` print('你会说英语吗?') print('क्या आप अंग्रेज़ी बोलते हैं?') ```py which will print ``` 你会说英语吗? क्या आप अंग्रेज़ी बोलते हैं? ```py You might be wondering how I managed to type the string 'क्या आप अंग्रेज़ी बोलते हैं?'. I didn't. Because most of the web uses UTF-8, I was able to cut the string from a webpage and paste it directly into my program. There are ways to directly enter Unicode characters from a keyboard, but unless you have a special keyboard, they are all rather cumbersome. ## 2.5 While Loops Near the end of Section 2.3, we mentioned that most computational tasks cannot be accomplished using branching programs. Consider, for example, writing a program that asks for the number of X's. You might think about writing something like ``` num_x = int(input('我应该打印字母 X 几次? ')) to_print = '' 如果 num_x == 1: to_print = 'X' elif num_x == 2: to_print = 'XX' elif num_x == 3: to_print = 'XXX' #… print(to_print) ```py But it would quickly become apparent that you would need as many conditionals as there are positive integers—and there are an infinite number of those. What you want to write is a program that looks like (the following is **pseudocode**, not Python) ``` num_x = int(input('我应该打印字母 X 几次? ')) to_print = '' # 将 X 连接到 to_print num_x 次 print(to_print) ```py When we want a program to do the same thing many times, we can use **iteration**. A generic iteration (also called **looping**) mechanism is shown in the box in Figure 2-6. Like a conditional statement, it begins with a test. If the test evaluates to True, the program executes the **loop** body once, and then goes back to reevaluate the test. This process is repeated until the test evaluates to False, after which control passes to the code following the iteration statement. ![c2-fig-0006.jpg](https://gitee.com/OpenDocCN/geekdoc-ds-zh/tree/master/docs/intr-comp-prog-py/img/c2-fig-0006.jpg) Figure 2-6 Flowchart for iteration We can write the kind of loop depicted in Figure 2-6 using a **while** statement. Consider the code in Figure 2-7. ![c2-fig-0007.jpg](https://gitee.com/OpenDocCN/geekdoc-ds-zh/tree/master/docs/intr-comp-prog-py/img/c2-fig-0007.jpg) Figure 2-7 Squaring an integer, the hard way The code starts by binding the variable xto the integer3. It then proceeds to square xby using repetitive addition. The table in Figure 2-8 shows the value associated with each variable each time the test at the start of the loop is reached. We constructed the table by **hand-simulating** the code, i.e., we pretended to be a Python interpreter and executed the program using pencil and paper. Using pencil and paper might seem quaint, but it is an excellent way to understand how a program behaves.¹⁹ ![c2-fig-0008.jpg](https://gitee.com/OpenDocCN/geekdoc-ds-zh/tree/master/docs/intr-comp-prog-py/img/c2-fig-0008.jpg) Figure 2-8 Hand simulation of a small program The fourth time the test is reached, it evaluates toFalseand flow of control proceeds to theprintstatement following the loop. For what values ofxwill this program terminate? There are three cases to consider:x == 0, x > 0, and x < 0. Suppose x == 0. The initial value of num_iterationswill also be0, and the loop body will never be executed. Suppose x > 0. The initial value of num_iterationswill be less thanx, and the loop body will be executed at least once. Each time the loop body is executed, the value of num_iterationsis increased by exactly1. This means that since num_iterationsstarted out less thanx, after some finite number of iterations of the loop, num_iterationswill equalx. At this point the loop test evaluates to False, and control proceeds to the code following the whilestatement. Supposex < 0. Something very bad happens. Control will enter the loop, and each iteration will move num_iterationsfarther fromxrather than closer to it. The program will therefore continue executing the loop forever (or until something else bad, e.g., an overflow error, occurs). How might we remove this flaw in the program? Changing the test tonum_iterations < abs(x)almost works. The loop terminates, but it prints a negative value. If the assignment statement inside the loop is also changed, toans = ans + abs(x), the code works properly. **Finger exercise:** Replace the comment in the following code with a whileloop. ``` num_x = int(input('我应该打印字母 X 几次? ')) to_print = '' #将 X 连接到 to_print num_x 次 print(to_print) ```py It is sometimes convenient to exit a loop without testing the loop condition. Executing a **break** statement terminates the loop in which it is contained and transfers control to the code immediately following the loop. For example, the code ``` # 找到一个同时能被 11 和 12 整除的正整数 x = 1 while True: 如果 x%11 == 0 且 x%12 == 0: break x = x + 1 print(x, '能被 11 和 12 整除') ```py prints ``` 132 能被 11 和 12 整除 ```py If abreakstatement is executed inside a nested loop (a loop inside another loop), the break will terminate the inner loop. **Finger exercise:** Write a program that asks the user to input 10 integers, and then prints the largest odd number that was entered. If no odd number was entered, it should print a message to that effect. ## 2.6 For Loops and Range Thewhileloops we have used so far are highly stylized, often iterating over a sequence of integers. Python provides a language mechanism, thefor**loop**, that can be used to simplify programs containing this kind of iteration. The general form of aforstatement is (recall that the words in italics are descriptions of what can appear, not actual code): ```for*variable*in *sequence*: *code block* ```py The variable followingforis bound to the first value in the sequence, and the code block is executed. The variable is then assigned the second value in the sequence, and the code block is executed again. The process continues until the sequence is exhausted or abreakstatement is executed within the code block. For example, the code ``` total = 0 for num in (77, 11, 3): total = total + num print(total) ```py will print91. The expression (77, 11, 3) is a **tuple**. We discuss tuples in detail in Section 5\. For now, just think of a tuple as a sequence of values. The sequence of values bound to *variable* is most commonly generated using the built-in function range, which returns a series of integers. The rangefunction takes three integer arguments:start, stop, and step. It produces the progression start, start + step, start + 2step,etc. Ifstep is positive, the last element is the largest integer such that (start + istep)is strictly less thanstop. If step is negative, the last element is the smallest integer such that (start + i*step)is greater thanstop. For example, the expression range(5, 40, 10)yields the sequence5, 15, 25, 35, and the expression range(40, 5, -10)yields the sequence40, 30, 20, 10. If the first argument to rangeis omitted, it defaults to0, and if the last argument (the step size) is omitted, it defaults to 1. For example, range(0, 3)andrange(3)both produce the sequence0, 1, 2. The numbers in the progression are generated on an “as needed” basis, so even expressions such as range(1000000)consume little memory. We will discussrangein more depth in Section 5.2. Consider the code ``` x = 4 for i in range(x): print(i) ```py It prints ``` 0 1 2 3 ```py The code in Figure 2-9 reimplements the algorithm in Figure 2-7 for squaring an integer (corrected so that it works for negative numbers). Notice that unlike thewhileloop implementation, the number of iterations is not controlled by an explicit test, and the index variablenum_iterationsis not explicitly incremented. ![c2-fig-0009.jpg](https://gitee.com/OpenDocCN/geekdoc-ds-zh/tree/master/docs/intr-comp-prog-py/img/c2-fig-0009.jpg) Figure 2-9 Using aforstatement Notice that the code in Figure 2-9 does not change the value ofnum_iterationswithin the body of theforloop. This is typical, but not necessary, which raises the question of what happens if the index variable is modified within theforloop. Consider ``` for i in range(2): print(i) i = 0 print(i) ```py Do you think that it will print0, 0, 1, 0, and then halt? Or do you think it will print 0over and over again? The answer is0, 0, 1, 0. Before the first iteration of the forloop, therangefunction is evaluated and the first value in the sequence it yields is assigned to the index variable,i. At the start of each subsequent iteration of the loop, iis assigned the next value in the sequence. When the sequence is exhausted, the loop terminates. The aboveforloop is equivalent to the code ``` index = 0 last_index = 1 while index <= last_index: i = index print(i) i = 0 print(i) index = index + 1 ```py Notice, by the way, that code with thewhileloop is considerably more cumbersome than theforloop. Theforloop is a convenient linguistic mechanism. Now, what do you think ``` x = 1 for i in range(x): print(i) x = 4 ```py prints? Just0, because the arguments to the rangefunction in the line withforare evaluated just before the first iteration of the loop, and not reevaluated for subsequent iterations. Now, let's see how often things are evaluated when we nest loops. Consider ``` x = 4 for j in range(x): for i in range(x): x = 2 ```py How many times is each of the two loops executed? We already saw that therange(x)controlling the outer loop is evaluated the first time it is reached and not reevaluated for each iteration, so there are four iterations of the outer loop. That implies that the innerfor loop is reached four times. The first time it is reached, the variable x = 4, so there will be four iterations. However, the next three times it is reached, x = 2, so there will be two iterations each time. Consequently, if you run ``` x = 3 for j in range(x): print('外循环的迭代') for i in range(x): print(' 内循环的迭代') x = 2```py it prints ``` 外循环的迭代 内循环的迭代 内循环的迭代 内循环的迭代 外循环的迭代 内循环的迭代 内循环的迭代 外循环的迭代 内循环的迭代 内循环的迭代 ```py Theforstatement can be used in conjunction with thein**operator** to conveniently iterate over characters of a string. For example, ``` total = 0 for c in '12345678': total = total + int(c) print(total) ```py sums the digits in the string denoted by the literal'12345678'and prints the total. **Finger exercise:** Write a program that prints the sum of the prime numbers greater than 2 and less than 1000\. Hint: you probably want to use aforloop that is a primality test nested inside aforloop that iterates over the odd integers between 3 and 999\. ## 2.7 Style Matters Much of this book is devoted to helping you learn a programming language. But knowing a language and knowing how to use a language well are two different things. Consider the following two sentences: *“Everybody knows that if a man is* *unmarried and has a lot of money, he* *needs to get married.”* *“It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.”*²⁰ Each is a proper English sentence, and each means approximately the same thing. But they are not equally compelling, and perhaps not equally easy to understand. Just as style matters when writing in English, style matters when writing in Python. However, while having a distinctive voice might be an asset for a novelist, it is not an asset for a programmer. The less time readers of a program have to spend thinking about things irrelevant to the meaning of the code, the better. That's why good programmers follow coding conventions designed to make programs easy to understand, rather than fun to read. Most Python programmers follow the conventions laid out in the **PEP 8** **style guide**.²¹ Like all sets of conventions, some of its prescriptions are arbitrary. For example, it prescribes using four spaces for indents. Why four spaces and not three or five? No particularly good reason. But if everybody uses the same number of spaces, it is easier to read (and perhaps combine) code written by different people. More generally, if everyone uses the same set of conventions when writing Python, readers can concentrate on understanding the semantics of the code rather than wasting mental cycles assimilating stylistic decisions. The most important conventions have to do with naming. We have already discussed the importance of using variable names that convey the meaning of the variable. Noun phrases work well for this. For example, we used the namenum_iterations for a variable denoting the number of iterations. When a name includes multiple words, the convention in Python is to use an underscore (_)to separate the words. Again, this convention is arbitrary. Some programmers prefer to use what is often called camelCase, e.g.,numIterations`—arguing that it is faster to type and uses less space. There are also some conventions for single-character variable names. The most important is to avoid using a lowercase L or uppercase I (which are easily confused with the number one) or an uppercase O (which is easily confused with the number zero). That's enough about conventions for now. We will return to the topic as we introduce various aspects of Python. We have now covered pretty much everything about Python that you need to know to start writing interesting programs that deal with numbers and strings. In the next chapter, we take a short break from learning Python, and use what you have already learned to solve some simple problems. ## 2.8 Terms Introduced in Chapter * low-level language * high-level language * interpreted language * compiled language * source code * machine code * Python * integrated development environment (IDE) * Anaconda * Spyder * IPython console * shell * program (script) * command (statement) * object * type * scalar object * non-scalar object * literal * floating point * bool * None * operator * expression * value * shell prompt * variable * binding * assignment * reserved word * comment (in code) * straight-line program * branching program * conditional * indentation (in Python) * nested statement * compound expression * constant time * computational complexity * conditional expression * strings * overloaded operator * repetition operator * type checking * indexing * slicing * type conversion (casting) * formatted string expression * input * Unicode * iteration (looping) * pseudocode * while loop * hand simulation * break * for loop * tuple * range * in operator * PEP 8 style guide````

第三章:一些简单的数值程序

既然我们已经涵盖了一些基本的 Python 结构,现在是时候开始思考如何将这些结构结合起来编写简单的程序。在这个过程中,我们会悄悄引入更多的语言结构和一些算法技术。

3.1 穷举枚举

图 3-1 中的代码会打印出一个整数的立方根(如果存在的话)。如果输入不是一个完美的立方,则会打印出相应的消息。操作符 != 意思是“不等于”。

c3-fig-0001.jpg

图 3-1 使用穷举枚举来寻找立方根

代码首先尝试将变量 ans 设置为 x 的绝对值的立方根。如果成功了,接着如果 x 为负,则将 ans 设置为 -ans。在这段代码中,重任(虽说也不算重)是在 while 循环中完成的。每当程序包含一个循环时,理解是什么导致程序最终退出这个循环是很重要的。对于什么值的 x,这个 while 循环会终止?答案是“所有整数”。这一点可以很简单地说明。

  • 表达式 ans**3 的值从 0 开始,每次循环时增大。

  • 当其值达到或超过 abs(x) 时,循环终止。

  • 由于 abs(x) 始终是正数,因此在循环必须终止之前,只有有限的迭代次数。

这个论点基于递减函数的概念。这个函数具有以下属性:

  • 它将一组程序变量映射到一个整数。

  • 当循环被进入时,它的值是非负的。

  • 当其值 ≤ 0 时,循环终止。

  • 它的值在每次循环时减少。

在 图 3-1 中,while 循环的递减函数是什么?它是 abs(x) ‑ ans**3

现在,让我们插入一些错误,看看会发生什么。首先,尝试注释掉语句 ans = 0。Python 解释器将不断打印错误信息。

`NameError: name 'ans' is not defined`

因为解释器尝试在 ans 被绑定到任何值之前找到它的绑定值。现在,恢复 ans 的初始化,将语句 ans = ans + 1 替换为 ans = ans,并尝试寻找 8 的立方根。当你等得不耐烦时,输入“control c”(同时按住 Ctrl 键和 c 键)。这将把你带回到 shell 的用户提示符。

现在,添加语句

print('Value of the decrementing function abs(x) - ans**3 is',
       abs(x) - ans**3)

在循环开始时,再次尝试运行。这次它将打印

Value of the decrementing function abs(x) - ans**3 is 8

一遍又一遍地。

程序会永远运行下去,因为循环体不再缩小 ans**3abs(x) 之间的距离。当遇到一个似乎没有终止的程序时,经验丰富的程序员常常会插入打印语句,比如这里的语句,以测试递减函数是否确实在递减。

本程序使用的算法技术是猜测与检查的一种变体,称为耗尽枚举。我们枚举所有可能性,直到找到正确答案或穷尽所有可能性。乍一看,这似乎是解决问题的一个极其愚蠢的方法。然而,令人惊讶的是,耗尽枚举算法往往是解决问题的最实际方法。它们通常易于实现和理解,并且在许多情况下,它们的运行速度足够快以满足所有实际需求。去掉或注释掉你为调试插入的打印语句,并重新插入语句ans = ans + 1。现在尝试寻找1957816251的立方根。程序几乎会瞬间完成。现在试试7406961012236344616

正如你所看到的,即使需要数百万次猜测,运行时间通常也不是问题。现代计算机速度惊人。执行一条指令所需的时间不到一纳秒——十亿分之一秒。很难想象这有多快。为了更好地理解,光在一英尺(0.3 米)中传播的时间稍微超过一纳秒。另一种理解方式是,在你声音传播 100 英尺的时间内,现代计算机可以执行数百万条指令。

仅为好玩,尝试执行该代码。

 max_val = int(input('Enter a postive integer: '))
i = 0
while i < max_val:
    i = i + 1
print(i)

看看你需要输入多大的整数,才能在结果打印之前感知到延迟。

让我们看一个耗尽枚举的另一个例子:测试一个整数是否为质数,并在不是时返回最小的因子。质数是大于 1 的整数,仅能被自身和 1 整除。例如,2、3、5 和 111,119 是质数,而 4、6、8 和 62,710,561 不是质数。

确定一个大于 3 的整数x是否为质数的最简单方法是将x除以 2 到x-1之间的每个整数。如果其中任何一个除法的余数为 0,则x不是质数,否则x是质数。图 3-2 中的代码实现了这种方法。它首先要求用户输入一个整数,将返回的字符串转换为int,并将该整数赋值给变量x。接着,它通过将guess初始化为2和将变量smallest_divisor初始化为None来设置耗尽枚举的初始条件——这表明在未证明的情况下,代码假定x为质数。

耗尽枚举是在for循环中完成的。当所有可能的x的整数因子都已尝试过,或者发现一个x的因子时,循环终止。

退出循环后,代码检查smallest_divisor的值并打印相应的文本。在进入循环之前初始化变量,然后检查该值在退出时是否已更改是一种常见的技巧。

c3-fig-0002.jpg

图 3-2 使用耗尽枚举测试质性。

练习: 修改图 3-2 中的代码,使其返回最大而不是最小的除数。提示:如果y*z = xyx的最小除数,z就是x的最大除数。

图 3-2 中的代码能够运行,但效率不必要地低。例如,检查大于 2 的偶数是没有必要的,因为如果一个整数能被任何偶数整除,那么它必定能被 2 整除。图 3-3 中的代码利用了这一点,先测试x是否是偶数。如果不是,它会使用循环测试x是否能被任何奇数整除。

尽管图 3-3 中的代码比图 3-2 中的代码稍复杂,但它明显更快,因为在循环中检查的数字减少了一半。将代码复杂性与运行效率进行权衡是一个常见现象。但更快并不总意味着更好。简单的代码明显正确,并且仍然足够快以便有用,值得赞美。

c3-fig-0003.jpg

图 3-3 更高效的质数测试

练习: 编写一个程序,要求用户输入一个整数,并打印两个整数rootpwr,使得1 < pwr < 6并且root**pwr等于用户输入的整数。如果不存在这样的整数对,应该打印相应的消息。

练习: 编写一个程序,打印大于 2 且小于 1000 的所有质数的和。提示:你可能想要一个循环,其中包含一个嵌套在循环内的质数测试,迭代 3 到 999 之间的奇数。

3.2 *似解和二分查找

想象一下,有人要求你编写一个程序,打印任何非负数的*方根。你应该怎么做?

你可能应该先说你需要一个更好的问题陈述。例如,如果被要求找到2的*方根,程序应该怎么做?2的*方根不是一个有理数。这意味着没有办法精确表示它的值为有限的数字字符串(或作为float),所以最初陈述的问题无法解决。

程序可以做的事情是找到*方根的*似值——即,接*实际*方根的答案,以便有用。我们稍后将在书中详细讨论这个问题。但现在,让我们把“足够接*”理解为在实际答案的某个常量范围内,称之为epsilon

图 3-4 中的代码实现了一个打印x的*方根*似值的算法。

c3-fig-0004.jpg

图 3-4 使用穷举枚举来*似*方根

我们再次使用穷举枚举。请注意,这种寻找*方根的方法与您在中学时学到的用铅笔找*方根的方法没有任何共同之处。计算机解决问题的最佳方法通常与手工解决问题的方法大相径庭。

如果x是 25,代码将打印

number of guesses = 49990
4.999000000001688 is close to square root of 25

我们应该失望于程序没有识别出25是一个完全*方并打印5吗?不。程序做了它应该做的事。虽然打印5也可以,但这样做与打印任何接*5的值并没有什么不同。

如果我们设定x = 0.25,你认为会发生什么?它会找到接*0.5的根吗?不。可惜,它会报告

number of guesses = 2501
Failed on square root of 0.25

穷举枚举是一种搜索技术,只有在被搜索的值集合中包含答案时才有效。在这种情况下,我们在枚举0x的值。当x01之间时,x的*方根不在这个区间内。解决这个问题的一种方法是改变while循环第一行中and的第二个操作数,以得到

`while abs(ans**2 - x) >= epsilon and ans*ans <= x:`

当我们在这个变化后运行代码时,它报告

0.48989999999996237 is close to square root of 0.25

现在,让我们想想程序运行需要多长时间。迭代次数取决于答案与我们起始点 0 的接*程度,以及步长的大小。粗略来说,程序最多会执行while循环x/step次。

让我们尝试在更大的数上运行代码,例如x = 123456。它将运行一段时间,然后打印

number of guesses = 3513631
Failed on square root of 123456

你认为发生了什么?肯定有一个浮点数可以将123456的*方根*似到0.01之内。为什么我们的程序没有找到它?问题在于我们的步长太大,程序跳过了所有合适的答案。我们又一次在一个不包含解的空间中进行穷举搜索。试着将step设为epsilon**3并运行程序。它最终会找到一个合适的答案,但你可能没有耐心等待它。

大概需要多少次猜测?步长将是0.000001123456的*方根大约是351.36。这意味着程序必须进行大约351,000,000次猜测才能找到满意的答案。我们可以通过更接*答案的起始点来加速,但这假设我们知道答案的邻域。

现在是寻找不同方法解决问题的时候了。我们需要选择一个更好的算法,而不是微调当前的算法。但在这样做之前,让我们看一个乍一看与根寻找完全不同的问题。

考虑发现一个以给定字母序列开头的单词是否出现在英语的纸质字典²²中的问题。理论上,穷举枚举是可行的。你可以从第一个单词开始,检查每个单词,直到找到一个以该字母序列开头的单词,或者检查完所有单词。如果字典包含 n 个单词,*均需要 n/2 次查询才能找到该单词。如果该单词不在字典中,则需要 n 次查询。当然,那些曾经在纸质(而非在线)字典中查找单词的人永远不会以这种方式进行。

幸运的是,出版纸质字典的人会费心将单词按字典顺序排列。这使我们能够打开书本到我们认为该单词可能存在的一页(例如,对于以字母 m 开头的单词,通常是在中间附*)。如果字母序列在页面上第一个单词之前,我们就知道要向后查找。如果字母序列在页面上最后一个单词之后,我们就知道要向前查找。否则,我们检查字母序列是否与页面上的单词匹配。

现在我们来把同样的思路应用于寻找 x 的*方根的问题。假设我们知道 x 的*方根的一个好*似值位于 0max 之间。我们可以利用数字是完全有序的这一事实。也就是说,对于任何一对不同的数字,n1n2,要么 n1 < n2,要么 n1 > n2。因此,我们可以认为 x 的*方根位于以下线段上

0_________________________________________________________max

然后开始搜索该区间。由于我们不一定知道从哪里开始搜索,让我们从中间开始。

0guessmax

如果这不是正确的答案(大多数时候都不是),请询问它是太大还是太小。如果太大,我们知道答案必须在左侧。如果太小,我们知道答案必须在右侧。然后我们在更小的区间上重复这个过程。图 3-5 包含了该算法的实现和测试。

c3-fig-0005.jpg

图 3-5 使用二分查找来*似*方根

当运行 x = 25 时,它打印

low = 0.0 high = 25 ans = 12.5
low = 0.0 high = 12.5 ans = 6.25
low = 0.0 high = 6.25 ans = 3.125
low = 3.125 high = 6.25 ans = 4.6875
low = 4.6875 high = 6.25 ans = 5.46875
low = 4.6875 high = 5.46875 ans = 5.078125
low = 4.6875 high = 5.078125 ans = 4.8828125
low = 4.8828125 high = 5.078125 ans = 4.98046875
low = 4.98046875 high = 5.078125 ans = 5.029296875
low = 4.98046875 high = 5.029296875 ans = 5.0048828125
low = 4.98046875 high = 5.0048828125 ans = 4.99267578125
low = 4.99267578125 high = 5.0048828125 ans = 4.998779296875
low = 4.998779296875 high = 5.0048828125 ans = 5.0018310546875
numGuesses = 13
5.00030517578125 is close to square root of 25

请注意,它找到的答案与我们早期的算法不同。这完全没问题,因为它仍然符合问题的规范。

更重要的是,请注意在循环的每次迭代中,要搜索的空间大小减半。因此,该算法被称为二分查找。二分查找相比于我们早期的算法有了巨大的改进,后者在每次迭代中仅减少了少量搜索空间。

让我们再次尝试x = 123456。这次程序仅需 30 次猜测就能找到一个可接受的答案。x = 123456789呢?仅需 45 次猜测。

使用该算法来寻找*方根并没有什么特别之处。例如,通过将几个2改为3,我们可以用它来*似一个非负数的立方根。在第四章中,我们将介绍一种语言机制,使我们能够将这段代码推广到寻找任何根。

二分搜索是一种广泛适用的技术,除了寻找根以外还可以用于许多其他事情。例如,图 3-6 中的代码使用二分搜索来寻找x的以 2 为底的对数的*似值(即,一个数字ans,使得2**ans接*于x)。它的结构与用于寻找*方根*似值的代码完全相同。它首先找到一个包含合适答案的区间,然后使用二分搜索高效地探索该区间。

c3-fig-0006.jpg

图 3-6 使用二分搜索来估计以 2 为底的对数

二分搜索是逐次逼*方法的一个示例。这种方法通过一系列猜测来工作,每个猜测都比前一个猜测更接*正确答案。我们将在本章稍后讨论一个重要的逐次逼*算法,牛顿法。

动手练习: 如果x = -25,那么图 3-5 中的代码会做什么?

动手练习: 为了让图 3-5 中的代码能够找到负数和正数的立方根*似值,需要做出什么更改?提示:考虑改变low以确保答案位于被搜索的区域内。

动手练习: 帝国大厦有 102 层。一名男子想知道他可以从哪个最高楼层扔鸡蛋而不会破。于是他提出从顶楼扔一个鸡蛋。如果鸡蛋破了,他就下降一层,再试一次。他会一直这样做,直到鸡蛋不破。最坏情况下,这种方法需要 102 个鸡蛋。实现一种方法,最多只使用七个鸡蛋。

3.3 关于使用浮点数的几点说明

大多数情况下,float类型的数字能够 reasonably 地*似真实数字。但“在大多数情况下”并不意味着在所有情况下,当它们不准确时,可能会导致意想不到的后果。例如,尝试运行以下代码:

x = 0.0
for i in range(10):
    x = x + 0.1
if x == 1.0:
    print(x, '= 1.0')
else:
    print(x, 'is not 1.0')

或许你和大多数人一样,发现它打印的结果令人惊讶。

0.9999999999999999 is not 1.0

为什么它首先会进入else语句呢?

要理解为什么会发生这种情况,我们需要了解在计算过程中浮点数是如何在计算机中表示的。要理解这一点,我们需要了解二进制数

当你第一次学习十进制数字——即以 10 为底的数字——时,你了解到任何十进制数字可以通过数字 0123456789 的序列表示。最右边的数字是 10⁰ 位,向左下一个数字是 10¹ 位,等等。例如,十进制数字序列 302 表示 3 * 100 + 0 * 10 + 2 * 1。一个长度为 n 的序列可以表示多少不同的数字?长度为 1 的序列可以表示 10 个数字(0-9);长度为 2 的序列可以表示 100 个不同的数字(0-99)。更一般地,长度为 n 的序列可以表示 10^n 个不同的数字。

二进制数字——以 2 为底的数字——工作原理类似。一个二进制数字由一系列数字表示,每个数字要么是 0 要么是 1。这些数字通常称为。最右边的数字是 2⁰ 位,向左下一个数字是 2¹ 位,等等。例如,二进制数字序列 101 表示 1 * 4 + 0 * 2 + 1 * 1 = 5。一个长度为 n 的序列可以表示多少不同的数字? 2^n。

手指练习: 二进制数 10011 的十进制等值是多少?

也许因为大多数人有十根手指,我们喜欢用十进制表示数字。另一方面,所有现代计算机系统都是用二进制表示数字。这并不是因为计算机天生只有两根手指,而是因为构建只能处于开或关两种状态的硬件开关很简单。计算机使用二进制表示法而人类使用十进制表示法可能会导致偶尔的认知失调。

在现代编程语言中,非整数数字使用一种称为浮点的表示法实现。暂时假设内部表示是十进制。我们可以将数字表示为一对整数——数字的有效数字和一个指数。例如,数字 1.949 可以表示为一对 (1949, -3),这表示 1949 * 10^(-3) 的乘积。

有效数字的数量决定了数字的精度。例如,如果只有两个有效数字,数字 1.949 将无法精确表示。它必须被转换为某种*似值,在这种情况下是 1.9。这种*似值称为舍入值

现代计算机使用二进制而不是十进制表示法。它们以二进制而不是十进制表示有效数字和指数,并将 2 而不是 10 提升到指数。例如,十进制数字 0.6255/8)表示为一对 (101, -11);因为 101 是数字 5 的二进制表示,-11 是 -3 的二进制表示,所以一对 (101, -11) 表示 5 * 2^(-3) = 5/8 = 0.625。

那么十进制分数1/10,我们在 Python 中写作0.1,最佳的四个有效二进制数字是(0011, -101)。这相当于3/32,即0.09375。如果我们有五个有效二进制数字,我们将0.1表示为(11001, -1000),这相当于25/256,即0.09765625。我们需要多少个有效数字才能准确表示0.1的浮点数?无限多个数字!不存在整数sigexp使得sig * 2^(-exp)等于0.1。因此,无论 Python(或任何其他语言)使用多少位来表示浮点数,它只能表示对0.1的*似。在大多数 Python 实现中,浮点数可用53位精度,因此存储的十进制数0.1的有效数字将是。

11001100110011001100110011001100110011001100110011001

这相当于十进制数。

0.1000000000000000055511151231257827021181583404541015625

非常接*1/10,但并不完全是1/10

回到最初的谜题,为什么。

`x = 0.0 for i in range(10):     x = x + 0.1 if x == 1.0:     print(x, '= 1.0') else:     print(x, 'is not 1.0')`

打印。

0.9999999999999999 is not 1.0

我们现在看到测试`x == 1.0`的结果为`False`,因为`x`绑定的值并不完全等于`1.0`。这解释了为什么执行了`else`分支。但为什么在`0.1`的浮点表示稍大于`0.1`时,系统却认为`x`小于`1.0`?因为在循环的某次迭代中,Python 的有效数字用完了,进行了向下的四舍五入。如果我们在`else`分支末尾添加代码`print x == 10.0*0.1`,会打印出`False`。这与我们小学老师教的不同,但十次加`0.1`的结果并不等于将`0.1`乘以`10`。顺便说一下,如果你想明确地对浮点数进行四舍五入,使用`round`函数。表达式`round(x, num_digits)`返回将`x`的值四舍五入到小数点后`num_digits`位的浮点数。例如,打印`round(2**0.5, 3)`会打印`1.414`,作为`2`的*方根的*似值。 实际数与浮点数之间的差异真的重要吗?大多数情况下,谢天谢地,它并不重要。`0.9999999999999999`、`1.0`和`1.00000000000000001`之间的差异几乎没有影响。然而,有一件事几乎总是值得担心,那就是等式测试。正如我们所见,使用`==`比较两个浮点值可能会产生令人惊讶的结果。询问两个浮点值是否足够接*,几乎总是比询问它们是否相同更合适。因此,例如,写`abs(x‑y) < 0.0001`要比`x == y`更好。 另一个需要担心的问题是舍入误差的积累。大多数时候事情运转良好,因为有时存储在计算机中的数字比预期稍大,有时则稍小。然而,在某些程序中,误差会朝同一方向累积。 ``## 3.4 牛顿–拉弗森方法²³ 最常用的*似算法通常归因于艾萨克·牛顿。通常称为牛顿法,有时也称为牛顿–拉弗森方法。²⁴ 它可以用于找到许多函数的实根,但我们只在寻找单变量多项式的实根的背景下讨论它。推广到多个变量的多项式在数学和算法上都是直接的。 一个多项式(按照约定,我们将变量写为*x*)要么是 0,要么是有限个非零项的和,例如3*x*² + 2*x* + 3。每一项,例如3*x*²,由一个常数(这一项的系数,在本例中为3)乘以变量(在本例中为*x*),后者的指数为非负整数(在本例中为2)。项的指数称为该项的次数。多项式的次数是任何单一项的最大次数。一些例子是3(次数为0)、2.5*x* + 12(次数为1)和3*x*²(次数为2)。 如果*p*是一个多项式,*r*是一个实数,我们将写*p*(*r*)表示多项式在*x* = *r*时的值。多项式*p*的一个是方程*p* = 0的解,即使得*p*(*r*) = 0*r*。例如,寻找24的*方根的*似值的问题可以表述为寻找一个*x*,使得*x*² − 24接*于0。 牛顿证明了一个定理,意味着如果一个值,称为*guess*,是多项式的根的*似值,那么*guess* − 𝑝(*guess*)/𝑝’(*guess*)(其中*p*’*p*的一阶导数)是比*guess*更好的*似值。 一个函数*f*(*x*)的一阶导数可以理解为表达*f*(*x*)的值相对于*x*的变化。例如,一个常数的一阶导数是0,因为常数的值不变。对于任何项*c***x^p*,该项的一阶导数是*c***p***x^p*^(−1)。因此,形式为 c3-fig-5001.jpg 的多项式的一阶导数是 c3-fig-5002.jpg 为了找到一个数字的*方根,例如k,我们需要找到一个值x,使得*x*² − *k* = 0。这个多项式的一阶导数就是2*x*。因此,我们知道可以通过选择下一个猜测为*guess* − (*guess*² − 𝑘)/2 * *guess*来改进当前猜测。图 3-7 包含代码,展示如何使用该方法快速找到*方根的*似值。 c3-fig-0007.jpg 图 3-7 牛顿–拉弗森方法的实现 指尖练习:在牛顿–拉弗森的实现中添加一些代码,以跟踪找到根所用的迭代次数。将该代码作为一个程序的一部分,比较牛顿–拉弗森法与二分搜索的效率。(你会发现牛顿–拉弗森法更高效。) ## 3.5 本章引入的术语 * 递减函数 * 猜测与检查 * 穷举枚举 * *似 * 全序 * 二分搜索 * 逐次*似 * 二进制数 * 位 * 开关 * 浮点数 * 有效数字 * 指数 * 精度 * 舍入 * 牛顿–拉弗森 * 多项式 * 系数 * 次数 * 根```

第四章:函数、作用域和抽象

到目前为止,我们已经介绍了数字、赋值、输入/输出、比较和循环结构。这个 Python 子集有多强大?从理论上讲,它强大到你所需的一切,即它是图灵完备的。这意味着,如果一个问题可以通过计算解决,那么它可以仅仅使用你已经看到的语言机制来解决。

但仅仅因为某件事可以做,并不意味着应该去做!虽然原则上任何计算都可以仅使用这些机制来实现,但这样做非常不实际。在上一章中,我们查看了一个寻找正数*方根*似值的算法,见图 4-1。

c4-fig-0001.jpg

图 4-1 使用二分查找法*似计算 x 的*方根

这是一个合理的代码片段,但缺乏通用性。它仅适用于分配给变量xepsilon的值。这意味着如果我们想重用它,就需要复制代码,可能还要编辑变量名,然后粘贴到我们想要的位置。我们不能轻松地在其他更复杂的计算中使用这个计算。此外,如果我们想计算立方根而不是*方根,我们就得编辑代码。如果我们想要一个能够计算*方根和立方根的程序(或者说在两个不同地方计算*方根),程序中将包含多块几乎相同的代码。

图 4-2 将图 4-1 中的代码改编为打印x1的*方根和x2的立方根的和。代码可以运行,但看起来不太美观。

c4-fig-0002.jpg

图 4-2 *方根和立方根的和

程序包含的代码越多,出错的机会就越大,代码的维护也越困难。例如,想象一下,如果二分查找法的初始实现存在错误,并且在测试程序时发现了这个错误,那么很容易在一个地方修复实现,而忽视了其他需要修复的类似代码。

幸运的是,Python 提供了几种语言特性,使得泛化和重用代码相对容易。最重要的是函数。

4.1 函数与作用域

我们已经使用了多个内置函数,例如在图 4-1 中使用的maxabs。程序员能够定义并像内置函数一样使用自己的函数,这在便利性上是一个质的飞跃。

4.1.1 函数定义

在 Python 中,每个函数定义的形式是²⁵

def *name of function* (*list of formal parameters*):
    *body of function*

例如,我们可以通过代码定义函数max_val²⁶

def max_val(x, y):
    if x > y:
        return x
    else:
        return y

def是一个保留字,告诉 Python 即将定义一个函数。函数名(在此示例中为max_val)只是一个用于引用函数的名称。PEP 8 规范要求函数名应全小写,单词用下划线分隔,以提高可读性。

函数名后括号中的名称序列(在此示例中为x,y)是函数的形式参数。当使用函数时,形式参数被绑定(如同赋值语句)到实际参数(通常称为实参)的函数调用(也称为函数调用)。例如,该调用

`max_val(3, 4)`

x绑定到3y绑定到4

函数体是任何 Python 代码的片段。²⁷ 然而,有一个特殊语句**return**,只能在函数体内使用。

函数调用是一个表达式,像所有表达式一样,它有一个值。该值由被调用的函数返回。例如,表达式max_val(3,4)*max_val(3,2)的值是12,因为第一次调用max_val返回int 4,第二次返回int 3。请注意,执行return语句会终止函数的调用。

概括地说,当调用函数时

  1. 1. 组成实际参数的表达式被求值,函数的形式参数被绑定到结果值。例如,调用max_val(3+4, z)将把形式参数x绑定到7,将形式参数y绑定到调用执行时变量z的值。

  2. 2. 执行点(下一条要执行的指令)从调用点移动到函数体中的第一条语句。

  3. 3. 函数体中的代码会执行,直到遇到return语句,在这种情况下,return后面表达式的值成为函数调用的值;如果没有更多语句可执行,函数将返回值None。(如果return后没有表达式,调用的值为None。)²⁸

  4. 4. 调用的值是返回的值。

  5. 5. 执行点被转移回紧接在调用后面的代码。

参数使程序员能够编写访问特定对象的代码,而是访问调用函数时选择用作实际参数的对象。这被称为lambda 抽象。²⁹

图 4-3 包含一个有三个形式参数并返回一个值的函数,称其为result,使得abs(result**power – x) >= epsilon

c4-fig-0003.jpg

图 4-3 一个寻找根的函数

图 4-4 包含可用于测试 find_root 是否按预期工作的代码。测试函数 test_find_root 的长度与 find_root 本身大致相同。对于缺乏经验的程序员来说,编写 测试函数 通常看起来是一种浪费精力。然而,经验丰富的程序员知道,编写测试代码的投资往往会带来丰厚的回报。这绝对胜过在键盘前反复输入测试用例到 shell 中进行 调试(找出程序为何不工作并修复的过程)。请注意,由于我们使用三个长度为三的元组调用 test_find_root,一次调用检查 27 种参数组合。最后,因为 test_find_root 检查 find_root 是否返回合适的答案并报告结果,它使程序员免于逐个检查每个输出并验证其正确性的乏味且容易出错的任务。我们将在第八章回到测试的主题。

手指练习: 使用 图 4-3 中的 find_root 函数打印 25 的*方根、-8 的立方根和 16 的四次根的*似值之和。使用 0.001 作为 epsilon。

手指练习: 编写一个函数 is_in,接受两个字符串作为参数,如果其中一个字符串在另一个字符串中出现,则返回 True,否则返回 False。提示:你可能想使用内置的 str 操作符 in

手指练习: 编写一个函数来测试 is_in

c4-fig-0004.jpg

图 4-4 测试 find_root 的代码

4.1.2 关键字参数和默认值

在 Python 中,正式参数绑定到实际参数有两种方式。最常见的方法是 位置参数——第一个正式参数绑定到第一个实际参数,第二个正式参数绑定到第二个实际参数,依此类推。Python 还支持 关键字参数,其中正式参数通过其名称绑定到实际参数。考虑以下函数定义

def print_name(first_name, last_name, reverse): 
   if reverse:
      print(last_name + ', ' + first_name)
   else:
      print(first_name, last_name)

函数 print_name 假设 first_namelast_name 是字符串,reverse 是布尔值。如果 reverse == True,它打印 last_name, first_name;否则,它打印 first_name last_name

以下每个都是 print_name 的等效调用:

print_name('Olga', 'Puchmajerova', False)
print_name('Olga', 'Puchmajerova', reverse = False)
print_name('Olga', last_name = 'Puchmajerova', reverse = False)
print_name(last_name = 'Puchmajerova', first_name = 'Olga',
          reverse = False)

尽管关键字参数可以在实际参数列表中以任何顺序出现,但在关键字参数后跟非关键字参数是非法的。因此,会产生错误消息

print_name('Olga', last_name = 'Puchmajerova', False)

关键字参数通常与 默认参数值 一起使用。例如,我们可以写

def print_name(first_name, last_name, reverse = False):
   if reverse:
      print(last_name + ', ' + first_name)
   else:
      print(first_name, last_name)

默认值允许程序员用少于指定数量的参数调用函数。例如,

print_name('Olga', 'Puchmajerova')
print_name('Olga', 'Puchmajerova', True)
print_name('Olga', 'Puchmajerova', reverse = True)

将打印

Olga Puchmajerova
Puchmajerova, Olga
Puchmajerova, Olga

最后两次调用 print_name 在语义上是等价的。最后一次调用的优势在于为可能神秘的参数 True 提供了一些文档。更一般地说,使用关键字参数降低了将实际参数错误绑定到形式参数的风险。这行代码

print_name(last_name = 'Puchmajerova', first_name = 'Olga')

不会对编写它的程序员的意图产生歧义。这是有用的,因为以错误的顺序调用函数并传递正确的参数是一种常见错误。

默认参数关联的值是在函数定义时计算的。这可能导致程序行为出人意料,正如我们在第 5.3 节中讨论的那样。

手指练习: 编写一个函数 mult,接受一个或两个整数作为参数。如果调用时传入两个参数,函数将打印两个参数的乘积;如果调用时传入一个参数,则打印该参数。

4.1.3 可变数量的参数

Python 有许多内置函数可以处理可变数量的参数。例如,

min(6,4)
min(3,4,1,6)

两者都是合法的(并且评估的结果符合你的预期)。Python 使程序员可以轻松定义接受可变数量参数的函数。拆包运算符 * 允许函数接受可变数量的位置参数。例如,

def mean(*args):
    # Assumes at least one argument and all arguments are numbers
    # Returns the mean of the arguments
    tot = 0
    for a in args:
        tot += a
    return tot/len(args)

打印 1.5 -1.0。请注意,参数列表中跟在 * 后面的名称不必是 args,可以是任何名称。对于 mean,更具描述性的写法可能是 def mean(*numbers)

4.1.4 作用域

我们来看另一个小例子:

def f(x): #name x used as formal parameter
    y = 1
    x = x + y
    print('x =', x)
    return x
x = 3
y = 2
z = f(x) #value of x used as actual parameter
print('z =', z)
print('x =', x)
print('y =', y)

运行时,这段代码打印

x = 4
z = 4
x = 3
y = 2

这里发生了什么?在调用 f 时,形式参数 xf 的函数体上下文中被局部绑定到实际参数 x 的值。尽管实际参数和形式参数同名,但它们不是同一个变量。每个函数定义了一个新的名称空间,也称为作用域。在 f 中使用的形式参数 x局部变量 y 仅在 f 的定义范围内存在。函数体内的赋值语句 x = x + y 将局部名称 x 绑定到对象 4。在 f 中的赋值对 f 作用域外存在的名称 xy 的绑定没有影响。

这里有一种思考方式:

  1. 1. 在顶层,即 shell 的级别,符号表 跟踪在该级别定义的所有名称及其当前绑定。

  2. 2. 当函数被调用时,会创建一个新的符号表(通常称为栈帧)。这个表跟踪在函数内定义的所有名称(包括形式参数)及其当前绑定。如果在函数体内再次调用该函数,则会创建另一个栈帧。

  3. 3. 当函数完成时,其栈帧会消失。

在 Python 中,你可以通过查看程序文本来确定名称的作用域。这被称为静态词法作用域。图 4-5 包含一个示例,说明了 Python 的作用域规则。与代码相关的栈帧历史在图 4-6 中描绘。

c4-fig-0005.jpg

图 4-5 嵌套作用域

图 4-6 中的第一列包含函数f体外已知的名称集合,即变量xz以及函数名称f。第一个赋值语句将x绑定到3

c4-fig-0006.jpg

图 4-6 栈帧

赋值语句z = f(x)首先通过调用函数f并传入x绑定的值来计算表达式f(x)。当进入f时,创建了一个栈帧,如第 2 列所示。栈帧中的名称有x(形式参数x,而不是调用上下文中的x)、gh。变量gh绑定到类型为function的对象。这些函数的属性由f内部的函数定义给出。

当从f内部调用h时,又创建了一个栈帧,如第 3 列所示。此帧仅包含局部变量z。为什么它不包含x呢?只有当名称是函数的形式参数或绑定到函数体内对象的变量时,才会将名称添加到与函数关联的作用域中。在h的主体内,x仅出现在赋值语句的右侧。一个名称(在这种情况下是x)出现在函数体(在这种情况下是h)中但未绑定到任何对象,导致解释器搜索与函数定义相关的作用域的栈帧(与f相关的栈帧)。如果找到了名称(在这种情况下找到了),则使用其绑定的值(4)。如果没有找到,则会产生错误信息。

h返回时,与h调用相关的栈帧消失(它从栈顶弹出),如第 4 列所示。请注意,我们从不移除栈中间的帧,而只移除最新添加的帧。由于这种“后进先出”(LIFO)的行为,我们称其为。(想象一下煎一叠煎饼。当第一个煎饼从*底锅上取下时,厨师将其放在一个餐盘上。随着每个后续煎饼从*底锅上取下,它被叠放在已经在餐盘上的煎饼上。当要吃煎饼时,首先上桌的煎饼是叠在最上面的,即最后一个放入的煎饼——这使得倒数第二个放入的煎饼成为新的顶部煎饼,接下来要上桌的煎饼。)

c4-fig-5001.jpg

回到我们的 Python 示例,g现在被调用,并添加一个包含g的局部变量x的栈帧(列 5)。当g返回时,该帧被弹出(列 6)。当f返回时,包含与f相关名称的栈帧被弹出,使我们回到原始栈帧(列7)。

请注意,当f返回时,尽管变量g不再存在,曾经与该名称绑定的function类型的对象仍然存在。这是因为函数是对象,可以像其他任何类型的对象一样被返回。因此,z可以绑定到f返回的值,函数调用z()可以用来调用在f中与名称g绑定的函数——即使名称gf的外部上下文中是未知的。

那么,图 4-5 中的代码打印了什么?它打印了

x = 4
z = 4
x = abc
x = 4
x = 3
z = <function f.<locals>.g at 0x1092a7510>
x = abc

对名称的引用顺序并不重要。如果在函数体内的任何地方将一个对象绑定到一个名称(即使它出现在赋值的左侧之前的表达式中),它被视为该函数的局部。³⁰ 请考虑以下代码

def f():
   print(x)
def g():
   print(x)
   x = 1
x = 3
f()
x = 3
g()

当调用f时,它打印3,但错误信息

UnboundLocalError: local variable 'x' referenced before assignment

当遇到g中的打印语句时会打印出来。这是因为print语句后面的赋值语句使得x变成了g的局部变量。而且因为xg的局部变量,所以在执行print语句时没有值。

还感到困惑吗?大多数人需要一点时间来理解作用域规则。不要让这困扰你。现在,继续前进,开始使用函数。大多数时候,你只想使用局部于函数的变量,而作用域的细微差别将无关紧要。事实上,如果你的程序依赖于一些微妙的作用域规则,你可能会考虑重写以避免这样做。

4.2 规范

一个规范定义了函数的实现者与将编写使用该函数的程序的人之间的契约。我们将函数的用户称为其客户。这个契约可以被视为包含两个部分:

  • 假设:这些描述了函数的客户必须满足的条件。通常,它们描述了对实际参数的约束。几乎总是,它们指定了每个参数的可接受类型集,并且不时对一个或多个参数的值施加一些约束。例如,find_root的规范可能要求power为正整数。

  • 保证:这些描述了在满足假设的情况下,函数必须满足的条件。例如,find_root的规范可能保证,如果被要求找到不存在的根(例如,负数的*方根),它将返回None

函数是一种创建计算元素的方法,我们可以将其视为原始元素。它们提供了分解和抽象。

分解创造结构。它允许我们将程序分解为合理自-contained 的部分,并可以在不同环境中重复使用。

抽象隐藏了细节。它允许我们像使用一个黑箱一样使用一段代码——即,我们看不到、也不需要看、甚至不应该想要看其内部细节。³¹ 抽象的本质是在特定上下文中保留相关信息,并遗忘该上下文中不相关的信息。在编程中有效使用抽象的关键是找到一个适合抽象构建者和潜在抽象客户端的相关性概念。这就是编程的真正艺术。

抽象就是关于遗忘的。有很多方法可以对其建模,例如,大多数青少年的听觉系统。

青少年说:我今晚可以借车吗?

父母说:是的,但要在午夜之前回来,并确保油箱是满的。

青少年听到:是的

青少年忽略了所有他或她认为无关紧要的琐碎细节。抽象是一个多对一的过程。如果父母说“是的,但要在凌晨 2 点之前回来,并确保车子干净”,这也会被抽象为“是的”。

通过类比,想象一下你被要求制作一个包含 25 节课的计算机科学入门课程。一种方法是招募 25 位教授,让每位教授准备一个小时的讲座,讲述他们最喜欢的话题。尽管你可能会得到 25 个精彩的小时,但整个课程可能会让人感觉像是皮兰德罗的寻找作者的六个角色(或你参加的那门有 15 位客座讲师的政治科学课程)。如果每位教授独立工作,他们就无法将自己讲座中的材料与其他讲座中的材料联系起来。

不知怎么的,你需要让每个人知道其他人正在做什么,而不产生太多的工作让没人愿意参与。这就是抽象的作用。你可以写 25 个规格,每个规格说明学生在每节课上应该学习什么材料,但不提供关于如何教授这些材料的任何细节。你得到的可能在教育上并不完美,但至少可能是有意义的。

这是组织使用程序员团队完成任务的方式。给定一个模块的规格,程序员可以在实现该模块时无需担心团队中其他程序员在做什么。此外,其他程序员可以利用该规格开始编写使用该模块的代码,而不必担心模块将如何实现。

图 4-7 为 图 4-3 中的 find_root 实现添加了一个规范。

c4-fig-0007.jpg

图 4-7 带有规范的函数定义。

三重引号之间的文本在 Python 中称为 docstring。按照惯例,Python 程序员使用 docstring 提供函数的规范。这些 docstring 可以使用内置函数 **help** 进行访问。

Python IDEs 的一个优点是它们提供了一个交互式工具,用于查询内置对象。如果你想知道某个特定函数的功能,只需在控制台窗口中输入 help(*object*)。例如,输入 help(abs) 会产生以下文本。

Help on built-in function abs in module builtins:

abs(x, /)
    Return the absolute value of the argument.

这告诉我们 abs 是一个将单个参数映射到其绝对值的函数。(参数列表中的 / 意味着该参数必须是位置参数。)如果你输入 help(),会启动一个交互式帮助会话,解释器会在控制台窗口中显示提示符 help>。交互模式的一个优点是,你可以获得有关不是对象的 Python 构造的帮助。例如,

help> if
The "if" statement
******************

The "if" statement is used for conditional execution:

   if_stmt ::= "if" expression ":" suite
               ("elif" expression ":" suite)*
               ["else" ":" suite]

It selects exactly one of the suites by evaluating the expressions one
by one until one is found to be true (see section Boolean operations
for the definition of true and false); then that suite is executed
(and no other part of the "if" statement is executed or evaluated).
If all expressions are false, the suite of the "else" clause, if
present, is executed.

Related help topics: TRUTHVALUE

可以通过输入 quit 退出交互帮助。

如果 图 4-4 中的代码已加载到 IDE 中,在 shell 中输入 help(find_root) 将显示

find_root(x, power, epsilon)
    Assumes x and epsilon int or float, power an int,
        epsilon > 0 & power >= 1
    Returns float y such that y**power is within epsilon of x.
        If such a float does not exist, it returns None

find_root 的规范是所有可能满足该规范的实现的抽象。find_root 的客户端可以假设实现符合规范,但不应假设其他内容。例如,客户端可以假设调用 find_root(4, 2, 0.01) 返回的某个值的*方在 3.994.01 之间。返回的值可以是正数或负数,即使 4 是一个完全*方,返回的值也可能不是 2-2。关键是,如果未满足规范的假设,则无法对调用函数的效果做出任何假设。例如,调用 find_root(8, 3, 0) 可能返回 2,但也可能崩溃、无限运行,或者返回与 8 的立方根相去甚远的某个数字。

指尖练习: 使用图 3-6 的算法,编写一个满足规范的函数。

def log(x, base, epsilon):
    """Assumes x and epsilon int or float, base an int,
           x > 1, epsilon > 0 & power >= 1
       Returns float y such that base**y is within epsilon of x."""

4.3 使用函数模块化代码。

到目前为止,我们实现的所有函数都很小,适合单页展示。随着我们实现更复杂的功能,将函数拆分为多个执行单一简单任务的函数是很方便的。为了说明这个想法,我们稍微多余地将 find_root 拆分为三个独立的函数,如 图 4-8 所示。每个函数都有自己的规范,并且每个函数作为独立实体是有意义的。函数 find_root_bounds 查找根必须位于的区间,bisection_solve 使用二分搜索在此区间中寻找根的*似值,而 find_root 则简单地调用其他两个函数并返回根。

这个版本的 find_root 比原始单体实现更容易理解吗?可能不是。一个好的经验法则是,如果一个函数可以舒适地放在单页上,它可能不需要被细分以便于理解。

c4-fig-0008.jpg

图 4-8 将 find_root 拆分为多个函数

4.4 函数作为对象

在 Python 中,函数是 一等对象。这意味着它们可以像其他类型的对象一样被处理,例如 intlist。它们有类型,例如,表达式 type(abs) 的值为 <type 'built-in_function_or_method'>;它们可以出现在表达式中,例如,作为赋值语句的右侧或作为函数的参数;它们可以由函数返回;等等。

使用函数作为参数允许一种称为 高阶编程 的编码风格。它使我们能够编写更具通用性的函数。例如,图 4-8 中的 bisection_solve 函数可以重写,以便应用于根以外的任务,如 图 4-9 所示。

c4-fig-0009.jpg

图 4-9 概括 bisection_solve

我们开始将整数参数 power 替换为一个函数 eval_ans,该函数将浮点数映射到浮点数。然后,我们将表达式 ans**power 的每个实例替换为函数调用 eval_ans(ans)

如果我们想使用新的 bisection_solve 打印出 99 的*方根*似值,我们可以运行代码

def square(ans):
    return ans**2
low, high = find_root_bounds(99, 2)
print(bisection_solve(99, square, 0.01, low, high)) 

为了简单地定义一个函数来计算*方,似乎有些多余。幸运的是,Python 支持使用保留字 lambda 创建匿名函数(即不绑定名称的函数)。lambda 表达式的一般形式是

lambda *sequence of variable names* : *expression*

例如,lambda 表达式 lambda x, y: x*y 返回一个返回其两个参数乘积的函数。lambda 表达式常作为高阶函数的参数使用。比如,我们可以用

print(bisection_solve(99, lambda ans: ans**2, 0.01, low, high)) 

练习: 编写一个具有两个数值参数的 lambda 表达式。如果第二个参数为零,它应该返回 None。否则,它应该返回第一个参数除以第二个参数的值。提示:使用条件表达式。

由于函数是一等对象,它们可以在函数内创建和返回。例如,给定函数定义

def create_eval_ans():
    power = input('Enter a positive integer: ')
    return lambda ans: ans**int(power)

代码

eval_ans = create_eval_ans()
print(bisection_solve(99, eval_ans, 0.01, low, high))

将打印 99 的 n^(th) 根的*似值,其中 n 是用户输入的数字。

我们对bisection_solve的概括意味着它现在不仅可以用来搜索根的逼*值,还可以搜索任何将浮点数映射到浮点数的单调³²函数的逼*值。例如,图 4-10 中的代码使用bisection_solve找到对数的逼*值。

c4-fig-0010.jpg

图 4-10 使用bisection_solve来逼*对数

请注意,log的实现包含了一个局部函数find_log_bounds的定义。这个函数本可以在log之外定义,但由于我们不期望在其他上下文中使用它,因此不这样做似乎更好。

4.5 方法,简单化

方法是类似函数的对象。它们可以带参数调用,可以返回值,并且可以具有副作用。它们在一些重要方面与函数有所不同,我们将在第十章中讨论。

目前,可以将方法视为为函数调用提供一种特殊语法。我们使用点表示法将第一个参数放在函数名称之前,而不是将其放在括号内。我们在这里引入方法,因为许多内置类型上的有用操作都是方法,因此通过点表示法调用。例如,如果s是一个字符串,可以使用find方法找到子字符串在s中第一次出现的索引。因此,如果s'abcbc',调用s.find('bc')将返回1。尝试将find视为函数,例如调用find(s,'bc'),会产生错误信息NameError: name 'find' is not defined

指尖练习: 如果subs中不存在,s.find(sub)会返回什么?

指尖练习: 使用find实现一个满足规范的函数

def find_last(s, sub):
    """s and sub are non-empty strings
       Returns the index of the last occurrence of sub in s.
       Returns None if sub does not occur in s""" 

4.6 在章节中引入的术语

  • 函数定义

  • 形式参数

  • 实际参数

  • 参数

  • 函数调用

  • 返回语句

  • 执行点

  • lambda 抽象

  • 测试函数

  • 调试

  • 位置参数

  • 关键字参数

  • 默认参数值

  • 解包运算符 (*)

  • 名称空间

  • 范围

  • 局部变量

  • 符号表

  • 栈帧

  • 静态(词法)作用域

  • 栈(后进先出)

  • 规范

  • 客户端

  • 假设

  • 保证

  • 分解

  • 抽象

  • 文档字符串

  • 帮助函数

  • 一等对象

  • 高阶编程

  • lambda 表达式

  • 方法

  • 点表示法

第五章:结构化类型与可变性

到目前为止,我们所查看的程序处理了三种类型的对象:intfloatstr。数值类型intfloat是标量类型。也就是说,这些类型的对象没有可访问的内部结构。相比之下,str可以被认为是结构化或非标量类型。我们可以使用索引提取字符串中的单个字符,并使用切片提取子字符串。

在本章中,我们介绍四种额外的结构化类型。其中,tuple是对str的简单概括。其他三种——listrangedict——则更有趣。我们还将回到高阶编程的话题,通过一些示例说明能够以与其他类型的对象相同的方式对待函数的实用性。

5.1 元组

像字符串一样,元组是不可变的有序元素序列。不同之处在于,元组的元素不一定是字符。单个元素可以是任何类型,且不必彼此具有相同类型。

类型为tuple的字面量是通过将用逗号分隔的元素列表括在圆括号中来书写的。例如,我们可以写

t1 = ()
t2 = (1, ‘two', 3)
print(t1)
print(t2)

不出所料,print语句产生的输出是

()
(1, ‘two', 3)

看这个例子,你可能会认为包含单一值1的元组应该写作(1)。但是,引用 H.R. Haldeman 引用理查德·尼克松的话,“这样做是错误的。” ³³ 由于圆括号用于分组表达式,(1)仅仅是写整数1的一种冗长方式。为了表示包含此值的单例元组,我们写作(1,)。几乎所有使用 Python 的人都有过一次或多次不小心省略那个恼人的逗号的经历。

可以对元组使用重复操作。例如,表达式3*('a', 2)的值为('a', 2, 'a', 2, 'a', 2)

像字符串一样,元组可以连接、索引和切片。考虑一下

t1 = (1, ‘two', 3)
t2 = (t1, 3.25)
print(t2)
print((t1 + t2))
print((t1 + t2)[3])
print((t1 + t2)[2:5]) 

第二个赋值语句将名称t2绑定到一个包含t1所绑定的元组和浮点数3.25的元组。这是可能的,因为元组和 Python 中的其他一切一样,都是一个对象,所以元组可以包含元组。因此,第一个print语句产生的输出是,

((1, ‘two', 3), 3.25)

第二个print语句打印由绑定到t1t2的值连接生成的值,这是一个包含五个元素的元组。它产生的输出是

(1, ‘two', 3, (1, ‘two', 3), 3.25)

下一个语句选择并打印连接后的元组的第四个元素(与往常一样,在 Python 中,索引从0开始),紧接着的语句创建并打印该元组的切片,产生的输出是

(1, ‘two', 3)
(3, (1, ‘two', 3), 3.25)

可以使用for语句遍历元组的元素。并且可以使用in运算符测试一个元组是否包含特定值。例如,以下代码

def intersect(t1, t2):
    """Assumes t1 and t2 are tuples
       Returns a tuple containing elements that are in
          both t1 and t2"""
    result = ()
    for e in t1:
        if e in t2:
            result += (e,)
    return result
print(intersect((1, 'a', 2), ('b', 2, 'a')))

打印('a', 2)

5.1.1 多重赋值

如果你知道序列的长度(例如,元组或字符串),使用 Python 的多重赋值语句提取单独元素会很方便。例如,语句x, y = (3, 4)会将x绑定为3,将y绑定为4。类似地,语句a, b, c = 'xyz'会将a绑定为'x'b绑定为'y'c绑定为'z'

当与返回多个值的函数一起使用时,这种机制特别方便。考虑函数定义

 def find_extreme_divisors(n1, n2):
    """Assumes that n1 and n2 are positive ints
        Returns a tuple containing the smallest common divisor > 1 and 
          the largest common divisor of n1 & n2\. If no common divisor,
          other than 1, returns (None, None)"""
    min_val, max_val = None, None
    for i in range(2, min(n1, n2) + 1):
        if n1%i == 0 and n2%i == 0:
            if min_val == None:
                min_val = i
            max_val = i
    return min_val, max_val

多重赋值语句

min_divisor, max_divisor = find_extreme_divisors(100, 200)

min_divisor绑定为2,将max_divisor绑定为200

5.2 范围与可迭代对象

如第 2.6 节所述,函数range生成range类型的对象。与字符串和元组一样,range类型的对象是不可变的。所有元组上的操作也适用于范围,除了连接和重复。例如,range(10)[2:6][2]的结果是4。当使用==运算符比较range类型的对象时,如果两个范围表示相同的整数序列,则返回True。例如,range(0, 7, 2) == range(0, 8, 2)的结果为True。然而,range(0, 7, 2) == range(6, -1, -2)的结果为False,因为虽然两个范围包含相同的整数,但它们的顺序不同。

tuple类型的对象不同,range类型的对象所占用的空间与其长度无关。因为一个范围完全由其起始、结束和步长值定义,所以它可以存储在较小的空间中。

range最常见的用法是在for循环中,但range类型的对象可以在任何可以使用整数序列的地方使用。

在 Python 3 中,range可迭代 对象的特例。所有可迭代类型都有一个方法,³⁴__iter__,返回类型迭代器的对象。然后,可以在for循环中使用该迭代器逐个返回对象序列。例如,元组是可迭代的,for语句

for elem in (1, 'a', 2, (3, 4)):

创建一个迭代器,逐个返回元组的元素。Python 有许多内置的可迭代类型,包括字符串、列表和字典。

许多有用的内置函数可以作用于可迭代对象。其中一些更有用的包括summinmax。函数sum可以应用于数字的可迭代对象,返回元素的总和。函数maxmin可以应用于有明确定义的元素顺序的可迭代对象。

指尖练习: 写一个表达式,计算一个数字元组的均值。使用函数sum

5.3 列表与可变性

像元组一样,列表是值的有序序列,每个值由索引标识。list类型的文字表达语法类似于元组;不同之处在于我们使用方括号而不是圆括号。空列表写作[],单例列表则写作不带(很容易忘记的)逗号的形式。

由于列表是可迭代的,我们可以使用for语句来迭代列表中的元素。因此,例如,代码

L = ['I did it all', 4, 'love']
for e in L:
    print(e)

产生的输出,

I did it all
4
Love

我们也可以像对元组一样对列表进行索引和切片。例如,代码

`L1 = [1, 2, 3] L2 = L1[-1::-1] for i in range(len(L1)):     print(L1[i]*L2[i])`

打印

3
4
3

用方括号执行三种不同的用途(list类型的文字、对可迭代对象的索引和对可迭代对象的切片)可能导致视觉上的混淆。例如,表达式[1,2,3,4][1:3][1],它的值为3,在三种方式中使用了方括号。这在实践中很少成为问题,因为大多数时候,列表是逐步构建的,而不是作为文字书写的。

列表与元组在一个重要方面不同:列表是可变的。相反,元组和字符串是不可变的。许多运算符可用于创建不可变类型的对象,并且变量可以绑定到这些类型的对象。但不可变类型的对象在创建后无法被修改。另一方面,可变类型的对象在创建后可以被修改。

修改对象和将对象赋值给变量之间的区别在开始时可能看起来微妙。然而,如果你不断重复这个咒语:“在 Python 中,变量仅仅是一个名称,即可以附加到对象上的标签,”这会带来清晰的理解。或许下面的一组示例也会有所帮助。

当这些语句

Techs = ['MIT', 'Caltech'] 
Ivys = ['Harvard', 'Yale', 'Brown']

当这些语句执行时,解释器创建两个新列表并将适当的变量绑定到它们,如图 5-1 所示。

c5-fig-0001.jpg

图 5-1 两个列表

赋值语句

Univs = [Techs, Ivys]
Univs1 = [['MIT', 'Caltech'], ['Harvard', 'Yale', 'Brown']]

还可以创建新列表并将变量绑定到它们。这些列表的元素本身也是列表。三个打印语句

print('Univs =', Univs)
print('Univs1 =', Univs1)
print(Univs == Univs1)

产生的输出

Univs = [['MIT', 'Caltech'], ['Harvard', 'Yale', 'Brown']]
Univs1 = [['MIT', 'Caltech'], ['Harvard', 'Yale', 'Brown']]
True

看起来好像UnivsUnivs1绑定到相同的值。但表象可能会欺骗。如图 5-2 所示,UnivsUnivs1绑定到完全不同的值。

c5-fig-0002.jpg

图 5-2 两个看似具有相同值但实际上不相同的列表

UnivsUnivs1绑定到不同对象可以通过内置 Python 函数**id**验证,该函数返回对象的唯一整数标识符。这个函数使我们能够通过比较它们的 id 来测试对象相等性。测试对象相等性的更简单方法是使用**is**运算符。当我们运行代码时

print(Univs == Univs1) #test value equality
print(id(Univs) == id(Univs1)) #test object equality
print(Univs is Univs1) #test object equality
print('Id of Univs =', id(Univs))
print('Id of Univs1 =', id(Univs1))

打印

True
False
False
Id of Univs = 4946827936
Id of Univs1 = 4946612464

(如果你运行这段代码,不要指望看到相同的唯一标识符。Python 的语义并未说明每个对象关联的标识符是什么;它仅要求没有两个对象具有相同的标识符。)

请注意,在图 5-2 中,Univs的元素不是与TechsIvys绑定的列表的副本,而是这些列表本身。Univs1的元素是包含与Univs中列表相同元素的列表,但它们不是相同的列表。我们可以通过运行代码来看到这一点

print('Ids of Univs[0] and Univs[1]', id(Univs[0]), id(Univs[1]))
print('Ids of Univs1[0] and Univs1[1]', id(Univs1[0]), id(Univs1[1]))

其打印结果为

Ids of Univs[0] and Univs[1] 4447807688 4456134664
Ids of Univs1[0] and Univs1[1] 4447805768 4447806728

为什么值与对象相等之间的差异会引起如此大的关注?这很重要,因为列表是可变的。考虑以下代码

Techs.append('RPI')

列表的append方法有一个副作用。它不是创建一个新列表,而是通过在现有列表Techs的末尾添加一个新元素(本例中是字符串'RPI')来修改现有列表。图 5-3 描绘了执行append后计算的状态。

c5-fig-0003.jpg

图 5-3 可变性的演示

Univs绑定的对象仍然包含相同的两个列表,但其中一个列表的内容已被更改。因此,print语句

print('Univs =', Univs)
print('Univs1 =', Univs1)

现在生成的输出是

Univs = [['MIT', 'Caltech', 'RPI'], ['Harvard', 'Yale', 'Brown']]
Univs1 = [['MIT', 'Caltech'], ['Harvard', 'Yale', 'Brown']]

我们在这里看到的叫做别名。有两条不同的路径指向同一个列表对象。一条路径通过变量Techs,另一条通过与Univs绑定的list对象的第一个元素。我们可以通过任一条路径修改对象,并且修改的效果将通过两条路径可见。这可能很方便,但也可能是危险的。无意中的别名会导致编程错误,这些错误往往非常难以追踪。例如,你认为以下代码打印什么

L1 = [[]]*2
L2 = [[], []]
for i in range(len(L1)):
    L1[i].append(i)
    L2[i].append(i)
print('L1 =', L1, 'but', 'L2 =', L2)

它打印出L1 = [[0, 1], [0, 1]]L2 = [[0], [1]]。为什么?因为第一个赋值语句创建了一个包含两个元素的列表,而这两个元素是同一个对象,而第二个赋值语句创建了一个包含两个不同对象的列表,这两个对象最初都是空列表。

手指练习: 以下代码打印什么?

`L = [1, 2, 3] L.append(L) print(L is L[-1])`

别名和可变性与默认参数值的相互作用是需要注意的事项。考虑以下代码

def append_val(val, list_1 = []):
    List_1.append(val)
    print(list_1)

append_val(3)
append_val(4)

你可能认为第二次调用append_val会打印出列表[4],因为它会将4添加到空列表中。实际上,它将打印[3, 4]。这是因为在函数定义时,创建了一个新类型为list的对象,初始值为空列表。每次调用append_val而不提供正式参数list_1的值时,函数定义时创建的对象会绑定到list_1,被修改,然后被打印。因此,第二次调用append_val会修改并打印已经被第一次调用该函数修改过的列表。

当我们将一个列表附加到另一个列表时,例如Techs.append(Ivys),原始结构保持不变。结果是一个包含列表的列表。假设我们不想保持这种结构,而是想将一个列表的元素添加到另一个列表中。我们可以使用列表连接(使用+运算符)或extend方法来做到这一点,例如,

L1 = [1,2,3]
L2 = [4,5,6]
L3 = L1 + L2
print('L3 =', L3)
L1.extend(L2)
print('L1 =', L1)
L1.append(L2)
print('L1 =', L1)

将打印

L3 = [1, 2, 3, 4, 5, 6]
L1 = [1, 2, 3, 4, 5, 6]
L1 = [1, 2, 3, 4, 5, 6, [4, 5, 6]]

请注意,运算符+没有副作用。它创建一个新列表并返回它。相反,extendappend都会改变L1

图 5-4 简要描述了一些与列表相关的方法。请注意,除了countindex,所有这些方法都会改变列表。

c5-fig-0004.jpg

图 5-4 与列表相关的常见方法

5.3.1 克隆

通常,避免在迭代时改变一个列表是明智的。考虑以下代码

def remove_dups(L1, L2):
    """Assumes that L1 and L2 are lists.
       Removes any element from L1 that also occurs in L2"""
    for e1 in L1:
        if e1 in L2:
            L1.remove(e1)
L1 = [1,2,3,4]
L2 = [1,2,5,6]
Remove_dups(L1, L2)
print('L1 =', L1)

你可能会惊讶地发现这会打印

L1 = [2, 3, 4]

for循环期间,Python 使用一个内部计数器跟踪当前在列表中的位置,该计数器在每次迭代结束时递增。当计数器的值达到列表的当前长度时,循环终止。如果在循环中列表没有被改变,这样的行为是可以预期的,但如果列表被改变,可能会有意想不到的后果。在这种情况下,隐藏的计数器初始值为0,发现L1[0]L2中,并将其移除——将L1的长度减少到3。计数器接着递增到1,代码继续检查L1[1]的值是否在L2中。请注意,这不是L1[1]的原始值(即2),而是L1[1]的当前值(即3)。正如你所看到的,弄清楚在循环中修改列表时发生了什么是可能的,但并不容易。而且,发生的事情可能是无意的,就像这个例子一样。

避免这种问题的一种方法是使用切片来克隆³⁵(即,创建一个副本)列表,并写作for e1 in L1[:]。请注意,写作

new_L1 = L1
for e1 in new_L1:

并不会解决问题。它不会创建L1的副本,而仅仅为现有列表引入一个新名称。

切片并不是在 Python 中克隆列表的唯一方法。表达式L.copy()L[:]的值相同。切片和copy都执行所谓的浅拷贝。浅拷贝创建一个新列表,然后将要复制列表的对象(而不是对象的副本)插入到新列表中。代码

L = [2]
L1 = [L]
L2 = L1[:]
L2 = copy.deepcopy(L1)
L.append(3)
print(f'L1 = {L1}, L2 = {L2}')

打印L1 = [[2, 3]] L2 = [[2, 3]],因为L1L2都包含在第一条赋值语句中绑定到L的对象。

如果要复制的列表包含可变对象且你也希望复制它们,请导入标准库模块copy并使用copy.deepcopy函数进行深复制。方法deepcopy创建一个新列表,然后将待复制列表中的对象复制到新列表中。如果我们将上述代码的第三行替换为L2 = copy.deepcopy(L1),它将打印L1 = [[2, 3]], L2 = [[2]],因为L1不会包含L所绑定的对象。

如果列表的元素是包含列表(或任何可变类型)的列表,理解copy.deepcopy会比较棘手。考虑

L1 = [2]
L2 = [[L1]]
L3 = copy.deepcopy(L2)
L1.append(3)

L3的值将为[[[2]]],因为copy.deepcopy不仅为列表[L1]创建新对象,也为列表L1创建新对象。也就是说,它会复制到最底层——大多数情况下。为什么是“大多数情况下”?代码

L1 = [2]
L1.append(L1)

创建一个包含自身的列表。尝试复制到最底层将永远不会终止。为了解决这个问题,copy.deepcopy对每个对象仅制作一次副本,然后在每个对象实例中使用该副本。即使在列表不包含自身的情况下,这也很重要。例如,

L1 = [2]
L2 = [L1, L1]
L3 = copy.deepcopy(L2)
L3[0].append(3)
print(L3)

打印[[2, 3], [2, 3]],因为copy.deepcopyL1进行了一次复制,并在L2中两次使用。

5.3.2 列表推导式

列表推导式提供了一种简洁的方式,通过迭代可迭代对象的值来应用操作。它创建一个新列表,其中每个元素是对可迭代对象中的某个值(例如,另一个列表的元素)应用给定操作的结果。其表达形式为

 [*expr* for *elem* in *iterable* if *test*]

评估该表达式等同于调用函数

def f(expr, old_list, test = lambda x: True):
    new_list = []
    for e in iterable:
        if test(e):
            new_list.append(expr(e))
    return new_list

例如,[e**2 for e in range(6)]评估为[0, 1, 4, 9, 16, 25][e**2 for e in range(8) if e%2 == 0]评估为[0, 4, 16, 36][x**2 for x in [2, 'a', 3, 4.0] if type(x) == int]评估为[4, 9]

列表推导式提供了一种方便的方式来初始化列表。例如,[[] for _ in range(10)]生成一个包含 10 个不同(即非别名)空列表的列表。变量名_表示该变量的值未在生成列表的元素中使用,即它仅是一个占位符。这种约定在 Python 程序中很常见。

Python 允许在列表推导式中使用多个for语句。考虑以下代码

L = [(x, y)
     for x in range(6) if x%2 == 0
     for y in range(6) if y%3 == 0]

Python 解释器首先评估第一个for,将值序列0,2,4赋给x。对于这三个x的值,它会评估第二个for(每次生成值序列0,3)。然后它将元组(x, y)添加到正在生成的列表中,从而生成列表

[(0, 0), (0, 3), (2, 0), (2, 3), (4, 0), (4, 3)]

当然,我们可以不使用列表推导式生成相同的列表,但代码会显得相对不够简洁:

L = []
for x in range(6):
    if x%2 == 0:
        for y in range(6):
            if y%3 == 0:
                L.append((x, y))

以下代码是将列表推导式嵌套在另一个列表推导式中的示例。

print([[(x,y) for x in range(6) if x%2 == 0]
     for y in range(6) if y%3 == 0])

它打印[[ (0, 0), (2, 0), (4, 0)], [(0, 3), (2, 3), (4, 3)]]

熟悉嵌套列表推导需要实践,但它们可以非常有用。让我们使用嵌套列表推导生成一个小于 100 的所有质数的列表。基本思想是使用一个推导生成所有候选数字的列表(即 2 到 99),第二个推导生成将候选质数除以每个潜在除数的余数的列表,以及使用内置函数all来测试这些余数中是否有任何为 0。

[x for x in range(2, 100) if all(x % y != 0  for y in range(3, x))]

评估表达式等同于调用该函数

def gen_primes():
    primes = []
    for x in range(2, 100):
        is_prime = True
        for y in range(3, x):
            if x%y == 0:
                is_prime = False
        if is_prime:
            primes.append(x)
    return primes

手指练习:编写一个列表推导,生成 2 到 100 之间的所有非质数。

一些 Python 程序员以奇妙而微妙的方式使用列表推导。这并不总是一个好主意。请记住,其他人可能需要阅读你的代码,而“微妙”通常不是程序的理想属性。

5.4 列表的高阶操作

在第 4.4 节中,我们介绍了高阶编程的概念。对于列表来说,它特别方便,如图 5-5 所示。

c5-fig-0005.jpg

图 5-5 对列表元素应用函数

函数apply_to_each被称为高阶,因为它的参数本身就是一个函数。第一次调用时,它通过对每个元素应用一元内置函数abs来修改L。第二次调用时,它对每个元素进行类型转换。第三次调用时,它通过应用使用lambda定义的函数来替换每个元素。它打印

L = [1, -2, 3.33]
Apply abs to each element of L.
L = [1, 2, 3.33]
Apply int to each element of [1, 2, 3.33].
L = [1, 2, 3]
Apply squaring to each element of [1, 2, 3].
L = [1, 4, 9]

Python 有一个内置的高阶函数map,它类似于但比图 5-5 中定义的apply_to_each函数更通用。在它最简单的形式中,map的第一个参数是一个一元函数(即只有一个参数的函数),第二个参数是适合作为第一个参数的值的任何有序集合。它通常用作列表推导的替代。例如,list(map(str, range(10)))等价于[str(e) for e in range(10)]

map函数通常与for循环一起使用。在for循环中使用时,map的行为类似于range函数,它为循环的每次迭代返回一个值。这些值是通过将第一个参数应用于第二个参数的每个元素生成的。例如,代码

`for i in map(lambda x: x**2, [2, 6, 4]):     print(i)` 

打印

4
36
16

更一般地说,map的第一个参数可以是一个有n个参数的函数,此时它必须后跟n个后续有序集合(每个集合长度相同)。例如,代码

L1 = [1, 28, 36]
L2 = [2, 57, 9]
for i in map(min, L1, L2):
    print(i)

打印

1
28
9

手指练习:实现一个满足以下规范的函数。提示:在实现的主体中使用lambda会很方便。

def f(L1, L2):
    """L1, L2 lists of same length of numbers
    returns the sum of raising each element in L1
    to the power of the element at the same index in L2
    For example, f([1,2], [2,3]) returns 9"""

5.5 字符串、元组、范围和列表

我们已经看过四种可迭代的序列类型:strtuplerangelist。它们相似之处在于这些类型的对象可以按照图 5-6 中描述的方式进行操作。它们的其他一些相似性和差异在图 5-7 中总结。

c5-fig-0006.jpg

图 5-6 序列类型的常见操作

c5-fig-0007.jpg

图 5-7 序列类型的比较

Python 程序员往往比使用元组更频繁地使用列表。由于列表是可变的,因此可以在计算过程中逐步构建。例如,以下代码逐步构建一个包含另一个列表中所有偶数的列表。

even_elems = []
for e in L:
    if e%2 == 0:
        even_elems.append(e)

由于字符串只能包含字符,因此它们的通用性远不如元组或列表。另一方面,当你处理字符字符串时,有许多有用的内置方法。图 5-8 包含了其中一些方法的简短描述。请记住,由于字符串是不可变的,这些方法都返回值,并且没有副作用。

c5-fig-0008.jpg

图 5-8 字符串上的一些方法

一个更有用的内置方法是 split,它接受两个字符串作为参数。第二个参数指定一个分隔符,用于将第一个参数拆分为一系列子字符串。例如,

print('My favorite professor–John G.–rocks'.split(' '))
print('My favorite professor–John G.–rocks'.split('-'))
print('My favorite professor–John G.–rocks'.split('–'))

打印

['My', 'favorite', 'professor–John', 'G.–rocks']
['My favorite professor', '', 'John G.', '', 'rocks']
['My favorite professor', 'John G.', 'rocks']

第二个参数是可选的。如果省略该参数,第一个字符串将使用任意的空白字符(空格、制表符、换行符、回车和换页)进行分割。³⁶

5.6 集合

集合是另一种集合类型。它们类似于数学中的集合概念,因为它们是无序的独特元素集合。它们用程序员称之为大括号、数学家称之为集合括号的符号表示,例如,

`baseball_teams = {'Dodgers', 'Giants', 'Padres', 'Rockies'} football_teams = {'Giants', 'Eagles', 'Cardinals', 'Cowboys'}`

由于集合的元素是无序的,尝试索引集合,例如,评估 baseball_teams[0] 会产生运行时错误。我们可以使用 for 语句来迭代集合的元素,但与我们看到的其他集合类型不同,元素产生的顺序是未定义的。

像列表一样,集合是可变的。我们使用 add 方法向集合中添加单个元素。通过将元素集合(例如列表)传递给 update 方法来向集合中添加多个元素。例如,代码

baseball_teams.add('Yankees')
football_teams.update(['Patriots', 'Jets'])
print(baseball_teams)
print(football_teams)

打印

{'Dodgers', 'Yankees', 'Padres', 'Rockies', 'Giants'}
{'Jets', 'Eagles', 'Patriots', 'Cowboys', 'Cardinals', 'Giants'}

(元素出现的顺序并不由语言定义,因此如果运行此示例,可能会得到不同的输出。)

可以使用 remove 方法从集合中移除元素,如果元素不在集合中则会引发错误,或者使用 discard 方法,如果元素不在集合中则不会引发错误。

可以使用 in 运算符测试一个对象是否属于集合。例如,'Rockies' in baseball_teams 返回 True。二元方法 unionintersectiondifferenceissubset 具有通常的数学含义。例如,

`print(baseball_teams.union({1, 2})) print(baseball_teams.intersection(football_teams)) print(baseball_teams.difference(football_teams)) print({'Padres', 'Yankees'}.issubset(baseball_teams))`

打印

{'Padres', 'Rockies', 1, 2, 'Giants', ‘Dodgers', 'Yankees'}
{'Giants'}
{'Padres', 'Rockies', ‘Dodgers', 'Yankees'}
True

集合的一个好处是许多方法都有方便的中缀运算符,包括 | 表示 union& 表示 intersect- 表示 difference<= 表示 subset,以及 >= 表示 superset。使用这些运算符可以使代码更易读。例如比较,

print(baseball_teams | {1, 2})
print(baseball_teams & football_teams)
print(baseball_teams - football_teams)
print({'Padres', 'Yankees'} <= baseball_teams)

与之前呈现的代码相同,它使用点表示法打印相同的值。

并非所有类型的对象都可以成为集合的元素。集合中的所有对象必须是可哈希的。一个对象是可哈希的,如果它有

  • 一个将对象映射到 int__hash__ 方法,并且 __hash__ 返回的值在对象生命周期内不会改变,并且

  • 一个用于比较它与其他对象相等的 __eq__ 方法。

Python 的标量不可变类型的所有对象都是可哈希的,而 Python 内置的可变类型的对象则不可哈希。一个非标量不可变类型的对象(例如,元组)是可哈希的,当且仅当它的所有元素都是可哈希的。

5.7 字典

**dict** 类型的对象(即字典的缩写)与列表类似,只不过我们使用而不是整数来索引它们。任何可哈希对象都可以用作键。可以将字典视为键/值对的集合。类型 dict 的字面量用大括号括起来,每个元素写为键后跟冒号再跟。例如,代码

month_numbers = {'Jan':1, 'Feb':2, 'Mar':3, 'Apr':4, 'May':5,
                1:'Jan', 2:'Feb', 3:'Mar', 4:'Apr', 5:'May'}
print(month_numbers)
print('The third month is ' + month_numbers[3])
dist = month_numbers['Apr'] - month_numbers['Jan']
print('Apr and Jan are', dist, 'months apart')

将打印

{'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May'}
The third month is Mar
Apr and Jan are 3 months apart

dict 中的条目不能使用索引访问。这就是为什么 month_numbers[1] 明确指向键为 1 的条目,而不是第二个条目。可以使用 in 运算符测试一个键是否在字典中定义。

像列表一样,字典是可变的。我们可以通过编写,例如,month_numbers['June'] = 6 来添加条目,或通过编写,例如,month_numbers['May'] = 'V'. 来更改条目。

字典是 Python 的一大亮点。它们大大减少了编写各种程序的难度。例如,在 图 5-9 中,我们使用字典编写了一个(相当糟糕的)程序来进行语言之间的翻译。

图中的代码打印

Je bois "good" rouge vin, et mange pain. 
I drink of wine red.

请记住,字典是可变的。因此,要注意副作用。例如,

FtoE['bois'] = 'wood'
print(translate('Je bois du vin rouge.', dicts, 'French to English'))

将打印

I wood of wine red.

c5-fig-0009.jpg

图 5-9 翻译文本(糟糕地)

许多编程语言没有内置类型来提供从键到值的映射。相反,程序员使用其他类型来提供类似的功能。例如,使用列表实现字典相对简单,其中每个元素是表示键/值对的元组。然后我们可以编写一个简单的函数来进行关联检索,例如,

def key_search(L, k):
    for elem in L:
        if elem[0] == k:
            return elem[1]
    return None

这种实现的问题在于其计算效率低下。在最坏的情况下,程序可能必须检查列表中的每个元素才能执行一次检索。相比之下,内置实现速度很快。它使用了一种称为哈希的技术,如第十二章所述,能够在几乎不依赖字典大小的时间内进行查找。

有多种方法可以使用for语句遍历字典中的条目。如果d是一个字典,形式为for k in d的循环会遍历d的键。选择键的顺序是根据它们在字典中插入的顺序。³⁷ 例如,

capitals = {'France': 'Paris', 'Italy': 'Rome', 'Japan': 'Kyoto'}
for key in capitals:
    print('The capital of', key, 'is', capitals[key])

打印

The capital of France is Paris
The capital of Italy is Rome
The capital of Japan is Kyoto

要遍历字典中的值,我们可以使用values方法。例如,

cities = []
for val in capitals.values():
    cities.append(val)
print(cities, 'is a list of capital cities')

打印['巴黎', '罗马', '京都'] 是一个首都城市的列表

values方法返回类型为dict_values的对象。这是一个视图对象的示例。视图对象是动态的,如果与之关联的对象发生更改,变化将通过视图对象可见。例如,代码

cap_vals = capitals.values()
print(cap_vals)
capitals['Japan'] = ‘Tokyo'
print(cap_vals)

打印

dict_values(['Paris', 'Rome', 'Kyoto'])
dict_values(['Paris', 'Rome', ‘Toyko'])

同样,keys方法返回类型为dict_keys的视图对象。视图对象可以转换为列表,例如,list(capitals.values())返回一个包含首都值的列表。

要遍历键/值对,我们使用items方法。此方法返回类型为dict_items的视图对象。类型为dict_items的对象的每个元素都是一个包含键及其关联值的tuple。例如,代码

 for key, val in capitals.items():
    print(val, 'is the capital of', key)

打印

Paris is the capital of France
Rome is the capital of Italy
Tokyo is the capital of Japan

指尖练习: 实现一个满足规范的函数

 def get_min(d):
    """d a dict mapping letters to ints
       returns the value in d with the key that occurs first in the
       alphabet. E.g., if d = {x = 11, b = 12}, get_min returns 12."""

使用元组作为键通常很方便。想象一下,使用形式为(flight_number, day)的元组来表示航空公司航班。然后很容易将这些元组作为键用于实现航班与到达时间映射的字典。列表不能用作键,因为列表类型的对象是不可哈希的。

正如我们所看到的,字典有许多有用的方法,包括一些用于删除元素的方法。我们不会在这里列举所有这些方法,但在书中的示例中会方便地使用它们。图 5-10 包含一些更有用的字典操作。

c5-fig-0010.jpg

图 5-10 字典上的一些常见操作

5.8 字典推导式

字典推导式类似于列表推导式。一般形式是

{*key*: *value* for *id1*, *id2* in *iterable* if *test*}

关键区别(除了使用大括号而不是方括号)是它使用两个值来创建字典的每个元素,并允许(但不要求)可迭代对象同时返回两个值。考虑一个将某些十进制数字映射到英语单词的字典:

number_to_word = {1: 'one', 2: ‘two', 3: ‘three', 4: 'four', 10: ‘ten'}

我们可以轻松地使用字典推导式来生成一个将单词映射到数字的字典。

word_to_number = {w: d for d, w in number_to_word.items()}

如果我们决定只想在word_to_number中使用单个数字,我们可以使用推导式。

word_to_number = {w: d for d, w in number_to_word.items() if d < 10}

现在,让我们尝试一些更有挑战性的内容。密码是一种算法,它将明文(人类可以轻松阅读的文本)映射到密文。最简单的密码是替换密码,它用一个独特的字符串替换明文中的每个字符。从原始字符到替换它们的字符串的映射称为密钥(与打开锁所用的钥匙类似,而不是用于 Python 字典的那种钥匙)。在 Python 中,字典提供了一种方便的方式来实现可以用于编码和解码文本的映射。

书本密码是一种密钥来源于书籍的密码。例如,它可能将明文中的每个字符映射到该字符在书中首次出现的数字索引(或书页上的索引)。假设发送者和接收者之前已达成一致使用的书,但拦截编码信息的对手并不知道使用了哪本书。

以下函数定义使用字典推导创建一个字典,该字典可用于使用书本密码编码明文。

 gen_code_keys = (lambda book, plain_text:(
    {c: str(book.find(c)) for c in plain_text}))

如果 plain_text 是 “no is no” 并且 book 以 “从前,在一个遥远的地方有一所房子” 开头,则调用 gen_code_keys(book, plain_text) 会返回。

{'n': '1', 'o': '7', ' ': '4', 'i': '13', ‘s': '26'}

顺便提一下,o 映射为七而不是零,因为 o 和 O 是不同的字符。如果 book堂吉诃德 的文本,³⁸ 调用 gen_code_keys(book, plain_text) 将返回。

{'n': '1', 'o': '13', ' ': '2', 'i': '6', ‘s': '57'}

现在我们有了编码字典,可以使用列表推导定义一个函数,利用它来加密明文。

 encoder = (lambda code_keys, plain_text:
    ''.join(['*' + code_keys[c] for c in plain_text])[1:])

由于明文中的字符可能被多个字符替换为密文中的字符,我们使用 * 来分隔密文中的字符。.join 操作符用于将字符串列表转换为单个字符串。

函数 encrypt 使用 gen_code_keysencoder 来加密明文。

encrypt = (lambda book, plain_text:
    encoder(gen_code_keys(book, plain_text), plain_text))

调用 encrypt(Don_Quixote, 'no is no') 返回。

1*13*2*6*57*2*1*13 

在我们能够解码密文之前,需要构建一个解码字典。简单的做法是反转编码字典,但那样会不诚实。书本密码的核心在于发送者发送加密信息,但没有关于密钥的任何信息。接收者解码信息所需的唯一条件是获得编码者使用的书。以下函数定义使用字典推导从书和编码信息构建解码密钥。

 gen_decode_keys = (lambda book, cipher_text:
    {s: book[int(s)] for s in cipher_text.split('*')})

调用 gen_decode_keys(Don_Quixote, '1*13*2*6*57*2*1*13') 会产生解密密钥。

{'1': 'n', '13': 'o', '2': ' ', '6': 'i', '57': ‘s'}

如果明文中出现的字符在书中不存在,就会发生一些不好的事情。code_keys 字典会将每个这样的字符映射到 -1,而 decode_keys 会将 -1 映射到书中最后一个字符。

指尖练习:解决上一段描述的问题。提示:简单的方法是通过向原始书籍附加内容来创建一本新书。

指尖练习:以encoderencrypt为模型,实现decoderdecrypt函数。使用它们解密消息

22*13*33*137*59*11*23*11*1*57*6*13*1*2*6*57*2*6*1*22*13*33*137*59*11*23*11*1*57*6*173*7*11

使用唐吉诃德的开头进行加密。

5.9 章节引入的术语

  • 元组

  • 多重赋值

  • 可迭代对象

  • 类型迭代器

  • 列表

  • 可变类型

  • 不可变类型

  • id 函数

  • 对象相等

  • 副作用

  • 别名

  • 克隆

  • 浅拷贝

  • 深拷贝

  • 列表推导式

  • 高阶函数

  • 空白字符

  • 集合

  • 可哈希类型

  • 字典

  • 视图对象

  • 字典推导式

  • 书本密码

第六章:递归与全局变量

你可能听说过递归,而且很可能把它视为一种相当微妙的编程技巧。这是计算机科学家传播的一个迷人的都市传说,让人们觉得我们比实际上聪明。递归是一个重要的概念,但并不是那么微妙,它不仅仅是一种编程技巧。

递归作为一种描述性方法被广泛使用,甚至连那些从未想过写程序的人也会使用它。考虑美国法律中对“出生公民权”概念的部分定义。大致来说,定义如下

  • 在美国境内出生的任何孩子或

  • 在美国境外婚生的任何孩子,其中一位父母是美国公民。

第一个部分很简单;如果你在美国出生,你就是出生公民(例如巴拉克·奥巴马)。如果你不是在美国出生,则取决于你出生时你的父母是否是美国公民。而你的父母是否是美国公民可能还取决于他们的父母是否是美国公民,依此类推。

一般来说,递归定义由两部分组成。至少有一个基例,直接指定特殊情况下的结果(上面示例中的情况 1),还有至少一个递归(归纳)情况(上面示例中的情况 2),根据某个其他输入的答案来定义答案,通常是同一问题的简化版本。基例的存在使得递归定义不成为循环定义。³⁹

世界上最简单的递归定义可能是阶乘函数(通常在数学中用!表示)针对自然数的定义。⁴⁰经典的归纳定义

c6-fig-5001.jpg

c6-fig-5002.jpg

第一个方程定义了基例。第二个方程在前一个数字的阶乘的基础上,定义了所有自然数的阶乘,除了基例。

图 6-1 包含了阶乘的迭代实现(fact_iter)和递归实现(fact_rec)。

c6-fig-0001.jpg

图 6-1 阶乘的迭代和递归实现

图 6-1 阶乘的迭代和递归实现

这个函数足够简单,两个实现都不难理解。然而,第二个实现是对原始递归定义的更直接翻译。

实现 fact_rec 通过在 fact_rec 的主体中调用 fact_rec 似乎有些作弊。这是因为与迭代实现的工作原理相同。我们知道 fact_iter 中的迭代将终止,因为 n 一开始为正数,并且在每次循环中减少 1。这意味着它不可能永远大于 1。类似地,如果 fact_rec 被以 1 调用,它会返回一个值而不进行递归调用。当它进行递归调用时,它总是以一个比被调用时小 1 的值进行。最终,递归以调用 fact_rec(1) 终止。

手指练习: 整数的谐波和,n > 0,可以使用公式计算 c6-fig-5003.jpg。编写一个递归函数来计算这个值。

6.1 斐波那契数

斐波那契数列是另一种常见的数学函数,通常以递归方式定义。“它们繁殖得像兔子一样快,”通常用来描述说话者认为增长过快的人口。在 1202 年,意大利数学家比萨的莱昂纳多,即斐波那契,提出了一个公式来量化这一概念,尽管有一些并不太现实的假设。⁴¹

假设一对新生的兔子,一只雄性和一只雌性,被放在一个围栏里(或者更糟的是,被释放到野外)。进一步假设这些兔子在一个月大时就能交配(令人惊讶的是,某些品种可以)并且有一个月的怀孕期(令人惊讶的是,某些品种确实如此)。最后,假设这些神话中的兔子永远不会死(这不是任何已知兔子品种的特性),而且雌性兔子从第二个月开始每个月都会产下一对新兔子(一个雄性,一个雌性)。六个月结束时会有多少只雌性兔子?

在第一个月的最后一天(称之为月份 0),将有一只雌性兔子(准备在下一个月的第一天交配)。在第二个月的最后一天,仍然只有一只雌性兔子(因为她直到下一个月的第一天才会产仔)。在下一个月的最后一天,将会有两只雌性兔子(一只怀孕的和一只不怀孕的)。在下一个月的最后一天,将会有三只雌性兔子(两只怀孕的和一只不怀孕的)。依此类推。让我们以表格形式查看这一进程,图 6-2。

c6-fig-0002.jpg

图 6-2 雌性兔子数量的增长

注意到对于月份 n > 1females(n) = females(n‑1) + females(n-2)。这并不是偶然。每只在月份 n-1 仍然存活的雌性将在月份 n 依然存活。此外,每只在月份 n‑2 存活的雌性将在月份 n 产生一只新的雌性。新的雌性可以加到月份 n-1 的雌性数量中,以获得月份 n 的雌性数量。

图 6-2 雌性兔子数量的增长

人口增长自然地通过递推描述⁴²

females(0) = 1
females(1) = 1
females(n + 2) = females(n+1) + females(n)

这个定义与阶乘的递归定义不同:

  • 它有两个基本情况,而不仅仅是一个。一般来说,我们可以有任意多的基本情况。

  • 在递归情况下,有两个递归调用,而不仅仅是一个。同样,我们可以有任意多的调用。

图 6-3 包含了斐波那契递推的直接实现,⁴³以及一个可以用来测试它的函数。

c6-fig-0003.jpg

图 6-3 递归实现斐波那契数列

编写代码是解决这个问题的简单部分。一旦我们从一个模糊的关于兔子的问题陈述转变为一组递归方程,代码几乎是自然而然地生成的。找到一种抽象的方法来表达当前问题的解决方案通常是构建一个有用程序中最困难的步骤。我们将在本书后面详细讨论这个问题。

正如你可能猜到的,这并不是野生兔子种群增长的完美模型。在 1859 年,澳大利亚农民托马斯·奥斯丁从英格兰进口了 24 只兔子作为猎物。一些兔子逃脱。十年后,澳大利亚每年大约有两百万只兔子被射杀或捕获,对种群没有明显影响。这是很多兔子,但与120^(th)斐波那契数r相差甚远。⁴⁴

虽然斐波那契数列并没有提供兔子种群增长的完美模型,但它确实具有许多有趣的数学性质。斐波那契数在自然界中也很常见。例如,大多数花朵的花瓣数是斐波那契数。

手指练习: 当图 6-3 中的fib实现被用来计算fib(5)时,它在计算fib(5)的过程中计算了多少次fib(2)的值?

6.2 回文

递归对许多不涉及数字的问题也很有用。图 6-4 包含一个函数is_palindrome,它检查字符串是否正反读相同。

c6-fig-0004.jpg

图 6-4 回文测试

函数is_palindrome包含两个内部辅助函数。这对函数的客户端没有太大兴趣,客户端只需关心is_palindrome的实现是否符合其规范。但你应该关心,因为通过检查实现可以学到很多东西。

辅助函数to_chars将所有字母转换为小写并移除所有非字母。它首先使用字符串上的内置方法生成一个与s相同的字符串,只是所有大写字母都被转换为小写。

辅助函数is_pal使用递归来完成实际工作。两个基本情况是长度为零或一的字符串。这意味着递归实现部分只会在长度为二或更多的字符串上被触及。在else子句中的连接⁴⁵是从左到右进行评估的。代码首先检查第一个和最后一个字符是否相同,如果相同,则继续检查去掉这两个字符的字符串是否为回文。在这个例子中,只有当第一个连接评估为True时,第二个连接才会被评估,这在语义上并不相关。然而,在书的后面部分,我们将看到一些例子,其中这种短路评估布尔表达式在语义上是相关的。

这个is_palindrome的实现是一个重要问题解决原则的例子,称为分而治之。(这个原则与分而治之算法相关,但略有不同,后者将在第十二章讨论。)这个问题解决原则是通过将一个难题拆分为一组子问题来征服一个困难的问题,具备以下属性。

  • 子问题比原始问题更容易解决。

  • 子问题的解决方案可以结合起来解决原始问题。

分而治之是一个古老的思想。尤利乌斯·凯撒践行了罗马人所称的divide et impera(分而治之)。英国人巧妙地运用这一方法来控制印度次大陆。本杰明·富兰克林非常清楚英国在运用这一技术方面的专业知识,因此在美国独立宣言签署时他说:“我们必须团结一致,否则我们必将各自面对困境。”

在这种情况下,我们通过将原始问题拆分为一个更简单的同类问题(检查一个较短的字符串是否为回文)和一个我们知道如何处理的简单问题(比较单个字符)来解决问题,然后使用逻辑运算符and结合解决方案。图 6-5 包含一些可视化这一过程的代码。

c6-fig-0005.jpg

图 6-5 可视化回文测试的代码

执行代码

print('Try dogGod')
print(is_palindrome('dogGod'))
print('Try doGood')
print(is_palindrome('doGood'))

打印

Try dogGod
  is_pal called with doggod
  is_pal called with oggo
  is_pal called with gg
  is_pal called with 
  About to return True from base case
  About to return True for gg
  About to return True for oggo
  About to return True for doggod
True
Try doGood
  is_pal called with dogood
  is_pal called with ogoo
  is_pal called with go
  About to return False for go
  About to return False for ogoo
  About to return False for dogood
False

6.3 全局变量

如果你尝试用一个大数字调用fib,你可能会注意到运行时间非常长。假设我们想知道进行了多少次递归调用。我们可以仔细分析代码来找出答案,在第十一章中我们将讨论如何做到这一点。另一种方法是添加一些代码来计算调用次数。一种方法是使用全局变量

到目前为止,我们所编写的所有函数仅通过其参数和返回值与环境进行通信。在大多数情况下,这正是应该的。这通常导致程序相对容易阅读、测试和调试。然而,偶尔情况下,全局变量也会派上用场。考虑图 6-6 中的代码。

c6-fig-0006.jpg

图 6-6 使用全局变量

在每个函数中,代码行global num_fib_calls告诉 Python,名称num_fib_calls应该在其出现的函数外部定义。如果我们没有包含代码global num_fib_calls,那么名称num_fib_calls将在函数fibtest_fib中是局部的,因为num_fib_callsfibtest_fib的赋值语句左侧。函数fibtest_fib都可以不受限制地访问变量num_fib_calls所引用的对象。函数test_fib每次调用fib时都会将num_fib_calls绑定为0,而fib每次进入时都会递增num_fib_calls的值。

调用test_fib(6)会产生输出

fib of 0 = 1
fib called 1 times.
fib of 1 = 1
fib called 1 times.
fib of 2 = 2
fib called 3 times.
fib of 3 = 3
fib called 5 times.
fib of 4 = 5
fib called 9 times.
fib of 5 = 8
fib called 15 times.
fib of 6 = 13
fib called 25 times.

我们对全局变量这一主题的引入怀有些许忧虑。自 1970 年代以来,持卡计算机科学家们对它们表示反对,这一点是有充分理由的。全局变量的滥用可能导致许多问题。使程序可读的关键在于局部性。人们是逐段阅读程序的,每一段所需的上下文越少,理解就越好。由于全局变量可以在多种地方被修改或读取,它们的不当使用可能破坏局部性。然而,在某些情况下,全局变量确实是所需的。全局变量最常见的用法可能是定义一个将在多个地方使用的全局常量。例如,某个编写与物理相关的程序的人可能希望一次性定义光速 C,然后在多个函数中使用它。

6.4 本章介绍的术语

  • 递归

  • 基本情况

  • 递归(归纳)情况

  • 归纳定义

  • 递归关系

  • 辅助函数

  • 短路求值

  • 分治法

  • 全局变量

  • 全局常量

第七章:模块和文件

到目前为止,我们假设 1)我们的整个程序存储在一个文件中,2)我们的程序不依赖于之前编写的代码(除了实现 Python 的代码),以及 3)我们的程序不访问之前收集的数据,也不以允许在程序运行结束后访问的方式存储结果。

第一个假设在程序较小时是完全合理的。然而,随着程序的增大,将不同部分存储在不同文件中通常更方便。比如说,想象一下多个开发者在同一个程序上工作。如果他们都试图更新同一个文件,那将是一场噩梦。在第 7.1 节中,我们讨论了一种机制,即 Python 模块,它允许我们轻松地从多个文件中的代码构建程序。

第二和第三个假设对于旨在帮助人们学习编程的练习是合理的,但在编写旨在完成某些有用功能的程序时,这种假设很少是合理的。在第 7.2 节中,我们展示了如何利用标准 Python 发行版中的库模块。在本章中,我们使用了其中几个模块,而在书的后面部分还会使用许多其他模块。第 7.3 节简要介绍了如何从文件中读取和写入数据。

7.1  模块

模块是一个包含 Python 定义和语句的.py文件。例如,我们可以创建一个包含图 7-1 中的代码的文件circle.py

c7-fig-0001.jpg

图 7-1 与圆和球体相关的一些代码

程序通过导入语句访问模块。例如,代码

import circle
pi = 3
print(pi)
print(circle.pi)
print(circle.area(3))
print(circle.circumference(3))
print(circle.sphere_surface(3))

将打印

3
3.14159
28.27431
18.849539999999998
113.09724

模块通常存储在单独的文件中。每个模块都有自己的私有符号表。因此,在circle.py中,我们以通常的方式访问对象(例如,piarea)。执行import M将在import出现的作用域中为模块M创建一个绑定。因此,在导入上下文中,我们使用点表示法来表明我们引用的是在导入模块中定义的名称。⁴⁶ 例如,在circle.py之外,引用picircle.pi可以(并且在这种情况下确实)指代不同的对象。

初看,使用点表示法似乎有些繁琐。另一方面,当导入一个模块时,通常不知道该模块的实现中可能使用了哪些局部名称。使用点表示法完全 限定 名称避免了因意外名称冲突而导致的错误。例如,在circle模块之外执行赋值pi = 3不会改变在circle模块中使用的pi的值。

正如我们所见,一个模块可以包含可执行语句以及函数定义。通常,这些语句用于初始化模块。因此,模块中的语句仅在模块首次被导入到程序时执行。此外,每个解释器会话中,一个模块只会被导入一次。如果你启动一个控制台,导入一个模块,然后更改该模块的内容,解释器仍将使用原始版本的模块。这在调试时可能导致令人困惑的行为。当有疑问时,请启动一个新的 shell。

一种允许导入程序在访问导入模块内定义的名称时省略模块名称的import语句变体。执行语句from M import *会在当前作用域中创建对M中定义的所有对象的绑定,但不包括M本身。例如,代码

from circle import *
print(pi)
print(circle.pi)

首先会打印3.14159,然后生成错误信息

`NameError: name 'circle' is not defined`

许多 Python 程序员对使用这种“通配符”import表示不满。他们认为这会使代码更难以阅读,因为不再明显一个名称(例如上述代码中的pi)是在哪里定义的。

一种常用的导入语句变体是

import *module_name* as *new_name*

这指示解释器导入名为module_name的模块,但将其重命名为new_name。如果module_name在导入程序中已被用于其他用途,这将非常有用。程序员使用这种形式的最常见原因是为长名称提供一个缩写。

7.2 使用预定义包

许多有用的模块包作为标准 Python 库的一部分而提供;我们将在本书后面使用其中的一些。此外,大多数 Python 发行版还附带标准库以外的包。Python 3.8 的 Anaconda 发行版包含超过 600 个包!我们将在本书后面使用其中的一些。

在这一节中,我们介绍两个标准包,mathcalendar,并给出一些简单的使用示例。顺便提一下,这些包与所有标准模块一样,使用我们尚未涉及的 Python 机制(例如,异常,在第九章中讨论)。

在之前的章节中,我们介绍了*似对数的各种方法。但我们没有告诉你最简单的方法。最简单的方法是简单地导入模块math。例如,要打印以 2 为底的 x 的对数,你只需写

import math
print(math.log(x, 2))

除了包含大约 50 个有用的数学函数外,math模块还包含几个有用的浮点常量,例如math.pimath.inf(正无穷大)。

设计用于支持数学编程的标准库模块仅占标准库模块的一小部分。

想象一下,比如你想打印 1949 年 3 月星期几的文本表示,就像右侧的图片。你可以在线查找那个月和那年的日历。然后,凭借足够的耐心和多次尝试,你可能会写出一个可以完成这项工作的打印语句。或者,你也可以简单地写

c7-fig-5001.jpg

import calendar as cal
cal_english = cal.TextCalendar()
print(cal_english.formatmonth(1949, 3))

或者,如果你更喜欢用法语、波兰语和丹麦语查看日历,你可以写

print(cal.LocaleTextCalendar(locale='fr_FR').formatmonth(2049, 3))
print(cal.LocaleTextCalendar(locale='pl_PL').formatmonth(2049, 3))
print(cal.LocaleTextCalendar(locale='da_dk').formatmonth(2049, 3))

将产生

c7-fig-5002.jpg

假设你想知道 2033 年圣诞节是星期几。该行

 print(cal.day_name[cal.weekday(2033, 12, 25)])

将回答这个问题。调用cal.weekday将返回一个表示星期几的整数,⁴⁷,然后用它来索引cal.day_name——一个包含英语星期几的列表。

现在,假设你想知道 2011 年美国感恩节是星期几。星期几很简单,因为美国感恩节总是在 11 月的第四个星期四。⁴⁸ 找到实际日期稍微复杂一些。首先,我们使用cal.monthcalendar获取一个表示月份周数的列表。列表的每个元素包含七个整数,表示月份的日期。如果该日期在该月不存在,则该周的列表第一个元素将为0。例如,如果一个有 31 天的月份从星期二开始,则列表的第一个元素将是[0, 1, 2, 3, 4, 5, 6],而列表的最后一个元素将是[30, 31, 0, 0, 0, 0, 0]

我们使用calendar.monthcalendar返回的列表来检查第一周是否有一个星期四。如果有,第四个星期四就在这个月的第四周(即索引 3);否则,它在第五周。

def find_thanksgiving(year):
    month = cal.monthcalendar(year, 11)
    if month[0][cal.THURSDAY] != 0:
        thanksgiving = month[3][cal.THURSDAY]
    else:
        thanksgiving = month[4][cal.THURSDAY]
    return thanksgiving
print('In 2011', 'U.S. Thanksgiving was on November',
      find_thanksgiving(2011))

指尖练习: 编写一个满足规范的函数。

 def shopping_days(year):
    """year a number >= 1941
       returns the number of days between U.S. Thanksgiving and
               Christmas in year"""

指尖练习: 自 1958 年以来,加拿大感恩节在十月的第二个星期一举行。编写一个接受年份(>1957)作为参数的函数,返回加拿大感恩节与圣诞节之间的天数。

按惯例,Python 程序员通常

  1. 1. 每行导入一个模块。

  2. 2. 将所有导入放在程序的开头。

  3. 3. 首先导入标准模块,其次是第三方模块(例如,通过 Anaconda 提供的模块),最后是特定应用程序的模块。

有时将所有导入放在程序的开头会导致问题。导入语句是可执行的代码行,Python 解释器在遇到时会执行它。一些模块包含在导入模块时会执行的代码。通常,这些代码初始化模块所使用的一些对象。由于这些代码可能会访问共享资源(例如计算机上的文件系统),因此导入在程序中的执行位置可能很重要。好消息是,这不太可能成为你可能使用的模块的问题。

7.3 文件

每个计算机系统使用 文件 来保存从一次计算到下一次计算的内容。Python 提供了许多创建和访问文件的功能。这里我们展示一些基本的功能。

每个操作系统(例如 Windows 和 macOS)都有自己用于创建和访问文件的文件系统。Python 通过一种称为 文件句柄 的方式实现操作系统的独立性。代码

name_handle = open('kids', 'w')

指示操作系统创建一个名为 kids 的文件,并返回该文件的文件句柄。open 的参数 'w' 表示文件将以 写入 模式打开。以下代码 打开 一个文件,使用 **write** 方法写入两行。(在 Python 字符串中,转义字符“\”用于表示下一个字符应以特殊方式处理。在这个例子中,字符串 '\n' 表示 换行符。)最后,代码 关闭 该文件。记得在程序使用完文件后关闭它。否则,有可能某些或所有写入的数据不会被保存。

name_handle = open('kids', 'w')
for i in range(2):
    name = input('Enter name: ')
    name_handle.write(name + '\n')
name_handle.close()

你可以通过使用 **with** 语句来确保不会忘记关闭文件。此形式的代码

with open(*file_name*) as *name_handle*:
   *code_block*

打开一个文件,将一个本地名称绑定到它,可以在 code_block 中使用,并在 code_block 退出时关闭该文件。

以下代码以 读取 模式打开一个文件(使用参数 'r'),并打印其内容。由于 Python 将文件视为一系列行,我们可以使用 for 语句遍历文件的内容。

with open('kids', 'r') as name_handle:
    for line in name_handle:
        print(line)

如果我们输入大卫和安德烈亚的名字,这将打印。

David
Andrea

大卫和安德烈亚之间的额外行是因为打印在文件中每行末尾遇到 '\n' 时会开始新的一行。我们可以通过写 print(line[:-1]) 来避免打印额外的行。

代码

name_handle = open('kids', 'w')
name_handle.write('Michael')
name_handle.write('Mark')
name_handle.close()
name_handle = open('kids', 'r')
for line in name_handle:
    print(line)

将打印单行 MichaelMark

注意,我们已经覆盖了文件 kids 的先前内容。如果我们不想这样做,可以使用参数 'a' 打开文件以 追加 模式(而不是写入模式)。例如,如果我们现在运行代码

name_handle = open('kids', 'a')
name_handle = open('kids', 'a')
name_handle.write('David')
name_handle.write('Andrea')
name_handle.close()
name_handle = open('kids', 'r')
for line in name_handle:
    print(line)

它将打印行 MichaelMarkDavidAndrea

指尖练习:编写一个程序,首先将斐波那契数列的前十个数字存储到名为fib_file的文件中。每个数字应单独占一行。程序然后从文件中读取这些数字并打印出来。

对文件的一些常见操作总结在图 7-2 中。

c7-fig-0002.jpg

图 7-2 访问文件的常见函数

7.4 本章介绍的术语

  • 模块

  • 导入语句

  • 完全限定名称

  • 标准 Python 库

  • 文件

  • 文件句柄

  • 写入和读取

  • 从文件中

  • 换行符

  • 打开和关闭文件

  • with 语句

  • 追加到文件

posted @   绝不原创的飞龙  阅读(26)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示