Python-编程学习指南-全-
Python 编程学习指南(全)
原文:Get programming _ learn to code with Python
译者:飞龙
单元 0. 学习如何编程
本单元从一点动机开始,说明无论你是谁,学习编程都是有益的;你甚至可以在日常生活中使用编程来简化某些任务。你将简要介绍在开始编程之前你应该熟悉的一些概念,并且你将了解在本书结束时你将能够完成的一些事情。
本单元通过将编程与烘焙进行类比来结束,这样你可以将编程视为一种需要练习和创造力的技能。本单元还概述了你在这一旅程中可以期待的内容:大量的练习!学习编程似乎是一项艰巨的任务,但最好是每天迈出小步,而不是偶尔的巨大飞跃。这是一条充满挑战但回报丰厚的道路。
让我们开始吧!
第 1 课. 为什么你应该学习如何编程?
在阅读第 1 课之后,你将能够
-
理解为什么编程很重要
-
制定学习编程的计划
1.1. 为什么编程很重要
编程是普遍的。无论你是谁,无论你做什么,你都可以学习编写可以帮助使你的生活更轻松的程序。
1.1.1. 编程不仅仅是专业人士的工作
对于经验丰富的程序员和从未编程过的人来说,一个误解是,一旦你开始学习如何编程,你就必须继续学习直到你成为一名专业的程序员。很可能,这种误解源于将编程与极其复杂的系统联系起来:你的操作系统、汽车/航空软件、学习的人工智能,以及其他许多系统。
我认为编程是一项技能,就像阅读/写作、数学或烹饪一样。你不必成为畅销书作家、世界级数学家或米其林星级厨师。只要在这些学科中有一点知识,你的生活就会显著改善:如果你知道如何阅读和写作,你就可以与他人沟通;如果你知道如何进行基本计算,你可以在餐厅里适当地给小费;如果你知道如何遵循食谱,你就可以在紧急情况下做出一顿饭。稍微了解一些编程将使你能够避免依赖他人,并使你能够更有效地完成你可能想要以特定方式完成的任务。
1.1.2. 提升你的生活
如果你学会了编程,你的技能可以用来有效地构建你自己的个人工具箱。你越尝试将编程融入日常生活,你就能更有效地解决个人任务。
为了保持你的技能,你可以编写适合你日常需求的定制程序。自己编写程序而不是使用现成的程序的好处是,你可以根据你的确切需求进行定制。例如:
-
你是否在随支票簿附带的纸质日志本中记录你写的支票?考虑将它们输入文件,并编写一个程序来读取文件并组织信息。有了编程,在数据读取后,你可以计算总和、按日期范围分离支票,或者你想要的任何其他操作。
-
你拍照并将它们下载到电脑上,但相机软件给出的文件名不是你想要的吗?与其手动为千张照片重命名,不如编写一个简短的程序来自动重命名所有文件。
-
你是一名准备 SAT 考试的学生,并想确保你的二次方程解是正确的吗?你可以编写一个程序,输入缺失的参数并为你解方程,这样当你手动操作时,你可以确信计算是正确的。
-
你是一位想给每个学生发送包含该学生测试成绩的个性化电子邮件的教师吗?与其复制粘贴文本并手动填写值,你可以编写一个程序,从文件中读取学生姓名、电子邮件地址和分数,然后自动为每个学生填写空白并发送电子邮件。
这些只是编程能帮助你变得更加有条理和自给自足的几个情况。
1.1.3. 挑战自己
初看起来,编程感觉是技术性的。一开始确实是,尤其是当你正在学习所有基本概念的时候。也许不太直观,但编程也是创造性的。当你熟悉了编程中完成一项任务的一些方法后,你就可以做出决定,选择哪种方式应用最好。例如,如果你正在读取文件,你是想一次性读取所有数据,存储它,然后进行分析,还是想一次读取一部分数据,边读边分析?
通过运用你获得的知识做出这些决定,你挑战自己批判性地思考你想要实现的目标以及如何最有效地实现它。
1.2. 你现在在哪里,你将去哪里
这本书并不假设你之前编程过。话虽如此,你应该熟悉以下内容:
-
理解变量——如果你修过涵盖初等代数的数学课程,你应该知道什么是变量。在下一单元中,你将看到编程环境中的变量是如何不同的。
-
理解真/假语句——你可以把语句看作是可以被判定为真或假的句子。例如,“正在下雨”是一个要么真要么假的语句。你还可以使用单词 not 来反转语句,使其具有相反的真值。例如,如果“正在下雨”是真的,那么“没有下雨”就是假的。
-
连接语句——当你有多个语句时,你可以通过使用单词 and 或 or 来连接它们。例如,如果“正在下雨”是真的,而“我饿了”是假的,那么“正在下雨并且我饿了”是假的,因为两部分都需要是真的。但“正在下雨或者我饿了”是真的,因为至少有一部分是真的。
-
做出决定——当你有多个语句时,你可以使用 if...then 来根据一个语句是否为真做出决定。例如,“如果下雨,那么地面是湿的”由两个语句组成:“正在下雨”和“地面是湿的”。语句“地面是湿的”是语句“正在下雨”的结果。
-
遵循流程图——你不需要了解流程图就能理解这本书,但理解它们需要与理解基本编程相同的技能。使用相同技能集的其他想法包括玩 20 个问题的游戏、遵循食谱、阅读选择你自己的冒险书或理解算法。你应该熟悉遵循一系列指令和做出分支决策。流程图显示一系列指令,这些指令从一个流向下一个,并允许你做出决策,这些决策将导致不同的路径。在流程图中,你会被问一系列问题,其答案有两个选择:是或否。根据你的答案,你将遵循流程图中的某些路径,最终会到达一个最终答案。图 1.1 是流程图的一个例子。
图 1.1. 今天是否带伞的流程图

了解上述技能就足以开始你的编程之旅。阅读完这本书后,你将了解编程的基础知识。你将学习的、可以应用于任何编程语言的基本概念如下:
-
在编程中使用变量、表达式和语句
-
让程序根据条件做出决策
-
让程序在某种条件下自动重复任务
-
重复使用语言内建的操作来帮助你更高效
-
通过将更大的任务分解成更小的部分,使你的代码更易于阅读和维护。
-
了解在不同情况下使用哪种数据结构(一种已经创建的、可以以特定格式存储信息的结构)是合适的
你将通过使用名为 Python(版本 3.5)的语言来学习如何编程。关于编程概念的任何知识都将很容易地转换到任何其他编程语言;不同语言之间的基础知识将是相同的。更具体地说,本书结束时,你将熟悉 Python 编程语言的细节。你将了解以下内容:
-
如何使用语言的语法(在英语中,相当于如何构成有效的句子)。
-
如何编写更复杂的程序,不同的代码块协同工作(在英语中,相当于写一个短篇故事)。
-
如何使用其他程序员编写的代码(在英语中,相当于引用他人的工作,这样你就不必重写它)。
-
如何有效地检查程序是否工作,包括测试和调试(在英语中,相当于检查拼写和语法错误)。
-
如何编写与键盘和鼠标交互的程序。
-
如何编写更以数据为中心或数学化的程序。
1.3. 我们学习如何编程的计划
在学习编程语言时,个人动力是决定成功与否的重要因素之一。放慢速度,大量练习,并留出时间吸收材料,会使通往成功的道路更加平稳。
1.3.1. 第一步
如果你之前从未编程过,这本书就是为你准备的。这本书被分为几个单元。一个单元是一系列课程,这些课程都围绕编程中的一个特定概念展开。单元中的第一节课通常是一个激励课程。单元中的最后一节课是一个总结性项目,它介绍了一个现实生活中的问题或任务。你可以自己尝试这个总结性项目,或者阅读解决方案的概述;它的目的是确保你理解这些概念。
你将有很多机会练习你所学的。在每节课的开始,你将看到一个简单的练习,称为考虑这一点,这将让你思考你周围的世界以及你与之互动的方式;这个练习介绍了课程的主要思想。它没有使用代码术语,并暗示了你在课程中将要学习的编程思想。在整个课程中,你会发现如何将概述练习的英文描述“翻译”成代码。每个课程都包含许多练习,帮助你理解概念;完成所有练习将有助于概念的理解。这些练习的答案可以在附录 A 中找到,以便你可以检查你的工作。
在前几节课中,亲自动手做练习非常重要,因为你会使用 Python 学习编程的基础。在最后几节课中,你将看到其他程序员编写的包,你将有机会学习如何使用这些包来构建更复杂的程序。其中一个包将让你构建可以视觉交互的程序,通过鼠标点击或键盘输入,你将看到你的程序在屏幕上更新图像。另一个包将向你展示如何处理输入数据。你将学习如何读取具有特定结构的文件,如何分析收集到的数据,以及如何将数据写入另一个文件。
1.3.2. 练习,练习,再练习
每个课程都有简短的练习和解决方案。在 Python 和编程一般而言,大量的练习对于真正理解概念至关重要——这尤其适用于那些之前从未编程过的人。在编写程序时不要因为错误而沮丧;通过纠正意外的错误,你会提高你的理解能力。
你可以将这些练习视为检查点,以帮助你了解你理解了多少。编程不是一种被动活动。你应该积极参与所呈现的材料和通过不断自己尝试来展示的概念。检查点练习涉及到课程中呈现的重要思想,你应该尝试所有这些练习以涵盖材料的广度。如果你感到好奇,你甚至可以对这些练习提出变体,并尝试为提出的问题编写新的程序!
1.3.3. 程序员思维
本书旨在提供独特的学习体验。我不仅想教你用 Python 编程,还想教你如何像程序员一样思考。
为了理解这一点,考虑以下隐喻。有两个人:一个小说作者和一个记者。小说作者是一个提出情节、人物、对话和互动的人,然后通过使用英语规则以有趣的方式将这些想法组合在一起。作者写故事供人们欣赏。记者不需要运用他们的创造性,而是基于事实寻找故事。记者然后将事实写成文字,也使用英语规则,以便人们获得信息。
我通过比较小说作者和记者来展示计算机科学家和程序员之间的区别。计算机科学家和程序员都知道如何编写计算机代码,并且他们都遵守编程语言的规则来创建执行特定任务的程序。就像作者思考一个独特的故事以及如何最好地安排它一样,计算机科学家可能会在提出想法上投入更多精力,而不是将想法转化为文字。计算机科学家会考虑全新的算法或研究理论问题,例如计算机能做什么和不能做什么。另一方面,程序员基于现有的算法或一组必须遵守的要求来实现程序。程序员对语言的细节了如指掌,并能快速、高效、正确地实现代码。在实践中,程序员和计算机科学家的角色经常重叠,而且并不总是有明确的区分。
本书将向你展示如何通过向计算机提供详细的指令来在计算机上实现任务,并帮助你熟练地完成这项工作。
程序员思维
在本书的其余部分,请注意这个框。
你将获得关于哪些像计算机程序员一样的思维原则适用于正在讨论的想法的有用提示。这些原则将本书串联起来,我希望重新审视这些想法能帮助你进入程序员的思维模式。
下一节课将概述几个原则,这些原则涉及到如何像程序员一样思考。在整个课程中,你将在可能的情况下被提醒这些原则,我希望你在阅读本书的过程中,开始自己思考这些原则。
摘要
在本节课中,我的目标是激发你学习编程的兴趣。你不必成为一名专业程序员。利用基本的编程思想和概念来改善你的个人生活,即使是以简单的方式。编程是一项技能,你练习得越多,就会越擅长。当你阅读这本书时,试着想想那些你手动完成的繁琐任务,看看是否可以用编程更有效地解决,并尝试去实现它。
让我们开始吧!
第 2 课. 学习编程语言的基本原则
在阅读第 2 课后,你将能够
-
理解编写计算机程序的过程
-
获得对“思考-编码-测试-调试-重复”范式的整体认识
-
理解如何处理编程问题
-
理解编写可读代码的意义
2.1. 编程作为一项技能
就像阅读、计数、弹钢琴或打网球一样,编程是一项技能。与任何技能一样,你必须通过大量的练习来培养它。练习需要你的奉献、毅力和自律。在你编程生涯的开始阶段,我强烈建议你尽可能多地编写代码。打开你的代码编辑器,输入你看到的每一行代码。尽量自己输入,而不是依赖复制粘贴。在这个阶段,目标是让编程成为第二本能,而不是快速编程。
这节课旨在激发你成为程序员的思维方式。第一节课介绍了本书中散布的“像程序员一样思考”的框框。以下部分提供了一个整体视角,概括了这些框框的主要思想。
考虑这一点
你想教一个穴居人如何打扮去参加面试。假设衣服已经摆好,穴居人对衣服很熟悉,但不知道如何穿衣。你会告诉他哪些步骤?尽可能具体。
答案:
1. 拿起内衣,左脚穿进一个洞,右脚穿进另一个洞,然后拉起。
2. 拿起衬衫,将一只手臂穿过一个袖子,然后将另一只手臂穿过另一个袖子。纽扣应该在你的胸前。通过将小纽扣插入孔中关闭衬衫。
3. 拿起裤子,一只脚放在一条裤腿上,另一只脚放在另一条裤腿上。裤口应该在前面。拉上拉链并扣好裤子。
4. 拿起一只袜子,把它穿在一只脚上。然后穿上鞋子。拉紧鞋带并系好。用另一只袜子和鞋子重复此操作。
2.2. 与烘焙的类比
假设我要求你为我烤一条面包。你将经历什么过程——从我开始给你任务,到你给我完成的面包?
2.2.1. 理解任务“烤一条面包”
第一步是确保你理解给定的任务。“烤一条面包”有点模糊。以下是一些你可能想要提出的问题来澄清任务:
-
面包的大小是多少?
-
应该是简单的面包还是有味道的面包?你必须使用或避免使用哪些特定成分?你有什么没有的成分?
-
你需要什么设备?设备是提供的,还是你需要自己购买?
-
有时间限制吗?
-
你能否查找并使用任何食谱,或者你必须自己创造一个?
确保你正确地获取这些细节,以避免在任务上重新开始。如果没有提供关于任务的更多细节,你提出的解决方案应该尽可能简单,并且对你来说应该尽可能少的工作。例如,你应该查找一个简单的食谱,而不是自己尝试找到正确的成分组合。另一个例子,首先尝试烤一个小面包,不要添加任何风味或香料,并使用面包机(如果你有的话)来节省时间。
2.2.2. 寻找食谱
在你澄清了关于任务的任何疑问或误解之后,你可以查找一个食谱或者自己创造一个。食谱告诉你如何完成任务。自己创造一个食谱是完成任务中最困难的部分。当你有一个可以遵循的食谱时,把所有东西组合起来不应该很难。
现在快速查看任何食谱。图 2.1 展示了一个样本食谱。一个食谱可能包括以下内容:
-
你应该采取的步骤及其顺序
-
具体的测量
-
指示何时以及重复任务的次数
-
你可以替换某些成分的替代方案
-
对菜肴的任何最终修饰以及如何上菜
图 2.1. 面包的样本食谱

食谱是一系列你必须遵循的步骤来烘焙面包。步骤是顺序的;例如,你不能在没有先将面团放入烤盘之前将面包从烤箱中取出。在某个步骤中,你可以选择放入一个物品而不是另一个;例如,你可以放入黄油或菜籽油,但不能两者都放。并且某些步骤可能需要重复,比如偶尔检查面包皮的颜色,在宣布面包烤好之前。
2.2.3. 用流程图可视化食谱
当你阅读食谱时,步骤的顺序可能用文字概述。为了帮助你理解如何成为一名程序员,你应该开始思考如何用流程图可视化食谱,正如在第一部分中简要讨论的那样。图 2.2 展示了如何用流程图表示烘焙面包。在这个场景中,你正在使用面包机,并且配料与图 2.1 中展示的略有不同。在流程图中,步骤以矩形框的形式输入。如果食谱允许可能的替代,用菱形框表示。如果食谱要求你重复一个任务,画一个箭头返回到重复序列的第一个步骤。
图 2.2. 烘焙面包的简单流程图。矩形框表示执行一个动作。菱形表示决策点。返回到前一步的线条表示序列重复。按照箭头指示追踪流程图的路径。

2.2.4. 使用现有的食谱还是自己创造一个?
现在有很多面包食谱。你怎么知道该用哪一个?对于像“烤一条面包”这样的模糊问题陈述,所有食谱都适用,因为它们都能完成任务。从这个意义上说,当你有一组可供选择的食谱时,更一般的问题陈述对你来说更容易,因为任何食谱都可能适用。
但是,如果你有一个挑剔的食客,并且被要求制作没有食谱的烘焙食品,你将很难完成任务。你将不得不尝试各种成分组合和数量,以及各种温度和烘焙时间。很可能会多次从头开始。
你最常遇到的问题类型是具体任务,你有一些相关信息,例如“用 4 杯面粉、1 汤匙糖、1 汤匙黄油、1 茶匙盐和 1 茶匙酵母为我烤一条两磅的迷迭香面包。”你可能找不到一个完全完成这个任务的食谱,但任务中已经提供了大量关键信息;在这个例子中,你有了所有成分的量,除了迷迭香的量。最难的部分是尝试将成分组合在一起,并确定应该添加多少迷迭香。如果你不是第一次烘焙,你将带着一些关于迷迭香应该加多少的直觉来完成任务。你练习得越多,就越容易。
从这个烘焙例子中应该得到的要点是,烘焙不仅仅是遵循食谱。首先,你必须理解你被要求烘焙的是什么。然后,你必须确定你是否有一些现成的食谱可以遵循。如果没有,你必须想出自己的食谱并进行实验,直到你有一个符合要求的产品。在下一节中,你将看到烘焙例子如何转化为编程。
2.3. 思考、编码、测试、调试、重复
在这本书中,你将编写简单和复杂的程序。无论程序的复杂程度如何,以有组织、结构化的方式处理每个问题都很重要。我建议使用图 2.3 中所示的“思考-编码-测试-调试-重复”范式,直到你对代码按照问题规范工作感到满意。
图 2.3. 这是用编程解决问题的理想方式。在编写任何代码之前理解问题。然后测试你编写的代码,并根据需要调试它。这个过程会重复进行,直到你的代码通过所有测试。

思考步骤相当于确保你理解了被要求制作的烘焙食品的类型。思考一下被提出的问题,并决定你是否有一些可能适用的食谱,或者你是否需要自己想出一个。在编程中,食谱就是算法。
代码步骤相当于亲自动手,通过实验可能的成分组合、任何替代品以及任何重复的部分(例如,每五分钟检查一次饼皮)。在编程中,你正在编写一个算法的实现。
测试步骤相当于确定最终产品是否符合任务预期的结果。例如,从烤箱里出来的烘焙食品是不是面包?在编程中,你用不同的输入运行程序,并检查实际输出是否与预期输出匹配。
调试步骤相当于调整食谱。例如,如果太咸了,就减少加盐的量。在编程中,你调试程序以找出导致不正确行为的代码行。如果你不遵循最佳实践,这是一个粗略的过程。一些将在本课的后面部分概述,单元 7 也包含一些调试技术。
这些四个步骤需要重复多次,直到你的代码通过所有测试。
2.3.1. 理解任务
当你遇到需要用编程解决的问题时,你绝对不应该立即开始编写代码。如果你一开始就写代码,你将直接进入图 2.3 中所示的代码步骤的循环。如果你第一次没有正确解决问题,你将不得不循环多次。通过在开始时思考问题,你可以最小化你将经历编程循环的次数。
随着你解决越来越难的问题,尝试将它们分解成更简单、步骤更少的更小问题也很重要。你可以先专注于解决这些小问题。例如,与其用异国风味的成分烤一个面包,不如先尝试几个小面包卷,以恰到好处地调整比例,而不浪费太多资源或时间。
当你遇到一个问题,你应该问自己以下问题:
-
这个程序打算完成什么?例如,“找到圆的面积。”
-
是否有与用户的交互?例如,“用户将输入一个数字”和“你向用户展示以该半径的圆的面积。”
-
用户给你提供什么类型的输入?例如,“用户将给你一个代表圆半径的数字。”
-
用户希望程序提供什么,以及以什么形式?例如,你可能向用户展示“12.57”,你可能更详细地展示“半径为 2 的圆的面积是 12.57”,或者你可能为用户画一张图。
我建议你通过以下两种方式重新描述问题,来组织你对问题的思考:
-
可视化问题。
-
写下几个样本输入,然后你期望的输出。
快速检查 2.1
Q1:
找到任何食谱(在盒子里或在网上查找)。为食谱试图实现的目标编写一个问题陈述。编写一个模糊的问题陈述。编写一个更具体的问题陈述。
2.3.2. 可视化任务
当你被分配一个使用编程来解决的问题时,将任务视为一个黑盒。一开始,不要担心实现。
定义
实现是编写代码来解决任务的方式。
不要担心实现的细节,考虑一下被询问的问题:程序与用户有交互吗?程序可能需要哪些输入?程序可能显示哪些输出?程序只是应该在幕后进行计算吗?
画一个图来展示你的程序与程序用户之间可能的交互是有帮助的。回到面包的例子。一个可能的黑盒可视化如图 2.4 所示。输入在黑盒的左侧表示,输出在右侧表示。
图 2.4。使用给定的一组成分烘焙一袋面包的黑盒可视化。

当你对黑盒的输入和输出有了一定的了解后,考虑任何可能需要考虑的特殊行为。程序在不同情况下会有不同的行为吗?在面包的例子中,如果你没有糖,你能用其他东西代替吗?如果你用盐代替糖,你会得到不同类型的面包吗?你应该写出程序在这些情况下会做什么。
所有这些具体的交互都可以在流程图中可视化。你可以在流程图中追踪许多路径,每条路径代表不同的可能实现和结果,如图 2.2 所示。
快速检查 2.2
Q1:
在你的烘焙冒险之后,你需要清理:洗碗和倒垃圾,按照这个顺序。将以下步骤和决策点组织成如图 2.2 所示的流程图。尽可能使用尽可能多的步骤/决策,但不必全部使用。
- 步骤:冲洗盘子
- 步骤:唱一首歌
- 步骤:扎紧垃圾袋
- 步骤:将垃圾带出室外
- 步骤:拿起一个脏盘子
- 步骤:用肥皂刷洗脏盘子
- 步骤:将干净的盘子放入晾架上
- 步骤:将垃圾袋倒放在地上
- 步骤:将一片垃圾放入垃圾袋
- 决策:垃圾袋里还需要放其他东西吗?
- 决策:我对我的烘焙技能满意吗?
- 决策:还有其他脏盘子吗?
- 决策:我今晚要看电影吗?
2.3.3. 编写伪代码
在这个阶段,你已经想出了测试用例,一些你可能需要小心处理的特殊行为,以及一个你认为能够完成任务的步骤序列的视觉表示。如果你已经画出了你的步骤序列,现在就是将你的草图转换成文字的时候了,使用编程概念。为了解决问题,你必须想出一组步骤来遵循,以便它们能够完成问题中概述的任务。
伪代码是英语和编程在纸上或代码编辑器中的混合体。它帮助你确保程序结构在各个点上正确:当你从用户那里获取输入时,当你显示输出时,当你需要做出决定时,以及当你需要重复一组步骤时。
将步骤序列用文字表达就像写下并尝试你的食谱。你必须使用你所知道的关于食材的味道和用途的知识来决定如何将它们安排在一起。在编程中,你必须使用你所知道的关于各种技术和构造的知识来组合代码,这是编程中最困难的部分。
在伪代码中,计算圆的面积可能看起来像这样:
-
从用户那里获取一个半径。
-
应用公式。
-
显示结果。
-
重复步骤 1-3,直到用户说停止。
在整本书中,你将看到一些例子,说明某些编程概念是有用的。唯一知道何时以及如何使用这些概念的方法是通过直觉,而这种直觉来自于大量的实践。
当然,用编程实现一个任务有无数种方法。走一条路然后发现自己卡住了并不是什么坏事;这样你会更好地理解为什么在那种情况下特定的方法不起作用。随着时间的推移和经验的积累,你会对何时使用一个概念而不是另一个概念有直觉。
快速检查 2.3
Q1:
这是一个问题陈述。勾股定理是 a² + b² = c²。求解 c。用伪代码编写一组步骤,说明你可能采取的求解 c 的方法。
提示:√(x²) = x
2.4. 编写可读的代码
随着你对编程的了解越来越多,特别是这本书中关于 Python 编程的了解,你会看到 Python 提供的语言特性,这些特性可以帮助你实现这一原则。我在这个课程中不会讨论这些内容。在你开始编写代码之前,重要的是要记住,你编写的任何代码都应该有意图让其他人阅读,包括自己在几周后!
2.4.1. 使用描述性和有意义的名称
这里是一个 Python 代码的简短片段,你现在不需要理解它。它由三行代码组成,按顺序从上到下评估。注意,它看起来与你在数学课中可能写的东西相似:
a = 3.1
b = 2.2
c = a * b * b
你能否从高层次上说出代码应该计算什么?实际上很难。假设我重写了代码:
pi = 3.1
radius = 2.2
# use the formula to calculate the area of a circle
circle_area = pi * radius * radius
现在你能从高层次上告诉代码应该做什么吗?是的!它计算——或者更确切地说,估计——半径为 2.2 的圆的面积。就像数学一样,编程语言使用变量来存储数据。编写可读代码的一个关键思想是使用描述性和有意义的变量名。在上面的代码中,pi是一个变量名,你可以用它来引用值 3.1。同样,radius和circle_area也是变量名。
2.4.2. 为代码添加注释
还要注意,前面的代码中包含了一条以#字符开头的行。这一行被称为注释。在 Python 中,注释以#字符开头,但在其他语言中,它可能以不同的特殊字符开头。注释行不是程序运行时执行的代码的一部分。相反,注释用于代码中描述代码的重要部分。
定义
注释是 Python 程序中的一行,以#开头。这些行在运行程序时被 Python 忽略。
注释应该帮助他人和你自己理解为什么以这种方式编写代码。它们不应该只是将代码实现的内容用文字表达出来。一个说“使用公式计算圆的面积”的注释比一个说“将π乘以半径再乘以半径”的注释要好得多。注意,前者解释了为什么代码使用是正确的,但后者只是简单地将代码实现的内容用文字表达出来。在这个例子中,阅读代码的人已经知道你在乘以三个值(因为他们知道如何阅读代码!),但他们可能不知道为什么你要进行乘法。
当注释描述了更大块代码背后的理由时,注释是有用的,尤其是当你想出了一种独特的方式来计算或实现某事时。注释应该描述特定代码块实现背后的主要思想,因为这对其他人可能不明显。当阅读代码的人理解了整体情况时,他们可以通过阅读每一行代码来深入了解代码的具体细节,看看你具体做了哪些计算。
快速检查 2.4
Q1:
这里有一段简短的 Python 代码,实现了以下问题的解决方案。填写注释。“你正在填充一个泳池,有两个水龙头。绿色水龙头 1.5 小时可以填满,蓝色水龙头 1.2 小时可以填满。你希望通过使用两个水龙头来加快这个过程。使用两个水龙头需要多长时间,以分钟计算?”
# Your comment here time_green = 1.5 time_blue = 1.2 # Your comment here minutes_green = 60 * time_green minutes_blue = 60 * time_blue # Your comment here rate_hose_green = 1 / minutes_green rate_hose_blue = 1 / minutes_blue # Your comment here rate_host_combined = rate_hose_green + rate_hose_blue # Your comment here time = 1 / rate_host_combined
摘要
在本课中,我的目标是教你
-
一个优秀的程序员应该遵循的思考-编码-测试-调试-重复的事件循环。
-
思考你被给出的问题,并理解被要求做什么。
-
在开始编写代码之前,根据问题描述来梳理输入和输出。
-
问题陈述并不一定会概述你应该采取的步骤来解决任务。可能需要你自己想出一个方案——一系列实现任务的步骤。
-
编写代码时,要考虑到代码的可读性。你应该使用描述性和有意义的名称,并编写注释,用文字描述问题和编码解决方案。
单元 1. 变量、类型、表达式和语句
在本单元中,你将下载包含 Python 3.5 版本和特殊文本编辑器的软件,以帮助你编写和运行你的 Python 程序。你将设置你的编程环境,并尝试运行一些简单的 Python 代码以确保一切设置正确。然后,你将了解任何编程语言的基础知识:各种类型的对象、变量、语句和表达式。这些都是程序的基础——就像字母、单词和句子对于英语语言一样。
本单元以一个综合项目结束:你的第一个 Python 程序!我会引导你完成每一个步骤。解决编程问题有许多方法,我会展示两种。如果你觉得有冒险精神,在看到完整解决方案之前,你可以自由尝试这个问题。
第 3 课:介绍 Python:一种编程语言
在阅读第 3 课之后,你将能够
-
理解 Python,你将要使用的编程语言
-
使用程序编写你的程序
-
理解编程开发环境的组成部分
3.1. 安装 Python
在写作本书时,Python 编程语言是教授入门计算机科学最受欢迎的语言。该语言被顶尖大学用来让学生接触编程,许多学生在进入大学时都提到 Python 是他们熟悉的语言。广泛来说,Python 用于构建应用程序和网站,并且被像 NASA、Google、Facebook 和 Pinterest 这样的公司用于后台维护功能和分析收集的数据。
Python 是一种优秀的通用编程语言,可以用来编写快速简单的程序。在设置好工作环境后,用 Python 编写程序不需要做太多的设置。
3.1.1. 什么是 Python?
Python 是一种由荷兰 Centrum Wiskunde & Informatica 的 Guido van Rossum 创建的编程语言。但“Python”这个名字也被用来指代解释器。
定义
Python 解释器是一个用于运行用 Python 编程语言编写的程序的程序。
在 Python 编程语言中,每个东西,称为对象,都与它相关的特征(数据)和与之交互的方式。例如,任何单词在 Python 中都是一个东西,或对象。与单词summer相关的数据是该序列中的字母字符。你可以与单词交互的一种方式是将每个字母都改为大写。一个更复杂的对象的例子是一辆自行车。与自行车相关的数据可能包括轮子的数量、高度、长度和颜色。自行车可以做的动作可能包括它可能会倒下,有人可以骑它,你也可以重新粉刷它。
在这本书中,你将使用写作时的最新 Python 版本,即版本 3.5。
3.1.2. 下载 Python 3.5 版本
你可以通过多种方式下载 Python 3.5 版本;你可以从官方 Python 网站www.python.org获取,或者通过提供 Python 语言以及预装额外包的任何第三方程序。在这本书中,我推荐你下载一个名为Anaconda Python Distribution的特定第三方程序。
3.1.3. Anaconda Python Distribution
你可以从www.anaconda.com下载 Anaconda Python Distribution。这个免费的 Python 发行版提供了 Python 的各种版本,并包括 400 多个最受欢迎的科学、数学、工程和数据分析包。还有一个不带任何额外包的轻量级版本,称为Miniconda。
前往下载页面,www.anaconda.com/downloads,并选择适合您操作系统的 Python 3.5 下载链接。按照默认值遵循安装说明,在您的计算机上安装该发行版。请注意,最新版本可能与 Python 3.5 不同,这是可以的。就我们的目的而言,Python 子版本之间的变化不会产生影响。
3.1.4. 集成开发环境
安装完成后,打开 Spyder,它是 Anaconda 的一部分程序。Spyder 是一个集成开发环境(IDE),您将使用它来编写和运行本书中的程序。
定义
一个 集成开发环境(IDE)是一个完整的编程环境,它有助于使您的程序编写体验更加愉快。
打开 Spyder
在 Windows 中,您可以从开始菜单中的 Anaconda 文件夹打开 Spyder,如图 3.1 所示。
图 3.1. 开始菜单中的 Anaconda 文件夹

Spyder IDE 提供的一些重要功能,如图 3.2 所示,包括以下内容:
-
一个用于编写 Python 程序的编辑器
-
在运行程序之前查看可能包含潜在错误或不效率的代码行的一种方式
-
一个用于与程序用户交互的控制台,通过输入和输出
-
查看程序中变量值的一种方式
-
逐行遍历您代码的一种方式
图 3.2 展示了整个 Spyder IDE 和代码编辑器中编写的部分代码。您不需要理解代码。
图 3.2. 包含代码编辑器、控制台和变量探索器窗口的 Spyder IDE

3.2. 设置您的开发空间
当您打开 Spyder,如图 3.2 所示,您会看到程序窗口被分为三个窗口面板:
-
左侧面板是编辑器,最初不包含代码,只有几行文本。您会注意到这些文本是绿色的,这意味着这是一个多行注释——不是将要运行的代码。
-
右上角的面板可能包含对象检查器、变量探索器或文件探索器。您不会使用这个窗口面板,但例如变量探索器会显示程序完成后每个变量的值。
-
右下角的面板默认是 IPython 控制台。在本课中,您将了解有关 IPython 控制台和文件编辑器的一些基础知识。
下两个部分将指导您在 Spyder 中进行简单计算。您将看到如何直接在控制台中输入计算,以及如何在代码编辑器中编写更复杂的程序。在下两个部分的结尾,您的 Spyder 会话应类似于图 3.3。
图 3.3. 在 IPython 控制台和代码编辑器中输入表达式后的 Spyder 会话

3.2.1. IPython 控制台
IPython 控制台是你可以快速测试命令以查看它们做什么的主要方式。更重要的是,用户将使用控制台与你的程序进行交互。IPython中的I代表交互式。IPython 控制台是一个高级控制台,为用户提供包括自动完成、之前输入的命令历史记录以及特殊单词的颜色高亮等便捷功能。
直接在控制台中编写命令
你可以直接在 IPython 控制台中编写单个命令来尝试一些事情并查看它们的效果。如果你刚开始编程,你应该多尝试一些。这是开始了解语句做什么以及表达式如何评估的最佳方式。
在控制台中输入 3 + 2 并按 Enter 键执行这个加法操作。你会看到结果 5,前面跟着文本 Out[]。现在输入 4 / 5 来执行这个除法操作,你会看到结果 0.8,前面也跟着文本 Out[]。
你可以将这个控制台想象成让你窥视你输入的表达式值的工具。为什么我说窥视?因为这些表达式的结果对用户是不可见的。要使它们对用户可见,你必须明确地将它们的值打印到控制台。在控制台中输入 print(3 + 2)。数字 5 再次被打印出来,但这次前面没有 Out[]。
两者 3 + 2 和 4 / 5 都被称为 Python 表达式。一般来说,Python 中任何可以评估为值的都是表达式。你将在第 4 课中看到更多表达式的例子。在下一节中,你将看到如何在文件编辑器中输入命令来编写更复杂的程序。
快速检查 3.1
以下表达式会向用户显示输出,还是只是让你窥视它们的值?请在控制台中输入这些表达式来检查自己!
1
6 < 72
print(0)3
7 * 0 + 44
print("hello")
控制台的主要用途
几乎没有程序员能够第一次就写出完美的程序。即使是经验丰富的程序员也会犯错。你第一次尝试编写程序时可能会有些不稳定,当你尝试运行程序时,会出现错误(bug)。
定义
错误是程序中的错误。
如果程序有错误,无论大小,你都必须尝试修复它们。你可以从调试过程中学到很多东西。当你开始编写更复杂的程序时,你可以从两个角色的角度考虑使用控制台:你作为程序员,以及作为与你的程序交互的人(用户)。图 3.4 展示了控制台允许你扮演的双重角色。程序员主要使用控制台来测试命令和调试程序。用户使用控制台通过输入输入并与正在运行的程序交互来查看程序输出。
图 3.4. 程序员使用控制台进行自己的测试和调试。他们直接在控制台中输入命令并查看输出。用户通过控制台与程序交互。他们在控制台中输入程序输入,并查看程序输出。

在这本书中,你将看到的绝大多数程序都没有图形界面。相反,你将编写通过控制台中的文本与用户交互的程序;当在控制台中提示时,用户将被给予输入文本/数字/符号的机会,你的程序将在控制台中显示结果。图 3.5 展示了用户可能如何与编写的程序交互的示例。
图 3.5. 用户与程序交互的示例

作为程序员,你将使用控制台扮演你程序用户的角色。这在调试程序(当你试图弄清楚为什么你的程序没有按预期工作)时最有用。当你使用文件编辑器编写更复杂的程序时,通常很有用,让控制台打印出程序中任何计算或对象的价值,而不仅仅是最终值。这样做可以帮助你确定程序中的中间值,并帮助你调试。如果运行程序就像尝试食谱一样,打印中间值就像尝试食谱中的项目以确保一切顺利。调试将在第 7 单元(kindle_split_045.html#part08)中更详细地介绍。
控制台对于尝试单个表达式并查看它们的值很有用。如果你想再次运行表达式,可以在控制台中重新输入,或者使用上箭头查看之前输入的表达式,然后按 Enter 键再次运行。文件编辑器将你的表达式保存到文件中,这样你就不需要重新输入它们。当你想编写多行以上的程序时,这可以节省很多时间。
3.2.2. 文件编辑器
当你编写包含超过几行代码的更复杂的 Python 程序时,你应该使用文件编辑器窗格。在这里,你可以逐行输入命令(在编程中称为语句),就像图 3.3 中所示。完成一组命令的编写后,你可以通过点击 Spyder 顶部工具栏中的绿色箭头来运行程序,如图图 3.6 所示。在 Anaconda 支持的所有操作系统上编辑和运行文件都是相同的:PC、Mac 和 Linux。本书展示了 Windows 操作系统的截图。
图 3.6. 点击绿色箭头按钮以运行程序。

并非所有代码行都会产生用户可见的输出
在空文件中,在第 8 行输入3 + 2,如图图 3.3 中之前所示。在下一行,输入4 / 5。现在不要输入其他任何内容。现在点击绿色箭头运行程序。第一次点击箭头时,可能会弹出一个窗口询问你工作目录;接受默认值即可。发生了什么?你右下角的控制台显示了类似以下的一些红色文本:
runfile('C:/Users/Ana/.spyder2-py3/temp.py',
wdir='C:/Users/Ana/.spyder2-py3')
那行指示你的程序已运行,但没有向用户显示任何内容。
现在做出以下添加。在第 10 行,输入print(3 + 2)。然后在下一行,输入print(4 / 5)。再次运行程序。现在发生了什么?你应该看到与图 3.3 中相同的内容。控制台显示了计算结果,每个结果都在不同的行上。
这是如何工作的?Python 解释器执行文件中的每一行。它首先运行语句3 + 2,并在内部计算这个表达式的结果。然后它内部计算4 / 5。因为这两个语句没有告诉 Python 显示计算结果,所以它们的值没有显示在控制台上。
Python 中的一个关键字print,用于当你想要将print后面的括号内任何内容的值输出到控制台时。在这种情况下,你展示了评估表达式3 + 2和4 / 5的结果,顺序如下。
快速检查 3.2
用户将在控制台看到以下哪种表达式?在文件编辑器中输入这些表达式,然后点击运行以检查!
1
print(4 - 4 * 4)2
print(19)3
19 - 10
保存文件
你应该将你编写的每个程序保存在单独的文件中,以保持你的组织。你之前编写代码的文件是一个临时文件,保存在 Anaconda 安装文件夹中的某个位置。从 Spyder 菜单栏中打开一个新的文件,如图图 3.7 所示。再次在新的文件中输入之前的两个print语句。
图 3.7. 在 Spyder 中打开的多个文件;每个文件在文件编辑器窗格中都有自己的标签。

小贴士
我强烈建议你再次输入命令而不是复制粘贴。重复是帮助你掌握编程的绝佳方式。强迫自己在编程生涯的初期输入命令将有助于加快你的学习进程,并使编写代码变得像第二本能一样自然。
现在将文件保存在你选择的目录中。你必须使用 .py 扩展名保存它。如果你不使用这个扩展名保存,你将无法运行程序(绿色运行按钮将变为灰色)。保存文件后,点击绿色运行按钮。之前相同的输出应该会显示在控制台中。
如果你关闭了你刚刚保存的文件,你的程序并没有丢失。你可以从文件菜单中重新打开文件。所有的代码仍然都在那里,你可以像刚刚编写它一样运行程序。
摘要
在本课中,我的目标是教你
-
如何使用 Python 3.5 版本和名为 Spyder 的 IDE 安装名为 Anaconda 的 Python 发行版
-
如何打开新文件,在文件中编写简单的程序,保存文件,并运行程序
-
如何在文件编辑器中编写代码并在编辑器窗格中打开多个文件
-
控制台允许你查看变量的值或向用户显示输出
-
如何使用
print语句将表达式值打印到控制台
第 4 课. 变量和表达式:给事物命名和赋值
阅读完第 4 课后,你将能够
-
编写创建 Python 对象的代码
-
编写将对象分配给变量的代码
在你的日常生活中,你会遇到许多物理对象,或事物。这些事物中的每一个都有一个名称。它们有名称是因为用名称来引用它们比用描述更容易。
当你总是操作事物或对象时,使用名称非常有帮助。有些事物很简单,比如数字 9。有些更复杂,比如字典。我可以把名字Nana给数字 9,把名字Bill给我的字典。你可以给事物(几乎)起任何你想要的名称。你甚至可以给事物组合命名。例如,如果我把香蕉粘在我的笔记本电脑盖上,创造一个新事物,我可以把这个新潮的创造物命名为 Banalaptop。单个事物也可以命名;如果我有两个苹果,我可以把一个命名为 Allie,另一个命名为 Ollie。
在你命名物品后,你可以后来无任何混淆地引用它们。使用名称的好处是,你不必重新创建(在编程中,重新计算)值。当你命名一个物品时,你本质上会记住关于它的每一个细节。
考虑这一点
扫描你现在所在的房间里的几样东西。然后采取以下步骤:
1. 将物品写下来。(我看到我的手机、一把椅子、一块地毯、纸张和一个水壶。)
2. 使用这些物品中的某些或全部来写一个句子。你可以多次使用一个对象。(我的水壶洒在我的手机和纸张上,现在我的手机坏了,我的纸张也毁了。)
3. 不使用其名称,为每个对象写一个描述。
4. 现在用只有描述的句子重新写你想到的那个句子。
5. 你写的句子使用物品名称或描述更容易阅读吗?
答案:
1. 一部手机、纸张和一个水壶。
2. 我的水壶洒在我的手机和纸张上,现在我的手机坏了,我的纸张也毁了。
3. 描述:
- 水壶—装透明液体的容器
- 手机—我用来打电话/发短信/看猫视频的矩形设备
- 纸张—印有黑白文字的薄薄的、白色的、易碎的东西的堆叠
4. 一个装透明液体的容器,它是我用来打电话/发短信/看猫视频的矩形设备,以及一堆薄薄的、白色的、易碎的东西,上面印有黑白文字,现在我的矩形设备坏了,我的黑白文字的东西也坏了。
5. 使用物品名称而不是描述,句子更容易阅读。
4.1. 给事物命名
你使用的每一件东西都有一个名称,这使得在对话中引用它们更容易。编写计算机程序就像编写你想要发生的事件和涉及的详细描述。在编程中,你通过使用变量来引用事物,这在第 4.2 节(section 4.2)中讨论了编程的上下文。
4.1.1. 数学与编程
当你听到变量这个词时,可能会让你想起数学课,在那里你用方程式进行计算,并被要求“解出x”。编程也使用变量,但方式不同。
在数学中,方程式的行表示等价。例如,“x = 1”表示“x等于 1”,而“2 * x = 3 * y”表示“2 乘以x等于 3 乘以y”。
在编程中,带有等号的代码行代表赋值。图 4.1 展示了 Python 中的赋值。
图 4.1. Python 中的赋值给名称。等号右边的任何表达式都会转换为一个单一值并赋予一个名称。

你使用等号来将变量赋值给值。例如,a = 1 或 c = a + b。等号右边的部分是一个具有值的表达式。
定义
一个表达式是一行可以简化为值的代码。
要得到值,你需要将表达式中的所有其他已知变量的值代入,并进行计算。例如,如果 a = 1 和 b = 2,那么 c = a + b = 1 + 2 = 3。在编程中,等号左边只能有变量的名字。
4.1.2. 计算机能做什么和不能做什么
一个重要的一点值得回顾:计算机需要被告知要做什么。计算机不能自己自发地解方程。如果你告诉计算机 a = 2,b = 2,并且 a + x = b,它不知道如何处理这些信息或如何自己解出x。a + x = b这一行并没有告诉计算机如何计算任何东西;它只是陈述了一个等价关系。
计算机需要被告知解决问题的方法。回想一下,当你按照烤面包的食谱时,你需要知道步骤。作为程序员的你,必须想出食谱并告诉计算机要做什么。为了想出食谱,你需要离开电脑,在纸上进行计算。然后你可以告诉计算机它需要计算值的步骤。
快速检查 4.1
判断计算机是否允许执行以下赋值操作。假设等号右边的每个事物都有一个值:
1
3 + 3 = 42
stuff + things = junk**3
stack = 1000 * papers + 40 * envelopes4
seasons = spring + summer + fall + winter
4.2. 变量的介绍
通过对编程中变量如何工作的直觉,你现在可以深入学习和了解变量是如何工作的。
4.2.1. 对象是可以操作的事物
在上一节中,我们讨论了事物。在 Python 中,一切都是对象。这意味着你可以在 Python 中创建的每一个事物都具有以下特点:
-
一种类型
-
一组操作
对象的类型告诉你与之相关的数据/值/属性/特性。操作是你可以告诉对象执行的命令;这些命令可能只适用于对象本身,或者可能是对象与其他对象交互的方式。
快速检查 4.2
Q1:
对于以下项目,写一些属性(描述它们的颜色、大小等)和一些操作(它能做什么,它能如何与其他事物交互等):
- 手机
- 狗
- 镜子
- 信用卡
4.2.2. 对象有名称
在程序中创建的每一个东西都可以赋予一个名称,以便以后引用。这些名称被称为变量,用于引用对象。
定义
变量用于将名称绑定到对象。变量名引用特定的对象。
例如:
-
如果
a = 1,那么名为a的对象具有值为1,你可以对其进行数学运算。 -
如果
greeting = "hello",那么名为greeting的对象具有值为"hello"(字符序列)。你可以对这个对象进行的操作包括“告诉我它有多少个字符”或“告诉我它是否包含字母 a”或“告诉我第一个 e 出现在哪个位置”。
在这两个例子中,等号左侧的项目是一个变量名,你可以用它来引用一个对象,而等号右侧的是对象本身,它具有值和一些你可以对其进行的操作。在 Python 中,你将变量名绑定到对象上。
等号右侧的对象不一定是单个对象。它可以是一个可以简化为给出值的计算。有了这个最终值,你就得到了一个对象。例如,a = 1 + 2 是对两个对象(1 和 2)的计算,可以简化为一个值为 3 的单一对象。
4.2.3. 允许的对象名称是什么?
你用变量名编写代码,使其他人对代码可读。许多编程语言,包括 Python,对你可以用于变量的名称有约束:
-
必须以字母(
a到z或A到Z)或下划线(_)开头。 -
变量名中的其他字符可以是字母、数字或下划线。
-
名称区分大小写。
-
名称可以是任意长度。
像程序员一样思考
如果你愿意,你可以有一个长度为 1,000,000,000,000,000 个字符的变量名。但不要这样做!这会使你的代码难以阅读。将代码行限制在最多 80 个字符,并尽量使你的名称尽可能简洁,同时保持可读性。
| |
快速检查 4.3
以下变量名允许吗?
1
A2
a-number3
14
%score5
num_people6
num_people_who_have_visited_boston_in_2016
编程语言有一些保留词,你不能将其用作变量名。对于 Python,Spyder 有语法高亮,它会改变特殊保留 Python 关键字的颜色。
定义
关键字是一个特殊单词。它被保留,因为在编程语言中它有特殊的意义。
图 4.2 展示了语法高亮的示例。一个良好的通用规则是,如果你想要使用的变量变成了不同的颜色,你不应该将其用作变量名。
图 4.2. 在代码编辑器中具有意义的特殊单词会改变颜色。作为一个通用规则,你不应该使用任何除了黑色之外变色的单词来命名变量。

除了命名变量的前面规则之外,这里还有一些指导方针可以帮助你编写更易读的程序:
-
选择描述性和有意义的名称,而不是简短的单字符名称。
-
使用下划线在变量词之间添加一个假想的空格。
-
不要使用过长的变量名。
-
在你的代码中保持一致性。
快速检查 4.4
以下变量名是否允许且良好?
1
customer_list2
3
rainbow_sparkly_unicorn4
list_of_things_I_need_to_pick_up_from_the_store
4.2.4. 创建变量
在你可以使用变量之前,你必须将其设置为一个值。通过使用等号将其分配给一个对象,你初始化变量。初始化将对象绑定到变量名。
定义
变量初始化将变量名绑定到一个对象上。
初始化变量后,你可以通过使用其变量名来引用特定的对象。在 Spyder 中,在控制台中输入以下行以初始化三个变量:
a = 1
b = 2
c = a + b
你可以使用变量浏览器来查看变量的名称、它们的类型、大小(你将在接下来的课程中了解这是什么意思),以及它们的值。图 4.3 展示了你的屏幕应该看起来是什么样子。
图 4.3. 如何在控制台中创建变量。变量浏览器会显示你在这个会话中设置和初始化的变量。

你应该看到变量浏览器中填充了你创建的变量及其值。如果你在控制台中输入一个变量的名称并按 Enter 键,这将允许你窥视其值。变量浏览器还在第二列中告诉你一些额外的信息:变量的类型。下一节将更详细地介绍这意味着什么。
4.2.5. 更新变量
在创建变量名后,你可以将其更新为任何对象的名称。你看到这些行初始化了三个变量:
a = 1
b = 2
c = a + b
你可以将 c 的值更新为其他值。现在你可以输入 c = a - b 来重新分配变量 c 以获得新的值。在变量探索器中,你应该看到变量 c 现在具有不同的值。图 4.4 显示了 Spyder 现在的样子。
图 4.4. 变量探索器有相同的变量 c,但具有新的值。

变量名仅仅是将名称绑定到对象上。相同的名称可以被重新分配给不同的对象。Python 中的一个操作,名为 id,以数字序列的形式显示对象的标识符。每个对象的标识符是唯一的,并且对象存在期间不会改变。在控制台中输入以下行:
c = 1
id(c)
c = 2
id(c)
在第一个 id(c) 命令之后,我的控制台输出了 1426714384。在第二个 id(c) 命令之后,我得到了 1426714416。这两个数字是相同变量名称的两个数字,因为数字 1 和 2 是不同的对象。
快速检查 4.5
假设你按以下顺序执行以下操作。为每个操作写一行代码:
1
将名为
apples的变量初始化为值5。2
将名为
oranges的变量初始化为值10。3
将名为
fruits的变量初始化为apples和oranges的和。4
将变量
apples重新分配为20。5
重新计算变量
fruits,就像之前一样。
概述
在本课中,我的目标是教你
-
创建和初始化变量
-
不是所有名称都允许用作变量名,并且命名变量有一些通用规则
-
对象有一个值
-
表达式是可以简化为值的代码行
-
对象有你可以对其进行的操作
-
变量是一个绑定到对象的名称
让我们看看你是否明白了 ...
Q4.1
你得到了以下问题。解出 x 的方程。用表达式表示 x,然后找到它的值。
a = 2 b = 2 a + x = bQ4.2
在 Spyder 控制台中输入
a + x = b并按 Enter 键。你会得到一个错误。可能是因为你没有告诉计算机a和b是什么。在控制台中输入以下行,每行后按 Enter 键。你仍然会得到错误吗?a = 2 b = 2 a + x = b
第 5 课. 对象类型和代码语句
读完第 5 课后,你将能够
-
编写创建各种类型对象的代码
-
编写简单的代码行来操作 Python 变量
假设你有一个如下所示的家族:
-
四个人—爱丽丝、鲍勃、夏洛特和戴维
-
三只猫—普里斯、薄荷和金克斯
-
两只狗—罗弗和扎普
每个人、猫和狗都是一个单独的对象。你给每个对象取了不同的名字,这样你就可以轻松地引用它们,并且其他人也知道你在谈论哪个对象。在这个家庭中,你有三种类型的对象:人、猫和狗。
每种对象类型都有与另一种类型不同的特征。人有手和脚,而猫和狗只有脚。猫和狗有胡须,但人没有。一种对象类型的特征唯一地识别了该类型中的所有单个对象。在编程中,特征被称为该类型的数据属性或值。
每种对象类型也有动作或行为。人可以开车,但狗和猫不能。猫可以爬树,但狗不能。一种对象类型可以执行的动作仅限于该对象。在编程中,动作被称为该类型的操作。
考虑这一点
你有一个球体和一个立方体。为每个对象写一些特征(选择唯一识别它们的特征)和一些你可以对每个对象执行的操作。
答案:
球体—圆形,有半径/直径,会滚动,会弹跳
立方体—所有边长相等,保持平坦,有顶点,可以站立在上面
5.1. 事物的类型
到目前为止,你已经创建了变量来存储对象。变量是对单个对象的命名。实际上,你可以将对象分类到不同的组中。同一组中的所有对象都将具有相同的类型;它们将具有相同的基本属性,并且它们将具有相同的基本操作来与之交互。
5.2. 编程中的基本对象类型
对象有
-
一种类型,决定了它们可以有哪些值
-
你可以用它们进行的操作
定义
一个对象类型告诉你该对象可以有哪些类型的值。
在大多数编程语言中,一些类型的对象是每种语言的基本构建块。这些类型的对象可能被称为原始类型,或标量。这些基本类型是语言内建的,并且其他任何类型的对象都可以由这些原始类型的组合构成。这类似于字母表中的 26 个字母是英语语言的基本构建块;从这 26 个字母中,你可以构成单词、句子、段落和小说。
Python 有五种基本类型的对象:整数、浮点数、布尔值、字符串,以及一个表示值缺失的特殊类型。在 Python 中,这五种类型被称为原始类型,语言中的其他任何类型的对象都可以用这五种类型构造。
5.2.1. 整数作为整数
类型为 整数(在 Python 中为 int 类型)的对象是一个值是实整数的对象。例如,0, 1, 2, 5, 1234, -4, -1000 都是整数。
你可以对这些数字进行的操作,正如你所期望的,是在数学课中你会对数字进行的操作。
你可以添加、减去、乘以和除以两个或更多数字。你可能需要用括号包围一个负数,以避免将负数与减法操作混淆。例如,
-
a = 1 + 2将值分别为1和2的整数对象相加,并将结果对象值3绑定到名为a的变量。 -
b = a + 2将名为a的整数对象的值和值为2的整数对象相加,并将结果对象的值绑定到名为b的变量。
你可以通过增加一个特定的值来增加一个数字。例如,
-
x = x + 1的意思是将1添加到x的值,并将该值重新绑定到名为x的变量。请注意,这与数学不同,在数学中,你会通过将x从等号的右侧移动到左侧(或从等式的两边减去x)来解这个方程,并将表达式简化为0 = 1。 -
x += 1是编程简写符号,表示x = x + 1。这是另一种有效语法。你可以将+=替换为*=,-=, 或/=,分别代表x = x * 1,x = x – 1,或x = x / 1。等号右侧的1也可以替换为任何其他值。
快速检查 5.1
编写一行代码以实现以下每个目标:
1
将 2 和 2 和 2 相加并将结果存储在名为
six的变量中。2
将变量
six与 -6 相乘并将结果存储在名为neg的变量中。3
使用简写符号将变量
neg除以 10 并将结果存储在同一个变量neg中。
5.2.2. 浮点数作为小数
类型为 浮点数(在 Python 中为 float 类型)的对象是一个值是十进制数的对象。例如,0.0, 2.0, 3.1415927 和 -22.7 都是浮点数。如果你玩过整数,你可能已经注意到当你除以两个数字时,结果是浮点类型。你可以对这些数字进行的操作与整数相同。
重要的是要理解以下两行代码导致两个表示两种类型对象的变量。变量 a 是整数,但变量 b 是浮点数:
a = 1
b = 1.0
快速检查 5.2
编写一行代码以实现以下每个目标:
1
将 0.25 乘以 2 并将结果存储在名为
half的变量中。2
从 1.0 减去变量
half并将结果存储在名为other_half的变量中。
5.2.3. 布尔值作为真/假数据
在编程中,你通常不仅要处理数字。一种比数字更简单的对象类型是 布尔值(在 Python 中,是 bool 类型);它只有两个可能的值,True 或 False。它们用这两个值之一替换表达式;例如,表达式 4 < 5 被替换为 False。你可以对布尔值执行的操作包括逻辑运算 and 和 or,这些在 第一部分 中简要介绍过。
快速检查 5.3
编写一行代码以实现以下每个要求:
1
将
True值存储在名为cold的变量中。2
将值
False存储在名为rain的变量中。3
将表达式
cold and rain的结果存储在名为day的变量中。
5.2.4. 字符串作为字符序列
一个有用的数据类型是字符串(在 Python 中,是 str 类型),在 第七部分 和 第八部分 中有更详细的介绍。简而言之,字符串 是由引号包围的字符序列。
一个字符是你可以通过按键盘上的一个键输入的任何东西。围绕字符的引号可以是单引号或双引号 (' 或 "),只要保持一致即可。例如,'hello',"we're # 1!","m.ss.ng c.ns.n.nts??",以及 "'"(两个双引号内的单引号)都是字符串的可能值。
你可以对字符串执行许多操作,这些操作在第七部分中详细介绍。
快速检查 5.4
编写一行代码以实现以下每个要求:
1
创建一个名为
one的变量,其值为"one"。2
创建一个名为
another_one的变量,其值为"1.0"。3
创建一个名为
last_one的变量,其值为"one 1"。
5.2.5. 值的缺失
你可能想在程序中指定一个值的缺失。例如,如果你刚得到一只宠物,还没有给它起名字,那么宠物就没有名字的值。编程语言允许你在这种情况下指定一个特殊值。在许多编程语言中,这被称为 null。在 Python 中,这个值是 None。因为 Python 中一切都是对象,所以这个 None 也有一个类型,NoneType。
快速检查 5.5
每个对象的类型是什么?
1
2.72
273
False4
"False"5
"0.0"6
-217
999999998
"None"9
None
5.3. 工作于基本数据类型值
现在你对将要处理的对象类型有了一定的了解,你可以开始编写多行代码。当你编写程序时,每一行代码被称为一个语句。一个语句可能包含也可能不包含一个表达式。
定义
任何一行代码都可以称为一个语句。
5.3.1. 表达式的构建块
一个表达式是对象之间的操作,可以简化为一个单一值。以下是一些表达式(和语句)的例子:
-
3 + 2 -
b - c(如果你知道b和c的值) -
1 / x
打印内容的代码行是一个语句,但不是一个表达式,因为打印的行为不能简化为一个值。同样,变量赋值是 Python 语句的一个例子,但不是一个表达式,因为赋值的行为没有值。
快速检查 5.6
记录以下每个是语句、表达式还是两者都是:
1
2.7 - 12
0 * 53
a = 5 + 64
print(21)
5.3.2. 在不同类型之间转换
如果你不确定一个对象的数据类型,你可以使用 Spyder 来检查。在控制台中,你可以使用一个特殊命令,type(),来获取对象的数据类型。例如,
-
在控制台中输入
type(3)并按回车键,可以看到3的类型是整数。 -
在控制台中输入
type("wicked")并按回车键,可以看到"wicked"的类型是字符串。
你也可以将对象从一种类型转换为另一种类型。在 Python 中,你用括号和要转换到的类型的名称包围你想要转换的对象。例如,
-
float(4)通过将整数4转换为浮点数4.0得到4.0。 -
int("4")通过将字符串"4"转换为整数4得到4。注意,你不能将非数字的字符串转换为整数或浮点数。如果你尝试将int("a")转换,你会得到一个错误。 -
str(3.5)通过将浮点数3.5转换为字符串"3.5"得到"3.5"。 -
int(3.94)通过将浮点数3.94转换为整数3得到3。注意,这截断了数字,只保留小数点前的整数部分。 -
int(False)通过将布尔False转换为整数0得到0。注意,int(True)得到1。
快速检查 5.7
编写一个表达式,将以下对象转换为所需的类型,然后预测转换后的值。记住,你可以通过在 Python 控制台中输入表达式来检查:
1
将
True转换为字符串2
将
3转换为浮点数3
3.8转换为str4
0.5转换为int5
"4"转换为int
5.3.3. 算术如何影响对象类型
数学运算是一个 Python 表达式的例子。当你用 Python 中的数字进行运算时,你会得到一个值,所以所有的数学运算在 Python 中都是表达式。
数学中的许多运算符在 Python 中也适用于数字(整数或浮点数)。当你进行数学运算时,你可以混合使用整数和浮点数。表 5.1 展示了当你对整数和浮点数的所有可能组合进行数学运算时会发生什么。表的第 一行说明,当你将一个整数加到另一个整数上时,你会得到一个整数结果。这个例子中,将 3 和 2 相加得到 5。当至少有一个操作数是浮点数时,结果将是浮点数。3.0 + 2、3 + 2.0 或 3.0 + 2.0 的结果都是浮点数 5.0。这个规则的例外是除法。当你除以两个数时,无论操作数的类型如何,你总是得到一个浮点数。
表 5.1 展示了两个你之前没有见过的运算——幂和余数:
-
幂 (**) 是一个基数被指数提升的结果。例如,3² 在 Python 中写作
3 ** 2。 -
余数 (
%) 会给出第一个操作数除以第二个操作数后的余数。例如,3 % 2会找出 2 可以进入 3 多少次(只进入一次),然后告诉你剩下多少(1,因为你要再加 1 到1 * 2才能得到 3)。
表 5.1. 整数和浮点数上的数学运算及其结果类型
| 第一个操作数的类型 | 操作符 | 第二个操作数的类型 | 结果类型 | 示例 |
|---|---|---|---|---|
| int | + - * ** % | int | int | 3 + 2 3 - 2 3 * 2 3 ** 2 3 % 2 |
| int | / | int | float | 3 / 2 |
| int | + - * / ** % | float | float | 3 + 2.0 3 - 2.0 3 * 2.0 3 / 2.0 3 ** 2.0 3 % 2.0 |
| float | + - * / ** % | int | float | 3.0 + 2 3.0 - 2 3.0 * 2 3.0 / 2 3.0 ** 2 3.0 % 2 |
| float | + - * / ** % | float | float | 3.0 + 2.0 3.0 - 2.0 3.0 * 2.0 3.0 / 2.0 3.0 ** 2.0 3.0 % 2.0 |
你可以对数字进行的另一个操作是使用 round() 命令来四舍五入它们;例如,round(3.1) 会给你一个整数 3,而 round(3.6) 会给你一个整数 4。
快速检查 5.8
每个表达式的结果值是什么类型?回想一下,你可以使用 type() 命令来检查。你甚至可以将一个表达式放在 type 的括号内;例如,type(3 + 2)。
1
2.25 - 12
3.0 * 33
2 * 44
round(2.01 * 100)5
2.0 ** 46
2 / 2.07
6 / 48
6 % 49
4 % 2
摘要
在本课中,我的目标是教你关于 Python 中几种基本类型的变量:整数、浮点数、布尔值、字符串,以及一个特殊的NoneType类型。你编写了代码来处理对象类型,并对整数和浮点数执行特定操作。你还编写了语句和表达式。以下是主要收获:
-
对象有一个值,你可以对其执行操作。
-
所有表达式都是语句,但并非所有语句都是表达式。
-
基本数据类型包括整数、浮点数、布尔值、字符串,以及一个表示值缺失的特殊类型。
第 6 课. 架构项目:你的第一个 Python 程序——将小时转换为分钟
在阅读第 6 课之后,你将能够
-
阅读你的第一个编程问题
-
探索两种可能的解决方案
-
编写你的第一个 Python 程序
到目前为止,你应该熟悉以下主要思想:
-
程序由一系列语句组成。
-
一些语句用于初始化变量。
-
一些语句可以是表达式来进行计算。
-
变量应该有描述性和有意义的名称,特别是为了帮助未来可能查看代码的程序员。
-
你已经看到的一些计算包括加法、减法、乘法、除法、余数和幂。
-
你可以将对象转换为不同的类型。
-
print命令可以用来在控制台显示输出。 -
你应该在代码中添加注释来记录代码正在做什么。
问题
你将看到的第一个编程任务是编写一个 Python 程序,将分钟转换为小时。你的程序将从包含分钟数的变量开始。你的程序将取这个数字,进行一些计算,并打印出转换为小时和分钟的转换结果。
你的程序应该以下述方式打印结果。如果分钟数是 121,程序应该打印如下:
Hours
2
Minutes
1
6.1. 思考-编写代码-测试-调试-重复
回想一下,在你开始编码之前,你应该确保理解问题。你可以通过将你的程序作为黑盒来获得整体情况。图 6.1 显示了你的程序作为黑盒,任何输入,以及你必须生成的任何输出。
图 6.1. 程序的输入是任何代表分钟的整数。程序进行一些计算,并打印出多少小时以及剩余的分钟数。

在你理解了输入和输出之后,想出一些输入,并写下对于每个输入你期望的输出是什么。以下是一些可能的分钟数及其转换为小时的输入:
-
“60 分钟”转换为“1 小时和 0 分钟”。
-
“30 分钟”转换为“0 小时和 30 分钟”。
-
“123 分钟”转换为“2 小时和 3 分钟”。
这些输入输出对被称为样本测试用例。在你编写程序后,你将能够使用这些输入和预期的输出来测试你的程序。
快速检查 6.1
给定以下分钟数输入,预期的输出是什么?
1
456
2
0
3
9999
6.2. 分解任务
现在你已经理解了问题在问什么,你必须弄清楚是否可以将它分解成更小的任务。
你需要输入要转换的内容,所以这可以是一个任务。你向用户展示结果,这可以是另一个任务。这两个任务最多只需要几行代码就能轻松实现。
设置输入的代码
为了设置输入,你需要初始化一个带有值的变量。你的变量名应该是描述性的,分钟数应该是一个整数。例如,
minutes_to_convert = 123
设置输出的代码
为了向用户展示输出,所需的格式如下,其中 <some number> 是由你的程序计算得出的:
Hours
<some number>
Minutes
<some number>
你通过使用 print 命令向用户展示输出。以下是代码:
print("Hours")
print(hours_part)
print("Minutes")
print(minutes_part)
在这里,hours_part 和 minutes_part 是你将在程序中计算的变量。
现在唯一剩下的事情就是想出一个将分钟转换为小时和分钟的方法。这将是整个任务中最复杂的一部分。
6.3. 实现转换公式
当你处理时间单位时,你知道 1 小时有 60 分钟。你的第一反应可能是将给定的分钟数除以 60。但是除法会得到一个小数:在第一次尝试中,给定 123 分钟,你的结果将是 2.05,而不是 2 小时和 3 分钟。
要正确地进行转换,你必须将问题分为两部分:找出小时数,然后找出分钟数。
6.3.1. 有多少小时?
回想一下,给定 123 分钟,123/60 的结果是 2.05。注意,整数部分 2 代表小时数。
快速检查 6.2
将以下数字除以 60,并确定结果的整数部分。你可以在 Spyder 中进行除法以检查自己:
1
800
2
0
3
777
回想一下,在 Python 中,你可以将一种类型转换为另一种类型。例如,你可以使用 float(3) 将整数 3 转换为浮点数,得到 3.0。当你将浮点数转换为整数时,你会移除小数点和它之后的所有内容。为了得到除法结果的整数部分,你可以将浮点数结果转换为整数。
快速检查 6.3
为以下每个点编写一行代码,然后回答最后的提问:
1
初始化一个名为
stars的变量,其值为50。2
初始化另一个名为
stripes的变量,其值为13。3
初始化另一个名为
ratio的变量,其值为stars除以stripes。问题:ratio的类型是什么?4
将
ratio转换为整数,并将结果保存到名为ratio_truncated的变量中。问题:ratio_truncated的类型是什么?
在给定的任务中,你将分钟数除以 60,并将结果转换为整数,以得到完整的小时数,如下所示:
minutes_to_convert = 123
hours_decimal = minutes_to_convert/60
hours_part = int(hours_decimal)
到目前为止,你的 hours_part 变量包含了从输入转换过来的小时数。
6.3.2. 有多少分钟?
找出分钟数稍微有点复杂。在本节中,你将看到两种实现方法:
-
方法 1—使用除法结果的整数部分。如果你使用 123 分钟的例子,如何将小数部分 0.05 转换为分钟?你应该将 0.05 乘以 60 得到 3。
-
方法 2—使用取余运算符,
%。再次,使用 123 分钟的例子。123 除以 60 的余数是 3。
6.4. 你的第一个 Python 程序:一种解决方案
使用方法 1 的最终程序代码显示在 列表 6.1 中。代码分为四个部分。第一部分初始化变量以保存要转换的给定分钟数。第二部分将给定输入转换为整数小时数。第三部分将给定输入转换为整数分钟数。最后一部分打印结果。
列表 6.1. 使用小数部分将分钟转换为小时和分钟
minutes_to_convert = 123
hours_decimal = minutes_to_convert/60 *1*
hours_part = int(hours_decimal) *1*
minutes_decimal = hours_decimal-hours_part *2*
minutes_part = round(minutes_decimal*60) *2*
print("Hours") *3*
print(hours_part) *3*
print("Minutes") *3*
print(minutes_part) *3*
-
1 找到小时数的十进制表示,并通过转换为 int 类型得到整数小时数
-
2 获取小数点后的部分并将其转换为整数分钟
-
3 打印结果
你计算小数数分钟的部分可能看起来有点令人畏惧,但你可以将它分解以了解发生了什么。以下行从除法中获取小数点后的部分:
minutes_decimal = hours_decimal-hours_part
对于示例,如果 minutes_to_convert 是 123,则计算为 minutes_decimal = hours_decimal - hours_part = 2.05 - 2 = 0.05。现在你必须将 0.05 转换为分钟。
以下行由两个独立的操作组成,如图 6.2 所示:
minutes_part = round(minutes_decimal * 60)
首先它将 minutes_decimal * 60 相乘:
然后它使用 round(minutes_decimal * 60) 四舍五入该结果。
图 6.2. 两个计算及其按顺序在变量 minutes_decimal 上的评估

为什么你需要执行所有这些操作?如果你用以下行而不是
minutes_part = minutes_decimal * 60
运行程序
minutes_part = round(minutes_decimal * 60)
你会发现一些有趣的事情。输出是
Hours
2
Minutes
2.9999999999999893
你期望看到 3,但看到 2.999999999893。发生了什么事?这种行为是由于 Python 存储浮点数的方式造成的。计算机不能精确地存储小数数,因为它们不能精确地表示分数。当它们在内存中表示 0.05 时,它们近似这个数字。当你乘以浮点数时,它们精确值和它们在内存中表示之间的微小差异会被放大。
当你将 0.05 乘以 60 时,结果会偏离 0.0000000000000107。你可以通过将最终答案四舍五入到整数 round(minutes_decimal * 60) 来解决这个问题。
快速检查 6.4
Q1:
将 列表 6.1 中的程序更改以找到从 789 分钟开始的小时和分钟。输出是什么?
6.5. 你的第一个 Python 程序:另一种解决方案
使用方法 2 的最终程序代码如下所示。代码被分为与上一个解决方案相同的四个部分:初始化、获取完整的小时数、获取完整的分钟数和打印结果。
列表 6.2. 使用余数将分钟转换为小时和分钟
minutes_to_convert = 123 *1*
hours_decimal = minutes_to_convert/60 *2*
hours_part = int(hours_decimal) *3*
minutes_part = minutes_to_convert%60 *4*
print("Hours")
print(hours_part)
print("Minutes")
print(minutes_part)
-
1 给定的分钟数
-
2 找到小时数的十进制版本
-
3 通过转换为 int 类型来获取完整的小时数
-
4 使用除以 60 后的余数来获取完整的分钟数
该程序的输出如下:
Hours
2
Minutes
3
这个版本的程序使用余数概念来提供一个更简洁的程序,其中你不需要进行任何“后处理”来四舍五入或转换为整数,就像你之前的方法那样。但良好的风格是在 minutes_part = minutes_to_convert % 60 这一行上方留下注释,以提醒自己除以 60 的余数给你的是完整的分钟数。适当的注释如下:
# the remainder gives the number of minutes remaining
摘要
在本课中,我的目标是教你如何将许多想法组合起来编写你的第一个 Python 程序。该程序结合了以下主要思想:
-
考虑给定的任务并将其分解为几个更小的任务
-
创建变量并将它们初始化为某个值
-
对变量执行操作
-
将变量类型转换为其他类型
-
向用户打印输出
让我们看看你是否掌握了这个 ...
Q6.1
编写一个程序,初始化一个变量值为 75 来表示华氏温度。然后使用公式 c = (f - 32) / 1.8 将该值转换为摄氏度。打印摄氏度值。
Q6.2
编写一个程序,初始化一个变量值为 5 来表示英里数。然后使用 km = miles / 0.62137 和 meters = 1000 * km 将此值转换为千米和米。以下形式打印结果:
miles 5 km 8.04672 meters 8046.72
单元 2. 字符串、元组以及与用户交互
在上一个单元中,你编写了简单的代码行来创建变量名,并将你的名字绑定到各种类型的对象上:整数、浮点数、布尔值,以及简要地,字符串。
在本单元中,你将编写操作字符序列(称为字符串)的代码。你将能够通过单行代码更改大小写、替换子字符串,以及找到单词的长度。然后你将看到如何创建存储多个对象的序列对象,以及如何访问存储的每个对象。
你将开始编写交互式代码。你将获取用户输入,对其进行一些计算或操作,然后向用户展示输出。有了这些,你的程序会变得更有趣,你也能开始炫耀了。
你将了解一些常见的错误信息,这些错误信息你可能已经遇到(并且无疑将继续遇到)。我想强调的是,每个人在某个时候都会编写出无法正常工作的代码。这是最好的学习体验!
在综合项目中,你将从用户那里获取两个名字,并以某种方式将它们混合在一起,组成一个“夫妻名字”。
第 7 课。介绍字符串对象:字符序列
在阅读完第 7 课后,你将能够
-
理解字符串对象是什么
-
看看字符串对象可以有哪些值
-
使用字符串对象进行一些基本操作
处理字符序列是很常见的。这些序列被称为字符串,你可以在字符串对象中存储任何字符序列:你的名字、你的电话号码、包括换行符的地址等等。以字符串格式存储信息是有用的。在你将数据表示为字符串之后,你可以执行许多操作。例如,如果你和一位朋友都在为一个项目进行研究,你可以在不同的概念上做笔记,然后合并你的发现。如果你在写文章时发现你过度使用了某个词,你可以删除该词的所有实例,或者用另一个词替换一些实例。如果你发现你意外地打开了大小写锁定键,你可以将整个文本转换为小写,而不是重写。
考虑以下情况
看看你的键盘。挑选出 10 个字符并写下它们。以任何顺序将它们串联起来。现在尝试以某种组合方式将它们组合成单词。
答案:
-
hjklasdfqw -
shawl或hi或flaw
7.1. 字符串作为字符序列
在第 4 课中,你学习了字符串是一个字符序列,并且该字符串中的所有字符都用引号表示。只要在一个字符串中保持一致,你可以使用"或'。Python 中字符串的类型是str。以下是一些字符串对象的示例:
-
"simple" -
'also a string' -
"a long string with Spaces and special sym&@L5_!" -
"525600" -
""(双引号之间没有内容的是一个空字符串。) -
''(单引号之间没有内容的是一个空字符串。)
字符序列可以包含数字、大写和小写字母、空格、表示换行的特殊字符以及任何顺序的符号。你知道一个对象是字符串,因为它以引号开头并以引号结尾。再次强调,用于结束字符串的引号类型必须与开始时使用的类型相同。
快速检查 7.1
以下哪些是有效的字符串对象?
1
"444"2
"finish line"3
'combo'4
checkered_flag5
"99 bbaalloonnss"
7.2. 字符串的基本操作
在处理字符串之前,你必须创建一个字符串对象并向其中添加内容。然后你可以通过对其执行操作来开始使用字符串。
7.2.1. 创建字符串对象
你可以通过初始化一个变量并将其绑定到对象来创建一个字符串对象。例如:
-
在语句
num_one = "one"中,变量num_one被绑定到一个类型为str的对象上,其值为"one"。 -
在语句
num_two = "2"中,变量num_two被绑定到一个类型为str的对象上,其值为"2"。重要的是要理解"2"是一个字符串,而不是整数2。
7.2.2. 理解字符串索引
因为字符串是由字符序列组成的,所以你可以确定字符串中某个位置的字符值。这被称为字符串的 索引,这是你可以对字符串对象执行的最基本操作。
在计算机科学中,你从 0 开始计数。这在操作字符串对象时使用。看看 图 7.1,它显示了一个值为 "Python rules!" 的字符串对象。每个字符都位于一个索引上。字符串中的第一个字符始终位于索引 0。对于字符串 "Python rules!",最后一个字符位于索引 12。
图 7.1. 字符串 “Python rules!” 及每个字符的索引。第一行显示使用正整数的索引,第二行显示使用负整数的索引。

你也可以反向计数。当你反向计数时,任何字符串的最后一个字符始终位于索引 -1。对于字符串 "Python rules!",第一个字符 P 位于索引 -13。注意,空格也是一个字符。
快速检查 7.2
对于字符串 "fall 4 leaves",以下字符的索引号是多少?给出正向和反向索引值:
1
42
f3
s
有一种特殊的方法可以用来索引字符串,以获取特定索引处的字符值。你使用方括号 [],并在方括号内放置任何你想要的索引值,只要它是整数值。以下是用字符串 "Python rules!" 的两个示例:
-
"Python rules!"[0]评估为'P'。 -
"Python rules!"[7]评估为'r'。
索引号可以是任何整数。如果它是负数会怎样?字符串中的最后一个字符被认为是索引 -1。你实际上是在逆序遍历字符串。
如果你将字符串赋值给一个变量,你也可以以更简洁的方式对其进行索引。例如,如果 cheer = "Python rules!",那么 cheer[2] 将给你 't' 的值。
快速检查 7.3
以下表达式评估结果是什么?在 Spyder 中尝试它们以进行检查!
1
"hey there"[1]2
"TV guide"[2]3
code = "L33t hax0r5" code[0] code[-4]
7.2.3. 理解字符串切片
到目前为止,你知道如何获取字符串中一个索引处的字符。但有时你可能想知道一组字符的值,从某个索引开始,到另一个索引结束。假设你是一名教师,以“##### FirstName LastName”的形式拥有你班级所有学生的信息。你只对名字感兴趣,并注意到前六个字符总是相同的:五个数字然后一个空格。你可以通过查看从第七个字符开始到字符串末尾的部分来提取你想要的数据。
以这种方式提取数据称为获取字符串的 子字符串。例如,字符串 s = "snap crackle pop" 中的字符 snap 是 s 的子字符串。
方括号可以用更复杂的方式使用。你可以使用它们在两个索引之间 切片 字符串并获取子字符串,根据某些规则。要切片字符串,你可以在方括号中放置最多三个整数,由冒号分隔:
[start_index:stop_index:step]
其中
-
start_index表示要取的第一个字符的索引。 -
stop_index表示你取字符的索引,但不包括stop_index处的字符。 -
step表示要跳过多少个字符(例如,取每第二个字符或每第四个字符)。正步长意味着你从左到右遍历字符串,反之亦然。
以下示例在 图 7.2 中展示,显示了字符被选择和组合的顺序,以给出最终的值。如果 cheer = "Python rules!",那么
-
cheer[2:7:1]的结果是'thon ',因为你从左到右步进,按顺序取每个字符,从索引 2 开始,不包括索引 7 的字符。 -
cheer[2:11:3]的结果是'tnu',因为你从左到右步进,每隔第三个字符取一个,从索引 2 开始,不包括索引 11 的字符。 -
cheer[-2:-11:-3]的结果是'sun',因为你从右到左步进,每隔第三个字符取一个,从索引 -2 开始,不包括索引 -11 的字符。
图 7.2. 字符串 “Python rules!” 切片的三种示例。每行的编号圆圈表示 Python 从字符串中检索字符的顺序,以形成新的子字符串。

快速检查 7.4
以下表达式评估为多少?尝试在 Spyder 中运行以检查自己!
1
"it's not impossible"[1:2:1]2
"Keeping Up With Python"[-1:-20:-2]3
secret = "mai p455w_zero_rD"
secret[-1:-8]
7.3. 字符串对象的其它操作
字符串是一个有趣的类型,因为你可以对字符串执行相当多的复杂操作。
7.3.1. 使用len()获取字符串中的字符数
假设你正在阅读学生的论文,并且你设定了 2,000 字符的限制。你如何确定学生使用了多少字符?你可以在一个字符串中设置整个论文,然后使用命令len()来获取字符串中的字符数。这包括引号之间的所有字符,包括空格和符号。空字符串的长度为 0。例如,
-
len("")的结果为0。 -
len("Boston 4 ever")的结果为13。 -
如果
a = "eh?",则len(a)的结果为3。
命令len()是特殊的,因为它可以用于其他类型的对象,而不仅仅是字符串对象。
你可以在字符串上执行的下几个操作将具有不同的外观。你将使用点表示法来对字符串对象发出命令。当你想要的命令是为仅与特定类型的对象一起工作时,你必须使用点表示法。例如,一个将字符串中的所有字母转换为大写的命令是为仅与字符串对象一起创建的。在数字上使用此命令没有意义,因此此命令在字符串对象上使用点表示法。
点表示法命令看起来与len()略有不同。你不需要在括号中放置字符串对象名称,而是将名称放在命令之前,并在它们之间放置一个点;例如,a.lower()而不是lower(a)。你可以将点视为表示仅与给定对象一起工作的命令——在这种情况下,是一个字符串。这涉及到一个更深入的概念,称为面向对象编程,你将在第 30 课中更详细地了解课程 30。
7.3.2. 使用upper()和lower()在字母大小写之间转换
假设你正在阅读学生的论文,其中一名学生将所有内容都写成大写字母。你可以在字符串中设置论文,然后更改字符串中字母的大小写。
有一些命令可用于操作字符串的大小写。这些命令仅影响字符串中的字母字符。数字和特殊字符不受影响:
-
lower()将字符串中的所有字母转换为小写。例如,"Ups AND Downs".lower()的结果为'ups and downs'。 -
upper()将字符串中的所有字母转换为大写。例如,"Ups AND Downs".upper()的结果为'UPS AND DOWNS'。 -
swapcase()将字符串中的小写字母转换为大写,反之亦然。例如,"Ups AND Downs".swapcase()的结果为'uPS and dOWNS'。 -
capitalize()将字符串中的第一个字符转换为大写字母,并将其余字母转换为小写。例如,"a long Time Ago...".capitalize()的结果为'A long time ago... '。
快速检查 7.5
给定 a = "python 4 ever&EVER"。评估以下表达式。然后尝试在 Spyder 中检查自己:
1
a.capitalize()2
a.swapcase()3
a.upper()4
a.lower()
摘要
在本课中,我的目标是教你们关于字符串对象的知识。你们看到了如何通过索引字符串来获取每个位置的元素,以及如何切片字符串来获取子字符串。你们还看到了如何获取字符串的长度,以及如何将所有字母转换为小写或大写。以下是主要收获:
-
字符串是由单个字符字符串组成的序列。
-
字符串对象由引号表示。
-
你可以对字符串执行许多操作来操作它们。
让我们看看你是否掌握了这些...
Q7.1
编写一个或多个使用字符串
"Guten Morgen"来获取TEN的命令。有多种方法可以实现这一点。Q7.2
编写一个或多个使用字符串
"RaceTrack"来获取Ace的命令。
第 8 课. 高级字符串操作
在阅读 第 8 课 之后,你将能够
-
操作子串
-
对字符串进行数学运算
如果你得到一个长文件,通常会将整个文件作为一个大字符串来读取。但是处理这样一个大字符串可能会很麻烦。你可能要做的一件有用的事情是将它分解成更小的子串——通常是通过换行符,这样每个段落或每个数据条目都可以单独查看。另一件有益的事情是找到相同单词的多个实例。你可以决定使用单词 very 超过 10 次是令人烦恼的。或者如果你正在阅读某人的获奖感言的记录,你可能想要找到所有 like 的实例并在发布之前将其删除。
考虑以下情况
在研究青少年发短信的方式时,你收集了一些数据。你得到了一个包含许多行的长字符串,格式如下:
-
0001: gr8 lets meet up 2day
-
0002: hey did u get my txt?
-
0003: ty, pls check for me
-
...
既然这是一个原始的大字符串,那么你可以采取哪些步骤来通过分析使其数据更易于接近?
答案:
1. 将大数据字符串分割成每行的子串。
2. 将常见的首字母缩略词替换为正确的单词(例如,pls 替换为 please)。
3. 统计某些单词出现的次数,以便报告最受欢迎的首字母缩略词。
8.1. 与子串相关的操作
在 第 7 课 中,你学习了当你知道要使用哪些索引时如何从字符串中检索子串。你可以执行更高级的操作,这些操作可以给你更多关于字符串组成的信息。
8.1.1. 使用 find() 函数在字符串中查找特定子串
假设你电脑上有一长串文件名,你想知道某个特定文件是否存在,或者你想要在文本文档中搜索一个单词。你可以通过使用 find() 命令在更大的字符串中找到特定的区分大小写的子串。
与操作大小写的命令一样,你写下要执行操作的字符串,然后是一个点,然后是命令名称,然后是括号。例如:"some_string".find()。注意,空字符串 '' 在每个字符串中都是存在的。
但这还不是全部。此外,你还通过将子串放入括号中来告诉命令你想要查找什么子串——例如,"some_string".find("ing")。
你想要查找的子串必须是一个字符串对象。你得到的结果是子串在字符串中开始的索引(从 0 开始)。如果有多个子串匹配,你得到第一个找到的子串的索引。如果子串不在字符串中,你得到 -1。例如,"some_string".find("ing") 的结果是 8,因为 "ing" 在 "some_string" 中从索引 8 开始。
如果你想要从字符串的末尾开始查找子字符串而不是从开头开始,你可以使用不同的命令,rfind()。rfind中的r代表反向查找。它查找离字符串末尾最近的子字符串,并报告子字符串开始的位置(从 0 开始)。
如果你设置 who = "me myself and I",那么图 8.1 展示了如何评估以下内容:
-
who.find("and")的评估结果为10,因为子字符串从索引 10 开始。 -
who.find("you")的评估结果为-1,因为子字符串不在字符串中。 -
who.find("e")的评估结果为1,因为子字符串的第一个出现位置在索引 1。 -
who.rfind("el")的评估结果为6,因为离字符串末尾最近的子字符串的第一个出现位置在索引 6。
图 8.1. 在字符串"me myself and I"中查找子字符串的四个示例。箭头指示查找子字符串的方向。勾号告诉你找到子字符串的索引。一个 x 告诉你子字符串没有找到。

快速检查 8.1
给定a = "python 4 ever&EVER"。评估以下表达式。然后在 Spyder 中尝试它们以检查自己:
1
a.find("E")2
a.find("eve")3
a.rfind("rev")4
a.rfind("VER")5
a.find(" ")6
a.rfind(" ")
8.1.2. 使用“in”查找字符串中是否存在子字符串
find和rfind操作会告诉你子字符串的位置。有时你只想知道子字符串是否在字符串中。这是find和rfind的一个小变化。当你不需要知道子字符串的确切位置时,你可以更有效地使用这个问题的“是”或“否”答案。因为只有两个值,所以这个问题的答案是布尔类型的对象,你得到的结果将是True或False。用于找到这个问题的答案的操作使用关键字in。例如,"a" in "abc"是一个评估为True的表达式,因为字符串"a"在字符串"abc"中。关键字in在 Python 中经常使用,因为它使得你编写的很多代码看起来非常像英语。
快速检查 8.2
给定a = "python 4 ever&EVER"。评估以下表达式。然后在 Spyder 中尝试它们以检查自己:
1
"on" in a2
"" in a3
"2 * 2" in a
8.1.3. 使用 count()计算子字符串出现的次数
尤其是在编辑文档时,您会发现确保您没有过度使用单词很有用。假设您正在编辑一篇文章,并发现在前一段落中,您已经使用了单词 so 五次。您不必手动计算整个文章中该单词出现的次数,而是可以通过对字符串进行操作自动找到子字符串 "so" 出现的次数。
您可以使用 count() 方法来计算一个子字符串在字符串中出现的次数,这将返回一个整数。例如,如果您有 fruit = "banana",那么 fruit.count("an") 的结果为 2。关于 count() 的一个重要点是它不会计算重叠的子字符串。fruit.count("ana") 的结果为 1,因为 "a" 在 "ana" 的两次出现之间重叠,如图 8.2 所示。
图 8.2. 计算字符串 "banana" 中 "ana" 出现的次数。答案是 1,因为 "a" 在两次出现之间重叠,而 Python 的 count() 命令不考虑这一点。

快速检查 8.3
给定 a = "python 4 ever&EVER",评估以下表达式。然后尝试在 Spyder 中运行它们以检查自己:
1
a.count("ev")2
a.count(" ")3
a.count(" 4 ")4
a.count("eVer")
8.1.4. 使用 replace() 替换子字符串
假设您的儿子写了一篇关于他最喜欢的水果——苹果的简短报告。报告截止的那天早上,他改变了主意,不再喜欢苹果,现在喜欢梨了。您可以将其整个报告作为一个字符串,并轻松地替换所有 apple 单词为 pear。
最后一个有用的字符串操作是将字符串中的一个子字符串替换为另一个子字符串。这个命令与之前的命令一样作用于字符串,但您必须在括号中输入两个项目,用逗号分隔。第一个项目是要查找的子字符串,第二个项目是替换的子字符串。这个命令会替换所有出现。例如,"variables have no spaces".replace(" ", "_") 将字符串 "variables have no spaces" 中的所有空格字符串替换为下划线字符串,并计算为 "variables_have_no_spaces"。
快速检查 8.4
给定 a = "Raining in the spring time.",评估以下表达式。然后尝试在 Spyder 中运行它们以检查自己:
1
a.replace("R", "r")2
a.replace("ing", "")3
a.replace("!", ".")4
b = a.replace("time","tiempo")
8.2. 数学运算
您可以在字符串对象上执行两种数学运算:加法和乘法。
加法操作只能在两个字符串对象之间进行,这被称为连接。例如,"one" + "two"的结果是'onetwo'。当你把两个字符串相加时,你会按照加法的顺序把每个字符串的值放在一起,以创建一个新的字符串对象。你可能想要把一个字符串加到另一个字符串上,比如,如果你有三个人共同完成了一份报告,并且各自写了不同的部分;剩下的只是将第一部分、第二部分和最后第三部分合并起来。
乘法操作只能在字符串对象和整数之间进行,这被称为重复。例如,3 * "a"的结果是'aaa'。当你用一个整数乘以一个字符串时,字符串会被重复那么多次。用数字乘以字符串通常用于节省时间和提高精度。例如,假设你想创建一个字符串,代表玩“猜字谜”游戏时所有的未知字母。你不必初始化一个字符串为"----------",而是可以这样做:"-" * 10。这在事先不知道要猜测的单词大小的情况下特别有用,你可以将大小存储在一个变量中,然后乘以"-"字符。
快速检查 8.5
评估以下表达式。然后在 Spyder 中尝试它们以检查自己:
1
"la" + "la" + "Land"2
"USA" + " vs " + "Canada"3
b = "NYc" c = 5 b * c4
color = "red" shape = "circle" number = 3 number * (color + "-" + shape)
概述
在本课中,我的目标是教你更多关于字符串对象的操作,特别是与子字符串相关的操作。你学习了如何查找子字符串是否在字符串中,获取其索引位置,计算其出现的次数,以及替换所有子字符串的出现。你还看到了如何将两个字符串相加,以及用数字乘以字符串的含义。以下是主要收获:
-
你只需通过几个操作就可以操纵一个字符串,使其看起来是你想要的样子。
-
连接两个字符串意味着你正在将它们相加。
-
重复一个字符串意味着你正在用数字乘以字符串。
让我们看看你是否掌握了这个...
Q8.1
编写一个程序,初始化一个字符串值为
"Eat Work Play Sleep repeat"。然后,使用你迄今为止学到的字符串操作命令来获取字符串"working playing"。
第 9 课. 简单错误信息
在阅读第 9 课之后,你将能够
-
理解错误信息出现的位置
-
培养阅读错误信息的直觉
在你开始编程时,有一件重要的事情要记住,那就是你不能编写一个会损坏你电脑的程序。如果出了任何问题,你总是可以关闭 Spyder 并重新启动它,而不会影响你电脑上其他任何东西的运行。
在编写和测试程序时犯错误是正常的。如果在生产环境中留下任何错误,可能会导致您的程序在客户使用时崩溃,从而导致差评。
9.1. 输入语句并尝试操作
你不应该害怕在 Spyder(控制台或文件编辑器)中尝试命令以查看会发生什么。这是培养你对迄今为止所见对象进行操作直觉的最好方法。如果你发现自己问,“如果...会发生什么?”,那么你很可能会亲自测试并立即得到答案。
9.2. 理解字符串错误信息
到目前为止,你已经看到了一些可以在 Python 字符串上进行的简单操作。我希望你在 Spyder 中尝试过一些操作。如果你尝试过,你可能尝试做一些不允许的操作,在这种情况下,你会收到一个错误信息。
例如,你可能想知道当你使用比字符串长度大的整数索引字符串时会发生什么。图 9.1 展示了以两种方式尝试这样做的情况:在控制台或编辑器中。在控制台,当你按下 Enter 键尝试索引列表中的行时,你会立即收到错误信息。错误名称首先显示(IndexError),然后是错误的一个简短说明(字符串索引超出范围)。
图 9.1. 尝试使用比字符串长度大的数字索引字符串时的错误信息

在文件编辑器中,你可以编写可能导致错误的代码行,但错误只有在运行代码时才会显现(通过点击工具栏顶部的绿色箭头)。在图 9.1 中,当你执行编辑器中的第 9 行时,你试图在列表中索引得太远。因为你正在运行一个 Python 文件,控制台上会显示更多信息,但重要的是所有文本的末尾。它显示了导致错误的行以及与控制台案例相同的错误名称和描述。
当你编写越来越复杂的程序时,你会遇到很多错误。不要害怕犯错,因为它们提供了很好的学习机会。
摘要
在这节课中,我的目标是教会你错误信息是有用的,并且可以引导你找到导致错误的行。不要害怕尝试命令来弄清楚各种命令或命令组合的作用。
让我们看看你是否明白了...
Q9.1
在控制台或编辑器中输入以下命令。然后根据第 7 课和第 8 课中的字符串命令,看看你是否能理解错误的意思:
"hello"[-6]"hello".upper("h")"hello".replace("a")"hello".count(3)"hello".count(h)"hello" * "2"
第 10 课。元组对象:任何类型的对象序列
在阅读了第 10 课之后,你将能够
-
通过使用元组创建任何类型的对象序列
-
对元组对象进行一些操作
-
通过使用元组交换变量值
假设我给你一个简单的任务,就是跟踪你最喜欢的超级英雄角色。比如说你有三个:蜘蛛侠、蝙蝠侠和超人。
使用你目前所学的知识,你可以尝试创建一个包含所有这些名称的字符串,名称之间用空格分隔,如下所示:"Spiderman Batman Superman"。使用你在第 7 课和第 8 课中学到的命令,你只需付出一点努力和关注,就能跟踪字符串中的索引并按需提取每个名称。
但如果你在字符串中保留了全名,如下所示:"Peter Parker Bruce Wayne Clark Kent"。现在提取每个人的名字变得更加困难,因为名字的第一部分和最后一部分也由空格分隔。你可以使用其他特殊字符,如逗号,来分隔全名,但这并不能解决使用字符串存储此数据最令人烦恼的问题:提取感兴趣的项目很麻烦,因为你必须跟踪起始和结束索引。
考虑这一点
看看你的冰箱。写下你能看到的所有物品,用逗号分隔。现在看看你的衣服篮子。写下你能看到的所有物品,用逗号分隔。
在每一组物品中:
-
你写下了多少个物品?
-
第一项是什么?中间项是什么(如果你有偶数个物品,向下取整)?
答案:
冰箱:牛奶,奶酪,花椰菜,胡萝卜,鸡蛋
-
五个物品
-
首先:牛奶;中间:花椰菜
购物袋:T 恤,袜子
-
两个物品
-
首先:T 恤;中间:T 恤
10.1. 元组作为数据序列
字符串存储字符序列。如果有一种方法可以存储序列中的单个对象,而不是仅限于字符串字符,那就方便多了。随着你编写更复杂的代码,能够表示任何类型的对象序列就变得很有用。
10.1.1. 创建元组对象
Python 有一种数据类型可以表示任何对象的序列,而不仅仅是单个字符的字符串。这种数据类型是元组。与字符串用引号表示的方式相同,元组用括号表示,()。元组中的各个对象由逗号分隔。一个元组的例子是 (1, "a", 9.9)。其他元组的例子如下:
-
()—一个空元组。 -
(1, 2, 3)—包含三个整数对象的元组。 -
("a", "b", "cde", "fg", "h")—包含五个字符串对象的元组。 -
(1, "2", False)—包含一个整数、一个字符串和一个布尔对象的元组。 -
(5, (6, 7))—包含一个整数和由两个整数组成的另一个元组的元组。 -
(5,)—包含单个对象的元组。注意额外的逗号,它告诉 Python 括号用于保持单元素元组,而不是在数学运算中表示优先级。
快速检查 10.1
以下哪些是有效的元组对象?
1
("carnival",)2
("ferris wheel", "rollercoaster")3
("tickets")4
((), ())
10.2. 理解元组操作
元组是字符串的更通用版本,因为元组中的每个项都是独立的对象。元组上的许多操作与字符串上的操作相同。
10.2.1. 使用 len() 获取元组长度
请记住,len() 命令可以用于其他对象,而不仅仅是字符串。当您在元组上使用 len() 时,您得到一个表示元组内部对象数量的值。例如,表达式 len((3, 5, "7", "9")) 表示您正在查找元组 (3, 5, "7", "9") 的长度(对象数量)。该表达式计算结果为 4,因为该元组有四个元素。
快速检查 10.2
评估以下表达式。然后尝试在 Spyder 中检查:
1
len(("hi", "hello", "hey", "hi"))2
len(("abc", (1, 2, 3)))3
len(((1, 2),))4
len(())
10.2.2. 使用 [] 索引和切片元组
因为元组是对象的序列,所以对元组的索引与对字符串的索引相同。您使用 [] 操作符,第一个对象位于索引 0,第二个对象位于索引 1,依此类推。例如,
-
(3, 5, "7", "9")[1]计算结果为5。 -
(3, (3, 5), "7", "9")[1]计算结果为(3, 5)。
与字符串相比,有一个特殊的情况是元组中的一个对象是另一个元组。例如,(3, (3, ("5", 7), 9), "a") 是一个元组,其索引为 1 的对象是另一个元组 (3, ("5", 7), 9)。反过来,该对象也可以进行索引。
您可以通过一系列索引操作来访问嵌套元组中的元素。例如,(3, (3, ("5", 7), 9), "a")[1][1][1] 计算结果为 7。这有点棘手,因为元组中可以嵌套其他元组。 展示了如何可视化这个表达式。
图 10.1. 元组 (3, (3, ("5", 7), 9), "a") 的结构。虚线表示元组中的独立对象。

逐步进行,您可以这样评估元组:
-
(3, (3, ("5", 7), 9), "a")[1]计算结果为元组(3, ("5", 7), 9)。 -
(3, ("5", 7), 9)[1]计算结果为元组("5", 7)。 -
("5", 7)[1]计算结果为7.
切片元组与切片字符串相同,遵循相同的规则。但你需要小心地认识到,你可能在某个位置有其他元组作为元素。
快速检查 10.3
评估以下表达式。然后在 Spyder 中尝试它们以检查自己:
1
("abc", (1, 2, 3))[1]2
("abc", (1, 2, "3"))[1][2]3
("abc", (1, 2), "3", 4, ("5", "6"))[1:3]4
a = 0 t = (True, "True") t[a]
10.2.3. 执行数学运算
你可以在元组上执行与字符串上允许执行相同的操作:加法和乘法。
你可以将两个元组相加以连接它们。例如,(1, 2) + (-1, -2)的结果是(1, 2, -1, -2)。
你可以用一个整数乘以一个元组,得到一个包含原始元组重复该次数的元组。例如,(1, 2) * 3的结果是(1, 2, 1, 2, 1, 2)。
快速检查 10.4
评估以下表达式。然后在 Spyder 中尝试它们以检查自己:
1
len("abc") * ("no",)2
2 * ("no", "no", "no")3
(0, 0, 0) + (1,)4
(1, 1) + (1, 1)
10.2.4. 交换元组内的对象
在本节中,你将看到另一种使用元组的有意思的方法。如果你要交换的变量是元组的元素,你可以使用元组来交换与变量名关联的对象值。例如,假设你开始时有这两个变量:
long = "hello"
short = "hi"
你想要编写一行代码,实现以下交换的等效结果:
long = "hi"
short = "hello"
图 10.2 展示了你应记住的以下代码的可视化,该代码实现了交换:
long = "hello"
short = "hi"
(short, long) = (long, short)
图 10.2. 使用元组在变量名之间交换两个对象的值

你开始时将"hello"绑定到变量long,将"hi"绑定到变量short。在执行(short, long) = (long, short)这一行后,short的值变为"hello",而long的值变为"hi"。
你可能认为等号左边的两个变量是不允许的。但回想一下,变量是在元组对象的上下文中,并且被括号包围的,所以等号左边的项目只有一个对象,一个元组。这个元组在其每个索引处都有绑定到其他对象的变量。你将看到以这种方式使用元组的原因在第 19 课中是有用的。
快速检查 10.5
编写一行代码来交换以下值:
1
s = "strong" w = "weak"2
yes = "affirmative" no = "negative"
概述
在这节课中,我的目标是教你关于元组以及它们如何以类似字符串的方式表现。你学习了如何通过索引元组来获取每个位置的元素,以及如何切片元组来获取其内部的元素;这些元素可以是原始对象,甚至可能是元组本身。与字符串不同,元组内的对象可以是另一个元组,从而在元组内部嵌套元组。最后,你可以使用元组来交换两个变量的值。以下是主要收获:
-
元组是任何类型对象的序列,甚至是其他元组。
-
你可以对多级元组进行索引。
-
你可以使用元组来交换变量的值。
让我们看看你是否掌握了这个...
Q10.1
编写一个程序,初始化字符串
word = "echo",空元组t = ()和整数count = 3。然后,使用你在这节课中学到的命令编写一系列命令,使t = ("echo", "echo", "echo", "cho", "cho", "cho", "ho", "ho", "ho", "o", "o", "o")并打印它。原始单词被添加到元组的末尾,然后添加没有第一个字母的原始单词到元组中,依此类推。原始单词的每个子串都会重复count次数。
第 11 课。与用户交互
在阅读第 11 课之后,你将能够
-
向用户打印值
-
向用户请求输入
-
将用户输入存储在变量中并对其进行操作
许多程序在幕后进行计算,但其中很少没有用户输入就变得有用。你想要编写程序的一个主要原因是向用户提供某种体验;这种体验依赖于用户和程序之间的互动。
考虑这一点
找到另一个人并与之交谈。你可以问什么样的问题?你得到什么回答?你能否基于你得到的特定回答进行扩展?
答案:
-
你好吗?
-
很好,期待周末。
-
我也是!周末有什么计划吗?
-
是的,我们要去露营,然后参观科学博物馆。如果有时间,也许可以去海滩,然后出去吃一顿美味的晚餐。你呢?
-
看电视。
11.1. 显示输出
要开始本节课,回想一下你可以在 Python 中使用print()命令在控制台上向用户显示值。从现在开始,你将经常使用print。
11.1.1. 打印表达式
你可以将任何表达式放在print()的括号内,因为所有表达式都会计算出一个值。例如,浮点数3.1的值是3.1,表达式3 * "a"的值是"aaa"。
列表 11.1 展示了如何将几个表达式的值打印给用户。括号中可以包含相当复杂的表达式。本列表中的代码打印出以下内容:
hello!
89.4
abcdef
ant
列表 11.1. 打印表达式
print("hello!") *1*
print(3*2*(17-2.1)) *2*
print("abc"+"def") *3*
word = "art" *4*
print(word.replace("r", "n")) *5*
-
1 一个字符串
-
2 一个数学表达式
-
3 连接两个字符串
-
4 创建一个变量
-
5 将“r”替换为“n”
注意,在本列表的每个示例中,你放入括号中的内容不一定是str类型的对象。例如,print(3*2*17-2.1))计算出的结果是float类型的对象。print命令可以与括号中的任何类型的对象一起使用。
快速检查 11.1
在编辑器中将每个语句写入文件并运行该文件。以下语句会打印出什么内容(如果有)?在 Spyder 中输入它们来检查自己:
1
print(13 - 1)2
"很好"3
a = "nice"4
b = " is the new cool" print(a.capitalize() + b)
11.1.2. 打印多个对象
在 print 后面的括号中放置多个对象,并混合使用它们的类型是可能的。如果您想放入不同的对象,请用逗号分隔每个对象。Python 解释器会自动在打印对象的值之间插入一个空格。如果您不希望有额外的空格,您必须将每个对象都转换为字符串,将它们连接起来,并在 print 的括号中使用这个结果。列表 11.2 展示了一个示例。在程序中,您想将一个数字除以另一个数字并打印结果。列表中的代码打印以下内容:
1 / 2 = 0.5
1/2=0.5
注意,打印的第一行在每两个对象之间有一个空格,但第二行没有。
列表 11.2. 打印多个对象
a = 1 *1*
b = 2 *1*
c = a/b *2*
print(a,"/",b,"=",c) *3*
add = str(a)+"/"+str(b)+"="+str(c) *4*
print(add) *5*
-
1 初始化变量
-
2 计算除法
-
3 使用逗号分隔整数(变量 a、b 和 c)和字符串("/" 和 "=")
-
4 使用 (str) 将整数转换为字符串,然后使用 + 运算符将它们与字符串 “/” 和 “=” 连接起来
-
5 打印字符串
快速检查 11.2
将以下每个点转换为 Python 语句以创建程序。完成后,运行程序以查看打印的内容:
1
创建一个名为
sweet的变量,其字符串值为"cookies"。2
创建一个名为
savory的变量,其字符串值为"pickles"。3
创建一个名为
num的变量,其整数值为100。4
编写一个
100 pickles and 100 cookies。5
编写一个
I choose the COOKIES!
11.2. 获取用户输入
创建程序的乐趣在于您能够与用户互动。您想使用用户的输入来指导计算、计算和操作。
11.2.1. 提示用户
您可以使用 input() 命令从用户那里获取输入。假设您想要求用户输入他们的名字。在 input() 的括号中,您放入一个表示用户提示的字符串对象。例如,以下行
input("What's your name? ")
将在控制台上显示以下文本,然后等待用户输入一些内容:
What's your name?
注意提示字符串末尾的额外空格。图 11.1 展示了带空格和不带空格的字符串提示之间的区别。您可以看到,用户将输入的任何文本都立即开始于提示字符串的末尾。一个好的规则是在您的提示字符串的末尾留一个空格,这样用户就可以区分提示和他们的输入。
图 11.1. 如何提示用户输入

快速检查 11.3
为以下每个编写一行代码:
1
请用户告诉你一个秘密。
2
请用户告诉他们最喜欢的颜色。
3
请用户输入以下任意一个字符:
#,$,%,&或*.
11.2.2. 读取输入
在提示用户输入后,你等待用户输入一些内容。如果你在测试你的程序,你可以扮演用户角色并自己输入不同的内容。用户通过按下 Enter 键来表示他们已完成输入。在那个时刻,你的程序将继续执行请求输入之后的下一行。
下面的代码示例展示了如何编写一个程序,让用户输入他们居住的城市。无论用户输入什么,程序都会始终打印 我住在波士顿。
列表 11.3. 用户住在哪里?
input("Where do you live? ") *1*
print("I live in Boston.") *2*
-
1 提示用户输入,此时程序停止并等待用户输入
-
2 用户按下 Enter 键后,程序执行并结束。
注意,程序并没有对用户输入进行任何操作。这是一个交互式程序,但它并不特别有趣或有用。更复杂的程序会将用户输入存储到变量中,然后对其进行操作。
11.2.3. 将输入存储在变量中
大多数程序都会对用户输入进行操作。用户输入的任何内容都会被转换为字符串对象。因为它是一个对象,所以你可以通过将用户的输入赋值给变量来将其绑定到变量上。例如,word_in = input("你最喜欢的单词是什么? ") 会将用户输入的内容存储在名为 word_in 的变量中。
下面的代码示例展示了如何使用用户输入来打印一个更定制的消息。无论用户输入什么,你都会将他们输入的第一个字母大写,在末尾添加一个感叹号,然后打印结果以及一条最终消息。
列表 11.4. 存储用户输入
user_place = input("Where do you live? ") *1*
text = user_place.capitalize() + "!" *2*
print(text) *3*
print("I hear it's nice there!") *3*
-
1 获取用户输入并将其存储在变量 user_place 中
-
2 连接两个字符串:用户输入的大写形式加上感叹号
-
3 打印一个定制的消息
在将用户输入作为字符串获取后,你可以对它执行任何允许在字符串上进行的操作。例如,你可以将其转换为小写或大写,查找子字符串的索引,并检查某些子字符串是否在用户的输入中。
快速检查 11.4
对于以下每个要求,编写代码行以实现示例输出:
1
请用户告诉他们最喜欢的歌曲的名字。然后分别打印歌曲的名字三次。
2
请用户输入一个名人的名字和姓氏。然后在一行打印名字,在另一行打印姓氏。
11.2.4. 将用户输入转换为不同类型
用户输入的任何内容都会转换为字符串对象。当你想编写处理数字的程序时,这并不方便。
列表 11.5 显示了一个程序,该程序要求用户输入一个数字,并打印该数字的平方。例如,如果用户输入5,程序将打印25。
你需要了解这个程序的一些事情。如果用户输入的不是整数,程序将立即结束并显示错误,因为 Python 不知道如何将任何非字符串整数值转换为整数对象。通过输入a或2.1作为用户输入来运行列表 11.5 程序;两者都会导致程序崩溃并显示错误。
当用户输入一个有效的数字(任何整数)时,请记住,尽管它看起来像数字,但用户输入的每一项都是字符串。如果用户输入5,Python 将其视为字符串"5"。为了与数字一起工作,你必须首先通过类型转换将字符串转换为整数——在字符串对象周围加上括号,并在字符串对象之前加上类型int。
列表 11.5. 使用用户输入进行计算
user_input = input("Enter a number to find the square of: ") *1*
num = int(user_input) *2*
print(num*num) *3*
-
1 获取用户输入并将其存储
-
2 将用户的输入转换为整数
-
3 打印数字的平方。前两行可以合并为
num = int(input("输入一个数字以找到其平方:"))。
快速检查 11.5
Q1:
修改程序,使其在列表 11.5 中打印到控制台的是十进制数字。
11.2.5. 请求更多输入
你可以编写从用户那里请求多个输入的程序。列表 11.6 显示了一个程序,该程序要求用户输入一个数字,然后另一个,并打印这些数字的乘积结果。但除了只打印结果外,你还在打印有用的附加文本,告诉用户你正在执行的操作以及操作的对象。例如,如果用户输入4.1和2.2,程序将显示4.1 * 2.2 = 9.02。
列表 11.6. 使用多个用户输入进行计算
num1 = float(input("Enter a number: ")) *1*
num2 = float(input("Enter another number: ")) *2*
print(num1, "*", num2, "=", num1*num2) *3*
-
1 获取一个数字并将其转换为浮点数
-
2 获取另一个数字并将其转换为浮点数
-
3 以显示你正在乘的两个数字及其结果的方式来美化打印乘法
摘要
在本课中,我的目标是教你们如何显示输出以及如何从用户那里获取输入。你们了解到,只需使用一个print语句就可以打印多个对象,并且 Python 会在每个对象之间自动添加一个空格。
你们学习了如何使用input()命令等待用户输入。该命令将用户输入的任何内容转换为字符串对象。如果你想在程序中处理数字,你必须自己将输入转换为适当的类型。以下是主要收获:
-
print可以一次用于多个对象。 -
你可以多次请求用户输入。每次,程序都会暂停并等待用户输入一些内容,用户通过按下回车键来表示他们已完成输入。
-
你可以将用户输入转换为其他类型,以便对其进行适当的操作。
让我们看看你是否掌握了这个...
Q11.1
编写一个程序,请求用户输入两个数字。将这些数字存储在变量
b和e中。程序计算并打印出幂b^e,并显示相应的信息。Q11.2
编写一个程序,请求用户输入他们的名字和年龄。使用合适的变量名来存储这些变量。计算用户在 25 年后的年龄。例如,如果用户输入
Bob和10,程序应该打印Hi Bob! In 25 years you will be 35!
第 12 课。综合项目:名字混合
在阅读第 12 课之后,你将能够
-
编写代码来解决编程任务
-
阅读程序的要求
-
从用户那里获取两个名和姓的输入,将它们混合(以某种方式组合),并向用户展示结果
-
系统性地构建代码来编写程序解决方案
问题
这是你的第一个交互式编程任务,所以让我们和用户一起玩得开心!你想要编写一个程序,能够自动将用户提供的两个名字组合起来。这是一个开放性问题陈述,所以让我们添加一些更多细节和限制:
-
告诉用户以“名 姓”的格式提供给你两个名字。
-
向用户展示两种可能的新名字,格式为“名 姓”。
-
新的名字是用户给出的名字的组合,新的姓是用户给出的姓的组合。例如,如果用户给你的是
Alice Cat和Bob Dog,一个可能的混合名字是Bolice Dot。
-
12.1. 理解问题陈述
你之前看到的检查点练习都很简单。这是你的第一个复杂的程序,你将不得不思考如何完成任务,而不是立即开始编码。
当你遇到一个问题陈述时,你应该寻找以下内容:
-
程序应完成的一般描述
-
你应该从用户那里获取的输入(如果有的话)
-
程序应输出的内容
-
程序在不同情况下的行为
你应该首先使用对你有效的方法来组织你对你所分配的任务的想法。理想情况下,你将完成以下三项:
-
绘制草图以理解所提要求
-
提出几个你可以用来测试你代码的例子
-
将你的草图和示例抽象成伪代码
12.1.1. 绘制问题的草图
在这个问题中,你被要求从用户那里获取输入。用户会给你两个名字。然后你将名字分开成名和姓。接下来,你将两个名字混合起来。同样,你将两个姓混合起来。最后,你将向用户展示你的新名字混合。展示了这个问题的三个部分。
图 12.1. 绘制程序的输入,输入名字的样本混合,以及展示给用户的输出

12.1.2. 提出几个例子
在你对程序的主要部分有了一个想法之后,你应该想出一些例子,你可以使用这些例子来测试你的程序。
这是一个重要的步骤。作为程序员,你需要模拟用户可能会输入到程序中的内容。用户是不可预测的,他们最喜欢的消遣之一就是试图让你的程序崩溃。
在这个步骤中,你应该尝试尽可能多地想出不同的输入。考虑短名、长名以及不同长度的名和姓的组合。是否有任何独特的名字?以下是一些用于测试的姓名样本类型:
-
具有两个字母的姓名(CJ Cool 和 AJ Bool)
-
具有多个字母的姓名(Moonandstarsandspace Knight)
-
具有偶数个字母的姓名(Lego Hurt)
-
具有奇数个字母的姓名(Sting Bling)
-
具有相同字母的姓名(Aaa)
-
两个相同的姓名(Meg Peg 和 Meg Peg)
你应该坚持使用不偏离用户输入内容的示例。在这种情况下,你要求用户输入名和姓。你不对用户输入与这些内容不匹配时程序如何工作做出任何保证。例如,输入 Ari L Mermaid 的用户不应期望程序按广告宣传的方式工作。
12.1.3. 将问题抽象为伪代码
现在你可以将你的程序划分为代码的块。在这个步骤中,你开始编写伪代码:英语和编程语法的混合。每个块将处理程序中的单独步骤。每个步骤的目的是在变量中收集数据,以便在后续步骤中使用。以下是程序中的主要步骤:
-
获取用户输入并将其存储在变量中。
-
将全名拆分为名和姓,并将它们存储在变量中。
-
决定你将如何拆分姓名。例如,在每个名和姓中找到中间点。将每个姓名的前半部分存储在变量中,并将每个姓名的后半部分存储在变量中。
-
将一个姓名的前半部分与另一个姓名的后半部分组合。重复此操作,直到你想要的第一和姓的组合。
接下来的几节将详细讨论这些步骤。
12.2. 拆分名和姓
你会注意到到目前为止你所做的一切都是为了试图理解问题中要求的内容。编码应该是最后一步,以减少你可能遇到的错误数量。
到目前为止,你可以开始编写单个语句块。在编写交互式程序时,你几乎总是从获取用户输入开始。以下列表显示了实现此功能的代码行。
列表 12.1. 获取用户输入
print("Welcome to the Mashup Game!")
name1 = input("Enter one full name (FIRST LAST): ") *1*
name2 = input("Enter another full name (FIRST LAST): ") *1*
- 1 请求用户以期望的格式输入
用户输入现在存储在两个变量中,具有适当的名称。
12.2.1. 找到名和姓之间的空格
在获取用户输入后,你应该将其拆分为名和姓。你将不得不将名和姓混合在一起,因此存储在一个变量中的全名没有帮助。拆分全名的第一步是找到名和姓之间的空格。
在第 7 课中,你学习了可以与字符串进行的各种操作。一个操作,find,可以告诉你特定字符的索引位置。在这种情况下,你感兴趣的是空格字符的索引," "。
12.2.2. 使用变量保存计算值
现在你将名字和姓氏保存到变量中以便以后使用。图 12.2 展示了如何通过空格的索引分割全名。
图 12.2. 通过空格的索引将全名分割成名字和姓氏

你首先找到空格的索引位置。从全名的开头到空格的位置是名字。从空格后的一个字母开始是姓氏。
你应该存储名字和姓氏,以便以后可以处理它们。列表 12.2 展示了如何做这件事。你使用字符串上的find操作来获取空格字符的索引位置。
知道这个位置后,你可以从全名的开头(从索引 0 开始)取到空格字符索引,并将该子字符串存储为名字。回想一下,使用some_string[a:b]对字符串进行索引意味着你从索引a到索引b - 1取所有字母。要获取姓氏,你从space字符索引后的一个字符开始,取到全名的末尾(包括字符串中最后一个字符的索引)。
列表 12.2. 在变量中存储名字和姓氏
space = name1.find(" ") *1*
name1_first = name1[0:space] *2*
name1_last = name1[space+1:len(name1)] *3*
space = name2.find(" ") *4*
name2_first = name2[0:space] *4*
name2_last = name2[space+1:len(name2)] *4*
-
1 获取并存储空格字符的索引,该字符区分名字和姓氏
-
2 从全名的开头取到空格字符,以存储姓氏
-
3 从空格后的一个字符开始,取到全名的末尾,以存储姓氏
-
4 对第二个名字重复操作
在这个阶段,你已经将两个名字和两个姓氏存储在变量中。
12.2.3. 测试到目前为止的结果
在列表 12.2 中编写的代码完成后,现在是时候在几个测试用例上运行程序了。测试到目前为止的代码意味着打印你创建的变量的值。你应该检查输出是否符合你的预期。如果你输入Aqua Man和Cat Woman,print语句
print(name1_first)
print(name1_last)
print(name2_first)
print(name2_last)
将打印出以下内容:
Aqua
Man
Cat
Woman
这一切看起来都正常,所以你可以继续代码的下一部分。
12.3. 存储所有名字的一半
根据你目前所知,你不能进行任何复杂的字母检测,以使混合结果看起来和听起来都恰到好处。相反,你可以做一些更简单但大多数时候都有效的方法。给定两个名字,取一个名字的前半部分和另一个名字的后半部分,并将它们合并,如图 12.3 所示。
图 12.3. 两种将水人和猫女混合的方法。你取每个名字的一半并合并它们。然后你取每个姓氏的一半并合并它们。

12.3.1. 找到名称的中点
您想要找到名称中字符串一半的索引。用户可以输入任何长度的名称,包括偶数或奇数个字符的名称。
偶数个字符的名称
对于字符数量为偶数的名称,将其一分为二很容易。您取单词的长度并除以 2,以得到字符串中半数索引点的整数值。在图 12.3 中,名称Aqua是这种情况的例子。
奇数个字符的名称
当名称字符数量为奇数时,您应该怎么做?在图 12.3 中,Cat 和 Woman 是例子。在 Python 中将奇数除以 2 会得到一个浮点数,例如 3.5 或 5.5。浮点数不能用作字符串的索引。
回想一下,您可以将浮点数转换为整数。例如,int(3.5)给出3。现在,具有奇数个字母的名称将具有向下取整的索引,图 12.3 上部的Man是一个例子。因此,具有奇数个字母的名称将使第二个半部分的名称提前一个索引:图 12.3 上部的Woman是一个例子。
将半部分保存到变量中的代码
列表 12.3 展示了如何存储每个名称的半部分。用户输入了两个完整的名称。您必须找到每个名称的半部分,但对于每个名称,重复相同的基本过程。
您首先找到名称的中点。您使用 Python 中的转换函数来处理具有奇数个字母的名称。如果一个名称有五个字母,第一个半部分将有两个字母,第二个半部分将有三个字母。转换为 int 不会影响具有偶数个字母的名称,因为例如int(3.0)是3。
列表 12.3. 存储每个名称的半部分
len_name1_first = len(name1_first) *1*
len_name2_first = len(name2_first) *1*
len_name1_last = len(name1_last) *1*
len_name2_last = len(name2_last) *1*
index_name1_first = int(len_name1_first/2) *2*
index_name2_first = int(len_name2_first/2) *2*
index_name1_last = int(len_name1_last/2) *2*
index_name2_last = int(len_name2_last/2) *2*
lefthalf_name1_first = name1_first[0:index_name1_first] *3*
righthalf_name1_first = name1_first[index_name1_first:len_name1_first] *4*
lefthalf_name2_first = name2_first[0:index_name2_first]
righthalf_name2_first = name2_first[index_name2_first:len_name2_first]
lefthalf_name1_last = name1_last[0:index_name1_last]
righthalf_name1_last = name1_last[index_name1_last:len_name1_last]
lefthalf_name2_last = name2_last[0:index_name2_last]
righthalf_name2_last = name2_last[index_name2_last:len_name2_last]
-
1 存储从输入中提取的姓名的第一和最后一个名称的长度
-
2 通过将中点转换为整数并向下取整以获得一个整数值来存储每个名称的中点索引
-
3 从开始到中间的名称
-
4 从中间到结尾的名称
现在您已经存储了名称的所有半部分。剩下要做的就是将它们组合起来。
12.4. 组合半部分
要组合半部分,您可以连接相关的变量。回想一下,连接使用两个字符串之间的+运算符。注意,这一步现在很简单,因为您已经计算并存储了所有必要的内容。
有关此操作的代码在列表 12.4 中。除了组合半部分外,您还应该确保将相关的半部分大写,使其看起来像是一个名称——例如,Blah而不是blah。您可以使用capitalize操作在第一个半部分上仅大写第一个字母。您使用lower操作在第二个半部分上使所有字母都变为小写。
关于列表 12.4 中的代码,有一点需要注意,那就是一些行上使用了反斜杠。反斜杠用于在代码中拆分跨越多行的语句。如果你在代码行中插入换行符,反斜杠会告诉 Python 继续读取下一行以找到语句的其余部分;如果没有反斜杠,当你运行程序时会出现错误。
列表 12.4. 合并名字
newname1_first = lefthalf_name1_first.capitalize() + \ *1*
righthalf_name2_first.lower() *2*
newname1_last = lefthalf_name1_last.capitalize() + \
righthalf_name2_last.lower()
newname2_first = lefthalf_name2_first.capitalize() + \
righthalf_name1_first.lower()
newname2_last = lefthalf_name2_last.capitalize() + \
righthalf_name1_last.lower()
print("All done! Here are two possibilities, pick the one you like best!")
print(newname1_first, newname1_last) *3*
print(newname2_first, newname2_last) *3*
-
1 将前半部分字符串大写化
-
2 确保后半部分字符串全部小写
-
3 向用户展示两个可能的名字
这段代码重复了同样的操作四次:获取两个新的名字组合,然后获取两个新的姓氏组合。首先,你从第一个用户的输入中取出名字的前半部分,并使用capitalize操作来确保第一个字母是大写的,其余都是小写。然后,你从第二个用户的输入中取出名字的后半部分,并确保所有字母都是小写。反斜杠告诉 Python 该语句跨越了两行。
在你合并了两个部分之后,最后一步是向用户展示结果。使用print操作并显示新的名字。试着在程序和不同的输入名字上玩玩!
摘要
在本课中,我的目标是让你编写一个程序,要求用户以特定格式输入两个名字。你操作了这些名字,创建了变量来保存每个名字的前半部分和后半部分。然后你合并了这些部分来混合输入的名字,并向用户展示了结果。以下是主要收获:
-
用户可以在你的程序中多次提供输入。
-
你可以使用
find操作来查找用户输入中的子字符串的位置。 -
你将操作过的字符串保存为变量,并使用
+操作符将字符串连接起来。 -
你使用了
print操作来向用户展示输出。
单元 3. 在程序中做出决策
在上一个单元中,你学习了字符串作为字符序列以及元组作为可以包含其他对象的实体。你还看到了如何通过提示用户输入、获取他们的输入、操作他们的输入以及在控制台中显示输出与用户交互。
在本单元中,你将编写做出决策的代码。这是编写一个酷炫的人工智能生物的第一步。你将插入分支,根据用户输入或某些变量的值,在代码中执行不同的语句。
在本单元的终期项目中,你将编写自己的“选择你的冒险”游戏。你将把用户困在一个荒岛上,向他们展示一组允许选择的单词,并观察他们是否能生存下来。
第 13 课. 程序中的决策介绍
在阅读第 13 课之后,你将能够
-
理解 Python 解释器如何做出决策
-
理解在做出决策时哪些代码行会被执行
-
编写代码,根据用户输入自动决定执行哪些代码行
当你编写程序时,你编写的是代码行。每一行代码被称为语句。你一直编写的是线性代码,这意味着当你运行程序时,每一行代码都会按照你编写的顺序执行;没有一行代码被执行多次,也没有一行代码被跳过。这相当于在没有做出任何决策的情况下度过一生;这将是一种限制性的体验世界的方式。你通过反应世界中的不同刺激来做出决策,这会导致更加有趣的经验。
考虑这一点
周一早上。你的第一个会议在上午 8:30,通勤需要 45 分钟。你的闹钟准时在早上 7:30 叫醒你。使用以下决策者来确定你是否有时间吃早餐。

一个流程图,用于决定闹钟响起后是否有时间吃早餐,以及你那天早上有一个会议
答案:你有时间吃早餐!
13.1. 使用条件语句做出决策
你希望程序在接收到不同的刺激时表现出不同的行为。刺激以程序输入的形式出现。输入可以由与程序交互的用户提供,或者可能是内部计算的结果。无论如何,当程序变得更有趣、更具交互性和更有用时,它们就会变得更有趣。
13.1.1. 是或否问题和真或假陈述
在你的日常生活中,你经常面临做出决策的情况:穿什么鞋,午饭吃什么,休息时在手机上玩什么游戏,等等。计算机擅长执行它被告知的事情,你可以编程让它为你做出这些决策。
当你做出决策时,你会提出一个问题。像“今天天气晴朗吗?”这样的问题可以用是或否来回答。所有是或否的问题都可以转换为真或假的陈述。Python 解释器不理解是或否,但它理解真或假(布尔逻辑)。问题“今天天气晴朗吗?”可以转换为陈述“今天天气晴朗。”如果你对问题回答是,那么这个陈述是真的。如果你回答否,那么这个陈述是假的。所有决策都可以简化为一个(或多个)是或否问题,或者等价地,一系列真或假的陈述。
快速检查 13.1
回答以下问题:是或否:
1
你害怕黑暗吗?
2
你的手机能放进口袋吗?
3
你今晚会去看电影吗?
4
5 乘以 5 等于 10 吗?
5
单词 "nibble" 的长度是否比单词 "googol" 长?
回想一下,Python 中的每一行代码都是一个语句。也回想一下,表达式是特定类型的语句或语句的一部分;表达式可以简化为一个值。值是 Python 中的一个对象;例如,整数、浮点数或布尔值。就像你在日常生活中做决定一样,你可以编写程序让计算机做出决定。真/假决策是一个评估为布尔值的 Python 表达式,称为布尔表达式。包含布尔表达式的语句称为条件语句。
快速检查 13.2
如果可能,将以下问题转换为布尔表达式。有没有不能转换的?
1
你住在树屋吗?
2
你晚饭吃什么?
3
你的车是什么颜色?
4
单词 "youniverse" 在字典中吗?
5
数字 7 是偶数吗?
6
变量
a和b是否相等?
| |
像程序员一样思考
计算机以真和假为工作方式,而不是以是和否为工作方式。你应该开始思考包含决策的表达式,将它们视为布尔表达式,这些表达式评估为真或假。
13.1.2. 在语句中添加条件
同样的思考过程可以应用于你编写的代码。你可以有特殊的语句,这些语句包含一个值要么为真要么为假的表达式。我们说这些语句是条件语句,并且它们包含一个评估为 Python 值 True 或 False 的表达式。评估为 True 或 False 的语句部分是条件布尔表达式。这部分驱动程序做出决定。
13.2. 编写决策代码
Python 有一组保留关键字。它们具有特殊含义,因此不能用作变量名。一个单词 if 被保留,因为它用于编写最简单的所有条件语句,即 if 条件语句。
13.2.1. 编写决策代码——示例
列表 13.1 展示了代码中的简单条件语句。你从用户那里获取一个输入数字。然后检查用户输入是否大于 0。如果是真的,你将打印一条额外的消息。最后,你打印一条不依赖于条件检查结果的消息给用户。
列表 13.1. 简单条件语句示例
num = int(input("Enter a number: ")) *1*
if num > 0: *2*
print("num is positive") *3*
print("finished comparing num to 0") *4*
-
1 等待用户输入并将用户输入分配给变量 num
-
2 检查存储在 num 中的值是否大于 0
-
3 如果 num 大于 0,则进入此块并执行此块中的所有行。
-
4 如果 num 不大于 0,则不要进入缩进的代码块,直接执行此行。
这是一种编写 if 语句的简单方法。代码执行会在遇到条件语句时停止,并执行所需的布尔检查。根据检查的结果,它将执行条件代码块内的语句或不会执行。 列表 13.1 可以重写为流程图,如图 13.1 所示。
图 13.1. 列表 13.1 中的代码流程。询问菱形的结果将决定你是否执行另一条代码语句。

图 13.1 是 列表 13.1 的视觉表示。你可以将条件语句视为一个你提出的问题,以决定是否跳过执行其代码块内的语句。在视觉表示中,如果条件检查的结果为假,你将选择“假”路径并跳过条件的代码块。如果条件检查的结果为真,你必须进入代码块,在视觉上表示为稍微绕道执行条件代码块内的语句。
快速检查 13.3
看一下这段代码片段:
if num < 10:
print("num is less than 10")
print("Finished")
1
如果
num的值为5,用户会在屏幕上看到什么?2
如果
num的值为10,用户会在屏幕上看到什么?3
如果
num的值为100,用户会在屏幕上看到什么?
13.2.2. 编写决策——通用方法
条件语句有一定的外观,你必须以这种方式精确地编写它们,这样 Python 才知道你想要做什么(参见以下列表)。这是 Python 语言语法的一部分。
列表 13.2. 编写简单 if 条件的通用方法
<some code before> *1*
if <conditional>: *2*
<do something> *3*
<some code after> *1*
-
1
在检查条件之前执行, 在条件之后执行。 -
2 关键字“if”开始条件行,后面跟着条件语句,然后是条件表达式。
-
3 缩进表示仅在条件为 True 时执行的代码
在 列表 13.2 中,你可以看到你编写的程序的结构开始发生变化。一些代码行缩进了四个空格。
条件语句会打断程序的流程。之前,你是在执行每一行代码。现在,你是在根据某个条件是否满足来选择是否执行一行代码。
快速检查 13.4
编写简单的条件语句来完成这些任务:
1
询问用户一个单词。打印用户给出的单词。如果用户给出的输入包含空格,也打印用户没有遵循指示。
2
请求用户输入两个数字。打印它们的和。如果和小于零,也打印“哇,负数和!”
13.3. 结构化你的程序
到目前为止,你可以看到,随着你设计程序以做出决策,你编写的程序的结构开始发生变化:
-
条件语句打断了程序的流程,这使得你的程序能够做出决定。
-
一些代码行是缩进的,这告诉 Python 它们与上方和下方的语句之间的关系。
-
在之前,你执行了每一行代码。现在,你根据是否满足某个条件来选择是否执行一行代码。
13.3.1. 做出多个决策
你可以通过将条件语句一个接一个地组合来有一系列if语句。每次遇到if语句时,你都会决定是否执行该if语句代码块内的代码。在下面的列表中,你可以看到一系列的三个条件语句。每个语句检查不同的条件:一个数字大于 0,一个数字小于 0,以及一个数字等于 0。
列表 13.3. 具有许多条件语句的代码
num_a = int(input("Pick a number: ")) *1*
if num_a > 0: *2*
print("Your number is positive") *3*
if num_a < 0: *4*
print("Your number is negative ") *5*
if num_a == 0: *6*
print("Your number is zero") *7*
print("Finished!") *8*
-
1 用户输入
-
2 检查数字是否大于 0。
-
3 如果前面的条件为真则执行
-
4 检查数字是否小于 0。
-
5 如果前面的条件为真则执行
-
6 检查数字是否等于 0。
-
7 如果前面的条件为真则执行
-
8 不论什么情况下都执行
注意,你使用双等号来检查相等性。这区分了相等(==)和变量赋值(=)。另外,注意条件print语句都是相同数量的缩进。
快速检查 13.5
Q1:
为列表 13.3 中的代码绘制流程图,以确保你理解决策是按顺序进行的。流程图是按视觉方式组织代码中所有可能路径的绝佳方式。这就像找出执行食谱的所有可能方式。
13.3.2. 基于另一个决策的结果做出决策
有时候,你想要根据先前决策的结果考虑第二个决策。例如,你只有在确定没有更多的谷物后才会决定买哪种谷物。
在 Python 程序中实现这一点的其中一种方法是使用嵌套条件:第二个条件只有在第一个条件的结果为True时才执行。条件代码块内的所有内容都是该代码块的一部分——甚至是一个嵌套条件。此外,嵌套条件将有自己的代码块。
列表 13.4 比较了两段代码;一个代码示例将一个条件嵌套在另一个条件中,另一个代码示例将条件串联起来。在嵌套代码中,嵌套条件语句(if num_b < 0)仅在外部条件(if num_a < 0)为True时执行。此外,嵌套条件内的代码块(print("num_b is negative"))仅在两个条件都为True时执行。在非嵌套代码中,嵌套条件语句(if num_b < 0)在程序每次运行时都会执行。嵌套条件内的代码块(print("num_b is negative"))仅在num_b小于 0 时执行。
列表 13.4. 通过嵌套或串联条件组合
Nested code Unnested code
num_a = int(input("Number? ")) num_a = int(input("Number? "))
num_b = int(input("Number? ")) num_b = int(input("Number? "))
if num_a < 0: *1* if num_a < 0: *1*
print("num_a: is negative") print("num_a is negative")
if num_b < 0: *2* if num_b < 0: *2*
print("num_b is negative") print("num_b is negative")
print("Finished") *3* print("Finished") *3*
-
1 第一个条件
-
2 第二个条件
-
3 要执行的语句
快速检查 13.6
Q1:
如果你为
num_a和num_b输入这些值,嵌套和非嵌套代码在列表 13.4 中会产生什么结果?自己编写代码检查一下!num_a num_b Nested Unnested ------------------------------------------- -9 5 ------------------------------------------- 9 5 ------------------------------------------- -9 -5 ------------------------------------------- 9 -5如果你不确定代码会发生什么或者为什么得到某个结果,尝试使用以下流程图中的相同值进行跟踪。
嵌套条件和非串联条件的区别。对于嵌套情况,只有当
num_a < 0决策的结果为True时,才会做出num_b < 0的决策。对于非嵌套情况,做出num_b < 0的决策时,不考虑num_a < 0决策的结果。
13.3.3. 嵌套条件的一个更复杂示例
作为最后一个练习,看看这个更复杂的任务。你将去杂货店为一周购买杂货。当你进入商店时,你会注意到巧克力棒。程序将帮助你决定购买多少巧克力棒。首先查看图 13.2 中的流程图,以帮助你在这些步骤中做出决定:
-
它会询问你是否饿了。
-
它会询问巧克力棒的价格是多少。
-
如果你饿了,且巧克力棒的价格低于一美元,就买下所有的。
-
如果你饿了,且巧克力棒的价格在 1 到 5 美元之间,就买 10 个。
-
如果你饿了,且巧克力棒的价格高于五美元,只买一个。
-
如果你不想吃,就别买。
-
然后,根据你购买的数量,收银员会做出评论。
图 13.2. 虚线框表示当hungry == "yes"条件为真时的代码块。虚线内是另一个条件,用于确定根据价格购买多少巧克力棒。

你可以看到程序的主要流程从上到下遵循垂直路径。每个决策都提供了偏离主路径的可能性。程序包含三个条件:一个用于确定你是否饿了,一个用于确定购买巧克力棒的数量,一个用于确定收银员的回答。
一个条件代码块可以包含其他条件。确定购买巧克力棒数量的条件嵌套在确定你是否饿了的条件中。
快速检查 13.7
Q1:
使用图 13.2 中的流程图作为指南,尝试编写一个执行此更复杂任务的 Python 程序。从思考-编写-测试-调试-重复的编程周期中,你已经得到了配方,所以你需要专注于编写-测试-调试部分。具体来说,测试步骤告诉你的程序如何表现。你的程序应该对相同的输入产生相同的输出。当你完成时,将你的代码与列表 13.5 进行比较,并记住以下要点:
- 变量名可以不同。
- 应该使用注释来帮助你理解哪些部分在哪里。
- 你可以重新排列一些条件以获得相同的行为;相同的输入应该产生相同的输出。
- 最重要的是,总有一种以上的正确实现方式。
列表 13.5. 决定购买多少巧克力的条件
price = float(input("How much does a chocolate bar cost? ")) *1*
hungry = input("Are you hungry (yes or no)? ") *1*
bars = 0
if hungry == "yes": *2*
if price < 1: *3*
print("Buy every chocolate bar they have.") *4*
bars = 100 *4*
if 1 <= price <= 5: *5*
print("Buy 10 chocolate bars.") *5*
bars = 10 *5*
if price > 5: *6*
print("Buy only one chocolate bar.") *6*
bars = 1 *6*
if hungry == "no": *7*
print("Stick to the shopping list.") *7*
if bars > 10: *8*
print("Cashier says: someone's hungry!") *8*
-
1 用户输入
-
2 条件检查以决定你是否饿了
-
3 条件检查以查看棒的价格是否低于 1 美元
-
4 当酒吧价格低于 1 美元时采取的行动
-
5 条件检查和当酒吧价格在 1 美元到 5 美元之间时采取的行动
-
6 当价格高于 5 美元时的条件检查和行动
-
7 当被询问是否饿了时说“不”时的条件检查和行动
-
8 只有当棒的数量大于 10 时才打印消息
摘要
在本课中,我的目标是教你如何使用if条件语句在代码中实现决策。条件语句为你的程序增加了一层复杂性。它们使程序能够偏离主程序流程并遵循代码其他部分的旁路。以下是一些主要收获:
-
if语句开始一个条件代码块。 -
一个程序可以有一个以上的条件,可以是串联或嵌套的。
-
嵌套条件是另一个条件代码块内的条件。
-
你可以通过使用流程图来可视化包含条件语句的程序。
当你开始编写涉及几个概念的程序时,积极参与解决问题很重要。拿出笔和纸,画出你的解决方案或写下你的思考过程。然后打开 Spyder,编写你的代码,并运行、测试和调试你的程序。别忘了注释你的代码。
让我们看看你是否掌握了这个...
Q13.1
你被给出了以下两个陈述:“x 是一个奇数”和“x + 1 是一个偶数”。使用这两个陈述编写一个条件语句和结果,形式为:
if <condition> then <outcome>。Q13.2
编写一个程序,创建一个变量,该变量可以是整数或字符串。如果变量是整数,打印
我是一个数字人。如果变量是字符串,打印我是一个文字人。Q13.3
编写一个程序,从用户那里读取一个字符串。如果字符串中至少包含一个空格,打印
这个字符串包含空格。Q13.4
编写一个程序,打印
猜猜我的数字!并将一个秘密数字赋值给一个变量。从用户那里读取一个整数。如果用户的猜测低于秘密数字,打印太低了。如果用户的猜测高于秘密数字,打印太高了。最后,如果用户的猜测与秘密数字相同,打印你猜对了!Q13.5
编写一个程序,从用户那里读取一个整数并打印该数字的绝对值。
第 14 课. 做更复杂的决策
在阅读完第 14 课后,你将能够
-
在一个条件语句中组合许多决定
-
在面对各种选项时做出选择
-
编写代码让计算机在几个选择之间做出决定
如果你每次只问一个问题来做决定,这会限制你的选择并耗费时间。比如说,你想买一部新手机。你只考虑了三部手机,但你不确定银行账户里有多少钱。另外,还有一个条件是手机必须是绿色的。使用是或否的问题,你可以问以下问题:
-
我的银行账户里是否有 400 到 600 美元?
-
我的银行账户里是否有 200 到 400 美元?
-
我的银行账户里是否有 0 到 100 美元?
-
手机 1 有绿色款吗?
-
手机 2 有绿色款吗?
-
手机 3 有绿色款吗?
因为你需要检查多个条件,你可以将两个(或更多)条件组合在一起。例如,你可以问,“我的银行账户里是否有 400 到 600 美元,并且手机 1 有绿色款吗?”
考虑这一点
你 7 岁,正在尝试根据你们共同参加的体育项目来选择你的最好朋友。体育项目的优先顺序是足球、篮球和棒球。你希望尽可能多地拥有共同的体育项目。如果不可能,你希望你的朋友尽可能多地从你喜欢的顺序中玩体育项目。按顺序列出所有可能的体育项目组合。汤米踢足球和打棒球。他在列表中的哪个位置有选择?
答案:
| 足球和篮球和棒球 |
|---|
| 足球和篮球 |
| 足球和棒球 |
| 篮球和棒球 |
| 足球 |
| 篮球 |
| 棒球 |
14.1. 组合多个条件
你知道如何编写依赖于一个条件是否为真的代码。这意味着决定“这个或不是这个”。有时,你想要做的决定可能是“这个或那个或那个或其它什么”。
例如,如果“正在下雨”是真的,而“我饿了”是假的,那么“正在下雨并且我饿了”是假的。表 14.1 显示了由两个陈述组成的语句的真值。
表 14.1. “和”和“或”组合的两个陈述的真值
| 陈述 1(例如:“正在下雨”) | 结合语句的词(<和>,<或>,<非>) | 陈述 2(例如:“我饿了”) | 结果(例如:“正在下雨 <_> 我饿了”) |
|---|---|---|---|
| 真 | <和> | 真 | 真 |
| 真 | <和> | 假 | 假 |
| 假 | <和> | 真 | 假 |
| 假 | <和> | 假 | 假 |
| 真 | <或> | 真 | 真 |
| 真 | <或> | 假 | 真 |
| 假 | <或> | 真 | 真 |
| 假 | <或> | 假 | 假 |
| N/A | <非> | 真 | 假 |
| N/A | <非> | 假 | 真 |
假设你正在准备一顿简单的意大利面晚餐。你是怎么考虑做的?你问问自己是否有意大利面和意大利面酱。如果你两者都有,你就可以做你的意大利面晚餐。注意,从这个简单的问题中产生了几个想法。
一个想法是你将两个问题合并为一个:你有意大利面和意大利面酱吗? 这些问题可以用不同的方式提出,以嵌套的方式,最终会得到相同的最终答案:你有意大利面吗?如果是,你有意大利面酱吗? 但是将两个问题合并在一起更容易理解。
另一个想法是你使用了一个重要的词,and,来连接两个有是/否答案的问题。词 and 和词 or 都是布尔运算符,它们用于连接有是/否答案的问题。
快速检查 14.1
使用布尔运算符 and/or 结合以下问题:
1
你需要牛奶吗?如果是,你有车吗?如果是,开车去商店买牛奶。
2
变量
a是零吗?如果是,变量b是零吗?如果是,变量c是零吗?如果是,那么所有变量都是零。3
你有夹克吗?你有毛衣吗?拿一件;外面很冷。
到目前为止的代码示例中,在条件语句内只有一个表达式会评估为 true 或 false。实际上,你可以根据多个条件做出决定。在编程中,你可以在一个 if 语句中组合多个条件表达式。这样,你不必为每个单独的条件编写单独的 if 语句。这导致代码更干净,更容易阅读和理解。
14.1.1. 条件由真/假表达式组成
你已经看到了只有单个表达式评估为 true/false 的条件语句;例如,num_a < 0。一个 if 语句可以检查多个条件,并根据整个表达式(由多个条件组成)是 true/false 而相应地行动。这就是你之前在 表 14.1 中看到的真值表有用的地方。你使用它通过布尔运算符 and 和 or 来组合多个表达式。在 Python 中,and 和 or 是关键字。
你可以有一个由多个表达式组成的 if 语句,如下所示。
列表 14.1. 一个 if 语句中的多个条件表达式
if num_a < 0 and num_b < 0:
print("both negative")
在进入 if 语句的代码块之前,这里必须做出两个决定:一个决定是 num_a < 0,另一个决定是 num_b < 0。
14.1.2. 运算符优先级规则
回想一下,表达式会被评估为一个值,这个值是 Python 对象——例如,一个整数值。在你开始组合多个表达式之后,你需要注意表达式及其各部分的评估顺序。
在数学中,你学习了加法、减法、乘法和除法的运算符优先级。在编程中,与数学中的优先级相同,但必须考虑额外的操作——比如比较运算符和逻辑运算符来组合布尔表达式。
表 14.2 显示了完整的运算符优先级规则集,它告诉你 Python 中哪些操作先于其他操作执行。这些优先级规则用于评估由较小的条件表达式组成的大条件的结果。
表 14.2。运算顺序,其中顶部的是首先执行的。同一单元格内具有相同优先级的操作是左结合的;它们按照在表达式中遇到的顺序从左到右执行。
| 运算符 | 含义 |
|---|---|
| () | 括号 |
| ** | 指数 |
| * / // % | 乘法 除法 整除 模数 |
| + - | 加法 减法 |
| == != > >= < <= is is not in not in | 等于 不等于 大于 大于等于 小于 小于等于 身份(对象是另一个对象) 身份(对象不是另一个对象) 成员(对象在另一个对象中) 成员(对象不在另一个对象中) |
| not | 逻辑非 |
| and | 逻辑与 |
| or | 逻辑或 |
快速检查 14.2
使用表 14.2 中的运算符优先级评估以下表达式:
1
3 < 2 ** 3 and 3 == 32
0 != 4 or (3/3 == 1 and (5 + 1) / 3 == 2)3
"a" in "code" or "b" in "Python" and len("program") == 7
看看下面的(错误)代码。它与 14.1 中的代码类似,除了将num_a < 0 and num_b < 0行写成num_a and num_b < 0。
列表 14.2。不按你想象的方式工作的代码
if num_a and num_b < 0:
print("both negative")
如果你用以下不同的num_a和num_b值运行代码,你将在表 14.3 中看到输出。空条目表示没有输出。注意,有一对值给出了误导性的打印输出。
表 14.3。在列表 14.2 中用不同的num_a和num_b值运行代码后的控制台输出结果
| num_a | num_b | 控制台输出 |
|---|---|---|
| -1 | -1 | 两个都是负数 |
| -1 | 1 | |
| 0 | -1 | |
| 0 | 1 | |
| 1 | -1 | 两个都是负数 |
| 1 | 1 |
当num_a = 1和num_b = -1时,打印到控制台的是两个都是负数,这是不正确的。使用优先级规则来查看发生了什么。在列表 14.2 中添加括号来表示首先评估的表达式。
根据表 14.2 中的优先级规则,逻辑运算符and的优先级低于比较运算符“小于”。表达式num_a and num_b < 0可以重写为(num_a and (num_b < 0))。
在 Python 中,除了 0 以外的所有整数值都被认为是True,整数值 0 被认为是False。if -1评估为if True,而if 0评估为if False。由于优先级规则,当num_a不是 0 时,表达式评估为True。当num_a = 1且num_b = -1时,代码错误地打印both negative,因为(num_a and (num_b < 0))评估为(1 and (-1 < 0)),这评估为(True and True),即True。
快速检查 14.3
Q1:
回到列表 14.1 中的代码。那里的条件可以使用优先级规则和括号重写为
((num_a < 0) and (num_b < 0))。为num_a和num_b的几个组合绘制一个表格,以说服自己所有可能的值对都会给出预期的打印输出。
14.2. 选择要执行的行
现在你已经理解了条件语句的用途以及如何在 Python 中编写一个条件语句。条件语句不一定要作为代码中的唯一“分支”使用。它们也可以用来决定要执行哪些代码块。
14.2.1. 做这个或那个
有时候你想执行一个任务而不是另一个。例如,你可能会说:“如果天气晴朗,我就步行去上班;否则,如果天气多云,我就拿伞步行去上班;否则,我就开车去。”为此,将使用elif和else关键字与if语句结合使用。
列表 14.3 展示了代码中的简单if-elif-else条件语句。你从用户那里获取一个输入数字。如果数字大于 0,你打印positive。否则,如果数字小于零,你打印negative。否则,你打印该数字为零。只有一条消息会被打印。
列表 14.3. 简单的if-elif-else条件语句示例
num = int(input("Enter a number: ")) *1*
if num > 0: *2*
print("num is positive") *3*
elif num < 0: *4*
print("num is negative") *5*
else: *6*
print("num is zero") *7*
-
1 用户输入
-
2 检查数字是否大于 0
-
3 打印一条消息
-
4 当 if num > 0 为 False 时,执行此条件以检查数字是否小于 0。
-
5 打印一条消息
-
6 当 elif num < 0 为 False 时,else 是一个通配符。
-
7 打印一条消息
在这里,你使用if语句开始一个条件。任何随后的elif或else语句都与该if语句相关联。这种结构意味着你将执行属于第一个为真的决策的代码块。
快速检查 14.4
Q1:
当你使用以下
num值运行列表 14.3 时,会打印什么?-3, 0, 2, 1?
图 14.1 展示了如何可视化多个决策。每个决策都是一个条件语句;一组决策是 if-elif-else 代码块的一部分。通过追踪 图 14.1 中由箭头表示的路径,遵循任何路径。主程序决策由菱形表示。第一个决策从 if 语句开始,表示决策块的开始。如果你从 if 语句追踪到标有 <rest of program> 的框,你会注意到你最多只能偏离程序的主路径一次。你将偏离的路径是第一个条件评估为 True 的路径。
图 14.1. 可视化一个通用的 if-elif-else 代码块。你最多只能通过执行 <do something> 一次来偏离主程序流程。你可以有零个或多个 elif 块,而 else 块是可选的。

图 14.1 中的 if-elif-else 代码块是一个通用块。你可以有以下变体:
-
只有一个
if语句(你已经在上一课中看到了) -
一个
if语句和一个elif语句 -
一个
if语句和多个elif语句 -
一个
if语句和一个else语句 -
一个
if语句,一个或多个elif语句,以及一个else语句
对于所有这些变体,执行的是第一个条件评估为 True 的绕行。如果没有评估为 True,则执行 else 绕行。如果前面的变体不包括 else 语句,则可能没有执行到 <do something> 的任何绕行。
快速检查 14.5
Q1:
为 列表 14.3 绘制流程图。
下面的列表显示了根据某些条件是否成立,执行一个动作或另一个动作的代码的通用编写方式,如图 14.1 所示。
列表 14.4. 简单 if-elif-else 条件的通用编写方式
if <conditional>: *1*
<do something>
elif <conditional>: *2*
<do something>
else: *3*
<do something>
-
1 关键词“if”开启条件块。
-
2 关键词“elif”开启“else if”条件块。
-
3 关键词“else”开启所有其他条件情况的捕获。
关键字 if 开启条件块,就像之前一样,后面跟着一个条件表达式,然后是冒号字符。当 if 语句的条件为 True 时,执行该 if 语句的代码块,然后跳过 if-elif-else 组中所有剩余的代码块。当 if 语句的条件为 False 时,你将检查 elif 语句中的条件。
如果 elif 语句中的条件为 True,则执行该 elif 语句的代码块,并跳过 if-elif-else 组中所有剩余的代码块。你可以有任意多的 elif 语句(零个或更多)。Python 会逐个检查条件,并执行第一个评估为 True 的代码块。
当 if 或任何 elif 语句中的条件都不为 True 时,else 语句内的代码块将被执行。您可以将 else 视为一个用于当其他条件都不为 True 时的通用条件。
当没有 else 语句,并且没有任何条件评估为 True 时,条件块不会执行任何操作。
快速检查 14.6
Q1:
查看以下代码片段:
With if-elif-else statements With if statements if num < 6: if num < 6: print("num is less than 6") print("num is less than 6") elif num < 10: if num < 10: print("num is less than 10") print("num is less than 10") elif num > 3: if num > 3: print("num is greater than 3") print("num is greater than 3") else: print("Finished.") print("No relation found.") print("Finished.")如果
num有以下值,用户会在屏幕上看到什么?num With if-elif-else With if -------------------------------------------------------- 20 -------------------------------------------------------- 9 -------------------------------------------------------- 5 -------------------------------------------------------- 0
14.2.2. 整合所有内容
到目前为止,您可以看到程序的结构再次发生了变化:
-
您可以通过检查不同的条件来决定做许多事情之一。
-
if-elif结构用于进入第一个为True的代码块。 -
else用于在没有任何其他条件为True时执行某些操作。
列表 14.5 展示了一个简单的程序,该程序检查用户输入。当用户为任一输入输入非整数值时,程序会向用户打印一条消息,然后继续执行下一个组,即同一缩进级别的 if-elif-else 语句。它不会进入与第一个 if 语句关联的 else 代码块,因为它已经执行了 if 语句内的代码块。
当用户输入两个有效的整数时,您将进入 else 代码块,并根据输入数字的符号打印一条消息。只有当嵌套的 if-elif-else 语句中第一次条件评估为 True 时关联的消息会被打印。在该代码块执行完毕后,您将进入下一个 if-elif-else 组,检查用户是否猜对了幸运数字。
列表 14.5. 使用 if-elif-else 语句的示例
num_a = 5
num_b = 7
lucky_num = 7
if type(num_a) != int or type(num_b) != int: *1*
print("You did not enter integers") *1*
else: *1*
if num_a > 0 and num_b > 0: *1* *2*
print("both numbers are positive") *1* *2*
elif num_a < 0 and num_b < 0: *1* *2*
print("both numbers are negative") *1* *2*
else: *1* *2*
print("numbers have opposite sign") *1* *2*
if num_a == lucky_num or num_b == lucky_num: *3*
print("you also guessed my lucky number!") *3*
else: *3*
print("I have a secret number in mind...") *3*
-
1 嵌套的 if-else 组
-
2 嵌套的 if-elif-else 组
-
3 另一个 if-else 组
像程序员一样思考
程序员编写的代码易于阅读,既是为了让其他人能够阅读,也是为了自己以后回顾。创建变量来存储复杂的计算并给它们起有描述性的名字是一个好主意,而不是直接将它们包含在条件语句中。例如,不要这样做 if (x ** 2 - x + 1 == 0) or (x + y ** 3 + x ** 2 == 0)。相反,创建变量 x_eq = x ** 2 - x + 1 和 xy_eq = x + y ** 3 + x ** 2,然后检查 if x_eq == 0 or xy_eq == 0。
Python 通过缩进代码块使得可视化哪些行应该被执行变得容易。您可以查看列表 14.5 并从块的角度来可视化代码。在图 14.2 中,您可以看到条件语句呈现出级联的外观。
图 14.2. 列表 14.5 的可视化,显示条件代码块

在条件组内,你将只执行评估结果为True的第一个分支。每当你在同一级别有另一个if语句时,你就是在开始另一个条件组。
你可以在图 14.2 中看到,两个主要的条件块位于主级别:一个用于检查用户输入,另一个用于检查幸运数字。使用这种可视化,你甚至可以提出对列表 14.5 中代码的重新编写,以消除第一个检查用户输入的代码块的else语句,并将其转换为elif语句。代码重写将在下一个列表中展示。
列表 14.6. 将列表 14.5 中的 else 转换为一系列 elif 的重写
num_a = 5
num_b = 7
lucky_num = 7
if type(num_a) != int or type(num_b) != int:
print("You did not enter integers")
elif num_a > 0 and num_b > 0: *1*
print("both numbers are positive")
elif num_a < 0 and num_b < 0: *1*
print("both numbers are negative")
else: *1*
print("numbers have opposite sign")
if num_a == lucky_num or num_b == lucky_num:
print("you also guessed my lucky number!")
else:
print("I have a secret number in mind...")
- 1 将列表 14.5 中的 else 块转换为一系列 elif 块
作为练习,你可以检查所有输入组合,并将列表 14.5 和列表 14.6 中代码的输出进行比较,以确保它们相同。
14.2.3. 从代码块的角度思考
重要的是要意识到,当你决定执行哪个分支时,你只查看特定的if-elif-else条件组,如列表 14.7 所示。if语句有一个检查,查看用户输入是否是元组greet_en或greet_sp中的字符串之一。其他两个elif每个都有一个嵌套的if-elif代码块。
列表 14.7. 多个if-elif-else代码块的示例
greeting = input("Say hi in English or Spanish! ")
greet_en = ("hi", "Hi", "hello", "Hello")
greet_sp = ("hola", "Hola")
if greeting not in greet_en and greeting not in greet_sp: *1*
print("I don't understand your greeting.")
elif greeting in greet_en: *1*
num = int(input("Enter 1 or 2: "))
print("You speak English!")
if num == 1: *2*
print("one")
elif num == 2: *2*
print("two")
elif greeting in greet_sp: *1*
num = int(input("Enter 1 or 2: "))
print("You speak Spanish!")
if num == 1: *3*
print("uno")
elif num == 2: *3*
print("dos")
-
1 由 if-elif 语句组成的代码块
-
2 包含 if-elif 块的嵌套块
-
3 包含另一个 if-elif 块的嵌套块
程序将通过if-elif-elif进入以下路径之一:
-
当用户输入的问候语不在
greet_en和greet_sp中时 -
通过
elif在greeting in greet_en时 -
通过
elif在greeting in greet_sp时
概述
在本课中,我的目标是教你如何使用if-elif-else条件语句来做出决策,并教你如何各种组合的各部分影响程序流程。你现在可以做出的决策现在更加复杂,因为你可以选择执行哪个代码。以下是主要收获:
-
在一个条件中评估多个表达式时,运算符优先级很重要。
-
if语句表示是否要走一条捷径。if-elif-else语句表示要走哪条捷径。 -
通过使用流程图来可视化更复杂的程序,这些程序包括条件语句。
当你开始编写涉及几个概念的程序时,积极参与解决问题很重要。拿出笔和纸,画出你的解决方案或写下你的思考过程。然后打开你喜欢的 IDE,输入你的代码,然后运行、测试和调试你的程序。别忘了给你的代码添加注释。
让我们看看你是否明白了...
Q14.1
编写一个程序,从用户那里读取两个数字。程序应该打印出这两个数字之间的关系,这将是以下之一:
数字相等,第一个数字小于第二个数字,第一个数字大于第二个数字。Q14.2
编写一个程序,从用户那里读取一个字符串。如果该字符串至少包含每个元音字母(a, e, i, o, u)中的一个,打印
您已经拥有了所有元音字母!另外,如果字符串以字母a开头并以字母z结尾,打印而且它有点字母顺序!
第 15 课. 期末项目:选择你的冒险
在阅读第 15 课之后,您将能够
-
编写一个选择你自己的冒险程序的代码
-
使用分支设置程序中的路径
这个期末项目是有些开放式的。
问题
您将使用条件语句和分支来创建一个故事。在每一个场景中,用户将输入一个单词。这个单词将告诉程序继续跟随哪个路径。您的程序应该处理用户可能选择的所有可能路径,但不需要处理任何意外的用户输入。
您将看到的演练是许多可能之一;在您的剧情中尽可能有创意!
15.1. 概述游戏规则
在获取用户输入时,您应该意识到他们可能不会按照规则来玩。在您的程序中,指定您期望他们做什么,并警告他们任何其他行为可能会使程序结束。
一个简单的print语句就足够了,如下面的列表所示。
列表 15.1. 获取用户输入
print("You are on a deserted island in a 2D world.")
print("Try to survive until rescue arrives!")
print("Available commands are in CAPITAL letters.") *1*
print("Any other command exits the program") *2*
print("First LOOK around...")
-
1 如何玩
-
2 非预期行为关闭程序。
根据程序规则,您必须处理任何大写字母输入的分支。程序开始时只有一个选项,以帮助用户习惯这种输入格式。
15.2. 创建不同的路径
程序的一般流程如下:
-
告诉用户他们的选择。
-
获取用户输入。
-
如果用户输入了选择 1,打印一条消息。对于这条路径,如果用户现在有更多选择,指出选择,获取输入,等等。
-
否则,如果用户选择了选项 2,打印另一条消息。对于这条路径,如果用户现在有更多选择,指出选择,获取输入,等等。
-
等等,对于有多少选择就有多少。对于每条路径,如果用户现在有更多选择,指出选择,获取输入,等等。
您将使用嵌套条件在路径内创建子路径。一个简单的程序在列表 15.2 中展示。它只深入两个条件;一个嵌套条件在另一个内部。用户在运行程序一次时最多可以做出两个选择。
代码列表首先要求用户输入。然后它通过一个针对关键字LOOK的条件来确保用户理解游戏的规则。如果用户输入了其他任何内容,它会显示一条消息,指出允许的命令以及用户将看到的内容。第一个条件检查用户是否输入了LOOK。如果用户确实这样做了,代码会再次获取用户输入,并处理两种可能性:用户输入了LEFT或RIGHT。代码为这些选择打印不同的消息。
列表 15.2. 只有一个选择的冒险
do = input(":: ") *1*
if do == "LOOK": *2*
print("You are stuck in a sand ditch.")
print("Crawl out LEFT or RIGHT.")
do = input(":: ") *3*
if do == "LEFT": *4*
print("You make it out and see a ship!")
print("You survived!")
elif do == "RIGHT": *5*
print("No can do. That side is very slippery.")
print("You fall very far into some weird cavern.")
print("You do not survive :(")
else: *6*
print("You can only do actions shown in capital letters.")
print("Try again!")
-
1 用户输入
-
2 如果用户输入 LOOK 的条件
-
3 用户查看后的用户输入
-
4 检查用户是否输入 LEFT 的条件
-
5 检查用户是否输入 RIGHT 的条件
-
6 一个提醒用户只能输入特定命令的块
程序中的两个选择听起来并不有趣。你可以为不同的场景添加更多选择。
15.3. 更多的选择?当然,请!
一个“选择你的冒险”游戏应该有不止一个或两个选择。使用许多嵌套条件来创建许多子路径通过代码。你可以使冒险变得容易或困难,例如,在代码中的 20 条可能路径中,可能只有一条通向生存。
图 15.1 展示了一个可能的代码结构。决策由用户输入一个单词来标记。根据选择的单词,用户将看到该路径的新情况。用户将继续做出选择,直到达到最终结果。
图 15.1. 箱子代表用户的选项。箱子下面的文本代表情况。灰色箭头显示已做出选择的路径。到笑脸或哭脸的虚线黑色线条代表程序的结束,表示生存与否。在五种可能的结果中,只有一种通向生存。

以下列表提供了与 图 15.1 相关的代码。只有一条路径通向生存。用户必须输入 LEFT,然后 CRAB,然后 YES,然后 TREE,然后 NO。任何其他选择都会导致显示用户在岛上丧生的文本。
列表 15.3. 一个可能的“选择你的冒险”代码
print("You are stuck in a sand ditch.")
print("Crawl out LEFT or RIGHT.")
do = input(":: ")
if do == "LEFT": *1*
print("You see a STARFISH and a CRAB on the sand.")
print("And you're hungry! Which do you eat?")
do = input(":: ")
if do == "STARFISH": *2*
print("Oh no! You immediately don't feel well.")
print("You do not survive :(")
elif do == "CRAB": *3*
print("Raw crab should be fine, right? YES or NO.")
do = input(":: ")
if do == "YES": *4*
print("Ok, You eat it raw. Fingers crossed.")
print("Food in your belly helps you see a TREE.")
do = input(":: ")
if do == "TREE": *5*
print("It's a coconut tree! And you're thirsty!")
print("Do you drink the coconut water? YES OR NO.")
do = input(":: ")
if do == "YES": *6*
print("Oh boy. Coconut water and raw crab don't mix.")
print("You do not survive :(")
elif do == "NO": *6*
print("Good choice.")
print("Look! It's a rescue plane! You made it! \o/")
elif do == "NO": *7*
print("Well, there's nothing else left to eat.")
print("You do not survive :(")
elif do == "RIGHT": *8*
print("No can do. That side is very slippery.")
print("You fall very far into some weird cavern.")
print("You do not survive :(")
-
1 首选
-
2 为 1 的 if 分支的选择
-
3 选项为 1 的 elif 分支
-
4 嵌套选择,为 3 的 if 分支
-
5 没有选择,只有一种可能性
-
6 为 5 的选择
-
7 为 3 的 elif 分支的嵌套选择
-
8 首选
摘要
在本节课中,我的目标是教会你如何使用条件语句编写一个程序,用户可以通过选择来尝试在程序开始时概述的场景中生存下来。为了创建用户在已经做出选择后可以做出的不同选择的路径,你使用了嵌套条件。以下是主要收获:
-
条件语句为用户提供选择。
-
嵌套条件在做出一个选择后提供另一组选择是有用的。
单元 4:重复任务
在上一个单元中,你学习了如何编写根据用户输入或程序内部计算自动做出决定的代码。在本单元中,你将编写能够自动执行一个或多个语句的代码。
经常你会发现自己在代码中想要重复执行相同的任务。计算机不介意被告知该做什么,并且特别擅长快速执行相同的任务。你将看到如何利用这一点,编写代码让计算机帮助你重复任务。
在综合项目中,你将编写一个程序,该程序会告诉你根据一组字母可以组成的所有单词。当你玩拼字游戏时,你可以使用这个程序来帮助你用手中剩余的字母拼出最佳单词!
第 16 课:使用循环重复任务
在阅读第 16 课之后,你将能够
-
理解代码重复执行的含义
-
在程序中编写循环
-
重复执行特定次数的操作
你迄今为止看到的程序中都有只执行一次的语句。在前一个单元中,你学习了如何在程序中添加决策点,通过使程序对输入做出反应来打断流程。决策点由条件语句控制,如果满足某个条件,程序可能会绕道执行其他代码行。
这些类型的程序仍然具有某种线性;语句按顺序执行,一个语句可以执行零次或最多一次。因此,程序中可以执行的最大语句数是程序中行数的最大值。
考虑以下内容
新年伊始,你的决心是每天做 10 个俯卧撑和 20 个仰卧起坐。查看以下流程图,以确定你一年内将做多少次仰卧起坐和俯卧撑。

一个流程图,说明了你可以如何重复执行某些任务。仰卧起坐重复 10 次,俯卧撑重复 20 次。仰卧起坐和俯卧撑序列每天都在进行。
答案:3,650 次仰卧起坐和 7,300 次俯卧撑
16.1. 重复任务
计算机的强大之处在于它们能够快速进行计算。使用你迄今为止学到的知识,如果你想多次执行一个语句或语句的微小变化,你必须在程序中再次输入它,以便解释器将其视为一个单独的命令。这样做就违背了让计算机为你工作的目的。在本课中,你将构建循环,告诉解释器重复执行某个任务(由一组语句表示)多次。
16.1.1. 向程序添加非线性
在你的日常生活中,你经常在改变一小部分的同时重复执行某个任务。例如,当你到达工作或学校时,你可能会用“嗨,乔”和“嗨,贝丝”然后“嗨,爱丽丝”来问候人们。所有重复操作中都有一些共同之处(单词嗨),但每次重复都会改变一小部分(一个人的名字)。作为另一个例子,你的洗发水瓶上可能标有“起泡,冲洗,重复”。起泡和冲洗可以被认为是每次重复都要执行,且顺序相同的小子任务。
计算机的一个许多用途是它们能够在短时间内执行许多计算。执行重复性任务是计算机最擅长的事情,编写播放列表中每首歌曲的播放任务很容易。每种编程语言都有一种方法来告诉计算机如何重复执行一组特定的命令。
16.1.2. 无限重复
计算机只会做你告诉它们做的事情,所以你必须小心并明确你的指令。它们不能猜测你的意图。假设你编写了一个程序来实现“泡沫、冲洗、重复”的过程。假设你以前从未使用过洗发水,你现在正在按照指示操作,没有应用任何其他逻辑或推理。你注意到指令有什么问题吗?不清楚何时停止“泡沫、冲洗”步骤。你应该“泡沫和冲洗”多少次?如果没有设定的重复次数,何时停止?这些特定的指令如此含糊,如果你告诉计算机执行,它将无限期地执行“泡沫、冲洗”过程。更好的指令是“泡沫、冲洗、需要时重复”。图 16.1 中的流程图显示了当你告诉计算机“泡沫-冲洗-重复”以及添加“需要时”条款以停止其无限重复时会发生什么。
图 16.1. (A) 一种泡沫-冲洗-重复的指令,如果字面理解,永远不会终止,以及(B) 需要时泡沫-冲洗-重复的指令,每次都会询问是否重复

因为计算机只会做它们被告知的事情,所以它们不能自己决定是否重复一组命令。当告诉计算机重复命令时,你必须小心并具体说明:你希望命令重复多少次,或者是否存在一个条件来决定是否再次重复?在泡沫-冲洗的例子中,“需要时”是一个决定你是否要重复泡沫-冲洗的条件。或者,你可以说你想要泡沫-冲洗三次然后停止。
像程序员一样思考
人类可以在不同情况下填补知识空白并推断某些想法。计算机只会做它们被告知的事情。当编写代码时,计算机将根据编程语言的规则执行你写的每一行代码。错误不会在代码中自发出现。如果存在错误,那是因为你将其放入其中。第 36 课讨论了调试程序的形式化方法。
16.2. 循环特定次数
在编程中,通过构建循环来实现重复。停止程序无限重复一组指令的一种方法是指定重复指令的次数。这种循环的名称是for循环。
16.2.1. for 循环
在 Python 中,告诉你如何循环特定次数的关键字是for。为了开始,这里有一种使用关键字重复命令多次的方法:
| 不使用循环 | 使用循环 |
|---|
|
print("echo")
print("echo")
print("echo")
print("echo")
|
for i in range(4):
print("echo")
|
不使用循环,你必须重复执行相同的命令,次数取决于你需要多少次。在这种情况下,命令是打印单词 echo 四次。但使用循环,你可以将代码压缩成只有两行。第一行告诉 Python 解释器重复某个命令的次数。第二行告诉解释器要重复的命令。
快速检查 16.1
1
编写一段代码,将单词 crazy 在单独的行上打印八次。
2
编写一段代码,将单词 centipede 在单独的行上打印 100 次。
现在假设你正在玩一个棋盘游戏;在你的回合,你必须掷三次骰子。每次掷骰子后,你都要移动你的棋子这么多步。假设玩家掷出了 4,然后是 2,然后是 6。
图 16.2 展示了每个步骤的流程图。通过仅掷三次骰子,通过编写命令进行掷骰子和移动棋子,重复这两个动作三次,就可以轻松地模拟游戏。但如果允许玩家在他们的回合中进行 100 次掷骰子,这会很快变得混乱。相反,最好通过使用 for 循环来模拟玩家的回合。
图 16.2。在(A)和(B)中,你掷三次骰子得到值 4、2、6。 (A) 表示你可以通过逐个写出命令来显式地移动棋子。 (B) 显示了你可以用 for 循环来表示相同的事情,只是你正在使用一个遍历代表每次掷骰子值的循环,并且通过使用变量 n 来泛化这些值。

玩家掷三次骰子得到一个值序列。将骰子的数字表示为变量 n。循环遍历代表掷骰子的序列,从第一个数字开始——在这个例子中是 n = 4。使用这个变量作为移动步数的数量,你将棋子移动 n 步。然后你转到序列中的下一个数字,n = 2,并将棋子移动 2 步。最后,你转到序列中的最后一个数字,n = 6,并将棋子移动 6 步。因为序列中没有更多的数字,你可以停止移动你的棋子,你的回合结束。
列表 16.1 展示了 for 循环的一般结构。可以使用 图 16.3 中的流程图来可视化相同的结构。想法是,你给出一个值序列。循环体重复执行,直到序列中的值全部执行完毕。每次重复时,你都会改变循环变量,使其成为值序列中的一个项目。当序列中的所有值都已执行完毕时,循环停止重复。
列表 16.1。编写 for 循环的一般方法
for <loop_variable> in <values>: *1*
<do something> *2*
-
1 表示循环的开始。 <loop_variable> 系统性地取
中每个项目的值。 -
2 为
中的每个项目执行代码块
图 16.3. 编写for循环的一般方法。你从值序列的第一个项开始,然后使用该项执行循环体。你得到值序列的下一个项,并使用该项执行循环体。当你已经通过并使用序列中的每个项执行了循环体时,循环结束。

for循环由两部分组成:for循环行定义和执行一定次数的代码块。
关键字for告诉 Python 你正在引入一个将被重复一定次数的块。在关键字之后,你命名一个循环变量。这可以是任何你想要的合法变量名。这个循环变量会自动在每次重复时改变其值,后续值从关键字in之后确定的值中获取。
与条件语句一样,循环中的缩进很重要。缩进的代码块告诉 Python,该代码块中的所有内容都是循环的一部分。
16.3. 循环 N 次
在上一节中,你没有对值的序列施加任何约束。有时,有一个遵循模式的值的序列的循环是有用的。例如,一个常见且有用的模式是,序列中的项按顺序递增 1, 2, 3...直到某个值N。因为在计算机科学中计数从 0 开始,一个更常见的数字序列是 0, 1, 2...直到某个值N – 1,以在序列中给出N个项。
16.3.1. 对 0 到 N-1 的常见序列进行循环
如果你想循环N次,你将代码列表 16.1 中的<values>替换为表达式range(N),其中N是一个整数。range是 Python 中的一个特殊过程。表达式range(N)产生序列 0, 1, 2, 3, ... N – 1。
快速检查 16.2
以下表达式评估得到什么序列的值?
1
range(1)2
range(5)3
range(100)
代码列表 16.2 展示了一个简单的for循环,该循环重复打印循环变量v的值。在代码列表 16.2 中,循环变量是v,循环变量所取值的序列由range(3)给出,循环体是一个print语句。
当程序在代码列表 16.2 运行时,它首先遇到for循环和range(3),它首先将0赋值给循环变量v,然后执行print语句。然后它将1赋值给循环变量v并执行print语句。然后它将2赋值给循环变量v并执行print语句。在这个例子中,这个过程重复了三次,实际上将循环变量赋值为 0, 1, 2。
代码列表 16.2. 打印循环变量值的for循环
for v in range(3): *1*
print("var v is", v) *2*
-
1 v 是循环变量。
-
2 打印循环变量
你可以通过使用不同的变量,比如 n_times,而不是 3,来泛化 列表 16.2 中的行为,以给出由 range(n_times) 表示的数字序列。然后循环将重复 n_times 次。每次循环变量取不同的值时,都会执行代码块内的语句。
16.3.2. 展开循环
你也可以用不同的方式思考循环。 列表 16.3 展示了如何展开循环(写出重复的步骤)以查看 Python 如何执行 列表 16.2 中的代码。在 列表 16.3 中,你可以看到变量 v 被分配了不同的值。打印变量的那些行对于 v 的每一个不同值都是相同的。由于打印变量 v 值的行被重复,因此编写此代码既低效又无聊,且容易出错。使用循环而不是这段代码要高效得多,也更容易阅读。
列表 16.3. 从 列表 16.2 中的展开 for 循环
v = 0 *1*
print("var v is", v)
v = 1 *2*
print("var v is", v)
v = 2 *3*
print("var v is", v)
-
1 变量
v(此处分配为 0)是 列表 16.2 中的循环变量。 -
2 手动将
v的值更改为 1。 -
3 手动将
v的值更改为 2。
摘要
在本课中,我的目标是教你为什么循环有用。你看到了 for 循环的作用以及如何在代码中设置 for 循环。从高层次来看,for 循环重复其代码块中的语句一定次数。循环变量是一个变量,其值随着每次循环重复通过循环序列中的项目而改变。
序列可以是一系列整数。你看到了由表达式 range(N) 创建的特殊序列,其中 N 是一个整数。这个表达式创建了一个序列 0, 1, 2, ... N – 1。以下是主要收获:
-
循环对于编写简洁且易于阅读的代码非常有用。
-
for循环使用一个循环变量,该变量从一系列元素中取值;这些元素可以是整数。 -
当序列中的元素是整数时,你可以使用特殊的
range表达式来创建特殊的序列。
让我们看看你是否理解了...
Q16.1
编写一段代码,要求用户输入一个数字。然后编写一个循环,该循环迭代该次数,并且每次打印
Hello。是否可以不使用for循环来编写此代码?
第 17 课. 自定义循环
在阅读第 17 课后,你将能够
-
编写更复杂的
for循环,从自定义值开始和结束 -
编写遍历字符串的循环
你编写程序是为了以某种方式让用户的生活更轻松,但这并不意味着程序员编写程序的经验应该是乏味的。许多编程语言已经对某些语言结构进行了自定义,以便程序员可以利用它们并更有效地编写代码。
考虑这一点
你给你的配偶列出一个你希望在一年内观看的电影列表。每个奇数编号的电影是动作片,每个偶数编号的电影是喜剧:
-
你可以遵循什么模式来确保你能够可靠地观看列表中的每一部喜剧电影?
-
你可以遵循什么模式来确保你能够可靠地观看列表中的每一部动作电影?
答案:
-
逐个过电影列表,并观看列表中的每部电影,从列表中的第二部开始。
-
逐个过电影列表,并观看列表中的每部电影,从列表中的第一部开始。
17.1. 自定义循环
当使用range关键字时,你可以指定起始值、结束值和步长。range(start,end,step)至少需要一个数字,并且可以接受其括号内的最多三个数字。编号规则与字符串索引类似:
-
第一个数字表示开始时的索引值。
-
中间的数字表示停止时的索引值,但代码不会执行。
-
最后一个数字表示步长(每“跳过多少数字”)。
你可以记住以下经验法则:
-
当你只给出括号中的一个数字时,这对应于
range(start,end,step)中的end。默认情况下,start为0,step默认为1。 -
当你只给出括号中的两个数字时,这对应于
range(start,end,step)中的start和end(按此顺序)。默认情况下,step为1。 -
当你在括号中给出所有三个数字时,这对应于
range(start,end,step)中的start、end和step(按此顺序)。
以下是一些使用range及其对应值序列的示例:
-
range(5)等价于range(0, 5)和range(0,5,1)—0, 1, 2, 3, 4 -
range(2,6)—2, 3, 4, 5 -
range(6,2)—没有值 -
range(2,8,2)—2, 4, 6 -
range(2,9,2)—2, 4, 6, 8 -
range(6,2,-1)—6, 5, 4, 3
快速检查 17.1
以下表达式评估为哪个值序列?如果你想在 Spyder 中检查自己,请编写一个循环,遍历这些范围,并打印循环变量的值:
1
range(0,9)2
range(3,8)3
range(-2,3,2)4
range(5,-5,-3)5
range(4,1,2)
for 循环可以遍历任何值序列,而不仅仅是数字。例如,一个字符串是由字符字符串组成的序列。
17.2. 遍历字符串
回想一下,循环变量在每次迭代中依次取序列中每个项的值。一个遍历数字 0, 1, 2 的循环变量在第一次遍历时取值为 0,在第二次遍历时取值为 1,在第三次遍历时取值为 2。如果你遍历一个字符串中的字符序列,那么你可能会有一个序列 a, b, c 而不是 0, 1, 2。遍历字符序列的循环变量将在第一次遍历时取值为 a,在第二次遍历时取值为 b,在第三次遍历时取值为 c。
在 第 16.2 节 中,你看到了在 for 循环的上下文中使用 in 关键字。在那里,in 关键字用于遍历值序列;在 第 16.2 节 中,值是从 0 到 N 的数字。in 关键字也可以用于遍历字符串中的字符,如下面的列表所示。给定一个字符串,比如 "abcde",你可以将其视为由字符串字符 a,b,c,d,e 组成的序列。
列表 17.1. 遍历字符串中每个字符的 for 循环
for ch in "Python is fun so far!": *1*
print("the character is", ch) *2*
-
1 ch 是循环变量。
-
2 打印循环变量
你创建的任何字符串都有一个固定的长度,因此遍历字符串中每个字符的 for 循环将重复与字符串长度相同的次数。在 列表 17.1 中,ch 是循环变量,它可以被命名为任何合法的 Python 变量名。你遍历的字符串长度为 21,因为空格和标点符号也被算作字符。在 列表 17.1 中的 for 循环重复 21 次;每次,变量 ch 将取字符串 "Python is fun so far!" 中每个不同字符的值。在代码块内部,你正在将循环变量的值打印到 Python 控制台。
快速检查 17.2
Q1:
编写一段代码,提示用户输入。然后编写一个循环,遍历每个字符。每当遇到元音字符时,代码会打印
vowel。
列表 17.1 中描述的方法是遍历字符串中每个字符的直观方式,并且当处理字符串中的字符时,这应该是你的首选方法。
如果 Python 不允许你直接遍历字符串中的字符,你将不得不使用一个循环变量,该变量遍历表示每个字符位置的整数序列,从 0 到字符串长度减 1。列表 17.2 展示了使用这种技术重写的列表 17.1。在列表 17.2 中,你必须为字符串创建一个变量,以便你可以在循环代码块内部访问它。循环仍然重复 21 次,但现在循环变量取值为 0,1,2,...20,以表示字符串中的每个索引位置。在代码块内部,你必须索引你的字符串变量以找到每个索引处的字符值。这段代码很繁琐,并且远不如列表 17.1 直观。
列表 17.2. 一个遍历字符串每个索引的for循环
my_string = "Python is fun so far!" *1*
len_s = len(my_string) *1*
for i in range(len_s): *2*
print("the character is", my_string[i]) *3*
-
1 将字符串及其长度存储在变量中
-
2 在 0 到 len_s - 1 之间迭代
-
3 字符串索引
图 17.1 展示了列表 17.1(在左侧)和列表 17.2(在右侧)的流程图。当你直接遍历字符时,循环变量会获取字符串中每个字符的值。当你遍历索引时,循环变量会获取从 0 到字符串长度减 1 的整数值。因为循环变量包含整数,所以你必须使用整数来索引字符串以检索该位置的字符值。这是使用my_string[i]计算的一个额外步骤。请注意,在列表 17.2 中,你需要跟踪更多的事情,而在列表 17.1 中,循环变量已经直接知道字符的值。
图 17.1. (A) 列表 17.1 和 (B) 列表 17.2 的流程图比较。在(A)中,循环变量ch取每个字符的值。在(B)中,循环变量取代表字符串索引的整数值,从 0 到字符串长度减 1。在(B)中,循环体内部有一个额外的步骤,将循环索引转换为该循环索引处的字符。

像程序员一样思考
编写更多的代码行或看起来复杂的代码并不会让你成为一个更好的程序员。Python 是一个很好的入门语言,因为它易于阅读,所以编写你的代码要遵循这个理念。如果你发现自己正在编写复杂的逻辑来完成一个简单的任务,或者重复多次,请退一步,用一张纸画出你想要实现的内容。使用互联网查看 Python 是否有简单的方法来完成你想要做的事情。
摘要
在本节课中,我的目标是向您介绍可以是一系列整数的序列。您看到了如何通过改变序列的起始值或结束值,甚至跳过数字来定制range表达式。序列也可以是一系列字符串字符。您看到了如何编写利用直接遍历字符串字符的能力的代码。以下是主要收获:
-
for循环使用一个循环变量,该变量从一系列项目中的值中取值;项目可以是整数或字符字符串。 -
当序列中的项目是整数时,您可以使用特殊的
range表达式来创建特殊序列。 -
当序列中的项目是字符串字符时,循环变量直接遍历字符串中的字符,而不是使用字符串的索引作为中间人。
让我们看看你是否掌握了这个...
Q17.1
编写一个程序,遍历 1 到 100 之间的所有偶数。如果该数字也能被 6 整除,则增加计数器。在程序结束时,打印出有多少个数字既是偶数也能被 6 整除。
Q17.2
编写一个程序,提示用户输入一个数字
n。然后使用循环重复打印消息。例如,如果用户输入99,则您的程序应打印如下内容:99 books on Python on the shelf 99 books on Python Take one down, pass it around, 98 books left. 98 books on Python on the shelf 98 books on Python Take one down, pass it around, 97 books left.... <等等>
1 book on Python on the shelf 1 book on Python Take one down, pass it around, no more books!Q17.3
编写一个程序,提示用户输入由单个空格分隔的姓名。您的程序应为每个输入的姓名打印问候语,每个问候语后跟一个换行符。例如,如果用户输入
Zoe Xander Young,则您的程序应输出Hi Zoe,然后在新的一行上输出Hi Xander,然后在新的一行上输出Hi Young。这个问题稍微复杂一些。回想一下您学到的关于字符串的知识;您将需要使用循环来查看输入中的每个字符,并将您看到的内容保存到代表姓名的变量中。别忘了在看到空格时重置您的姓名变量!
第 18 课。在条件保持时重复任务
阅读第 18 课第 18 课后,你将能够
-
理解另一种在程序中编写循环的语法
-
在某个条件为真时重复动作
-
提前退出循环
-
在循环中跳过语句
在前面的课程中,你假设你知道想要重复代码块次数。但假设,例如,你正在和你朋友玩游戏。你的朋友试图猜你心里想的数字。你事先知道你的朋友会猜多少次吗?实际上不知道。你想要不断要求他们再试一次,直到他们猜对。在这个游戏中,你不知道你想要重复任务的次数。因为你不知道,你不能使用for循环。Python 有另一种在这种情况下有用的循环类型:while循环。
考虑这个
仅使用以下场景中提供的信息,你知道你想要重复任务的最大次数吗?
-
你有五个电视频道,你使用向上按钮循环查看每个频道有什么节目。
-
吃掉盒子里的所有饼干。
-
每次看到大众甲壳虫时都说“打甲壳虫”。
-
在你的慢跑歌曲播放列表上点击“下一曲”,直到你已经试听了 20 首歌曲。
答案:
-
是的
-
是的
-
不是
-
是的
18.1. 当条件为真时循环
如果你有一个必须重复不确定次数的任务,for循环就不合适,因为它不会工作。
18.1.1. 循环进行猜测
从猜词游戏开始。你想一个秘密单词,并要求玩家猜你的单词。每次玩家猜测时,告诉他们他们是否猜对了。如果他们猜错了,再问一次。记录玩家猜对的次数。图 18.1 显示了这个游戏的流程图。
图 18.1. 猜词游戏的流程图。用户猜测一个单词。猜测循环由灰色菱形表示,它检查用户的猜测是否等于秘密单词。如果是,游戏结束。如果不是,你告诉玩家他们猜错了,要求他们再次猜测,并将他们猜测的次数加 1。

列表 18.1 展示了游戏的代码实现。用户试图猜测程序员选择的秘密单词。首先提示用户输入一个单词。第一次进入 while 循环时,您将用户猜测与秘密单词进行比较。如果猜测不正确,您将进入由三个语句组成的 while 循环代码块。首先打印用户迄今为止猜测的次数。然后要求用户再次猜测;请注意,用户的猜测被分配给变量 guess,该变量用于 while 循环的条件中,以检查猜测是否与秘密单词匹配。最后,您增加尝试次数以准确统计用户尝试猜测单词的次数。
执行这三行代码后,您再次检查 while 循环的条件,这次是使用更新的猜测。如果用户继续错误地猜测秘密单词,程序不会越过 while 循环及其代码块。当用户猜对秘密单词时,while 循环的条件变为假,while 循环的代码块不会执行。相反,您跳过 while 代码块,移动到 while 循环及其代码块之后的语句,并打印一条祝贺信息。在这个游戏中,您必须使用 while 循环,因为您不知道用户会给出多少次错误的猜测。
列表 18.1. 一个猜谜游戏的 while 循环示例
secret = "code"
guess = input("Guess a word: ")
tries = 1
while guess != secret: *1*
print("You tried to guess", tries, "times")
guess = input("Guess again: ") *2*
tries += 1
print("You got it!") *3*
-
1 检查猜测是否与秘密单词不同
-
2 再次询问用户
-
3 当猜测正确时到达
在这一点上,您应该注意到代码块必须包含一个语句来更改条件本身。如果代码块独立于条件,您将进入无限循环。在 列表 18.1 中,猜测是通过要求用户输入另一个单词来更新的。
18.1.2. while 循环
在 Python 中,开始 while 循环的关键字不出所料是 while。以下列表显示了编写 while 循环的一般方法。
列表 18.2. 编写 while 循环的一般方法
while <condition>: *1*
<do something>
- 1 表示循环的开始
当 Python 首次遇到 while 循环时,它会检查条件是否为真。如果是,它将进入 while 循环代码块,并执行该块中的语句。完成代码块后,它再次检查条件。只要条件为真,它就会执行 while 循环内部的代码块。
快速检查 18.1
Q1:
编写一段代码,提示用户输入介于 1 和 14 之间的数字。如果用户猜对了,打印
You guessed right, my number was并然后打印该数字。否则,继续要求用户再次猜测。
18.1.3. 无限循环
使用 while 循环,可以编写永远不会结束的代码。例如,这段代码无限地打印 when will it end?!
while True:
print("when will it end?!")
让这样的程序长时间运行会减慢你的电脑。但如果是这种情况,不要慌张!有几种方法可以手动停止进入无限循环的程序,如图 18.2 所示。你可以做以下之一:
-
点击控制台顶部的红色方块。
-
点击控制台,然后按 Ctrl-C(按住 Ctrl 键然后按 C 键)。
-
点击控制台中的菜单(在红色方块旁边)并选择“重启内核”。
图 18.2. 要手动退出无限循环,你可以点击红色方块,或者按 Ctrl-C,或者选择红色方块旁边的控制台菜单中的“重启内核”。

18.2. 使用 for 循环与 while 循环的比较
任何for循环都可以转换为while循环。for循环迭代一组固定次数。要将此转换为while循环,你需要添加一个变量,其值在while条件中检查。该变量在每次通过while循环时都会改变。以下列表显示了并排的for循环和while循环。while循环的情况更详细。你必须自己初始化循环变量;否则,Python 不知道在循环内部你指的是哪个变量x。你还必须增加循环变量。在for循环的情况下,Python 会自动为你完成这两个步骤。
列表 18.3. 将for循环重写为while循环
使用 for 循环
for x in range(3): *1*
print("var x is", x)
使用 while 循环
x = 0 *2*
while x < 3:
print("var x is", x)
x += 1 *3*
-
1 表示循环的开始
-
2 初始化循环变量
-
3 增加循环变量
在列表 18.3 中,你必须创建另一个变量。你必须在while循环内部手动增加它的值;记住,for循环会自动增加循环变量的值。图 18.3 显示了如何可视化列表 18.3 中的代码。
图 18.3. (A)显示了打印循环变量值的for循环代码,每次循环迭代都会打印。 (B)显示了转换为while循环的while循环代码以及变量在for循环转换为while循环时的变化值。你必须自己创建变量并在while循环体内部自己增加它。此外,你必须编写一个作为变量函数的条件,这将导致while循环体重复三次。

任何for循环都可以转换为while循环。但并非所有while循环都可以转换为for循环,因为有些while循环没有固定的迭代次数。例如,当你要求用户猜测一个数字时,你不知道用户需要尝试多少次才能猜到数字,因此你不知道使用for循环应该使用什么序列的项。
快速检查 18.2
Q1:
使用
for循环重写以下代码:password = "robot fort flower graph" space_count = 0 i= 0 while i < len(password): if password[i] == " ": space_count += 1 i += 1 print(space_count)
| |
像程序员一样思考
在编程中,通常有多种方法来完成某件事。有些方法简洁,而有些则冗长。一个优秀的 Python 程序员应该找到一种方法来编写尽可能简单、简洁的代码,同时易于理解。
18.3. 操作循环
你已经看到了 for 和 while 循环的基本结构。它们的行为简单明了,但它们有点限制性。当你使用 for 循环时,你会根据你的值序列(整数或字符串字符)中的项目数量来遍历循环。使用 while 循环时,唯一停止重复的方法是让 while 循环条件变为假。
但为了编写更灵活的代码,你可能希望有提前退出 for 循环的选项。或者,如果你在代码块中遇到一个与 while 循环条件无关的事件,你可能希望提前退出 while 循环。
18.3.1. 提前退出循环
你知道退出 while 循环的唯一方法就是让 while 条件变为假。但通常你希望提前退出循环(无论是 for 循环还是 while 循环)。Python 中的一个关键字 break 使得在 Python 执行该关键字时可以随时退出循环,即使 while 循环条件仍然为真。列表 18.4 展示了使用 break 关键字的示例代码。
在 列表 18.4 中,你看到了在循环中添加了一个额外的条件来检查用户是否尝试猜测至少 100 次。当这个条件为真时,你会打印一条消息并退出循环。当你遇到 break 语句时,循环会立即终止;break 语句之后,但仍在循环内的任何内容都不会执行。因为现在有退出循环的原因不仅仅是用户猜对了词,你必须在循环之后添加一个条件来检查循环终止的原因。
列表 18.4. 使用 break 关键字
secret = "code"
max_tries = 100
guess = input("Guess a word: ")
tries = 1
while guess != secret:
print("You tried to guess", tries, "times")
if tries == max_tries:
print("You ran out of tries.")
break *1*
guess = input("Guess again: ")
tries += 1
if tries <= max_tries and guess == secret: *2*
print("You got it!") *2*
-
1 当超过最大尝试次数时退出循环
-
2 检查退出循环的原因
为什么这段代码在 while 循环之后有一个额外的 if 语句?考虑两种情况:用户猜对了秘密词,或者用户用完了尝试次数。在任何一种情况下,你都会停止执行 while 循环,并执行 while 循环块之后的任何语句。你只想在退出循环是因为正确猜测的情况下打印祝贺信息。祝贺信息必须在 while 循环终止后打印,但你不能直接打印信息。while 循环可能因为用户用完了尝试次数而终止;你只是不知道为什么退出 while 循环。
你需要添加一个条件来检查用户是否还有剩余的尝试次数,以及用户的猜测是否与秘密匹配。这个条件确保如果用户还有剩余的尝试次数,你退出 while 循环是因为用户猜对了秘密词,而不是因为用户用完了尝试次数。
像程序员一样思考
总是创建变量来存储你将在代码中多次重用的值是一个好主意。在列表 18.4 中,你创建了一个名为max_tries的变量来保存用户猜测次数的数量。如果你决定更改该值,你只需在初始化的地方更改一次(而不是试图记住你使用它的所有地方)。
break语句与for和while循环一起工作。它在许多情况下可能很有用,但你必须谨慎使用。如果你有嵌套循环,只有break语句所在的循环会终止。
快速检查 18.3
编写一个程序,使用while循环让用户猜测你选择的秘密单词。用户有 21 次机会。当用户猜对时,结束程序。如果用户用完了所有 21 次机会,退出循环并打印一条适当的消息。
18.3.2. 跳到循环的开始
上一个部分中的break语句会导致循环中剩余的所有语句被跳过,并且执行的下一条语句是循环之后的语句。
你可能还会遇到的情况是你想要跳过循环内的任何剩余语句,并回到循环的开始再次检查条件。为此,你使用continue关键字,这通常用于使代码看起来更整洁。考虑列表 18.5。这两个版本的代码做的是同一件事。在第一个版本中,你使用嵌套条件来确保所有条件都得到满足。在第二个版本中,continue关键字跳过循环内的所有后续语句,并快速跳转到循环的开始,使用序列中的下一个x。
列表 18.5. 比较使用和不使用continue关键字的代码
### Version 1 of some code ###
x = 0
for x in range(100):
print("x is", x)
if x > 5:
print("x is greater than 5")
if x%10 != 0:
print("x is not divisible by 10")
if x==2 or x==4 or x==16 or x==32 or x==64:
print("x is a power of 2")
# perhaps more code
### Version 2 of some code ###
x = 0
for x in range(100):
print("x is", x)
if x <= 5:
continue *1*
print("x is greater than 5") *2*
if x%10 == 0:
continue *1*
print("x is not divisible by 10") *3*
if x!=2 and x!=4 and x!=16 and x!=32 and x!=64:
continue *1*
print("x is a power of 2") *4*
# perhaps more code
-
1 跳过剩余的循环语句
-
2 当 x > 5 时到达这里
-
3 当 x%10 ! = 0 时到达这里
-
4 当 x 是 2、4、16、32 或 64 时到达这里
在列表 18.5 中,你可以编写相同代码的两个版本:带有和不带有continue关键字。在包含continue关键字的版本中,当条件表达式评估为真时,循环中剩余的所有语句都会被跳过。你将回到循环的开始,为x分配下一个值,就像循环语句被执行一样。但是,不使用continue关键字的代码最终会比使用continue关键字的代码复杂得多。在这种情况下,当你有很多代码需要在多个嵌套条件成立时执行时,使用continue关键字是有用的。
概述
在本课中,我的目标是让你使用while循环编写重复特定任务的程序。while循环在某个条件保持时重复任务。
每次你编写一个for循环时,你都可以将其转换为while循环。但反过来并不总是可能的。这是因为for循环会重复执行一定次数,但进入while循环的条件可能没有已知、固定的次数。在示例中,你看到了可以要求用户输入一个值;你不知道用户会输入多少次错误值,这就是为什么在这种情况下while循环很有用。
你还看到了如何在循环中使用break和continue语句。break语句用于停止执行最内层循环中剩余的所有语句。continue语句用于跳过最内层循环中剩余的所有语句,并从最内层循环的开始处继续。
这里是这个课程的关键要点:
-
while循环会在特定条件保持的情况下重复执行语句。 -
可以将
for循环写成while循环,但反过来可能不成立。 -
可以使用
break语句提前退出循环。 -
可以使用
continue语句来跳过循环中剩余的语句,并再次检查while循环的条件,或者跳到for循环序列中的下一个项目。 -
如果尝试一种循环(
for或while)并发现它不适合你的程序,没有惩罚。尝试一些事情,把它看作是尝试拼凑一个编码谜题。
让我们看看你是否掌握了这个...
Q18.1
这个程序有一个错误。更改一行以避免无限循环。对于几组输入,写出程序执行的操作和它应该执行的操作。
num = 8 guess = int(input("Guess my number: ")) while guess != num: guess = input("Guess again: ") print("Right!")Q18.2
编写一个程序,询问用户是否想玩游戏。如果用户输入
y或yes,表明你在想一个介于 1 到 10 之间的数字,并要求用户猜测这个数字。你的程序应该继续要求用户猜测数字,直到他们猜对为止。如果他们猜对了,打印一条祝贺信息,然后询问他们是否想再玩一次。只要用户输入y或yes,这个过程就应该重复进行。
第 19 课. 期末项目:Scrabble,艺术版
在阅读第 19 课之后,你将能够
-
应用条件语句和循环来编写更复杂的程序
-
理解程序中对你提出的要求
-
在开始编码之前,制定解决问题的计划
-
将问题分解成更小的子问题
-
编写解决方案的代码
你正在和你孩子玩一个更简单的 Scrabble 版本。到目前为止,孩子们赢得了大多数游戏,你意识到这是因为你没有从给定的瓷砖中挑选出最好的单词。你决定你需要一点计算机程序的帮助。
问题
编写一个程序,可以告诉你可以从一组瓷砖中形成的单词;所有有效单词的集合是所有英语单词的子集(在这种情况下,仅限于与艺术相关的单词)。在处理从给定瓷砖中选择最佳单词时,以下是一些需要注意的细节:
-
所有与艺术相关的有效单词都给你作为字符串,每个单词由换行符分隔。字符串按长度组织单词,从短到长。所有有效单词只包含字母(没有空格、连字符或特殊符号)。例如,
"""art hue ink oil pen wax clay draw film ... crosshatching """ -
你得到的瓷砖数量可能不同;它不是一个固定的数字。
-
瓷片上的字母没有点值;它们都值相同。
-
你得到的瓷砖是以字符串的形式给出的。例如,
tiles = "hijklmnop"。 -
以字符串元组的形式报告你可以用你的瓷砖形成的所有有效单词;例如,
('ink', 'oil', 'kiln')。
19.1. 理解问题陈述
这个编程任务听起来很复杂,所以试着将其分解成几个子任务。这个问题有两个主要部分:
-
以你可以处理的形式表示所有可能的有效单词。将单词从一长串字符转换为字符串单词元组。
-
判断列表中所有有效单词是否可以用你给出的瓷砖组合成。
19.1.1. 改变所有有效单词的表示形式
让我们解决第一部分,这将帮助你创建所有有效单词的元组,以便你以后可以处理它们。你需要这样做,因为如果你保持有效单词不变,你将有一个难以处理的字符长串。
所有有效单词的集合以字符串的形式给出。对计算机来说,人类看到的每一行并不是带有单词的行,而是一长串字符。
绘制出问题
总是先画一个小草图,看看你需要做什么。对人眼来说,字符串看起来组织得很好,你可以分辨出单词,但计算机不知道字符串中单词的概念,它只看到单个字符。计算机看到的是类似"""art\nhue\nink\noil\n...\ncrosshatching"""的东西。
你用眼睛可以看到的行断是单个字符,每个都称为换行符(或行断),表示为\n。你需要找出每个换行符的位置,以便分隔每个单词。图 19.1 展示了你可能会以更系统的方式考虑这一点。
图 19.1. 将字符字符串转换为单词。在上面的例子中,命名为start,你可以通过使用开始和结束指针来跟踪你在字符字符串中的位置。在中间的例子中,命名为found word,当你达到换行字符时停止改变结束指针。在底部的例子中,命名为start next,你将开始和结束指针重置为换行符之后的字符。

通过这个简单的草图,你就可以看到如何完成这个任务。开始和结束指针从大字符串的开头开始。当你寻找换行字符来标记单词的结尾时,你会增加结束指针直到找到\n。在那个点上,你可以从开始指针到结束指针存储单词。然后,将两个指针移动到换行字符之后的索引处,开始寻找下一个单词。
提出一些例子
编写一些测试用例,你可能想在编写程序时考虑这些用例。尽量考虑简单的情况和复杂的情况。例如,所有有效的单词可能只是一个单词,比如words = """art""",或者可能是一两个单词,比如问题陈述中给出的例子。
将问题抽象为伪代码
现在你已经了解了如何将字符转换为单词,你可以开始编写代码和文本的混合体,帮助你将整体情况定位,并开始考虑细节。
因为你需要查看字符串中的所有字母,你需要一个循环。在循环中,你决定是否找到了换行字符。如果你找到了换行字符,保存单词并重置你的指针索引。如果你没有找到换行字符,只增加结束索引直到你找到。伪代码可能看起来像这样:
word_string = """art
hue
ink
"""
set start and end to 0
set empty tuple to store all valid words
for letter in word_string:
if letter is a newline:
save word from start to ends in tuple for all valid words
reset start and end to 1 past the newline character
else:
increment end
19.1.2. 使用给定的拼图制作一个有效的单词
现在,你可以考虑使用给定的拼图来决定是否可以组成一个有效的单词,有效的单词来自允许的单词列表。
绘制出问题
如同往常,画图有助于理解。这个问题的一部分逻辑可以通过几种方式来处理:
-
你可以从查看手中的拼图开始。找出所有可能的组合。然后你可以查看每个字母组合,看看它们是否与任何有效的单词匹配。
-
你可以从查看有效的单词开始,看看每个是否可以用你拥有的拼图制作。
像程序员一样思考
当你试图决定如何解决问题时,这部分内容至关重要。绘图的过程可以帮助你在确定一种方法之前想出几种方法。如果你立即开始编码,你可能会感觉被限制在一条可能或可能不适合当前问题的路径上。草图的过程将帮助你看到在没有做出任何承诺的情况下,几种解决方案可能引发的问题。
第一个选项,虽然可能更直观,但根据你目前所知,实现起来可能有点困难,因为它涉及到找到所有拼图块的所有组合和排列。第二个选项目前更合适。图 19.2 说明了第二个选项。
图 19.2。给定有效的单词和一组拼图块,从第一个有效的单词开始,检查其所有字母是否都在拼图块的集合中。如果是这样,将其添加到你可以用它组成的单词集合中。如果至少有一个字母在有效的单词中但不在拼图块中,你就无法组成那个单词。

你将遍历每个有效的单词,然后查看该单词中的每个字母,检查你是否能在你的拼图块中找到该字母。在你遍历了单词的所有字母并且你能在你的拼图块中找到它们之后,你就可以用你的拼图块组成那个单词。一旦你发现有一个字母不在你的拼图块中,你就可以立即停止,因为你无法组成那个单词。
提出一些例子
提出例子可以帮助你确定你可能需要在代码中处理的特殊情况。以下是一些你可能想要确保你的代码可以处理的拼图块示例:
-
单个拼图块——在这种情况下,你无法组成任何有效的单词。
-
所有的拼图块恰好可以组成一个有效的单词——当
tiles = "art"时,你可以组成单词art。 -
给定的所有拼图块恰好可以组成两个有效的单词——当
tiles = "euhtar"时,你可以组成art和hue。 -
你可以组成一个有效的单词,但会有多余的拼图块剩下——当
tiles = "tkabr"时,你可以组成art,并且还剩下k和b。 -
你只有一个特定字母的拼图块,但一个有效的单词需要两个该字母——当
tiles = "colr"时,你无法组成单词color,因为你只有一个o。
将问题抽象成伪代码
使用伪代码,你可以开始思考在提出例子时发现的更多细节。你需要遍历每个有效的单词,看看你是否可以用你的拼图块组成它,所以你需要一个循环。然后你将遍历该单词中的每个字母;你需要在第一个循环内部嵌套一个循环。一旦你找到一个不在你的拼图块中的字母,你就可以立即退出内部循环。但如果你看的每个字母都在你的拼图块中,你就继续。
这个逻辑有两个棘手的部分:(1)如何跟踪有相同字母多次出现的单词,以及(2)如何判断你在拼图中找到了完整的单词。你不需要在伪代码中详细说明如何做到这些,但你应该能够判断这些问题是否可以解决。我可以告诉你,它们是可以解决的,你将在下一节中看到。这个部分的伪代码可能如下所示
for word in valid_words:
for letter in word:
if letter not in tiles:
stop looking at remaining letters and go to next word
else:
remove letter from tiles (in case of duplicates)
if all letters in valid word found in tiles:
add word to found_words
注意,这个问题中有很多事情在进行,并且比你所习惯的问题有更多的变量需要跟踪!如果不先思考这个问题,你会很快迷失方向。在这个时候,如果你理解了这个问题的主要组成部分,你就可以开始编写代码了。一个重要的第一步是决定如何将你的代码分成更小、更易于管理的块。
像程序员一样思考
将代码分成更小的部分是程序员的一项必要且重要的技能,有几个重要的原因:
-
将大问题分解成小块后,它们看起来就不那么令人畏惧了。
-
当你可以只关注问题的相关部分时,编写代码会更容易。
-
与整个程序相比,模块的可能输入数量通常要小得多,因此这些部分更容易调试。
当你知道每个单独的部分都按预期工作后,你可以将它们组合起来创建你的最终程序。你编写的程序越多,你就越能掌握什么会是一个好的、连贯的代码块。
19.2. 将你的代码分成块
你现在可以开始思考如何将代码分成逻辑上的小块。第一块通常是查看输入并提取你想要使用的所有有用信息。
-
设置与艺术相关的有效单词(作为一个字符串)和设置你开始时的拼图(作为一个字符串)。
-
设置起始和结束指针的初始化,以找到所有有效单词。
-
设置一个空元组,以便在找到所有有效单词时将其添加到其中。
-
为你在拼图中发现的所有单词设置一个空元组。
列表 19.1 提供了这些初始化的代码。你会注意到一些新东西:一个包含字符的字符串变量,这些字符位于三重引号之间。三重引号允许你创建跨越多行的字符串对象。三重引号内的所有字符都是字符串对象的一部分,包括换行符!
列表 19.1. Scrabble: Art Edition 初始化代码
words = """art *1*
hue
ink
...
crosshatching
"""
tiles = "hijklmnop"
all_valid_words = () *2*
start = 0 *3*
end = 0 *4*
found_words = () *5*
-
1 作为大字符串的有效单词
-
2 用于所有有效单词的空元组
-
3 初始化索引搜索的起始指针
-
4 初始化索引搜索的结束指针
-
5 用于拼图中找到的单词的空元组
在这个程序中,第二个逻辑块是将所有单词的大字符串转换为包含字符串元素的元组。根据之前编写的伪代码,你只需要将英语部分转换为代码。你必须小心处理的部分是如何将有效单词添加到你的有效单词元组中。注意,你找到的单词是一个单独的单词,它被添加到元组中,所以你将不得不在你的有效单词元组和刚刚找到的单词的单例元组之间使用连接运算符。
列表 19.2 显示了代码。指针start和end最初为 0,指向第一个字符。单词作为一个大字符串被读取,因此你遍历每个字符。当字符是换行符时,你知道你已经到达了单词的末尾。在那个点上,你通过使用start和end指针位置来索引保存单词。然后,将指针重置为换行符位置之后的一个位置;这是下一个有效单词开始的字符。如果字符不是换行符,你仍在读取它是什么单词,所以只移动end指针。
列表 19.2. 拼字游戏:艺术版代码以获取所有有效单词
for char in words: *1*
if char == "\n": *2*
all_valid_words = all_valid_words + (words[start:end],) *3*
start = end + 1 *4*
end = end +1 *4*
else:
end = end + 1 *5*
-
1 遍历每个字符
-
2 检查字符是否为换行符
-
3 将单例元组添加到当前所有有效单词元组中
-
4 将开始和结束指针移动到下一个单词的开始
-
5 只移动结束指针
第三和最后一个逻辑块是检查每个有效单词是否可以使用你的拼图来制作。与前面的块一样,你可以复制伪代码并填写空白。在伪代码中留下了几个有趣但未解决的问题:(1) 如何跟踪具有相同字母多个副本的单词,以及(2) 如何判断你已经在拼图中找到了完整的单词。
为了解决(1),你可以编写代码,在匹配到有效单词时移除拼图。对于每个新的有效单词,你可以使用一个名为tiles_left的变量,最初包含你所有的拼图,以跟踪你剩余的拼图。当你遍历有效单词中的每个字母并发现它在你的拼图中时,你可以更新tiles_left以包含所有字母,除了刚刚找到的字母。
为了解决(2),你知道如果你找到了所有拼图,并且你在找到它们时从tiles_left中移除了拼图,那么移除的拼图数量加上有效单词的长度将等于你开始时的拼图数量。
以下列出代码。这段代码中有一个嵌套循环。外层循环遍历每个有效单词,内层循环遍历给定有效单词的每个字母。一旦你看到单词中有一个不在你的瓷砖中的字母,你就可以停止查看这个单词,转到下一个单词。否则,继续查找。变量 tiles_left 存储检查有效单词中的字母是否在你瓷砖中后你剩下的瓷砖。每次你发现一个字母在你瓷砖中,你就得到它的位置,并使用所有剩余的字母创建一个新的 tiles_left。最后一步是检查你是否用完了所有字母制作了一个完整的、有效的单词。如果是这样,就添加这个单词。
列表 19.3. 检查是否可以用瓷砖制作有效单词的游戏代码
for word in all_valid_words: *1*
tiles_left = tiles *2*
for letter in word: *3*
if letter not in tiles_left: *4*
break *4*
else:
index = tiles_left.find(letter) *5*
tiles_left = tiles_left[:index]+tiles_left[index+1:] *6*
if len(word) == len(tiles)-len(tiles_left): *7*
found_words = found_words + (word,) *8*
print(found_words)
-
1 查看每个有效单词
-
2 tiles_left 处理重复的瓷砖
-
3 查看有效单词中的每个字母
-
4 如果字母不在 tiles_left 中则停止查找
-
5 在 tiles_left 中查找字母的位置
-
6 移除字母并创建新的 tiles_left
-
7 检查是否找到了整个单词
-
8 将单词添加到 found_words
最后,你打印出你可以制作的所有单词。但你可以调整结果,以选择特定长度的单词,或者只选择最长的单词,或者包含特定字母的单词(无论你更喜欢哪种)。
摘要
在本课中,我的目标是向你展示如何思考解决复杂问题的方法,并带你通过一个你可以使用编程来为你的情况编写自定义程序的现实生活问题。在编码之前理解问题可以是一个巨大的信心提升。你可以使用图片或简单的输入值和预期输出,以帮助你细化对问题的理解。
当你理解了问题后,你应该编写一些伪代码。你可以混合使用英语和代码来查看在开始编码之前是否需要进一步分解问题。
最后一步是查看你提出的视觉表示和抽象,并将这些作为代码中的自然划分。这些较小的部分在编码时更容易处理,并且它们也提供了在编码时休息、测试和调试代码的自然点。
这里是关键要点:
-
通过绘制一些相关的图片来理解被提出的问题。
-
通过提出一些简单的测试用例来理解被提出的问题。
-
将问题的部分进行泛化,以得出公式或完成每个部分的逻辑。
-
伪代码可能很有用,特别是对于编写包含条件或循环结构的算法逻辑。
-
从代码片段的角度思考,并问自己代码是否有任何自然的划分——例如,初始化变量、实现一个或多个算法和清理代码。
单元 5. 将你的代码组织成可重用块
在上一个单元中,你学习了如何编写自动重复任务的代码。你的程序现在变得越来越复杂了!
在本单元中,你将了解如何开始将你的代码组织成函数。函数是可重用的代码块,可以在程序的任何位置调用以执行特定任务。功能代码块可以接收输入,执行操作,并将结果发送回程序中需要它的任何部分。使用函数会使你的代码看起来更加整洁且易于阅读。
在综合项目中,你将编写一个程序,该程序从两个文件中读取数据:一个文件包含你朋友的姓名和电话号码,另一个文件包含地区代码和他们所在的州。你的程序将告诉你你有多少个朋友以及他们来自哪些州。
第 20 课. 编写持久程序的构建
在阅读第 20 课之后,你将能够
-
理解一个更大的任务是如何被分解成模块的
-
理解为什么你应该隐藏复杂任务的细节
-
理解任务相互依赖或相互独立的意义
你看到了循环如何有助于让计算机重复执行一组语句多次。当你编写代码时,重要的是要意识到你如何利用计算机的力量使你的生活变得更轻松。在本课中,你将把这个想法更进一步,看看如何将一个较大的程序分解成较小的子程序,每个子程序都是为了完成一个特定的任务。
例如,如果你把制造汽车的过程看作是一个大程序,你永远不会建造一个能制造整个汽车的机器。那将是一个极其复杂的机器。相反,你会建造各种机器和机器人,专注于执行不同的特定任务:一个机器可能组装车架,一个可能喷漆车架,另一个可能编程车载电脑。
考虑这一点
你要结婚了!你没有时间自己处理所有事情,所以你想雇佣人来处理各种任务。列出你可以外包的任务。
回答:寻找并预订场地,决定餐饮(食物、酒吧、蛋糕),确定宾客名单(邀请人,跟踪出席者,座位安排),装饰,聘请司仪,并为婚礼团队打扮。
20.1. 将大任务分解成小任务
将一个任务分解成更小任务的背后主要思想是帮助你更有效地编写程序。如果你从一个较小的难题开始,你可以更快地调试它。如果你知道几个较小的难题按预期工作,你可以专注于确保它们能很好地协同工作,而不是试图一次性调试一个庞大而复杂的难题。
20.1.1. 在线订购商品
想想当你在线订购商品时会发生什么。你首先在网站订单表单上填写你的个人信息,然后以商品被送到你家结束。这个整个过程可以分解成几个步骤,就像你在图 20.1 中看到的那样:
-
你填写一个网页表单来下单。订单信息发送给卖家,卖家提取重要细节:什么商品,数量,以及你的姓名/地址。
-
使用商品类型和数量,卖家(人或机器人)在仓库中找到商品并将其交给包装员。
-
包装员将商品取走并放入一个箱子中。
-
使用你的姓名/地址,其他人制作一个运输标签。
-
箱子与标签匹配,包裹被送往邮局,邮局负责找到你的房子并交付包裹。
图 20.1. 将在线订购商品的任务划分为更小、自包含和可重用的子任务的一种可能方式。每个灰色框代表一个任务。框左侧是任务的输入,框右侧是任务的输出。

图 20.1 展示了如何将订购商品的大任务划分为五个其他子任务。每个子任务可能由不同的人或机器处理,并代表在线订购商品过程中的不同专业领域。
这个例子还说明了几个其他重要的观点。第一个观点是 任务依赖/独立性。
定义
如果一个任务在另一个任务完成之前不能开始,那么这个任务就依赖于另一个任务。如果两个任务可以同时执行,那么这两个任务是独立的。
一些任务依赖于其他任务的完成,而有些任务是完全独立的。你首先执行“提取订单详情”任务。你使用它的输出来做“在仓库中找到商品”和“制作标签”任务。注意,最后这两个任务是相互独立的,可以按任何顺序执行。任务“装箱”依赖于“在仓库中找到商品”任务。任务“通过邮件发送”依赖于“装箱”和“制作标签”任务完成之前才能开始。
快速检查 20.1
以下动作是依赖的还是独立的?
1
(1) 吃派和 (2) 在一张纸上写下 3.1415927。
2
(1) 你没有互联网连接和 (2) 你无法检查你的电子邮件。
3
(1) 1 月 1 日和 (2) 天气晴朗。
定义
任务抽象是一种简化任务的方法,通过使用最少的信息来理解它;你隐藏了所有不必要的细节。
要理解在线订购商品时会发生什么,你不需要了解幕后每个细节。这引出了第二个观点:抽象。在仓库的例子中,你不需要知道如何在仓库中找到商品的细节;卖家是雇佣人来取你的商品还是使用复杂的机器人对你来说并不重要。你只需要知道你提供“商品名称”和“商品数量”,然后你就能得到请求的商品。
从广义上讲,要理解一个任务,你只需要在开始之前知道任务需要什么输入(例如,表格上的个人信息)以及任务将做什么(例如,物品出现在你的门口)。你不需要知道任务中每一步的细节来理解它做什么。
快速检查 20.2
对于以下每个任务,可能的输入和输出(如果有)是什么?询问执行每个动作所需的物品以及执行动作后得到的物品:
1
写婚礼请柬
2
打电话
3
抛硬币
4
买裙子
第三个想法是关于可重用子任务。
定义
可重用子任务是那些步骤可以与不同的输入一起重用以产生不同输出的任务。
有时候,你可能想要执行一个与另一个任务略有不同的任务。在仓库的例子中,你可能想要在仓库里找一本书,或者你可能想要找一辆自行车。为每一个你可能想要取回的物品都配备一个单独的机器人是没有意义的!这会导致太多做类似事情的机器人!最好做一个能够找到你想要的任何物品的机器人。或者制作两个机器人:一个用于取回大件物品,另一个用于小件物品。在创建子任务的同时,使子任务足够通用以便可重用之间的权衡可能是主观的。在接下来的几节课中,通过一点练习,你会掌握找到良好平衡的方法。
快速检查 20.3
Q1:
将以下任务分解成更小的子任务:“研究蜡笔的历史,写一篇五页的论文,并进行一次演讲。”绘制类似于图 20.1 的图表。
20.1.2. 理解主要观点
当你处理任务时,考虑每个任务都是一个黑盒。
定义
黑盒是一种可视化执行特定任务系统的方法。系统顶部的黑盒提醒你,你不需要(或不需要)看到盒子内部才能理解系统做什么。
现在,你不需要知道该任务是如何完成的;你只是在尝试从这些较小任务的角度可视化你的整体系统,而不陷入它们的细节。
从图 20.1 中的“在仓库中查找”任务开始,查看图 20.2 以了解任务在黑盒下的一个可能外观。没有黑盒,你会得到更多关于任务如何实现的细节——使用输入执行了哪些步骤和动作。但这些细节并不能帮助你理解任务本身;任务实现的细节对于理解任务做什么并不重要或必要。在某些情况下,看到这些细节甚至可能造成更多的困惑。最终,无论是否有黑盒,系统的输入和输出都是相同的。
图 20.2. 在任务上方带有和没有黑盒的“在仓库中查找”。看到在仓库中找到和取回物品的细节并不能增加对任务本身的更多理解。

每个任务都是一个动作或步骤的序列。这些步骤应该足够通用,以便可以针对任何适当的输入重复使用。你如何确定什么是一个适当的输入?你需要记录你的黑盒,以便任何想要使用它们的人都知道他们需要提供什么输入以及他们将会得到什么输出。
20.2. 在编程中引入代码黑盒
你之前看到的程序足够简单,整个程序就是一个黑盒。你被分配的任务并不复杂,不需要有专门用于不同任务的代码片段;你的整个程序都是代码片段,每个都执行一个任务。
到目前为止,你的程序主要做了以下事情:(1)向用户请求输入,(2)执行一些操作,以及(3)显示一些输出。从现在开始,你会发现将程序分解成更小、更易于管理的部分是很有帮助且必要的。每个部分将解决一部分谜题。你可以将这些部分组合起来实现一个更大的程序。
在编程中,这些任务被认为是代码的黑盒。你不需要知道每个代码块是如何工作的。你只需要知道输入到盒子的内容,盒子应该做什么,以及盒子给出的输出。你正在将编程任务抽象为这三部分信息。每个黑盒变成一个 模块 代码。
定义
代码模块是一段实现特定任务的代码。模块与输入、任务和输出相关联。
20.2.1. 使用代码模块
模块化 是将一个大程序分解成更小的任务。你为每个任务单独编写代码,独立于其他任务。一般来说,每个代码模块应该能够独立存在。你应该能够快速测试你为这个模块编写的代码是否工作。以这种方式将更大的任务分解会使更大的问题看起来更容易,并将减少你调试所需的时间。
20.2.2. 抽象代码
你可能看过电视并使用遥控器来换频道。如果我把组装电视和遥控器所需的所有部件都给你,你会知道如何把它们组装起来吗?可能不会。但如果我们为你组装了电视和遥控器,你会知道如何使用这两个设备来完成像换频道这样的任务吗?可能会的。这是因为你知道每个项目的输入,每个项目应该做什么,以及每个项目输出什么。图 20.3 和 表 20.1 展示了使用电视遥控器的输入、行为和输出。
图 20.3. 遥控器和电视的黑盒视图

表 20.1. 电视和遥控器用于更改频道或音量的输入、行为和输出
| 项目 | 输入 | 行为 | 输出 |
|---|---|---|---|
| 遥控器 | 按下按钮 | 根据按下的按钮生成信号 | 无线信号 |
| TV | 来自遥控器的无线信号 | 屏幕上的图像改变(整个图像或出现音量条)或音量改变 | 你看到或听到的内容改变 |
在编程中,抽象的目标是在高级别呈现想法。这是记录一段代码执行过程的过程,包含三个关键信息点:输入、任务和输出。你已经看到了如何使用这些信息来表示黑盒。
代码中的抽象消除了任务/模块代码实现细节;你不再查看模块的代码,而是查看它的文档。为了记录模块,你使用一种特殊的代码注释,称为docstring。docstring 包含以下信息:
-
模块的所有输入—由变量及其类型表示。
-
模块应该做什么—它的功能。
-
模块给出的输出是什么—这可能是一个对象(变量)或者模块打印的内容。
你将在下一课中看到代码和 docstring 的示例。
20.2.3. 代码重用
假设有人给你两个数字,你想要能够对这两个数字执行四种操作:加、减、乘和除。代码可能看起来像以下列表。
列表 20.1. 添加、减法、乘法和除法两个数字的代码
a = 1 *1*
b = 2 *1*
print(a+b)
print(a-b)
print(a*b)
print(a/b)
- 1 变量 a 和 b 用于执行 a + b, a - b, a * b 和 a / b 操作。
除了这段代码,你还想对另一对数字进行加、减、乘和除操作。然后是另一对数字。要编写一个程序对多对数字执行相同的四种操作,你必须复制粘贴列表 20.1 中的代码,并多次更改a和b的值。这听起来很繁琐,看起来也很丑,就像以下列表所示!请注意,无论变量a和b是什么,执行操作的代码都是相同的。
列表 20.2. 对三对数字进行加、减、乘和除的代码
a = 1 *1*
b = 2 *1*
print(a+b) *1*
print(a-b) *1*
print(a*b) *1*
print(a/b) *1*
a = 3 *2*
b = 4 *2*
print(a+b) *2*
print(a-b) *2*
print(a*b) *2*
print(a/b) *2*
a = 5 *3*
b = 6 *3*
print(a+b) *3*
print(a-b) *3*
print(a*b) *3*
print(a/b) *3*
-
1 对 a = 1 和 b = 2 执行操作的代码
-
2 对 a = 3 和 b = 4 执行操作的代码
-
3 对 a = 5 和 b = 6 执行操作的代码
这就是重用性概念发挥作用的地方。你执行操作并打印操作结果的部分在任意一对数字a和b之间是通用的。每次都复制粘贴它没有意义。相反,将这个通用的操作集视为一个黑盒;这个黑盒的输入(以及输出)会变化。图 20.4 展示了可以执行任何两个数字a和b(其中a和b现在是黑盒的输入)的四种简单数学操作的任务的黑盒视图。
图 20.4. 执行任何两个数字加、减、乘和除操作的代码的黑盒视图

现在,你不再需要在程序中复制粘贴代码并更改其中的一小部分,你可以在代码周围写一个黑色盒子,它是可重用的。这个盒子是一个 代码包装器,它给程序添加了一部分功能。你可以通过重用已经写好的包装器来编写更复杂的程序。在列表 20.2 中的代码可以使用黑色盒子的概念进行抽象,变成以下样子。变量 a 和 b 仍然会改变,但现在你正在使用包裹在黑色盒子中的代码。执行四个数学运算的四个代码行被简化为一个黑色盒子下的一个捆绑包。
列表 20.3. 为三对数字添加、减去、乘法和除法的代码
a = 1
b = 2
< wrapper for operations_with_a_and_b > *1*
a = 3
b = 4
< wrapper for operations_with_a_and_b > *1*
a = 5
b = 6
< wrapper for operations_with_a_and_b > *1*
- 1 (非实际代码) 用于表示执行四个操作的黑色盒子的占位符
在下一课中,你将看到如何编写围绕代码的黑色盒子的包装器的详细内容。你还将看到如何在你的程序中使用这些包装器。这些包装器被称为函数。
20.3. 子任务存在于它们自己的环境中
想象一下和另外两个人一起做一个小组项目;你必须研究电话的历史并做一个展示。你是领导者。你的工作是分配任务给其他两个人,并做最后的展示。作为领导者,你不需要做任何研究。相反,你召唤其他两个小组成员进行研究,然后他们把他们的结果传达给你。
另外两个人就像较小的工人模块,帮助你完成项目。他们负责进行研究,得出结果,并给你提供他们发现总结。这展示了将更大的任务分解为子任务的概念 *。
注意,作为领导者,你并不关心他们研究的细节。你不在乎他们是否使用互联网,去图书馆,或者采访一个随机的人群。你只想让他们告诉你他们的发现。他们给你的总结展示了 抽象细节 的概念。
每个进行研究的人可能会使用具有相同名称的物品。一个人可能读一本名为 电话 的儿童图画书,另一个人可能读一本名为 电话 的参考书。除非这两个人互相传递书籍或进行沟通,否则他们不知道对方收集了什么信息。每个研究人员都在他们自己的环境中,他们收集的任何信息都只属于他们——除非他们分享。你可以把代码模块想象成一个用于完成特定任务的微型程序。每个模块存在于它们自己的环境中,独立于其他模块的环境。模块内部创建的任何物品都是特定于该模块的,除非明确传递给另一个模块。模块可以通过输出和输入传递物品。你将在下一课中看到许多代码中这种外观的例子。
在团队项目示例中,团队项目就像主程序。每个人就像一个独立的模块,负责执行一项任务。有些任务可能相互通信,而有些则可能不通信。对于更大的团队项目,如果负责独立部分的人不需要与其他人共享信息,那么团队中的一些人可能不需要与其他人共享信息。但所有团队成员都与领导者沟通,以传达收集到的信息。
每个人都在一个独立的环境中进行研究。他们可能会使用不同的对象或方法进行研究,每种方法只对一个人的环境有用。领导者不需要知道研究进行的细节。
快速检查 20.4
Q1:
在本节描述的团队项目中,为研究电话的任务绘制一个黑盒系统。为每个人绘制一个黑盒,并指出每个人可能作为输入和可能输出的内容。
摘要
在本课中,我的目标是教你为什么将任务视为黑盒,最终视为代码模块很重要。你看到了不同的模块可以协同工作,相互传递信息以实现更大的目标。每个模块都生活在自己的环境中,它创建的任何信息都是私有的,除非明确通过输出传递。从更大的角度来看,你不需要知道模块完成其特定任务的细节。以下是主要收获:
-
模块是独立的,并且在其自身的自包含环境中。
-
代码模块应该只编写一次,并且可以用不同的输入进行重用。
-
抽象掉模块的细节,让你能够专注于许多模块如何协同工作以完成更大的任务。
让我们看看你是否掌握了这个...
Q20.1
将以下任务划分为更小的子任务:“在餐厅点了几份订单并获得了饮料和食物。”绘制一个图表。
第 21 课:使用函数实现模块化和抽象
在阅读第 21 课之后,你将能够
-
编写使用函数的代码
-
编写具有(零个或多个)参数的函数
-
编写可能或可能不返回指定值的函数
-
理解变量值在不同函数环境中的变化
在第 20 课中,你看到将更大的任务分解成模块可以帮助你思考问题。分解任务的过程导致了两个重要的想法:模块化和抽象。模块化是指有更小(更多或更少独立)的问题要逐一解决。抽象是指能够在更高层次上思考模块本身,而不必担心实现每个模块的细节。你已经在日常生活中做了很多这样的事情;例如,你可以使用汽车,而不必知道如何建造它。
这些模块对于清理更大的任务最有用。它们抽象了某些任务。你只需要确定一次如何实现一个任务的细节。然后你可以使用许多输入来重用任务以获取输出,而无需再次重写它。
考虑以下内容
对于以下每个场景,找出哪些步骤可以抽象成一个模块:
-
你在学校。你的老师正在点名。她叫一个名字。如果学生在这里,那个学生就会说出他们的名字。为每个上课的学生重复这个过程。
-
汽车制造商每天组装 100 辆汽车。组装过程包括(1)组装车架,(2)安装发动机,(3)添加电子设备,以及(4)喷漆。有 25 辆红色、25 辆黑色、25 辆白色和 25 辆蓝色的汽车。
答案:
-
模块:调用姓名
-
模块:组装车架 模块:安装发动机 模块:添加电子设备 模块:喷漆车身
21.1. 编写一个函数
在许多编程语言中,函数用于表示实现简单任务的代码模块。当你编写函数时,你必须考虑三件事:
-
函数接收的输入
-
函数执行的操作/计算
-
函数返回的内容
回想一下前面“考虑以下内容”练习中的点名示例。这是对该情况的一点点修改,以及使用函数的一个可能实现。函数以关键字def开始。在函数内部,你使用三引号注释说明函数做了什么——提到输入是什么,函数做了什么,以及函数返回什么。在列表 21.1 中显示的函数中,你使用for循环检查教室名单上的每个学生是否也物理上在教室里。符合这个标准的人会打印出他们的名字。在for循环的末尾,你返回字符串finished taking attendance。单词return也是与函数相关联的关键字。
列表 21.1. 课堂点名函数
def take_attendance(classroom, who_is_here): *1*
""" *2*
classroom, tuple *2*
who_is_here, tuple *2*
Checks if every item in classroom is in who_is_here *2*
And prints their name if so. *2*
Returns "finished taking attendance" *2*
""" *2*
for kid in classroom: *3*
if kid in who_is_here: *4*
print(kid) *5*
return "finished taking attendance" *6*
-
1 函数定义
-
2 函数规范(文档字符串)
-
3 遍历班级中的每个学生
-
4 检查学生是否也在 who_is_here 元组中
-
5 打印在场的孩子的名字
-
6 返回一个字符串
列表 21.1 展示了一个 Python 函数。当你告诉 Python 你想要定义一个函数时,你使用 def 关键字。在 def 关键字之后,你命名你的函数。在这个例子中,名称是 take_attendance。函数名称遵循与变量相同的规则。在函数名称之后,你将所有输入放入括号中,并用逗号分隔。你用冒号字符结束函数定义行。
Python 如何知道哪些代码行是函数的一部分,哪些不是?所有你想成为函数一部分的行都进行了缩进——这与循环和条件语句中使用的相同概念。
快速检查 21.1
编写一行代码以定义具有以下规范的函数:
1
一个名为
set_color的函数,接收两个输入:一个名为name的字符串(表示对象的名称)和一个名为color的字符串(表示颜色的名称)2
一个名为
get_inverse的函数,接收一个输入:一个名为num的数字3
一个名为
print_my_name的函数,不接收任何输入
21.1.1. 函数基础:函数接收什么
你使用函数来使生活更轻松。你编写一个函数,以便可以重复使用其核心代码,只需更改几个变量值即可。这允许你避免复制和粘贴实现,只需更改几个变量值。
函数的所有输入都是称为 参数 或 参数 的变量。更具体地说,它们被称为 形式参数 或 形式参数,因为在函数定义内部,这些变量没有任何值。只有当你用一些值调用函数时,才会给它们赋值,你将在后面的部分中看到如何做到这一点。
快速检查 21.2
对于以下函数定义,每个函数接收多少个参数?
1
def func_1(one, two, three):2
def func_2():3
def func_3(head, shoulders, knees, toes):
21.1.2. 函数基础:函数做什么
当你编写函数时,你假设函数的所有参数都有值,在函数内部编写代码。函数的实现只是 Python 代码,但它从缩进开始。程序员可以以任何他们想要的方式实现函数。
快速检查 21.3
以下每个函数的主体是否都编写得没有错误?
1
def func_1(one, two, three): if one == two + three: print("equal")2
def func_2(): return(True and True)3
def func_3(head, shoulders, knees): return "and toes"
21.1.3. 函数基础:函数返回的内容
函数应该做些事情。你使用它们在略有不同的输入上重复相同的操作。因此,函数名称通常是描述性动作词和短语:get_something、set_something、do_something以及类似这些。
快速检查 21.4
为执行以下操作的函数想一个合适的名字:
1
一个能告诉你树龄的函数
2
一个能翻译你的狗在说什么的函数
3
一个能拍下云朵并告诉你与之最相似的动物的函数
4
一个能让你看到 50 年后自己模样的函数
一个创建其自身环境的函数,因此在这个环境中创建的所有变量在函数外部都不可访问。函数的目的是执行一个任务并传递其结果。在 Python 中,使用 return 关键字传递结果。包含 return 关键字的代码行指示 Python 它已经完成了函数内部的代码,并准备好将值传递给程序中的另一段代码。
在 列表 21.2 中,程序将两个字符串输入连接在一起,并返回连接结果的长度。该函数接受两个字符串作为参数。它将它们相加并将连接存储在名为 word 的变量中。函数返回值 len(word),它是一个整数,对应于变量 word 所持有的值的长度。你可以在 return 语句之后在函数内部编写代码,但它不会被执行。
列表 21.2. 一个告诉你两个字符串相加长度函数
def get_word_length(word1, word2): *1*
word = word1+word2 *2*
return len(word) *3*
print("this never gets printed") *4*
-
1 函数定义;接受两个参数
-
2 连接两个参数
-
3 返回语句;返回连接的长度
-
4 返回语句之后的内容不会执行
快速检查 21.5
每个函数返回什么?返回变量的类型是什么?
1
def func_1(sign): return len(sign)2
def func_2(): return (True and True)3
def func_3(head, shoulders, knees): return("and toes")
21.2. 使用函数
在 第 21.1 节 中,你学习了如何定义一个函数。在代码中定义一个函数只告诉 Python 现在有一个名为此的函数将要执行某些操作。函数不会运行以产生结果,直到它在代码的另一个地方被调用。
假设你已经在你的代码中定义了 word_length 函数,如 列表 21.2 所示。现在你想要使用这个函数来告诉你全名中有多少个字母。列表 21.3 展示了如何调用该函数。你输入函数的名称,并给它提供 实际参数——在程序中有值的变量。这与你之前看到的用于定义函数的形式参数形成对比。
列表 21.3. 如何调用函数
def word_length(word1, word2): *1*
word = word1+word2 *1*
return len(word) *1*
print("this never gets printed")
length1 = word_length("Rob", "Banks") *2*
length2 = word_length("Barbie", "Kenn") *2*
length3 = word_length("Holly", "Jolley") *2*
print("One name is", length1, "letters long.") *3*
print("Another name is", length2, "letters long.") *3*
print("The final name is", length3, "letters long.") *3*
-
1 word_length 函数的定义代码
-
2 每行使用不同的输入调用函数,并将函数的返回值分配给变量。
-
3 每行打印变量。
图 21.1 展示了函数调用 word_length("Rob", "Banks") 发生的情况。每当进行函数调用时,都会创建一个新的 作用域(或环境),并与该特定的函数调用相关联。你可以将作用域视为一个包含其自身变量的独立小程序,这些变量对程序的其他任何部分都是不可访问的。
图 21.1. 当你在
中进行函数调用时会发生什么?使用
和
,第一个参数映射到函数的作用域中。使用
和
,第二个参数映射。
是函数内部创建的另一个变量。
是返回值。函数返回后,函数作用域及其所有变量都会消失。

作用域创建后,每个实际参数都会映射到函数的形式参数,保持顺序。此时,形式参数有值。随着函数的执行和其语句的执行,任何创建的变量仅存在于该函数调用的作用域中。
21.2.1. 返回多个值
你可能已经注意到,函数只能返回一个对象。但你可以通过使用元组来“欺骗”函数,使其返回多个值。元组中的每个项目都是不同的值。这样,函数只返回一个对象(一个元组),但元组有尽可能多的不同值(通过其元素),满足你的需求。例如,你可以有一个函数,它接受一个国家的名称,并返回一个元组,其第一个元素是该国家中心的纬度,第二个元素是该国家中心的经度。
然后,当你调用函数时,你可以将返回元组中的每个项目分配给不同的变量,如下所示。函数 add_sub 对两个参数进行加法和减法运算,并返回一个包含这两个值的元组。当你调用函数时,将返回结果分配给另一个元组 (a,b),这样 a 就得到加法的结果,而 b 得到减法的结果。
列表 21.4. 返回一个元组
def add_sub(n1, n2):
add = n1 + n2
sub = n1 - n2
return (add, sub) *1*
(a, b) = add_sub(3,4) *2*
-
1 返回包含加法和减法值的元组
-
2 将结果赋值给一个元组
快速检查 21.6
1
完成以下函数,该函数告诉你数字和花色是否与秘密值匹配以及赢得的金额:
def guessed_card(number, suit, bet): money_won = 0 guessed = False if number == 8 and suit == "hearts": money_won = 10*bet guessed = True else: money_won = bet/10 # write one line to return two things: # how much money you won and whether you # guessed right or not2
使用你在(1)中编写的函数。如果按照以下顺序执行,以下行将打印什么?
print(guessed_card(8, "hearts", 10))print(guessed_card("8", "hearts", 10))guessed_card(10, "spades", 5)(amount, did_win) = guessed_card("eight", "hearts", 80) print(did_win) print(amount)
21.2.2. 没有返回语句的函数
你可能想要编写一些打印消息但不显式返回任何值的函数。Python 允许你在函数内部省略 return 语句。如果你没有写 return 语句,Python 会自动在函数中返回值 None。None 是一个类型为 NoneType 的特殊对象,表示没有值。
查看列表 21.5 中的示例代码。你正在和孩子们玩游戏。他们在藏起来,你看不见他们。你按顺序叫他们的名字:
-
如果他们从藏身之处出来到你面前,这就像有一个函数返回一个对象给调用者。
-
如果他们大声喊“here”但不出现在你面前,这就像有一个函数不返回孩子对象但向用户打印一些东西。你需要从他们那里得到一个对象,所以他们都同意,如果他们不出现在你面前,他们会向你扔一张写着 None 的纸条。
列表 21.5 定义了两个函数。一个打印给定的参数(并隐式返回 None)。另一个返回给定参数的值。这里正在发生四件事情:
-
执行的主程序的第一行是
say_name("Dora")。这一行打印Dora,因为say_name函数内部有一个print语句。函数调用的结果没有打印出来。 -
下一行,
show_kid("Ellie")不打印任何内容,因为show_kid函数内部没有打印任何内容,函数调用的结果也没有打印出来。 -
下一行,
print(say_name("Frank"))打印两样东西:Frank和None。它打印Frank,因为say_name函数内部有一个print语句。打印None,因为say_name函数没有return语句(因此默认返回None),并且say_name("Frank")的返回结果通过print打印出来。 -
最后,
print(show_kid("Gus"))打印Gus,因为show_kid返回传递给它的名字,并且show_kid("Gus")周围的print打印返回的值。
列表 21.5. 带有和没有返回语句的函数
def say_name(kid): *1*
print(kid) *2*
def show_kid(kid): *3*
return kid *4*
say_name("Dora") *5*
show_kid("Ellie") *6*
print(say_name("Frank")) *7*
print(show_kid("Gus")) *8*
-
1 接收一个包含孩子名字的字符串
-
2 没有显式返回任何内容,所以 Python 返回 None
-
3 接收一个包含孩子名字的字符串
-
4 返回一个字符串
-
5 将“Dora”打印到控制台
-
6 不打印任何内容到控制台
-
7 打印 Frank,然后 None,到控制台
-
8 将“Gus”打印到控制台
一条特别有趣的行是 print(say_name("Frank"))。函数调用本身打印了孩子的名字,Frank。因为没有return语句,Python 自动返回None。然后,print(say_name("Frank"))这一行被替换为返回值,给出print(None),然后打印值None到控制台。重要的是要理解None不是string类型的对象。它是NoneType类型对象的唯一值。图 21.2 显示了哪个返回值替换了每个函数调用。
图 21.2. 四种组合,表示调用不返回值的函数、调用返回值的函数、打印调用不返回值的函数的结果以及打印调用返回值的函数的结果。黑色框是函数调用,灰色框是函数被调用时幕后发生的事情。如果函数没有显式的return语句,则自动添加return None。黑色虚线表示将替换函数调用的值。

快速检查 21.7
给定以下函数和变量初始化,如果按以下顺序执行,每行将打印什么?
def make_sentence(who, what):
doing = who+" is "+what
return doing
def show_story(person, action, number, thing):
what = make_sentence(person, action)
num_times = str(number) + " " + thing
my_story = what + " " + num_times
print(my_story)
who = "Hector"
what = "eating"
thing = "bananas"
number = 8
1
sentence = make_sentence(who, thing)2
print(make_sentence(who, what))3
your_story = show_story(who, what, number, thing)4
my_story = show_story(sentence, what, number, thing)5
print(your_story)
21.3. 记录你的函数
除了函数是模块化代码的一种方式外,它们也是抽象代码块的一种方式。你看到了通过传递参数来实现抽象,这样函数就可以更通用地使用。抽象也通过函数规范或文档字符串来实现。你可以快速阅读文档字符串,以了解函数接受的输入、它应该做什么以及它返回什么。扫描文档字符串的文本比阅读函数实现要快得多。
这里是一个函数文档字符串的示例,该函数的实现你可以在列表 21.1 中看到:
def take_attendance(classroom, who_is_here):
"""
classroom, tuple of strings
who_is_here, tuple of strings
Prints the names of all kids in class who are also in who_is_here
Returns a string, "finished taking attendance"
"""
函数文档字符串从函数内部开始,缩进。三引号 """ 表示文档字符串的开始和结束。文档字符串包括以下内容:
-
每个输入参数的名称和类型
-
函数的功能简要概述
-
返回值的含义和类型
摘要
在本节课中,我的目标是让你编写简单的 Python 函数。函数接收输入,执行操作,并返回一个值。这是你在程序中编写可重用代码的一种方式。函数是以通用方式编写的代码模块。在你的程序中,你可以通过传递特定的值来调用函数,以获取返回的值。返回的值可以随后在你的代码中使用。你编写函数规范以记录你的工作,这样你就不必阅读整个代码块来了解函数的功能。以下是主要收获:
-
函数定义仅仅是定义本身。函数只有在代码的其他地方被调用时才会被执行。
-
函数调用被替换为返回的值。
-
函数返回一个对象,但你可以使用元组来返回多个值。
-
函数文档字符串用于记录函数实现的文档和抽象细节。
让我们看看你是否理解了...
Q21.1
- 编写一个名为
calculate_total的函数,该函数接收两个参数:一个名为price的浮点数和一个名为percent的整数。该函数计算并返回一个新数值,表示价格加上小费:total = price + percent * price。- 使用价格为 20 和百分比为 15 调用你的函数。
- 在程序中完成以下代码以使用你的函数:
my_price = 78.55 my_tip = 20 # write a line to calculate and save the new total # write a line to print a message with the new total
第 22 课。函数的高级操作
在阅读第 22 课(lesson 22)之后,你将能够
-
将函数(作为一个对象)作为参数传递给另一个函数
-
从另一个函数返回一个函数(作为一个对象)
-
根据某些规则理解哪些变量属于哪个作用域
在正式学习第 21 课(lesson 21)中的函数之前,你已经看到了并使用了简单的代码中的函数。以下是你已经使用过的一些函数:
-
len()—例如,len("coffee") -
range()—例如,range(4) -
print()—例如,print("机智的信息") -
abs(),sum(),max(),min(),round(),pow()—例如,max(3,7,1) -
str(),int(),float(),bool()—例如,int(4.5)
考虑这一点
对于以下每个函数调用,该函数接收多少个参数,返回值的类型是什么?
-
len("How are you doing today?") -
max(len("please"), len("pass"), len("the"), len("salt")) -
str(525600) -
sum((24, 7, 365))
答案:
-
接收一个参数,返回
24 -
接收四个参数,返回
6 -
接收一个参数,返回
"525600" -
接收一个参数,返回
396
22.1.1. 考虑具有两个帽子的函数
回想一下,函数定义定义了一系列可以在程序中用不同输入稍后调用的命令。将这个想法比作造车和开车:首先必须有人造车,但造好之后,车就停在车库中,直到有人想使用它。而想使用车的人不需要知道如何造车,这个人可以多次使用它。从两个角度思考函数可能会有所帮助:一个是编写函数的人,另一个是想要使用函数的人。第 22.1.1 节和 22.1.2 节简要回顾了你在前一课应该掌握的主要思想。
22.1.1. 作者帽子
你以通用方式编写函数,使其能够与各种值一起工作。通过假设给定的输入是命名变量来泛化函数。输入被称为形式参数。你在函数内部执行操作,假设你为这些参数有值。
在函数内部定义的参数和变量仅存在于函数的作用域(或环境)中。函数作用域从函数被调用到函数返回值的时间存在。
你通过函数规范或文档字符串来抽象模块。文档字符串是一个以三引号开始的跨多行的注释,并以三引号结束,"""。
在文档字符串中,你通常写(1)函数应该接收哪些输入及其类型,(2)函数应该做什么,以及(3)函数返回什么。假设输入符合规范,函数假定会按规范正确行为并保证返回一个值。
22.1.2. 用户帽子
使用函数很简单。在主程序代码中的另一个语句中调用函数。当你调用函数时,你用值来调用它。这些值是实际参数,并替换函数的形式参数。函数通过使用实际参数值执行其应有的操作。
函数的输出就是函数返回的内容。函数的返回值会被返回给调用函数的语句。函数调用的表达式会被替换为返回值。
22.2. 函数作用域
“什么发生在拉斯维加斯就留在拉斯维加斯”这句话准确地描述了函数调用背后的场景;函数代码块中发生的事情就留在函数代码块中。函数参数只存在于函数的作用域内。你可以在不同的函数作用域中使用相同的名字,因为它们指向不同的对象。如果你尝试在定义它的函数外部访问变量,你会得到一个错误。Python 一次只能在一个作用域内,并且只知道当前作用域内的变量。
22.2.1. 简单的作用域示例
函数可以创建一个与另一个函数或主程序中另一个变量同名的新变量。Python 知道这些是不同的对象;它们只是碰巧有相同的名字。
假设你正在阅读两本书,每本书都有一个名为彼得的角色。在每本书中,彼得是不同的人,即使你使用了相同的名字。看看列表 22.1。这段代码打印了两个数字。第一个数字是 5,第二个是 30。在这段代码中,你看到了两个名为peter的变量被定义。但这两个变量存在于不同的作用域中:一个在fairy_tale函数的作用域内,另一个在主程序作用域内。
列表 22.1. 在不同作用域中定义同名变量
def fairy_tale(): *1*
peter = 5 *2*
print(peter) *3*
peter = 30 *4*
fairy_tale() *5*
print(peter) *6*
-
1 函数定义
-
2 函数中名为 peter 的变量,其值为 5
-
3 打印 5
-
4 在主程序中首先执行的行,创建了一个名为 peter 的变量,其值为 30
-
5 创建新作用域的函数调用,打印 5,然后调用返回 None,作用域结束
-
6 打印 30
22.2.2. 作用域规则
这里是决定使用哪个变量的规则(如果你在程序中有多个同名变量):
-
在当前作用域中查找具有该名称的变量。如果存在,则使用该变量。如果不存在,则在调用函数的任何作用域中查找。可能另一个函数调用了它。
-
如果调用者的作用域中存在同名变量,则使用该变量。
-
依次在外部作用域中查找,直到到达主程序作用域,也称为全局作用域。你无法在全局作用域之外查找。在全局作用域中存在的所有变量都称为全局变量。
-
如果该名称的变量不在全局作用域中,将显示一个错误信息,指出变量不存在。
下面的四个列表代码展示了不同作用域中具有相同名称的变量的几个场景。有几个有趣的事情需要注意:
-
你可以在函数内部访问一个没有在函数内部定义的变量。只要主程序作用域中存在该名称的变量,就不会出现错误。
-
你不能在函数内部对一个没有首先在函数内部定义的变量进行赋值。
在下面的列表中,函数 e() 展示了你可以创建并访问一个与全局作用域中的变量同名的新变量。
列表 22.2. 初始化变量的函数
def e():
v = 5
print(v) *1*
v = 1
e() *2*
-
1 使用函数中的 v 变量
-
2 函数调用正确;在函数内部使用 v 变量
在下一个列表中,函数 f() 展示了访问一个即使不是在函数内部创建的变量也是可以的,因为全局作用域中存在同名变量。
列表 22.3. 访问其作用域外变量的函数
def f():
print(v) *1*
v = 1
f() *2*
-
1 访问作用域外的变量
-
2 函数调用正确;使用程序中的 v 变量
在下一个列表中,函数 g() 展示了使用未在函数中定义的变量进行操作是可以的,因为你只是访问它们的值,而不是试图改变它们。
列表 22.4. 访问其作用域外多个变量的函数
def g():
print(v+x) *1*
v = 1
x = 2
g() *2*
-
1 仅访问变量
-
2 函数调用正确;使用全局作用域中的 v 和 x 变量
在下面的列表中,函数 h() 展示了尝试在函数内部对一个未先定义的变量进行加值操作会导致错误。
列表 22.5. 尝试修改其作用域外定义的变量的函数
def h():
v += 5 *1*
v = 1
h() *2*
-
1 在定义函数内部的变量之前对变量 v 进行操作
-
2 函数调用产生错误
函数很棒,因为它们将问题分解成更小的部分,而不是一次查看数百或数千行代码。但函数也引入了作用域;有了这个,你可以在不同的作用域中拥有相同名称的变量,而它们不会相互干扰。你需要注意你当前正在查看的作用域。
像程序员一样思考
你应该开始养成跟踪程序的习惯。要跟踪程序,你应该逐行阅读,绘制当前的作用域,并写下当前作用域中任何变量及其值。
以下列表显示了一个简单的函数定义和一些函数调用。在代码中,您有一个函数,如果数字是奇数则返回 "odd",如果是偶数则返回 "even"。函数内部的代码不打印任何内容,它只返回结果。代码从 num = 4 行开始运行,因为上面的所有内容都是函数定义。这个变量在全局作用域中。函数调用 odd_or_even(num) 创建了一个作用域,并将值 4 映射到函数定义中的形式参数。您进行所有计算并返回 "even",因为 4 除以 2 的余数是 0。print(odd_or_even(num)) 打印返回的值,"even"。打印后,您计算 odd_or_even(5)。这个函数调用的返回值没有被使用(没有打印),并且没有对其执行任何操作。
列表 22.6. 显示不同作用域规则的函数
def odd_or_even(num): *1*
num = num%2 *2*
if num == 1:
return "odd"
else:
return "even"
num = 4 *3*
print(odd_or_even(num)) *4*
odd_or_even(5) *5*
-
1 函数定义接受一个参数,num
-
2 num 除以 2 的余数
-
3 全局作用域中的变量
-
4 一个打印其返回值的函数调用
-
5 一个没有对其返回值进行任何操作的函数调用
图 22.1 展示了您可能如何绘制一个程序的跟踪图。其中一件棘手的事情是您有两个名为 num 的变量。但由于它们在不同的作用域中,它们不会相互干扰。
快速检查 22.1
对于以下代码,每一行将打印什么?
def f(a, b):
x = a+b
y = a-b
print(x*y)
return x/y
a = 1
b = 2
x = 5
y = 6
1
print(f(x, y))2
print(f(a, b))3
print(f(x, a))4
print(f(y, b))
图 22.1.
一个指示数字是奇数还是偶数的程序跟踪图。在每一行,您绘制您所在的作用域以及该作用域中存在的所有变量。在
中,您开始程序并处于带有箭头的行。在该行执行之后,程序的作用域包含一个函数定义和一个名为 num 的变量。在
中,您刚刚调用了 print(odd_or_even(num)) 函数。您创建了一个新的作用域。请注意,全局作用域仍然存在,但现在不是焦点。在
的左侧面板中,您有一个参数 num 作为值为 4 的变量。在
的中间面板中,您正在执行函数调用内的 num=num%2 行,并在函数调用作用域内重新分配,只在该函数调用作用域内将变量 num 赋值为 0。在
的右侧面板中,您做出决定并返回 "even"。在
中,函数返回了 "even",函数调用的作用域消失了。您回到了全局作用域并执行了两个打印操作。打印函数调用显示了 "even",打印 num 显示了 4,因为您正在使用全局作用域中的 num 变量。

22.3. 函数嵌套
就像你可以有嵌套循环一样,你也可以有嵌套函数。这些是其他函数内部的函数定义。Python 只知道外函数作用域内的内函数——并且只有当外函数被调用时。
列表 22.7 展示了函数 sing() 内部的嵌套函数 stop()。全局作用域是主程序作用域。它有一个 sing() 的函数定义。在这个点上,函数定义只是一些代码。函数不会执行,直到你进行函数调用。当你试图在主程序作用域中调用 stop() 时,你会得到一个错误。
在 sing() 的定义内部,你定义了另一个名为 stop() 的函数,它也包含代码。你不需要关心那是什么代码,直到你进行函数调用。在 sing() 内部,stop() 的调用不会导致错误,因为 stop() 在 sing() 内部定义。对于主程序来说,只有函数 sing 是其作用域。
列表 22.7. 函数嵌套
def sing():
def stop(line): *1*
print("STOP",line)
stop("it's hammer time") *2*
stop("in the name of love") *2*
stop("hey, what's that sound") *2*
stop() *3*
sing()
-
1 sing() 内部的函数定义
-
2 sing() 内部对 stop 函数的调用
-
3 错误,因为全局范围内不存在 stop()
快速检查 22.2
对于以下代码,每一行将打印什么?
def add_one(a, b):
x = a+1
y = b+1
def mult(a,b):
return a*b
return mult(x,y)
a = 1
b = 2
x = 5
y = 6
1
print(add_one(x, y))2
print(add_one(a, b))3
print(add_one(x, a))4
print(add_one(y, b))
22.4. 将函数作为参数传递
你已经见过 int、string、float 和 Boolean 类型的对象。在 Python 中,一切都是对象,所以你定义的任何函数都是函数类型的对象。任何对象都可以作为函数的参数传递,甚至其他函数!
你想编写代码来制作两种三明治之一。BLT 三明治告诉你里面包含培根、生菜和番茄。早餐三明治告诉你里面包含鸡蛋和奶酪。在 列表 22.8 中,blt 和 breakfast 都是返回字符串的函数。
函数 sandwich 接收一个名为 kind_of_sandwich 的参数。这个参数是一个函数对象。在 sandwich 函数内部,你可以像平常一样通过在其后添加括号来调用 kind_of_sandwich。
当你调用 sandwich 函数时,你用函数对象作为参数调用它。你给出你想要制作的三明治的函数名。你不在 blt 或 breakfast 后面放括号作为参数,因为你想传递函数对象本身。如果你使用 blt() 或 breakfast(),这将是一个字符串对象,因为这是一个返回字符串的函数调用
列表 22.8. 将函数对象作为参数传递给另一个函数
def sandwich(kind_of_sandwich): *1*
print("--------")
print(kind_of_sandwich ()) *2*
print("--------")
def blt():
my_blt = " bacon\nlettuce\n tomato"
return my_blt
def breakfast():
my_ec = " eggegg\n cheese"
return my_ec
print(sandwich(blt)) *3*
-
1 kind_of_sandwich 是一个参数。
-
2 kind_of_sandwich 带括号表示函数调用。
-
3 仅使用函数名(对象)
快速检查 22.3
Q1:
绘制 列表 22.8 中的程序跟踪。在每一行,决定作用域、打印的内容、变量及其值,以及如果有的话,函数返回的内容。
22.5. 返回一个函数
因为函数是一个对象,你也可以有返回其他函数的函数。当你想要有专门的函数时,这很有用。通常,当你有嵌套函数时,你会返回函数。为了返回一个函数对象,你只需返回函数名。回想一下,在函数名后加括号会进行函数调用,这是你不想做的。
返回一个函数在你想要在其他函数内部有专门的函数时很有用。在 列表 22.9 中,有一个名为 grumpy 的函数,它打印一条消息。在 grumpy 函数内部,有一个名为 no_n_times 的另一个函数。它打印一条消息,然后在该函数内部定义另一个函数,名为 no_m_more_times。最内层的函数 no_m_more_times 打印一条消息,然后打印 no n + m 次。
你正在使用 no_m_more_times 嵌套在 no_n_times 中的事实,因此它知道变量 n,而无需将那个变量作为参数发送。
函数 no_n_times 返回函数 no_m_more_times 本身。函数 grumpy 返回函数 no_n_times。
当你使用 grumpy()(4)(2) 进行函数调用时,你从左到右工作,并在过程中用它们返回的内容替换函数调用。注意以下内容:
-
你不需要打印
grumpy的返回值,因为你在函数内部打印内容。 -
函数调用
grumpy()被替换为grumpy返回的内容,即函数no_n_times。 -
现在
no_n_times(4)被替换为它返回的内容,即函数no_m_more_times。 -
最后,
no_m_more_times(2)是最后一个函数调用,它将打印出所有的数字。
列表 22.9. 从另一个函数返回函数对象
def grumpy(): *1*
print("I am a grumpy cat:")
def no_n_times(n): *2*
print("No", n,"times...")
def no_m_more_times(m): *3*
print("...and no", m,"more times")
for i in range(n+m): *4*
print("no")
return no_m_more_times *5*
return no_n_times *6*
grumpy()(4)(2) *7*
-
1 函数定义
-
2 嵌套函数定义
-
3 嵌套函数定义
-
4 循环打印单词“no” n + m 次
-
5 函数 no_n_times 返回函数 no_m_more_times
-
6 函数 grumpy 返回函数 no_n_times
-
7 主程序中的函数调用
这个例子表明函数调用是左结合的,所以你从左到右用它们返回的函数替换调用。例如,如果你有四个嵌套函数,每个都返回一个函数,你会使用 f()()()()。
快速检查 22.4
Q1:
绘制 列表 22.9 中的程序跟踪。在每一行,决定作用域、打印的内容、变量及其值,以及如果有的话,函数返回的内容。
22.6. 总结
在本课中,我的目标是教你关于函数的微妙之处。这些想法只是刚刚触及了你可以用函数做什么的表面。你创建了具有相同名称的变量的函数,并看到由于函数作用域,它们之间没有干扰。你了解到函数是 Python 对象,并且可以将它们作为参数传递给其他函数或由其他函数返回。以下是主要收获:
-
你已经使用过内置函数了,现在你理解了为什么你以那种方式编写它们。它们接受参数,并在执行计算后返回一个值。
-
你可以通过在其他函数内部定义函数来嵌套函数。嵌套函数仅存在于封装函数的作用域内。
-
你可以像传递其他对象一样传递函数对象。你可以将它们用作参数,也可以返回它们。
让我们看看你是否理解了...
Q22.1
填写以下代码的缺失部分:
def area(shape, n): # write a line to return the area # of a generic shape with a parameter of n def circle(radius): return 3.14*radius**2 def square(length): return length*length print(area(circle,5)) # example function call
- 写一行代码使用
area()来计算半径为 10 的圆的面积。- 写一行代码使用
area()来计算边长为 5 的正方形的面积。- 写一行代码使用
area()来计算直径为 4 的圆的面积。Q22.2
填写以下代码的缺失部分:
def person(age): print("I am a person") def student(major): print("I like learning") def vacation(place): print("But I need to take breaks") print(age,"|",major,"|",place) # write a line to return the appropriate function # write a line to return the appropriate function例如,函数调用
person(12)("Math")("beach") # example function call应该打印出以下内容:
I am a person I like learning But I need to take breaks 12 | Math | beach
- 编写一个函数调用,年龄为
29,专业为"CS",假期地点为"Japan"。- 编写一个函数调用,使其打印输出的最后一行如下:
23 | "Law" | "Florida"
第 23 课:综合项目:分析你的朋友
在阅读完第 23 课后,你将能够
-
编写一个函数来逐行读取文件
-
将文件中的数字和字符串保存到变量中
-
编写一个函数来分析存储的信息
你到目前为止看到的两种输入数据的方式是:(1) 在你的程序中预定义变量,或者(2) 要求用户逐个输入数据。但是,当用户有很多信息要输入到你的程序中时,你不能期望他们实时输入。让他们以文件的形式提供信息通常很有用。
计算机擅长快速进行大量计算。计算机的一个自然用途是编写程序,可以从文件中读取大量数据,并对这些数据进行简单的分析。例如,你可以将 Microsoft Excel 电子表格中的数据导出为文件,或者你可以下载数据(如天气或选举数据)。在你得到一个以某种方式结构的文件后,你可以使用对该结构的了解来编写一个程序,以顺序读取和存储文件中的信息。在你的程序中存储的数据,你可以分析它(例如,找到平均值、最大值/最小值和重复项)。
除了复习本单元的概念外,本课还将向你展示如何从文件中读取数据。
问题
编写一个程序,从文件中读取特定格式的输入,包括你所有朋友的姓名和电话号码。你的程序应该存储这些信息并以某种方式分析它们。例如,你可以根据电话号码的区号显示用户朋友居住的地区,以及他们居住的州的数量。
23.1. 读取文件
你将编写一个名为 read_file 的函数,用于遍历每一行,并将每行的信息放入变量中。
23.1.1. 文件格式
此函数假设用户以以下格式提供信息,每行包含不同的信息:
Friend 1 name
Friend 1 phone number
Friend 2 name
Friend 2 phone number
<and so on>
每条信息都在单独的一行上,这意味着你的程序将在每行的末尾有一个换行符。Python 有一种处理这种格式的方法,你很快就会看到。知道了这个格式,你可以逐行读取文件。你将每行存储在元组中,从第一行开始。然后,你将每行的另一部分,从第二行开始,存储在另一个元组中。元组看起来像这样:
(Friend 1 name, Friend 2 name, <and so on>)
(Friend 1 phone, Friend 2 phone, <and so on>)
注意,在索引 0 处,两个元组都存储有关朋友 1 的信息;在索引 1 处,两个元组都存储有关朋友 2 的信息,依此类推。
你必须逐行阅读。这应该会触发使用循环逐行遍历的想法。循环将文件中的每一行作为字符串读取。
23.1.2. 换行符
每一行的末尾都有一个特殊的隐藏字符,即换行符。该字符的表示为 \n。为了看到这个字符的效果,请在你的控制台中输入以下内容:
print("no newline")
控制台打印出短语no newline然后给你提示再次输入。现在输入以下内容:
print("yes newline\n")
现在你看到打印内容和下一个提示之间有一个额外的空行。这是因为反斜杠和字母n的特殊字符组合告诉 Python 你想要一个新行。
23.1.3. 删除换行符
当你从文件中读取一行时,该行包含你看到的所有字符以及换行符。你想要存储除那个特殊字符之外的所有内容,因此需要在存储信息之前将其删除。
因为每行你读取的都是一个字符串,所以你可以使用字符串方法。最简单的事情就是将\n的每个出现替换为空字符串“”。这将有效地删除换行符。
以下列表显示了如何将换行符替换为空字符串,并将结果保存到变量中。
列表 23.1. 删除换行符
word = "bird\n" *1*
print(word) *2*
word = word.replace("\n", "") *3*
print(word) *4*
-
1 创建一个值包含换行符的字符串的变量
-
2 打印带有额外换行的单词
-
3 将换行符替换为空字符串,并将结果赋值回同一变量
-
4 不打印额外行
像程序员一样思考
对一个程序员来说直观的,可能对另一个程序员来说就不直观。通常,编写一段代码的方式不止一种。面对编写一行代码的情况,在编写自己的代码之前,浏览 Python 文档以查看你可以使用哪些函数。例如,列表 23.1 使用字符串上的replace方法将换行符替换为空格字符。Python 文档中还有一个适合使用的函数:strip。strip函数会从字符串的开始和结束处删除所有实例的特定字符。以下两行做的是同样的事情:
word = word.replace("\n", "")
word = word.strip("\n")
23.1.4. 使用元组存储信息
现在每行都清理了换行符,你剩下的是纯数据,作为字符串。下一步是将它存储在变量中。因为你会有一组数据,你应该使用一个元组来存储所有名称,另一个元组来存储所有电话号码。
每次你在一行中读取信息时,将新信息添加到元组中。记住,将一个项目添加到元组中会给你一个包含旧信息的元组,你刚刚添加的项目位于元组的末尾。现在你有了旧元组中的所有信息,加上你刚刚读取的那行中的新信息。图 23.1 显示了文件中的哪些行存储在哪个元组中。在下一节中,你将看到相应的代码。
图 23.1. 输入数据包含数据行。第一行是朋友的姓名,第二行是朋友的电话号码。第三行是你第二个朋友的姓名,第四行是他们的电话号码,以此类推。从第一行开始,每隔一行取出来,存储你朋友的所有姓名到一个元组中。从第二行开始,取所有电话号码,并将它们存储到另一个单独的元组中。

23.1.5. 返回内容
你正在编写一个函数,该函数执行简单的读取文件、组织信息和返回组织信息的任务。现在你有了两个元组(一个包含所有姓名,另一个包含所有电话号码,如图 23.1 所示),返回一个元组元组,如下所示:
((Friend1 Name, Friend2 Name, ...), (Friend1 phone, Friend2 phone, ...))
--------------------------------- -----------------------------------
one tuple other tuple
你必须返回一个元组元组,因为函数只能返回一个东西。回想一下第 21 课,返回一个包含多个元素的元组可以让你绕过这个限制!
以下列表显示了读取数据的函数代码。函数read_file接受一个文件对象;你将在本课的后面看到这意味着什么。它遍历文件中的每一行,并去除行中的换行符。如果你正在查看偶数行,你将添加到你的姓名元组中。如果你正在查看奇数行,你将添加到你的电话号码元组中。在这两种情况下,请注意你正在添加一个单元素元组,因此需要在括号中添加一个额外的逗号。最后,该函数返回一个元组元组,以便你可以传递从文件中解析出的信息。
列表 23.2. 从文件中读取姓名和电话号码
def read_file(file):
""" *1*
file, a file object *1*
Starting from the first line, it reads every 2 lines *1*
and stores them in a tuple. *1*
Starting from the second line, it reads every 2 lines *1*
and stores them in a tuple. *1*
Returns a tuple of the two tuples. *1*
""" *1*
first_every_2 = () *2*
second_every_2 = () *2*
line_count = 0 *3*
for line in file: *4*
stripped_line = line.replace("\n", "") *5*
if line_count%2 == 0: *6*
first_every_2 += (stripped_line,) *7*
elif line_count%2 == 1: *8*
second_every_2 += (stripped_line,) *9*
line_count += 1 *10*
return (first_every_2, second_every_2) *11*
-
1 文档字符串
-
2 空元组用于姓名和电话号码
-
3 行号计数器
-
4 遍历每一行
-
5 移除换行符
-
6 奇数行
-
7 添加到姓名元组
-
8 偶数行
-
9 添加到电话号码元组
-
10 增加行号
-
11 返回元组元组
23.2. 清理用户输入
现在你有了用户给你提供的信息,在两个元组中:一个元组包含人名,另一个元组包含电话号码。
你从未指定电话号码的格式,因此用户可以有一个包含任何格式的电话号码的文件;用户可能有破折号、括号、空格或任何其他奇怪的字符。在你分析这些数字之前,你必须将它们转换成一致的形式。这意味着移除所有特殊字符,并保留所有数字。
这似乎是一个适合函数的工作。函数 sanitize 通过使用你学过的 replace 方法来替换所有特殊字符为空字符串 “”。下面的列表显示了可能的实现。你遍历每个字符串,并替换掉可能出现在电话号码中的不必要的字符。在移除破折号、空格和括号后,将清理后的电话号码(作为一个字符串)放入你返回的新元组中。
列表 23.3. 从电话号码中移除空格、破折号和括号
def sanitize(some_tuple):
"""
phones, a tuple of strings
Removes all spaces, dashes, and open/closed parentheses
in each string
Returns a tuple with cleaned up string elements
"""
clean_string = () *1*
for st in some_tuple:
st = st.replace(" ", "")
st = st.replace("-", "") *2*
st = st.replace("(", "") *2*
st = st.replace(")", "") *2*
clean_string += (st,) *3*
return clean_string *4*
-
1 将不必要的字符替换为空字符串
-
2 空元组
-
3 将清理后的数字添加到新元组
-
4 返回新元组
23.3. 测试和调试到目前为止的工作
更大的任务剩余部分是对这些数据进行分析。在继续之前,进行一些测试(如果需要,进行调试)以确保你编写的两个函数能够很好地协同工作。
到目前为止,你编写了两个执行一些有趣任务的函数。回想一下,函数只有在更大程序中的某个地方被调用时才会运行。现在你将编写代码将这些函数集成在一起。
23.3.1. 文件对象
当你与文件一起工作时,你必须创建文件 对象。与其他你迄今为止看到的对象一样,Python 知道如何与这些文件对象一起工作以执行专门的操作。例如,在你编写的 read_file 函数中,你能够写 for line in file 来遍历特定文件对象中的每一行。
23.3.2. 使用名字和电话号码写入文本文件
在 Spyder 中创建一个新文件。按照 read_file 期望的格式输入几行数据。从名字开始;在下一行,输入一个电话号码,然后是另一个名字,然后是另一个电话号码,依此类推。例如,
Bob
000 123-4567
Mom Bob
(890) 098-7654
Dad Bob
321-098-0000
现在将文件保存为 friends.txt 或你想要的任何其他名称。确保将文件保存在你编写的 Python 程序所在的同一文件夹中。这个文件将被你的程序读取,因此它是一个纯文本文件。
23.3.3. 打开文件进行读取
你通过打开一个文件名来创建文件对象。你使用一个名为 open 的函数,该函数接受一个包含你想要读取的文件名的字符串。文件必须位于你的 .py 程序文件所在的同一文件夹中。
列表 23.4 展示了如何打开一个文件,运行你编写的函数,并检查函数是否返回正确的结果。你使用 open 函数打开一个名为 friends.txt 的文件。这创建了一个文件对象,它是函数 read_file() 的参数。read_file() 返回一个元组的元组。你将返回值存储在两个元组中:一个用于名字,一个用于电话号码。
你可以通过将电话号码元组作为参数调用 sanitize 函数来测试你的函数是否工作。在每一步,你都可以打印变量并查看输出是否符合你的预期。
列表 23.4. 从文件中读取名字和电话号码
friends_file = open('friends.txt') *1*
(names, phones) = read_file(friends_file) *2*
print(names) *3*
print(phones) *3*
clean_phones = sanitize(phones) *4*
print(clean_phones) *5*
friends_file.close() *6*
-
1 打开文件
-
2 调用函数
-
3 打印给用户的输出
-
4 检查你的函数是否工作
-
5 打印给用户的输出
-
6 关闭文件
在编写更多代码之前,逐个测试函数是一个好习惯。偶尔,你应该测试以确保从一个函数的输出传递到另一个函数的输入的数据能够很好地协同工作。
23.4. 重复使用函数
函数的伟大之处在于它们的可重用性。你不需要为特定类型的数据编写它们;例如,如果你编写了一个添加两个数字的程序,你可以调用该函数来添加表示温度、年龄或重量的两个数字。
你已经编写了一个读取数据的函数。你使用该函数读取并存储名字和电话号码到两个元组中。你可以重用该函数来读取以相同格式组织的数据集。
因为用户将给你电话号码,假设你有一个包含区号及其所属州的文件。这个文件中的行将与包含人名和电话号码的文件格式相同:
Area code 1
State 1
Area code 2
State 2
<and so on>
map_areacodes_states.txt 文件的前几行如下:
201
New Jersey
202
Washington D.C.
203
Connecticut
204
<and so on>
使用这个文件,你可以调用相同的函数 read_data 并存储返回的值:
map_file = open('map_areacodes_states.txt')
(areacodes, places) = read_file(map_file)
23.5. 分析信息
现在是时候将所有东西组合在一起了。你已经收集了所有数据并将它们存储到变量中。你现在拥有的数据如下:
-
人的名字
-
每个名字对应的电话号码
-
区号
-
对应区号的州
23.5.1. 规范
编写一个名为 analyze_friends 的函数,它接受你的四个元组:第一个是朋友的姓名,第二个是他们的电话号码,第三个是所有区号,第四个是对应区号的地点。
函数打印信息。它不返回任何内容。假设文件中给出的朋友如下:
Ana
801-456-789
Ben
609 4567890
Cory
(206)-345-2619
Danny
6095648765
然后该函数将打印以下内容:
You have 4 friends!
They live in ('Utah', 'New Jersey', 'Washington')
注意,尽管你有四个朋友,但其中两个住在同一个州,所以你将只打印唯一的州。以下是你将要编写的函数的文档字符串:
def analyze_friends(names, phones, all_areacodes, all_places):
"""
names, a tuple of friend names
phones, a tuple of phone numbers without special symbols
all_areacodes, a tuple of strings for the area codes
all_places, a tuple of strings for the US states
Prints out how many friends you have and every unique
state that is represented by their phone numbers.
"""
23.5.2. 辅助函数
分析信息的任务足够复杂,你应该编写辅助函数。辅助函数是帮助另一个函数完成任务的功能。
唯一的区号
你将要编写的第一个辅助函数是 get_unique_area_codes。它不接受任何参数,并返回一个只包含唯一区号的元组,顺序不限。换句话说,它不会在区号元组中重复区号。
列表 23.5 显示了该函数。这个函数将嵌套在 analyze_friends 函数中。因为它嵌套,所以这个函数知道 analyze_friends 给出的所有参数。这包括 phones 元组,这意味着你不需要再次将此元组作为参数传递给 get_unique_area_codes。
函数遍历 phones 中的每个数字,只查看前三位数字(区号)。它跟踪到目前为止看到的所有区号,并且只有当它不在其中时,才将其添加到唯一的区号元组中。
列表 23.5. 仅保留唯一区号的辅助函数
def get_unique_area_codes():
"""
Returns a tuple of all unique area codes in phones
"""
area_codes = () *1*
for ph in phones: *2*
if ph[0:3] not in area_codes: *3*
area_codes += (ph[0:3],) *4*
return area_codes
-
1 元组用于包含唯一的区号
-
2 遍历每个区号,变量 phones 是 analyze_friends 的参数
-
3 检查区号是否已存在
-
4 将唯一代码元组与单元素元组连接
将区号映射到州
analyze_friends 函数的两个输入是包含区号和州的元组。现在你想使用这些元组将每个唯一的区号映射到其对应的州。你可以编写另一个函数来完成这个任务;将其命名为 get_states。该函数接收一个区号元组并返回一个与每个区号对应的州元组。这个函数也嵌套在 analyze_friends 函数内部,因此它将知道传递给 analyze_friends 的所有参数。
列表 23.6 展示了如何做到这一点。你使用循环遍历每个区号。对于有效的区号,你现在必须确定给定区号在区号元组中的位置。你使用元组的 index 方法来获取这个值。回想一下,区号元组和州元组是一一对应的(这就是我们在从文件中读取它们时创建它们的方式)。你使用从区号元组获得的索引在州元组的相同位置查找州。
一个好的程序员会预见用户输入可能出现的任何问题,并尝试优雅地处理它们。例如,有时用户可能会输入一个无效的区号。你可以通过编写类似“如果你给我一个无效的区号,我将将其与名为 BAD AREACODE 的州关联”的代码来预见这种情况。
列表 23.6. 从唯一区号查找州的辅助函数
def get_states(some_areacodes):
"""
some_areacodes, a tuple of area codes
Returns a tuple of the states associated with those area codes
"""
states = ()
for ac in some_areacodes:
if ac not in all_areacodes: *1*
states += ("BAD AREACODE",)
else:
index = all_areacodes.index(ac) *2*
states += (all_places[index],) *3*
return states
-
1 用户给出了一个无效值;变量 all_areacodes 是 analyze_friends 的参数
-
2 查找区号在元组中的位置
-
3 使用位置查找州
这就是 analyze_friends 函数嵌套的辅助函数的全部内容。现在你可以使用它们,使得 analyze_friends 函数内部的代码简单易读,如下面的列表所示。你只需调用辅助函数并打印它们返回的信息。
列表 23.7. analyze_friends 函数的主体
def analyze_friends(names, phones, all_areacodes, all_places):
"""
names, a tuple of friend names
phones, a tuple of phone numbers without special symbols
all_areacodes, a tuple of strings for the area codes
all_places, a tuple of strings for the US states
Prints out how many friends you have and every unique
state that is represented by their phone numbers.
"""
def get_unique_area_codes():
"""
Returns a tuple of all unique area codes in phones
"""
area_codes = ()
for ph in phones:
if ph[0:3] not in area_codes:
area_codes += (ph[0:3],)
return area_codes
def get_states(some_areacodes):
"""
some_area_codes, a tuple of area codes
Returns a tuple of the states associated with those area codes
"""
states = ()
for ac in some_areacodes:
if ac not in all_areacodes:
states += ("BAD AREACODE",)
else:
index = all_areacodes.index(ac)
states += (all_places[index],)
return states
num_friends = len(names) *1*
unique_area_codes = get_unique_area_codes() *2*
unique_states = get_states(unique_area_codes) *3*
print("You have", num_friends, "friends!") *4*
print("They live in", unique_states) *5*
*6*
-
1 朋友数量
-
2 仅保留唯一的区号
-
3 获取与唯一区号对应的州
-
4 打印朋友数量
-
5 打印唯一的州
-
6 没有返回值
程序的最后一步是读取两个文件,调用分析数据的函数,然后关闭文件。以下列表显示了这一过程。
列表 23.8. 读取文件、分析内容和关闭文件的命令
friends_file = open('friends.txt') *1*
(names, phones) = read_file(friends_file) *2*
areacodes_file = open('map_areacodes_states.txt') *1*
(areacodes, states) = read_file(areacodes_file) *2*
clean_phones = sanitize(phones) *3*
analyze_friends(names, clean_phones, areacodes, states) *4*
friends_file.close() *5*
areacodes_file.close() *5*
-
1 在程序相同的目录中打开文件
-
2 使用相同的函数读取两个不同的数据集
-
3 标准化电话数据
-
4 调用执行大部分工作的函数
-
5 关闭文件
摘要
在本课中,我的目标是教您如何处理分析您朋友数据的问题。您编写了一些专门执行特定任务的函数。一个函数从文件中读取数据。您使用了该函数两次:一次读取姓名和电话号码,另一次读取区号和州。另一个函数通过从电话号码中删除不必要的字符来清理数据。最后一个函数分析了您从文件中收集的数据。该函数包含两个辅助函数:一个用于从一组区号中返回唯一的区号,另一个将唯一的区号转换为相应的州。以下是主要收获:
-
您可以在 Python 中打开文件以处理其内容(读取字符串形式的行)。
-
函数对于组织代码很有用。您可以重复使用您编写的任何函数,并使用不同的输入。
-
您应该经常测试您的函数。编写一个函数并立即测试它。当您有几个函数时,请确保它们能很好地协同工作。
-
如果嵌套函数只与特定任务相关,而不是整个程序,则可以在其他函数内部嵌套函数。
单元 6:处理可变数据类型
在上一个单元中,你学习了如何使用函数来组织你的代码。函数用于编写模块化代码,其部分可以在更大的程序的不同部分中重用。
在本单元中,你将了解 Python 中的两种新数据类型:列表和字典。这些数据类型是可变的,因为你可以直接修改它们,而无需复制它们。在编写复杂程序时,尤其是存储可能发生变化的大量数据集合的程序时,可变数据类型被广泛使用。例如,你可能想要维护你公司产品的库存或所有员工的及其信息。可变对象不需要在每次更改时复制对象的额外开销。
在综合项目中,你将编写一个程序,为两份文档分配相似度得分。你将读取两个文件,并使用一个指标来确定两份作品之间的相似程度,基于两份文档中的单词数量以及它们共有的单词数量。你将使用字典将单词与它们出现的频率配对。然后,你将使用一个公式根据这些频率来计算文档之间的差异。
第 24 课:可变和不可变对象
在阅读完第 24 课后,你将能够
-
理解不可变对象是什么
-
理解可变对象是什么
-
理解对象在计算机内存中的存储方式
考虑以下场景。你买了一所房子,大小正好适合你;足够一个人居住。然后你结婚了,没有空间给你的配偶。你有两个选择:给房子扩建,或者拆掉整个房子并建一个更大的新房子来容纳两个人。扩建房子比拆掉一个完好无损的房子并为了扩建而制作一个精确的副本更有意义。现在,你有了孩子,决定你需要更多的空间。再次,你是扩建房子还是拆掉房子建一个能容纳三个人的新房子?再次,扩建房子比拆掉房子建新房子更有意义。当你往房子里添加更多的人时,保持相同的结构并对其进行修改会更快、成本更低。
在某些情况下,能够将你的数据放入某种容器中以便修改容器内的数据,而不是必须创建一个新的容器并将修改后的数据放入其中,这很有帮助。
考虑这一点
这个练习需要一张纸和你的电脑。想想你访问过的所有国家的名字。如果你需要灵感,假设你访问过加拿大、巴西、俄罗斯和冰岛:
-
在一张纸上,用钢笔按字母顺序写下你访问过的所有国家,每行一个。
-
在你的计算机上的文本编辑器中,输入相同的国家列表。
-
假设你意识到你访问过的是格陵兰而不是冰岛。修改你的纸上的列表,使其按字母顺序包含加拿大、巴西、俄罗斯和格陵兰。你能否在不重写整个列表的情况下修改列表(每行一个国家)?你能否在不重写整个列表的情况下在电脑上修改列表(每行一个国家)?
答案:
使用钢笔,我必须重写列表。否则,划掉并在新国家名称旁边写上新的国家名称会太乱。在文本编辑器中,我可以直接替换名称。
24.1. 不可变对象
你所看到的所有 Python 对象(布尔值、整数、浮点数、字符串和元组)都是不可变的。在你创建对象并为其分配值之后,你不能修改该值。
定义
一个不可变对象是一个其值不能改变的对象。
在计算机内存中,这背后意味着什么?创建并赋予值的对象会在内存中分配一个空间。绑定到该对象的变量名称指向内存中的那个位置。图 24.1 显示了对象的内存位置以及当你使用表达式a = 1然后a = 2将相同的变量绑定到新对象时会发生什么。值为1的对象仍然存在于内存中,但你已经失去了对它的绑定。
图 24.1. 叫做 a 的变量绑定到一个值为 1 的对象,位于一个内存位置。当 a 变量绑定到一个不同的值为 2 的对象时,原始的值为 1 的对象仍然在内存中;你只是不能再通过变量访问它了。

你可以使用 id() 函数查看对象被分配的内存位置的值。在控制台中输入以下内容:
a = 1
id(a)
显示的值代表通过名为 a 的变量访问的值为 1 的对象在内存中的位置。现在,输入以下内容:
a = 2
id(a)
如前所述,显示的值代表值为 2 的对象在内存中的位置,通过名为 a 的变量访问。为什么在两种情况下使用变量名 a 时这些值不同?我们回到这样一个观点:变量名是一个绑定到对象的名称。名称指向一个对象;在第一种情况下,变量指向值为 1 的整数对象,然后指向值为 2 的对象。id() 函数告诉你变量名指向的对象的内存位置,而不是关于变量名本身的信息。
你迄今为止看到的对象类型在创建后不能被修改。假设你有以下几行代码,它们按照显示的顺序执行。你初始化两个变量 a 和 b,分别绑定到值为 1 和 2 的对象。然后你将变量 a 的绑定更改为一个值为 3 的不同对象:
a = 1
b = 2
a = 3
图 24.2 展示了随着每行代码的执行,你程序内存中存在的对象:
-
当你创建一个值为
1的对象时,你将其绑定到名为a的变量。 -
当你创建一个值为
2的对象时,你将其绑定到名为b的变量。 -
在最后一行,你将变量名
a重新绑定到一个全新的对象上,其值为3。
图 24.2. 变量绑定到对象的进程。在左侧,a = 1 显示对象 1 位于某个内存位置。在中间,a = 1 然后是 b = 2。值为 1 和 2 的对象位于不同的内存位置。在右侧,a = 1 然后是 b = 2 和 a = 3。名为 a 的变量绑定到了一个不同的对象,但原始对象仍然存在于内存中。

值为 1 的旧对象可能仍然存在于计算机内存中,但你失去了对其的绑定;你不再有变量名作为引用它的方式。
当一个不可变对象失去了其变量引用后,Python 解释器可能会删除该对象,以回收它占用的计算机内存,并将其用于其他目的。与一些其他编程语言不同,你(作为程序员)不必担心删除旧对象;Python 通过一个称为 垃圾回收 的过程为你处理这个问题。
快速检查 24.1
Q1:
画出一个类似于图 24.2 的图表,以展示变量和它们指向的对象(以及任何剩余的对象)对于以下语句序列:
sq = 2 * 2 ci = 3.14 ci = 22 / 7 ci = 3
24.2. 可变性的需求
在你失去对象绑定后,就没有办法回到那个对象了。如果你想程序记住它的值,你需要将其值存储在一个临时变量中。使用临时变量来存储你现在不需要但将来可能需要的值,并不是一种高效的编程方式。这浪费了内存,并导致代码杂乱无章,充满了大部分永远不会再次使用的变量。
如果不可变对象是创建后其值不能改变的对象,那么可变对象是创建后其值可以改变的对象。可变对象通常是能够存储数据集合的对象。在本单元的后续课程中,你将看到列表(Python 类型list)和字典(Python 类型dict)作为可变对象的例子。
定义
一个 可变对象 是一个其值可以改变的对象。
例如,你可以制作一个你需要从杂货店购买的项目清单;当你决定你需要什么时,你将项目添加到清单中。当你购买东西时,你从清单中移除它们。请注意,你正在使用相同的清单并对其进行修改(划掉或添加到末尾),而不是拥有许多清单,每次你想进行更改时都要复制项目。作为另一个例子,你可以将你的杂货需求保存在一个字典中,该字典将商店中你需要购买的每个项目映射到表示你需要数量的数字。
图 24.3 展示了当你将变量绑定到可变对象时在内存中发生的情况。当你修改对象时,你保持相同的变量绑定,并且位于相同内存位置的对象被直接修改。
图 24.3。在左侧,你有一个位于特定内存位置的购物清单。在右侧,你向购物清单中添加另一项,并且位于相同内存位置的对象被直接修改。

可变对象在编程中更加灵活,因为你可以在不丢失对其绑定的情况下修改对象本身。
首先,可变对象可以表现得和不可变对象一样。如果你将一个示例购物清单重新绑定到变量a并检查其内存位置,你会看到内存位置发生变化,并且与原始列表的绑定丢失:
a = ["milk", "eggs"]
id(a)
a = ["milk", "eggs", "bread"]
id(a)
但是你有选择直接修改原始对象而不丢失对其绑定的选项。在下面的代码中,你添加了一个额外的项目(将其添加到列表的末尾)。变量a绑定的对象的内存位置保持不变。以下代码的行为在图 24.3 中显示:
a = ["milk", "eggs"]
id(a)
a.append("bread")
id(a)
可变对象在编程中有几个用途。
首先,你可以在对象中存储属于集合的数据(例如,人的列表或人的电话号码映射),并且你可以保留该对象以供以后使用。
在对象创建之后,你可以向对象本身添加数据,也可以从对象本身移除数据,而不需要创建一个新的对象。当你拥有这个对象时,你也可以通过修改对象本身中的元素来修改集合中的元素,而不是创建一个只有其中一个值被修改的新对象副本。
最后,你可以通过保持相同的对象并在原地进行重新排列来重新排列集合中的数据——例如,如果你有一份人的名单,并且你想按字母顺序对其进行排序。
对于大量数据集合,每次你对其做出更改时都将其复制到新对象中将会效率低下。
快速检查 24.2
你会使用可变类型还是不可变类型的对象来存储以下信息?
1
州内的城市
2
你的年龄
3
超市中商品组及其成本
4
汽车的颜色
摘要
在本课中,我的目标是教你如何理解对象在计算机内存中的存在。一些对象的值在创建后不能改变(不可变对象)。一些对象的值在创建后可以改变(可变对象)。你可能需要使用一种或另一种类型的对象,这取决于你使用编程尝试完成的任务。以下是主要收获:
-
不可变对象不能改变它们的值(例如,字符串、整数、浮点数、布尔值)。
-
可变对象可以改变它们的值(在本单元中,你将看到列表和字典)。
让我们看看你是否掌握了这个...
Q24.1
在以下图中,每个面板展示了一个新的代码操作。以下哪些变量绑定到不可变对象?哪些绑定到可变对象?
向操作两个变量(
one和age)的代码中添加表达式的进程
第 25 课. 使用列表
在阅读了第 25 课之后,你将能够
-
构建 Python 列表
-
在 Python 列表中添加项目、删除项目和修改项目
-
对列表元素执行操作
可变数据类型是你可以执行操作的对象类型,这样对象的值就会被修改;修改是在原地进行的,也就是说,不会制作对象的副本。在处理大量数据时,需要可变的数据类型,直接修改存储的数据比每次操作都将其复制到新对象中更有效。
有时,重新使用一个对象并修改其值是有用的,而不是创建新对象。这在你有一个表示其他对象有序集合的对象时尤其如此。大多数编程语言都有一种数据类型来表示这种有序集合,你可以对其进行修改。在 Python 中,这被称为列表。列表是一种你之前没有见过的新的对象类型。它表示其他对象类型的有序集合。在许多其他类型中,你可以有数字列表、字符串列表,甚至混合对象类型的列表。
通常你会发现你需要按特定顺序存储对象。例如,如果你保存一个购物清单,第一个项目将在第一行,第二个项目在下一行,依此类推。列表是有序的,这源于每个项目在列表中都有一个特定的位置,你的第一个项目将始终位于列表的第一个位置,最后一个项目将位于列表的最后一个位置。本课将向你展示如何创建列表并执行操作以修改列表;这些操作可能会移动列表中的项目,但列表将始终在其第一个位置和最后一个位置有一个项目。在第 27 课中介绍的字典,是一种以无序方式存储对象的数据类型。
考虑以下情况
你维护一份你公司所有员工的名单。这份名单存放在电脑上的一个文档中。以下哪个事件会要求你开始一个新的文档,复制所有信息,然后对该事件进行处理?
-
有人在公司入职。
-
有人在公司离职。
-
有人在公司更改了他们的名字。
-
列表将按姓氏排序,而不是按名字排序。
答案:
无。
25.1. 列表与元组
列表是任何对象类型的集合;它就像你之前已经见过并使用过的元组。元组和列表都有顺序,即集合中的第一个元素位于索引 0,第二个元素位于索引 1,依此类推。主要区别在于列表是可变对象,而元组不是。你可以在同一个列表对象中添加元素、从列表中删除元素以及修改列表中的元素。对于元组,每次你进行操作时,都会创建一个新的元组对象,其值是已更改的元组。
元组通常用于存储更多或更少固定不变的数据。您可以在元组中存储的数据示例包括二维平面中的一对坐标点,或书中单词出现的页码和行号。列表用于存储更动态的数据。当您需要存储经常变化的数据时,您会使用它们,例如添加、更改值、删除、重新排序、排序等。例如,您可以使用列表来存储学生的成绩或冰箱中的物品。
快速检查 25.1
对于以下每个,您会使用元组还是列表来存储信息?
1
字母对及其在字母表中的位置:(1,a),(2,b),依此类推。
2
美国成年人的鞋码
3
从 1950 年到 2015 年城市对及其平均降雪量
4
美国所有人的姓名
5
您手机/电脑上的应用程序名称
25.2. 创建列表和获取特定位置的元素
您可以通过使用方括号来创建一个 Python 列表。行 L = [] 创建了一个表示空列表(没有元素的列表)的对象,并将名称 L 绑定到该空列表对象。L 是一个变量名,您可以使用任何您想要的变量名。您还可以创建一个初始包含项的列表。以下行创建了一个包含三个元素的列表,并将变量名 grocery 绑定到该列表:
grocery = ["milk", "eggs", "bread"]
与字符串和元组一样,您可以使用 len() 获取列表的长度。命令 len(L),其中 L 是一个列表,告诉您列表 L 中有多少个元素。空列表有 0 个元素。前面名为 grocery 的列表的长度是 3。
回想一下,在编程中,我们从 0 开始计数。与字符串和元组一样,列表中的第一个元素位于索引 0,第二个元素位于索引 1,依此类推。列表中的最后一个元素位于 len(L) - 1。如果您有一个购物清单 grocery = ["milk" "eggs", "bread"],您可以通过使用方括号索引列表来获取每个元素的价值,就像元组和字符串一样。以下代码索引列表并打印每个元素:
grocery = ["milk", "eggs", "bread"]
print(grocery[0])
print(grocery[1])
print(grocery[2])
如果您尝试索引比列表长度更远的位置会发生什么?比如说您有以下代码:
grocery = ["milk", "eggs", "bread"]
print(grocery[3])
您将得到以下错误,它告诉您您正在尝试索引一个比列表长度更长的列表:
Traceback (most recent call last):
File "<ipython-input-14-c90317837012>", line 2, in <module>
print(grocery[3])
IndexError: list index out of range
回想一下,因为列表中只有三个元素,并且第一个元素位于索引 0,所以 grocery 中的最后一个元素位于索引 2。索引位置 3 超出了列表的范围。
快速检查 25.2
您有以下列表:
desk_items = ["stapler", "tape", "keyboard", "monitor", "mouse"]
每个命令打印了什么?
1
print(desk_items[1])2
print(desk_items[4])3
print(desk_items[5])4
print(desk_items[0])
25.3. 计算元素的数量和位置
除了使用 len() 命令计算列表中所有元素的数量之外,你还可以通过使用 count() 操作来计算特定元素出现的次数。命令 L.count(e) 会告诉你元素 e 在列表 L 中出现的次数。例如,如果你正在查看你的购物清单,你可以计算单词 cheese 来确保你的清单上有五种奶酪,以制作你的五奶酪披萨食谱。
你还可以使用 index() 操作确定列表中第一个匹配值的索引。命令 L.index(e) 会告诉你元素 e 在列表 L 中第一次出现的位置(从 0 开始计数)。以下列表显示了 count() 和 index() 的工作方式。
列表 25.1. 使用 count 和 index 操作列表
years = [1984, 1986, 1988, 1988]
print(len(years)) *1*
print(years.count(1988)) *2*
print(years.count(2017)) *3*
print(years.index(1986)) *4*
print(years.index(1988)) *5*
-
1 打印 4 因为列表有四个元素
-
2 打印 2 因为列表中数字 1988 出现了两次
-
3 打印 0 因为 2017 在列表年份中根本不存在
-
4 打印 1 因为 1986 出现在索引 1(从 0 开始计数)
-
5 打印 2 因为索引只找到数字 1988 的第一次出现索引
如果你尝试获取列表中不存在元素的索引会发生什么?比如说你有以下代码:
L = []
L.index(0)
当你运行它时,你会得到以下错误。错误信息包含了导致错误的行号和行本身的信息。错误信息的最后一行包含了命令失败的原因:ValueError: 0 is not in list。这是在列表中不存在值的索引操作预期的行为:
Traceback (most recent call last):
File "<ipython-input-15-b3f3f6d671a3>", line 2, in <module>
L.index(0)
ValueError: 0 is not in list
快速检查 25.3
Q1:
以下代码打印的是什么?如果有错误,请写 error:
L = ["one", "three", "two", "three", "four", "three", "three", "five"] print(L.count("one")) print(L.count("three")) print(L.count("zero")) print(len(L)) print(L.index("two")) print(L.index("zero"))
25.4. 向列表添加项目:append、insert 和 extend
一个空的列表是没有用的。同样,一个你创建后就必须立即填充的列表也是没有用的。在你创建列表之后,你将想要向其中添加更多项目。例如,给定一个代表你每周购物清单的空纸条,你将想要在想到它们时将项目添加到列表的末尾,而不是每次添加项目时都在一张新的纸上转录所有内容。
要向列表添加更多元素,你可以使用三种操作之一:append、insert 和 extend。
25.4.1. 使用 append
L.append(e) 将元素 e 添加到你的列表 L 的末尾。向列表添加元素始终会将元素附加到列表的末尾,即最高索引。你一次只能添加一个元素。列表 L 被修改以包含一个额外的值。
假设你有一个最初为空的购物清单:
grocery = []
你可以按照以下方式添加一个项目:
grocery.append("bread")
列表现在包含一个元素:字符串 "bread"。
25.4.2. 使用 insert
L.insert(i, e)在列表L的索引i处添加元素e。你一次只能插入一个元素。为了找到插入的位置,从 0 开始计数列表L中的元素。当你插入时,该索引及其之后的所有元素都会向列表的末尾移动。列表L被修改以包含一个额外的值。
假设你有这个购物清单:
grocery = ["bread", "milk"]
你可以在两个现有项目之间插入一个项目:
grocery.insert(1, "eggs")
列表包含所有项目:
["bread", "eggs", "milk"]
25.4.3. 使用 extend
L.extend(M)将列表M中的所有元素追加到列表L的末尾。你实际上是将列表M中的所有元素追加到列表L的末尾,保持它们的顺序。列表L被修改以包含M中的所有元素。列表M保持不变。
假设你有一个初始的购物清单和一个要购买有趣东西的清单:
grocery = ["bread", "eggs", "milk"]
for_fun = ["drone", "vr glasses", "game console"]
你可以扩展这两个列表:
grocery.extend(for_fun)
这为你提供了一个主购物清单:
["bread", "eggs", "milk", "drone", "vr glasses", "game console"]
列表 25.2. 向列表中添加项目
first3letters = [] *1*
first3letters.append("a") *2*
first3letters.append("c") *3*
first3letters.insert(1,"b") *4*
print(first3letters) *5*
last3letters = ["x", "y", "z"] *6*
first3letters.extend(last3letters) *7*
print(first3letters) *8*
last3letters.extend(first3letters) *9*
print(last3letters) *10*
-
1 空列表
-
2 将字母“a”添加到列表 first3letters 的末尾
-
3 将字母“c”添加到列表 first3letters 的末尾
-
4 将字母“b”添加到列表 first3letters 的索引 1 处
-
5 打印[‘a’,‘b’,‘c’]
-
6 创建包含三个元素的列表
-
7 将元素“x”、“y”、“z”添加到列表 first3letters 的末尾
-
8 打印[‘a’,‘b’,‘c’,‘x’,‘y’,‘z’]
-
9 使用新修改的列表 first3letters 的内容扩展列表 last3letters,向 last3letters 列表添加“a”、“b”、“c”、“x”、“y”、“z”
-
10 打印[‘x’,‘y’,‘z’,‘a’,‘b’,‘c’,‘x’,‘y’,‘z’]
由于列表是可变对象,你在列表上执行的一些操作会导致列表对象本身发生变化。在列表 25.2 中,每次你向first3letters列表追加、插入或扩展它时,该列表都会被修改。同样,当你使用first3letters列表扩展last3letters时,last3letters列表也会被修改。
快速检查 25.4
每个代码片段执行后打印的内容是什么?
1
one = [1] one.append("1") print(one)2
zero = [] zero.append(0) zero.append(["zero"]) print(zero)3
two = [] three = [] three.extend(two) print(three)4
four = [1,2,3,4] four.insert(len(four), 5) print(four) four.insert(0, 0) print(four)
25.5. 从列表中删除项目:pop
如果你只能向列表中添加项目,那么列表就没有用了。它们会不断增长,很快就会变得难以管理。在删除项目时拥有可变对象也很有用,这样你就不必每次更改时都复制。例如,当你保持你的购物清单时,你希望在购买项目时从清单中删除它们,这样你就知道不再需要寻找它们了。每次你删除一个项目,你都可以保留相同的列表,而不是将你仍然需要的所有项目转录到一个新列表中。
你可以使用pop()操作从列表中删除项目,如列表 25.3 所示。命令L.pop()将从列表L的最后一个位置删除元素。你可以选择在括号中指定一个数字,表示要删除的值的索引,使用L.pop(i)。当删除时,被删除元素的列表被修改。所有在删除元素之后的元素都会向一个位置移动以替换被删除的元素。这个操作是一个函数,并返回被删除的元素。
列表 25.3. 从列表中删除
polite = ["please", "and", "thank", "you"] *1*
print(polite.pop()) *2*
print(polite) *3*
print(polite.pop(1)) *4*
print(polite) *5*
-
1 四个元素的列表
-
2 打印出“you”,因为 pop()返回最后一个索引的元素值
-
3 打印出[‘please’, ‘and’, ‘thank’],因为 pop()删除了最后一个元素
-
4 打印出“and”,因为 pop(1)返回索引 1 的元素值
-
5 打印出[‘please’, ‘thank’],因为上一行删除了索引 1 的元素(列表中的第二个元素)
与向列表中添加项目一样,从列表中删除项目也会修改列表。每个删除项目的操作都会改变操作所进行的列表。在列表 25.3 中,每次打印列表时,都会打印出修改后的列表。
快速检查 25.5
Q1:
当这段代码执行时,会打印出什么内容?
pi = [3, ".", 1, 4, 1, 5, 9] pi.pop(1) print(pi) pi.pop() print(pi) pi.pop() print(pi)
25.6. 修改元素值
到目前为止,你可以在列表中添加和删除项目。由于可变性,你甚至可以修改列表中现有的对象元素以更改它们的值。例如,在你的购物清单中,你意识到你需要切达干酪而不是马苏里拉干酪。和之前一样,由于可变性的特性,你不需要将列表复制到另一张纸上,只更改一个项目,而是更合理地通过将马苏里拉干酪替换为切达干酪来修改你目前拥有的列表。
要修改列表中的元素,你首先需要访问该元素本身,然后分配给它一个新的值。你通过使用其索引来访问元素。例如 L[0] 指的是第一个元素。当你创建列表时使用的方括号现在在列表变量名右侧放置时有这个新用途。以下列表显示了如何进行此操作。
列表 25.4. 修改元素值
colors = ["red", "blue", "yellow"] *1*
colors[0] = "orange" *2*
print(colors) *3*
colors[1] = "green" *4*
print(colors) *5*
colors[2] = "purple" *6*
print(colors) *7*
-
1 初始化字符串列表
-
2 将“red”更改为“orange”
-
3 打印出[‘orange’, ‘blue’, ‘yellow’],因为第 2 行更改了索引 0 的元素
-
4 将修改后的列表中的“blue”更改为“green”,现在索引 0 的元素是“orange”
-
5 打印出[‘orange’, ‘green’, ‘yellow’],因为第 4 行更改了索引 1 的元素
-
6 在修改后的列表中将“yellow”更改为“purple”,列表中的元素索引 0 为“orange”,索引 1 为“green”
-
7 打印出[‘orange’, ‘green’, ‘purple’],因为第 6 行更改了索引 2 的元素
可变列表中的元素可以被修改为包含不同的值。在列表被修改后,从那时起进行的每个操作都是在修改后的列表上进行的。
快速检查 25.6
如果 L 是一个包含以下代码第一行所示数字的整数列表,那么在执行以下四个后续操作之后,L 的值是什么?
L = [1, 2, 3, 5, 7, 11, 13, 17]
1
L[3] = 42
L[4] = 63
L[-1] = L[0](回想一下从 第 7 课 中如何使用负索引。)4
L[0] = L[1] + 1
摘要
在本课中,我的目标是教你一种新的数据类型,Python 列表。列表是一个可变对象,其值可以改变。列表包含元素,你可以向其中添加元素,从其中删除元素,更改元素值,并对整个列表执行操作。以下是主要收获:
-
列表可以是空的,也可以包含元素。
-
你可以向列表的末尾添加一个元素,在特定索引处添加,或者通过添加多个元素来扩展它。
-
你可以从列表中删除元素,从末尾或从特定索引处删除。
-
你可以更改元素值。
-
每个操作都会修改列表,因此列表对象会发生变化,而无需将其重新分配给另一个变量。
让我们看看你是否掌握了这个...
Q25.1
你从一个空列表开始,打算包含餐厅菜单中的项目:
menu = []
编写一个或多个命令来修改列表,以便列表菜单包含
["pizza", "beer", "fries", "wings", "salad"].继续编写一个或多个命令来修改列表,使其包含
["salad", "fries", "wings", "pizza"].最后,编写一个或多个命令来修改列表,使其包含
["salad", "quinoa", "steak"].Q25.2
编写一个名为
unique的函数。它接受一个参数,名为L的列表。该函数不会修改L并返回一个新列表,其中只包含L中的独特元素。Q25.3
编写一个名为
common的函数。它接受两个参数,名为L1和L2的列表。该函数不会修改L1或L2。如果L1中每个独特的元素都在L2中,并且如果L2中每个独特的元素都在L1中,则返回True。否则返回False。提示:尝试重用你的 Q25.2 中的函数。例如,
common([1,2,3], [3,1,2])返回Truecommon([1,1,1], [1])返回Truecommon([1], [1, 2])返回False
第 26 课. 列表的进阶操作
在阅读 第 26 课 之后,你将能够
-
构建元素为列表的列表
-
排序和反转列表元素
-
通过在字符上分割将字符串转换为列表
列表通常用于表示一组项目,通常是相同类型的,但并不一定是。你会发现列表元素本身也是列表可能很有用。例如,假设你想要保存你家中所有项目的列表。因为你有很多项目,所以有子列表会更组织,每个子列表代表一个房间,子列表的元素是该房间中的所有项目。
在这一点上,退一步理解这个新的可变对象——列表——所发生的事情是很重要的。列表会直接被你对它们所做的任何操作修改。因为列表是直接修改的,所以在操作后你不需要将列表重新赋值给一个新的变量;列表本身现在包含已更改的值。要查看修改后的列表的值,你可以打印它。
考虑这一点
你的朋友能背诵到 100 位的圆周率。你将每个数字添加到列表中,当他告诉你时。你想要找出前 100 位中有多少个零。你如何快速做到这一点?
答案:
如果你排序了列表,你可以计算列表开头有多少个零。
26.1. 排序和反转列表
在你有一个元素列表之后,你可以执行重新排列整个列表中元素的运算。例如,如果你有一个班级中学生的列表,你不需要保留两个相同学生的列表:一个排序,一个未排序。你可以从一个未排序的列表开始,然后在需要时直接对其进行排序。当你只关心列表的内容时,以排序的方式存储它可能更受欢迎。但请注意,一旦排序,除非你从头开始重新创建它,否则你不能回到列表的未排序版本。
因为列表是可变的,所以你可以使用操作 sort() 对列表进行排序,这样原始列表的元素现在就是有序的。命令 L.sort() 将按升序(对于数字)和字典序(对于字母或字符串)对列表 L 进行排序。相比之下,如果你想要对一个不可变的元组对象中的项目进行排序,那么在从末尾开始连接项目时,你会创建许多中间对象(取最后一个项目并将其放在索引 0,取倒数第二个项目并将其放在索引 1,依此类推)。
反转列表也可能很有用。例如,如果你有一个学生姓名的列表并且按字母顺序排序了它们,你可以反转列表,使它们按逆字母顺序排序。命令 L.reverse() 反转列表 L,使得原来的第一个元素现在在末尾,依此类推。
列表 26.1. 排序和反转列表
heights = [1.4, 1.3, 1.5, 2, 1.4, 1.5, 1]
heights.reverse() *1*
print(heights) *2*
heights.sort() *3*
print(heights) *4*
heights.reverse() *5*
print(heights) *6*
-
1 反转原始列表
-
2 打印 [1, 1.5, 1.4, 2, 1.5, 1.3, 1.4],因为上一行通过将第一个元素移动到最后位置,第二个元素移动到倒数第二个位置,依此类推来反转原始列表
-
3 按升序排序列表
-
4 打印 [1, 1.3, 1.4, 1.4, 1.5, 1.5, 2],因为上一行按升序排序了列表
-
5 反转排序后的列表
-
6 打印 [2, 1.5, 1.5, 1.4, 1.4, 1.3, 1],因为上一行反转了按升序排序的列表
快速检查 26.1
Q1:
每次操作后列表 L 的值是多少?
L = ["p", "r", "o", "g", "r", "a", "m", "m", "i", "n", "g"] L.reverse() L.sort() L.reverse() L.reverse() L.sort()
你已经见过包含浮点数、整数或字符串的列表。但列表可以包含任何类型的元素,包括其他列表!
26.2. 列表列表
如果你想要编写一个游戏,特别是依赖于用户在特定位置的游戏,你通常会想要考虑在二维坐标系中表示的棋盘位置。列表可以帮助你通过使用一个其元素也是列表的列表来表示二维坐标系。以下列表创建了一个列表 L,其三个元素都是空列表,然后填充了这个列表。
列表 26.2. 创建和填充列表列表
L = [[], [], []] *1*
L[0] = [1,2,3] *2*
L[1].append('t') *3*
L[1].append('o') *4*
L[1][0] = 'd' *5*
-
1 空的列表列表
-
2 L 的值为 [[1,2,3], [], []],因为你将索引 0 的元素设置为列表 [1,2,3]。
-
3 L 的值为 [[1,2,3], ['t'], []],因为你将字符串 't' 添加到了中间的空列表中。
-
4 L 的值为 [[1,2,3], ['t', 'o'], []],因为你将字符串 'o' 添加到了已经修改过的中间列表中。
-
5 L 的值为 [[1,2,3], ['d', 'o'], []],因为你访问了索引 1(一个列表)的对象,然后访问该对象的索引 0(字母 t)的元素来更改它(改为字母 d)。
使用列表列表进行工作时,在索引列表以处理其元素时增加了另一层间接性。第一次索引列表列表(甚至列表列表列表列表),你访问该位置的对象。如果该位置的对象是一个列表,你可以进一步索引该列表,依此类推。
你可以使用列表列表来表示井字棋盘。列表 26.3 显示了使用列表设置棋盘的代码。因为列表是一维的,你可以认为外部列表的每个元素是棋盘中的一行。每个子列表将包含该行中每列的所有元素。
列表 26.3. 使用列表表示的井字棋盘
x = 'x' *1*
o = 'o' *2*
empty = '_' *3*
board = [[x, empty, o], [empty, x, o], [x, empty, empty]] *4*
-
1 变量 x
-
2 变量 o
-
3 空间
-
4 用其值替换每个变量。变量 board 有三行(每个子列表一行)和三列(每个子列表有三个元素)。
用代码表示的井字棋盘看起来像这样:
x _ o
_ x o
x _ _
通过使用列表中的列表,您可以调整子列表的数量以及每个子列表包含的元素数量来表示任何大小的井字棋板。
快速检查 26.2
使用在 列表 26.3 中设置的变量,编写一行代码来设置一个看起来像这样的板:
1
一个 3 × 3 的板
x x x
o o o
2
一个 3 × 4 的板
x o x o
o o x x
o _ x x
26.3. 将字符串转换为列表
假设您得到一个包含以逗号分隔的电子邮件数据的字符串。您希望将每个电子邮件地址分离出来,并将每个地址保存在一个列表中。以下示例字符串显示了输入数据可能的样子:
emails = "zebra@zoo.com,red@colors.com,tom.sawyer@book.com,pea@veg.com"
您可以通过使用字符串操作来解决这个问题,但这种方式可能有些繁琐。首先,您需要找到第一个逗号的位置。然后,将电子邮件保存为从字符串 emails 开始到该索引的子字符串。接着,将字符串从该索引到 emails 结束的部分保存到另一个变量中。最后,重复这个过程,直到没有更多的逗号可以找到。这个解决方案使用了循环,并迫使您创建不必要的变量。
使用列表为这个问题提供了一个简单的一行解决方案。使用前面的 emails 字符串,您可以这样做:
emails_list = emails.split(',')
这行代码使用了名为 emails 的字符串上的 split() 操作。在 split() 的括号中,您可以放置您想要分割字符串的元素。在这种情况下,您想要在逗号上分割。运行该命令的结果是 emails_list 是一个包含每个逗号之间子字符串的字符串列表,如下所示:
['zebra@zoo.com', 'red@colors.com', 'tom.sawyer@book.com', 'pea@veg.com']
注意,现在每个电子邮件都是列表 emails_list 中的一个单独元素,这使得处理起来变得容易。
快速检查 26.3
编写一行代码以实现以下任务:
1
通过空格字符分割字符串
" abcdefghijklmnopqrstuvwxyz"。2
通过单词分割字符串
"spaces and more spaces"。3
通过字母
s分割字符串"the secret of life is 42"。
通过在 第 25 课 中看到的列表操作(排序和反转列表),您现在能够模拟现实生活中的现象:物品的栈和队列。
26.4. 列表的应用
你为什么需要通过使用列表来模拟一个栈或队列呢?这是一个有点哲学性的问题,它暗示了你将在下一单元中看到的内容。一个更基本的问题是,当我可以创建一堆整数/浮点/字符串对象并记住它们的顺序时,我为什么还需要一个列表对象呢?这个想法是,你使用更简单的对象来创建具有更多特定行为的更复杂对象。就像列表是由一组有序对象组成的一样,栈或队列也是由列表组成的。你可以自己创建一个栈或队列对象,使得它们的构造相同(你使用列表),但行为不同。
26.4.1. 栈
想象一下煎饼堆。当它们被制作时,新的煎饼被添加到堆的顶部。当煎饼被吃掉时,它从堆的顶部取出。你可以用列表来模拟这种行为。栈的顶部是列表的末尾。每次你有新元素时,你使用 append() 方法将其添加到列表的末尾。每次你想取出一个元素时,你使用 pop() 方法从列表的末尾移除它。
列表 26.4 展示了 Python 中煎饼栈的实现。假设你有蓝莓和巧克力煎饼。蓝莓煎饼由元素 'b'(字母 b 作为字符串)表示,巧克力煎饼由 'c'(字母 c 作为字符串)表示。你的煎饼栈最初是一个空列表(还没有制作煎饼)。一位厨师制作一批煎饼;厨师也是一个包含煎饼元素的列表。一旦厨师制作了一批煎饼,就会使用 extend() 方法将这批煎饼添加到栈中。吃煎饼的人可以用栈上的 pop() 操作来表示。
列表 26.4. 使用列表表示的煎饼栈
stack = [] *1*
cook = ['blueberry', 'blueberry', 'blueberry'] *2*
stack.extend(cook) *3*
stack.pop() *4*
stack.pop() *4*
cook = ['chocolate', 'chocolate'] *5*
stack.extend(cook) *6*
stack.pop() *4*
cook = ['blueberry', 'blueberry'] *5*
stack.extend(cook) *7*
stack.pop() *8*
stack.pop() *8*
stack.pop() *8*
-
1 空列表
-
2 制作了三个煎饼的列表
-
3 将厨师制作的煎饼添加到栈中
-
4 移除列表中的最后一个元素
-
5 新一批煎饼
-
6 将厨师的一批煎饼添加到栈的末尾
-
7 将厨师的一批煎饼添加到栈的末尾
-
8 移除列表中的最后一个元素
栈是一种 先进后出 结构,因为最先添加到栈中的项目是最后被取出的。另一方面,队列是 先进先出,因为最先添加到队列中的项目是第一个被取出的。
26.4.2. 队列
想象一下杂货店的队列。当新来的人到达时,他们站在队伍的末尾。当人们被帮助时,那些在队列中等待时间最长的人(队伍的前面)将被下一个帮助。
你可以使用列表来模拟队列。当你得到新元素时,你将它们添加到列表的末尾。当你想要取出一个元素时,你从列表的起始位置移除它。
代码列表 26.5 展示了代码中模拟队列的一个示例。你的杂货店有一条队伍,用列表表示。当顾客进来时,你使用 append() 将他们添加到列表的末尾。当顾客得到帮助时,你使用 pop(0) 从队伍的前端移除他们。
列表 26.5。用列表表示的人队列
line = [] *1*
line.append('Ana') *2*
line.append('Bob') *3*
line.pop(0) *4*
line.append('Claire') *5*
line.append('Dave') *5*
line.pop(0) *6*
line.pop(0) *6*
line.pop(0) *6*
-
1 空列表
-
2 现在队列中一个人的名单
-
3 现在队列中的两个人名单
-
4 从队列中移除的第一个人
-
5 新人添加到列表的末尾
-
6 从列表开头移除的人
使用更复杂的数据类型,如列表,你可以模拟现实生活中的动作。在这种情况下,你可以使用特定的操作序列来模拟对象栈和队列。
快速检查 26.4
以下情况最符合队列、栈还是都不是?
1
你的文本编辑器的撤销机制
2
将网球放入容器中然后取出来
3
排队等待检查的汽车
4
机场行李进入传送带并被其主人取走
摘要
在本课中,我的目标是教你更多关于列表的操作。你排序了一个列表,反转了一个列表,创建了包含其他列表作为元素的列表,并通过在字符上分割将字符串转换为列表。以下是主要收获:
-
列表可以包含其他列表作为元素。
-
你可以对列表的元素进行排序或反转。
-
栈和队列的行为可以使用列表实现。
让我们看看你是否掌握了这个...
Q26.1
编写一个程序,该程序接受一个包含以逗号分隔的城市名称的字符串,然后按顺序打印城市名称列表。你可以从以下内容开始:
cities = "san francisco,boston,chicago,indianapolis"Q26.2
编写一个名为
is_permutation的函数。它接受两个列表,L1和L2。如果L1和L2是彼此的排列,则函数返回True。否则返回False。L1中的每个元素都在L2中,反之亦然,只是顺序不同。例如,
is_permutation([1,2,3], [3,1,2])返回True.is_permutation([1,1,1,2], [1,2,1,1])返回True.is_permutation([1,2,3,1], [1,2,3])返回False.
第 27 课。字典作为对象之间的映射
阅读第 27 课[(#ch27)],你将能够
-
理解字典对象数据类型是什么
-
在字典中添加、删除和查找对象
-
理解何时使用字典对象
-
理解字典和列表之间的区别
在上一课中,你学习了列表作为具有列表中特定位置元素的数据集合。列表在你想要存储一组对象时很有用;你看到你可以存储一组名字或一组数字。但在现实生活中,你经常有数据对:一个单词及其含义、一个单词及其同义词列表、一个人及其电话号码、一部电影及其评分、一首歌及其艺术家,等等。
图 27.1 从上一课的购物清单隐喻中汲取灵感,并展示了将其应用于字典的一种方法。在列表中,你的购物项目是按顺序列举的;第一个项目在第一行,以此类推。你可以将列表视为将数字 0、1、2 等按顺序映射到列表中的每个项目。使用字典,你可以获得更多的灵活性,可以映射什么以及映射到什么。在图 27.1 中,购物字典现在将一个项目映射到其数量。
图 27.1。列表将第一个项目放在位置 0,第二个项目放在位置 1,第三个项目放在位置 2。字典没有位置,而是将一个对象映射到另一个对象;在这里,它将购物项目映射到其数量。

你可以将列表视为一种结构,它将整数(索引 0、1、2、3 等)映射到对象;列表只能通过整数索引。字典是一种可以将任何对象(而不仅仅是整数)映射到任何其他对象的数据结构;使用任何对象进行索引在具有数据对的情况下更有用。像列表一样,字典是可变的,这意味着当你对字典对象进行更改时,对象本身会发生变化,而不必复制它。
考虑这一点
词典将一种语言中的单词映射到另一种语言中的对应词。对于以下每个场景,你能否将一个事物映射到另一个事物?
-
你的朋友及其电话号码
-
你看过的所有电影
-
你最喜欢的歌曲中每个单词出现的次数
-
该地区每个咖啡馆及其 Wi-Fi 可用性
-
家具店中所有可用的油漆名称
-
你的同事及其上下班时间
答案:
-
是
-
否
-
是
-
是
-
否
-
是
27.1. 创建字典、键和值
许多编程语言都有一种将对象映射到彼此或使用另一个对象查找一个对象的方法。在 Python 中,这样的对象被称为具有对象类型dict的字典。
字典将一个对象映射到另一个对象;我们也可以说,你使用另一个对象查找一个对象。如果你想到一个传统的单词字典,你查找一个词来找到它的意思。在编程中,你查找的项目(在传统字典中,是 词)被称为 键,项目查找返回的内容(在传统字典中,是 意思)被称为 值。在编程中,字典存储条目,每个条目都是一个键值对。你使用一个对象(键)来查找另一个对象(值)。
你可以使用花括号创建一个空的 Python 字典:
grocery = {}
这条命令创建了一个没有条目且绑定到名为 grocery 的变量的空字典。
你也可以创建一个已经包含条目的字典。在你的购物清单中,你将一个购物项目映射到其数量。换句话说,grocery 的键将是表示购物项目的字符串,grocery 的值将是表示数量的整数:
grocery = {"milk": 1, "eggs": 12, "bread": 2}
这行代码创建了一个包含三个条目的字典,如图 27.2 所示。字典中的每个条目都由逗号分隔。一个条目的键和值由冒号分隔。键在冒号的左边,该键的值在冒号的右边。
图 27.2. 初始化了三个条目的字典。条目由逗号分隔。每个条目的键在冒号的左边,键对应的值在冒号的右边。

字典的键和值都是单个对象。一个字典条目不能有多个对象作为其键或多个对象作为其值。如果你想存储多个对象作为值,你可以将所有对象存储在一个元组中,因为元组是一个对象。例如,你的购物清单可以有字符串键 "eggs" 和元组值 (1, "carton") 或 (12, "individual")。注意,在两种情况下,值都是单个 Python 对象,一个元组。
快速检查 27.1
对于以下每种情况,编写一行代码创建一个字典并将其绑定到具有适当名称的变量。对于每个,也指出键和值:
1
员工姓名及其电话号码和地址的空字典
2
1990 年和 2000 年每个城市得到的英寸数的空字典
3
房屋物品及其价值的字典:一台价值 2,000 美元的电视和一台价值 1,500 美元的沙发
| |
快速检查 27.2
对于以下每个情况,字典中有多少条条目?键的类型是什么,值的类型是什么?
1
d = {1:-1, 2:-2, 3:-3}2
d = {"1":1, "2":2, "3":3}3
d = {2:[0,2], 5:[1,1,1,1,1], 3:[2,1,0]}
27.2. 向字典中添加键值对
空字典或具有固定数量的条目的字典没有用。你想要添加更多的条目来存储更多信息。要添加一个新的键值对,你使用方括号,就像使用列表一样:
d[k] = v
这个命令将键 k 和其关联的值 v 添加到字典 d 中。如果你尝试向相同的键再次添加,与该键关联的先前值将被覆盖。
在任何时候,你都可以使用 len() 函数来告诉你字典中的条目数量。以下列表向字典中添加项。
列表 27.1. 向字典中添加对
legs = {} *1*
legs["human"] = 2 *2*
legs["cat"] = 4 *3*
legs["snake"] = 0 *4*
print(len(legs)) *5*
legs["cat"] = 3 *6*
print(len(legs)) *7*
print(legs) *8*
-
1 空字典
-
2 添加键 “human” 并将其值设为 2
-
3 添加键 “cat” 并将其值设为 4
-
4 添加键 “snake” 并将其值设为 0
-
5 打印 3,因为字典中有三个条目
-
6 将键 “cat” 的值更改为 3
-
7 打印 3,因为你只修改了 “cat” 的条目
-
8 打印
{‘human’: 2, ‘snake’: 0, ‘cat’: 3}
上一段代码展示了使用 Python 3.5 的输出。如果你使用的是 Python 的不同版本,你可能会看到字典中的不同顺序。使用 Python 3.5 的输出,你可能已经注意到你添加到字典中的项是一个人,然后是一只猫,然后是一条蛇。但是当你打印 列表 27.1 中的字典时,字典打印出了不同的顺序。这是字典的正常行为,第 27.4.1 节将进一步讨论这一点。
27.2.1. 对键的限制的简短偏离
当你尝试将一个对象键插入到一个已经包含该键的字典中时,现有键的值会被覆盖。这引出了一个关于你可以将哪些类型的对象存储为字典键的有趣观点。
在字典中不能有相同的键多次出现;如果你这样做,当你要检索值时,Python 就不知道你指的是哪个键。例如,如果你有一个 box 作为键,它映射到 container,还有一个 box 也映射到 fight,那么你给想要 box 定义的人提供哪个定义?第一个你找到的?最后一个?两个都给?答案并不明确。Python 保证字典中的所有键都是唯一的对象,而不是处理这种情况。
Python 语言是如何以及为什么做出这样的保证的?Python 强制要求键是一个不可变对象。这个决定是因为 Python 实现字典对象的方式。
给定一个键,Python 使用基于键值的公式来计算存储值的存储位置。这个公式被称为 散列函数。使用这种方法是为了当你想要查找一个值时,你可以通过运行公式而不是遍历所有键来快速检索值。因为散列函数的结果在插入或查找项时应该是相同的,所以值的存储位置是固定的。使用不可变对象,你会得到相同的存储位置值,因为不可变对象的价值不会改变。但是,如果你使用了一个可变对象,那么在应用于已更改的键值时,散列函数可能会给出不同的存储位置值,而不是原始的键值。
快速检查 27.3
Q1:
执行每行后字典的值是多少?
city_pop = {} city_pop["LA"] = 3884 city_pop["NYC"] = 8406 city_pop["SF"] = 837 city_pop["LA"] = 4031
27.3. 从字典中删除键值对
与列表一样,你可以在将项目放入字典后使用 pop() 操作来删除项目。命令 d.pop(k) 将删除字典 d 中与键 k 对应的键值条目。这个操作就像一个函数,并返回与键 k 关联的字典中的值。在下一条列表中的 pop 操作之后,字典 household 将具有值 {"person":4, "cat":2, "dog":1}。
列表 27.2. 从字典中删除配对
household = {"person":4, "cat":2, "dog":1, "fish":2} *1*
removed = household.pop("fish") *2*
print(removed) *3*
-
1 填充字典
-
2 删除键为“fish”的条目,并将与键“fish”关联的值保存到变量中
-
3 打印已删除条目的值
快速检查 27.4
Q1:
以下代码打印了什么?如果有错误,请写错误:
constants = {"pi":3.14, "e":2.72, "pyth":1.41, "golden":1.62} print(constants.pop("pi")) print(constants.pop("pyth")) print(constants.pop("i"))
27.4. 获取字典中的所有键和值
Python 有两个操作允许你获取字典中的所有键和所有值。这在你需要遍历所有键值对以找到符合某些标准的条目时非常有用。例如,如果你有一个将歌曲名称映射到其评分的字典,你可能想检索所有键值对,并仅保留评分是 4 或 5 的条目。
如果一个名为 songs 的字典包含歌曲和评分的配对,你可以使用 songs.keys() 来获取字典中的所有键。以下代码打印 dict_keys(['believe', 'roar', 'let it be']):
songs = {"believe": 3, "roar": 5, "let it be": 4}
print(songs.keys())
表达式 dict_keys(['believe', 'roar', 'let it be']) 是一个包含字典中所有键的 Python 对象。
你可以直接使用 for 循环遍历返回的键,如下所示:
for one_song in songs.keys():
或者,你也可以通过将返回的键转换为列表来保存键,如下命令所示:
all_songs = list(songs.keys())
同样,命令songs.values()给你字典songs中的所有值。你可以直接遍历它们,或者如果你需要在代码的后面使用它们,可以将它们转换为列表。通常,遍历字典中的键最有用,因为在你知道一个键之后,你总是可以查找与该键对应的值。
让我们看看另一个例子。假设你班上有以下学生的数据:
Name Quiz 1 Grade Quiz 2 Grade
Chris 100 70
Angela 90 100
Bruce 80 40
Stacey 70 70
列表 27.3 展示了如何使用字典命令来跟踪班级中的学生和他们的成绩。首先,你创建一个字典,将学生姓名映射到他们在考试中的成绩。假设每个学生参加了两次考试,字典中每个学生的值将是一个包含两个元素的列表。使用字典,你可以通过遍历所有键来打印班级中所有学生的姓名,你也可以遍历所有值并打印所有测验的平均分。最后,你甚至可以修改每个键的值,通过将两个测验的平均值添加到列表的末尾。
列表 27.3. 使用字典跟踪学生成绩
grades = {} *1*
grades["Chris"] = [100, 70] *1*
grades["Angela"] = [90, 100] *1*
grades["Bruce"] = [80, 40] *1*
grades["Stacey"] = [70, 70] *1*
for student in grades.keys(): *2*
print(student) *2*
for quizzes in grades.values(): *3*
print(sum(quizzes)/2) *2*
for student in grades.keys(): *4*
scores = grades[student] *5*
grades[student].append(sum(scores)/2) *6*
print(grades) *7*
-
1 设置字典,将字符串映射到包含两个测验分数的列表
-
2 遍历键并打印它们
-
3 遍历值并打印它们的平均值
-
4 遍历所有键
-
5 获取每个学生的分数并将它们分配给 scores 变量,以便在下一条线中进行平均计算
-
6 计算元素的平均值并将它追加到列表的末尾
-
7 打印{‘Bruce’: [80, 40, 60.0], ‘Stacey’: [70, 70, 70.0], ‘Angela’: [90, 100, 95.0], ‘Chris’: [100, 70, 85.0]}
快速检查 27.5
Q1:
你有以下代码行,通过将每个人的年龄增加 1 来对员工数据库进行操作。这段代码会打印什么?
employees = {"John": 34, "Mary": 24, "Erin": 50} for em in employees.keys(): employees[em] += 1 for em in employees.keys(): print(employees[em])
27.4.1. 字典对没有排序
在本课中,我提到你可能根据你使用的 Python 版本看到不同的结果。如果你使用 Python 3.5 来运行列表 27.3 中的代码,你可能会注意到一些奇怪的事情。打印所有键的输出如下:
Bruce
Stacey
Angela
Chris
但当你向字典中添加项目时,你添加了 Chris,然后是 Angela,然后是 Bruce,然后是 Stacey。这些顺序似乎不匹配。与列表不同,Python 字典忘记了项目添加到字典中的顺序。当你请求键或值时,你无法保证它们返回的顺序。你可以通过输入以下代码来查看这一点,该代码检查两个字典之间的相等性,然后是两个列表之间的相等性:
print({"Angela": 70, "Bruce": 50} == {"Bruce": 50, "Angela": 70})
print(["Angela", "Bruce"] == ["Bruce", "Angela"])
尽管条目放入的顺序不同,这两个字典是相等的。相比之下,两个名字的列表必须以相同的顺序排列才能被认为是相等的。
27.5. 为什么你应该使用字典?
到现在为止,应该很清楚,字典是非常有用的对象,因为它们将对象(键)映射到其他对象(值),并且你可以根据键来查找值。字典有两个常见的用途:跟踪某物出现的次数,以及使用字典将项目映射到函数。
27.5.1. 使用频率字典进行计数
可能字典最常见的用途之一是跟踪某种物品的数量。例如,如果你在编写 Scrabble 游戏,你可能使用字典来跟踪手中每个字母的数量。如果你有一个文本文档,你可能想跟踪你使用每个单词的次数。在列表 27.4 中,你将构建一个频率字典,将单词映射到它在歌曲中出现的次数。该代码通过在空格处拆分字符串来创建单词列表。使用最初为空的字典,你遍历列表中的所有单词并执行以下两项之一:
-
如果你还没有将单词添加到字典中,则将其添加为键,并设置其计数为 1。
-
如果你已经将单词添加到字典中,则将其计数增加 1。
列表 27.4. 构建频率字典
lyrics = "Happy birthday to you Happy birthday to you Happy birthday dear
Happy birthday to you" *1*
counts = {} *2*
words = lyrics.split(" ") *3*
for w in words: *4*
w = w.lower() *5*
if w not in counts:
counts[w] = 1 *6*
else:
counts[w] += 1 *7*
print(counts) *8*
-
1 包含歌词的字符串
-
2 空频率字典
-
3 通过在空格字符上拆分字符串来获取字符串中的所有单词列表
-
4 遍历上一行中的列表中的每个单词
-
5 转换为小写
-
6 单词尚未在字典中,因此将其作为键添加,并设置其值为 1
-
7 单词已在字典中,因此将其计数增加 1
-
8 打印 {‘happy’: 4, ‘to’: 3, ‘dear’: 1, ‘you’: 3, ‘birthday’: 4}
频率字典是 Python 字典的有用应用,你将在课程 29 的顶石项目中编写一个函数来构建频率字典。
27.5.2. 构建非传统字典
Python 字典是一种有用的数据结构。它允许通过使用另一个对象的值来查找另一个对象的值来轻松访问一个对象的值。任何你需要映射两个项目并在以后访问它们的时候,字典应该是你首先尝试使用的东西。但是,有一些不太明显的使用字典的场景。一种用途是将常用名称映射到函数。在列表 27.5 中,你定义了三个函数,给定一个输入变量,可以找到三个常见形状的面积:正方形、圆形和等边三角形。
你可以构建一个将字符串映射到函数本身的字典,该函数通过函数名引用。当你查找每个字符串时,你会得到函数对象。然后,使用函数对象,你可以使用参数来调用它。在列表 27.5 中,当你使用print(areas"sq")行中的"sq"访问字典时,areas["sq"]检索到的值是名为square的函数。然后,当你使用areas"sq"时,该函数在数字n = 2上被调用。
列表 27.5. 字典和函数
def square(x): *1*
return x*x *1*
def circle(r): *2*
return 3.14*r*r *2*
def equilateraltriangle(s): *3*
return (s*s)*(3**0.5)/4 *3*
areas = {"sq": square, "ci": circle, "eqtri": equilateraltriangle} *4*
n = 2
print(areas"sq") *5*
print(areas"ci") *6*
print(areas"eqtri") *7*
-
1 已知计算正方形面积的函数
-
2 已知计算圆面积的函数
-
3 已知计算等边三角形面积的函数
-
4 字典将字符串映射到函数
-
5 在 n(n 为 2)上通过键“sq”调用字典中映射的函数
-
6 通过键“ci”在 n(n 为 2)上调用字典中映射的函数
-
7 在 n(n 为 2)上通过键“eqtri”调用字典中映射的函数
摘要
在本课中,我的目标是教你一个新的数据类型,Python 字典。字典将一个对象映射到另一个对象。像列表一样,字典是可变对象,你可以向其中添加、从其中删除和更改元素。与列表不同,字典没有顺序,并且只允许某些对象类型作为键。以下是主要收获:
-
字典是可变的。
-
字典键必须是不可变对象。
-
字典值可以是可变的或不可变的。
-
字典没有顺序。
让我们看看你是否掌握了这个...
Q27.1
编写一个程序,使用字典来完成以下任务。给定一个歌曲名称(字符串)映射到评分(整数)的字典,打印所有评分正好为 5 的歌曲名称。
Q27.2
编写一个名为
replace的函数。它接受一个字典d和两个值v和e。该函数不返回任何内容。它修改d,使得d中所有的值v都被e替换。例如,
replace({1:2, 3:4, 4:2}, 2, 7)修改d为{1: 7, 3: 4, 4: 7}.replace({1:2, 3:1, 4:2}, 1, 2)修改d为{1: 2, 3: 2, 4: 2}.Q27.3
编写一个名为
invert的函数。它接受一个字典d。该函数返回一个新的字典d_inv。d_inv中的键是d中的唯一值。d_inv中对应键的值是一个列表。该列表包含所有在d中映射到相同值的键。例如,
invert({1:2, 3:4, 5:6})返回{2: [1], 4: [3], 6: [5]}.invert({1:2, 2:1, 3:3})返回{1: [2], 2: [1], 3: [3]}.invert({1:1, 3:1, 5:1})返回{1: [1, 3, 5]}.
第 28 课:别名和复制列表和字典
在阅读 第 28 课 后,你将能够
-
为可变对象(列表和字典)创建别名
-
对可变对象(列表和字典)进行复制
-
对列表进行排序后的复制
-
根据某些标准从可变对象中删除元素
可变对象很方便使用,因为它们允许你修改对象本身而不需要复制。当你的可变对象很大时,这种行为是有意义的,因为否则每次对其做出更改时都复制一个大项是昂贵且浪费的。但是使用可变对象引入了一个你需要注意的副作用:你可以有多个变量绑定到同一个可变对象,并且可以通过这两个名称之一来修改对象。
考虑这一点
想想一个名人。他们有哪些别名,或者他们还有哪些其他名字或昵称?
答案:比尔·盖茨
昵称:比尔,威廉,威廉·盖茨,威廉·亨利·盖茨三世
假设你有一份关于著名计算机科学家格蕾丝·霍珀的数据。让我们说格蕾丝·霍珀是一个对象,她的值是一个标签列表:["programmer", "admiral", "female"]。对她来说,朋友们可能知道她叫 Grace,其他人可能叫 Ms. Hopper,她的昵称是 Amazing Grace。所有这些名字都是同一个人的别名,同一个具有相同标签字符串的对象。现在假设一个认识她叫 Grace 的人向她的标签列表中添加另一个值:"deceased"。对于认识她叫 Grace 的人来说,她的标签列表现在是 ["programmer", "admiral", "female", "deceased"]。但是由于标签指的是同一个人,她的其他所有别名现在也指向新的标签列表。
28.1. 使用对象别名
在 Python 中,变量名 是指向对象的名称。该对象位于计算机内存中的特定位置。在 第 24 课 中,你使用了 id() 函数来查看对象的内存位置的数值表示。
28.1.1. 不可变对象的别名
在查看可变对象之前,让我们看看当你使用两个指向不可变对象的变量名之间的赋值运算符(等号)时会发生什么。在控制台中输入以下命令,并使用 id() 函数查看变量 a 和 b 的内存位置:
a = 1
id(a)
Out[2]: 1906901488
b = a
id(b)
Out[4]: 1906901488
Out 行显示了 id() 函数的输出。注意,a 和 b 都是指向相同对象的名称(一个值为 1 的整数)。如果你改变 a 指向的对象会发生什么?在下面的代码中,你重新分配变量名 a 以指向不同的对象:
a = 2
id(a)
Out[6]: 1906901520
id(b)
Out[7]: 1906901488
a
Out[8]: 2
b
Out[9]: 1
注意,名为 a 的变量现在指向一个完全不同的对象,具有不同的内存位置。但是这个操作并没有改变 b 指向的变量名,所以 b 指向的对象与之前相同的内存位置。
快速检查 28.1
你有一个名为 x 的变量,它指向一个不可变对象,x = "me"。运行 id(x) 会得到 2899205431680。对于以下每一行,确定该变量的 ID 是否与 id(x) 相同。假设这些行依次执行:
1
y = x # what is id(y)2
z = y # what is id(z)3
a = "me" # what is id(a)
28.1.2. 可变对象的别名
你可以在可变对象上执行与 28.1.1 节 中相同的命令序列,例如列表。在下面的代码中,你可以看到使用指向列表的变量名之间的赋值运算符的行为与不可变对象相同。在可变对象上使用赋值运算符不会创建副本;它创建了一个别名。别名 是对同一对象的另一个名称:
genius = ["einstein", "galileo"]
id(genius)
Out[9]: 2899318203976
smart = genius
id(smart)
Out[11]: 2899318203976
对象 genius 和 smart 指向的内存位置相同,因为它们指向同一个对象。图 28.1 展示了变量 smart 和 genius 如何指向同一个对象。
图 28.1. 在左侧面板中,你创建了指向列表 ["einstein", "galileo"] 的变量 genius。在右侧面板中,变量 smart 指向与 genius 相同的对象。

快速检查 28.2
你有一个名为 x 的变量,它指向一个可变对象,x = ["me", "I"]。运行 id(x) 会得到 2899318311816。对于以下每一行,确定该变量的 ID 是否与 id(x) 相同。假设这些行依次执行:
1
y = x # y 的 id 是什么2
z = y # z 的 id 是什么3
a = ["me", "I"] # a 的 id 是什么
可变对象和不可变对象之间的区别在下一组命令中很明显,当你修改列表时:
genius.append("shakespeare")
id(genius)
Out[13]: 2899318203976
id(smart)
Out[14]: 2899318203976
genius
Out[16]: ["einstein", "galileo", "shakespeare"]
smart
Out[15]: ["einstein", "galileo", "shakespeare"]
当你修改一个可变对象时,对象本身会发生变化。当你向 genius 指向的列表中添加值时,列表对象本身会发生变化。变量名 genius 和 smart 仍然指向相同的对象和相同的内存位置。变量名 smart 指向的对象也会发生变化(因为它指向与变量 genius 相同的东西)。这如图 28.2 所示。
图 28.2. 当通过变量 genius 修改列表对象 ["einstein", "galileo"] 时,变量 smart 也指向被修改的列表对象。

在可变列表之间使用等号意味着,如果你通过一个变量名修改列表,所有指向同一列表的其他变量名都将指向被修改的值。
快速检查 28.3
你有一个名为 x 的变量,它指向一个可变对象,x = ["me", "I"]。对于以下每个点,回答问题:
1
执行以下行之后,
x发生变化了吗?y = x x.append("myself")2
执行以下行之后,
x发生变化了吗?y = x y.pop()3
执行以下行之后,
x发生变化了吗?y = x y.append("myself")4
执行以下行之后,
x发生变化了吗?y = x y.sort()5
执行以下行之后,
x发生变化了吗?y = [x, x] y.append(x)
28.1.3. 可变对象作为函数参数
在 第 5 单元 中,你看到函数内部的变量与函数外部的变量是独立的。你可以在函数外部有一个名为 x 的变量,在函数内部有一个名为 x 的参数。由于作用域规则,它们不会相互干扰。当处理可变对象时,将可变对象作为实际参数传递给函数意味着实际参数将是别名。
列表 28.1 展示了实现函数的代码。该函数名为 add_word()。它的输入参数是一个字典、一个单词和一个定义。该函数会修改字典,使得即使在外部访问时,字典也包含新添加的单词。然后代码使用名为 words 的字典作为实际参数调用该函数。在函数调用中,名为 d 的形式参数现在是字典 words 的别名。在函数内部对 d 的任何修改都会反映在访问字典 words 时。
列表 28.1. 修改字典的函数
def add_word(d, word, definition): *1*
""" d, dict that maps strings to lists of strings
word, a string
definition, a string
Mutates d by adding the entry word:definition
If word is already in d, append definition to word's value list
Does not return anything
"""
if word in d: *2*
d[word].append(definition) *3*
else: *4*
d[word] = [definition] *5*
words = {} *6*
add_word(words, 'box', 'fight') *7*
print(words) *8*
add_word(words, 'box', 'container') *9*
print(words) *10*
add_word(words, 'ox', 'animal') *11*
print(words) *12*
-
1 接受字典、字符串(单词)和另一个字符串(定义)作为参数的函数
-
2 字典中的单词
-
3 将定义添加到键的值列表的末尾
-
4 字典中不存在的单词
-
5 使用一个单词作为键的值创建新的列表
-
6 函数外部;创建一个空字典
-
7 使用名为“words”的字典作为实际参数调用函数
-
8 打印
{‘box’: [‘fight’]} -
9 再次调用函数以向“box”键的值追加
-
10 打印
{‘box’: [‘fight’, ‘container’]} -
11 打印
{‘ox’: [‘animal’], ‘box’: [‘fight’, ‘container’]} -
12 再次调用函数以添加另一个条目
28.2. 可变对象的复制
当你想复制一个可变对象时,你需要使用一个函数来明确告诉 Python 你想要复制。有两种方法可以实现这一点:使用与另一个列表相同元素的新列表,或者使用一个函数。
28.2.1. 复制可变对象的命令
复制的一种方法是通过创建一个具有与另一个列表相同值的新列表对象。给定一个包含一些元素的列表 artists,以下命令创建了一个新的列表对象,并将变量名 painters 绑定到它:
painters = list(artists)
新的列表对象具有与 artists 相同的元素。例如,以下代码显示列表 painters 和 artists 指向的对象是不同的,因为修改一个不会修改另一个:
artists = ["monet", "picasso"]
painters = list(artists)
painters.append("van gogh")
painters
Out[24]: ["monet", "picasso", "van gogh"]
artists
Out[25]: ["monet", "picasso"]
另一种方法是使用 copy() 函数。如果 artists 是一个列表,以下命令创建了一个新对象,它具有与 artists 相同的元素,但被复制到新对象中:
painters = artists.copy()
以下代码展示了如何使用 copy 命令:
artists = ["monet", "picasso"]
painters = artists.copy()
painters.append("van gogh")
painters
Out[24]: ["monet", "picasso", "van gogh"]
artists
Out[25]: ["monet", "picasso"]
从控制台输出中,你可以看到 painters 和 artists 指向的列表对象是分开的,因为对一个对象的修改不会影响另一个对象。图 28.3 展示了复制操作的含义。
图 28.3. 在左侧面板中,复制对象 ["monet", "picasso"] 创建了一个具有相同元素的新对象。在右侧面板中,你可以修改一个对象而不会干扰另一个对象。

28.2.2. 获取排序列表的副本
你可以看到你可以对列表进行排序,使列表本身直接被修改。对于列表 L,命令是 L.sort()。在某些情况下,你可能希望保留原始列表并获取列表的排序副本,同时保持原始列表不变。
Python 有一个函数允许你在单行中完成复制列表并排序的操作。以下命令显示了一个返回列表排序版本并将其存储在另一个列表中的函数:
kid_ages = [2,1,4]
sorted_ages = sorted(kid_ages)
sorted_ages
Out[61]: [1, 2, 4]
kid_ages
Out[62]: [2, 1, 4]
你可以看到变量 sorted_ages 指向一个排序后的列表,但原始列表 kid_ages 保持不变。之前,当你写下命令 kid_ages.sort() 时,kid_ages 会改变,使其排序而不创建副本。
快速检查 28.4
编写一行代码以实现以下每个目标:
1
创建一个名为
order的变量,它是名为chaos的列表的排序副本2
对名为
colors的列表进行排序3
对名为
deck的列表进行排序,并将其别名设置为名为cards的变量
28.2.3. 在迭代可变对象时的注意事项
经常需要编写代码从可变对象中删除满足某些条件的项。例如,假设你有一个包含歌曲及其评分的字典。你想从字典中删除所有评分为 1 的歌曲。
列表 28.2 尝试(但失败)完成此操作。代码遍历字典中的每个键。它检查与该键关联的值是否为 1。如果是,它从字典中删除该键值对。代码运行失败并显示错误消息 RuntimeError: dictionary changed size during iteration。Python 不允许你在迭代字典时更改字典的大小。
列表 28.2. 在迭代字典时尝试从字典中删除元素
songs = {"Wannabe": 1, "Roar": 1, "Let It Be": 5, "Red Corvette": 4} *1*
for s in songs.keys(): *2*
if songs[s] == 1: *3*
songs.pop(s) *4*
-
1 首歌曲字典
-
2 遍历每一对
-
3 如果评分值为 1...
-
4 ...删除具有该值的歌曲
假设你尝试做同样的事情,但这次不是使用字典,而是使用列表。以下列表显示了你可以如何完成这个任务,但它也未能正确执行。这次代码没有失败。但它具有与预期不同的行为;它为 songs 提供了错误的值:[1, 5, 4] 而不是 [5, 4]。
列表 28.3. 尝试在遍历列表的同时删除元素
songs = [1, 1, 5, 4] *1*
for s in songs: *2*
if s == 1: *3*
songs.pop(s) *4*
print(songs) *5*
-
1 歌曲评分列表
-
2 遍历每个评分
-
3 如果评分值为 1...
-
4 ...删除具有该值的歌曲
-
5 打印 [1,5,4]
你可以看到在遍历列表的同时删除项目时的错误效果。循环到达索引 0 的元素,看到它是 1,并将其从列表中删除。现在列表是 [1, 5, 4]。接下来,循环查看索引 1 的元素。这个元素现在来自已更改的列表 [1, 5, 4],因此它查看数字 5。这个数字不等于 1,所以它没有删除它。然后它最终查看列表 [1, 5, 4] 中索引 2 的元素,数字 4。它也不等于 1,所以它保留它。这里的问题是,当你弹出你看到的第一个 1 时,你减少了列表的长度。现在索引计数比原始列表少一个。实际上,原始列表 [1, 1, 5, 4] 中的第二个 1 被跳过了。
如果你需要从列表中删除(或添加)项目,你首先需要创建一个副本。你可以遍历列表副本,然后在原始列表上重新开始,通过在遍历副本时添加你想要保留的项目。以下列表显示了如何修改 列表 28.3 中的代码以正确执行。这段代码不会引发错误,现在 songs 的正确值现在是 [5, 4]。
列表 28.4. 在遍历列表的同时正确删除元素的方法
songs = [1, 1, 5, 4] *1*
songs_copy = songs.copy() *2*
songs = [] *3*
for s in songs_copy: *4*
if s != 1: *5*
songs.append(s) *6*
print(songs) *7*
-
1 原始评分列表
-
2 创建对象的副本
-
3 将原始列表设置为空
-
4 遍历列表中的每个评分
-
5 如果评分需要保留...
-
6 ...将评分添加到原始列表中
-
7 打印 [5,4]
28.2.4. 为什么存在别名?
如果别名一个对象引入了意外更改你未打算更改的对象的问题,为什么一开始要使用别名?为什么不总是创建副本?所有 Python 对象都存储在计算机内存中。列表和字典是“重”对象,与整数或布尔值不同。例如,每次你进行函数调用时,创建副本可能会严重削弱程序,特别是当有大量函数调用时。如果你有一个包含美国所有人名的列表,每次你想添加新的人名时都要复制这个列表,这可能会很慢。
摘要
在本课中,我的目标是教你关于处理可变对象的细微差别。可变对象很有用,因为它们可以存储大量易于修改的数据。由于你处理的是包含许多元素的可变对象,因此每次操作都进行复制在计算机时间和空间效率上变得低效。默认情况下,Python 为对象提供别名,使用赋值运算符创建一个指向同一对象的新变量;这被称为 别名。Python 认识到在某些情况下,你可能想要创建一个可变对象的副本,并允许你明确地告诉它你想这样做。以下是主要收获:
-
Python 为所有对象类型提供了别名。
-
别名可变对象可能会导致意外的副作用。
-
通过一个别名修改可变对象会导致通过该对象的所有其他别名看到变化。
-
你可以通过创建一个新的对象并复制原始对象的所有元素来创建一个可变对象的副本。
让我们看看你是否理解了...
Q28.1
编写一个名为
invert_dict的函数,该函数接受一个字典作为输入。该函数返回一个新的字典;现在的值是原来的键,现在的键是原来的值。假设输入字典的值是不可变的且唯一的。Q28.2
编写一个名为
invert_dict_inplace的函数,该函数接受一个字典作为输入。该函数不返回任何内容。它修改传入的字典,使得值现在是原来的键,键现在是原来的值。假设输入字典的值是不可变的且唯一的。
第 29 课. 核心项目:文档相似度
在阅读第 29 课之后,你将能够
-
以两个文件作为输入并确定它们的相似度
-
通过使用函数编写有组织的代码
-
在实际环境中了解如何使用字典和列表。
两个句子、段落或文章有多相似?你可以编写一个程序,结合字典和列表来计算两篇作品的相似度。如果你是教师,你可以使用这个程序来检查论文提交之间的相似度。如果你正在修改自己的文档,你可以使用这个程序作为版本控制,比较文档的不同版本,以查看主要更改发生在哪里。
问题
你被提供了包含文本的两个文件。使用文件名,编写一个程序来读取文档,并使用一个指标来确定它们的相似度。完全相同的文档应得分为 1,没有任何共同单词的文档应得分为 0。
给定这个问题描述,你需要决定一些事情:
-
你是否计算文件中的标点符号或仅计算单词?
-
你关心文件中单词的顺序吗?如果两个文件有相同的单词但顺序不同,它们仍然是相同的吗?
-
你使用什么指标来为相似度分配数值?
这些是需要回答的重要问题,但当一个问题时,更重要的行动是将它分解成子任务。每个子任务将成为它自己的模块,或者用 Python 术语来说,是一个函数。
29.1. 将问题分解为任务
如果你重新阅读问题陈述,你可以看到存在一些自然划分,用于自包含的任务:
-
获取文件名,打开文件,并读取信息。
-
获取文件中的所有单词。
-
将每个单词映射到其出现的频率。让我们先同意,目前顺序不重要。
-
计算相似度。
注意到在分解任务时,你还没有对实现方式做出任何具体决定。你只是分解了你的原始问题。
像程序员一样思考
当思考如何分解你的问题时,选择并编写任务,使它们可以重用。例如,创建一个任务,读取文件名并返回文件内容,而不是读取确切两个文件名并返回它们的内容。想法是,读取一个文件名的函数更灵活,如果需要,你可以调用它两次(或更多次)。
29.2. 读取文件信息
第一步是编写一个函数,它接受一个文件名,读取内容,并以可用的形式返回内容。一个不错的选择是返回文件的所有内容作为一个(可能很大的)字符串。列表 29.1 展示了执行此操作的函数。它使用 Python 函数通过提供的文件名打开文件,将整个内容读入一个字符串,并返回该字符串。当函数被文件名调用时,它将返回包含所有文件内容的字符串。
列表 29.1. 读取文件
def read_text(filename):
""" *1*
filename: string, name of file to read *1*
returns: string, contains all file contents *1*
""" *1*
inFile = open(filename, 'r') *2*
line = inFile.read() *3*
return line *4*
text = read_text("sonnet18.txt") *5*
print(text)
-
1 文档字符串
-
2 使用文件名打开文件的 Python 函数
-
3 读取所有内容作为字符串的 Python 函数
-
4 返回字符串
-
5 函数调用
在你编写一个函数之后,你应该测试它,并在必要时进行调试。为了测试这个函数,你需要创建一个包含内容的文件。在这个课程中,在你的 .py 文件所在的同一文件夹中创建一个空的文本文档。将内容填充到文本文件中并保存;我使用了莎士比亚的“第 18 首十四行诗”。现在,在 .py 文件中,你可以通过以下方式调用函数
print(read_text("sonnet18.txt"))
当你运行文件时,控制台应该打印出文件的全部内容。
像程序员一样思考
编写函数的目的是让你的生活变得更简单。函数应该是自包含的代码块,你只需要调试一次,但可以多次重用。当你集成多个函数时,你需要调试的是它们如何交互,而不是调试函数本身。
29.3. 保存文件中的所有单词
现在你有一个返回包含文件所有内容的字符串的函数。一个巨大的字符串对计算机没有帮助。记住 Python 与对象一起工作,包含大量文本的大字符串是一个对象。你希望将这个大字符串分成几部分。如果你正在比较两个文档,字符串的自然分解是将它分成单词。
像程序员一样思考
面对一项任务时,你通常会需要决定使用哪种数据结构(类型)。在开始编码之前,考虑你学过的每个数据类型,并决定它是否是合适的。当有多种可能的工作方式时,选择最简单的一种。
这个将字符串分解的任务将通过一个函数来完成。它的输入是一个字符串。它的输出可以是许多种东西之一。随着编码实践的增多,你将更快地认识到何时使用某些对象类型以及为什么。在这种情况下,你将把字符串中的所有单词分离成一个列表,其中每个单词都是列表中的一个元素。列表 29.2 展示了代码。它首先进行一些清理,将换行符替换为空格,并移除所有特殊字符。表达式string.punctuation本身也是一个字符串,其值是字符串对象可能具有的所有标点符号的集合:
"!#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~
在文本清理完毕后,你使用split操作在空格字符上拆分字符串,并返回一个包含所有单词的列表(因为所有单词都由空格分隔)。
列表 29.2. 从字符串中查找单词
import string *1*
def find_words(text):
"""
text: string
returns: list of words from input text
"""
text = text.replace("\n", " ") *2*
for char in string.punctuation: *3*
text = text.replace(char, "") *4*
words = text.split(" ") *5*
return words *6*
words = find_words(text) *7*
-
1 引入与字符串相关的函数
-
2 将换行符替换为空格
-
3 使用字符串中的预设标点符号
-
4 将标点符号替换为空字符串
-
5 使用空格分隔符制作所有单词的列表
-
6 返回单词列表
-
7 函数调用
像程序员一样思考
在对大型输入文件运行函数之前,先在包含几个单词的小测试文件上尝试。这样,如果出了问题,你不必查看数百行代码来找出问题所在。
你在 sonnet18.txt 文本文件上运行此函数:
Shall I compare thee to a summer's day?
Thou art more lovely and more temperate:
Rough winds do shake the darling buds of May,
And summer's lease hath all too short a date:
Sometime too hot the eye of heaven shines,
And often is his gold complexion dimmed,
And every fair from fair sometime declines,
By chance, or nature's changing course untrimmed:
But thy eternal summer shall not fade,
Nor lose possession of that fair thou ow'st,
Nor shall death brag thou wander'st in his shade,
When in eternal lines to time thou grow'st,
So long as men can breathe, or eyes can see,
So long lives this, and this gives life to thee.
如果你输入以下代码,你将在控制台得到所有单词的列表:
print(find_words(text))
这会为 sonnet18.txt 打印以下列表:
['Shall', 'I', 'compare', ... LIST TRUNCATED ..., 'life', 'to', 'thee']
29.4. 将单词映射到它们的频率
现在你已经有一个单词列表,你有一个 Python 对象,你可以更深入地分析其内容。在这个阶段,你应该在考虑如何找到两个文档之间的相似度。至少,你可能想知道文档中每个单词的数量。
注意,当你创建单词列表时,列表包含了从原始字符串中按顺序排列的所有单词。如果有重复的单词,它们会被添加为另一个列表元素。为了给你更多关于单词的信息,你希望将每个单词与其出现的频率配对。希望“配对”这个短语让你相信 Python 字典将是一个合适的数据结构。在这种情况下,你将构建一个频率字典。以下列表显示了完成此任务的代码。
列表 29.3. 为单词制作频率字典
def frequencies(words):
"""
words: list of words
returns: frequency dictionary for input words
"""
freq_dict = {} *1*
for word in words: *2*
if word in freq_dict: *3*
freq_dict[word] += 1 *4*
else: *5*
freq_dict[word] = 1 *6*
return freq_dict *7*
freq_dict = frequencies(words) *8*
-
1 初始空字典
-
2 查看列表中的每个单词
-
3 如果单词已在字典中...
-
4 ...将其计数加一
-
5 字典中尚无此单词
-
6 添加单词并将其计数设置为 1
-
7 返回字典
-
8 函数调用
频率字典是字典在此问题中的一个有用应用。它将一个单词映射到你在文本中看到它的次数。当你比较两个文档时,你可以使用这些信息。
29.5. 通过相似度分数比较两个文档
现在你必须决定使用哪个公式来比较两个文档,给定每个单词出现的次数。首先,公式不需要太复杂。作为一个初始步骤,你可以使用一个简单的指标来进行比较,看看效果如何。假设以下步骤将通过每个单词的运行总和来计算分数:
-
在两个频率字典中查找一个单词(每个文档一个)。
-
如果它在两个中都有,添加计数的差异。如果它只出现在其中一个中,添加那个计数的值(实际上添加了一个字典中的计数与另一个字典中的 0 之间的差异)。
-
分数是总差异与两个文档中单词总数的比值。
在提出一个度量标准后,进行合理性检查是很重要的。如果文档完全相同,两个频率字典中所有单词计数的差异为 0。将这个值除以两个字典中单词的总数得到 0。如果文档没有共同的单词,差异的总和将是“一个文档中的总单词数”加上“另一个文档中的总单词数”。将这个值除以两个文档中单词的总数得到一个比率为 1。这些比率是有意义的,除了你希望完全相同的文档具有比率为 1,而完全不同的文档具有比率为 0。为了解决这个问题,从 1 中减去比率。
列表 29.4 显示了给定两个输入字典计算相似度的代码。代码遍历一个字典的键;哪个都无关紧要,因为你将在另一个循环中遍历另一个字典。
当你遍历一个字典的键时,检查该键是否也在另一个字典中。回想一下,你正在查看每个键的值;值是单词在一个文本中出现的次数。如果单词在两个字典中,取两个频率计数的差异。如果它不在,取存在于其中一个字典中的计数。
在你完成遍历一个字典后,再遍历另一个字典。你不再需要查看两个字典值之间的差异,因为你已经之前计算过。现在你只是查看另一个字典中是否有任何单词不在第一个字典中。如果有,将它们的计数相加。
最后,当你有了差异的总和时,将其除以两个字典中单词的总数。取这个值的 1 减去,以匹配原始问题的评分规范。
列表 29.4. 给定两个输入字典计算相似度
def calculate_similarity(dict1, dict2):
"""
dict1: frequency dictionary for one text
dict2: frequency dictionary for another text
returns: float, representing how similar both texts are to each other
"""
diff = 0
total = 0
for word in dict1.keys(): *1*
if word in dict2.keys(): *2*
diff += abs(dict1[word] - dict2[word]) *3*
else: *4*
diff += dict1[word] *5*
for word in dict2.keys(): *6*
if word not in dict1.keys(): *7*
diff += dict2[word] *8*
total = sum(dict1.values()) + sum(dict2.values()) *9*
difference = diff / total *10*
similar = 1.0 – difference *11*
return round(similar, 2) *12*
-
1 遍历一个字典中的单词
-
2 单词同时出现在两个字典中
-
3 添加频率差异
-
4 单词未出现在另一个字典中
-
5 添加整个频率
-
6 遍历另一个字典中的单词
-
7 在两个字典中都计算过的单词;只查看不在 dict1 中的单词
-
8 添加整个频率
-
9 两个字典中的总单词数
-
10 将差异除以总单词数
-
11 从 1 中减去差值
-
12 四舍五入到两位小数并返回介于 0 和 1 之间的分数
函数返回一个介于 0 和 1 之间的浮点数。数字越低,文档越不相似,反之亦然。
29.6. 将所有内容组合在一起
最后一步是在文本文件上测试代码。在将你的程序用于两个单独的文件之前,先进行一次合理性检查:首先,使用相同的文件作为两个文本来检查你得到的评分是否为 1.0,然后使用十四行诗文件和一个空文件作为另一个来检查你得到的评分是否为 0.0。
现在,使用莎士比亚的“十四行诗 18”和“十四行诗 19”来测试两篇作品,然后修改“十四行诗 18”,将单词“夏天”改为“冬天”,以查看程序是否认为它们几乎完全相同。
“十四行诗 18”的文本之前已经展示过。以下是“十四行诗 19”的文本:
Devouring Time, blunt thou the lion's paws,
And make the earth devour her own sweet brood;
Pluck the keen teeth from the fierce tiger's jaws,
And burn the long-lived phoenix in her blood;
Make glad and sorry seasons as thou fleet'st,
And do whate'er thou wilt, swift-footed Time,
To the wide world and all her fading sweets;
But I forbid thee one most heinous crime:
O! carve not with thy hours my love's fair brow,
Nor draw no lines there with thine antique pen;
Him in thy course untainted do allow
For beauty's pattern to succeeding men.
Yet, do thy worst old Time: despite thy wrong,
My love shall in my verse ever live young.
以下列表打开两个文件,读取它们的单词,创建频率字典,并计算它们的相似度。
列表 29.5. 运行文档相似度程序的代码
text_1 = read_text("sonnet18.txt")
text_2 = read_text("sonnet19.txt")
words_1 = find_words(text_1)
words_2 = find_words(text_2)
freq_dict_1 = frequencies(words_1)
freq_dict_2 = frequencies(words_2)
print(calculate_similarity(freq_dict_1, freq_dict_2))
当我在“十四行诗 18”和“十四行诗 19”上运行程序时,相似度评分为 0.24。它们是两篇不同的作品,所以得分接近 0 是有道理的。当我将程序运行在“十四行诗 18”和我修改过的“十四行诗 18”(将三个“夏天”的实例改为“冬天”)上时,评分为 0.97。这也是有道理的,因为这两篇作品几乎相同。
29.7. 一种可能的扩展
你可以通过查看单词对而不是单个单词来使你的程序更健壮。在你将文件作为字符串读取之后,查看单词对,称为“二元组”,并将它们保存在列表中。查看二元组而不是单词可以提高你的程序,因为单词对在语言中通常能更好地指示相似度。这可能导致更准确的设置和更好的书面文本模型。如果你想的话,你还可以在计算相似度评分时使用二元组和单词的混合。
摘要
在本节课中,我的目标是教你如何编写一个程序,该程序读取两个文件,将它们的内容转换为字符串,使用列表存储文件中的所有单词,然后创建一个频率字典来存储每个单词及其在文件中出现的次数。你通过比较两个频率字典中单词计数的差异来得出文件相似度的评分。以下是主要收获:
-
你通过使用可重用的函数编写了模块化代码。
-
你使用列表来存储单个元素。
-
你使用字典将一个单词映射到其计数。
第 7 单元:通过面向对象编程创建自己的对象类型
在前面的单元中,你使用了各种 Python 对象类型。你编写了创建多个不同类型和相同类型对象的程序。你的对象通过相互交换信息并共同完成任务来实现某种特定任务。
在本单元中,你将学习如何创建自己的对象类型。一个对象由两个属性定义:一组属性和一组行为。例如,整数有一个属性,即一个整数。整数的操作集包括你可以在整数上执行的所有操作(加、减、取绝对值等等)。对象类型为程序员提供了一种将属性和行为打包在一起的方法,并允许你创建自己定制的对象类型,以便在程序中使用。
在综合项目中,你将编写一个模拟玩扑克牌游戏“战争”的程序。这是一个两人游戏,使用一副牌。每位玩家轮流翻牌;牌面较大的玩家获胜并将牌交给另一玩家。当牌堆没有更多牌时,游戏结束。你将创建两种新的对象类型,一个用于表示玩游戏的角色,另一个用于表示牌堆。你将决定每个对象类型将具有哪些属性和行为,然后使用你的对象类型来玩游戏。
第 30 课. 创建自己的对象类型
在阅读第 30 课后,你将能够
-
理解一个对象具有属性
-
理解一个对象与其关联的操作
-
理解在处理对象时点符号的含义
你在日常生活中一直在使用对象。你使用电脑和手机,处理盒子信封,与人交往和动物互动。甚至数字和单词都是基本对象。
你使用的每个对象都是由其他对象组成的。除了物质的基本构建块,你与之交互的每个对象都可以分解成更小的对象。例如,你的计算器可以分解成几个基本组件:逻辑芯片、屏幕和按钮(以及每个组件都可以进一步分解成更小的组件)。甚至一个句子也可以分解成按一定顺序排列的单独单词。
你与之交互的每个对象都有一定的行为。例如,一个基本的计算器可以进行数学运算,但不能检查电子邮件。计算器已被编程,根据按下的哪个键或按钮以某种方式工作。不同语言的单词可以按照语言的规则以不同的方式排列,以形成有意义的句子。
当你构建复杂系统时,你可以重用你已经构建的对象,而不必回到物质的基本构建块。例如,电脑可能有一个计算器已经有的相同逻辑芯片,用于基本的算术运算。除此之外,电脑可能还内置了允许它访问互联网或显示彩色图形的组件。
同样的概念可以应用于编程!你可以创建更复杂的对象类型来在你的程序中使用,这些类型由其他对象类型组成。实际上,你可能已经注意到列表和字典是由其他对象类型组成的对象类型:列表包含一组对象,字典包含一组对象的成对。
考虑这一点
这里有一些两个对象的属性和行为。你能区分属性和行为吗?这些对象是什么?
-
两只眼睛
-
在键盘上睡觉
-
没有眼睛
-
任何颜色
-
抓挠
-
弹跳
-
皮毛
-
圆形
-
滚动
-
隐藏
-
四条腿
答案:
-
一只猫。
-
特征:两只眼睛,皮毛,四条腿
-
行为:在键盘上睡觉,抓挠,隐藏
-
一个球。
-
特征:没有眼睛,圆形,任何颜色
-
行为:弹跳,滚动
30.1. 为什么你需要新的对象类型?
自从你写下第一行代码开始,你就一直在使用对象类型。整数、浮点数、字符串、布尔值、元组、列表和字典都是对象类型。它们是 Python 语言内建的对象,这意味着当你启动 Python 时,它们默认可用。当你使用列表(和字典)时,你可能已经注意到它们是由其他对象类型组成的对象类型。例如,列表L = [1,2,3]是由整数组成的列表。
整数、浮点数和布尔值是原子对象,因为它们不能被分解成更小的对象类型;这些类型是 Python 语言的基本构建块。字符串、元组、列表和字典是非原子对象,因为它们可以被分解成其他对象。
使用不同的对象类型有助于组织你的代码并使其更易于阅读。想象一下,如果你只能使用原子数据类型,代码会多么混乱。如果你想要编写包含你的购物清单的代码,你可能需要为列表中的每个条目创建一个字符串变量。这会使你的程序很快变得杂乱无章。当你意识到有更多项目需要添加时,你必须创建变量。
随着你继续构建更复杂的程序,你会发现你需要创建自己的对象类型。这些对象类型“保存”了一组属性和一组行为在这个新类型的对象下。属性和行为是作为程序员的你需要决定和定义的事情。当你构建程序时,你可以从其他类型创建新的对象类型,甚至是你自己创建的类型。
快速检查 30.1
对于以下每个场景,你是否需要创建一个新的对象类型,或者你可以用你已知的对象类型来表示它?
1
某个人的年龄
2
一组地图点的纬度和经度
3
一个人
4
椅子
30.2. 对象由什么组成?
对象类型由两件事定义:一组属性和一组行为。
30.2.1. 对象属性
对象类型 属性 是定义你的对象的数据。你可以使用哪些特征来解释你对象的“外观”?
假设你想创建一个表示汽车的物体类型。哪些数据可以描述一个通用的汽车?作为汽车类型的创建者,你可以决定多少或多少数据定义了通用的汽车。数据可以是长度、宽度、高度或门数。
在你决定了特定对象类型的属性之后,这些选择将定义你的类型,并将固定不变。当你开始向你的类型添加行为时,你可以操作这些属性。
这里有一些对象类型属性的更多示例。如果你有一个圆形类型,它的数据可能是它的半径。如果你有一个“地图上的点”类型,数据可能是纬度和经度的值。如果你有一个房间类型,它的数据可能是它的长度、宽度、高度、里面的物品数量以及是否有占用者。
快速检查 30.2
你可以使用哪些适当的数据来表示以下每种类型?
1
矩形
2
电视
3
椅子
4
人员
30.2.2. 对象行为
对象类型的行为是定义你的对象的操作。人们可以通过哪些方式与你的类型互动?
让我们回到通用的汽车类型。人们如何与汽车互动呢?再次强调,作为汽车对象的创造者,你将决定允许人们以多少种方式与之互动。汽车的行为可能包括改变汽车的颜色、让汽车发出声音,或者让汽车的车轮转动。
这些操作是此类对象(仅此类对象)可以执行的动作。这些可以是对象本身执行的动作,或者对象与其他对象交互的方式。
其他对象类型是如何表现的?对于一个圆,一个动作可能是获取它的面积或周长。对于地图上的一个点,一个动作可能是获取它所在的国度和另一个动作可能是获取两点之间的距离。对于房间,一个动作可能是添加一个物品,这将增加物品计数 1,或者移除一个物品以减少物品计数,另一个动作可能是获取房间的体积。
快速检查 30.3
对于以下每个对象类型,你可以添加哪些适当的行为?
1
矩形
2
电视
3
椅子
4
人
30.3. 使用点表示法
你已经对对象类型有了概念。对象类型具有属性和操作。以下是一些你已经使用过的对象类型:
-
整数是一个整数。它的操作包括加法、减法、乘法、除法、转换为浮点数以及许多其他操作。
-
字符串是一系列字符。它的操作包括加法、索引、切片、查找子字符串、用另一个子字符串替换子字符串以及许多其他操作。
-
字典有一个键、一个值和一个将键映射到内存位置以放置值的公式。它的操作包括获取所有键、获取所有值、使用键进行索引以及许多其他操作。
属性和行为是为特定对象类型定义的,并且属于该对象类型;其他对象类型不知道它们。
在第 7 课中,你曾在字符串上使用点表示法。点表示法表示你正在访问特定对象类型的数据或行为。当你使用点表示法时,你向 Python 表明你想要对一个特定对象类型执行特定操作或访问其特定属性。Python 知道如何推断运行此操作的对象类型,因为你在一个对象上使用了点表示法。例如,当你创建了一个名为L的列表时,你使用L.append()向列表中添加了一个项目。点表示法引导 Python 查看操作append被应用到的对象L。Python 知道L是list类型,并检查list对象类型是否定义了名为append的操作。
快速检查 30.4
在以下点表示法的示例中,操作是在哪种对象类型上进行的?
1
"wow".replace("o", "a")2
[1,2,3].append(4)3
{1:1, 2:2}.keys()4
len("lalala")
概述
在本课中,我的目标是教会你对象类型由两件事表示:其数据属性和其行为。你已经使用了内置的 Python 对象,甚至看到了点表示法被用于更复杂类型,包括字符串、列表和字典。以下是主要收获:
-
一个对象类型具有数据属性:构成该类型的其他对象。
-
一个对象类型具有行为:允许与该类型对象交互的操作。
-
同类型的对象了解定义它们的属性和行为。
-
点表示法用于访问对象的数据和操作。
第 31 课. 为对象类型创建类
在阅读第 31 课之后,你将能够
-
定义 Python 类
-
定义类的数据属性
-
定义类操作
-
使用类创建该类型的对象并执行操作
你可以创建自己的对象类型来满足你程序的需求。除了原子对象类型(int,float,bool),你创建的任何对象都是由其他预存在的对象组成的。作为实现新对象类型的人,你可以定义组成对象的属性以及你将允许对象拥有的行为(无论是独立拥有还是与其他对象交互时)。
你通常定义自己的对象是为了拥有定制的属性和行为,这样你就可以重用它们。在本课中,你将从两个角度来审视你编写的代码,就像你编写自己的函数时一样。你将把自己从新对象类型的程序员/编写者以及新创建对象类型的程序员/使用者中分离出来。
在使用类定义对象类型之前,你应该对如何实现它有一个大致的了解,通过回答两个问题:
-
你的对象由什么组成(它的特征,或属性)?
-
你想让你的对象做什么(它的行为,或操作)?
考虑这一点
列表和整数是两种类型的对象。列举一些你可以进行的操作
-
在一个列表上
-
在一个或多个数字上
你是否注意到你在每个对象上可以进行的多数操作之间有什么不同?
答案:
-
Append, extend, pop, index, remove, in -
+, -, *, /, %, negate, convert to a string
列表上的大多数操作都使用点符号进行,但数字上的操作使用数学符号。
31.1. 通过类实现新对象类型
创建你自己的对象类型的第一个部分是定义类。你使用class关键字来完成这个操作。你可能想要创建的一个简单对象类型是表示圆的对象。你告诉 Python 你想要通过类来定义一个新的对象类型。考虑以下行:
class Circle(object):
关键字class开始定义。单词Circle既是你的类名也是你想要定义的对象类型的名称。在括号中,单词object意味着你的类将是一个 Python 对象。你定义的所有类都将成为 Python 对象。因此,使用你的类创建的对象将继承任何 Python 对象都具有的所有基本行为和功能——例如,通过使用赋值运算符将变量绑定到你的对象上。
快速检查 31.1
编写一行代码来定义以下对象的类:
1
一个人
2
一辆车
3
一台计算机
31.2. 数据属性作为对象属性
在开始定义类之后,你必须决定你的对象将如何初始化。大部分情况下,这涉及到决定你将如何表示你的对象以及定义它的数据。你将初始化这些对象。对象属性被称为对象的 数据属性。
31.2.1. 使用 init 初始化对象
要初始化你的对象,你必须实现一个特殊操作,即 __init__ 操作(注意 init 前后的双下划线):
class Circle(object):
def __init__(self):
# code here
__init__ 的定义看起来像函数,但它是在类内部定义的。任何在类内部定义的函数都被称为 方法。
定义
方法是在类内部定义的函数,它定义了你可以对该类型的对象执行的操作。
__init__ 方法内部的代码通常初始化定义对象的属性数据。你决定你的圆类在首次创建时初始化一个半径为 0 的圆。
31.2.2. 在 init 中创建对象属性
一个对象的数据属性是另一个对象。你的对象可能由多个数据属性定义。为了告诉 Python 你想要定义对象的数据属性,你使用一个名为 self 的变量,并在其后加上一个点。在 Circle 类中,你初始化一个半径作为圆的数据属性,并将其初始化为 0:
class Circle(object):
def __init__(self):
self.radius = 0
注意在 __init__ 的定义中,你使用了一个名为 self 的参数。然后,在方法内部,你使用 self. 来设置圆的数据属性。变量 self 用于告诉 Python,你将使用这个变量来引用你创建的任何 Circle 类型的对象。你创建的任何圆都将通过 self.radius 访问其自己的半径。在这个阶段,请注意你仍在定义类,还没有创建任何特定的对象。你可以将 self 视为任何 Circle 类型对象的占位符变量。
在 __init__ 中,你使用 self.radius 来告诉 Python,变量 radius 属于 Circle 类型的对象。你创建的任何 Circle 类型的对象都将有一个名为 radius 的变量,其值可以在对象之间不同。使用 self. 定义的每个变量都引用对象的数据属性。
快速检查 31.2
Q1:
编写一个
__init__方法,包含以下场景的数据属性初始化:
- 一个人
- 一辆车
- 一台计算机
31.3. 方法作为对象操作和行为
你的对象具有通过在对象上或与对象一起执行的操作定义的行为。你通过方法实现操作。对于一个圆,你可以通过编写另一个方法来更改其半径:
class Circle(object):
def __init__(self):
self.radius = 0
def change_radius(self, radius):
self.radius = radius
方法看起来像函数。与 __init__ 方法一样,你将 self 作为方法的第一个参数。方法定义表明这是一个名为 change_radius 的方法,它接受一个名为 radius 的参数。
方法内部只有一行。因为你想要修改类的数据属性,所以在方法内部使用 self. 来访问半径并改变它的值。
圆形对象还有另一个行为,就是告诉你它的半径:
class Circle(object):
def __init__(self):
self.radius = 0
def change_radius(self, radius):
self.radius = radius
def get_radius(self):
return self.radius
同样,这也是一个方法,它除了 self 之外没有其他参数。它所做的只是返回其数据属性 radius 的值。和之前一样,你使用 self 来访问数据属性。
快速检查 31.3
Q1:
假设你创建了一个具有以下初始化方法的
Door对象类型:class Door(object): def __init__(self): self.width = 1 self.height = 1 self.open = False
- 编写一个返回门是否打开的方法。
- 编写一个返回门面积的方法。
31.4. 使用你定义的对象类型
每次你创建一个对象时,你已经在使用别人编写的对象类型:例如,int = 3 或 L = []。这些是简写符号,而不是使用类的名称。
在 Python 中,以下两个是等价的:L = [] 和 L = list()。在这里,list 是某人实现供他人使用的 list 类的名称。
现在,你可以用你自己的对象类型做同样的事情。对于 Circle 类,你可以按照以下方式创建一个新的 Circle 对象:
one_circle = Circle()
我们说变量 one_circle 被绑定到一个 Circle 类的实例对象上。换句话说,one_circle 是一个 Circle。
定义
实例是特定对象类型的特定对象。
你可以通过调用类名并将新对象绑定到另一个变量名来创建任意多个实例:
one_circle = Circle()
another_circle = Circle()
在创建类的实例之后,你可以在对象上执行操作。在一个 Circle 实例上,你可以执行的操作只有两个:更改其半径或让对象告诉你它的半径。
请记住,点符号表示操作作用于特定的对象。例如,
one_circle.change_radius(4)
注意,你向这个函数传递了一个实际参数(4),而定义中有两个形式参数(self 和 radius)。Python 总是自动将 self 的值分配为调用方法的对象(在这种情况下是 one_circle)。调用方法的对象是点号之前的对象。此代码只更改了名为 one_circle 的这个实例的半径,将其更改为 4。程序中可能创建的所有其他对象实例保持不变。假设你像下面这样请求半径值:
print(one_circle.get_radius())
print(another_circle.get_radius())
这将打印以下内容:
4
0
在这里,one_circle 的半径被更改为 4,但你没有更改 another_circle 的半径。你怎么知道这一点?因为圆的半径是一个数据属性,并且使用 self 定义。
这在 图 31.1 中显示:每个对象都有自己的半径数据属性,更改一个不会影响另一个。
图 31.1. 左边是两个圆对象的数据属性。在右边,你可以看到在使用点符号更改其值后,一个数据属性发生了变化。

快速检查 31.4
Q1:
假设你以以下方式创建一个
Door对象类型:class Door(object): def __init__(self): self.width = 1 self.height = 1 self.open = False def change_state(self): self.open = not self.open def scale(self, factor): self.height *= factor self.width *= factor
- 编写一行代码以创建一个新的
Door对象并将其绑定到名为square_door的变量。- 编写一行代码以改变
square_door的状态。- 编写一行代码以将门缩放到原来的三倍大小。
31.5. 在 init 中使用参数创建类
现在,你想要创建另一个类来表示一个矩形。以下列表显示了代码。
列表 31.1. 一个Rectangle类
class Rectangle(object):
""" a rectangle object with a length and a width """
def __init__(self, length, width):
self.length = length
self.width = width
def set_length(self, length):
self.length = length
def set_width(self, width):
self.width = width
这段代码提出了两个新的想法。首先,在__init__中除了self之外还有两个参数。当你创建一个新的Rectangle对象时,你必须用两个值初始化它:一个用于长度,一个用于宽度。
你可以这样做到:
a_rectangle = Rectangle(2,4)
假设你没有输入两个参数并这样做:
bad_rectangle = Rectangle(2)
然后,当你初始化对象时,Python 会给出一个错误,表示它期望两个参数,但你只提供了一个:
TypeError: __init__() missing 1 required positional argument: 'width'
在这个__init__中要注意的另一件事是参数和数据属性具有相同的名称。它们不必相同,但通常是这样的。当你想要使用类方法访问对象属性的值时,只有属性名称才是重要的。方法参数是形式参数,用于在初始化对象时传递数据,它们是临时的;它们在方法调用结束时结束,而数据属性在整个对象实例的生命周期中持续存在。
31.6. 在类名上使用点表示法,而不是在对象上
你一直通过省略self参数并让 Python 自动决定self的值来初始化和使用对象。这是 Python 的一个很好的特性,它允许程序员编写更简洁的代码。
在代码中,有一种更明确的方式来做到这一点,即直接为self提供一个参数,而不依赖于 Python 检测它应该是什么。
回到你定义的Circle类,你可以再次初始化一个对象,设置半径,并按以下方式打印半径:
c = Circle()
c.change_radius(2)
r = c.get_radius()
print(r)
在初始化对象之后,对对象进行操作的一种更明确的方式是使用类名和对象直接,如下所示:
c = Circle()
Circle.change_radius(c, 2)
r = Circle.get_radius(c)
print(r)
注意到你在类名上调用方法。此外,你现在正在将两个参数传递给change_radius:
-
c是你想要进行操作的物体,并分配给self。 -
2是新半径的值。
如果你直接在对象上调用方法,例如c.change_radius(2),Python 知道self的参数应该是c,推断出c是Circle类型的对象,并将背后的代码转换为Circle.change_radius(c, 2)。
快速检查 31.5
Q1:
你有以下几行代码。将标记的代码转换为使用显式调用方法的方式(在类名上使用点表示法):
a = Rectangle(1,1) b = Rectangle(1,1) a.set_length(4) # change this b.set_width(4) # change this
摘要
在本课中,我的目标是教你如何在 Python 中定义一个类。以下是主要收获:
-
类定义了一个对象类型。
-
类定义了数据属性(属性)和方法(操作)。
-
self是一个传统上用于引用对象类型的通用实例的变量名。 -
__init__方法是一种特殊操作,用于定义如何初始化一个对象。当创建对象时会被调用。 -
你可以定义其他方法(例如,类内的函数)来执行其他操作。
-
在使用类时,对象上的点符号用于访问数据属性和方法。
让我们看看你是否掌握了这个...
Q31.1
为圆类编写一个名为
get_area的方法。它通过使用公式 3.14 * radius² 返回圆的面积。通过创建一个对象并打印方法调用的结果来测试你的方法。Q31.2
为
Rectangle类编写两个名为get_area和get_perimeter的方法。通过创建一个对象并打印方法调用的结果来测试你的方法:
get_area通过使用公式 length * width 返回矩形的面积。get_perimeter通过计算 2 * length + 2 * width 返回矩形的周长。
32. 与你自己的对象类型一起工作
在阅读第 32 课之后,你将能够
-
定义一个类来模拟栈
-
使用你定义的其他对象类
到目前为止,你知道如何创建一个类。正式来说,一个类在 Python 中代表一个对象类型。你为什么要首先创建自己的对象类型呢?因为对象类型将一组属性和一组行为打包在一个数据结构中。有了这个很好地打包的数据结构,你知道所有承担这种类型的对象在定义它们的属性集合中是一致的,并且在它们可以执行的操作集合中也是一致的。
对象类型背后的有用想法是,你可以基于你创建的对象类型构建更复杂的对象。
考虑这一点
将以下每个对象细分到更小的对象,然后继续细分,直到你可以使用内置类型(int、float、string、bool)定义最小的对象:
-
雪花
-
森林
答案:
-
雪是由雪花组成的。雪花有六个面,由晶体组成。晶体是由按一定配置排列的水分子组成的(一个列表)。
-
森林由树木组成。一棵树有一个树干和叶子。树干有长度(float)和直径(float)。叶子有颜色(string)。
32.1. 定义栈对象
在第 26 课中,你使用了列表以及一系列的追加和弹出操作来实现煎饼栈。在你执行操作时,你小心地确保操作符合栈的行为:在列表末尾添加并从列表末尾移除。
使用类,你可以创建一个栈对象,该对象为你强制执行栈规则,这样你就不需要在程序运行时跟踪它们。
像程序员一样思考
使用类,你可以隐藏类的实现细节,对于使用类的人来说。你不需要详细说明你将如何做某事,只需说明你希望有某些行为;例如,在栈中你可以添加/移除项目。这些行为的实现可以以各种方式完成,而这些细节对于理解对象是什么以及如何使用它并不是必要的。
32.1.1. 选择数据属性
你将栈对象类型命名为Stack。第一步是决定如何表示栈。在第 26 课中,你使用列表来模拟栈,所以使用一个属性来表示栈是有意义的:一个列表。
像程序员一样思考
在决定哪些数据属性应该代表对象类型时,做以下两件事之一可能会有所帮助:
-
列出你所知道的数据类型,并考虑每种类型是否适合使用。请记住,一个对象类型可以由多个数据属性表示。
-
从你希望对象具有的行为开始。通常,你可以通过注意到你想要的行为可以用一个或多个你已知的已知的数结构来表示,来决定数据属性。
你通常在类的初始化方法中定义数据属性:
class Stack(object):
def __init__( self):
self.stack = []
栈将使用列表来表示。你可以决定最初栈是空的,因此使用 self.stack = [] 初始化 Stack 对象的数据属性。
32.1.2. 实现方法
在决定定义对象类型的数据属性之后,你需要决定你的对象类型将具有哪些行为。你应该决定你希望你的对象如何表现,以及希望使用你的类的其他人如何与之交互。
列表 32.1 提供了 Stack 类的完整定义。除了初始化方法外,还有其他七个方法定义了你可以与栈类型对象交互的方式。get_stack_elements 方法返回数据属性的副本,以防止用户修改数据属性。
add_one 和 remove_one 方法与栈的行为一致;你在列表的一端添加元素,并从同一端移除。同样,add_many 和 remove_many 方法多次添加和移除一定数量的元素,也是从同一端。size 方法返回栈中元素的数量。最后,prettyprint_stack 方法将栈中的每个元素打印(因此返回 None)到一行上,较新的元素在顶部。
列表 32.1. Stack 类的定义
class Stack(object):
def __init__( self):
self.stack = [] *1*
def get_stack_elements(self):
return self.stack.copy() *2*
def add_one(self , item):
self.stack.append(item) *3*
def add_many(self , item, n):
for i in range(n): *4*
self.stack.append(item) *4*
def remove_one(self):
self.stack.pop() *5*
def remove_many(self , n):
for i in range(n): *6*
self.stack.pop() *6*
def size(self):
return len(self.stack) *7*
def prettyprint(self):
for thing in self.stack[::-1]: *8*
print('|_',thing, '_|') *8*
-
1 列表数据属性定义了栈。
-
2 返回表示栈的数据属性的副本
-
3 向栈中添加一个项目的方 法;将其添加到列表的末尾
-
4 向栈中添加 n 个相同项目的方 法
-
5 从栈中移除一个项目的方 法
-
6 从栈中移除 n 个项目的方 法
-
7 告诉你栈中项目数量的方 法
-
8 打印一个栈,每个项目在一行上,较新的项目在顶部
有一点需要注意。在栈的实现中,你决定从列表的末尾添加和移除元素。一个同样有效的设计决策可能是从列表的起始位置添加和移除。请注意,只要你的决策和你要尝试实现的对象的行为保持一致,可能存在多种实现方式。
快速检查 32.1
Q1:
为
Stack对象编写一个名为add_list的方法,该方法接受一个列表作为参数。列表中的每个元素都添加到栈中,列表开头的元素首先添加到栈中。
32.2. 使用栈对象
现在你已经使用 Python 类定义了一个 Stack 对象类型,你可以开始创建 Stack 对象并使用它们进行操作。
32.2.1. 制作煎饼堆
你首先解决的是向你的栈中添加松饼的传统任务。假设一个松饼由表示松饼风味的字符串定义:"chocolate" 或 "blueberry"。
第一步是创建一个栈对象,你将在其中添加你的松饼。列表 32.2 展示了简单的命令序列:
-
通过初始化一个
Stack对象来创建一个空栈。 -
通过在栈上调用
add_one来添加一个蓝莓松饼。 -
通过在栈上调用
add_many方法来添加四个巧克力松饼。
添加到栈中的项目是表示松饼风味的字符串。你调用的所有方法都是在你创建的对象上,使用点符号。
列表 32.2. 创建 Stack 对象并向其中添加松饼
pancakes = Stack() *1*
pancakes.add_one("blueberry") *2*
pancakes.add_many("chocolate", 4) *3*
print(pancakes.size()) *4*
pancakes.remove_one() *5*
print(pancakes.size()) *6*
pancakes.prettyprint() *7*
-
1 创建一个栈并将 Stack 对象绑定到名为 pancakes 的变量
-
2 添加了一个蓝莓松饼
-
3 添加了四个巧克力松饼
-
4 打印五个
-
5 移除了最后添加的字符串,一个“chocolate”松饼
-
6 打印四个
-
7 在每行打印每个松饼的风味:顶部有三个巧克力松饼,底部有一个蓝莓松饼
图 32.1 展示了向栈中添加项目以及通过 self.stack 访问的列表数据属性的步骤。
图 32.1. 从左到右,第一个面板显示了一个空的松饼栈。第二个面板显示当你添加一个项目时栈的状态:一个 "blueberry"。第三个面板显示你添加了四个相同项目后的栈:四个 "chocolate"。最后一个面板显示你移除一个项目后的栈:最后添加的一个 "chocolate"。

注意,在这个代码片段中,每个方法的行为都完全像一个函数:它接受参数,通过执行命令来完成工作,并返回一个值。你可以有不需要返回显式值的函数,例如 prettyprint 方法。在这种情况下,当你调用该方法时,你不需要打印结果,因为没有返回任何有趣的内容;方法本身打印了一些值。
32.2.2. 创建一个圆圈栈
现在你有一个 Stack 对象,你可以向栈中添加任何其他类型的对象,而不仅仅是原子对象(int、float 或 bool)。你可以添加你创建的对象类型。
你在第 31 课中编写了一个表示 Circle 对象的类,所以现在你可以创建一个圆圈栈。列表 32.3 展示了如何做到这一点。这与你在列表 32.2 中添加松饼的方式类似。唯一的区别是,在将对象添加到栈中之前,你现在必须初始化一个圆圈对象。如果你正在运行以下列表,你必须将定义 Circle 对象的代码复制到同一文件中,这样 Python 才知道什么是 Circle。
列表 32.3. 创建 Stack 对象并向其中添加 Circle 对象
circles = Stack() *1*
one_circle = Circle() *2*
one_circle.change_radius(2) *2*
circles.add_one(one_circle) *2*
for i in range(5): *3*
one_circle = Circle() *4*
one_circle.change_radius(1) *4*
circles.add_one(one_circle) *4*
print(circles.size()) *5*
circles.prettyprint() *6*
-
1 创建了一个栈并将 Stack 对象绑定到名为 circles 的变量
-
2 创建一个新的圆形对象,将其半径设置为 2,并将圆形添加到栈中
-
3 循环以添加五个新的圆形对象
-
4 在循环的每次迭代中创建一个新的圆形对象,将其半径设置为 1,并将其添加到栈中
-
5 打印了 6
-
6 打印与每个圆形对象相关的 Python 信息(其类型和内存中的位置)
图 32.2 展示了圆形栈可能的外观。
图 32.2. 半径为 2 的圆形位于底部,因为它是最先添加的。然后你创建一个半径为 1 的新圆形,重复五次,并将每个圆形添加到栈中。

你可能还会注意到在 Stack 类中有一个名为 add_many 的方法。而不是一次添加一个圆形的循环,假设你创建了一个半径为 1 的圆形,并使用以下列表中的属性在栈上调用 add_many,如图 32.3 所示。
列表 32.4. 创建 Stack 对象并多次添加相同的圆形对象
circles = Stack() *1*
one_circle = Circle() *1*
one_circle.change_radius(2) *1*
circles.add_one(one_circle) *1*
one_circle = Circle() *2*
one_circle.change_radius(1) *2*
circles.add_many(one_circle, 5) *3*
print(circles.size()) *4*
circles.prettyprint() *5*
-
1 与 列表 32.3 中的操作相同
-
2 创建一个新的圆形对象;将其半径设置为 1
-
3 使用
Stack类中定义的方法将相同的圆形对象添加五次 -
4 打印了 6,这是到目前为止添加的圆形总数
-
5 打印与每个圆形对象相关的 Python 信息(其类型和内存中的位置)
图 32.3. 半径为 2 的圆形位于底部,因为它是最先添加的。然后你创建一个半径为 1 的新圆形,并将这个相同的圆形对象添加到栈中五次

让我们比较一下 列表 32.3 和 列表 32.4 中的两个栈的外观。在 列表 32.3 中,你在循环的每次迭代中创建了一个新的圆形对象。当你使用 prettyprint 方法输出你的栈时,输出看起来像这样,表示正在打印的对象的类型及其在内存中的位置:
|_ <__main__.Circle object at 0x00000200B8B90BA8> _|
|_ <__main__.Circle object at 0x00000200B8B90F98> _|
|_ <__main__.Circle object at 0x00000200B8B90EF0> _|
|_ <__main__.Circle object at 0x00000200B8B90710> _|
|_ <__main__.Circle object at 0x00000200B8B7BA58> _|
|_ <__main__.Circle object at 0x00000200B8B7BF28> _|
在 列表 32.4 中,你只创建了一个新的圆形对象,并将其添加了五次。当你使用 prettyprint 方法输出你的栈时,输出现在看起来像这样:
|_ <__main__.Circle object at 0x00000200B8B7BA58> _|
|_ <__main__.Circle object at 0x00000200B8B7BA58> _|
|_ <__main__.Circle object at 0x00000200B8B7BA58> _|
|_ <__main__.Circle object at 0x00000200B8B7BA58> _|
|_ <__main__.Circle object at 0x00000200B8B7BA58> _|
|_ <__main__.Circle object at 0x000001F1E0E0CA90> _|
使用 Python 打印的内存位置,你可以看到这两段代码之间的差异。列表 32.3 在循环的每次迭代中创建一个新的对象,并将其添加到栈中;恰好每个对象都关联着相同的数据,即半径为 1。另一方面,列表 32.4 创建了一个对象,并将同一个对象多次添加。
在 第 33 课 中,你将了解如何编写自己的方法来覆盖默认的 Python print 方法,以便你可以打印与你的对象相关的信息,而不是内存位置。
快速检查 32.2
问题 1:
编写代码创建两个栈。向一个栈中添加三个半径为 3 的圆形对象,向另一个栈中添加五个宽度为 1、长度为 1 的相同矩形对象。使用第 31 课中定义的
Circle和Rectangle类。
摘要
在本课中,我的目标是教你如何定义多个对象,并在同一程序中使用它们。以下是主要收获:
-
定义一个类需要决定如何表示它。
-
定义一个类还需要决定如何使用它以及实现哪些方法。
-
一个类将属性和行为打包成一个对象类型,这样这个类型的所有对象都具有相同的数据和方法。
-
使用该类涉及创建一个或多个该类型的对象,并对其进行一系列操作。
让我们看看你是否掌握了这个...
Q32.1
以类似栈的方式编写一个队列的类。回想一下,添加到队列中的项目被添加到一端,从队列中移除的项目从另一端移除:
- 决定哪种数据结构将表示你的队列。
- 实现
__init__。- 实现获取大小、添加一个、添加多个、移除一个、移除多个以及显示队列的方法。
- 编写代码创建队列对象并对它们执行一些操作。
第 33 课. 自定义类
阅读完第 33 课后,你将能够
-
向你的类中添加特殊的 Python 方法
-
在你的类中使用特殊运算符,如
+、-、/和*
自从你编写了第一个 Python 程序以来,你一直在使用 Python 语言中定义的类。Python 语言中最基本的对象类型,称为内置类型,允许你在这些类型上使用特殊运算符。例如,你使用了+运算符在两个数字之间。你能够使用[]运算符索引字符串或列表。你能够在任何这些类型的对象上使用print()语句,以及列表和字典。
考虑这一点
-
列举五个可以在整数之间进行的操作。
-
列举两个字符串之间可以进行的操作。
-
列举一个可以在字符串和整数之间进行的操作。
答案:
-
+, -, *, /, % -
+ -
*
每个操作都使用符号进行简写表示。然而,符号只是一个简写符号。每个操作实际上是一个方法,你可以定义它来与特定类型的对象一起工作。
33.1. 重写特殊方法
对象上的每个操作都在类中以方法的形式实现。但你可能已经注意到,当你处理简单的对象类型,如int、float和str时,你使用了几个简写符号。这些简写符号就像在两个这样的对象之间使用+或-或*或/运算符一样。甚至像print()这样的操作,在括号中使用对象,也是类中方法的简写符号。你可以在自己的类中实现这样的方法,以便你可以在自己的对象类型上使用简写符号。
表 33.1 列出了一些特殊方法,但还有很多。请注意,所有这些特殊方法都以双下划线开始和结束。这是 Python 语言特有的,其他语言可能有不同的约定。
表 33.1. Python 中的几个特殊方法
| 类别 | 运算符 | 方法名 |
|---|---|---|
| 数学运算 | + - * / | add sub mul truediv |
| 比较运算 | == < > | eq lt gt |
| 其他 | print() 和 str() 创建一个对象——例如,some_object = ClassName() | str init |
要使特殊操作能够与你的类一起工作,你可以重写这些特殊方法。重写意味着你将在自己的类中实现该方法,并决定该方法将做什么,而不是由通用 Python 对象实现的默认行为。
首先创建一个新类型的对象,表示一个分数。一个分数有一个分子和一个分母。因此,Fraction对象的属性是两个整数。以下列表展示了Fraction类的基本定义。
列表 33.1. Fraction类的定义
class Fraction(object):
def __init__(self, top, bottom): *1*
self.top = top *2*
self.bottom = bottom *2*
-
1 初始化方法接受两个参数。
-
2 使用参数初始化数据属性
使用这个定义,你现在可以创建两个 Fraction 对象并尝试将它们相加:
half = Fraction(1,2)
quarter = Fraction(1,4)
print(half + quarter)
将 1/2 和 1/4 相加应该得到 3/4。但当你运行代码片段时,你得到这个错误:
TypeError: unsupported operand type(s) for +: 'Fraction' and 'Fraction'
这告诉你 Python 不知道如何将两个 Fraction 对象相加。这个错误是有意义的,因为你从未为 Fraction 对象类型定义过这个操作。
要告诉 Python 如何使用 + 操作符,你需要实现特殊方法 __add__(在名称 add 前后使用双下划线)。加法作用于两个对象:一个是调用该方法的对象,另一个是方法的参数。在方法内部,你通过引用两个对象的分子和分母来执行两个 Fraction 对象的加法,如下所示。
列表 33.2. 添加和乘法两个 Fraction 对象的方法
class Fraction(object):
def __init__(self, top, bottom):
self.top = top
self.bottom = bottom
def __add__(self, other_fraction): *1*
new_top = self.top*other_fraction.bottom + \ *2*
self.bottom*other_fraction.top *3*
new_bottom = self.bottom*other_fraction.bottom *4*
return Fraction(new_top, new_bottom) *5*
def __mul__(self, other_fraction): *6*
new_top = self.top*other_fraction.top *6*
new_bottom = self.bottom*other_fraction.bottom *6*
return Fraction(new_top, new_bottom) *6*
-
1 定义特殊方法以实现两个
Fraction对象之间的 + 操作符 -
2 使用反斜杠将行拆分为两行
-
3 从加法中计算分子
-
4 从加法中计算分母
-
5 返回一个新的
Fraction对象,使用新的分子和分母创建 -
6 乘法两个
Fraction对象的方法
快速检查 33.1
Q1:
为
Fraction对象编写一个方法,使用两个Fraction对象之间的-操作符。
33.2. 覆盖 print() 以与你的类一起使用
现在你已经定义了 Fraction 对象之间的 + 操作符,你可以尝试之前的相同代码:
half = Fraction(1,2)
quarter = Fraction(1,4)
print(half + quarter)
这段代码不再报错。相反,它打印了对象的类型及其内存位置:
<__main__.Fraction object at 0x00000200B8BDC240>
但这根本没有任何信息量。你更希望看到分数的值!你需要实现另一个特殊函数,一个告诉 Python 如何打印你类型对象的函数。为此,你实现特殊方法 __str__,如下所示。
列表 33.3. 打印 Fraction 对象的方法
class Fraction(object):
def __init__(self, top, bottom):
self.top = top
self.bottom = bottom
def __add__(self, other_fraction):
new_top = self.top*other_fraction.bottom + \
self.bottom*other_fraction.top
new_bottom = self.bottom*other_fraction.bottom
return Fraction(new_top, new_bottom)
def __mul__(self, other_fraction):
new_top = self.top*other_fraction.top
new_bottom = self.bottom*other_fraction.bottom
return Fraction(new_top, new_bottom)
def __str__(self): *1*
return str(self.top)+"/"+str(self.bottom) *2*
-
1 定义打印
Fraction对象的方法 -
2 返回一个字符串,要打印的内容
现在,当你对一个 Fraction 对象使用 print,或者当你使用 str() 将你的对象转换为字符串时,它将调用 __str__ 方法。例如,以下代码打印 1/2 而不是内存位置:
half = Fraction(1, 2)
print(half)
以下创建了一个值为 1/2 的字符串对象:
half = Fraction(1, 2)
half_string = str(half)
快速检查 33.2
Q1:
将
Fraction对象的__str__方法更改为在一行上打印分子,在下一行打印两个破折号,在第三行打印分母。行print(Fraction(1,2))打印如下:1 -- 2
33.3. 背后发生的事情
当你使用特殊操作符时,究竟发生了什么?让我们看看细节,以及当你添加两个 Fraction 对象时会发生什么:
half = Fraction(1,2)
quarter = Fraction(1,4)
考虑这一行:
half + quarter
它取第一个操作数half,并对其应用特殊方法__add__。这相当于以下代码:
half.__add__(quarter)
此外,每个方法调用都可以通过使用类名并显式地为self参数提供一个参数来重写。前面的行等价于以下代码:
Fraction.__add__(half, quarter)
尽管被称为特殊方法,但所有以双下划线开始和结束的方法都是普通方法。它们在对象上被调用,接受参数,并返回一个值。使它们特殊的是,还有另一种调用方法的方式。你可以使用特殊运算符(例如,数学符号)或使用相当知名的功能(例如,len()或str()或print()等)来调用它们。这种简写符号在其他人阅读代码时通常比阅读正式函数调用符号更直观。
像程序员一样思考
作为程序员,你应该有一个很好的目标,那就是让使用你定义的类的其他程序员的生活更轻松。这包括记录你的类和方法,以及在可能的情况下,实现特殊方法,以便其他人可以直观地使用你的类。
| |
快速检查 33.3
以两种方式重写以下每一行:通过在对象上调用方法,以及通过使用类名调用方法。假设你开始于以下代码:
half = Fraction(1,2)
quarter = Fraction(1,4)
1
quarter * half2
print(quarter)3
print(half * half)
33.4. 你可以用类做什么?
你已经看到了使用 Python 类创建自己的对象类型的细节和语法。本节将展示在某些情况下你可能想要创建的类的示例。
33.4.1. 排程事件
假设你被要求排程一系列事件。例如,你将参加一个电影节,你想要安排你的日程表中的电影。
不使用类
如果你没有使用类,你可以使用一个列表来保存你想要观看的所有电影。列表中的每个元素都是一个要观看的电影。关于电影的相关信息包括其名称、开始时间、结束时间,以及可能还有评论家的评分。这些信息可以作为列表的元素存储在元组中。注意,几乎立即列表就变得难以使用。如果你想要访问每部电影的评分,你将依赖于两次索引——首先索引电影列表,然后索引元组以检索评分。
使用类
了解类之后,你可能会有把每个对象都变成类的冲动。在排程问题中,你可以创建以下类:
-
Time类表示一个时间对象。此类对象将具有数据属性:小时(int)、分钟(int)和秒(int)。对此对象的操作可以是找出两个时间之间的差异,或者将其转换为总小时数、分钟数或秒数。 -
Movie类表示一个电影对象。此类对象将具有数据属性:名称(string)、开始时间(Time)、结束时间(Time)和评分(int)。对此类的操作将是检查两个电影是否在时间上重叠,或者两个电影的评分是否很高。
使用这两个类,你可以抽象出在特定时间段内安排一系列电影的一些烦人细节。现在,你可以创建一个 Movie 对象列表。如果你需要索引列表(例如访问评分),你可以使用电影类中定义的命名方法。
使用过多的类
理解有多少类太多是很重要的。例如,你可以创建一个类来表示 Hour。但这个抽象没有增加任何价值,因为它的表示将是一个整数,在这种情况下,你可以直接使用这个整数。
摘要
在本课中,我的目标是教你如何定义特殊方法,这些方法允许你使用多个对象并在你的对象类型上使用运算符。以下是主要收获:
-
特殊方法有特定的名称,并在名称前后使用双下划线。其他语言可能采用不同的方法。
-
特殊方法有简写符号。
让我们看看你是否掌握了这个...
Q33.1
编写一个方法,允许你在
Circle和Stack上使用Stack的prettyprint相同的方式打印每个对象。你的Circle print应该打印字符串"circle: 1"(或圆的半径是多少)。你必须实现Stack类和Circle类中的__str__方法。例如,以下行circles = Stack() one_circle = Circle() one_circle.change_radius(1) circles.add_one(one_circle) two_circle = Circle() two_circle.change_radius(2) circles.add_one(two_circle) print(circles)应该打印这个:
|_ circle: 2 _| |_ circle: 1 _|
第 34 课。综合项目:卡片游戏
在阅读第 34 课之后,你将能够
-
使用类构建更复杂的程序
-
使用他人创建的类来改进你的程序
-
允许用户玩战争牌局的简单版本
当你创建自己的对象类型时,你可以组织更大的程序,使其更容易编写。与函数一起引入的模块化和抽象原则也适用于类。类用于封装一组属性和行为,这些属性和行为对一组对象是通用的,以便对象可以在程序中一致地使用。
使用类的一个常见初始程序是模拟与用户玩某种游戏。
问题
你想模拟玩战争牌局。每一轮,玩家将从一副牌中抽一张牌并比较牌面。牌面较高的玩家赢得该轮,并将他们的牌交给另一玩家。经过多轮比赛后,当牌堆为空时,确定赢家。赢家是手中牌较少的人。你将创建两种类型的对象:Player 和 CardDeck。在定义了这些类之后,你将编写代码来模拟两个玩家之间的游戏。你将首先询问用户的姓名,然后创建两个 Player 对象。两位玩家将使用同一副牌。然后,你将使用 Player 和 CardDeck 类中定义的方法来自动模拟轮次并确定赢家。
34.1. 使用现有的类
预先构建到 Python 语言中的对象始终可供你在程序中使用;这些对象如 int、float、list 和 dict。但许多其他类已经被其他程序员编写,并且可以用来增强你程序的功能。你不需要在代码文件中键入它们的类定义,而是可以使用一个 import 语句将另一个类的定义引入到你的文件中。这样,你可以创建该类型的对象,并在你的代码中使用该类的功能。
你在卡片游戏中想要使用的一个有用的类是 random 类。你可以使用以下方式引入随机类定义:
import random
现在,你可以创建一个可以执行随机数操作的对象。你可以在类名上使用点符号,如第 31 课中提到的,并调用你想要使用的方法以及它期望的任何参数。例如,
r = random.random()
这会给你一个介于 0(包括)和 1(不包括)之间的随机数,并将其绑定到变量 r。这里还有一个例子:
r = random.randint(a, b)
这行代码会给你一个介于 a 和 b(包括)之间的随机整数,并将其绑定到变量 r。现在考虑这一行:
r = random.choice(L)
它会从列表 L 中随机选择一个元素并将其绑定到变量 r。
34.2. 详细说明游戏规则
在开始编码之前的第一步是理解你希望程序如何运行,以及具体的游戏规则是什么:
-
为了简化,假设一副牌包含四种花色,每种花色都有 2 到 9 的牌。在表示牌时,使用
"2H"表示红心 2,"4D"表示方片 4,"7S"表示黑桃 7,"9C"表示梅花 9,依此类推。 -
玩家有一个名字(字符串)和一副牌(列表)。
-
游戏开始时,询问两名玩家的名字并将它们设置好。
-
每轮游戏,向每位玩家的手中各添加一张牌。
-
比较刚刚添加到每位玩家手中的牌:首先按数字比较,如果相同,则按黑桃 > 红心 > 方片 > 梅花排序。
-
持有较大牌的玩家从手中移除该牌,持有较小牌的玩家拿走该牌并将其添加到自己的手中。
-
当牌堆为空时,比较玩家手中的牌数;牌数较少的玩家获胜。
您将定义两个类:一个用于 Player,另一个用于 CardDeck。
34.3. 定义 Player 类
玩家由名字和手牌定义。名字是一个字符串,手牌是一个字符串列表,代表牌。创建 Player 对象时,您需要提供一个名字作为参数,并假设他们手中没有牌。
第一步是定义 __init__ 方法,告诉 Python 如何初始化一个 Player 对象。知道您有一个 Player 对象的两个数据属性,您还可以编写一个方法来返回 Player 的名字。这在上面的列表中显示。
列表 34.1. Player 类的定义
class Player(object):
""" a player """
def __init__(self, name):
""" sets the name and an empty hand """
self.hand = [] *1*
self.name = name *2*
def get_name(self): *3*
""" Returns the name of the player """ *3*
return self.name *3*
-
1 将手设为一个空列表*
-
2 在创建
Player对象时将传入的字符串设置为名字* -
3 一个返回玩家名字的方法*
现在,根据游戏规则,玩家还可以向手中添加一张牌并从手中移除一张牌。请注意,您需要检查添加的牌是否有效,确保其值不是 None。为了检查玩家手中的牌数并确定获胜者,您还可以添加一个方法来告诉您手中的牌数。以下列表显示了这三个方法。
列表 34.2. Player 类的定义
class Player(object):
""" a player """
# methods from Listing 34.1
def add_card_to_hand(self, card): *1*
""" card, a string
Adds valid card to the player's hand """
if card != None:
self.hand.append(card) *1*
def remove_card_from_hand(self, card): *2*
""" card, a string
Remove card from the player's hand """
self.hand.remove(card) *2*
def hand_size(self): *3*
""" Returns the number of cards in player's hand """
return len(self.hand) *3*
-
1 向手中添加一张牌将其添加到列表中,并且只添加具有有效数字和花色的牌。*
-
2 从手中移除一张牌找到该牌并将其从列表中移除。*
-
3 手的大小返回列表中的元素数量。*
34.4. 定义 CardDeck 类
CardDeck 类将代表一副牌。牌堆有 32 张牌,每种花色(黑桃、红心、方片、梅花)都有 2 到 9 的牌。以下列表显示了如何初始化对象类型。将只有一个数据属性,即牌堆中所有可能牌的列表。每张牌都用一个字符串表示;例如,红心 3 的形式为 "3H"。
列表 34.3. CardDeck 类的初始化
class CardDeck(object):
""" A deck of cards 2-9 of spades, hearts, diamons, clubs """
def __init__(self):
""" a deck of cards (strings e.g. "2C" for the 2 of clubs)
contains all cards possible """
hearts = "2H,3H,4H,5H,6H,7H,8H,9H" *1*
diamonds = "2D,3D,4D,5D,6D,7D,8D,9D" *1*
spades = "2S,3S,4S,5S,6S,7S,8S,9S" *1*
clubs = "2C,3C,4C,5C,6C,7C,8C,9C" *1*
self.deck = hearts.split(',')+diamonds.split(',') + \ *2*
spades.split(',')+clubs.split(',')
-
1 创建一副牌中所有可能牌的字符串*
-
2 在逗号处分割长字符串,并将所有牌(字符串)添加到牌堆列表中。*
在你决定使用包含牌组中所有卡片的列表来表示牌组之后,你可以开始实现这个类的相关方法。这个类将使用 random 类来随机选择玩家将使用的卡片。一个方法将返回牌组中的一张随机卡片;另一个方法将比较两张卡片并告诉你哪一张更高。
列表 34.4. CardDeck 类中的方法
import random
class CardDeck(object):
""" A deck of cards 2-9 of spades, hearts, diamonds, clubs """
def __init__(self):
""" a deck of cards (strings e.g. "2C" for the 2 of clubs)
contains all cards possible """
hearts = "2H,3H,4H,5H,6H,7H,8H,9H"
diamonds = "2D,3D,4D,5D,6D,7D,8D,9D"
spades = "2S,3S,4S,5S,6S,7S,8S,9S"
clubs = "2C,3C,4C,5C,6C,7C,8C,9C"
self.deck = hearts.split(',')+diamonds.split(',') \
+ spades.split(',')+clubs.split(',')
def get_card(self):
""" Returns one random card (string) and
returns None if there are no more cards """
if len(self.deck) < 1: *1*
return None *1*
card = random.choice(self.deck) *2*
self.deck.remove(card) *3*
return card *4*
def compare_cards(self, card1, card2):
""" returns the larger card according to
(1) the larger of the numbers or, if equal,
(2) Spades > Hearts > Diamonds > Clubs """
if card1[0] > card2[0]: *5*
return card1 *5*
elif card1[0] < card2[0]: *6*
return card2 *6*
elif card1[1] > card2[1]: *7*
return card1 *7*
else: *7*
return card2 *7*
-
1 如果牌组中没有更多卡片,则返回 None。
-
2 从牌组列表中随机选择一张卡片
-
3 从牌组列表中移除卡片
-
4 返回卡片的值(字符串)
-
5 检查卡片号码值,如果更高则返回第一张卡片
-
6 检查卡片号码值,如果更高则返回第二张卡片
-
7 当卡片号码值相等时,使用花色。
34.5. 模拟牌局
在你定义了帮助你模拟牌局的对象类型之后,你可以编写使用这些类型的代码。
34.5.1. 设置对象
第一步是设置游戏,创建两个 Player 对象和一个 CardDeck 对象。你要求输入两位玩家的名字,为每位玩家创建一个新的 Player 对象,并调用设置名字的方法。这将在下面的列表中展示。
列表 34.5. 初始化游戏变量和对象
name1 = input("What's your name? Player 1: ") *1*
player1 = Player(name1) *2*
name2 = input("What's your name? Player 2: ")
player2 = Player(name2)
deck = CardDeck() *3*
-
1 获取玩家 1 的用户输入名字
-
2 创建一个新的
Player对象 -
3 创建一个新的
CardDeck对象
在初始化你将在游戏中使用的对象之后,你现在可以模拟游戏了。
34.5.2. 模拟游戏中的回合
一局游戏由许多回合组成,并持续到牌组为空。可以计算出玩家将玩多少轮;如果每位玩家在每一轮都抽一张牌,而牌组中有 32 张牌,那么将有 16 轮。你可以使用 for 循环来计数回合,但 while 循环也是实现回合的可接受方式。
在每一轮中,每位玩家都会得到一张牌,所以对牌组调用 get_card 方法两次,一次为每位玩家。然后,每个玩家对象调用 add_card_to_hand,将牌组返回的随机卡片添加到他们的手中。
然后,两位玩家至少都有一张牌,需要考虑两种情况:
-
游戏结束,因为牌组已空。
-
牌组中仍然有卡片,玩家必须比较并决定谁给对方一张牌。
当游戏结束时,你通过在每个玩家对象上调用 hand_size 来检查手牌的大小。手牌更大的玩家输,然后你退出循环。
如果游戏还没有结束,你需要通过在包含两名玩家牌的牌组上调用 compare_cards 方法来决定哪位玩家拥有较大的牌。返回值是较大的牌,如果数值相等,花色将决定哪张牌更重。如果较大的牌与 player1 的牌相同,player1 需要将牌给 player2。在代码中,这对应于 player1 调用 remove_card_from_hand 和 player2 调用 add_card_to_hand。当较大的牌与 player2 的牌相同时,也会发生类似的情况。请参阅以下列表。
列表 34.6. 模拟游戏回合的循环
name1 = input("What's your name? Player 1: ")
player1 = Player(name1)
name2 = input("What's your name? Player 2: ")
player2 = Player(name2)
deck = CardDeck()
while True:
player1_card = deck.get_card()
player2_card = deck.get_card()
player1.add_card_to_hand(player1_card)
player2.add_card_to_hand(player2_card)
if player1_card == None or player2_card == None: *1*
print("Game Over. No more cards in deck.")
print(name1, " has ", player1.hand_size())
print(name2, " has ", player2.hand_size())
print("Who won?")
if player1.hand_size() > player2.hand_size(): *2*
print(name2, " wins!") *2*
elif player1.hand_size() < player2.hand_size(): *3*
print(name1, " wins!") *2*
else: *4*
print("A Tie!") *4*
break *5*
else: *6*
print(name1, ": ", player1_card)
print(name2, ": ", player2_card)
if deck.compare_cards(player1_card,player2_card)==player1_card: *7*
player2.add_card_to_hand(player1_card) *8*
player1.remove_card_from_hand(player1_card) *9*
else:
player1.add_card_to_hand(player2_card)
player2.remove_card_from_hand(player2_card)
-
1 游戏结束,因为至少有一名玩家没有更多的牌
-
2 检查手牌的大小,player2 胜出,因为他们手中的牌更少
-
3 检查手牌的大小,player1 胜出,因为他们手中的牌更少
-
4 玩家拥有相同数量的牌,因此是平局
-
5 当一名玩家获胜或出现平局时,break 语句会退出 while 循环。
-
6 游戏可以继续,因为手中仍有牌需要比较
-
7 比较玩家之间的牌,返回的是较大的牌
-
8 较大的牌属于 player1,因此将 player1 的牌添加到 player2 的手中
-
9 较大的牌属于 player1,因此从 player1 的手中移除其牌。
34.6. 使用类实现模块化和抽象
实现这个游戏是一项庞大的任务。如果不将问题分解成更小的子任务,编写游戏代码会很快变得杂乱无章。
使用对象和面向对象编程,你还成功地进一步模块化了你的程序。你将代码分离成不同的对象,并为每个对象提供一组数据属性和一组方法。
使用面向对象编程还允许你分离两个主要思想:创建组织代码的类,以及使用这些类来实现玩游戏的代码。在模拟游戏玩法时,你能够一致地使用相同类型的对象,从而产生整洁且易于阅读的代码。这抽象了对象类型及其方法实现的具体细节,并且你能够使用方法的文档字符串来决定在模拟过程中哪些方法是合适的。
概述
在本课中,我的目标是教你如何编写一个更大的程序,该程序使用其他人创建的类来改进你的程序,以及如何创建自己的类并使用它们来玩游戏。
类定义的代码只需要编写一次。它决定了你的对象的整体属性以及你可以对这些对象执行的操作。此代码不操作任何特定的对象。游戏玩法本身的代码(不包括类定义的代码)很简单,因为你正在创建对象并在适当的对象上调用方法。
这种结构将描述一个对象是什么以及它能做什么的代码与使用这些对象来完成各种任务的代码分开。这样,你就可以隐藏一些不必要的编码细节,这些细节对于实现游戏玩法并不需要知道。
单元 8:使用库增强您的程序
对于您编写的所有程序的大部分,它们都依赖于内置对象和您自己创建的对象的组合。编程的一个很大部分是学习如何利用他人编写的代码来为您服务。您可以将他们的代码引入自己的程序中,然后使用他们已经编写的函数和类。您已经在一些综合项目中做了一些这样的工作。
您为什么要这样做呢?通常,不同的程序员需要完成相同的一组任务。他们不必独立提出自己的解决方案,而是可以使用包含帮助实现目标代码的库。许多语言允许程序员创建库。该库可能包含在语言中,或者可能在网上找到并单独分发。库通常捆绑在一起,包含具有相同风格的函数和类。
在本单元中,您将了解可用于各种任务的库以及它们的使用方法。您将看到三个简单的库:math 库包含帮助您进行数学运算的函数,random 库包含允许您处理随机数的函数,而 time 库包含允许您使用计算机时钟暂停您的程序或对其计时函数。您还将看到两个更复杂的库:unittest 库将帮助您构建测试,以便您可以检查您的代码是否按预期运行,而 tkinter 库将帮助您通过图形用户界面为您的程序添加一个视觉层。
在综合项目中,您将编写一个玩捉迷藏游戏的程序。两名玩家将使用键盘在屏幕上追逐对方。当一名玩家足够接近另一名玩家时,您将打印出他们已被标记的信息。
第 35 课:有用的库
在阅读完第 35 课后,你将能够
-
将外部标准 Python 包中的库引入到你的代码中
-
使用
math库来进行数学运算 -
使用
random库来生成随机数 -
使用
time库来计时程序
编程是一种通常在基于他人已完成的工作时最有效率和最愉快的活动。一些问题已经被解决,并且可能已经编写了代码来解决与你试图解决的问题类似的任务。你几乎不可能需要从头开始实现代码来完成一个任务。在任何语言中,都存在你可以用来以模块化方式帮助编码任务的库:通过构建已经编写、测试和调试为正确性和效率的代码。
在某种程度上,你已经在做这件事了!你一直在使用 Python 语言内建的对象和操作。想象一下,如果你不得不学习如何在计算机中处理内存位置并从头开始构建一切,学习编程会有多困难。
考虑这一点
大部分编程都是基于已有的对象和思想来构建的。想想你到目前为止学到了什么。有哪些例子是你基于现有事物进行构建的?
答案:
你可以使用其他人编写的代码(甚至是你之前编写的)。你从简单的对象类型开始,然后创建更复杂的类型。通过使用函数和重用具有不同输入的函数,你构建了抽象层。
35.1. 导入库
争议性地,你到目前为止已经学到了两件重要的事情:
-
如何创建自己的函数
-
如何创建自己的对象类型,这些类型将一组属性和行为打包在一起
更复杂的代码需要包含许多函数和对象类型,你必须包括它们的定义。一种方法是将定义复制粘贴到你的代码中。但还有一种更常见且更不容易出错的方法。当你在其他文件中定义了函数和类时,你可以在代码的顶部使用一个 import 语句。你可能在不同的文件中定义不同的函数或类的原因是为了保持代码的整洁,与抽象的概念保持一致。
假设你有一个文件,其中定义了你已经见过的两个类:Circle 和 Rectangle。在另一个文件中,你想要使用这些类。你只需在文件中添加一行来引入在另一个文件中定义的类。
在一个名为 shapes.py 的文件中,你定义了 Circle 和 Rectangle 类。在另一个文件中,你可以使用以下方式引入这些类:
import shapes
这个过程称为 导入,告诉 Python 将名为 shapes.py 的文件中定义的所有类引入。注意,import 行使用了包含你想要导入的定义的文件名,并且在文件名后省略了 .py。为了使这一行工作,这两个文件必须位于你的电脑上的同一文件夹中。图 35.1 显示了代码的组织结构。
图 35.1. 同一文件夹中的两个文件:shapes.py 和 test.py。一个文件定义了 Circle 和 Rectangle 类。另一个文件导入了 shapes.py 中定义的类,并通过创建这些类型的不同对象并更改它们的数据属性来使用它们。

导入是一个常见的实践,它促进了代码的组织和清理。通常,你会在代码中导入库。库是一个或多个模块,而模块是一个包含定义的文件。库通常将相关用途的模块捆绑在一起。库可以是语言内建的(包含在语言安装中)或第三方(从其他在线资源下载)。在本单元中,你将只使用内建库。
将 Python 库想象成一个商店。有些商店很大,比如百货商店,它们能帮你在一个地方找到所有物品,但可能没有一些专门的商品。其他商店较小,比如购物中心的小摊位,它们专注于一种商品(例如,手机或香水),并提供与该类型商品更广泛的选择。
当使用库时,你的第一个动作是查看库的文档,以了解库中定义的类和函数。对于内建的库(语言的一部分),这些文档可以在 Python 网站上找到:docs.python.org。该网站链接到 Python 最新版本的文档,但你也可以查看任何先前版本的文档。本书使用 Python 版本 3.5。如果你没有互联网连接来查看在线文档,你还可以通过 Python 控制台查看文档。你将在下一节中看到如何这样做。
快速检查 35.1
Q1:
假设你有三个文件:
- fruits.py 包含了水果类的定义。
- activities.py 包含了你一天中会做的活动的函数。
- life.py 包含了生命游戏的玩法。
你想在 life.py 中使用 fruits.py 和 activities.py 中定义的类和函数。你需要编写哪些行?
35.2. 使用 math 库进行数学运算
最有用的库之一是数学库。Python 3.5 的数学库文档在 docs.python.org/3.5/library/math.html 上。它处理数字上的数学运算,这些运算不是内置于语言中的。要在线下查看数学库文档,你可以在 IPython 控制台中输入以下内容:
import math
help(math)
控制台显示了在数学库中定义的所有类和函数,以及它们的文档字符串。你可以浏览文档字符串,看看它们中的任何一个是否对你的代码有用。
数学库包含按类型组织的函数:数论和表示函数、幂和对数函数、三角函数、角度转换和双曲函数。它还包含两个你可以使用的常量:π和 e。
假设你想要模拟在田野中向你的朋友投掷球。你想要看看这个投掷是否能达到你的朋友那里,考虑到一点宽容度,因为你的朋友可以跳起来接球。让我们看看如何编写一个为你进行这种模拟的程序。你需要询问用户你的朋友离你有多远,你投掷球的速度以及投掷球的角度。程序将告诉你球是否能飞得足够远以至于可以被接住。图 35.2 显示了设置。
图 35.2. 以一定速度和角度投掷球以使其飞到一定距离的设置

你可以使用以下公式来计算当以一定速度和角度投掷球时球能飞多远:
reach = 2 * speed2 * sin(angle) * cos(angle) / 9.8
列表 35.1 显示了这个简单程序的代码。它首先询问用户到朋友的距离、投掷球的速度以及投掷球的角度。然后它使用公式来计算球会飞多远。考虑到接收者能够伸手去接球的一定宽容度,它会显示以下三种信息之一:被接住、未达目标或飞得太远。为了计算正弦和余弦值,你将使用数学库中的函数,因此你必须使用 import 语句引入库。除了实现公式外,只有一个细节需要处理。角度可以用度或弧度来衡量。数学库中的函数假设角度是以弧度给出的,因此你需要使用数学库中的函数将角度从度转换为弧度。
列表 35.1. 使用数学库以角度投掷球
import math *1*
distance = float(input("How far away is your friend? (m) "))
speed = float(input("How fast can you throw? (m/s) "))
angle_d = float(input("What angle do you want to throw at? (degrees) "))
tolerance = 2
angle_r = math.radians(angle_d) *2*
reach = 2*speed**2*math.sin(angle_r)*math.cos(angle_r)/9.8 *3*
if reach > distance - tolerance and reach < distance + tolerance:
print("Nice throw!")
elif reach < distance - tolerance:
print("You didn't throw far enough.")
else:
print("You threw too far.")
-
1 导入数学库函数
-
2 实现公式,使用数学库函数
-
3 库 math.sin 和 math.cos 以弧度为输入,而不是度,因此需要转换用户输入。
快速检查 35.2
Q1:
修改程序,使其只询问用户朋友有多远以及抛球的速度。然后,程序会遍历从 0 到 90 度的所有角度,并打印出球是否成功到达。
35.3. 使用 random 库生成随机数
随机库提供了许多操作,你可以使用这些操作为程序添加不可预测性。该库的文档位于docs.python.org/3.5/library/random.html。
35.3.1. 随机化列表
在你的程序中添加不可预测性和不确定性可以增加更多功能,并使它们对用户更有趣。这种不可预测性来自伪随机数生成器,可以帮助你做一些事情,比如在某个范围内选择一个随机数,在列表或字典中随机选择一个项目,或者随机重新排列列表,等等。
例如,假设你有一个人员名单,并想随机选择一个人。尝试在文件中输入此代码并运行它:
import random
people = ["Ana","Bob","Carl","Doug","Elle","Finn"]
print(random.choice(people))
它会随机打印出名单中的人的一个元素。如果你多次运行程序,你会注意到每次运行都会得到不同的输出。
你甚至可以从名单中选择一定数量的人:
import random
people = ["Ana","Bob","Carl","Doug","Elle","Finn"]
print(random.sample(people, 3))
这段代码确保同一个人不会被重复选择,并打印出指定数量的元素列表(在这个例子中是三个)。
35.3.2. 模拟随机游戏
随机库的另一个常见用途是玩随机游戏。你可以通过使用random.random()函数来模拟某些事件发生的概率:第一个random是库名,第二个random是函数名,它恰好与库名相同。此函数返回一个介于 0(包含)和 1(不包含)之间的随机浮点数。
列表 35.2 展示了一个与用户玩剪刀石头布的程序。程序首先要求用户做出选择。然后,它通过使用random.random()获取一个随机数。为了模拟计算机选择石头、纸牌或剪刀的 1/3 概率,你可以检查生成的随机数是否落在三个范围之一:0 到 1/3、1/3 到 2/3 和 2/3 到 1。
列表 35.2. 使用 random 库玩剪刀石头布
import random *1*
choice = input("Choose rock, paper, or scissors: ")
r = random.random() *2*
if r < 1/3: *3*
print("Computer chose rock.")
if choice == "paper":
print("You win!")
elif choice == "scissors":
print("You lose.")
else:
print("Tie.")
elif 1/3 <= r < 2/3: *4*
print("Computer chose paper.")
if choice == "scissors":
print("You win!")
elif choice == "rock":
print("You lose.")
else:
print("Tie.")
else: *5*
print("Computer chose scissors.")
if choice == "rock":
print("You win!")
elif choice == "paper":
print("You lose.")
else:
print("Tie.")
-
1 引入 random 库中定义的函数
-
2 选择介于 0(包含)和 1(不包含)之间的随机浮点数
-
3 计算机选择石头的情况,概率为 1/3
-
4 计算机选择纸牌的情况,概率为 1/3
-
5 计算机选择剪刀的情况,概率为 1/3
35.3.3. 使用种子复制结果
当你的程序没有产生你想要的结果时,你需要测试它们以找出问题所在。处理随机数的程序增加了一层复杂性;涉及随机数的程序有时可能工作,有时则不行,导致令人沮丧的调试会话。
随机库生成的随机数并不是真正的随机数。它们是伪随机数。它们看起来是随机的,但是由对频繁变化或不可预测的事物(如自特定日期以来的毫秒数)应用函数的结果决定的。日期生成伪随机序列的第一个数字,该序列中的每个数字都是根据前一个数字生成的。随机库允许你使用random.seed(N)来设置随机数,其中N是任何整数。设置种子允许你从一个已知的数字开始。只要种子设置为相同的值,你程序中生成的随机数序列在每次运行程序时都将相同。以下行生成一个介于 2 和 17 之间的随机整数,然后是介于 30 和 88 之间的随机整数:
import random
print(random.randint(2,17))
print(random.randint(30,88))
如果你多次运行这个程序,打印出的数字很可能会改变。但你可以通过设置种子来确保每次运行程序时这两个数字都相同,方法如下:
import random
random.seed(0)
print(random.randint(2,17))
print(random.randint(30,88))
每次运行程序时,该程序都会打印出 14 和 78。通过更改种子函数内的整数,你可以生成不同的序列。例如,如果你将random.seed(0)更改为random.seed(5),那么每次运行此程序时,它现在都会打印出 10 和 77。请注意,如果你使用的是 3.5 以外的 Python 版本,这些数字可能会改变。
快速检查 35.3
Q1:
编写一个模拟抛掷硬币 100 次的程序。然后打印出有多少次出现正面和多少次出现反面。
35.4. 使用时间库计时程序
当你开始处理可能需要很长时间才能运行的程序时,了解它们已经运行了多长时间会很好。时间库中有可以帮助你做到这一点的函数,并且其文档可在docs.python.org/3.5/library/time.html找到。
35.4.1. 使用时钟
计算机运行速度很快,但它们能多快地完成简单的计算呢?你可以通过计时让计算机计数到一百万来回答这个问题。列表 35.3 展示了完成此操作的代码。在增加计数器的循环之前,你保存了计算机时钟上的当前时间。然后运行循环。循环结束后,再次获取计算机上的当前时间。开始和结束时间之间的差异告诉你程序运行了多长时间。
列表 35.3. 使用时间库来显示程序运行所需的时间
import time *1*
start = time.clock() *2*
count = 0 *3*
for i in range(1000000): *3*
count += 1 *3*
end = time.clock() *4*
print(end-start) *5*
-
1 引入在时间库中定义的函数
-
2 获取时钟上的当前时间,单位为毫秒
-
3 让计算机计数到一百万的代码
-
4 获取时钟上的当前时间,单位为毫秒
-
5 打印开始和结束时间之间的差异
这个程序在我的电脑上运行大约需要 0.2 秒。这个时间会根据您的电脑的新旧和速度以及运行的其他应用程序的数量而变化。如果您在后台有视频流,电脑可能会将资源分配给执行该操作,而不是运行您的程序,因此您可能会看到更长的时间打印出来。
35.4.2. 暂停程序
时间库还允许您使用睡眠函数暂停您的程序。这会阻止它执行下一行,直到经过指定的时间。这种用法之一是向用户显示加载屏幕。列表 35.4 展示了如何打印每半秒显示 10% 增量的进度条。代码打印以下内容,每行之间有半秒的暂停。你能通过查看代码来解释为什么代码会打印多个星号吗?你需要回顾一下关于字符串和字符串操作的课程:
Loading...
[ ] 0 % complete
[ * ] 10 % complete
[ ** ] 20 % complete
[ *** ] 30 % complete
[ **** ] 40 % complete
[ ***** ] 50 % complete
[ ****** ] 60 % complete
[ ******* ] 70 % complete
[ ******** ] 80 % complete
[ ********* ] 90 % complete
列表 35.4. 使用时间库显示进度条
import time *1*
print("Loading...")
for i in range(10): *2*
print("[",i*"*",(10-i)*" ","]",i*10,"% complete") *3*
time.sleep(0.5) *4*
-
1 引入时间库中定义的函数
-
2 代表 10% 增量的循环
-
3 使用多个 * 字符打印进度
-
4 程序暂停半秒
快速检查 35.4
Q1:
编写一个程序,生成 1000 万个随机数,然后打印出完成此操作所需的时间。
概述
在本课中,我的目标是教会您如何使用其他程序员创建的库来增强您自己的程序。这里展示的库很简单,但使用它们可以带来更有趣的用户体验。以下是主要收获:
-
将处理类似功能的代码组织在单独的文件中,可以使代码更容易阅读。
-
库将一组动作相关的函数和类存储在一个地方。
让我们看看你是否掌握了这个...
Q35.1
编写一个程序,让用户与计算机掷骰子。首先,模拟用户掷一个六面的骰子,并向用户展示结果。然后,模拟计算机掷一个六面的骰子,添加 2 秒的延迟,并展示结果。每次掷骰子后,询问用户是否想要再次掷骰子。当用户完成游戏后,向他们展示他们玩游戏的时间(以秒为单位)。
第 36 课:测试和调试你的程序
在阅读第 36 课之后,你将能够
-
使用 unittest 库
-
为你的程序编写测试
-
高效地调试你的程序
很可能你不会在第一次尝试时就编写出一个完美的程序。通常,你会编写代码,用几个输入测试它,对其进行修改,然后再次测试,重复这个过程,直到程序按预期运行。
考虑以下内容
回顾你到目前为止的编程经验。当你编写一个程序并且它不起作用时,你会做些什么来修复它?
答案:
查看错误消息(如果有),看看是否有像行号这样的线索可以指引我找到问题所在。在特定位置放置print语句。尝试不同的输入。
36.1. 使用 unittest 库
Python 有许多库可以帮助你围绕你的程序创建测试结构。当你有包含不同函数的程序时,测试库特别有用。一个测试库随 Python 安装一起提供,Python 3.5 的文档可在docs.python.org/3.5/library/unittest.html找到。
要创建一组测试,你需要创建一个代表该组测试的类。该类内部的方法代表不同的测试。你想要运行的每个测试都应该以test_作为前缀,然后是任何方法名。以下列表包含定义了两个简单测试并运行它们的代码。
列表 36.1:简单的测试套件
import unittest *1*
class TestMyCode(unittest.TestCase): *2*
def test_addition_2_2(self): *3*
self.assertEqual(2+2, 4) *4*
def test_subtraction_2_2(self): *5*
self.assertNotEqual(2-2, 4) *6*
unittest.main() *7*
-
1 导入 unittest 库
-
2 用于执行测试套件类的类
-
3 测试 2 + 2 是否等于 4 的方法
-
4 测试检查 2 + 2 是否等于 4,通过断言 2 + 2 和 4 相等
-
5 测试 2 - 2 是否不等于 4 的方法
-
6 测试检查 2 - 2 是否不等于 4,通过断言 2 - 2 和 4 不相等
-
7 运行你在测试套件类中定义的测试
代码打印以下内容:
Ran 2 tests in 0.001s
OK
这是预期的,因为第一个测试检查 2 + 2 是否等于 4,这是正确的。第二个测试检查 2 - 2 是否不等于 4,这也是正确的。现在假设通过以下更改使其中一个测试为False:
def test_addition_2_2(self):
self.assertEqual(2+2, 5)
现在测试检查 2 + 2 是否等于 5,这是False。再次运行测试程序现在会打印以下内容:
FAIL: test_addition_2_2 (__main__.TestMyCode)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:/Users/Ana/.spyder-py3/temp.py", line 5, in test_addition_2_2
self.assertEqual(2+2, 5)
AssertionError: 4 != 5
----------------------------------------------------------------------
Ran 2 tests in 0.002s
FAILED (failures=1)
这条消息充满了信息。它告诉你以下内容:
-
哪个测试套件失败了:
TestMyCode -
哪个测试失败了:
test_addition_2_2 -
测试中失败的哪一行:
self.assertEqual(2+2, 5) -
为什么它失败了,通过比较它得到的价值和预期的价值:
4 != 5
以这种方式比较值有点愚蠢。显然,你永远不会需要检查 2 + 2 是否等于 4。你通常需要测试具有更多实质性的代码;通常,你想要确保函数做正确的事情。你将在下一节中看到如何做到这一点。
快速检查 36.1
Q1:
填写以下每一行:
class TestMyCode(unittest.TestCase): def test_addition_5_5(self): # fill this in to test 5+5 def test_remainder_6_2(self): # fill this in to test the remainder when 6 is divided by 2
36.2. 将程序与测试分离
你应该将你作为程序编写的一部分代码与编写来测试程序的代码解耦。解耦强化了模块化的概念,即你将代码分离到不同的文件中。这样,你可以避免在程序本身中添加不必要的测试命令。
假设你有一个文件包含 列表 36.2 中显示的两个函数。一个函数检查一个数是否为素数(只能被 1 和它本身整除,且不等于 1),并返回 True 或 False。另一个返回一个数的绝对值。这些函数的实现中都有错误。在进一步阅读如何编写测试之前,你能找出这些错误吗?
列表 36.2. 包含要测试的函数的文件,命名为 funcs.py
def is_prime(n):
prime = True
for i in range(1,n):
if n%i == 0:
prime = False
return prime
def absolute_value(n):
if n < 0:
return -n
elif n > 0:
return n
在一个单独的文件中,你可以编写单元测试来检查你编写的函数是否按预期运行。你可以创建不同的类来组织与不同函数对应的测试套件。这是一个好主意,因为你应该为每个函数编写多个测试,确保尝试各种输入。为每个编写的函数编写测试被称为 单元测试,因为你是通过单独测试每个函数的行为来进行的。
定义
单元测试 是一系列测试,用于检查实际输出是否与函数的预期输出匹配。
为一个方法编写单元测试的常见方式是使用 Arrange Act Assert 模式:
-
Arrange—设置对象及其值以传递给正在接受单元测试的函数
-
Act—使用之前设置的参数调用函数
-
Assert—确保函数的行为符合预期
以下列表显示了在 funcs.py 中对函数进行单元测试的代码。类 TestPrime 包含与 is_prime 函数相关的测试,类 TestAbs 包含与 absolute_value 函数相关的测试。
列表 36.3. 包含测试的文件,命名为 test.py
import unittest *1*
import funcs *2*
class TestPrime(unittest.TestCase): *3*
def test_prime_5(self): *4*
isprime = funcs.is_prime(5) *5*
self.assertEqual(isprime, True) *6*
def test_prime_4(self):
isprime = funcs.is_prime(4)
self.assertEqual(isprime, False)
def test_prime_10000(self):
isprime = funcs.is_prime(10000)
self.assertEqual(isprime, False)
class TestAbs(unittest.TestCase):
def test_abs_5(self):
absolute = funcs.absolute_value(5)
self.assertEqual(absolute, 5)
def test_abs_neg5(self):
absolute = funcs.absolute_value(-5)
self.assertEqual(absolute, 5)
def test_abs_0(self):
absolute = funcs.absolute_value(0)
self.assertEqual(absolute, 0)
unittest.main()
-
1 引入 unittest 类和函数
-
2 引入在 funcs.py 中定义的函数
-
3 一个类用于一组测试
-
4 一个测试,方法名描述了测试的目的
-
5 从 funcs.py 文件中调用 is_prime 函数,参数为 5,并将返回值赋给 isprime
-
6 添加测试以检查函数调用结果是否为 True
运行此代码显示它运行了六个测试并发现了两个错误:
FAIL: test_abs_0 (__main__.TestAbs)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:/Users/Ana/test.py", line 24, in test_abs_0
self.assertEqual(absolute, 0)
AssertionError: None != 0
======================================================================
FAIL: test_prime_5 (__main__.TestPrime)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:/Users/Ana/test.py", line 7, in test_prime_5
self.assertEqual(isprime, True)
AssertionError: False != True
----------------------------------------------------------------------
Ran 6 tests in 0.000s
FAILED (failures=2)
根据这些信息,你可以修改 funcs.py 中的函数来尝试修复它们。这里提供的信息告诉你哪些测试失败了:test_abs_0 和 test_prime_5。你现在可以回到你的函数并尝试修复它。这个过程被称为 调试,将在下一节中讨论。
像程序员一样思考
使用描述性的方法名称对于在测试失败时提供快速、一目了然的信息是有用的。你可以包括函数名称、输入,以及可能的一个或两个词的描述,说明它正在测试什么。
重要的是要逐步进行更改。每次你进行更改时,你应该通过运行 tests.py 再次运行测试。你这样做是为了确保你为修复一个问题所做的任何更改不会导致另一个问题出现。
快速检查 36.2
Q1:
修改列表 36.2 中的代码以修复两个错误。每次更改后,运行 tests.py 以查看你是否已修复错误。
36.2.1. 测试类型
unittest 库有多种测试可以执行,不仅仅是使用assertEqual检查一个值是否等于另一个值。完整的列表可以在库的文档中找到。花点时间浏览一下这个列表。
快速检查 36.3
在查看测试列表时,以下哪种情况最合适?
1
检查一个值是否为
False2
检查一个值是否在列表中
3
检查两个字典是否相等
36.3. 调试你的代码
调试代码的过程在某种程度上是一种艺术形式,因为没有一个特定的公式来指导如何进行。这个过程在你有一组失败的测试之后开始。这些测试为你提供了一个在代码中查找的位置和具体条件;你给一个函数提供了一组输入,该函数给出了你预期的输出。
通常,调试的一个暴力解决方案是最有效的;这意味着系统地查看每一行,并使用笔和纸来记录值。从导致测试失败的原因开始,假装你是计算机,执行每一行。写下分配给每个变量的值,并问问自己这是否正确。一旦你发现从你期望的值中计算出的错误值,你很可能已经找到了错误发生的地方。现在,你需要根据你所学到的知识来找出错误发生的原因。
当你逐行跟踪程序时,一个常见的错误是假设简单的代码行是正确的,特别是如果你正在调试自己的代码。对每一行都持怀疑态度。假装你正在向一个对编程一无所知的人解释你的代码。这个过程被称为橡皮鸭调试,这意味着你向一个真实(或想象)的橡皮鸭或任何其他非生命物体解释你的代码。这迫使你用普通的英语而不是编程术语来解释代码,并让你告诉你的助手每一行代码试图完成什么。
36.3.1. 使用工具帮助你逐步执行代码
已经设计了许多工具来帮助你使调试过程更容易。Spyder 内置了一个调试器。这个名字有点误导,因为它不会为你进行调试。相反,它会随着你每一步的操作为你打印变量值。当变量的值不是你期望的时,你仍然需要自己确定代码中哪一行是错误的。你可以对任何你写的代码使用调试器。
你可以使用 Spyder 调试器来识别列表 36.2 中显示的代码的问题,使用列表 36.3 中创建的测试。你知道有两个测试失败了:test_abs_0和test_prime_5。你应该分别单独调试每一个。
现在,你将调试test_abs_0。图 36.1 显示了在打开 tests.py 后你的 Spyder 编辑器应该看起来是什么样子。你左边有编辑器,右下角是 IPython 控制台,右上角是变量浏览器。
图 36.1. 调试窗口截图

第一步是在你的代码中设置一个断点(图 36.1 中的#1)。断点表示代码中的一个位置,你希望在这里停止执行以便检查值。因为测试test_abs_0失败了,所以在那个方法的第一行设置一个断点。你可以在想要设置断点的行的左侧双击区域来插入一个断点;会出现一个点。
然后,你可以以调试模式开始运行程序。这是通过点击带有蓝色箭头和两条垂直线的按钮来完成的(图 36.1 中的#2)。现在控制台会显示代码的前几行和一个箭头,----->,指示要执行哪一行:
----> 1 import unittest
2 import funcs
3
4 class TestPrime(unittest.TestCase):
5 def test_prime_5(self):
点击蓝色双箭头(图 36.1 中的#4)以转到你设置的断点。现在,控制台显示了你在代码中的位置:
21 self.assertEqual(absolute, 5)
22 def test_abs_0(self):
1--> 23 absolute = funcs.absolute_value(0)
24 self.assertEqual(absolute, 0)
25
你想进入函数中查看为什么absolute的值不是 0 并且这个测试失败了。点击带有小蓝色箭头并指向三条水平线的按钮以“进入”函数(图 36.1 中的#3)。现在你已经在函数调用内部了。你的变量浏览器窗口应该显示n的值是 0,控制台显示你已经进入了函数调用:
6 return prime
7
----> 8 def absolute_value(n):
9 if n < 0:
10 return -n
如果你再点击两次“进入”按钮,它将带你到那一行
9 if n < 0:
10 return -n
---> 11 elif n > 0:
12 return n
13
在这一点上,你可以看到问题所在。if语句没有执行,因为n是 0。else语句也没有执行,因为n是 0。你很可能已经找到了问题,现在可以通过点击蓝色方块退出调试。函数返回None,因为没有处理当n为 0 时程序应该做什么的情况。
快速检查 36.4
使用调试器调试其他失败的测试用例test_prime_5:
1
在适当的行设置一个断点。
2
运行调试器;转到断点。
3
进入函数调用,并继续逐步通过程序,直到看到问题。
摘要
在本节课中,我的目标是教你们测试和调试的基础知识,并让你们在导入和使用库方面有更多的实践机会。我展示了如何使用 unittest 库来组织测试,以及将代码与测试框架分离的重要性。你们使用调试器逐步执行了代码。
看看你是否掌握了这个...
Q36.1
这里有一个有问题的程序。编写单元测试并尝试调试它:
def remove_buggy(L, e): """ L, list e, any object Removes all e from L. """ for i in L: if e == i: L.remove(i)
第 37 课. 图形用户界面库
在阅读第 37 课之后,你将能够
-
描述图形用户界面
-
使用图形用户界面库编写程序
你编写过的每个程序都以基于文本的方式与用户交互。你一直在屏幕上显示文本并从用户那里获取文本输入。虽然你可以用这种方式编写有趣的程序,但用户缺少更直观的体验。
考虑这一点
想想你在日常生活中使用的程序:上网的浏览器。你与浏览器有哪些交互?
答案:你打开浏览器窗口,点击按钮,滚动,选择文本,然后关闭浏览器窗口。
许多编程语言都有库可以帮助程序员编写可视化应用程序。这些应用程序使用用户和程序之间熟悉的界面:按钮、文本框、绘图画布、图标等等。
37.1. 图形用户界面库
图形用户界面(GUI)库是一组类和方法,它们知道如何将用户和操作系统接口,以显示称为小部件的图形控制元素。小部件旨在通过交互来增强用户体验:按钮、滚动条、菜单、窗口、绘图画布、进度条或对话框是一些例子。
Python 附带了一个用于 GUI 的标准库,称为 tkinter,其文档可在docs.python.org/3.5/library/tkinter.html#module-tkinter找到。
开发 GUI 应用程序通常需要三个步骤。本课将演示每个步骤。与其他任何程序一样,开发还涉及设置单元测试和调试。以下是三个步骤:
-
通过确定其大小、位置和标题来设置窗口。
-
添加小部件,这些是交互式“事物”,如按钮或菜单等。
-
为小部件选择行为以处理事件,例如点击按钮或选择菜单项。行为通过编写函数形式的事件处理程序来实现,这些函数告诉程序当用户与特定小部件交互时应该采取哪些操作。
快速检查 37.1
你可以在以下每个选项上执行哪些操作?
1
一个按钮
2
一个滚动条
3
一个菜单
4
一个画布
37.2. 使用 tkinter 库设置程序
所有 GUI 程序通常在窗口中运行。可以通过更改其标题、大小和背景颜色来自定义窗口。以下列表显示了如何创建一个 800 x 200 像素大小的窗口,标题为“我的第一个 GUI”,背景颜色为灰色。
列表 37.1. 创建窗口
import tkinter *1*
window = tkinter.Tk() *2*
window.geometry("800x200") *3*
window.title("My first GUI") *4*
window.configure(background="grey") *5*
window.mainloop() *6*
-
1 导入 tkinter 库
-
2 创建一个新的对象并将其绑定到名为 window 的变量
-
3 改变窗口大小
-
4 为窗口添加标题
-
5 改变窗口的背景颜色
-
6 启动程序
运行此程序后,你的电脑屏幕上会出现一个新窗口。如果你看不到它,它可能被你打开的另一个窗口遮挡,所以请在任务栏上寻找新的图标。你可以通过关闭窗口来终止你的程序。
图 37.1 显示了在 Windows 操作系统上窗口的外观。如果你使用的是 Linux 或 Mac 操作系统,窗口的外观可能会有所不同。
图 37.1. 一个 800 x 200 像素的窗口,标题为“我的第一个 GUI”,背景颜色为灰色

快速检查 37.2
为以下每个要求编写一个程序:
1
创建一个标题为“go go go”背景颜色为绿色的 500 x 200 窗口
2
创建一个标题为“Tall One”背景颜色为红色的 100 x 900 窗口
3
创建两个 100 x 100 窗口,没有标题,但一个的背景颜色为白色,另一个为黑色
37.3. 添加小部件
一个空白的窗口并不有趣。用户没有可以点击的东西!在你创建窗口后,你可以开始添加小部件。你将创建一个包含三个按钮、一个文本框、一个进度条和一个标签的程序。
要添加小部件,你需要两行代码:一行用于创建小部件,另一行用于将其放置在窗口上。以下代码通过创建一个按钮并将其添加到窗口对象(列表 37.1)中,展示了这两行代码。第一行创建按钮并将其绑定到名为btn的变量。第二行(使用 pack)将其添加到窗口中:
btn = tkinter.Button(window)
btn.pack()
下一个列表显示了如何添加三个按钮、一个文本框和一个标签,假设你已经创建了一个窗口,如列表 37.1 所示。
列表 37.2. 向窗口添加小部件
import tkinter
window = tkinter.Tk()
window.geometry("800x200")
window.title("My first GUI")
window.configure(background="grey")
red = tkinter.Button(window, text="Red", bg="red") *1*
red.pack() *2*
yellow = tkinter.Button(window, text="Yellow", bg="yellow")
yellow.pack()
green = tkinter.Button(window, text="Green", bg="green")
green.pack()
textbox = tkinter.Entry(window) *3*
textbox.pack() *3*
colorlabel = tkinter.Label(window, height="10", width="10") *4*
colorlabel.pack() *4*
window.mainloop()
-
1 创建一个新的按钮,背景颜色为红色,上面写着“红色”
-
2 将具有这些属性的按钮添加到窗口中
-
3 创建并添加一个可以输入文本的框
-
4 创建并添加一个高度为 10 的标签
当你运行此程序时,你会得到一个看起来像图 37.2 中的窗口。
图 37.2. 添加了三个按钮、一个文本框和一个标签后的窗口。最上面的按钮是红色的,上面写着 Red,中间的按钮是黄色的,上面写着 Yellow,下面的按钮是绿色的,上面写着 Green。

你可以在程序中添加许多小部件。标准 tkinter 库中包含的小部件列在表 37.1 中。
表 37.1. tkinter 中可用的小部件
| 小部件名称 | 描述 | 小部件名称 | 描述 |
|---|---|---|---|
| 按钮 | 显示一个按钮 | 菜单按钮 | 显示菜单 |
| 画布 | 绘制形状 | 选项菜单 | 显示弹出菜单 |
| 复选框 | 通过复选框显示选项(可以选中多个选项) | 分割窗口 | 包含可以调整大小的窗口面板 |
| 输入框 | 用户可以输入文本的文本字段 | 单选按钮 | 通过单选按钮显示选项(只能选择一个选项) |
| 框架 | 放置其他小部件的容器 | 滑块 | 显示滑块 |
| 标签 | 显示单行文本或图像 | 滚动条 | 为其他小部件添加滚动条 |
| 标签框架 | 添加空间的容器 | 滚动条 | 为其他小部件添加滚动条 |
| 列表框 | 通过列表显示选项 | 文本 | 显示多行文本 |
| 菜单 | 显示命令(包含在菜单按钮内) | 顶层窗口 | 允许有单独的窗口容器 |
快速检查 37.3
Q1:
编写一行代码以创建以下内容:
- 一个带有文本“点击此处”的橙色按钮
- 两个单选按钮
- 一个复选框
37.4. 添加事件处理器
到目前为止,你已经创建了 GUI 窗口并将控件添加到其中。最后一步是编写代码,告诉程序当用户与控件交互时程序应该做什么。代码必须以某种方式将控件与操作链接起来。
当你创建一个控件时,你给它一个你想运行的命令的名称。该命令是同一程序中的函数。以下列表显示了一个示例代码片段,该片段在按钮控件被点击时更改窗口的背景颜色。
列表 37.3. 按钮点击事件处理器
import tkinter
def change_color(): *1*
window.configure(background="white") *2*
window = tkinter.Tk()
window.geometry("800x200")
window.title("My first GUI")
window.configure(background="grey")
white = tkinter.Button(window, text="Click", command=change_color) *3*
white.pack()
window.mainloop()
-
1 表示要发生事件的函数
-
2 函数更改窗口的背景颜色。
-
3 通过将函数名称分配给命令参数,具有相关操作的按钮
图 37.3 显示了按钮被点击后的屏幕外观。窗口最初是灰色,但当你点击按钮后,它变成了白色。再次点击按钮不会将颜色改回灰色;它保持白色。
图 37.3. 点击按钮后,窗口背景颜色从灰色变为白色。

你可以用事件处理器做更多有趣的事情。当你编写 GUI 时,你可以应用书中学到的一切。对于最后的例子,你将编写倒计时计时器的代码。你将看到如何从其他控件中读取信息,在事件处理器中使用循环,甚至使用另一个库。
列表 37.4 显示了一个程序,该程序读取用户在文本框中输入的数字,然后从该数字倒计时到 0,每秒更改一次数字。程序中有四个控件:
-
一个带有用户说明的标签
-
一个用户可以输入数字的文本框
-
一个开始倒计时的按钮
-
一个标签来显示变化的数字
按钮是唯一一个与其相关联事件处理器的控件。该事件的处理函数将执行以下操作:
-
将标签的颜色更改为白色
-
从文本框获取数字并将其转换为整数
-
使用文本框中的数字在循环中使用,从该值开始,到 0 结束
大部分工作都是在循环内部完成的。它使用循环变量i来更改标签的文本。注意,你正在给标签的文本参数赋予一个变量名,其值在每次循环中都会改变。然后,它调用一个更新方法来刷新窗口并显示更改。最后,它使用time库中的sleep方法暂停执行一秒钟。如果没有sleep方法,倒计时会进行得太快,以至于你无法看到数字的变化。
列表 37.4. 读取文本框并倒计时相应秒数的程序
import tkinter
import time
def countdown(): *1*
countlabel.configure(background="white") *2*
howlong = int(textbox.get()) *3*
for i in range(howlong,0,-1): *4*
countlabel.configure(text=i) *5*
window.update() *6*
time.sleep(1) *7*
countlabel.configure(text="DONE!") *8*
window = tkinter.Tk()
window.geometry("800x600")
window.title("My first GUI")
window.configure(background="grey")
lbl = tkinter.Label(window, text="How many seconds to count down?") *9*
lbl.pack()
textbox = tkinter.Entry(window) *10*
textbox.pack()
count = tkinter.Button(window, text="Countdown!", command=countdown) *11*
count.pack()
countlabel = tkinter.Label(window, height="10", width="10") *12*
countlabel.pack()
window.mainloop()
-
1 事件处理函数
-
2 将标签的颜色更改为白色
-
3 从文本框获取值并将其转换为 int
-
4 从文本框中的数字开始循环直到 0
-
5 将标签上的文本更改为循环变量的值
-
6 更新窗口以显示标签上的更新值
-
7 使用 time 库等待一秒钟
-
8 在达到 0 后更改标签的文本为“完成”
-
9 一个带有用户说明的标签
-
10 用户输入数字的文本框
-
11 启动倒计时的按钮,其功能写在第一行作为事件命令
-
12 用于打印倒计时的标签
通过设置窗口、添加小部件和创建事件处理程序的能力,你可以编写许多视觉上吸引人且独特交互的程序。
快速检查 37.4
Q1:
编写代码创建一个按钮。当点击时,按钮随机选择红色、绿色或蓝色,并将窗口的背景颜色更改为所选颜色。
摘要
在本课中,我的目标是教你如何使用图形用户界面库。该库包含帮助程序员操作操作系统图形元素的类和方法。以下是主要收获:
-
你的 GUI 程序在窗口内运行。
-
在窗口中,你可以添加称为小部件的图形元素。
-
你可以添加在用户与小部件交互时执行任务的函数。
让我们看看你是否掌握了这些...
Q37.1
编写一个程序,将人们的姓名、电话号码和电子邮件存储在电话簿中。窗口应包含三个文本框,分别用于姓名、电话和电子邮件。然后应有一个按钮用于添加联系人,还有一个按钮用于显示所有联系人。最后,它应有一个标签。当用户点击添加按钮时,程序会读取文本框并存储信息。当用户点击显示按钮时,它会读取存储的所有联系人并将它们打印在标签上。
第 38 课. 综合项目:捉迷藏游戏
在阅读第 38 课之后,你将能够
-
使用 tkinter 库编写一个简单的游戏
-
使用类和面向对象编程来组织 GUI 代码
-
编写与用户交互的代码,使用键盘
-
使用画布在你的程序中绘制形状
当你想到一个使用 GUI 的程序时,最常见的一种程序类型就是游戏。短小且互动的游戏可以提供快速的娱乐。当你自己编写它们时,它们甚至更有趣!
问题
使用 tkinter 库编写一个 GUI 游戏。该游戏模拟捉迷藏游戏。你应在窗口内创建两个玩家。玩家的位置和大小可以在开始时随机化。两个玩家将使用相同的键盘:一个将使用 W、A、S、D 键,另一个将使用 I、J、K、L 键来移动他们的棋子。用户决定哪一个将尝试捕捉另一个。然后,他们将使用各自的键在窗口内移动,试图触摸另一个玩家。当他们触摸到另一个玩家时,屏幕上应出现“Tag”这个词。
这是一个简单的游戏,编写它的代码不会很长。在编写 GUI 或像游戏这样的视觉应用程序时,一开始不要过于雄心勃勃是非常重要的。从一个更简单的问题开始,随着事情的开始工作,逐步构建。
38.1. 确定问题的各个部分
针对这个问题,现在是时候确定它的各个部分了。如果你逐步进行,你会发现编写代码会更容易。最终,你需要完成三个任务:
-
创建两个形状
-
当按下某些键时,在窗口内移动它们
-
检测两个形状是否接触
这些都可以编写为独立的代码片段,可以单独测试。
38.2. 在窗口中创建两个形状
与你见过的其他 GUI 一样,第一步是创建一个窗口并向其中添加你的游戏将使用的小部件。下一个列表显示了这段代码。窗口将只包含一个小部件,即画布。画布小部件是一个矩形区域,你可以在这里放置形状和其他图形对象。
列表 38.1. 初始化窗口和小部件
import tkinter
window = tkinter.Tk()
window.geometry("800x800")
window.title("Tag!")
canvas = tkinter.Canvas(window) *1*
canvas.pack(expand=1, fill='both') *2*
-
1 包含玩家形状的画布小部件
-
2 添加画布以使其填充整个窗口,并在窗口大小调整时缩放
现在是时候创建玩家的形状了。你将制作玩家棋子为矩形。这些形状将是添加到画布上的对象,而不是窗口本身。由于你将创建多个玩家,以模块化方式思考是个好主意。你将创建一个玩家类,它将在画布上初始化玩家棋子。
为了使游戏更有趣,你可以使用随机数来设置形状的起始位置和大小。图 38.1 展示了如何构建矩形。你将选择一个随机坐标作为左上角 x1 和 y1。然后,你将选择一个随机数作为矩形的大小。x2 和 y2 坐标通过将大小加到 x1 和 y1 上来计算。
图 38.1. 通过选择左上角坐标和随机大小来构建矩形游戏部件

创建矩形的随机性意味着每次创建新的玩家时,矩形都会在窗口的随机位置放置,并且大小也是随机的。
列表 38.2 展示了创建玩家矩形部件的代码。代码创建了一个名为 Player 的类。你将使用矩形来表示玩家的部件。画布上的任何对象都由四个整数的元组表示,x1、y1、x2、y2,其中 (x1、y1) 是形状的左上角,(x2、y2) 是形状的右下角。
列表 38.2. 用于玩家的类
import random
class Player(object):
def __init__(self, canvas, color): *1*
self.color = color *2*
size = random.randint(1,100) *3*
x1 = random.randint(100,700) *4*
y1 = random.randint(100,700) *5*
x2 = x1+size *6*
y2 = y1+size *7*
self.coords = [x1, y1, x2, y2] *8*
self.piece = canvas.create_rectangle(self.coords) *9*
canvas.itemconfig(self.piece, fill=color) *10*
-
1
init方法用于创建一个对象,该对象接受要添加形状的画布以及形状的颜色 -
2 将对象的颜色设置为数据属性
-
3 为玩家部件的大小选择 1 到 100 之间的随机数
-
4 对象左上角 x 值,在指定的范围内随机选择
-
5 对象左上角 y 值,在指定的范围内随机选择
-
6 对象右下角坐标的 x 值
-
7 对象右下角坐标的 y 值
-
8 将对象的坐标设置为数据属性,类型为列表
-
9 将玩家部件数据属性设置为矩形,放置在上述坐标指定的位置
-
10 通过引用上一行的变量名 self.piece 设置玩家部件的颜色
创建窗口后,你可以使用以下代码将玩家添加到画布上,该代码在同一个画布上创建了两个 Player 对象,一个黄色和一个蓝色:
player1 = Player(canvas, "yellow")
player2 = Player(canvas, "blue")
运行代码后,你会得到一个看起来像图 38.2 的窗口。你有两个不同颜色的形状,它们位于随机位置和随机大小。当鼠标点击或按键时,没有任何动作发生。
图 38.2. 创建两个玩家对象后的游戏。每次程序运行时,每个方块的位子和大小都会变化。

38.3. 在画布内移动形状
每个形状都响应相同类型的事件,即按键:
-
要将形状向上移动,按 W 键移动一个形状,按 I 键移动另一个形状。
-
要将形状向左移动,按 A 键移动一个形状,按 J 键移动另一个形状。
-
要将形状向下移动,按 S 键移动一个形状,按 K 键移动另一个形状。
-
要将形状向右移动,按 D 键移动一个形状,按 L 键移动另一个形状。
你将不得不创建一个函数,作为画布上任何按键事件的处理器。在函数内部,你将根据按下的按钮移动一个玩家或另一个玩家。以下列表显示了代码。在这个代码中,move是你将在Player类中定义的方法。它将使用"u"向上,"d"向下,"r"向右,"l"向左来移动玩家的位置。
列表 38.3. 在画布上按下任何键时的事件处理函数
def handle_key(event): *1*
if event.char == 'w' : *2*
player1.move("u") *3*
if event.char == 's' :
player1.move("d")
if event.char == 'a' :
player1.move("l")
if event.char == 'd' :
player1.move("r")
if event.char == 'i' : *4*
player2.move("u") *5*
if event.char == 'k' :
player2.move("d")
if event.char == 'j' :
player2.move("l")
if event.char == 'l' :
player2.move("r")
window = tkinter.Tk()
window.geometry("800x800")
window.title("Tag!")
canvas = tkinter.Canvas(window)
canvas.pack(expand=1, fill='both')
player1 = Player(canvas, "yellow")
player2 = Player(canvas, "blue")
canvas.bind_all('<Key>', handle_key) *6*
-
1 事件处理函数
-
2 检查事件中的按键是否是 W
-
3 move 是你将在 player 类中定义的方法,所以调用该方法来移动形状向上。
-
4 检查事件中的按键是否是 I
-
5 因为 move 是在 player 类中定义的,所以你调用它来移动形状向上。
-
6 对于画布,任何按键事件都会调用 handle_key 函数。
注意这个代码的模块化做得很好。因为它将移动形状的逻辑移到了 player 类中,所以很容易理解正在发生的事情。在事件处理函数中,你只需决定移动哪个玩家以及移动的方向,方向由"u"表示向上,"d"表示向下,"l"表示向左,"r"表示向右。
在Player类中,你可以编写代码通过改变坐标值来处理形状的移动。列表 38.4 显示了代码。对于玩家可以移动的每个方向,你修改坐标数据属性。然后,你更新形状在画布上的坐标为新坐标。两个玩家不能同时移动。当一个玩家开始移动后,一旦另一个玩家按下键,它就会停止移动。
列表 38.4. 在画布内移动形状的方法
class Player(object):
def __init__(self, canvas, color):
size = random.randint(1,100)
x1 = random.randint(100,700)
y1 = random.randint(100,700)
x2 = x1+size
y2 = y1+size
self.color = color
self.coords = [x1, y1, x2, y2]
self.piece = canvas.create_rectangle(self.coords, tags=color)
canvas.itemconfig(self.piece, fill=color)
def move(self, direction): *1*
if direction == 'u': *2*
self.coords[1] -= 10 *3*
self.coords[3] -= 10 *3*
canvas.coords(self.piece, self.coords) *4*
if direction == 'd':
self.coords[1] += 10
self.coords[3] += 10
canvas.coords(self.piece, self.coords)
if direction == 'l':
self.coords[0] -= 10
self.coords[2] -= 10
canvas.coords(self.piece, self.coords)
if direction == 'r':
self.coords[0] += 10
self.coords[2] += 10
canvas.coords(self.piece, self.coords)
-
1 移动形状的方法;接受一个方向:‘u’,‘d’,‘l’,‘r’之一
-
2 对四种可能的输入(‘u’,‘d’,‘l’,‘r’)执行不同的操作
-
3 如果你正在向上移动,通过索引列表 coords 减少 y1 和 y2 的值
-
4 将矩形(由 self.piece 表示)的坐标更改为新坐标
此代码由任何创建的 player 对象使用。它遵循抽象和模块化原则,因为它位于Player类下,这意味着你只需编写一次,但它可以被任何对象重用。
现在当你运行程序时,W,A,S,D 键将移动黄色的形状在窗口周围,而 I,J,K,L 键将移动蓝色的形状。你甚至可以请别人和你一起玩来测试代码。你会注意到,如果你按住一个键,形状将连续移动,但只要你按下另一个键,形状就会停止并按照另一个按键移动(直到再次按下另一个键)。追逐形状很有趣,但它们接触时不会发生任何事情。
38.4. 检测形状之间的碰撞
游戏的最后一部分是添加检测两个形状是否碰撞的代码逻辑。毕竟,这是一个捉迷藏游戏,当形状接触到另一个形状时,得到通知会很好。代码逻辑将包括在画布上调用两个方法。它将在处理画布按键事件的同一个事件函数中实现。这是因为每次按键后,你都想看到是否发生了碰撞。
列表 38.5 展示了检测两个形状之间碰撞的代码。按照设计,在 tkinter 库中,添加到画布上的每个形状都会分配一个 ID。第一个添加的形状获得 ID 1,第二个获得 ID 2,依此类推。你首先添加到画布上的形状是黄色的。代码背后的想法是,通过调用 bbox 方法来获取画布上第一个形状的坐标,该方法找到形状周围的边界框。在矩形的情况下,边界框就是矩形本身,但在其他情况下,边界框是一个形状勉强适合的矩形。然后,你使用边界框的坐标作为参数,在画布上调用 find_overlapping 方法。该方法返回一个元组,告诉你所有在该框内的 ID。因为作为参数给出的坐标是其中一个形状的边界框坐标,所以当形状重叠时,该方法将返回一个包含 (1, 2) 的元组。剩下要做的就是检查 ID 为 2 的形状是否在返回的元组中。如果是,那么就在画布上添加文本。
列表 38.5。检测碰撞
def handle_key(event):
yellow_xy = canvas.bbox(1) *1*
overlapping = canvas.find_overlapping(
yellow_xy[0],yellow_xy[1],yellow_xy[2],yellow_xy[3]) *2*
if 2 in overlapping: *3*
canvas.create_text(100,100,font=("Arial",20),text="Tag!") *4*
-
1 获取一个形状周围的坐标
-
2 找到所有在由这些坐标形成的框内的形状的 ID
-
3 检查另一个形状的 ID 是否在重叠的 ID 中
-
4 在画布上添加文本
一旦一个形状接触到另一个形状,屏幕将看起来像图 38.3。
图 38.3。当一个形状与另一个形状的边界框重叠时,画布上会打印出文本 Tag!。

38.5. 可能的扩展
这个游戏有许多扩展的可能性。在这个课程中,你的编码是一个很好的开始。以下是一些想法:
-
而不是关闭窗口并重新启动来再次玩游戏,添加一个按钮来询问用户是否再次玩游戏。当他们这样做时,为你的形状选择另一个随机位置和大小。
-
允许形状逃脱。如果形状在接触一次之后没有接触,则从画布上删除文本。
-
允许玩家通过改变颜色或改变形状为圆形来自定义他们的形状。
摘要
在本节课中,我的目标是教你们如何使用更高级的 GUI 元素来制作游戏。你们使用画布添加形状到 GUI 中,并添加了一个事件处理器,根据按下的哪个键来移动形状。你们还看到了如何检测画布中形状之间的碰撞。你们还看到了如何通过使用类和函数来编写整洁、有组织且易于阅读的代码,这些类和函数用于代码的主要部分,这些部分你们知道将会被重复使用。
附录 A. 课程练习答案
本附录包含课程中找到的练习的答案。快速检查的答案非常直接,但某些总结练习的答案可能有几种不同的方式。我已为每种情况提供了一种可能的解决方案,但你的答案可能与我提供的略有不同。
第 2 课
快速检查的答案
快速检查 2.1
1:
问题—制作麦片奶酪。
模糊陈述—将麦片奶酪倒入沸水中煮 12 分钟。
具体陈述—将 6 杯水倒入锅中,调高炉灶温度,等待水沸腾,倒入面条,煮 12 分钟,沥干面条,加入一包奶酪,搅拌。
快速检查 2.2
1:
快速检查 2.3
1:
- 保持感兴趣值的左侧:c² = a² + b²
- 开平方:c = √(a² + b²)
快速检查 2.4
1:
# initialize times to fill pool (in fraction hours) # convert times to minutes # convert times to rates # add rates # solve for minutes when using both hoses
第 3 课
快速检查的答案
快速检查 3.1
1
窥视
2
打印
3
窥视
4
打印
快速检查 3.2
1
将看到
-122
将看到
193
控制台上不会看到任何输出
第 4 课
快速检查的答案
快速检查 4.1
1
不允许
2
不允许
3
允许
4
允许
快速检查 4.2
1:
手机属性—长方形、光滑、黑色、发光、4 英寸 x 2 英寸,有按钮。操作—点击按钮、发出声音、扔掉、打电话、发送电子邮件。
狗的属性—毛茸茸的、四只脚、一个嘴巴、两只眼睛、一个鼻子、两只耳朵。操作—吠叫、抓挠、奔跑、跳跃、哀嚎、舔舐。
镜子属性—反光、易碎、尖锐。操作—破碎、显示反光。
信用卡属性—3 英寸 x 2 英寸,薄、灵活,有数字和字母。操作—滑动、用来开门、用来购物。
快速检查 4.3
1
是
2
不允许
3
不
4
不
5
是
6
是
快速检查 4.4
1
是
2
不
3
不(描述性但不具有意义,除非你在编写关于独角兽的程序)
4
不允许(太长了)
快速检查 4.5
1
apples = 52
oranges = 103
fruits = apples + oranges4
apples = 205
fruits = apples + oranges
概括性问题答案
Q4.1
x = b - a = 2 - 2 = 0Q4.2
你仍然遇到了错误。这是因为 Python 解释器不明白最后一行该做什么。解释器期望等号左边有一个名称,但
a + x不是一个名称。
第 5 课
快速检查答案
快速检查 5.1
1
six = 2 + 2 + 22
neg = six * (-6)3
neg /= 10
快速检查 5.2
1
half = 0.25 * 22
other_half = 1.0 - half
快速检查 5.3
1
cold = True2
rain = False3
day = cold and rain
快速检查 5.4
1
one = "one"或one = 'one'2
another_one = "1.0"或another_one = '1.0'3
last_one = "one 1"或last_one = 'one 1'
快速检查 5.5
1
浮点型
2
整型
3
布尔型
4
字符串
5
字符串
6
整型
7
整型
8
字符串
9
NoneType
快速检查 5.6
1
陈述和表达式
2
陈述和表达式
3
陈述
4
陈述
快速检查 5.7
1
str(True)
'True'2
float(3) 3.03
str(3.8) '3.8'4
int(0.5) 05
int("4") 4
快速检查 5.8
1
float 1.252
float 9.03
int 84
int 2015
float 16.06
float 1.07
float 1.58
int 29
int 0
第 6 课
快速检查答案
快速检查 6.1
1
7 小时 36 分钟
2
0 小时 0 分钟
3
166 小时 39 分钟
快速检查 6.2
1
13
2
0
3
12
快速检查 6.3
1
stars = 502
stripes = 133
ratio = stars/stripes ratio是一个浮点数4
ratio_truncated = int(ratio) ratio_truncated是一个整数
快速检查 6.4
1:
minutes_to_convert = 789 hours_decimal = minutes_to_convert/60 hours_part = int(hours_decimal) minutes_decimal = hours_decimal-hours_part minutes_part = round(minutes_decimal*60) print("Hours") print(hours_part) print("Minutes") print(minutes_part)输出:
Hours 13 Minutes 9
概括问题答案
Q6.1
fah = 75 cel = (fah-32)/1.8 print(cel)Q6.2
miles = 5 km = miles/0.62137 meters = 1000*km print("miles") print(miles) print("km") print(km) print("meters") print(meters)
第 7 课
快速检查答案
快速检查 7.1
1
Yes
2
Yes
3
No
4
No
5
Yes
快速检查 7.2
1
向前:5 向后:-8
2
向前:0 向后:-13
3
向前:12 向后:-1
快速检查 7.3
1
'e'2
' '(空格字符)
3
'L' 'x'
快速检查 7.4
1
't'2
'nhy tWp np'3
''(空字符串,因为起始索引在字符串中比结束索引更远,但步长为 1)
快速检查 7.5
1
'Python 4 ever&ever'2
'PYTHON 4 EVER&ever'3
'PYTHON 4 EVER&EVER'4
'python 4 ever&ever'
概括问题答案
Q7.1
s = "Guten Morgen" s[2:5].upper()Q7.2
s = "RaceTrack" s[1:4].captalize()
第 8 课
快速检查答案
快速检查 8.1
1
142
93
-14
155
66
8
快速检查 8.2
1
True2
True3
False
快速检查 8.3
1
12
23
14
0
快速检查 8.4
1
'raining in the spring time.'2
'Rain in the spr time.'3
'Raining in the spring time.'4
(无输出) 但
b现在是'Raining in the spring tiempo.'
快速检查 8.5
1
'lalaLand'2
'USA vs Canada'3
'NYcNYcNYcNYcNYc'4
'red-circlered-circlered-circle'
概括性问题答案
Q8.1
还有许多其他方法可以实现这一点!
s = "Eat Work Play Sleep repeat" s = s.replace(" ", "ing ") s = s[7:22] s = s.lower() print(s)
第 9 课
概括性问题答案
Q9.1
- 你尝试访问字符串中超出字符串大小的索引。
- 你尝试在不需要任何参数的情况下调用命令。
- 当命令需要两个参数时,你却只提供了一个对象。
- 你尝试使用错误类型的对象调用命令。你必须提供一个字符串对象,而不是整数对象。
- 你尝试使用变量名而不是字符串对象来调用命令。如果你在使用它之前将
h初始化为字符串,这将有效。- 你尝试将两个字符串相乘,但只允许将两个字符串相加或将字符串与整数相乘。
第 10 课
快速检查答案
快速检查 10.1
1
Yes2
Yes3
No4
Yes
快速检查 10.2
1
42
23
14
0
快速检查 10.3
1
(1, 2, 3)2
'3'3
((1,2), '3')4
True
快速检查 10.4
1
('no', 'no', 'no')2
('no', 'no', 'no', 'no', 'no', 'no')3
(0, 0, 0, 1)4
(1, 1, 1, 1)
快速检查 10.5
1
(s, w) = (w, s)2
(no, yes) = (yes, no)
概括性问题答案
Q10.1
你可以有很多种方法来做这件事。这里有一种方法:
word = "echo" t = () count = 3 echo = (word,) echo *= count cho = (word[1:],) cho *= count ho = (word[2:],) ho *= count o = (word[3:],) o *= count t = echo + cho + ho + o print(t)
第 11 课
快速检查答案
快速检查 11.1
1
122
(没有打印任何内容)
3
Nice is the new cool
快速检查 11.2
1
sweet = "cookies"2
savory = "pickles"3
num = 1004
print(num, savory, "and", num, sweet)5
print("I choose the " + sweet.upper() + "!")
快速检查 11.3
**1
input("告诉我一个秘密: ")2
input("你最喜欢的颜色是什么? ")3
input("输入以下之一:# 或 $ 或 % 或 & 或 *: ")
快速检查 11.4
1
song = input("Tell me your favorite song: ") print(song) print(song) print(song)2
celeb = input("Tell me the first & last name of a celebrity: ") space = celeb.find(" ") print(celeb[0:space]) print(celeb[space+1:len(celeb)])
快速检查 11.5
1:
user_input = input("Enter a number to find the square of: ") num = float(user_input) print(num*num)
概括性问题的答案
Q11.1
b = int(input("Enter a number: ")) e = int(input("Enter a number: ")) b_e = b**e print("b to the power of e is", b_e)Q11.2
name = input("What's your name? ") age = int(input("How old are you? ")) older = age+25 print("Hi " + name + "! In 25 years you will be " + str(older) + "!")
第 13 课
快速检查的答案
快速检查 13.1
1
否
2
是
3
否
4
否
5
否
快速检查 13.2
1
你住在树屋里。
2
(无法转换。)
3
(无法转换。)
4
单词“youniverse”在字典中。
5
数字 7 是偶数。
6
变量 a 和 b 相等
快速检查 13.3
1
num is less than 10 Finished**2
Finished3
Finished
快速检查 13.4
1
word = input("Tell me a word: ") print(word) if " " in word: print("You did not follow directions!")2
num1 = int(input("One number: ")) num2 = int(input("Another number: ")) print(num1+num2) if num1+num2 < 0: print("Wow, negative sum!")
快速检查 13.5
1:
图 A.1. 列表 13.3 中程序的流程图
快速检查 13.6
1:
num_a num_b Answer (nested) Answer (unnested) ----------------------------------------------------------------------- -9 5 num_a: is negative num_a: is negative Finished Finished ----------------------------------------------------------------------- 9 5 Finished Finished ----------------------------------------------------------------------- -9 -5 num_a: is negative num_a: is negative num_b is negative num_b is negative Finished Finished ----------------------------------------------------------------------- 9 -5 Finished num_b is negative Finished
快速检查 13.7
1:
在 列表 13.5 中显示的一个可能的解决方案。
概括性问题的答案
Q13.1
如果 x 是一个奇数,那么 X + 1 是一个偶数。
Q13.2
var = 0 if type(var) == int: print("I'm a numbers person.") if type(var) == str: print("I'm a words person.")Q13.3
words = input("Tell me anything: ") if " " in words: print("This string has spaces.")Q13.4
print("Guess my number! ") secret = 7 num = int(input("What's your guess? ")) if num < secret: print("Too low.") if num > secret: print("Too high.") if num == secret: print("You got it!")Q13.5
num = int(input("Tell me a number: ")) if num >= 0: print("Absolute value:", num) if num < 0: print("Absolute value:", -num)
第 14 课
快速检查的答案
快速检查 14.1
1
你需要牛奶并且有车吗?如果是,开车去商店买牛奶。
2
变量
a是零且变量b是零且变量c是零吗?如果是,那么所有变量都是零。3
你有夹克或毛衣吗?拿一件;外面很冷。
快速检查 14.2
1
是
2
是
3
假
快速检查 14.3
1:
num_a num_b 0 0 0 -5 -20 0 -1 -1 -20 -988
快速检查 14.4
1:
num *is* -3 *Output:* num is negative num *is* 0 *Output:* num is zero num *is* 2 *Output:* num is positive num *is* 1 *Output:* num is positive
快速检查 14.5
1:
图 A.2. 列表 14.3 中代码的流程图
快速检查 14.6
**1:
num With if-elif-else With if --------------------------------------------------------------- 20 num is greater than 3 num is greater than 3 Finished. Finished. --------------------------------------------------------------- 9 num is less than 10 num is less than 10 Finished. num is greater than 3 Finished. --------------------------------------------------------------- 5 num is less than 6 num is less than 6 Finished. num is less than 10 num is greater than 3 Finished. --------------------------------------------------------------- 0 num is less than 6 num is less than 6 Finished. num is less than 10 Finished.
摘要问题的答案
**Q14.1
num1 = int(input("One number: ")) num2 = int(input("Another number: ")) if num1 < num2: print("first number is less than the second number") elif num2 < num1: print("first number is greater than the second number") else: print("numbers are equal")**Q14.2
words = input("Enter anything: ") if "a" in words and "e" in words and "i" in words and "o" in words and "u" in words: print("You have all the vowels!") if words[0] == 'a' and words[-1] == 'z': print("And it's sort of alphabetical!")
第 16 课
快速检查答案
快速检查 16.1
**1
for i in range(8): print("crazy")**2
for i in range(100): print("centipede")
快速检查 16.2
1
0, 1
**2
0, 1, 2, 3, 4
**3
0, 1, 2, 3, 4, 5, 6, ..., 99
摘要问题的答案
**Q16.1
num = int(input("Tell me a number: ")) for i in range(num): print("Hello")没有使用
for循环就无法编写代码,因为你不知道用户会给出多少数字。
第 17 课
快速检查答案
快速检查 17.1
**1
0, 1, 2, 3, 4, 5, 6, 7, 8
**2
3, 4, 5, 6, 7
3
-2, 0, 2
**4
5, 2, -1, -4
**5
(无内容)
快速检查 17.2
**1:
vowels = "aeiou" words = input("Tell me something: ") for letter in words: if letter in vowels: print("vowel")
摘要问题的答案
**Q17.1
counter = 0 for num in range(2, 100, 2): if num%6 == 0: counter += 1 print(counter, "numbers are even and divisible by 6")**Q17.2
count = int(input("How many books on Python do you have? ")) for n in range(count,0,-1): if n == 1: print(n, "book on Python on the shelf", n, "book on Python") print("Take one down, pass it around, no more books!") else: print(n, "books on Python on the shelf", n, "books on Python") print("Take one down, pass it around,", n-1, " books left.")**Q17.3
names = input("Tell me some names, separated by spaces: ") name= "" for ch in names: if ch == " ": print("Hi", name) name = "" else: name += ch # deal with the last name given (does not have a space after it) lastspace = names.rfind(" ") print("Hi", names[lastspace+1:])
第 18 课
快速检查答案
快速检查 18.1
**1:
password = "robot fort flower graph" space_count = 0 for ch in password: if ch == " ": space_count += 1 print(space_count)作为补充说明,前面的代码也可以通过字符串上的一个命令
count,使用password.count(" ")来实现。
快速检查 18.2
**1:
secret = "snake" word = input("What's my secret word? ") guesses = 1 while word != secret: word = input("What's my secret word? ") if guesses == 20 and word != secret: print("You did not get it.") break guesses += 1
摘要问题的答案
**Q18.1
# corrected code num = 8 guess = int(input("Guess my number: ")) while guess != num: guess = int(input("Guess again: ")) print("Right!")**Q18.2
play = input("Play? y or yes: ") while play == 'y' or play == "yes": num = 8 guess = int(input("Guess a number! ")) while guess != num: guess = int(input("Guess again: ")) print("Right!") play = input("Play? y or yes: ") print("See you later!")
第 20 课
快速检查答案
快速检查 20.1
**1
独立
2
依赖的
**3
独立
快速检查 20.2
**1
**2
输入:电话号码,电话
输出:无输出
**3
在:硬币
出:正面或反面
4
在:金钱
出口:一件连衣裙
快速检查 20.3
**1:
快速检查 20.4
**1:
摘要问题的答案
**Q20.1
第 21 课
快速检查答案
快速检查 21.1
**1
def 设置颜色(名称, 颜色):**2
def get_inverse(num):**3
def 打印我的名字():
快速检查 21.2
**1
3
**2
0
**3
4
快速检查 21.3
**1
是(当 2 和 3 是可以相加的变量类型时)
2
是
3
否(缩进错误)
快速检查 21.4
这些只是几种可能性;还有许多其他可能性:
1
get_age或get_tree_age2
translate或dog_says3
cloud_to_animal或take_picture4
age或get_age或years_later
快速检查 21.5
1
变量符号长度(返回类型是整数)
2
是(返回类型是布尔值)
3
"and toes"(返回类型是字符串)
快速检查 21.6
1
return (money_won, guessed)2
(100, True)(1.0, False)- 不打印任何内容
- 不打印任何内容
False 8.0
快速检查 21.7
1
没有打印任何内容
2
Hector is eating3
Hector is eating 8 bananas4
Hector is bananas is eating 8 bananas5
None
概括性问题答案
Q21.1
def calculate_total(price, percent): tip = price*percent/100 total = price + tip return totalcalculate_total(20, 15)my_price = 78.55 my_tip = 20 total = calculate_total(my_price, my_tip) print("Total is:", total)
第 22 课
快速检查答案
快速检查 22.1
1
-11
-11.0
2
-3
-3.0
3
24
1.5
4
32
2.0
快速检查 22.2
1
42
2
6
3
12
4
21
快速检查 22.3
1:
------------------------------------------------- def sandwich(kind_of_sandwich): print("--------") print(kind_of_sandwich ()) print("--------") def blt(): my_blt = " bacon\nlettuce\n tomato" return my_blt def breakfast(): my_ec = " eggegg\n cheese" return my_ec print(sandwich(blt)) <-------- here GLOBAL SCOPE sandwich: (some code) blt: (some code) breakfast: (some code ------------------------------------------------- def sandwich(kind_of_sandwich): <-------- here print("--------") print(kind_of_sandwich ()) print("--------") def blt(): my_blt = " bacon\nlettuce\n tomato" return my_blt def breakfast(): my_ec = " eggegg\n cheese" return my_ec print(sandwich(blt)) GLOBAL SCOPE sandwich: (some code) blt: (some code) breakfast: (some code SCOPE OF sandwich(blt) kind_of_sandwich: blt ------------------------------------------------- def sandwich(kind_of_sandwich): print("--------") print(kind_of_sandwich ()) <-------- here print("--------") def blt(): my_blt = " bacon\nlettuce\n tomato" return my_blt def breakfast(): my_ec = " eggegg\n cheese" return my_ec print(sandwich(blt)) GLOBAL SCOPE sandwich: (some code) blt: (some code) breakfast: (some code SCOPE OF sandwich(blt) kind_of_sandwich: blt ------------------------------------------------- def sandwich(kind_of_sandwich): print("--------") print(kind_of_sandwich ()) print("--------") def blt(): <-------- here my_blt = " bacon\nlettuce\n tomato" return my_blt def breakfast(): my_ec = " eggegg\n cheese" return my_ec print(sandwich(blt)) GLOBAL SCOPE sandwich: (some code) blt: (some code) breakfast: (some code SCOPE OF sandwich(blt) kind_of_sandwich: blt SCOPE OF blt() ------------------------------------------------- def sandwich(kind_of_sandwich): print("--------") print(kind_of_sandwich ()) print("--------") def blt(): my_blt = " bacon\nlettuce\n tomato" return my_blt <-------- here def breakfast(): my_ec = " eggegg\n cheese" return my_ec print(sandwich(blt)) GLOBAL SCOPE sandwich: (some code) blt: (some code) breakfast: (some code SCOPE OF sandwich(blt) kind_of_sandwich: blt SCOPE OF blt() Returns: bacon lettuce tomato ------------------------------------------------- def sandwich(kind_of_sandwich): print("--------") print(kind_of_sandwich ()) print("--------") <-------- here def blt(): my_blt = " bacon\nlettuce\n tomato" return my_blt def breakfast(): my_ec = " eggegg\n cheese" return my_ec print(sandwich(blt)) GLOBAL SCOPE sandwich: (some code) blt: (some code) breakfast: (some code SCOPE OF sandwich(blt) kind_of_sandwich: blt returns: None
快速检查 22.4
1:
def grumpy(): print("I am a grumpy cat:") def no_n_times(n): print("No", n,"times...") def no_m_more_times(m): print("...and no", m,"more times") for i in range(n+m): print("no") return no_m_more_times return no_n_times grumpy()(4)(2) <---------- here GLOBAL SCOPE grumpy: (some code) ----------------------------------------------------------------- def grumpy(): <---------- here print("I am a grumpy cat:") def no_n_times(n): print("No", n,"times...") def no_m_more_times(m): print("...and no", m,"more times") for i in range(n+m): print("no") return no_m_more_times return no_n_times grumpy()(4)(2) GLOBAL SCOPE grumpy: (some code) SCOPE OF grumpy() ----------------------------------------------------------------- def grumpy(): print("I am a grumpy cat:") def no_n_times(n): print("No", n,"times...") def no_m_more_times(m): print("...and no", m,"more times") for i in range(n+m): print("no") return no_m_more_times return no_n_times <---------- here grumpy()(4)(2) GLOBAL SCOPE grumpy: (some code) SCOPE OF grumpy() no_n_times(): (some code) Returns: no_n_times ----------------------------------------------------------------- def grumpy(): print("I am a grumpy cat:") def no_n_times(n): print("No", n,"times...") def no_m_more_times(m): print("...and no", m,"more times") for i in range(n+m): print("no") return no_m_more_times return no_n_times grumpy()(4)(2) <---------- here this line is now no_n_times(4)(2) GLOBAL SCOPE grumpy: (some code) ----------------------------------------------------------------- def grumpy(): print("I am a grumpy cat:") def no_n_times(n): <---------- here print("No", n,"times...") def no_m_more_times(m): print("...and no", m,"more times") for i in range(n+m): print("no") return no_m_more_times return no_n_times grumpy()(4)(2) GLOBAL SCOPE grumpy: (some code) SCOPE OF no_n_times(4) n: no_m_more_times: (some code) ----------------------------------------------------------------- def grumpy(): print("I am a grumpy cat:") def no_n_times(n): print("No", n,"times...") def no_m_more_times(m): print("...and no", m,"more times") for i in range(n+m): print("no") return no_m_more_times <---------- here return no_n_times grumpy()(4)(2) GLOBAL SCOPE grumpy: (some code) SCOPE OF no_n_times(4) n: 4 no_m_more_times: (some code) Returns: no_m_more_times ----------------------------------------------------------------- def grumpy(): print("I am a grumpy cat:") def no_n_times(n): print("No", n,"times...") def no_m_more_times(m): print("...and no", m,"more times") for i in range(n+m): print("no") return no_m_more_times return no_n_times grumpy()(4)(2) <---------- here this line is now no_m_more_times(2) GLOBAL SCOPE grumpy: (some code) ----------------------------------------------------------------- def grumpy(): print("I am a grumpy cat:") def no_n_times(n): print("No", n,"times...") def no_m_more_times(m): <---------- here print("...and no", m,"more times") for i in range(n+m): print("no") return no_m_more_times return no_n_times grumpy()(4)(2) GLOBAL SCOPE grumpy: (some code) SCOPE OF no_m_more_times(2) m: 2 ----------------------------------------------------------------- def grumpy(): print("I am a grumpy cat:") def no_n_times(n): print("No", n,"times...") def no_m_more_times(m): print("...and no", m,"more times") for i in range(n+m): print("no") <---------- here return no_m_more_times return no_n_times grumpy()(4)(2) GLOBAL SCOPE grumpy: (some code) SCOPE OF no_m_more_times(2) m: 2 Returns: None ----------------------------------------------------------------- def grumpy(): print("I am a grumpy cat:") def no_n_times(n): print("No", n,"times...") def no_m_more_times(m): print("...and no", m,"more times") for i in range(n+m): print("no") return no_m_more_times return no_n_times grumpy()(4)(2) <---------- here and done with this line GLOBAL SCOPE grumpy: (some code)
概括性问题答案
Q22.1
def area(shape, n): # write a line to return the area # of a generic shape with a parameter of n return shape(n)
area(circle, 10)area(square, 5)area(circle, 4/2)Q22.2
def person(age): print("I am a person") def student(major): print("I like learning") def vacation(place): print("But I need to take breaks") print(age,"|",major,"|",place) return vacation return student
person(29)("CS")("Japan")person(23)("Law")("Florida")
第 24 课
快速检查答案
快速检查 24.1
1:
图 A.3. 语句序列的可视化
快速检查 24.2
1
要么是不可变对象(元组,因为城市的名称不会改变)或可变对象(列表,因为可能需要添加/删除城市)
2
不可变对象,一个整数(因为年龄只有一个项目,所以将其变为可变对象会过度设计,因为改变它的开销不值得)
3
可变对象,一个字典用于存储物品及其成本
4
不可变对象,一个字符串
概括性问题答案
Q24.1
one是一个不可变对象。
age是一个可变对象。
第 25 课
快速检查答案
快速检查 25.1
1
元组
2
元组
3
元组
4
列表
5
列表
快速检查 25.2
1
磁带
2
鼠标
3
错误,索引超出范围
4
撞击器
快速检查 25.3
1:
1 4 0 8 2 error
快速检查 25.4
1
[1, '1']2
[0, ['zero']](注意第二个元素是另一个列表。)3
[]4
[1,2,3,4,5] [0,1,2,3,4,5]
快速检查 25.5
1:
[3,1,4,1,5,9] [3,1,4,1,5] [3,1,4,1]
快速检查 25.6
1
[1, 2, 3, 4, 7, 11, 13, 17]2
[1, 2, 3, 4, 6, 11, 13, 17]3
[1, 2, 3, 4, 6, 11, 13, 1]4
[3, 2, 3, 4, 6, 11, 13, 1]
概括问题答案
Q25.1
menu = [] menu.append("pizza") menu.append("beer") menu.append("fries") menu.append("wings") menu.append("salad")menu[0] = menu[-1] menu[-1] = "" menu.pop(1) menu[-1] = "pizza"menu.pop() menu.pop() menu.pop() menu.append("quinoa") menu.append("steak")Q25.2
def unique(L): L_unique = [] for n in L: if n not in L_unique: L_unique.append(n) return L_uniqueQ25.3
def unique(L): L_unique = [] for n in L: if n not in L_unique: L_unique.append(n) return L_unique def common(L1, L2): unique_L1 = unique(L1) unique_L2 = unique(L2) length_L1 = len(unique_L1) length_L2 = len(unique_L2) if length_L1 != length_L2: return False else: for i in range(length_L1): if L1[i] not in L2: return False return True
第 26 课
快速检查答案
快速检查 26.1
1:
['g', 'n', 'i', 'm', 'm', 'a', 'r', 'g', 'o', 'r', 'p'] ['a', 'g', 'g', 'i', 'm', 'm', 'n', 'o', 'p', 'r', 'r'] ['r', 'r', 'p', 'o', 'n', 'm', 'm', 'i', 'g', 'g', 'a'] ['a', 'g', 'g', 'i', 'm', 'm', 'n', 'o', 'p', 'r', 'r'] ['a', 'g', 'g', 'i', 'm', 'm', 'n', 'o', 'p', 'r', 'r']
快速检查 26.2
1
board = [[empty, empty, empty], [x, x, x], [o, o, o]]2
board = [[x, o, x, o], [o, o, x, x], [o, empty, x, x]]
快速检查 26.3
1
" abcdefghijklmnopqrstuvwxyz".split(" ")2
"spaces and more spaces".split(" ")3
"the secret of life is 42".split("s")
快速检查 26.4
1
栈
2
栈
3
队列
4
都不是(因为第一个取出的行李可能永远不会被取走)
概括问题答案
Q26.1
cities = "san francisco,boston,chicago,indianapolis" city_list = cities.split(",") city_list.sort() print(city_list)Q26.2
def is_permutation(L1, L2): L1.sort() L2.sort() return L1 == L2
第 27 课
快速检查答案
快速检查 27.1
1
employee_database = {} Key: string for the name Value: tuple of (phone number as a string, home address as a string)2
snow_accumulation = {} Key: string for the city Value: tuple (int year 1990, float for snow in 1990, int year 2000, float for snow in 2000)3
valuables = {"tv": 2000, "sofa": 1500} Key: string for the item name Value: int for the value
快速检查 27.2
1
三项。将整数映射到整数。
2
三项。将字符串映射到整数。
3
三项。将整数映射到列表。
快速检查 27.3
1:
{} {'LA': 3884} {'NYC': 8406, 'LA': 3884} {'NYC': 8406, 'LA': 3884, 'SF': 837} {'NYC': 8406, 'LA': 4031, 'SF': 837}
快速检查 27.4
1:
3.14 1.41 (there will be an error)
快速检查 27.5
1:
(顺序不重要,你将在下一节中看到。)
25 51 35
简答题答案
Q27.1
songs = {"Wannabe": 3, "Roar": 4, "Let It Be": 5, "Red Corvette": 5, "Something": 1} for s in songs.keys(): if songs[s] == 5: print(s)Q27.2
def replace(d, v, e): for k in d: if d[k] == v: d[k] = eQ27.3
def invert(d): d_inv = {} for k in d: v = d[k] if v not in d_inv: d_inv[v] = [k] else: d_inv[v].append(k) return d_inv
第 28 课
快速检查答案
快速检查 28.1
1
相同的 ID
2
相同的 ID
3
相同的 ID(技术上,这应该是一个不同的 ID,因为不可变对象没有别名。但 Python 在幕后通过引用具有相同值的现有对象而不是创建另一个对象来进行优化。这些优化并不保证总是发生。)
快速检查 28.2
1
相同的 ID
2
相同的 ID
3
不同的 ID(你正在创建另一个恰好具有相同元素的对象,而不是别名。)
快速检查 28.3
1
是
2
是
3
是
4
是
5
否
快速检查 28.4
1
order = sorted(chaos)2
colors.sort()3
cards = deck
4 deck.sort()
简答题答案
Q28.1
def invert_dict(d): new_d = {} for k in d.keys(): new_d[d[k]] = k return new_dQ28.2
def invert_dict_inplace(d): new_d = d.copy() d = {} for k in new_d.keys(): d[d_new[k]] = k
第 30 课
快速检查答案
快速检查 30.1
1
是,用整数
2
是,用元组(或列表)
3
否(需要决定定义人的哪些属性和行为——例如,名字、年龄、身高、体重,他们能走路、说话吗?)
4
否(需要决定定义椅子的哪些属性和行为——例如,腿的数量、高度、深度,你能用椅子做什么?)
快速检查 30.2
1
宽度和高度
2
宽度、高度、深度、端口数量、像素数量等等
3
腿的数量、是否有靠背、是否有垫子
4
姓名、年龄、身高、体重、发色、眼色等等
快速检查 30.3
1
求面积或周长
2
打开/关闭它,获取其对角线,将电缆连接到端口
3
让一个人坐在椅子上,砍掉一条腿,加一个垫子
4
改变名字,增加年龄,改变发色
快速检查 30.4
1
字符串
2
列表
3
词典
4
字符串
第 31 课
快速检查答案
快速检查 31.1
1
class Person(object):2
class Car(object):3
class Computer(object):
快速检查 31.2
1:
class Person(object): def __init__(self): self.name = "" self.age = 0 class Car(object): def __init__(self): self.length = 0 self.width = 0 self.height = 0 class Computer(object): def __init__(self): self.on = False self.touchscreen = False
快速检查 31.3
1:
class Door(object): def __init__(self): self.width = 1 self.height = 1 self.open = False def get_status(self): return self.open def get_area(self): return self.width*self.height
快速检查 31.4
1:
square_door = Door() square_door.change_state() square_door.scale(3)
快速检查 31.5
1:
a = Rectangle(1,1) b = Rectangle(1,1) Rectangle.set_length(a, 4) Rectangle.set_width(b, 4)
答案总结问题
Q31.1
def get_area(self): """ returns area of a circle """ return 3.14*self.radius**2 # testing method a = Circle() print(a.get_area()) # shoould be 0 a.change_radius(3) print(a.get_area()) # should be 28.26Q31.2
def get_area(self): """ returns area of a rectangle """ return self.length*self.width def get_perimeter(self): """ returns perimeter of a rectangle """ return self.length*2 + self.width*2
第 32 课
快速检查答案
快速检查 32.1
1:
def add_list(self, L): for e in L: self.stack.append(e)
快速检查 32.2
1:
circles = Stack() for i in range(3): one_circle = Circle() one_circle.change_radius(3) circles.add_one(one_circle) rectangles = Stack() one_rectangle = Rectangle(1, 1) rectangles.add_many(one_rectangle, 5)
答案总结问题
Q32.1
class Queue(object): def __init__(self): self.queue = [] def get_queue_elements(self): return self.queue.copy() def add_one(self, item): self.queue.append(item) def add_many(self, item, n): for i in range(n): self.queue.append(item) def remove_one(self): self.queue.pop(0) def remove_many(self, n): for i in range(n): self.queue.pop(0) def size(self): return len(self.queue) def prettyprint(self): for thing in self.queue[::-1]: print('|_',thing, '_|') # testing the class by making objects and doing operations a = Queue() a.add_one(3) a.add_one(1) a.prettyprint() a.add_many(6,2) a.prettyprint() a.remove_one() a.prettyprint() b = Queue() b.prettyprint()
第 33 课
快速检查答案
快速检查 33.1
1:
def __sub__(self, other_fraction): new_top = self.top*other_fraction.bottom - \ self.bottom*other_fraction.top new_bottom = self.bottom*other_fraction.bottom return Fraction(new_top, new_bottom)
快速检查 33.2
1:
def __str__(self): toreturn = str(self.top) + "\n--\n" + str(self.bottom) return toreturn
快速检查 33.3
1
quarter.__mul__(half) Fraction.__mul__(quarter, half)2
quarter.__str__() Fraction.__str__(quarter)3
(half.__mul__(half)).__str__() Fraction.__str__(Fraction.__mul__(half, half))
答案总结问题
Q33.1
class Circle(object): def __init__(self): self.radius = 0 def change_radius(self, radius): self.radius = radius def get_radius(self): return self.radius def __str__(self): return "circle: "+str(self.radius) class Stack(object): def __init__( self): self.stack = [] def get_stack_elements(self): return self.stack.copy() def add_one(self , item): self.stack.append(item) def add_many(self , item, n): for i in range(n): self.stack.append(item) def remove_one(self): self.stack.pop() def remove_many(self , n): for i in range(n): self.stack.pop() def size(self): return len(self.stack) def prettyprint(self): for thing in self.stack[::-1]: print('|_',thing, '_|') def __str__(self): ret = "" for thing in self.stack[::-1]: ret += ('|_ '+str(thing)+ ' _|\n') return ret
第 35 课
快速检查答案
快速检查 35.1
1:
import fruits import activities
快速检查 35.2
1:
import math distance = float(input("How far away is your friend? (m) ")) speed = float(input("How fast can you throw? (m/s) ")) tolerance = 2 # 0 degrees means throw horizontal and 90 degrees means straight up for i in range(0,91): angle_r = math.radians(i) reach = 2*speed**2*math.sin(angle_r)*math.cos(angle_r)/9.8 if reach > distance - tolerance and reach < distance + tolerance: print("angle: ", i, "Nice throw!") elif reach < distance - tolerance: print("angle: ", i, "You didn't throw far enough.") else: print("angle: ", i, "You threw too far.")
快速检查 35.3
1:
import random heads = 0 tails = 0 for i in range(100): r = random.random() if r < 0.5: heads += 1 else: tails += 1 print("Heads:", heads) print("Tails:", tails)
快速检查 35.4
1:
import time import random count = 0 start = time.clock() for i in range(10000000): count += 1 random.random() end = time.clock() print(end-start) # prints about 4.5 seconds
答案总结问题
Q35.1
import time import random def roll_dice(): r = str(random.randint(1,6)) # put bars around the number so it looks like a dice dice = " _ \n|" + r + "|" print(dice) return r start = time.clock() p = "roll" while p == "roll": print("You rolled a dice...") userroll = roll_dice() print("Computer rolling...") comproll = roll_dice() time.sleep(2) if userroll >= comproll: print("You win!") else: print("You lose.") p = input("Type roll to roll again, any other key to quit: ") end = time.clock() print("You played for", end-start, "seconds.")
第 36 课
快速检查答案
快速检查 36.1
1:
class TestMyCode(unittest.TestCase): def test_addition_5_5(self): self.assertEqual(5+5, 10) def test_remainder_6_2(self): self.assertEqual(6%2, 0)
快速检查 36.2
1:
def is_prime(n): prime = True for i in range(2,n): if n%i == 0: prime = False return prime def absolute_value(n): if n < 0: return -n elif n >= 0: return n
快速检查 36.3
1
assertFalse(x, msg=None)2
assertIn(a, b, msg=None)3
assertDictEqual(a, b, msg=None)
快速检查 36.4
1
在行
isprime = funcs.is_prime(5)处设置断点2
点击带有两个竖线的蓝色箭头,点击带有两个箭头的按钮
3
进入函数并注意循环从 1 开始,而不是 2
答案总结问题
Q36.1
import unittest def remove_buggy(L, e): """ L, list e, any object Removes all e from L. """ for i in L: if e == i: L.remove(i) def remove_fixed(L, e): """ L, list e, any object Removes all e from L. """ for i in L.copy(): if e == i: L.remove(i) class Tests(unittest.TestCase): def test_123_1(self): L = [1,2,3] e = 1 remove_buggy(L,e) self.assertEqual(L, [2,3]) def test_1123_1(self): L = [1,1,2,3] e = 1 remove_buggy(L,e) self.assertEqual(L, [2,3]) unittest.main()
第 37 课
快速检查答案
快速检查 37.1
1
按钮:点击它
2
滚动条:按住鼠标按钮并拖动
3
菜单:将鼠标悬停在项目上并点击它
4
画布:画线、圆、矩形、擦除
快速检查 37.2
1
import tkinter window = tkinter.Tk() window.geometry("500x200") window.title("go go go") window.configure(background="green") window.mainloop()2
import tkinter window = tkinter.Tk() window.geometry("100x900") window.title("Tall One") window.configure(background="red") window.mainloop()3
import tkinter window1 = tkinter.Tk() window1.geometry("100x100") window1.configure(background="white") window2 = tkinter.Tk() window2.geometry("100x100") window2.configure(background="black") window1.mainloop() window2.mainloop()
快速检查 37.3
1:
btn = tkinter.Button(window, text="Click here", bg="orange") radio_btn1 = tkinter.Radiobutton() radio_btn2 = tkinter.Radiobutton() check_btn = tkinter.Checkbutton()
快速检查 37.4
1:
import tkinter import random def changecolor(): r = random.choice(["red", "green", "blue"]) window.configure(background=r) window = tkinter.Tk() window.geometry("800x600") window.title("My first GUI") btn = tkinter.Button(window, text="Random color!", command=changecolor) btn.pack() window.mainloop()
概括问题答案
Q37.1
import tkinter window = tkinter.Tk() window.geometry("200x800") window.title("PhoneBook") phonebook = {} def add(): name = txt_name.get() phone = txt_phone.get() email = txt_email.get() phonebook[name] = [phone, email] lbl.configure(text = "Contact added!") def show(): s = "" for name, details in phonebook.items(): s += name+"\n"+details[0]+"\n"+details[1]+"\n\n" lbl.configure(text=s) txt_name = tkinter.Entry() txt_phone = tkinter.Entry() txt_email = tkinter.Entry() btn_add = tkinter.Button(text="Add contact", command=add) btn_show = tkinter.Button(text="Show all", command=show) lbl = tkinter.Label() txt_name.pack() txt_phone.pack() txt_email.pack() btn_add.pack() btn_show.pack() lbl.pack() window.mainloop()
附录 B. Python 快速参考
变量名
-
区分大小写
-
不能以数字开头
-
不能是 Python 关键字
-
允许—
name,my_name,my_1st_name,name2 -
不允许—
13_numbers,print
字符串
| 描述 | 操作符 | 示例 | 输出 |
|---|---|---|---|
| 等于 | == | 'me' == 'ME' 'you' == 'you' | False True |
| 不等于 | != | 'me' != 'ME' 'you' != 'you' | True False |
| 小于 | < | 'A' < 'a' 'b' < 'a' | True False |
| 小于等于 | <= | 'Z' <= 'a' 'a' <= 'a' | True True |
| 大于 | > | 'a' > 'B' 'a' > 'z' | True False |
| 大于等于 | >= | 'a' >= 'a' 'a' >= 'z' | True False |
| 包含 | in | 'Go' in 'Gopher' 'py' in 'PYTHON' | True False |
| 长度 | len | len('program') len('') | 7 0 |
字符串索引 s = "Python 快速参考"
| 索引/切片 | 结果 |
|---|---|
| s[0] | 'P' |
| s[-1] | 't' |
| s[6] | ' ' |
| s[2:10] | 'thon Che' |
| s[7:15:2] | 'CetS' |
| s[4:8:-1] | '' |
| s[13:3:-2] | 'SteCn' |
| s[::] | 'Python 快速参考' |
| s[::-1] | 'teehS taehC nohtyP' |
列表 L = ['hello', 'hi', 'welcome']
| 切片 | 结果 |
|---|---|
| 索引/切片 | 与字符串相同 |
| L[0][0] | 'h' |
| len(L) | 3 |
| L.append([]) | ['hello', 'hi', 'welcome', []] |
| 'hi' in L | True |
| L[1] = 'bye' | ['hello', 'bye', 'welcome'] |
| L.remove('welcome') | ['hello', 'hi'] |
可变与不可变
-
不可变类型—整数、浮点数、字符串、布尔值。
-
可变类型—列表、字典。
-
在迭代时要注意修改。
字典
-
键不能是可变对象。
-
值可以是可变的或不可变的。
附录 C. 有趣的 Python 库
你见过的库
| 名称 | 描述 |
|---|---|
| math | 数学运算 |
| 随机 | 使用伪随机数的操作 |
| time | 使用时钟的操作 |
| unittest | 添加测试的框架 |
| tkinter | 使用图形用户界面 |
其他有趣的库
| 名称 | 描述 |
|---|
| numpy | 高级数学运算:
-
创建数据的多维数组和矩阵
-
用全零、随机数等填充数组
-
对元素或对进行数组数学运算
-
重新塑形数组
|
| scrapy | 用于网络爬取:
-
爬取网站并提取数据
-
可以以多种标准格式(CSV、JSON、XML)导出数据
|
| matplotlib | 制作图表和图形:
-
制作条形图、折线图、直方图、箱线图、饼图、散点图、饼图
-
创建图像、轮廓、流线图
-
添加文本、标签、坐标轴、图例,更改数据标记
|
| pygame | 2D 游戏开发:
-
可以添加图片、绘制形状、加载光标
-
根据操纵杆、鼠标或键盘输入管理事件
-
操作声音、图像和时间
-
通过缩放、旋转或翻转来转换图片
|
| scipy | 科学计算工具和算法:
-
求解积分、微分方程和优化问题
-
可以聚类数据,进行信号处理(以及图像的扭曲),以及各种统计分析
|
| smtplib | 电子邮件:
-
设置数据和编写带有标题的电子邮件消息
-
验证和加密
|
| pillow | 处理图像:
-
创建缩略图、转换格式、打印
-
处理图像(调整大小、旋转、改变对比度和亮度、执行扭曲)
|
| wxpyton | 使用图形用户界面(tkinter 的替代方案) |
|---|---|
| pyqt | 使用图形用户界面(tkinter 的替代方案) |
| nltk | 自然语言工具包:
-
分析单词、句子、文本
-
将单词标记为对应于词性
-
从文本中提取名称到类别(人、地点、时间、数量等)
|
| basemap | 在地图上绘制 2D 数据:
-
matplotlib 的扩展
-
绘制海岸线、大陆、国家
-
绘制点和轮廓
-
读取点数据以绘制多边形
|
| sqlalchemy | 数据库:
- 以面向对象的方式与数据库交互的接口
|
| pandas | 数据分析:
- 与表格数据、时间序列数据、矩阵数据、统计数据一起工作
|
程序员思维:大概念
| 思想 | 程序员思维——细节 | 部分 |
|---|---|---|
| 设置 | 不要立即开始编码。你可能会发现自己陷入了一条可能或可能不适合当前问题的路径。 | 19.1.2 |
| 设置 | 如果你发现自己正在编写复杂的逻辑来完成简单的任务或多次重复自己,退一步,用一张纸画出你想要实现的内容。 | 17.2 |
| 设置 | 在思考如何分解你的问题时,选择任务并以可重用的方式编写任务。 | 29.1 |
| 设置 | 在开始编码之前,思考你学过的每种数据类型,并决定它是否适合使用。当有多种可能的工作方式时,选择最简单的一种。 | 29.3 |
| 设置 | 在选择表示对象类型的数据属性时,你可以(1)列出你已知的数据类型,并询问每种是否适合使用。(2)注意你想要的操作是否可以用你已知的一个或多个数据结构来表示。 | 32.1.1 |
| 设置 | 考虑包含决策的表达式作为布尔表达式(其结果为真或假),而不是问题(其答案为是或否)。 | 13.1.1 |
| 设置 | 计算机只做它们被告知的事情。在编写代码时,计算机将根据编程语言的规则执行你写的每一行。 | 16.1.2 |
| 可读性 | 程序员编写的代码既便于他人阅读,也便于自己日后回顾。使用描述性变量来存储复杂的计算。 | 14.2.2 |
| 可读性 | 不要使用非常长的变量名。它们会使你的代码难以阅读。 | 4.2.3 |
| 可读性 | 创建变量来存储你将在代码中多次重用的值。 | 18.3.1 |
| 可读性 | 编写尽可能简单、简短且易于理解的代码。 | 18.2 |
| 抽象 | 通过使用你的类,你可以隐藏实现细节,用户不需要知道实现就能使用该类。 | 32.1 |
| 抽象 | 在对大型输入文件运行函数之前,先在较小的测试文件上尝试。 | 29.3 |
| 模块化 | 将你的代码分成更小的部分,并分别测试每个部分。在你知道每个单独的部分按预期工作后,你可以将这些部分组合起来创建你的最终程序。 | 19.1.2 |
| 模块化 | 函数应该是自包含的代码块,你只需要调试一次,但可以多次重用。 | 29.2 |
| 调试 | 为方法使用描述性名称在测试失败时提供快速、一目了然的信息是有用的。 | 36.2 |
| 调试 | 在调试过程中,逐步跟踪程序。逐行检查,绘制你所在的范围,并记录当前范围内任何变量及其值。 | 22.2.2 |
| 文档 | 面对编写代码时,浏览 Python 文档,查看在编写自己的代码之前可以使用哪些函数。 | 23.1.3 |
| 文档 | 程序员试图让其他程序员的编程生活更轻松。记录你的类和方法,并在可能的情况下实现特殊方法。 | 33.3 |



中进行函数调用时会发生什么?使用
和
,第一个参数映射到函数的作用域中。使用
和
,第二个参数映射。
是函数内部创建的另一个变量。
是返回值。函数返回后,函数作用域及其所有变量都会消失。
一个指示数字是奇数还是偶数的程序跟踪图。在每一行,您绘制您所在的作用域以及该作用域中存在的所有变量。在
中,您刚刚调用了
中,函数返回了 







浙公网安备 33010602011771号