特伦托大学编程基础笔记-全-
特伦托大学编程基础笔记(全)
001:欢迎来到LTP! 🎉
在本节课中,我们将介绍《学习编程:基础知识》这门课程的整体安排、学习方式以及核心学习理念。
大家好,欢迎来到《学习编程:基础知识》课程。本课程为期七周,每周你需要观看大约一小时的视频内容。在整个课程中,将会布置家庭作业和编程任务。这些练习为你提供了实践的机会。实践是学习编程的关键,这类似于学习演奏乐器时练习的重要性。
我们很高兴能教授来自世界各地的数万名学员。课程设有一个讨论板,我们将使用它进行交流,而非电子邮件。我们非常期待接下来的七周,感谢你的加入。
上一节我们介绍了课程的基本信息,本节中我们来看看课程的核心学习理念与沟通方式。
以下是本课程的两个核心要点:
- 实践是关键:学习编程与学习乐器类似,必须通过持续的练习来掌握。课程中的作业与编程任务就是为此设计。
- 使用讨论板交流:课程的主要沟通渠道是讨论板,而非电子邮件,这有助于集中问题和分享知识。
本节课中我们一起学习了《学习编程:基础知识》课程的持续时间、学习形式,并强调了实践的核心地位以及课程讨论板这一官方交流渠道。准备好开始你的编程学习之旅吧。
002:安装Python 🐍
在本节课中,我们将学习如何下载和安装Python编程语言,并初步了解什么是程序。
程序无处不在。当你使用网页浏览器、玩电脑游戏或在手机上发送短信时,你都在使用程序。甚至你的操作系统,如macOS、Linux或Windows,本身也是一个程序。程序本质上是一组指令的集合。我们通常说程序被“运行”或“执行”。
程序运行在硬件上,例如你的手机、笔记本电脑或平板电脑。同时,程序也可以被其他程序运行。例如,当你在网页浏览器中玩游戏时,你的浏览器知道如何运行一种名为JavaScript的编程语言编写的程序。
维基百科上有一个页面列出了所有曾被记录过的编程语言,数量多达数百种。你可能听说过其中一些,但几乎没有人能了解这里列出的所有语言。
Python就是众多编程语言中的一种。为了运行Python程序,你需要下载一个能理解这种语言的程序,就像浏览器理解JavaScript一样。
下载Python
为了完成本课程的学习,你需要从Python官网下载这个程序。Python主要有两个版本:Python 2和Python 3。我们将使用Python 3。
以下是下载步骤:
- 访问Python官网。
- 点击“Quick links”下的下载链接,进入Python 3的下载页面。
- 根据你的操作系统选择对应的安装包:
- Linux用户:可以使用包管理器安装Python 3。
- Windows用户:点击对应的Windows安装链接。
- macOS用户:点击对应的macOS安装链接。
对于使用较新版本macOS的用户,可能还需要安装一个名为“Tkinter”的组件,以便运行我们用来编写Python的程序。
如果你是Mac用户,请滚动到页面底部,点击相关链接,然后下载并安装最新版本的ActiveTcl。
编写Python的工具:IDLE

下载并安装Python后,我们将使用一个名为IDLE的程序来编写Python代码。IDLE是以Monty Python成员Eric Idle的名字命名的,它会随Python一同安装。
启动IDLE非常简单:
- 在你的电脑上找到IDLE图标。
- 双击该图标。
启动后,你应该能看到一个类似下图的窗口,这表明你的Python环境已经准备就绪。



本节课中,我们一起学习了程序的基本概念,并完成了Python编程环境的搭建。我们了解到程序是指令的集合,并成功从官网下载了Python 3,同时认识了我们将要使用的代码编辑器IDLE。现在,你的电脑已经准备好运行Python程序了。
003:Python 作为计算器 🧮

在本节课中,我们将开始学习编程。我们将这节课称为“Python 作为计算器”,因为我们将重点学习数学表达式。在本节课以及整个课程中,我们将使用 IDLE,这是一个随 Python 安装包一同提供的程序。

启动 IDLE 与 Shell 窗口
现在,让我们切换到 IDLE 并开始编程。我们看到的这个窗口叫做 Shell。在这里,我们可以尝试 Python 指令并探索 Python 的功能。>>> 这个符号被称为 提示符。在提示符旁边,我们将输入 Python 指令,当我们按下回车键时,Python 就会执行这些指令。
例如,让我们输入指令 2 + 3 并让 Python 执行它。需要注意的是,在我按下回车键之前,什么都不会发生。
让我们看另一个表达式:6 - 2。同样,直到我按下回车键,Python 才会执行这个表达式。另一个表达式可以是 7 * 3。
到目前为止,我们已经看到了三个运算符:加法、减法和乘法。
更多数学运算符
另一个运算是 幂运算。我们将 2 ** 5 这个表达式读作“2 的 5 次方”。
接下来,让我们计算 4 / 2。当 Python 执行这个表达式时,我们得到的结果看起来与之前略有不同。表达式的结果是 2.0,而不仅仅是 2。
这里我们看到,Python 有多种数据类型。其中两种数值类型是 int(代表整数)和 float(代表浮点数)。这个除法运算给了我们一个浮点数结果。
让我们探索更多除法的例子。5 / 2 得到 2.5。我们预期 2 / 3 会得到 0.66666...(6 无限循环)。它确实给出了这个结果,只不过不是无限个 6,而是受限于可用的有效数字位数。Python 可用的内存有限,因此我们得到的数字 2 / 3 的结果是真实数值的一个近似值。
浮点数是对实数的近似。
另一个例子是 5 / 3。我们可以看到结果是一个近似值 1.66666,最后一位实际上是 7。同样,7 / 3 得到 2.33333,最后一位是 5。/ 运算符执行的是浮点数除法。由于浮点数是实数的近似值,存在一些不精确性,这就是我们在这里看到的情况。
整数除法与取余运算
Python 有第二种除法,称为 整数除法。我们使用 // 运算符。让我们用整数除法计算 4 // 2。当 Python 执行这个表达式时,结果将是一个 int 类型,而不是 float。
当我们用整数除法计算 2 // 3 时,我们也得到一个整数 0。理解这个结果的方式是,我们可以将 2 / 3 重写为 0 + 2/3,而我们得到的是结果的整数部分。
让我们考虑 5 // 3。结果同样是整数 1。我们可以将 5 / 3 重写为 1 + 2/3,而我们得到了 1。
用整数除法计算 7 // 3,我们得到 2。这可以重写为 2 + 1/3。
当我们进行整数除法时,只得到了除法结果的一部分。另一部分我们可以使用一个不同的运算符来获取,这个运算符叫做 取余运算符 或 模运算符。我们使用百分号 % 来表示这个操作。
4 % 2 的读法是“4 模 2”,结果是 0,因为 4 除以 2 没有余数。
2 % 3 的余数是 2。5 % 3 的余数是 2。再回顾一下,我们可以将 5 / 3 重写为 1 + 2/3。当我们计算 5 // 3 时,我们得到整数部分 1。当我们计算 5 % 3 时,我们得到分数部分的分子 2。
计算 7 % 3,我们得到 1,因为该表达式可以重写为 2 + 1/3,我们得到了那个 1。
组合表达式与运算顺序
现在我们已经分别使用了这些运算符,让我们将它们组合成表达式。
3 + 4 - 5:运算是从左到右执行的,3 加 4,然后从结果中减去 5。
4 + 5 * 3:在这种情况下,运算顺序规定乘法应先于加法执行。5 乘以 3,然后将结果与 4 相加。
下一个表达式将使用负数。我们用于减法的负号 - 也可以与单个数字一起使用,表示取负。-10 * 3 + 5 ** 3。运算顺序将如下应用:
- 首先计算
5 ** 3,因为幂运算的优先级最高。 - 然后,这里的负号首先应用于
10。 - 结果
-10乘以3。 - 最后,使用加法将这些值组合起来。
就像在常规数学中一样,我们可以使用括号来覆盖运算符的优先级。我可以让表达式 (4 + 5) 先计算,然后再将结果乘以 3。同样,我可以通过使用括号让第二个表达式中的加法先发生。
语法错误与语义错误
到目前为止,我们给 Python 的每条指令都产生了一个结果。例如,当我们要求 Python 执行表达式 2 + 3 时,它给了我们结果 5。这是因为表达式 2 + 3 遵循 Python 语言的语法。语法 是规定哪些符号组合是合法的规则,而 2 + 3 在 Python 中是一个有效的表达式。
符号组合 3 + 在语法上是无效的。因此,当我要求 Python 执行这个表达式时,我们得到一个错误。这是我们的第一个 语法错误,在整个课程中我们会看到更多。
Python 不理解如何处理那个符号组合,它无法返回结果,所以它给出了一个错误。
另一个会导致语法错误的符号组合是单独使用幂运算符而不提供任何操作数 **。
我们也可以写一个像我们刚才看到的表达式,只是没有左括号,只包含一个右括号 )。这会产生语法错误。
如果我们包含一个左括号 ( 但没有右括号,那么当我们按下回车键时,会出现不同的情况——看起来什么也没发生。这是因为 Python 允许指令跨越多行,当我按下回车键时,它实际上在等待右括号。直到我给出那个右括号并按下回车键,表达式才会被执行。
除了语法错误,我们还会遇到 语义错误。当特定表达式的含义无效时,就会发生语义错误。
例如,2 + 3 的语法是有效的,这是一个合法的符号组合。该表达式的语义是 2 加上 3,所以这没问题。
4 / 0 在语法上是有效的。我们可以使用这个符号组合。然而,这个表达式的含义是无效的。不可能将一个数除以 0。因此,我们得到一个 零除错误,这是一个语义错误。
总结

在本节课中,我们一起学习了如何将 Python 用作计算器。我们介绍了基本的数学运算符(+, -, *, /, //, %, **),理解了整数和浮点数的区别,并掌握了运算顺序的规则。我们还初步认识了 IDLE 的 Shell 环境,并了解了两种常见的错误类型:语法错误(无效的符号组合)和 语义错误(表达式含义无效,如除以零)。通过使用括号,我们可以控制表达式的计算顺序。这些基础知识是后续所有编程学习的基石。
004:Python与计算机内存 💾

在本节课中,我们将要学习程序如何存储和跟踪信息。我们将了解计算机内存的基本模型,以及Python如何使用变量来管理内存中的数据。




程序需要跟踪大量信息。其中一部分对用户是可见的,例如电子表格中的数值,但大部分信息是隐藏的。例如,当一个电子表格程序计算一组数字的平均值时,程序首先将所有数字相加,然后计算数字的个数,最后进行相应的除法运算。


所有这些数值都存储在计算机内存中。在本课程中,我们将计算机内存视为一个非常长的存储位置列表,每个位置都有一个唯一的编号,称为内存地址。


我们通常会在内存地址前加上前缀 X,以便与其他数字区分开来。例如,X201 表示内存地址 201。


你可以将这些内存地址想象成长长街道上的门牌号。


正如我们所说,数值存储在计算机内存中。因此,数字 8.5 可能位于内存地址 X34,而数字 44 可能位于内存地址 X35。

程序需要一种方法来跟踪所有这些数值。它们通过使用一种叫做变量的东西来实现。变量是内存中的一个命名位置。

Python在一个独立于数值的内存区域中跟踪变量。我们可能有一个名为 shoe_size 的变量,它存储着内存地址 X34。这意味着 shoe_size 指向数值 8.5。


同样,我们可能有一个名为 paul_age 的变量,其值为 44。这是通过将内存地址 X35 存储在变量 paul_age 中来实现的。
Python的一个优点是,你无需担心具体选择了哪些内存地址。Python会为你处理这一切。实际上,你甚至无法选择使用哪些内存地址。然而,在编程时,在脑海中保持这个模型非常有帮助。




以下是本课程中将使用的一些术语:
- 数值拥有一个内存地址。
- 变量包含一个内存地址。
- 变量引用或指向一个数值。

以下是一些我们如何使用这些术语的例子:
- 数值
8.5拥有内存地址X34。 - 变量
shoe_size包含内存地址X34。 - 变量
shoe_size的值是8.5。 - 变量
shoe_size引用数值8.5。 - 变量
shoe_size指向数值8.5。




本节课中,我们一起学习了计算机内存的基本概念。我们了解到内存由一系列带有地址的位置组成,而Python变量则是这些位置的“名字”,它们存储着地址并指向具体的数值。理解变量、内存地址和数值之间的关系,是学习编程的重要基础。
005:变量 📦
在本节课中,我们将学习如何记住Python表达式的计算结果,以便后续使用这些值。我们将通过计算三角形面积的例子来探索这一概念。
概述
上一节我们介绍了如何在Python中计算表达式。本节中,我们来看看如何将计算结果保存起来,以便在后续的代码中重复使用。我们将学习变量和赋值语句,这是编程中存储和操作数据的基础。
计算三角形面积
作为回顾,如果我们知道三角形的底边长度和高度,那么其面积的计算公式为:
面积 = (底边长度 × 高度) / 2
例如,如果高度是12,底边长度是20,那么面积就是 (20 * 12) / 2。
让我们在Python中探索这个计算过程。
引入变量
你已经知道如何在Python中输入并计算表达式。但为了计算多个三角形的面积,我们将向你展示如何引入像 base 和 height 这样的词来代表具体的数值。这通过赋值语句来完成。
以下是一个例子:
base = 20
这条语句会计算 20 这个值,并将其与变量 base 关联起来。base 被称为变量,因为它的值可以改变,我们稍后会看到这一点。
我们可以为另一个变量 height 做类似的操作:
height = 12
之后,这两个名字就可以在Python交互式环境中像它们的值一样被使用。因此,base 的值是20,height 关联的值是12。
理解赋值语句
请注意,编程中的等号 = 与数学中的等号含义非常不同。在编程中,= 表示赋值,即将右边的值赋予左边的变量。
由于计算机程序可能变得相当复杂,我们将引入一种描述变量及其值的图示方法。
我们会为变量 base 画一个旁边的方框。在Python中,值 20 并不直接放在这个方框里。相反,像Python中的所有其他值一样,20 存储在特定的内存地址中。
假设在内存地址 x3 处存储着值 20。赋值语句 base = 20 会获取这个地址 x3,并将其放入与 base 关联的方框中。因此,base 包含 x3,这意味着它指向存储着值 20 的内存地址 x3。
类似地,变量 height 也有一个方框来跟踪其值,其值也是一个内存地址。假设是 x7,它指向存储着值 12 的地方。这些地址是任意的,由Python管理,我们无需关心具体是什么地址,只需理解变量与其值之间的这种关联关系即可。
在表达式中使用变量
回到我们的编程问题,我们可以在表达式中使用 base 和 height:
area = base * height / 2
执行 base * height / 2 会得到与直接计算 20 * 12 / 2 相同的结果。我们还可以将这个表达式的结果赋值给另一个变量,我们称之为 area。
让我们检查一下 area 的值:
print(area) # 输出:120.0
修改变量的值
现在,我们将再次使用赋值语句为 base 和 height 赋予新的值:
base = 2.5
height = 7
在我们的图示中,这会将一个新值 2.5 放在某个内存地址(例如 x4)处,然后赋值语句会用 x4 替换掉 base 方框中原先的 x3。这样,base 就不再指向 20,而是关联到 2.5。
同样,height = 7 会将值 7 放在某个地址(例如 x1)处,并将 x1 放入变量 height 的方框中,从而断开旧的联系,使 height 现在指向 7。
如果我们再次计算面积:
area = base * height / 2
print(area) # 输出:8.75
新的面积是8.75,这正是 2.5 * 7 / 2 的结果。
赋值语句的规则
每个赋值语句都具有以下形式:
variable = expression
以下是执行赋值语句的规则,你应该尽力记住它们,因为这在课程中会反复出现:
- 计算等号右边的表达式。
- 这个表达式的值有一个内存地址。
- 存储这个内存地址到等号左边的变量中。
变量的命名规则
每种编程语言都有一套关于合法名称的规则。在Python中:
以下是有效的变量名规则:
- 变量名必须以字母或下划线
_开头。 - 变量名可以包含字母、数字和下划线。
- 允许使用大写和小写字母。
格式错误的变量名会导致语法错误。例如:
2nd_base = 10 # 错误:不能以数字开头
my-variable = 5 # 错误:包含了连字符“-”
Python也是大小写敏感的。这意味着 seconds_in_minute 和 Seconds_In_Minute 是两个不同的变量。
seconds_in_minute = 60
Seconds_In_Minute = 60 # 这是另一个不同的变量

第一个变量名 seconds_in_minute 显然比第二个带有大写字母的版本更具可读性。选择好的变量名非常重要,因为程序可能会被阅读和改进很多年。
Python的命名约定
每种编程语言都有一套命名约定,就像网站有特定的风格和布局一样。在Python中,大多数变量名只使用小写字母,并用下划线来分隔单词。我们称之为蛇形命名法。

例如:
user_nametotal_countis_valid_input
总结
本节课中,我们一起学习了Python中变量的核心概念。我们了解了如何使用赋值语句(=)将值存储到变量中,以及变量在内存中如何通过地址指向实际的值。我们还学习了如何修改变量的值,以及在表达式中使用变量进行计算。最后,我们掌握了Python中变量名的命名规则和推荐的蛇形命名法约定。

理解变量是编写任何程序的基础,它使我们能够保存数据、传递信息并构建复杂的逻辑。在接下来的课程中,我们将继续利用变量来探索更多的编程概念。
006:可视化赋值语句 🖥️

在本节课中,我们将学习如何使用可视化工具来观察程序执行过程中计算机内存的状态变化。这有助于我们更直观地理解变量和赋值语句的工作原理。
概述 📋

我们已经学习了变量和赋值语句的基本概念。现在,我们将探索一个名为“可视化器”的工具。这个工具能让我们在程序执行时,清晰地看到计算机内存中变量的状态变化。
可视化赋值语句执行过程
以下是我们要分析的代码片段,包含三个赋值语句:

x = 1
y = x + 2
x = 7
第一个语句将值 1 赋给变量 x。
第二个语句计算表达式 x + 2 的值,并将结果赋给变量 y。
第三个语句将值 7 再次赋给变量 x。这意味着 x 的值在第三行会发生变化。

逐步执行与内存状态观察
让我们使用可视化器来逐步执行这些语句,并观察内存的变化。
初始状态
程序尚未开始执行,因此内存中还没有创建任何变量。可视化器中的高亮行表示即将执行的语句。

执行第一行语句
当我们点击“向前”执行第一行 x = 1 时,会发生以下情况:
- 计算机会在内存中创建一个值为
1的对象。 - 变量
x将存储这个对象的内存地址(即指向它)。
在可视化器中,我们可以看到变量x的方框内有一个箭头,指向内存中值为1的存储块。此时,x引用了值1。
执行第二行语句
接下来,我们准备执行第二行 y = x + 2。执行过程分为两步:
- 计算右侧表达式:计算
x + 2的值。由于x当前是1,所以1 + 2的结果是3。 - 赋值:将结果
3的内存地址存储到变量y中。
点击执行后,我们确实看到:
- 内存中创建了一个值为
3的新对象。 - 变量
y的方框内出现箭头,指向这个值为3的对象。 - 将鼠标悬停在
y上,可以清楚地看到它与值3之间的引用关系。
重要提示:赋值语句 y = x + 2 与数学等式有本质区别。它并不意味着 y 永远等于 x + 2。它只是在执行这一行代码的瞬间,将当时 x + 2 的计算结果赋给 y。
执行第三行语句


现在,我们要执行第三行 x = 7。这将清晰地展示变量值的独立性。
当我们点击执行时,会发生以下变化:
- 计算机会在内存中创建值为
7的新对象。 - 变量
x的引用被更新,转而指向这个新对象(值为7)。 - 关键观察:变量
y的引用没有改变,它仍然指向值为3的对象。

因此,执行完毕后,x 的值变为 7,而 y 的值仍然是 3。这证明了赋值语句只会改变被赋值变量(本例中是 x)的引用,不会影响其他变量(本例中是 y)。
总结 🎯
本节课中,我们一起学习了:
- 使用可视化工具跟踪程序执行和内存状态。
- 逐步分析了
x = 1,y = x + 2,x = 7这三个赋值语句的执行过程。 - 理解了赋值语句的两个核心步骤:计算右侧表达式和将结果地址存入左侧变量。
- 明确了赋值语句
=不是声明永恒等式,它只改变特定变量在特定时刻的值。 - 通过最后一步
x = 7的执行,直观地看到了变量x值的改变并不会影响之前已经赋值的变量y。

通过可视化观察,我们巩固了对变量、内存地址和赋值操作的理解,这是编程中非常基础且重要的概念。
007:内置函数

在本节课中,我们将要学习Python中的内置函数。我们将了解什么是函数调用,如何查找可用的内置函数,以及如何使用help函数来了解特定函数的功能。
什么是内置函数?
上一节我们介绍了Python的数学运算符,如乘法和减法。然而,除了数学运算,我们还需要执行许多其他操作,但并没有足够多的符号来表示所有这些操作。

例如,我们可能想找出几个数字中最大的一个。这个操作就没有对应的符号。因此,Python提供了一组内置函数,允许我们执行这些操作。本节中我们来看看这些函数。
使用 max 函数
让我们从刚才提到的例子开始:找出几个数字中最大的一个。我们将使用的Python内置函数是 max 函数。
函数调用的形式是:函数名、一个开括号、一个由逗号分隔的表达式列表(称为参数),以及一个闭括号。当调用函数时,Python首先计算参数的值,然后调用函数。
以下是 max 函数的一些调用示例:
-
调用
max并传入两个参数:max(36.7, 23.4)这将返回
36.7。 -
调用
max并传入多个参数:max(36.7, 23.4, 100.0, 98.6)这将返回
100.0。 -
调用
max并传入整数参数:max(10, 20, 30)这将返回
30。 -
调用
max并传入不同类型的参数:max(10.5, 20)这将返回
20。
如何发现内置函数?
我们如何知道 max 函数存在?如何知道它需要什么参数以及会返回什么值?
我们可以使用另一个名为 dir 的内置函数来查找可用的内置函数。调用 dir(__builtins__) 会返回一个很长的列表。目前,我们只需关注那些完全由小写字母组成的函数名。
使用 help 函数了解详情
内置函数 help 可以用来获取关于某个函数的详细信息。例如,我们可以用它来了解 abs 函数。

调用 help(abs) 会返回对 abs 函数的描述。从中我们可以看到,abs 接受一个数字参数,并返回该数字的绝对值。
让我们尝试使用 abs 函数:
-
传入一个浮点数参数:
abs(-3.5)这将返回
3.5。 -
传入一个整数参数:
abs(-10)这将返回
10。
探索 pow 函数
现在,我们使用 help 来了解另一个内置函数 pow。调用 help(pow) 会显示其描述。
从描述中可以看到,pow 函数有两个必需参数(x 和 y),以及一个在方括号内的可选参数(z)。这意味着调用 pow 时,至少需要两个参数,也可以提供第三个参数。pow 函数会返回一个数字。
让我们调用 pow 函数:
- 传入两个参数:
这将返回pow(2, 5)32。实际上,这等价于使用幂运算符计算 2 的 5 次方:2 ** 5。

本节课中我们一起学习了Python内置函数的基本概念。我们了解了如何使用 max 函数求最大值,如何使用 dir 函数发现可用函数,以及如何使用 help 函数获取函数说明。我们还实践了 abs 和 pow 函数的使用。理解如何调用和探索内置函数是掌握Python编程的重要一步。
008:07_定义函数 🧑💻

在本节课中,我们将学习如何定义自己的函数。除了使用Python内置的函数,我们还可以创建自定义函数来执行特定任务。

函数定义基础
上一节我们介绍了使用内置函数,本节中我们来看看如何创建自己的函数。让我们从一个简单的数学例子开始。
函数 S(x) 接受参数 x 并计算其平方值。以下是该函数在Python中的表示:
def f(x):
return x * x
def 关键字向Python表明我们正在定义一个函数。f 是函数名。x 被称为函数的参数,它是一个变量,其值将在函数被调用时提供。冒号表示接下来是函数被调用时要执行的代码。return 关键字表示我们将传回一个值。
return语句的一般形式是关键字 return 后跟一个表达式。执行return语句的规则是:首先,计算表达式以产生一个内存地址;其次,将该内存地址传回给调用者。
函数定义的一般形式是:关键字 def,后跟函数名,然后是零个或多个由逗号分隔的参数。接下来是函数体,它由一个或多个语句组成,通常以return语句结束。请注意,函数体是缩进的。
调用函数
让我们调用函数 f,并传入参数 3。
f(3)
当函数被调用时,参数 x 被赋值为 3 的内存地址。我们可以将其想象成一个赋值语句:x = 3。执行函数调用后,函数返回结果 9。
函数调用实际上是表达式,因此我们可以使用变量来存储结果。
result = f(3)
回忆一下执行赋值语句的规则:首先,计算右侧的表达式以产生一个内存地址;其次,将该内存地址存储在变量中。因此,result 获得了调用 f(3) 的结果,即 9。
编写实用函数:计算三角形面积
现在让我们定义一个更实用的函数。回忆变量课程中计算三角形面积的方法:底乘以高,然后除以二。
我们将编写一个函数来计算三角形的面积。
def area(base, height):
return base * height / 2
我们以关键字 def 开始。接下来是函数名,这里使用 area。函数名后是函数的参数,本例中有两个参数:base 和 height。我们以冒号结束这一行,然后编写函数体。函数将返回 base * height / 2 的结果。
让我们调用这个函数。我们将执行 area 并传入两个参数:3 和 4。
area(3, 4)

参数 base 获得值 3,参数 height 获得值 4。然后计算表达式 base * height / 2 并返回结果 6.0。

我们可以再次调用它,这次传入一个整数和一个浮点数。
area(5, 2.5)
执行函数调用的扩展规则是:
- 计算参数以产生内存地址。
- 将这些内存地址存储在相应的参数中。
- 执行函数体。
将函数保存在文件中
如果我们重启Python shell(通过Shell菜单点击Restart),则会丢失在shell中完成的所有工作,包括 area 函数的定义。此时尝试调用 area 会导致NameError,因为 area 未定义。
大多数Python程序都保存在文件中。让我们创建一个新文件并将函数定义 area 保存在其中。
转到 File -> New Window。将我们的函数定义放入其中。我们将此文件保存为 triangle.py。我们所有的Python程序都将保存在 .py 文件中。
调整窗口大小,以便在左侧看到shell,在右侧看到我们的 triangle.py 文件。
现在我们的 area 函数定义在 triangle.py 中,我们可以运行这个文件。点击 Run -> Run Module。这使得 area 函数定义在shell中可用。现在当我们在shell中调用 area 时,它知道 area 是什么并能够执行该函数。
在文件中定义多个函数
我们可以在 triangle.py 文件中定义多个函数。让我们添加另一个函数定义。
我们将定义一个计算三角形周长的函数。我们将传入三条边:side1, side2, side3。函数将返回这些边的总和。
def perimeter(side1, side2, side3):
return side1 + side2 + side3
此时我已保存 triangle.py。如果我返回shell并尝试执行 perimeter 函数,将会看到一个错误。这是因为虽然函数已保存在 triangle.py 中,但我们尚未运行 triangle.py,因此shell不知道 perimeter 是什么。
在shell中执行函数之前,我们需要运行该模块。运行模块后,我就可以调用 perimeter 并执行它了。

本节课中我们一起学习了如何定义自定义函数,包括函数定义的结构、参数的使用、return语句的作用,以及如何将函数保存在 .py 文件中并通过运行模块使其在交互式环境中可用。我们还学习了在一个文件中定义多个函数的方法。
009:字符串类型 🧵

在本节课中,我们将要学习Python编程中的一个基础且无处不在的数据类型:字符串。我们将了解如何创建字符串、处理字符串中的特殊字符,以及如何对字符串进行基本的操作。
字符串字面量
字符串字面量是由字符组成的序列。在Python中,这种类型被称为 str。

字符串以单引号或双引号开始和结束。例如:'hello' 或 "How are you?"。
字符串中可以包含标点符号,正如我们刚才所见,也可以包含连字符。
字符串是值,可以用于赋值语句中。
例如,在晴天我们可能想说:“多么美好的一天。” 如果是暴风雨天,我们可能想说:“浑身湿透了。”

引号与转义字符

但是,我们立即遇到了一个问题。当我们尝试在单引号字符串内部使用单引号时,Python会产生语法错误,因为它无法区分字符串的结束和内部的引号。
为了解决这个问题,Python允许你使用双引号来定义字符串。这样,内部的单引号就不会引起混淆。
我们还有另一种选择:使用转义字符(转义序列)。如果我们使用反斜杠 \,Python会将其后的单引号视为字符串内容的一部分,而不是字符串的结束标记。在输出时,我们不会看到这个反斜杠,并且字符串两端的引号可能会被转换为双引号。
示例代码:
# 使用双引号
sunny_greeting = "It's a beautiful day."
# 使用转义字符
stormy_greeting = 'It\'s all wet.'
字符串操作
我们可以使用加号 + 运算符来连接字符串。需要注意的是,连接时我们必须自己添加空格。
示例代码:
start = "I love "
noun = "programming"
result = start + noun # 结果为 "I love programming"
利用这个特性,我们可以编写一个简单的“填词游戏”(madlibs)。例如,我们询问朋友一个名词,然后将它插入到一个句子中。
我们还可以使用乘法运算符 * 来重复一个字符串。与常规数学运算一样,乘法的优先级高于加法。我们可以使用括号 () 来改变运算顺序。
示例代码:
word = "Ha"
repeated = word * 3 # 结果为 "HaHaHa"
combined = "Start" + word * 2 # 结果为 "StartHaHa"
combined_with_parens = ("Start" + word) * 2 # 结果为 "StartHaStartHa"
类型错误与限制
然而,我们不能将一个字符串与一个整数或浮点数相加。Python不会隐式地将数字转换为字符串,尝试这样做会导致类型错误(TypeError)。
示例代码:
# 这会导致 TypeError
# result = "Number: " + 42

同样,我们也不能对两个字符串使用乘法运算符,即使字符串内部是数字。其他运算符如减法 -、除法 / 和幂运算 ** 也不能用于两个字符串之间。所有这些操作都会引发类型错误。
示例代码:
# 以下操作都会导致 TypeError
# result = "hello" - "o"
# result = "10" / "2"
# result = "a" ** 2
总结
本节课中,我们一起学习了Python中字符串类型的基础知识。我们了解了如何用单引号或双引号创建字符串,如何处理字符串内的引号冲突(使用不同的引号或转义字符),以及如何使用 + 和 * 运算符进行字符串的连接与重复。同时,我们也明确了字符串与数字不能直接相加,以及字符串之间不支持减、除、乘方等运算的限制。掌握这些是进行更复杂文本处理的第一步。
010:输入输出与字符串格式化 📝


在本节课中,我们将深入学习字符串的更多操作,特别是字符串格式化。同时,我们将编写一些交互式程序,学习如何向用户显示信息以及如何从用户那里获取输入。
使用 print 函数输出信息
上一节我们介绍了函数的基本概念,本节中我们来看看如何使用 print 函数向屏幕输出信息。
我们调用Python内置的 print 函数,并向其传递一个参数。例如,传递字符串 "Ho"。"Ho" 将被显示在屏幕上。请注意,显示时没有引号。引号仅用于Python内部的字符串表示,用户是看不到的。
再次调用 print,这次传递一个数学表达式作为参数。例如,表达式 3 + 7 - 3。这个表达式会先被求值,然后将结果打印到屏幕上。
第三次调用 print,这次传递两个参数:两个字符串 "hello" 和 "there"。当字符串被显示时,默认情况下它们之间会有一个空格。因此,屏幕上打印的是 "hello there"。
定义并比较两个函数
接下来,我们将在名为 calc.py 的文件中定义两个函数。这两个函数都用于计算一个数的平方,但一个函数返回结果,另一个函数打印结果。
我们以关键字 def 开始函数定义。第一个函数名为 square_return。它接受一个参数 numb。这个函数的功能是返回 numb 的平方。其核心公式为:
return numb ** 2
第二个函数名为 square_print。它也接受一个参数 nu。它同样计算 nu 的平方,但不是返回值,而是打印它。为了让打印输出与 square_return 的返回值有所区别,我们在 print 函数的参数列表中添加了一个字符串前缀。其核心代码为:
print('The square of nu is', nu ** 2)
运行模块后,我们可以在Shell中调用这两个函数。
调用 square_return(4) 并将结果存储在变量 answer_return 中。执行时,屏幕上没有打印任何内容。检查 answer_return 变量,它指向值 16。因此,square_return 返回了 16 的内存地址。
调用 square_print(4)。这次,屏幕上显示 "The square of nu is 16"。检查存储结果的变量 answer_print,我们发现它没有值。当一个函数没有执行 return 语句时,Python会返回 None,answer_print 就指向 None。
因为 answer_return 指向一个数值,我们可以在数学表达式中使用它,例如 answer_return * 5。然而,answer_print 指向 None,尝试进行数学运算会导致错误。


使用 input 函数获取用户输入
我们已经知道如何使用 print 向用户显示信息。下一步是使用Python内置的 input 函数从用户那里获取信息。
我们调用 input 函数,并向其传递一个字符串参数。例如,询问用户 "What is your name?"。函数执行时,该字符串会打印到屏幕上,程序会等待用户在提示符处输入内容。
用户输入 "John" 并按下回车后,函数执行完毕。用户输入的字符串 "John" 作为函数的返回值,其类型是字符串。
再次调用 input 函数,这次将返回的结果存储在一个名为 name 的变量中。用户输入他们的名字,该值作为字符串返回,其内存地址存储在 name 变量中。检查 name 的内容,它指向字符串 "John"。
现在使用 input 提示用户输入位置。字符串 "What is your location?" 被打印,用户输入 "Toronto"。该值由函数返回,因此 location 变量指向 "Toronto",name 指向 "John"。
组合输出信息
现在,我们使用 name 和 location 这两个变量向用户显示一条消息。调用 print 函数,传递三个参数:name、字符串 "lives in" 和 location。这样,打印出的结果是 "John lives in Toronto"。
之前提到,name 和 location 都指向字符串。实际上,input 函数返回的所有值的类型都是字符串。
再次调用 input,询问 "How many cups of coffee have you had today?"。用户输入 2,这个值由 input 函数作为字符串返回。因此,num_coffee 指向字符串 "2",而不是整数 2。
字符串的多种表示方式


你已经见过使用单引号和双引号表示字符串。现在,我想向你展示第三种字符串格式:三引号字符串。
三引号字符串可以跨越多行。例如,打印一个三引号字符串,第一行是 "How",第二行是 "are",第三行是 "you"。输出效果是每个单词单独一行。
使用相同的字符串,但不打印它,而是将其存储在一个变量 s 中。当我们检查变量 s 的内容时,发现它与我们输入时看起来有很大不同。首先,它不再有三引号,而是单引号。其次,在我按下回车键的地方,我们看到了 \n。这个字符称为换行符,是Python中的一个特殊字符。
反斜杠符号是转义字符,\n 这个组合是转义序列“换行”。
还有其他几个特殊字符。例如,打印数字三、四和五,并用制表符分隔。我们使用 3,然后对于制表符,使用转义序列 \t,接着是 4、\t 和 5。输出结果是 3 4 5。
除了换行和制表符,还有其他一些有用的转义序列:
\\:当我们需要在字符串中打印一个单独的反斜杠时使用。\':当我们需要在字符串中包含单引号时使用。例如:'don\'t'。\":当我们需要在字符串中包含双引号时使用。例如:'He says, \"Hi.\"'。
总结

本节课中我们一起学习了:
- 如何使用
print函数输出不同类型的数据(字符串、表达式结果、多个参数)。 - 如何定义函数,并理解
return返回值与print打印输出的区别。 - 如何使用
input函数获取用户输入,并理解其返回值始终是字符串类型。 - 如何组合变量和字符串向用户输出信息。
- 字符串的多种表示方式:单引号、双引号和三引号。
- 常见的转义序列,如
\n(换行)、\t(制表符)、\\(反斜杠)、\'(单引号)和\"(双引号),以及它们在字符串中的用途。
011:05_文档字符串与函数帮助

在本节课中,我们将要学习如何为自定义函数编写文档字符串,以及如何利用Python内置的help系统来查看函数信息。
查看内置函数的帮助信息
上一节我们介绍了函数的基本概念,本节中我们来看看如何获取函数的说明信息。Python内置了一个名为help的函数,它可以显示关于某个函数的信息。

例如,当你调用help函数并传入绝对值函数abs时,你会得到一个关于其功能的清晰描述。
help(abs)
此外,在IDLE等集成开发环境中,当你开始输入一个函数调用时,它也会自动提供信息。例如,开始输入abs(时,会弹出一个黄色小框,显示参数类型和返回值类型。
为自定义函数编写文档字符串
你可以通过编写一种特定格式的文档,称为文档字符串,来为你自己的函数提供同样的信息。
以下是我们为上节课编写的三角形面积函数area所写的文档字符串。注意,我们使用了三引号字符串,因为描述内容跨越多行。
def area(base, height):
"""
返回以给定底边和高度计算的三角形面积。
参数:
base (float): 三角形的底边长度。
height (float): 三角形的高度。
返回:
float: 计算出的三角形面积。
"""
return base * height / 2
现在,我们运行包含此函数的模块,使其在Python Shell中可用。当我调用help(area)时,就能看到我们在文档字符串中编写的内容。
help(area)
此外,文档字符串的第一行内容,会在我开始输入area(调用时显示出来,就像内置函数一样。
文档字符串的重要性与规范
以下是编写和使用文档字符串的几个要点:
- 描述功能:清晰说明函数的作用。
- 说明参数:列出每个参数的名称、预期类型和作用。
- 说明返回值:明确函数返回什么类型的值。
- 接入帮助系统:使你自定义的函数能完美接入Python的
help系统。
从今以后,我们将始终为我们的函数提供文档字符串,这既能描述函数的功能,也能接入Python的帮助系统,使代码更易读、更易维护。
总结

本节课中我们一起学习了文档字符串。我们了解了如何使用help()函数查看内置函数的说明,更重要的是,我们学会了如何通过编写三引号格式的文档字符串,为我们自己创建的函数添加详细的说明文档,并使其能够集成到Python的交互式帮助系统中。这是编写清晰、专业代码的重要一步。
012:函数设计方法 🧩


在本节课中,我们将学习一个名为“设计配方”的系列步骤。你可以使用这个方法来编写自己的函数。
概述
我们将通过开发一个将华氏温度转换为摄氏温度的函数,来详细解释设计配方的每个步骤。这个方法将帮助你系统地构建一个包含五个部分(加上测试部分)的完整函数定义。

设计配方详解

上一节我们介绍了设计配方的概念,本节中我们来看看它的具体组成部分。一个完整的函数定义通常包含以下五个部分:
- 函数头:包含函数名和参数。
- 类型契约:描述参数和返回值的预期类型。
- 描述:用文字说明函数的功能。
- 使用示例:展示函数调用示例及预期返回值。
- 函数体:实现函数功能的实际代码。
我们的设计配方将指导你完成这五个部分的开发,并额外增加一个步骤:在编写完成后测试函数。
实践:创建温度转换函数
现在,让我们应用设计配方来解决一个具体问题:美国使用华氏度,加拿大使用摄氏度。我们需要编写一个在这两种温度单位之间进行转换的函数。
第一步:构思使用示例
首先,思考你希望函数做什么,并为你将要编写的函数构思一两个调用示例。你需要为函数想一个名字(后续可以修改)。函数名通常是动词或动词短语。
以下是构思的示例:
- 调用
convert_to_celsius(32),我们希望返回0。 - 调用
convert_to_celsius(212),我们希望返回100。
第二步:确定类型契约
接下来,确定参数的类型和返回值的类型。从示例看,参数和返回值都应该是数字(整数或浮点数)。
因此,类型契约可以写为:(number) -> number
第三步:编写函数头
在有了调用示例和对类型的猜测后,可以编写函数头。选择有意义的参数名,以便于理解。
根据以上信息,我们的函数头是:
def convert_to_celsius(fahrenheit):
第四步:编写函数描述
现在,你应该对函数的功能有了清晰的认识,是时候用文字描述它了。描述中应明确提及参数并说明返回值。
我们的函数描述是:
返回与华氏度数值相等的摄氏度数值。


第五步:编写函数体代码
在仔细思考了示例、契约、函数头和描述之后,编写函数代码会容易得多。根据公式,摄氏温度 = (华氏温度 - 32) * 5 / 9。
因此,函数体代码为:
return (fahrenheit - 32) * 5 / 9
第六步:测试函数
最后,也是最关键的一步,是用你之前构思的示例来测试函数。由于你已经知道了预期的返回值,可以立即判断代码或示例是否正确。
我们运行模块并进行测试:
- 调用
convert_to_celsius(32),预期得到0,但实际得到了14.222...。 - 调用
convert_to_celsius(212),预期得到100,但实际得到了100.0。
第一个结果明显错误。问题在于运算优先级:减法的优先级低于乘法和除法。因此,代码实际计算的是 32 * 5 / 9,最后才减去32。我们需要用括号来修正运算顺序。
修正后的函数体应为:
return (fahrenheit - 32) * 5 / 9
保存并重新运行测试:
convert_to_celsius(32)现在返回0.0。convert_to_celsius(212)返回100.0。
结果与示例基本一致,只是返回的是浮点数而非整数。这是因为在Python中,除法运算总是产生浮点数。这并非计算错误,而是我们的示例和类型契约不够精确。我们可以将契约中的 number 具体化为 float。
总结

本节课中,我们一起学习了函数设计的“配方”。我们逐步完成了从构思示例、确定类型、编写函数头与描述,到最终实现代码和进行测试的全过程。通过为华氏度转摄氏度编写函数这个具体例子,你掌握了创建可靠、文档清晰的自定义函数的系统方法。记住这个配方,它将成为你编程工具箱中的重要工具。
013:函数复用



在本节课中,我们将学习如何复用函数。一旦定义了一个函数,我们就可以在合适的地方反复调用它。我们将重点探讨如何在其他函数定义内部,以及在其他函数调用内部调用我们已定义的函数,从而实现代码的复用。
设计方法回顾
在之前的课程中,我们编写了包含两个函数的 triangle.py 程序。这段代码是根据设计方法的步骤修改后的版本。
回顾一下设计方法,我们首先需要一两个示例,接着是类型契约、函数头、函数描述。然后我们编写函数体并测试函数。
添加新函数:计算半周长
现在,我们为 triangle.py 添加一个新函数,用于计算三角形的半周长。半周长就是周长的一半。
遵循设计方法,第一步是构思示例。我们将调用函数 semiperimeter。该函数需要三条边的长度来进行计算。因此,在函数调用中,我们想象传入三个长度值,返回的结果应该是周长的一半。
让我们再构思一个函数调用示例,这次传入浮点数参数。我们期望返回的结果是 12.9。
设计方法的下一步是类型契约。对于 semiperimeter 函数,参数类型是数字(可能是整数或浮点数),函数返回一个浮点数。
接下来,我们提供函数头。以关键字 def 开头,后跟我们已在示例调用中确定的函数名,并传入三个参数,这里我们称它们为 side1、side2 和 side3。
然后是函数描述。我们需要说明函数将返回什么,并解释参数的用途。所以,我们将返回一个三角形的半周长,该三角形的边长由参数给出。
现在,我们准备编写函数体。这个函数将返回半周长,这意味着我们需要计算三条边的和,然后将结果除以 2。
请注意,semiperimeter 函数体的一部分与 perimeter 函数体的一部分完全相同。与其重复计算周长的代码,我们可以在 semiperimeter 函数内部调用 perimeter 函数。通过复用 perimeter 函数,我们降低了在计算周长时出错的风险,因为该函数已经过测试。
现在该测试 semiperimeter 函数了。我们首先运行模块,然后在交互式环境中通过调用该函数来执行它。我将使用函数文档字符串中相同的示例,我们得到了返回值 6。然后,如果我使用文档字符串中的浮点数(整数和浮点数的混合)调用它,我们得到返回值 12.9。因此,该函数的表现符合我们的预期。
函数调用作为参数
让我们考虑另一个问题。对于这个问题,我们将利用之前编写的 area 函数。回想一下,area 函数有两个参数:底和高,然后返回三角形的面积。
这个问题涉及两块披萨。派对上剩下这两块披萨片。一个饥饿的人试图找出哪一块更大。由于披萨片是三角形,这个人测量了每块披萨片的底和高。
既然我们知道了每块披萨片的底和高,就可以用我们的程序来帮助确定哪一块更大。我们还将使用 Python 的一个内置函数 max 来帮助我们。
因此,我们首先调用 max,传递给 max 的第一个参数将是第一块披萨片的面积(底为 3.8,高为 7.0)。max 函数的第二个参数将是第二块披萨片的面积(底为 3.5,高为 6.8)。当我们执行这个函数时,我们就能找出这两个面积中哪个更大。
现在我们可以检查返回值 13.29(循环小数)对应哪块披萨片。从那个函数调用中我们可以看到,第一块披萨片的面积与最大面积匹配。这意味着获胜的披萨片是这一块。
这个例子表明,我们不仅可以在函数定义内部调用函数(就像在 semiperimeter 例子中那样),还可以将函数调用作为参数传递给其他函数,因为函数调用本身也是表达式。

总结

本节课中,我们一起学习了函数复用的核心概念。我们回顾了设计方法,并实践了通过在其他函数定义内部调用现有函数来构建新函数(如 semiperimeter)。更重要的是,我们了解到函数调用可以作为表达式,直接作为参数传递给其他函数(如 max 函数),这极大地增强了代码的灵活性和复用性。掌握这些技巧,能帮助你编写出更简洁、更健壮的程序。
014:可视化函数调用 🎬
在本节课中,我们将使用Python可视化工具,探索函数调用在计算机内存中是如何被管理的。

概述
我们将通过一个具体的例子,逐步观察当程序调用一个函数时,内存中变量的创建、函数栈帧的生成与销毁,以及返回值是如何传递的。这有助于我们理解程序的执行流程。

函数定义

我们编写了一个名为 convert_to_minutes 的函数,它接收一个表示小时数的参数,并返回对应的分钟数。
def convert_to_minutes(num_hours):
result = num_hours * 60
return result



例如,如果我们用参数 2 调用 convert_to_minutes 函数,我们期望得到返回值 120,因为两小时包含120分钟。请注意代码的缩进,所有属于函数的代码都必须缩进,通常是四个空格。
可视化执行过程

让我们可视化运行这个程序时会发生什么。
函数对象的创建

当我们读取函数定义时,或者说当Python解释器执行到它时,会创建一个名为 convert_to_minutes 的变量。这个变量包含一个函数对象的内存地址。该函数对象包含了关于这个函数的所有信息,包括需要执行的代码和参数。


执行赋值语句
程序现在暂停在第 10 行,这是一个赋值语句。注意,变量 minutes_two 尚未被创建。这将在我们完成执行赋值语句后发生。


首先,我们必须计算等号右侧的表达式,而这个表达式是一个函数调用。一旦这个函数调用返回一个值,minutes_two 变量就会被创建,并存储该返回值的内存地址。

执行函数调用
为了执行这个函数调用,需要先对参数进行求值。这将为值 2 创建一个内存空间(“盒子”)。此外,还会在内存中创建一个新的区域,用于跟踪 convert_to_minutes 函数执行期间发生的情况。
这里是为参数 2 创建的值,以及函数内部的变量 num_hours。

内存结构:堆与调用栈
计算机内存右侧的区域称为 堆(Heap),它包含了程序执行期间创建的所有值。


内存左侧的区域称为 调用栈(Call Stack)。之所以称为栈,是因为它像一叠盘子。当一个函数被调用时,会创建一个栈帧;当该函数返回时,其栈帧会从栈中移除,控制权交还给该函数的调用者。
每个栈帧包含特定于程序该部分的变量。主栈帧包含在任何函数外部创建的变量,而函数的栈帧则包含参数和局部变量。局部变量是指在函数内部创建的任何变量,例如变量 result。
函数内部执行

第 7 行包含一个赋值语句。我们计算右侧表达式:查找 num_hours 的值,它是 2,然后计算 2 * 60 得到 120。因此,我们将看到值 120 被创建,并且变量 result 在 convert_to_minutes 函数内部被创建。result 指向 120,num_hours 指向 2。

接下来,我们将执行 return 语句。这会计算 result 的值(即 120),并产生其内存地址。
函数返回与变量赋值

注意,我们可以看到返回值是什么。当我们再前进一步时,我们将退出 convert_to_minutes 函数。控制权将传递回程序中我们之前所在的位置。

那正是在这里。我们将完成这次以参数 2 对 convert_to_minutes 的调用,它将返回给我们值 120 的内存地址。这应该会创建变量 minutes_two。
确实,这正是发生的情况。对 convert_to_minutes 调用的栈帧消失了。从计算机内存中移除,这就是函数退出时发生的情况。
第二次函数调用

我们将逐步执行另一次对 convert_to_minutes 的调用。这次我们将传入参数 3,并将结果存储在变量 minutes_three 中。让我们观察这个过程。



我们计算 3,这会在堆中创建它。num_hours 现在指向这个值。在第一次调用时,它曾指向值 2;现在,由于我们使用了不同的参数调用它,它指向值 3。
现在,当我计算 num_hours * 60 时,我查看这里的栈帧,发现是 3 * 60,结果是 180。因此,我们将得到一个指向 180 的变量 result。值 120 仍然存在于堆中,因为在这个函数外部,变量 minutes_two 仍然指向它。
现在我们将返回 180。这将产生内存地址 ID5,它将被存储在新创建的、位于主模块中的变量 minutes_three 中。


总结
本节课中,我们一起学习了函数调用的内存管理机制。我们通过可视化工具观察到:
- 函数定义会创建一个函数对象。
- 函数调用时,参数被求值,并在堆中创建对应的值。
- 调用栈会为函数调用创建一个新的栈帧,用于管理参数和局部变量。
- 函数内部执行计算,并将结果返回。
- 函数返回后,其栈帧被销毁,返回值被传递给调用者并赋值给变量。
- 多次调用同一函数时,每次调用都会创建独立的栈帧和局部变量。

理解这些概念对于掌握程序如何运行至关重要。
015:函数、变量与调用栈 🧠


在本节课中,我们将要学习变量的创建时机、它们在计算机内存中的生命周期,以及当两个不同函数中存在同名变量时会发生什么。理解这些概念对于掌握程序执行流程至关重要。


上一节我们介绍了函数的基本概念,本节中我们来看看函数调用时内存的具体变化。

我们首先看到函数 convert_to_minutes,这是在函数可视化课程中探讨过的。我们新增了函数 convert_to_seconds,它用于将小时数转换为等效的秒数。
convert_to_seconds 函数通过调用 convert_to_minutes 来完成其工作,然后将结果乘以 60。其核心逻辑可以用以下公式表示:
seconds = convert_to_minutes(hours) * 60


让我们使用可视化工具来探索这个过程。我们快速跳过函数定义部分。
两个函数对象被创建。convert_to_minutes 指向其中一个函数对象,convert_to_seconds 指向另一个。现在,我们即将执行一个赋值语句。
第一步是计算右侧表达式,这是一个函数调用。Python 首先计算参数并为其值创建一个对象。接着,Python 在调用栈上创建一个栈帧,并将值 2 的内存地址存储在参数 num_hours 中。


作为提醒,这个区域被称为调用栈。它被显示为一个倒置的栈帧堆叠。在执行函数调用期间,会创建一个新的栈帧。每当一个函数退出时,当前的栈帧就会被擦除,并继续执行上一个函数。
convert_to_seconds 函数体中的第一行是另一个赋值语句。右侧是一个函数调用。我们遵循一贯的规则。
以下是执行步骤:
- 计算参数
num_hours,它包含值2的内存地址。 - 为
convert_to_minutes的调用创建一个栈帧,其参数num_hours包含该值的内存地址。

现在有两个名为 num_hours 的变量。一个在 convert_to_minutes 调用的栈帧中,另一个在 convert_to_seconds 调用的栈帧中。Python 将这两个正在运行的函数保存在内存的不同区域,因此不会混淆该使用哪个变量。

继续跟踪执行过程。convert_to_minutes 函数体中的第一行是一个赋值语句。右侧是一个表达式 num_hours * 60。但我们应该使用哪个 num_hours 呢?
答案是 Python 会查看当前的栈帧。num_hours 指向 2,因此 num_hours * 60 的计算结果是 120。赋值语句的第二步是将 120 赋值给变量 minutes。

如果变量 minutes 在当前栈帧中不存在,那么它将被创建。每个栈帧都包含自己的一组变量。在 convert_to_minutes 的栈帧中,变量是 num_hours 和 minutes。convert_to_seconds 的栈帧目前只有变量 num_hours,尽管在其执行结束时,它也将拥有 minutes 和 seconds。

主程序的栈帧目前包含 convert_to_minutes 和 convert_to_seconds,尽管当第 20 行的赋值语句执行完毕时,变量 seconds 也会出现在那里。


我们现在即将返回变量 minutes 的值并退出当前函数。但是我们要返回到哪里呢?答案总是返回到调用栈上的下一个栈帧。当前的栈帧是在这次 convert_to_minutes 调用期间创建的。当这次调用完成时,当前的栈帧被擦除,Python 将返回值作为这个函数调用表达式的值。
这次调用位于一个赋值语句的右侧。为了完成赋值,变量 minutes 将被创建,Python 将存储返回值的内存地址。

让我们观察这个过程。注意,convert_to_minutes 的栈帧已被擦除,变量 minutes 已在 convert_to_seconds 的栈帧中被创建。

这里我们有另一个赋值语句。右侧 minutes * 60 的计算结果是 120 * 60 的值。一个新的变量 seconds 将被创建,并包含这个新值的内存地址。

接下来,我们返回 seconds。seconds 指向 7200。因此,这就是第 20 行对 convert_to_seconds 的调用所产生的值。


本节课中我们一起学习了变量的作用域和生命周期,以及调用栈如何管理函数调用和变量存储。我们看到了同名变量如何在不同函数的独立栈帧中共存而互不干扰,也观察了函数调用、返回和栈帧创建与销毁的完整流程。理解调用栈是理解程序执行顺序和内存管理的基础。
016:布尔类型 🔢


在本节课中,我们将学习真与假的值,也就是布尔值。我们将探索Python中的布尔类型以及可以应用于布尔值的运算符。
比较运算符
上一节我们介绍了算术运算符,如乘法和减法。本节中,我们来看看Python中的一些比较运算符。
以下是Python中常用的比较运算符:

3 < 4:小于运算符。表达式求值后返回一个布尔值。3 > 8:大于运算符。返回False。8 > 3:返回True。3.5 >= 3.4:大于或等于运算符。返回True。

相等与不等运算符
接下来,我们学习相等与不等运算符。
以下是相等与不等运算符的示例:
7 == 7:相等运算符。注意,需要使用两个等号==,因为单个等号=用于赋值操作。7 == 7.0:整数与浮点数比较,结果也为True。x = 7; y = 8; x == y:将相等运算符应用于变量,结果为False。3 != 4:不等运算符。检查两个值是否不相等,结果为True。
逻辑运算符
现在,我们进入逻辑运算符的学习。逻辑运算符应用于布尔值,并产生布尔结果。
Python有三个主要的逻辑运算符:not、and 和 or。
not 运算符
not 运算符用于对布尔值取反。
以下是 not 运算符的用法:
grade = 80grade >= 50:结果为True。not (grade >= 50):对True取反,结果为False。not not (grade >= 50):两次取反,结果等同于grade >= 50,为True。
and 运算符
and 运算符用于检查两个条件是否同时为真。
以下是 and 运算符的用法:


grade = 80; grade2 = 70(grade >= 50) and (grade2 >= 50):两个条件都为真,结果为True。grade = 40; (grade >= 50) and (grade2 >= 50):第一个条件为假,整个表达式立即求值为False,不会检查第二个条件。grade = 80; grade2 = 40; (grade >= 50) and (grade2 >= 50):第一个为真,检查第二个为假,结果为False。
总结:and 运算符仅在两个操作数都为 True 时才返回 True,否则返回 False。

or 运算符

or 运算符用于检查两个条件中是否至少有一个为真。
以下是 or 运算符的用法:
grade = 80; grade2 = 70(grade >= 50) or (grade2 >= 50):至少一个为真,结果为True。grade = 40; (grade >= 50) or (grade2 >= 50):第一个为假,检查第二个为真,结果为True。grade = 80; grade2 = 40; (grade >= 50) or (grade2 >= 50):第一个为真,整个表达式立即求值为True,不会检查第二个条件。
总结:or 运算符在至少一个操作数为 True 时返回 True,否则返回 False。


运算符优先级与括号
最后,我们学习如何将多个运算符组合在单个表达式中,并理解运算符的优先级。
让我们分析一个复合表达式:not grade >= 50 or grade2 >= 50。
这个表达式有两种可能的解释,取决于运算符的优先级:
or先运算,然后not:not (grade >= 50 or grade2 >= 50)not先运算,然后or:(not grade >= 50) or grade2 >= 50
在Python中,逻辑运算符的优先级顺序是:not > and > or。因此,上述表达式实际按第二种方式求值。
为了确保表达式按你期望的顺序求值,并提高代码的可读性,建议使用括号。
例如:
(grade >= 50) and (grade2 >= 50):括号使逻辑更清晰。(grade + grade2) / 2 >= 50:算术运算符优先级高于比较运算符,但括号使意图更明确。
总结:使用括号可以明确指定运算顺序,避免混淆,并让代码更容易理解。

本节课中我们一起学习了布尔类型、比较运算符(<, >, ==, !=)以及逻辑运算符(not, and, or)。我们了解了每个运算符的行为,并通过示例掌握了如何组合它们来构建更复杂的条件判断。最后,我们强调了使用括号来控制运算顺序和提升代码可读性的重要性。
017:整数、字符串与浮点数之间的转换 🔄
在本节课中,我们将学习如何在Python的三种基本数据类型——整数(int)、字符串(str)和浮点数(float)——之间进行转换。掌握类型转换是处理用户输入和进行数据计算的关键步骤。

类型转换的基本方法
到目前为止,我们已经学习了三种数据类型:int、str和float。它们之间可以相互转换。
以下是转换的基本方法:
int():将其他类型转换为整数。str():将其他类型转换为字符串。float():将其他类型转换为浮点数。
从整数到字符串
我们可以将整数转换为字符串。例如,取数字3,然后调用str()函数。
str(3)
这将返回一个长度为1的字符串,内容是字符‘3’。我们可以将这个结果赋值给一个变量。
my_string = str(3)
既然my_string是一个字符串,我们就可以对它进行字符串操作,比如乘以一个数字。
my_string * 5
这会产生字符串‘33333’。
从字符串到整数
上一节我们介绍了如何将整数转为字符串,本节中我们来看看反向操作。如果我们有一个由数字字符组成的字符串,可以将其转换为整数。
例如,有一个包含5个字符‘3’的字符串。
int(‘33333’)
这将得到整数33333。我们甚至可以将转换后的结果再次转换回字符串。
str(int(‘33333’))
这得到的结果,与直接将整数33333转换为字符串是一样的。
涉及浮点数的转换
浮点数也可以转换为字符串。
str(3.14159)
如果对一个由数字组成的字符串使用int()函数,Python会从字符串中提取出数值并返回对应的整数。
int(‘42’)
同样,我们可以对字符串使用float()函数。
float(‘3.14’)
需要注意的是,即使给float()函数一个表示整数的字符串,它也会将其转换为浮点数。
float(‘7’)
转换时的注意事项
进行类型转换时必须小心。如果对一个包含非数字字符的字符串调用int()函数,程序将会报错。
例如:
int(‘7 apples’)
这会引发一个ValueError错误,提示信息类似于“以10为基数的int()的无效文字:‘7 apples’”。这表明Python无法完成这次转换。
类型转换的实际应用
一个常见的应用场景是处理用户输入。当我们使用input()函数获取用户输入时,无论用户键入什么,返回的都是字符串类型。
假设我们编写一个程序询问用户需要多少双鞋:
user_input = input(‘请告诉我鞋子的数量:’)
如果用户输入‘863’,我们得到的是一个字符串‘863’。假设我们的库存只有627双鞋,目前我们无法直接比较字符串‘863’和整数627。
解决方案就是将用户输入的结果转换为整数。
shoes_requested = int(user_input)
shoes_in_stock = 627
现在,我们就可以进行比较了:
if shoes_requested <= shoes_in_stock:
print(‘库存充足。’)
else:
print(‘库存不足。’)
在这个例子中,答案是“库存不足”。

本节课中我们一起学习了Python中int、str和float三种基本数据类型之间的转换。我们了解了如何使用int()、str()和float()函数进行转换,并特别注意了转换可能引发的错误。最后,我们通过一个处理用户输入的实际例子,看到了类型转换在编程中的重要作用。掌握这些知识,能让你更灵活地处理和操作不同类型的数据。
018:使用非内置函数导入

在本节课中,我们将学习如何导入和使用Python中非内置的函数。Python拥有大量函数,但大多数并不直接作为内置函数提供,而是被组织在不同的模块中。我们将通过一个计算三角形面积的例子,演示如何导入和使用这些模块中的函数,以及如何导入我们自己编写的模块。

导入模块的概念
上一节我们介绍了内置函数。本节中我们来看看如何访问那些非内置的函数。
Python拥有成百上千的函数,其中大多数并非作为内置函数立即可用。这些函数被保存在不同的模块中。因此,你需要告诉Python你希望使用它们。同样地,你自己编写的函数也可以保存在不同的模块中。
示例:使用海伦公式计算三角形面积
我们将通过一个三角形面积计算的例子来演示这一点。我们将定义第二个函数,用于在已知三角形三边长(但不知道底和高)的情况下计算面积。这次,我们将使用海伦公式(也称为希罗公式)来计算面积。
该公式涉及半周长和三边长度。其中,s 是半周长,s1、s2 和 s3 是三角形三边的长度。
为了编写这段代码,我们需要计算半周长。我们已经在 triangle.py 文件中定义了一个计算半周长的函数。此外,我们还需要一个计算平方根的函数,这样的函数是存在的,但它不是内置函数,而是定义在另一个名为 math.py 的文件中。
math 是一个模块,triangle 也是一个模块。顾名思义,math 模块包含多个与数学相关的函数。
以下是查看模块中函数列表的步骤:
- 导入模块以获取访问权限。
- 调用内置函数
dir()查看模块内容。
我们可以看到名为 sqrt 的函数在可用函数列表中。现在我们可以运行 help 来获取关于 sqrt 的更多信息。
当我们询问关于 sqrt 的信息时,需要指定它在 math 模块中。我们使用模块名后跟一个点,然后是函数名来实现这一点。这会告诉我们关于 sqrt 函数的信息。
编写函数
现在我们已经准备好编写函数了。
第一步是编写几个函数调用示例。为此,我们需要决定一个有意义的函数名。我将使用 area_hero 来表示使用海伦公式计算面积。
这个函数接收三个边长作为参数。例如,对于边长为3、4、5的三角形,我期望它返回 6.0。
我们将添加第二个函数调用示例。通过计算器,我算出对于边长为5、12、13的三角形,结果应该大约是 27.731(由于Python使用浮点运算,可能会有更多小数位)。这能让我们判断编写的函数是否正确。
接下来,我们提供类型契约。对于这个函数,我们将传入三个数字(整数或浮点数),并返回一个浮点数。

现在我们可以添加函数头,以 def 开头,然后是函数名和三个参数,我将其命名为 side_one、side_two 和 side_three。在此阶段,我们可以添加描述。
这个函数将返回一个边长为 side_one、side_two 和 side_three 的三角形的面积。

完成文档字符串后,我们现在可以编写函数体。
实现函数体
公式中四次使用了半周长。与其用相同的参数调用四次 semi_perimeter 函数,不如调用一次并将结果存储在一个变量中。
我将这个变量命名为 semi,然后我们调用 semi_perimeter 函数一次来计算它,传入 side_one、side_two 和 side_three 作为参数。
接着,我们可以继续计算面积。计算面积时,我们将使用 sqrt 函数,而 sqrt 在 math 模块中。因此,要访问 sqrt,我们需要导入 math 模块。我将在文件顶部进行导入,然后就能访问 sqrt 函数。
让我们来计算面积。我们将使用 math.sqrt(...) 的格式调用 sqrt 函数,并传入以下表达式:
semi * (semi - side_one) * (semi - side_two) * (semi - side_three)
这个计算出的面积就是函数将要返回的值。
最后,我们需要通过调用函数几次来测试它。运行模块后,复制并粘贴这些函数调用。第一个函数调用应该得到 6.0,确实如此。第二个示例应该得到类似我们之前计算的结果,实际上它给出了更多小数位,我将用这个结果来验证。
导入自定义模块
我们不仅可以导入Python的模块,还可以导入我们自己编写的模块。例如,如果我们想在另一个模块中使用我们的三角形函数,我们可以导入 triangle 模块。被导入的模块应该与导入它的模块位于同一目录中。

本节课中我们一起学习了如何导入和使用Python模块中的非内置函数。我们通过一个具体例子,演示了从 math 模块导入 sqrt 函数,并利用它和自定义函数来实现海伦公式计算三角形面积。我们还了解了如何导入自己编写的模块。掌握模块导入是组织和管理更复杂代码的关键一步。
019:条件语句



在本节课中,我们将学习如何使用布尔表达式来控制程序执行哪些指令。到目前为止,我们编写的程序每次运行时都执行相同的指令序列。通过引入条件语句,我们可以让程序根据不同的情况做出不同的反应。
问题引入

让我们考虑一个实际问题。一个航班原计划在特定时间到达,但现在预计在另一个时间到达。我们需要编写一个函数,根据这两个时间返回航班状态:准时、提前或延误。
对于这个函数,时间将用一个浮点数表示。例如,凌晨3点表示为 3.0,下午2点30分(即14:30)表示为 14.5。因此,我们处理的时间将在 0.0(包含)到 24.0(不包含)之间。这被称为前置条件。在编写函数时,我们期望提供的时间会在这个范围内。
定义函数
让我们开始定义这个函数。首先,我们写一些函数调用的例子。我将这个函数命名为 report_status,它有两个参数:第一个是计划到达时间,第二个是新的预计到达时间。
以下是函数在不同情况下的预期行为:
- 当两个时间相同时,返回消息
"on time"。 - 当计划时间是
12.5,而新的预计到达时间是11.5或更早时,函数返回"early"。 - 当计划时间是
9.0,而新的预计到达时间更晚时,函数应返回消息"delayed"。
现在,我们来编写函数头。函数有两个参数,都是数字。在这些例子中,我传入的是浮点数,但我们也可以传入整数值,所以参数类型可以使用 number。函数将返回一个字符串。
函数头如下:
def report_status(scheduled_time, estimated_time):
接下来是文档字符串。这个函数将返回航班状态,是三个字符串之一:"on time"、"early" 或 "delayed"。它返回的是针对计划在 scheduled_time 到达,但现在预计在 estimated_time 到达的航班的狀態。文档字符串中还需要添加一个信息:前置条件是 scheduled_time 和 estimated_time 都必须在 0.0 到 24.0 之间。
编写函数体
现在,是时候编写函数体了。我们将分几个步骤来完成,最终让整个函数正常工作。
首先,我们需要弄清楚如何有时执行某些语句,有时不执行。例如,我们想返回字符串 "on time",但我们不希望每次函数执行时都返回这个字符串,而只在 scheduled_time 等于 estimated_time 时才返回。




我们将使用 if 语句 来表达这一点。这个 if 语句将有一个布尔条件:scheduled_time 等于 estimated_time。我们将把这个 return 语句放在 if 的代码块内。注意,它是缩进的,这表明它属于 if 语句。
我们这样理解这段代码:如果 scheduled_time 等于 estimated_time(即该布尔表达式为真),那么就执行 return "on time" 语句。否则,就不执行,程序将继续执行 if 语句之后的任何代码。
让我们保存并运行它。当我在交互式环境中用相等的时间调用这个函数时,它报告状态为 "on time"。如果此时我用一对不相等的时间调用这个函数,那么什么也不会返回。更准确地说,看起来什么也没返回,因为即使没有执行 return 语句,函数仍然会返回一个值。
让我们打印这个函数调用,这样函数返回的任何值都会被打印出来。我们可以看到打印出了 None。当一个函数调用执行时,如果没有执行任何 return 语句,函数就会返回值 None。None 的类型是 NoneType,而不是 string,这违反了我们在 report_status 函数中指定的类型约定。我们需要完成函数的其余部分,确保遵守该类型约定。
扩展条件判断


让我们回到函数定义。现在我们只有一个 if 语句,当这个布尔条件为真时,返回 "on time"。当它为假时,我们想做点别的事情。所以,我们先检查这个条件是否为真,然后我们将使用 elif(else if 的缩写)来检查第二个条件。
这次,我们将检查 scheduled_time 是否大于 estimated_time。如果是,我们将返回 "early"。


在这个阶段,让我们运行它。当我们用相等的时间调用这个函数时,它说 "on time"。当我们用更早的预计时间调用它时,它报告 "early"。但是此时,当我们用更晚的预计时间调用它时,返回的是 None,所以我们还没有完全完成。
我们将为 if 语句添加第三部分,即一个 else 子句。else 表示:如果前面的所有条件或表达式都不为真,那么就执行这个。我们已经检查了它们是否相等、是否大于,剩下的唯一情况就是 scheduled_time 小于 estimated_time。在这种情况下,函数将返回 "delayed"。
让我们再次运行它,检查所有情况:当时间相等时、当预计时间更早时、当预计时间更晚时。现在我们得到了正确的报告。
if 语句的一般特性

以下是关于 if 语句结构的一些要点:
- 一个
if语句可以关联零个或多个elif子句。 - 一个
if语句可以关联零个或一个else子句。 else子句必须是if语句的最后一个子句。


当我们有一个包含多个表达式的 if 语句时,我们按顺序计算每个表达式。对于第一个计算结果为 True 的表达式,就执行该部分 if 语句的代码块。在该代码块执行完毕后,if 语句就终止了,程序会退出并继续执行它后面的代码,而不再检查任何后续条件。

总结


本节课中,我们一起学习了如何使用 if、elif 和 else 语句来编写条件判断逻辑。我们通过一个航班状态报告器的例子,实践了如何根据不同的输入(计划时间和预计时间)执行不同的代码路径。关键点在于理解布尔表达式如何控制程序流程,以及 if 语句各部分的执行顺序和规则。这使我们能够编写出可以根据不同情况做出决策的程序。
020:布尔函数与条件语句的简洁写法

在本节课中,我们将探讨一个编程风格问题,它出现在你将布尔函数、布尔表达式和条件语句三者结合使用时。我们将学习如何编写更简洁、更高效的代码。
概述

你已经学习了布尔函数和 if 语句。现在,我们将研究一种编程风格问题,它在你将布尔函数、布尔表达式和 if 语句三者结合使用时可能出现。我们将通过编写一个判断数字奇偶性的函数来演示这个问题,并学习如何简化代码。
编写判断奇偶性的函数
我们将从一个具体例子开始:编写一个函数,如果参数是偶数则返回 True,否则返回 False。
例如,调用 is_even(4) 应返回 True,而调用 is_even(77) 应返回 False。这个函数的输入是一个整数,输出是一个布尔值。
函数头可以这样定义:函数名为 is_even,参数名为 num(代表数字)。函数的目标是判断 num 是否为偶数。
探索判断逻辑
在编写具体代码前,我们需要找到判断一个数字是奇数还是偶数的方法。我们可以在 Python 交互式环境中进行探索。
请注意,如果将 4 除以 2,余数为 0;而将 3、5 或 7 除以 2,则有余数。因此,我们可以使用取模运算符 %。
4 % 2 == 0 # 结果为 True
77 % 2 == 0 # 结果为 False
任何奇数除以 2 的余数都是 1。因此,表达式 num % 2 == 0 就能给出我们想要的结果:如果为 True,则 num 是偶数;如果为 False,则 num 是奇数。
初始实现方式
基于以上逻辑,我们可能会写出如下代码:
def is_even(num):
if num % 2 == 0:
return True
else:
return False
这个函数确实可以工作。我们来测试一下:
is_even(4) # 返回 True
is_even(3) # 返回 False
函数运行正常。然而,这个函数体有三行,太长了。我们实际上并不需要使用 if 语句。
简化代码
问题的关键在于,num % 2 == 0 本身就是一个布尔表达式。正如我们之前看到的,4 % 2 == 0 直接产生了值 True。
我们不需要说“如果某条件为真,则返回真;否则返回假”。我们可以直接返回这个布尔表达式的结果。
因此,我们可以将函数体简化成一行:
def is_even(num):
return num % 2 == 0
让我们在交互式环境中测试这个新版本:
is_even(4) # 仍然返回 True
is_even(3) # 仍然返回 False
新版本和旧版本一样有效,但更简短。经过一段时间,你会发现这种写法比带有 if 和 else 的长版本更可取。
适应简洁写法
对于许多初学者来说,直接使用布尔表达式可能有些陌生,甚至让人感到不自在或困惑。这很正常。
以下是一些建议帮助你适应:
- 多在 Python 交互式环境中练习,尝试各种能产生布尔值的表达式。
- 检查你自己的代码,如果你发现类似“如果某条件成立则返回
True,否则返回False”的模式,看看是否能将其简化为直接返回该条件表达式。
总结

本节课中,我们一起学习了如何简化结合了布尔表达式和条件语句的代码。核心要点是:当函数需要根据一个条件返回 True 或 False 时,通常可以直接返回该条件表达式本身,而无需使用完整的 if-else 结构。这能使代码更简洁、更易读。记住这个模式,它在你未来的编程实践中会非常有用。
021:结构化-if语句 🧠

在本节课中,我们将学习如何结构化 if 语句。我们将通过分析两个具体问题,探讨针对每个问题使用 if 语句的不同解决方案,并理解不同结构之间的区别。

问题一:成绩判断
我们来看第一个例子。这个问题涉及两个成绩:grade1 和 grade2。我们给 grade1 赋值为 70,给 grade2 赋值为 80。
以下是使用 if-elif 结构的代码:


grade1 = 70
grade2 = 80
if grade1 > 50:
print("Grade 1 is greater than 50.")
elif grade2 > 50:
print("Grade 2 is greater than 50.")
这些布尔表达式会从上到下依次求值。当其中一个表达式为真时,其关联的代码块就会被执行,并且 if 语句会在此处退出,不再继续评估后续的表达式。

因此,在这种情况下,最多只会执行两个 print 语句中的一个。

让我们分析这段代码。因为 grade1 大于 50,条件为真,我们进入 if 的代码块。由于与 if 关联的表达式之一为真,在执行完此代码块后,if 语句将退出,程序会跳转到第 9 行。


对比:两个独立的 if 语句

现在,我们来看另一种写法。在第 9 行有另一个 if 语句,在第 11 行有第三个 if 语句。
if grade1 > 50:
print("Grade 1 is greater than 50.")

if grade2 > 50:
print("Grade 2 is greater than 50.")
第 9 行 if 的条件与第 4 行相同,第 11 行 if 的条件与第 6 行 elif 的条件相同。


然而,因为第 9 行到第 12 行的代码是两个独立的 if 语句,而不是第 4 行到第 7 行那样的 if-elif 结构,所以它的行为是不同的。
我们首先评估第 9 行 if 关联的表达式,它为真,因此我们进入其代码块并打印消息。此时,我们会退出那个 if 语句,继续执行后续代码,即第 11 行的另一个独立的 if 语句。
这个布尔表达式也为真。因此,在这种情况下,它的代码块也会被执行。
所以,if-elif 并不等同于两个 if 语句。我们在编写代码时需要考虑到这一点,并在每种情况下注意使用合适的结构。

问题二:是否带伞

让我们考虑另一个问题。这个问题涉及预测我们是否应该带伞。我们将考虑两个因素:是否预计有降水(使用布尔值 True 或 False)以及今天的温度(本例中为 +8 摄氏度)。
以下是使用嵌套 if 语句的初始代码:
precipitation = True
temperature = 8
if precipitation:
if temperature > 0:
print("Bring your umbrella.")
这个 if 语句首先检查是否预计有降水,条件为真,因此我们进入 if 的代码块。在代码块中,实际上有另一个 if 语句,一个 if 语句嵌套在另一个里面。
然后我们检查那个布尔条件,发现它为真,因此我们打印消息“Bring your umbrella”。
简化:使用逻辑运算符
我想修改这段代码,写出一个没有嵌套的第二个版本。注意,当 precipitation 为真,并且与第二个 if 关联的条件也为真时,我们才打印“Bring your umbrella”。所以两个条件都需要为真。
我们可以使用布尔运算符 and 来表达这一点,这样我们就可以将其结构化为一个单一的 if 语句:
if precipitation and temperature > 0:
print("Bring your umbrella.")
让我们可视化执行过程。在第一种情况下,变量的值导致我们打印“Bring your umbrella”。在第二种情况下,我们评估这个布尔条件。precipitation 的值为 True,temperature 的值为 8,所以这个表达式也为真,消息“Bring your umbrella”被显示出来。
扩展:添加 else 分支
现在,让我们扩展这个例子。我将在第 5 行的 if 语句中添加一个 else。这个 else 描述了当温度不大于零时要打印的语句。
if precipitation:
if temperature > 0:
print("Bring your umbrella.")
else:
print("It's freezing, but bring your umbrella anyway.")
现在,我将编辑第 10 行的代码,使那个 if 语句与第 4 行的等价。让我们看看 else 代码执行的条件:precipitation 条件需要为真,temperature > 0 条件必须为假,然后 else 才会被执行。
所以我们可以使用一个 elif 来表达这一点。我们需要确保第一个条件为真,并且我们不需要检查第二个条件,因为只有当它为假时我们才会到达 elif。在这种情况下,我们将打印与上面相同的消息。
让我们再次可视化执行过程。实际上,还有一件事要做。让我们把温度改为 -3,这样就在冰点以下,我们会打印那些消息。
temperature = -3
# 版本 A: 嵌套 if-else
if precipitation:
if temperature > 0:
print("Bring your umbrella.")
else:
print("It's freezing, but bring your umbrella anyway.")
# 版本 B: 使用 if-elif
if precipitation and temperature > 0:
print("Bring your umbrella.")
elif precipitation:
print("It's freezing, but bring your umbrella anyway.")
所以,如果 precipitation 为真,我们进入 if 的代码块。检查这个条件,因为我刚刚改变了 temperature 的值,它为假,所以我们打印 else 中的语句。
在第二个 if 语句中,我们检查第一个条件,因为温度不大于零,所以为假。现在我们必须检查 elif 子句,这个条件为真,因此我们进入那个 elif 的代码块并打印相同的消息。所以这两段代码是等价的。
总结

本节课中,我们一起学习了如何结构化 if 语句。我们通过两个例子探讨了 if-elif 结构与多个独立 if 语句的区别,并学习了如何使用逻辑运算符 and 来简化嵌套的 if 语句,以及如何将嵌套的 if-else 结构转换为等价的 if-elif 结构。理解这些不同结构的执行流程对于编写清晰、正确的条件判断代码至关重要。
022:更多字符串运算符 📚
在本节课中,我们将学习Python中除加法和乘法之外的其他字符串运算符。我们将探索如何比较字符串、检查子字符串以及获取字符串的长度。

你已经了解了如何将两个字符串相加,以及如何通过将字符串与整数相乘来重复该字符串。
在本视频中,我们将探索其他字符串运算符。
相等与不等比较
我们首先来看如何比较两个字符串是否相等。

我们将字符串 "cat" 赋值给变量 solution。现在,我们使用两个等号 == 来比较 solution 是否指向字符串 "cat"。
solution = "cat"
print(solution == "cat") # 输出:True
此外,还有一个不等运算符,它由一个感叹号和一个等号组成 !=。
print(solution != "cat") # 输出:False
接下来,我们让 solution 指向一个新的字符串,然后检查相同的操作。
solution = "dog"
print(solution == "cat") # 输出:False
print(solution != "cat") # 输出:True
字典序比较
我们还可以比较两个字符串的字典顺序(即字母顺序)。
以下是字典序比较的运算符。

print("cat" < "dog") # 输出:True
在这里,Python 会从字符串开头开始,逐个字母进行比较。
例如,比较 "cat" 和 "dog":
- 首先比较第一个字母
'c'和'd'。由于'c'在字母表中位于'd'之前,因此"cat"小于"dog"。

上面我们使用了小于运算符 <。我们同样有大于运算符 >。
print("cat" > "dog") # 输出:False
我们还有小于等于 <= 和大于等于 >= 运算符。
print("cat" <= "cat") # 输出:True
print("cat" >= "cat") # 输出:True
大小写与不同类型比较
我们也可以比较大写字母。实际上,大小写是重要的,并且大写字母被认为小于小写字母。
print("A" < "a") # 输出:True
任何你可以输入的字母都可以进行比较。
我们总是可以比较两个不同类型的值是否相等或不等。
print(42 == "42") # 输出:False
print(42 != "42") # 输出:True
但是,我们通常不能比较两种不同类型的项目的顺序。
# 以下代码会引发 TypeError
# print(42 < "42")
成员检查运算符 in
这是一个新的字符串运算符。我们可以使用 in 来检查一个字符串是否包含在另一个字符串中。
print("a" in "apple") # 输出:True
使用这个运算符,我们可以检查字母 'C' 是否是元音字母。
print("C" in "AEIOUaeiou") # 输出:False
字母的顺序很重要。"zoo" 并不在 "woos" 中。
print("zoo" in "woos") # 输出:False
我们可以询问空字符串是否在另一个字符串中,答案是肯定的。同时,空字符串也包含在它自身中。
print("" in "hello") # 输出:True
print("" in "") # 输出:True
获取字符串长度:len() 函数
本节课我们将讨论的最后一个内容是一个函数,而不是运算符。len() 函数接收一个字符串,并告诉你其中有多少个字符。
print(len("")) # 输出:0
print(len("Abracadabra")) # 输出:11
print(len("a" * 10 + "bc")) # 输出:12 (10个'a' + 'b' + 'c')
空字符串有 0 个字符。"Abracadabra" 有 11 个字符,而表达式 "a" * 10 + "bc" 的结果有 12 个字符。

本节课中,我们一起学习了Python中更多的字符串操作。我们掌握了如何使用 == 和 != 比较字符串是否相等,如何使用 <、>、<=、>= 进行字典序比较,以及大小写在比较中的影响。我们还学习了使用 in 运算符检查子字符串,并使用 len() 函数获取字符串的长度。这些工具将帮助你更有效地处理和检查文本数据。
023:字符串索引与切片 📚

在本节课中,我们将学习如何从字符串中提取部分内容。我们将重点介绍两种核心技术:索引和切片。通过它们,你可以轻松获取字符串中的特定字符或子串。

字符串索引 🔢
在Python中,字符串中的每个字符都有一个索引,代表它在字符串中的位置。我们以一个示例字符串 "Learn to program" 来演示。
字符串索引从 0 开始计数,而不是1。例如:
- 索引
0对应字符'L'。 - 索引
1对应字符'e'。 - 索引
2对应字符'a'。
我们也可以使用负索引从字符串的末尾开始计数:
- 索引
-1对应最后一个字符'm'。 - 索引
-2对应倒数第二个字符'a'。 - 索引
-3对应倒数第三个字符'r'。
使用索引,我们可以提取字符串中的单个字符,语法如下:
s = "Learn to program"
character = s[index]
字符串切片 ✂️
上一节我们介绍了如何使用索引获取单个字符。本节中我们来看看如何一次性获取多个字符,这需要使用切片操作。
切片允许我们提取字符串的一部分(子串)。其基本语法是:
substring = s[start_index:end_index]
这里,start_index 是起始索引(包含),end_index 是结束索引(不包含)。切片将返回从 start_index 到 end_index-1 的所有字符。
以下是使用示例字符串 s = "Learn to program" 的一些切片示例:
s[0:5]返回"Learn"(索引0到4)。s[6:8]返回"to"(索引6到7)。s[9:16]返回"program"(索引9到15)。
切片操作非常灵活,可以省略起始或结束索引:
- 省略起始索引(
[:end_index])表示从字符串开头开始切片。 - 省略结束索引(
[start_index:])表示切片到字符串末尾。 - 两者都省略(
[:])会返回整个字符串的副本。
例如:
s[9:]和s[9:len(s)]都返回"program"。s[:8]返回"Learn to "。s[:]返回"Learn to program"。
负索引同样可以用于切片:
s[1:8]、s[1:-8]和s[-15:-8]这三个表达式是等价的,都返回"earn to"。
字符串的不可变性 🔒
需要牢记的一个重要概念是:字符串在Python中是不可变的。这意味着索引和切片操作不会修改原始字符串,它们只是基于原字符串创建新的字符串。
以下尝试修改字符串的操作都会导致错误:
s[6] = 'D' # 错误!不能通过索引修改字符
s[9:16] = 'code' # 错误!不能通过切片修改子串
那么,如果我们想将字符串 s 从 "Learn to program" 改为 "Learned to program" 该怎么办呢?方法不是修改原字符串,而是创建一个新的字符串并让变量指向它。
以下是实现方法:
s = "Learn to program"
# 组合新字符串:取"Learn" + "ed" + " to program"
new_s = s[:5] + "ed" + s[5:]
print(new_s) # 输出:Learned to program
# 现在可以让变量s指向这个新字符串
s = new_s
这个过程是:我们利用原字符串的切片(s[:5] 和 s[5:])与字符串 "ed" 进行拼接(使用 + 运算符),从而生成了一个全新的字符串 "Learned to program",最后让变量 s 引用这个新字符串。
总结 📝
本节课中我们一起学习了处理字符串的两个核心技巧:
- 索引:使用
s[index]获取字符串中特定位置的单个字符。索引可以从头(从0开始)或从尾(使用负索引)计数。 - 切片:使用
s[start:end]获取字符串的一部分(子串)。切片范围包含起始索引,但不包含结束索引。可以灵活地省略起始或结束索引。 - 字符串的不可变性:所有针对字符串的索引和切片操作都不会改变原字符串,而是返回新的字符串。要“修改”一个字符串,需要创建新的字符串并重新赋值给变量。

掌握索引和切片是进行文本处理和数据操作的基础,希望你通过练习能熟练运用它们。
024:对象内部的函数 🧵

在本节课中,我们将要学习字符串方法。我们将了解什么是方法,它与之前学过的模块函数有何不同,并探索几个常用的字符串方法。
到目前为止,我们定义和使用的函数都来自模块。例如,math模块包含了与数学相关的函数,如平方根和阶乘。

实际上,值(也称为对象)内部也可以包含函数。对象内部的函数被称为方法。在本讲中,我们将探索字符串方法。
回顾模块函数
作为快速回顾,要使用定义在math模块中的函数,我们首先需要导入math。这会创建一个名为math的变量,它引用包含各种数学函数的模块对象。然后,我们使用这个math变量来调用其中的函数。
我们将使用类似的方式来调用字符串方法。
什么是字符串方法?
以下是一个来自路易斯·卡罗尔《爱丽丝梦游仙境》的引文:
white_rabbit = 'I’m late, I’m late, I’m late!'
我们可以通过调用white_rabbit对象内部的lower方法,获得一个新字符串,其中所有大写字母都被替换为对应的小写字母。
lowercase_rabbit = white_rabbit.lower()
这并不会改变white_rabbit所引用的字符串,而是产生一个新的字符串。因此,white_rabbit仍然指向原始字符串。
如何探索可用的方法?
为了找出有哪些方法可用,我们可以使用dir()函数。
dir(white_rabbit)
如果我们使用type(str)而不是变量white_rabbit,也能得到相同的答案。
我们可以使用help()函数来获取任何方法的帮助信息。例如,查看lower方法的帮助:
help(str.lower)
返回信息:“Return a copy of the string S converted to lower case.”
常用字符串方法示例
以下是几个核心字符串方法的介绍和使用示例。
1. count 方法
count方法返回子字符串在指定范围内出现的次数。
help(str.count)
返回信息:“Return the number of non-overlapping occurrences of substring sub in string S, starting at index start and going to index end.”
方括号[]表示可选参数。如果只提供一个参数,它将在整个字符串中搜索。否则,它将从start索引开始,搜索到end索引。
我们可以用它来查找子字符串'late'在white_rabbit中出现了多少次。
count_late = white_rabbit.count('late')
2. capitalize 方法
capitalize方法返回字符串的副本,并将首字母大写。
capitalized = white_rabbit.capitalize()
如果字符串已经首字母大写,则直接返回其副本。
3. find 方法
find方法用于查找一个字符串在另一个字符串中第一次出现的索引。
index = white_rabbit.find('late')
我们可以指定起始索引。下面的代码要求查找从索引7开始(或之后)的'late'的第一次出现位置。
index = white_rabbit.find('late', 7)
如果子字符串不存在,则返回-1。find方法是区分大小写的。
4. rfind 方法
rfind方法用于搜索最后一次出现的位置。rfind代表“right find”。
last_index = white_rabbit.rfind('late')
5. 去除空格的方法
通常,我们想要去除字符串开头或结尾的空格。
lstrip(左去除)方法返回一个新字符串,并移除开头的空白字符。left_stripped = ' hello'.lstrip()rstrip(右去除)方法在字符串的右端进行操作。right_stripped = 'hello '.rstrip()- 如果想同时去除两端的空白字符,可以使用
strip方法。stripped = ' hello '.strip()
重要提示:以上所有方法都不会修改原始字符串,它们都会产生新的字符串。
总结

本节课中,我们一起学习了字符串方法。我们了解到方法是对象内部的函数,并探索了lower、count、capitalize、find、rfind以及去除空格系列方法的使用。我们不会向你介绍每一个字符串方法,你可以并且应该使用dir()和help()来探索其他存在的字符串方法。这是我们对字符串和字符串方法的快速导览的结束。
025:遍历字符串的循环 🔄

在本节课中,我们将要学习如何使用 for 循环来遍历字符串中的每一个字符。我们将通过两个具体的编程问题——统计字符串中的元音字母数量以及收集字符串中的所有元音字母——来深入理解循环和累加器的概念。

字符串索引回顾 📍
之前我们了解到,字符串中的每个字符都有一个与之关联的索引,并且我们学习了如何使用索引来访问每个字符。
在本节中,我们将使用一种叫做 for 循环的结构,来依次访问字符串中的每一个字符。
for 循环基础
首先,我们创建一个示例字符串 hi there。我们将使用这个字符串来编写第一个 for 循环,该循环将逐个打印字符串中的每个字符。
s = "hi there"
for char in s:
print(char)
变量 char 是 character 的缩写,它只是一个变量名。随着循环的执行,char 的值会改变。
在循环的第一次迭代中,char 获得 s 在位置 0 的值,即 H,然后打印 H。
在循环的下一次迭代中,char 的值变为 s 在位置 1 的字符,即 I,然后打印 I。
在第三次迭代中,char 获得 s 在位置 2 的值,即一个空格,然后打印该空格。
这个模式会持续进行,直到到达字符串的末尾。因此,s 中的每个字符都会被逐个打印出来。
char 是一个变量名,我们可以轻松地选择其他名称。例如,下面的代码是等效的:
for banana in s:
print(banana)
当然,最好选择一个有意义的变量名,以表明该变量所代表的内容。
循环内的多个操作
下面是另一个 for 循环的例子。在这个循环中,有两个 print 语句。
for char in s:
print(char)
print(char)
在循环的第一次迭代中,char 获得值 H,然后 H 被打印两次。接着 char 获得 I,I 被打印两次,依此类推,直到到达字符串 s 的末尾。
应用:统计元音字母数量 🧮
现在,我们将使用 for 循环来解决一个问题。我们要解决的第一个问题是统计字符串中元音字母的数量。
首先,我们编写几个示例函数调用。我们将函数命名为 count_vowels,它接受一个字符串参数。
# 示例调用
count_vowels("Happy Anniversary!") # 应返回 5
count_vowels("xyz") # 应返回 0
接下来,我们添加类型约定:函数接受一个字符串参数,并返回一个整数。
def count_vowels(s: str) -> int:
"""返回字符串 s 中的元音字母数量。不将字母 Y 视为元音。"""
在编写函数体之前,我们先思考一下如何确定示例调用的结果。我们逐个检查字符串中的每个字符,从 H 开始,判断它是否是元音,并记录结果。
我们将按照这个思路来编写程序。我们需要一个变量来跟踪到目前为止看到的元音数量。初始时,我们看到了 0 个元音。
def count_vowels(s: str) -> int:
"""返回字符串 s 中的元音字母数量。不将字母 Y 视为元音。"""
num_vowels = 0 # 初始化累加器
for char in s:
if char in "aeiouAEIOU":
num_vowels = num_vowels + 1 # 发现元音,累加器加一
return num_vowels
让我们暂停一下,思考这个赋值语句。注意,赋值语句右侧的表达式涉及变量 num_vowels。如果循环外没有初始化 num_vowels = 0 的语句,当执行到这个赋值语句并查找 num_vowels 的值时,就会发生名称错误。因此,这个初始化语句至关重要。
现在,让我们运行这段代码来检查我们的示例调用。
# 测试
print(count_vowels("Happy Anniversary!")) # 输出: 5
print(count_vowels("xyz")) # 输出: 0
变量 num_vowels 是一个累加器,因为它累积信息。它的初始值为 0,每当发现一个元音时,它的值就增加 1。循环在检查完 s 中的每个字母并统计完所有元音后结束。
应用:收集元音字母 📝
我们的下一个任务是返回一个包含给定字符串中所有元音字母的字符串。
我们再次从示例调用开始。可以使用与上一个函数相同的例子。
# 示例调用
collect_vowels("Happy Anniversary!") # 应返回 "aAiea"
collect_vowels("xyz") # 应返回 ""
类型约定:该函数接受一个字符串参数,并返回一个字符串。
def collect_vowels(s: str) -> str:
"""返回字符串 s 中的所有元音字母。不将字母 Y 视为元音。"""
与 count_vowels 类似,我们通过逐个检查字符串中的字符来解决这个问题。每当我们遇到一个元音,就将其记录下来。
这次,我们需要在一个字符串中累积信息。因此,我将使用变量 vowels 来引用一个字符串,该字符串初始为空,因为我们还没有看到任何元音。
def collect_vowels(s: str) -> str:
"""返回字符串 s 中的所有元音字母。不将字母 Y 视为元音。"""
vowels = "" # 初始化字符串累加器
for char in s:
if char in "aeiouAEIOU":
vowels = vowels + char # 发现元音,将其连接到累加字符串
return vowels
在函数结束时,一旦 for 循环执行完毕,并且检查了字符串中的每个字符,vowels 将引用一个包含 s 中所有元音字母的字符串,这就是要返回的值。
现在,我们来测试这个函数。
# 测试
print(collect_vowels("Happy Anniversary!")) # 输出: "aAiea"
print(collect_vowels("xyz")) # 输出: ""
与变量 num_vowels 一样,变量 vowels 也是一个累加器,它在函数执行过程中累积一个字符串。就像 num_vowels 从 0 开始增长一样,vowels 从空字符串开始,也在不断增长。

总结 📚
本节课中,我们一起学习了如何使用 for 循环来遍历字符串中的每个字符。我们通过两个实例深入理解了循环的工作原理:
- 统计元音数量:我们使用一个整数累加器
num_vowels,在循环中遇到元音时将其值加一。 - 收集元音字母:我们使用一个字符串累加器
vowels,在循环中将遇到的每个元音字符连接到字符串末尾。


关键概念在于累加器模式:初始化一个变量(如 0 或 ""),在循环中根据条件更新它,最后返回其值。这是编程中处理序列数据(如字符串)的常用且强大的技术。
026:IDLE调试器使用指南 🔧

在本节课中,我们将学习如何使用Python IDLE自带的调试器。调试器是一个类似于Python可视化工具的程序执行跟踪工具,但它不依赖图形界面,功能更强大,尤其适合处理更复杂的代码或当可视化工具无法使用时。
上一节我们介绍了Python可视化工具的局限性。本节中我们来看看IDLE内置的调试器如何帮助我们理解和跟踪代码的执行过程。
Python可视化工具的局限性

Python可视化工具存在一些限制。
它不允许使用import语句。
并且它在执行500步后会停止跟踪。
启动IDLE调试器
IDLE自带一个调试器。
这是一个非常类似可视化工具的工具,但没有漂亮的图形界面。

这里我们看到两个函数:convert_to_minutes和convert_to_seconds,我们在上周的课程中探讨过它们。
要在IDLE的调试器中运行此代码,我们需要确保Python Shell窗口在最前面,然后选择 Debug -> Debugger。
这会打开一个名为“Debug Control”的窗口。
我现在将调整这个窗口的大小,以便在执行时能看到所有内容。
我们将勾选 Source 复选框。
然后我们将回到要调试的模块,并选择 Run -> Run Module。



理解调试器界面
调试器启动后,程序会暂停在第一条可执行语句前。
以下是调试器“Debug Control”窗口的主要区域介绍:
中间区域显示调用堆栈,或者至少是已被调用的函数。


这里是当前调用内部的变量。
注意,__builtins__变量在那里,还有三个我们在本课程中尚未讨论的其他变量。

Step按钮类似于可视化工具中的“Forward”(前进)按钮。

逐步执行代码
我们定义了两个函数,注意下方有对应的变量指向这些函数对象。
这个值(例如0x000001F1D6C12F70)是一个内存地址。

你会注意到地址中包含字母,这是因为它是十六进制(base 16)。
在编程中广泛使用十六进制,它使用数字0到9,加上字母A、B、C、D、E和F,总共16个字符。
在代码编辑器中,当前要执行的行会有一个灰色高亮显示。
第20行恰好就是那一行。因此,调试器在这里告诉我们哪一行即将被执行。
当我们点击 Step 时,我们将执行对convert_to_seconds的函数调用。
跟踪函数调用与变量
目前,在convert_to_seconds函数内部,我们只有参数num_hours可用。
如果我们想查看模块级别的变量,只需在“Debug Control”窗口的堆栈区域点击“main”,这将切换“Locals”窗格(局部变量)的视图。
我们回到convert_to_seconds的帧,然后Step Into进入对convert_to_minutes的调用。
现在,调用堆栈上有三个与我们的程序相关的项目。
让我们再执行一行来定义变量mins。现在minutes的值是120。
为了验证,我们回到convert_to_seconds的帧,注意它只有一个变量numb_hours。
原因是,我们还没有完成这次对convert_to_minutes的调用,因此变量minutes尚未被创建。
我将视图切换回最顶层的帧(convert_to_minutes)。当我点击 Step 时,它将一次性执行return语句,包括从堆栈中弹出该函数的帧。在返回发生之前,我们看不到返回值。
完成计算并返回
所以我们回到了convert_to_seconds中,我们即将计算120 * 60并将结果赋值给seconds。
执行完毕,seconds的值是7200,然后我们将返回7200。
回到主模块中对convert_to_seconds的调用,它将在主模块的堆栈帧中创建一个变量seconds。
将视图切换回主模块帧,点击 Step 会导致我们的程序结束。

因为这是程序中的最后一条语句。
让我们切换回那里,注意我们确实有一个变量seconds被创建了。
关于程序结束时的注意事项
在程序结束时可能会有点令人困惑。
因为如果我位于一个return语句上,并且该return语句位于一个函数内部,而这个函数执行的是最后一条指令,那么程序将会结束。
并且这个堆栈帧不会被移除,因为一旦程序执行完毕,调试器在程序结束时不会更新显示。


核心操作与概念
以下是调试过程中的核心操作:
- Step:执行当前行,并前进到下一行。如果当前行包含函数调用,Step会进入该函数。
- Step Over:执行当前行。如果当前行是函数调用,则一次性执行完整个函数,不会进入函数内部。
- Go:继续执行程序,直到遇到断点或程序结束。
- Quit:停止调试并终止程序。
以下是调试器界面中显示的关键信息:
- 调用堆栈 (Call Stack):显示当前正在执行的函数及其调用链。
- 局部变量 (Locals):显示当前函数帧中的所有局部变量及其值。
- 全局变量 (Globals):显示模块级别的全局变量。
- 源代码高亮:在编辑器中高亮显示即将执行的代码行。
总结

本节课中我们一起学习了如何使用IDLE内置的调试器。我们了解了如何启动调试器,逐步执行代码,观察调用堆栈的变化,以及查看不同作用域下的变量值。调试器是一个强大的工具,可以帮助你深入理解程序执行流程,定位逻辑错误。记住,调试的核心思想是让程序“慢下来”,让你能够仔细观察每一步的状态变化。多加练习,你就能熟练运用它来分析和修复代码。
027:While循环 🌀

在本节课中,我们将要学习一种新的循环结构——while循环。与之前学过的for循环不同,while循环允许我们根据一个条件来重复执行代码块,而不是遍历一个固定的序列。这使得它在处理不确定次数的循环时非常有用。

从if语句到while循环
上一节我们介绍了for循环,本节中我们来看看while循环。首先,让我们回顾一下if语句的一般形式。
一个if语句依赖于一个布尔表达式。当该表达式为真时,if语句的主体被执行。如果布尔表达式为假,则程序跳过if语句的主体,继续执行后面的语句。
while循环的形式与if语句相似。它同样依赖于一个布尔表达式。如果布尔表达式为真,while循环的主体就会被执行。然而,与if不同的是,while循环在执行完主体后,会再次检查布尔表达式。如果循环条件仍然为真,那么while循环的主体将再次执行。这个过程会一直重复,直到循环条件变为假。
while循环的基本示例
让我们通过一个简单的例子来理解while循环的工作原理。
num = 2
while num < 100:
num = num * 2
print(num)
在这个例子中,变量num的初始值是2。只要num小于100,while循环就会继续执行。在循环体内,num的值会翻倍并被打印出来。因此,这个循环会执行六次,num的值依次变为4、8、16、32、64,最终变为128。此时,num < 100的条件变为假,循环停止。
如果num的初始值是10,那么循环体只会执行四次(num变为20、40、80、160),循环条件就会变为假。
如果num的初始值大于或等于100,那么循环条件从一开始就是假的,循环体一次也不会执行。

使用while循环处理字符串

现在,让我们来解决课程开始时提到的问题:遍历一个字符串,直到遇到一个元音字母为止。


首先,回忆一下我们如何使用for循环来遍历字符串:


s = "Hello"
for character in s:
print(character)
这个for循环会依次打印出字符串s中的每一个字符。
当我们想用while循环实现类似功能,但只遍历到第一个元音字母时,我们需要手动管理索引。以下是实现步骤:
- 创建一个变量
i,初始值为0,代表我们要检查的字符串的第一个索引。 - 循环条件是:当前索引
i处的字符不是元音字母。 - 在循环体内,我们打印当前字符,然后将索引
i加1,以移动到下一个字符。


以下是代码实现:
s = "Hello"
i = 0
vowels = "aeiouAEIOU"
while i < len(s) and s[i] not in vowels:
print(s[i])
i = i + 1


请注意,我们在循环条件中增加了i < len(s)。这是为了防止当字符串中没有元音字母时,索引i会超出字符串的范围,导致“索引错误”。Python对and操作进行“惰性求值”,如果第一个操作数(i < len(s))为假,就不会计算第二个操作数,从而避免了访问无效索引。
对于字符串"Hello",循环会打印出'H',然后在遇到'e'时停止。对于字符串"There",循环会打印出'T'和'h',然后在遇到'e'时停止。对于字符串"xyz"(全是辅音),循环会打印出所有三个字符,然后因为i不再小于字符串长度而正常退出。
函数应用:up_to_vowel
基于上面的思路,我们可以编写一个函数up_to_vowel(s),它接收一个字符串参数s,并返回从开头到(但不包括)第一个元音字母的子字符串。
我们使用“累加器模式”来解决这个问题:
def up_to_vowel(s):
"""返回字符串s中从开头到第一个元音字母(不含)的子字符串。
"""
before_vowel = ''
i = 0
vowels = "aeiouAEIOU"
while i < len(s) and s[i] not in vowels:
before_vowel = before_vowel + s[i]
i = i + 1
return before_vowel
让我们测试这个函数:
up_to_vowel("Hello")返回"H"up_to_vowel("There")返回"Th"up_to_vowel("xyz")返回"xyz"
函数应用:get_answer
while循环的另一个常见用途是反复提示用户输入,直到获得有效响应。我们将实现一个函数get_answer(prompt),它向用户显示提示信息prompt,要求输入“是”或“否”,并持续提示直到用户输入这两个有效答案之一。
以下是实现思路:
- 首先,使用
input(prompt)获取用户的初始答案。 - 只要用户的答案不是“yes”也不是“no”,就继续用相同的提示信息要求用户输入。
- 当用户最终输入“yes”或“no”时,退出循环并返回该答案。
def get_answer(prompt):
"""提示用户输入,直到用户输入‘yes’或‘no’为止,然后返回该答案。
"""
answer = input(prompt)
while answer != "yes" and answer != "no":
answer = input(prompt)
return answer
这个函数的执行依赖于用户输入。如果用户第一次就输入“yes”或“no”,循环体不会执行,函数直接返回答案。如果用户输入了其他内容(如“maybe”),循环条件为真,程序会再次提示用户,直到获得有效输入。
总结

本节课中我们一起学习了while循环。我们了解到:
while循环基于一个布尔条件重复执行代码块。- 它与
for循环的关键区别在于,while循环的次数在编写代码时可能是不确定的。 - 我们学习了如何用
while循环遍历字符串直到满足特定条件(如遇到元音字母),并注意了防止索引越界的技巧。 - 我们应用
while循环实现了两个函数:up_to_vowel用于提取子字符串,get_answer用于获取有效的用户输入。 - 掌握
while循环是控制程序流程的重要一步,它让你能处理更多需要根据动态条件进行重复的任务。
028:代码注释


在本节课中,我们将要学习如何在Python代码中使用注释。注释是用于解释代码目的和逻辑的文本,它们不会被Python解释器执行。随着我们开始编写更复杂的程序,使用注释来记录代码将变得至关重要。

上一节我们介绍了条件语句和循环,这使得我们要处理的问题变得更加复杂。本节中我们来看看如何通过注释来清晰地解释这些复杂代码的逻辑。
什么是注释?


一个注释以井号(#)字符开始,并持续到该行的末尾。Python会忽略这些字符。它们的作用是记录代码,类似于文档字符串(docstrings)记录函数。

以下是注释的基本语法:
# 这是一个注释。这一整行都会被Python忽略。
print("Hello, World!") # 这是一个行内注释,放在代码后面。
注释的作用:解释变量关系


让我们通过一个具体的函数来理解注释的用途。以下是之前课程中提到的 up_to_vowel 函数,我们为其添加了两个注释。
def up_to_vowel(s):
""" (str) -> str
返回字符串 s 中从开头到第一个元音字母(但不包括该元音)的子串。
"""
# before_vowel 包含了 s[0:i] 中的所有字符
before_vowel = ''
i = 0
# 累积字符串开头的非元音字母
while i < len(s) and s[i] not in 'aeiouAEIOU':
before_vowel = before_vowel + s[i]
i = i + 1
return before_vowel


第一个注释 # before_vowel 包含了 s[0:i] 中的所有字符 描述了函数中使用的变量之间的关系。它说明了在循环的任何时刻,变量 before_vowel 中存储的内容都等于字符串 s 从索引0到索引 i(不包括 i)的子串。

为了理解这意味着什么,我们以单词 "ZYMURGY" 为例进行分析。字符串索引从0开始:Z(0), Y(1), M(2), U(3), R(4), G(5), Y(6)。

根据Python切片规则 s[0:i] 不包含索引 i 处的字符。因此:
s[0:0]是空字符串''s[0:1]是'Z's[0:2]是'ZY's[0:3]是'ZYM'- 以此类推。
在函数开始时,我们将 before_vowel 初始化为空字符串 '',将 i 初始化为 0。此时 s[0:0] 确实是空字符串,这验证了注释中描述的关系在初始状态下成立。


随着循环执行,每次我们将一个非元音字符添加到 before_vowel 末尾,并将 i 加1。这个操作始终维持着 before_vowel == s[0:i] 的关系。例如,当 i 为2时,before_vowel 的值是 'ZY',恰好等于 s[0:2]。
找出并维持变量之间的这种关系是编写正确循环的关键。一旦你理清了这种关系,最好将其写在注释里,这样下次阅读代码时就不需要重新推导。


注释的作用:描述代码目的
第二个注释 # 累积字符串开头的非元音字母 描述了循环做什么,而不是怎么做。
这类似于函数的文档字符串(docstring),它说明函数的功能,而不深入其内部实现细节。这个注释告诉阅读代码的人:这个循环的目的是收集字符串开头连续的非元音字母。
注释是写给程序员看的,因此它们不需要描述循环每一步是如何工作的。你应该尝试为复杂的代码段添加注释,以解释其目的。同时,也应该用注释来描述变量之间的关系,这有助于理解代码的逻辑流。
以下是关于编写良好注释的一些建议:

- 解释“为什么”和“是什么”:注释应说明代码段的目的或某个复杂逻辑的原因。
- 记录变量关系:对于循环和复杂状态变更,注明关键变量之间是如何关联的。
- 保持简洁和更新:注释应简洁明了,并且在代码修改时,记得同步更新相关的注释。

总结
本节课中我们一起学习了Python代码注释的用法。我们了解到:
- 注释以
#开头,用于对代码进行解释说明,Python会忽略它们。 - 注释主要有两个作用:一是描述变量之间的重要关系(如
before_vowel == s[0:i]),帮助理解程序状态;二是说明一段代码块(如循环)的总体目的。 - 为复杂逻辑和变量关系添加注释,可以极大地提高代码的可读性和可维护性,方便你和其他人未来理解代码意图。

通过有意识地使用注释,你可以让自己的程序逻辑更清晰,更像一个完整的、可被理解的作品,而不仅仅是一堆让计算机执行的指令。
029:列表类型入门 📚

在本节课中,我们将要学习Python中一个非常重要的数据类型——列表。列表用于存储一系列有序的数据项,例如学生成绩或书名。我们将了解如何创建列表、访问其中的元素,以及使用一些内置函数和循环来处理列表。

列表的创建与索引
列表使用方括号 [] 创建,其中的元素用逗号分隔。列表中的每个元素都有一个位置索引,索引从 0 开始计数。
例如,我们创建一个包含三个整数的成绩列表:
grades = [80, 90, 70]
在这个列表中:
grades[0]指的是整数80。grades[1]指的是整数90。grades[2]指的是整数70。
列表切片
与字符串类似,我们可以使用切片操作来获取列表的一部分。切片语法为 list[start:end],其中 start 是起始索引(包含),end 是结束索引(不包含)。
以下是几个切片示例:
grades[1:2] # 结果是 [90],包含索引1,不包含索引2
grades[0:2] # 结果是 [80, 90]
列表切片的规则与字符串切片完全相同。
成员检查与内置函数
我们可以使用 in 操作符来检查一个元素是否存在于列表中。
80 in grades # 结果为 True,因为80在列表中
60 in grades # 结果为 False,因为60不在列表中
Python的许多内置函数也可以应用于列表。
以下是几个常用函数:
len(list):返回列表的长度(元素个数)。min(list):返回列表中的最小值。max(list):返回列表中的最大值。sum(list):对列表中的所有数值元素求和。
让我们将这些函数应用于 grades 列表:
len(grades) # 结果为 3
min(grades) # 结果为 70
max(grades) # 结果为 90
sum(grades) # 结果为 240 (80+90+70)
不同类型的列表
列表不仅可以包含数字,还可以包含字符串或其他任何类型的数据。
例如,我们创建一个学科名称的字符串列表:
subjects = [‘chemistry‘, ‘physics‘, ‘math‘, ‘bio‘]
len、min、max 函数同样适用于字符串列表,它们会基于字典顺序进行比较。
len(subjects) # 结果为 4
min(subjects) # 结果为 ‘bio‘ (按字母顺序最早)
max(subjects) # 结果为 ‘math‘ (按字母顺序最晚)
需要注意的是,sum 函数不能用于字符串列表,尝试这样做会导致 TypeError。
混合类型列表
一个列表甚至可以同时包含不同类型的数据。
例如,创建一个表示街道地址的列表:
street_address = [1600, ‘Pennsylvania Avenue‘]
这个列表包含了一个整数(门牌号)和一个字符串(街道名)。
遍历列表
我们经常需要逐个处理列表中的每个元素,这时可以使用 for 循环。
以下是遍历 grades 列表的示例:
for grade in grades:
print(grade)
循环会依次将列表中的每个元素(80, 90, 70)赋值给变量 grade 并执行打印操作。变量名 grade 可以替换为任何其他有效的名称,如 item 或 element。
同样地,我们可以遍历 subjects 列表:
for subject in subjects:
print(subject)
循环会从索引0开始,依次处理列表中的每个元素,直到列表末尾。

本节课中,我们一起学习了Python列表的基础知识。我们了解了如何创建列表、通过索引和切片访问元素、使用 in 操作符和 len、min、max、sum 等内置函数,以及如何使用 for 循环来遍历列表中的所有元素。列表是组织和管理数据集合的强大工具,是后续学习更复杂数据结构的基础。
030:列表方法 📚

在本节课中,我们将学习Python列表的各种方法。列表是Python中一种非常灵活的数据结构,而方法则是与特定对象(如列表)关联的函数。我们将通过一个收集并管理个人喜爱颜色的示例,逐一介绍列表的常用方法,包括如何添加、删除、查找和排序列表中的元素。

什么是列表方法?🤔
上一节我们介绍了列表的基本概念。本节中,我们来看看列表的“方法”。一个方法是存在于对象内部的函数。对于列表类型,它内置了多种方法。
以下是列表类型的主要方法(忽略那些以下划线开头的方法):
appendcountextendindexinsertpopremovereversesort
这些方法中,有些会修改列表本身,有些则只是返回信息。
构建初始列表 🏗️
我们将通过一个示例来演示这些方法。目标是跟踪一个人最喜欢的颜色。首先,我们需要一个循环来收集用户输入的所有颜色,但我们必须先询问第一个颜色。
以下是初始设置:
colors = []
prompt = "请输入你最喜欢的颜色之一,按回车键结束:"

我们通过调用input函数并传入提示语来要求用户输入第一个颜色。
color = input(prompt)
假设用户输入了“blue”。此时,变量color指向字符串“blue”,而变量colors仍然指向一个空列表[]。

为了累积用户输入的信息,我们将使用一个while循环。只要color不是空字符串(即用户没有直接按回车),我们就执行循环。
while color != "":
colors.append(color)
color = input(prompt)
在循环体内,我们首先将当前颜色color追加到列表colors中。然后,为下一次循环迭代做准备,再次询问用户输入另一个颜色。
假设我们依次输入了“yellow”和“brown”,然后按回车键结束循环。此时,检查colors变量,会发现它包含了字符串:[‘blue‘, ‘yellow‘, ‘brown‘]。

修改列表:添加与删除元素 🔧
上一节我们创建了一个基础的颜色列表。本节中,我们来看看如何修改它。
使用 extend 方法添加多个元素
我们发现忘记了一些颜色。可以使用extend方法,通过传入一个我们忘记的颜色列表来扩展colors列表。
colors.extend(["hot pink", "neon green"])
现在,检查colors,会看到:[‘blue‘, ‘yellow‘, ‘brown‘, ‘hot pink‘, ‘neon green‘]。
注意:extend方法期望接收一个列表作为参数。如果直接传入多个字符串,如colors.extend("auburn", "teal", "magenta"),会导致TypeError。
使用 pop 方法移除元素
我们不太喜欢“neon green”了。可以调用colors.pop()来移除它。pop方法默认移除列表中的最后一个元素。
colors.pop()
检查colors,现在列表是:[‘blue‘, ‘yellow‘, ‘brown‘, ‘hot pink‘]。“neon green”已不在其中。
需要指出的是,colors.pop()不仅移除了最后一个元素,还返回了被移除的元素。因此你可以在赋值语句中使用它。像colors.pop()这样既返回信息又对数据(如列表)产生影响的函数,被称为具有副作用。
我们也不喜欢“brown”了,但这次我们想移除列表中特定位置的元素。可以使用pop方法的可选参数来指定要移除元素的索引。“brown”在索引2的位置(列表索引从0开始)。
colors.pop(2)
这将移除并返回索引2处的元素(即“brown”)。现在colors变为:[‘blue‘, ‘yellow‘, ‘hot pink‘]。
总结一下:
colors.pop()移除列表中的最后一个项目。colors.pop(index)移除给定索引处的项目。
使用 remove 方法按值移除元素

remove方法接收一个要移除的对象(值),而不是索引。它会移除列表中该对象的第一个匹配项。
colors.remove("yellow")
现在colors只剩下:[‘blue‘, ‘hot pink‘]。
但是,如果我们尝试移除一个不在列表中的元素(例如“black”),remove方法会引发一个ValueError。
colors.remove("black") # 这会引发 ValueError
因此,在调用可能引发错误的方法或函数之前,最好先进行检查。我们可以先使用count方法计算某个值在列表中出现的次数。
if colors.count("yellow") > 0:
colors.remove("yellow")
更标准的做法是使用in关键字进行检查:
if "yellow" in colors:
colors.remove("yellow")
如果“yellow”不在列表中,if语句的条件会评估为False,我们就不会调用colors.remove,从而避免了错误。

组织列表:排序与反转 🔄
上一节我们学习了如何增删列表元素。本节中,我们来看看如何重新组织列表的顺序。

首先,让我们用extend方法正确地添加一些新颜色。
colors.extend(["auburn", "teal", "magenta"])
现在colors是:[‘blue‘, ‘hot pink‘, ‘auburn‘, ‘teal‘, ‘magenta‘]。

使用 sort 方法排序

我们可以调用colors.sort()对列表进行原地排序(按字母顺序)。
colors.sort()
排序后,colors变为:[‘auburn‘, ‘blue‘, ‘hot pink‘, ‘magenta‘, ‘teal‘]。
使用 reverse 方法反转

我们可以调用colors.reverse()来反转列表的顺序。
colors.reverse()
反转后,colors变为:[‘teal‘, ‘magenta‘, ‘hot pink‘, ‘blue‘, ‘auburn‘]。

精确控制:插入与查找元素 🎯
使用 insert 方法插入元素
我们对移除“brown”感到有些愧疚,想把它放回列表中它应该在的位置。可以使用insert方法在特定索引处插入一个元素。

我们想在倒数第二个位置(索引-2)插入“brown”。
colors.insert(-2, "brown")
insert方法不会替换原索引处的元素,而是将该位置及之后的元素向后移动,为新元素腾出空间。现在colors是:[‘teal‘, ‘magenta‘, ‘hot pink‘, ‘brown‘, ‘blue‘, ‘auburn‘]。
使用 index 方法查找元素索引
现在,我们觉得“hot pink”对于现在的我们来说有点太鲜艳了,想移除它。为了移除,我们需要知道它的索引。可以使用index方法来查找。

但是,和remove方法一样,如果查找的元素不在列表中,index方法也会引发ValueError。
colors.index("neon green") # 会引发 ValueError,因为我们已没有"neon green"
为了防止错误发生,我们在调用index之前先进行检查。
if "hot pink" in colors:
where = colors.index("hot pink")
colors.pop(where)
首先检查“hot pink”是否在列表中。如果在,则获取它的索引并赋值给变量where,然后使用colors.pop(where)将其移除。
最终,我们剩下的颜色列表是:[‘teal‘, ‘magenta‘, ‘brown‘, ‘blue‘, ‘auburn‘]。
总结 📝

本节课中,我们一起学习了Python列表的核心方法。我们通过管理一个颜色列表的完整流程,实践了如何使用append和extend添加元素,使用pop和remove删除元素,使用sort和reverse调整顺序,以及使用insert在指定位置插入元素。同时,我们强调了在使用remove和index等方法前,使用in关键字或count方法进行检查的重要性,以避免程序因值错误而崩溃。掌握这些方法,你就能有效地操作和管理列表数据了。
编程基础:05:可变性与别名 🧠

在本节课中,我们将学习可变对象的概念,以及别名现象如何影响程序行为。理解这些概念对于掌握Python中数据如何被修改至关重要。


内存地址的作用
之前我们介绍的内存地址即将变得非常有用。事实证明,你可以修改某些对象的内容。如果没有内存地址,将很难解释其中发生的变化。

列表对象的可变性
上一节我们提到了内存地址,本节中我们来看看列表对象如何被修改。

在代码的第一行是一个赋值语句。我们计算右侧的表达式,这会创建一个列表对象以及六个整数:0, 2, 4, 6, 8, 10。创建这个列表结构后,我们将列表对象的内存地址赋值给变量 lst。
lst = [0, 2, 4, 6, 8, 10]

因此,lst 包含内存地址 ID1,它指向列表对象。在索引0处是内存地址 ID2,它指向整数对象 0。索引1指向 2,索引2指向 4,依此类推。
修改列表元素
在第二行是另一个赋值语句。左侧不仅仅是一个变量,而是更复杂的结构:lst[2]。
lst[2] = 5
我们实际上要修改那个内存位置。执行该赋值语句后,lst[2] 将不再指向整数 4,而是指向整数 5。我们正在修改堆中的一个对象。
执行后,我们看到在内存地址 ID8 处有一个新对象,lst[2] 现在指向这个新对象。


别名的概念
让我们编辑代码,看看其他情况。我们将用一些其他代码替换这段代码。
第一行,我们知道会发生什么。list1 指向一个列表。list1[0] 指向 0,list1[1] 指向 2,list1[2] 指向 4,等等。
在第二行,我做了一件相当有趣的事情。我将变量 list1 中的内存地址,放入了变量 list2 中。
list1 = [0, 2, 4, 6, 8, 10]
list2 = list1
唯一改变的是模块内存的内容。现在,list1 和 list2 都指向同一个列表对象。
这被称为别名:当两个变量包含相同的内存地址时,就发生了别名现象。

通过别名观察修改
目前一切顺利。我们现在要做的是,将 list1 指向的对象的最后一个元素赋值为 17。
list1[-1] = 17
列表中索引5处的元素将改变。我们将失去对 10 的引用,取而代之的是索引5处的元素将指向整数 17。
有趣的是,list2 也观察到了这个变化。list2 包含列表对象的内存地址。我们通过 list1 改变了那个对象,但 list2 也随之改变。
因此,当我们打印 list1[-1] 时,得到 17。当我们打印 list2[-1] 时,也会得到完全相同的结果。

函数中的别名示例
我们将再看一个别名的例子。这里有一个函数 double_even_indices,它接受一个整数列表作为参数,不返回任何内容(或者说返回 None)。
def double_even_indices(lst):
"""将列表中索引为0, 2, 4...的整数值加倍。"""
i = 0
while i < len(lst):
lst[i] = lst[i] * 2
i = i + 2

文档字符串说明:从索引0开始,将列表中每隔一个的整数值加倍。因此,索引0, 2, 4, 6, 8...处的值将被加倍。
我们将 i 初始化为0,i 将是下一个要加倍的元素的索引。只要还有剩余元素(即 i 小于列表长度),循环就会继续。在循环内,我们查看索引 i 处的值,将其加倍,然后将新值的引用存储回列表的索引 i 处。接着,我们将 i 加2,以跳到下一个偶数索引。

我们的主程序将创建一个列表,赋值给变量 list1,打印该列表,调用函数处理该列表中偶数索引的整数,然后再次打印列表。
list1 = [11, 12, 13, 14, 15, 16, 17]
print(list1)
double_even_indices(list1)
print(list1)
我们定义函数,创建列表结构,并打印列表。注意,变量 list1 包含内存地址 ID2,而 ID2 是我们的列表对象的内存地址。
我们将为此创建一个别名,但不是通过赋值语句,而是通过函数调用。我们计算参数,得到内存地址 ID2,这个地址将被放入函数 double_even_indices 创建的新帧中的变量 lst 里。
现在,在函数 double_even_indices 中,变量 lst 指向那个列表对象。变量 list1 也指向同一个列表对象。这意味着我们对那个列表对象所做的任何更改,两个变量都能看到。
我们开始执行函数。i 从索引0开始。列表长度是7,所以0小于7。我们将索引0处的值(11)乘以2得到22,然后让 lst[0] 指向这个新的22。接着 i 加2变为2。
2小于7,我们对索引2处的值(13)执行相同操作,将其加倍为26,并让 lst[2] 指向26。i 加2变为4。
4小于7,我们再次操作,将索引4处的15替换为30。i 加2变为6。
6小于7,我们将索引6处的17加倍为34,并让 lst[6] 指向34。i 加2变为8。
现在 i 为8,这使得循环条件 i < len(lst) 为假。一旦循环条件为假,循环终止,我们退出循环。
函数 double_even_indices 末尾没有其他语句,因此它将返回 None。记住,如果函数中没有 return 语句,函数将返回 NoneType 类型的值 None。
回到主模块,我们刚刚调用了 double_even_indices,现在继续执行第15行(即第二个 print(list1))。
list1 现在指向的列表对象在索引0处是22,然后是12, 26, 14, 30, 16, 34。尽管我们在函数体内从未提及 list1,但 list1 现在指向一个已被修改的对象。因此,我们的输出包含了新的值。


可变性与文档字符串
由于别名的存在,你需要记住哪些类型是可变的,哪些是不可变的。到目前为止,列表是唯一可变的类型,其他类型都是不可变的。


此外,在为可变类型编写文档字符串时需要格外小心。你需要明确说明函数内部的代码是否会修改参数。
总结
本节课中,我们一起学习了:
- 可变对象:列表等对象的内容可以被修改。
- 别名:当多个变量引用同一个对象时,通过任何一个变量修改对象,其他变量也会看到变化。
- 函数中的别名:将可变对象传递给函数时,函数内部对参数的修改会影响原始对象。
- 注意事项:需要清楚哪些类型是可变的,并在文档中明确函数是否会修改其参数。

理解可变性和别名是避免程序中意外副作用的关键。
032:范围

在本节课中,我们将学习Python内置函数range。range函数用于生成一个数字序列,这个序列在表示字符串或列表的索引时非常有用。我们将通过几个例子来了解range的基本用法及其参数。
使用range生成数字序列

在第一个例子中,我们将结合for循环和range函数来生成并显示从0到9(包括9)的整数序列。

以下是具体步骤:
for number in range(10):
print(number)
屏幕上显示的数字是从0到9的整数值。默认情况下,range生成的序列起始值是0,结束值是传入的参数值(但不包括该值),步长默认为1。
探索range函数的参数
让我们查看range函数的帮助文档。虽然帮助文档内容较多,但我们可以关注其函数签名。
从函数签名可以看出,range最多可以接受三个参数。在我们之前的例子中,我们只提供了一个参数,即stop(停止值)。我们还可以选择性地提供start(起始值,默认为0)和step(步长,默认为1)参数。
使用range生成字符串索引
range函数的一个常见用途是生成代表字符串或列表索引的数字序列。
以字符串“computer science”为例,它有16个字符,因此索引值从0到15。使用range,我们可以生成这些值。
以下是具体步骤:
s = "computer science"
for i in range(len(s)):
print(i)
这里,i代表字符串s的索引。打印出的值是0到15。
调整起始索引
如果我们只想从索引1开始,可以使用start参数。
以下是具体步骤:
s = "computer science"
for i in range(1, len(s)):
print(i)
这次打印出的值是从1到15(包括15)。
生成奇数索引
如果我们只想要奇数索引,可以使用所有三个参数。
以下是具体步骤:
s = "computer science"
for i in range(1, len(s), 2):
print(i)
这里,起始值为1,步长为2,因此生成的序列是1、3、5、7等,即字符串s的奇数索引。
总结

在本节课中,我们一起学习了Python的range函数。我们了解了如何使用range生成数字序列,如何通过调整start、stop和step参数来控制序列的起始值、结束值和步长。我们还探讨了如何使用range生成字符串的索引序列,这在处理字符串和列表时非常有用。
033:循环遍历索引 🔄


在本节课中,我们将学习如何使用 for 循环来遍历字符串和列表的索引,而不是直接遍历它们的元素。这将使我们能够解决一些仅靠遍历元素无法解决的问题。


为什么需要遍历索引? 🤔
在前面的课程中,我们使用 for 循环来遍历字符串的字符以及列表的项。然而,有些问题需要我们比较一个元素与其相邻元素,或者需要知道元素在序列中的具体位置。这时,直接访问元素的索引就变得至关重要。
例如,我们需要比较字符串中相邻的字符是否相同。仅仅知道字符本身是不够的,我们还需要知道每个字符在字符串中的位置,才能找到它“旁边”的字符进行比较。
问题一:统计相邻重复字符对 📊
我们的第一个任务是编写一个函数,统计一个字符串中相邻且相同的字符对的数量。

例如,在字符串 "ACCFGG" 中,有三对相邻的相同字符:CC、GG 和 FF。

解决思路
要解决这个问题,我们需要比较字符串中每一对相邻的字符。这意味着我们需要访问字符的索引。
- 索引
0的字符需要与索引1的字符比较。 - 索引
1的字符需要与索引2的字符比较。 - 以此类推,直到比较索引
-2(倒数第二个)和索引-1(最后一个)的字符。

我们可以用公式来描述这个过程:比较字符串在索引 i 和索引 i+1 处的字符。

为了生成这些 i 值,我们将使用 range 函数。我们需要 i 的值从 0 开始,一直到字符串长度减 2(即倒数第二个索引)。因此,循环可以这样写:
for i in range(len(s) - 1):
# s[i] 将与 s[i+1] 比较

编写函数

以下是实现该功能的函数 count_adjacent_repeats:


def count_adjacent_repeats(s):
"""
返回字符串 s 中相邻重复字符对的数量。
"""
repeats = 0 # 初始化计数器
for i in range(len(s) - 1): # 遍历到倒数第二个字符的索引
if s[i] == s[i + 1]: # 比较当前字符和下一个字符
repeats = repeats + 1 # 如果相同,计数器加1
return repeats # 返回最终计数


让我们测试一下这个函数:


print(count_adjacent_repeats("ACCFGG")) # 输出:3
print(count_adjacent_repeats("ABCCBBA")) # 输出:2

函数按预期工作。通过遍历索引,我们成功地比较了每一对相邻的字符。

问题二:列表左移 🔀

现在,我们来看另一个需要遍历索引的问题:编写一个函数,将列表中的所有元素向左移动一位,并将第一个元素移到列表末尾。


函数的前提条件是列表长度至少为 1。

例如,列表 ["A", "B", "C", "D"] 经过左移后,应该变成 ["B", "C", "D", "A"]。
解决思路
这个操作的关键在于,当我们把第二个元素(索引 1)移动到第一个位置(索引 0)时,会覆盖掉原来的第一个元素。因此,我们需要在移动开始前,用一个变量(例如 first_item)保存第一个元素的值。
然后,我们从索引 1 开始遍历到列表末尾。在循环中,我们将每个元素赋值给它左边的位置:L[i-1] = L[i]。
循环结束后,所有元素都向左移动了一位,最后一个位置是空的。这时,我们将之前保存的 first_item 赋值给最后一个位置 L[-1]。
编写函数
以下是实现该功能的函数 shift_left:
def shift_left(L):
"""
将列表 L 中的元素向左移动一位。
第一个元素被移动到列表末尾。
修改原列表,不返回新列表。
"""
if len(L) > 0: # 确保列表非空
first_item = L[0] # 保存第一个元素
for i in range(1, len(L)): # 从索引1开始遍历
L[i - 1] = L[i] # 将当前元素移到左边
L[-1] = first_item # 将原第一个元素放到末尾
让我们测试一下这个函数:

letters = ["A", "B", "C", "D"]
shift_left(letters)
print(letters) # 输出:['B', 'C', 'D', 'A']

函数成功修改了原列表。注意,这个函数没有返回值(或者说返回 None),它的作用是通过修改参数来体现的。

总结 📝

在本节课中,我们一起学习了如何使用 for 循环和 range() 函数来遍历序列的索引,而不仅仅是元素值。


- 核心概念:当我们需要根据元素在序列中的位置来解决问题时(例如比较相邻项、交换位置),遍历索引是必要的方法。
- 关键工具:
range(len(sequence))是生成索引的常用模式。通过调整range的起始和结束参数,我们可以精确控制需要遍历哪些位置。 - 应用实例:
- 我们编写了
count_adjacent_repeats函数,通过比较s[i]和s[i+1]来统计相邻重复字符。 - 我们编写了
shift_left函数,通过操作L[i-1] = L[i]来实现列表元素的左移,并在过程中使用临时变量保存关键数据。
- 我们编写了


掌握遍历索引的技巧,极大地扩展了我们处理序列数据的能力,为解决更复杂的编程问题奠定了基础。
034:平行列表与字符串 📚

在本节课中,我们将学习如何使用索引循环来同时处理两个不同的列表或字符串。我们将通过编写两个具体的函数来实践这一概念:一个用于对两个列表中对应位置的元素求和,另一个用于统计两个字符串中对应位置字符相同的次数。

我们已经学习过如何对单个字符串或列表使用索引循环。
本节中,我们将使用索引循环来访问两个不同列表中对应位置的元素,以及两个不同字符串中对应位置的字符。



我们将从实现一个名为 sum_items 的函数开始。


它接收两个参数 list1 和 list2,它们都是数字列表,并返回一个新的数字列表。


该函数的任务是返回一个新列表,其中每个元素是 list1 和 list2 在对应位置上元素的和。
让我们探讨一下“对应位置”的含义。
如果 list1 是一个包含三个元素 [1, 2, 3] 的列表,而 list2 是一个包含三个元素 [2, 4, 2] 的列表。
那么每个列表都有三个元素,索引分别为 0、1 和 2。
当我们说要考虑 list1 和 list2 的对应元素时,我们指的是位于相同索引处的元素。
例如,在索引 0 处,值 1 和 2 相加得到 3。在索引 1 处,值 2 和 4 相加得到 6。在索引 2 处,值 3 和 2 相加得到 5。
这些结果组成了函数返回的新列表 [3, 6, 5]。
现在,让我们来编写这段代码。

我们将返回一个新列表,因此创建一个变量 sum_list,初始指向一个空列表 []。
这个列表将在函数执行过程中逐个元素地构建。

从示例中我们看到,需要使用索引 i 来访问 list1 和 list2 的元素。
因此,我们需要遍历索引。
以下是实现步骤:
- 创建一个空列表
sum_list作为累加器。 - 使用
for循环遍历列表的索引范围。 - 在循环体内,计算两个列表在相同索引处元素的和。
- 将每次计算的和添加到
sum_list中。 - 循环结束后,返回
sum_list。
def sum_items(list1, list2):
sum_list = []
for i in range(len(list1)):
total = list1[i] + list2[i]
sum_list.append(total)
return sum_list
注意,由于函数的前提条件是 list1 和 list2 长度相同,因此我们可以选择任意一个列表的长度来生成索引范围。


现在让我们运行一个示例函数调用。首先导入模块,然后在交互式环境中调用函数,可以看到它返回了列表 [3, 6, 5]。
接下来,让我们实现另一个函数。

这个函数名为 count_matches,它涉及两个字符串。它有两个字符串参数 s1 和 s2,并返回一个整数,表示在字符串 s1 中有多少个位置上的字符与 s2 对应位置上的字符相同。



例如,对于字符串 "ape" 和 "ate",在索引 0 处都有 'a',在索引 2 处都有 'e',所以有两个匹配。对于字符串 "head" 和 "hard",在索引 0 处都有 'h',在索引 3 处都有 'd',同样有两个匹配。
实现这个函数的第一步是创建一个累加器。我将其命名为 num_matches,初始值为 0。这个变量将用于累加匹配的数量。
从示例中我们看到,需要比较字符串在相同索引处的字符。
这意味着我们将编写一个遍历字符串索引的循环。
以下是实现步骤:
- 创建一个整数变量
num_matches并初始化为 0。 - 使用
for循环遍历字符串s1的索引范围。 - 在循环体内,比较
s1[i]和s2[i]是否相等。 - 如果相等,则将
num_matches加 1。 - 循环结束后,返回
num_matches。
def count_matches(s1, s2):
num_matches = 0
for i in range(len(s1)):
if s1[i] == s2[i]:
num_matches = num_matches + 1
return num_matches
让我们运行示例函数调用。首先测试 "ape" 和 "ate",返回 2。然后测试 "head" 和 "hard",同样返回 2。


本节课中,我们一起学习了如何使用索引循环处理平行列表和字符串。我们实现了两个函数:sum_items 用于计算两个列表对应元素的和,count_matches 用于统计两个字符串对应位置字符相同的次数。关键在于使用 range(len(...)) 生成索引,并在循环体内通过相同的索引访问两个数据结构中的对应元素。
035:嵌套列表 📚

在本节课中,我们将要学习一种特殊的列表结构——嵌套列表。我们将了解什么是嵌套列表,如何访问其中的元素,并编写一个函数来计算嵌套列表中数值的平均值。

什么是嵌套列表? 📦
列表可以包含任何类型的元素。在本讲中,我们将处理那些元素本身也是列表的列表。这种列表被称为嵌套列表,即一个列表包含在另一个列表之中。
我们的第一个例子是一个嵌套列表,其中每个元素都包含一个作业名称(例如 assignment1)和该作业的成绩。因此,列表中的每一项都是一个包含两个元素的列表。
grades = [['assignment1', 80], ['assignment2', 90], ['assignment3', 70]]

列表 grades 的长度是 3,因为它有三个项。而每一项本身又是一个包含两个元素的列表。
访问嵌套列表的元素 🔍
我们可以通过索引来查看这一点。
grades[0] # 结果是列表 ['assignment1', 80]
grades[1] # 结果是列表 ['assignment2', 90]
grades[2] # 结果是列表 ['assignment3', 70]

这些内部列表(或称为项)的长度都是 2。我们可以通过将索引 0 处的列表传递给 len 函数来验证:
len(grades[0]) # 结果是 2
使用循环遍历嵌套列表 🔄

访问 grades 中元素的另一种方法是使用 for 循环,就像列表元素是字符串或整数时一样。
for item in grades:
print(item)
打印出的每个 item 都将是一个包含两个元素的列表。由于 grades 的每一项都是一个列表,它也可以被进一步索引。

双重索引访问内部值 🎯

因为索引 grades[0] 得到一个包含字符串 'assignment1' 和成绩 80 的列表,所以我们可以使用第二个索引来访问每个部分。
grades[0][0] # 结果是 'assignment1'
grades[0][1] # 结果是 80

类似地,对于 grades 中索引 1 处的元素:
grades[1][0] # 结果是 'assignment2'
grades[1][1] # 结果是 90

对于索引 2 处的元素:
grades[2][0] # 结果是 'assignment3'
grades[2][1] # 结果是 70

编写函数:计算平均成绩 📊
接下来,我们将编写一个函数,来计算这种形式列表中的平均成绩。
函数名为 calculate_average。请注意其类型约定:它接收一个参数,该参数是一个“列表的列表”,其中内部列表包含字符串和数字,这描述了我们一直在处理的列表类型。该函数将返回一个浮点数,即给定列表中成绩的平均值。
def calculate_average(assignment_grades):
"""
计算嵌套列表中所有成绩的平均值。
参数: assignment_grades (list of list of [str, number]): 作业名称和成绩的列表。
返回: float: 成绩的平均值。
"""
total = 0 # 用于累加成绩总和
for item in assignment_grades:
total = total + item[1] # item[1] 是成绩
average = total / len(assignment_grades) # 总和除以成绩数量
return average
要计算平均值,我们需要将所有成绩相加,然后除以成绩的数量。变量 total 用于跟踪成绩的总和,初始值为 0。我们遍历作业成绩列表,每个 item 都是一个包含字符串和整数的两元素列表。因此,我们将当前 total 加上列表项在索引 1 处的值(即成绩)。
for 循环执行完毕后,我们将总和 total 除以成绩的数量(即列表 assignment_grades 的长度),然后返回这个平均值。

让我们运行一个示例调用:

grades = [['assignment1', 80], ['assignment2', 90], ['assignment3', 70]]
result = calculate_average(grades)
print(result) # 输出 80.0
确认它确实返回了 80.0。
总结 ✨

本节课中我们一起学习了嵌套列表。我们了解到嵌套列表是包含其他列表作为元素的列表。我们学习了如何使用双重索引(例如 list[0][1])来访问内部列表的值,以及如何使用 for 循环遍历它们。最后,我们应用这些知识编写了一个 calculate_average 函数,用于计算嵌套列表中数值数据的平均值。掌握嵌套列表是处理更复杂、结构化数据的重要一步。
036:嵌套循环 🔄


在本节课中,我们将要学习循环结构中的一个重要概念:嵌套循环。循环体内部可以包含任何语句,包括其他循环。我们将通过具体的例子来探索这个概念,并学习如何编写一个计算嵌套列表平均值的函数。
嵌套循环的工作原理
上一节我们介绍了基本的循环结构,本节中我们来看看当一个循环位于另一个循环内部时会发生什么。我们称外层的循环为“外层循环”,内部的循环为“内层循环”。
以下是一个嵌套循环的示例代码:
for i in range(10, 13):
for j in range(1, 5):
print(i, j)
以下是这段代码的执行步骤:


- 外层循环开始,
i首先取值为10。 - 进入内层循环,
j开始取值。 - 内层循环会完整地执行一遍,
j会依次取值为1,2,3,4。- 当
i为10时,会依次打印:(10, 1),(10, 2),(10, 3),(10, 4)。
- 当
- 内层循环执行完毕后,程序回到外层循环。
i取下一个值11,然后再次完整执行内层循环,打印(11, 1),(11, 2),(11, 3),(11, 4)。- 最后,
i取值为12,并最后一次执行内层循环,打印(12, 1),(12, 2),(12, 3),(12, 4)。 - 所有循环执行完毕,程序结束。
关键点在于:对于外层循环的每一次迭代,内层循环都会从头到尾完整地执行一遍。
实践案例:计算各科平均分 📊
理解了嵌套循环的基本原理后,我们来看一个更贴近实际的例子。假设我们有一个列表,里面包含了多个列表,每个子列表代表一个学生在一门课程中获得的分数。
我们的目标是:为每一门课程(即每一个子列表)计算平均分,并将所有结果放入一个新列表中返回。
第一步:计算单个列表的平均值


在解决嵌套列表问题之前,我们先解决一个更简单的问题:如何计算一个列表的平均值?


假设有一门“英语”课的成绩列表:[70, 75, 80]。

计算平均值的步骤如下:
- 初始化一个累加器
total,从0开始。 - 遍历列表中的每一个分数
mark。 - 将每个
mark加到total上。 - 遍历结束后,用
total除以列表的长度,得到平均值。

用代码表示这个过程:

english_grades = [70, 75, 80]
total = 0
for mark in english_grades:
total = total + mark
average = total / len(english_grades)
# average 的结果是 75.0



第二步:扩展到嵌套列表

现在,我们要对包含多个课程成绩的嵌套列表进行操作。例如:
grades = [[70, 75, 80], [70, 80, 90, 100], [80, 100]]

我们的函数设计如下:
- 函数名:
averages - 参数:一个列表的列表(
list of list of numbers),即grades。 - 返回值:一个新列表,其中每个元素是对应子列表的平均值。
以下是实现思路:
- 创建一个空列表
averages_list用于存放结果。 - 使用外层循环遍历
grades中的每一个子列表(代表一门课)。 - 对于每个子列表,使用内层循环(即第一步的代码)计算其平均值。
- 将计算出的平均值添加到
averages_list中。 - 循环结束后,返回
averages_list。
第三步:代码实现与执行过程

根据以上思路,我们可以写出完整的函数:



def averages(grades):
averages_list = [] # 用于存放结果的空列表
for grade_list in grades: # 外层循环:遍历每一门课
total = 0 # 为当前课程初始化总分
for mark in grade_list: # 内层循环:遍历当前课的每个分数
total = total + mark # 累加分数
# 计算当前课程的平均分并添加到结果列表
averages_list.append(total / len(grade_list))
return averages_list # 返回所有平均分的列表
让我们通过一个具体的例子来跟踪程序的执行过程。假设我们调用 averages([[70, 75, 80], [70, 80, 90, 100], [80, 100]])。
- 第一次外层循环:
grade_list是[70, 75, 80]。- 内层循环累加:
total = 0 + 70 + 75 + 80 = 225。 - 计算平均分:
225 / 3 = 75,将75加入averages_list。
- 内层循环累加:
- 第二次外层循环:
grade_list是[70, 80, 90, 100]。total重置为0,然后累加:0 + 70 + 80 + 90 + 100 = 340。- 计算平均分:
340 / 4 = 85,将85加入averages_list。
- 第三次外层循环:
grade_list是[80, 100]。total重置为0,然后累加:0 + 80 + 100 = 180。- 计算平均分:
180 / 2 = 90,将90加入averages_list。
- 外层循环结束,函数返回
averages_list,其最终值为[75, 85, 90]。
总结
本节课中我们一起学习了嵌套循环的概念和应用。
- 我们了解到,循环体内部可以包含另一个循环,内层循环会针对外层循环的每次迭代完整执行。
- 我们通过一个计算各科成绩平均分的案例,实践了如何使用嵌套循环处理列表的列表这种数据结构。
- 关键步骤是:使用外层循环遍历每个子列表,再使用内层循环处理子列表内的每个元素,最后将结果汇总。

掌握嵌套循环是编写复杂程序逻辑的重要基础,它允许我们高效地处理多维数据。
037:读取文件 📖

在本节课中,我们将要学习如何在Python程序中读取文本文件。到目前为止,我们的程序都使用计算机内存来存储信息,但大多数信息实际上存储在计算机的文件中。本节将介绍四种读取文本文件的方法。

打开文件
在读取文件之前,我们需要告诉Python打开它。内置函数 open 可以完成这个任务。它接受两个参数:文件名和一个表示我们想要读取(而非写入)该文件的字母 'r'。
如果文件与程序在同一目录下,我们可以直接使用文件名。如果文件位于其他目录(例如我们这里显示的目录),则必须提供文件的完整路径。
file_handle = open('文件名.txt', 'r')

方法一:使用 readline 逐行读取

打开文件后,我们可以使用一个名为 readline 的方法。该方法从文件中读取一行,包括该行末尾的换行符,并将其返回。
line = file_handle.readline()
我们可以通过反复调用 readline 来读取整个文件。请注意,即使是空行,其末尾也包含换行符。

当读取到文件末尾时,再次调用 readline 会返回一个空字符串。readline 返回空字符串的唯一情况就是我们已到达文件末尾。

这意味着我们可以使用一个 while 循环来读取文件中的所有行。我们将关闭并重新打开文件,以便从文件开头开始读取。
以下是使用循环读取文件的示例:


file_handle = open('文件名.txt', 'r')
line = file_handle.readline()
while line != '':
print(line, end='')
line = file_handle.readline()
file_handle.close()
运行此循环时,你可能会看到文件行之间出现空行。这是因为 readline 返回的整行包含末尾的换行符,而 print 函数默认又会添加一个换行符。为了防止这种情况发生,我们在调用 print 函数时,将 end 参数设置为空字符串 ''。
方法二:使用 for 循环遍历文件
如果我们知道需要读取文件中的每一行,有一种更简单的方法。Python的 for 循环可以直接用于文件对象,每次迭代获取一行(包括换行符)。
file_handle = open('文件名.txt', 'r')
for line in file_handle:
print(line, end='')
file_handle.close()
方法三:使用 read 一次性读取整个文件
如果你的文件不是特别大,可以使用 read 方法一次性将整个文件内容读取为一个单独的字符串。
file_handle = open('文件名.txt', 'r')
entire_content = file_handle.read()
print(entire_content)
file_handle.close()
方法四:使用 readlines 读取所有行到列表
我们将展示的第四种也是最后一种读取文件内容的方法是使用 readlines 方法。该方法返回一个列表,其中包含文件中的所有行。
file_handle = open('文件名.txt', 'r')
lines_list = file_handle.readlines()
file_handle.close()
我们可以使用这个列表来打印文件中的所有行,方法与之前类似。如果我们希望保留这个列表以便后续访问其中的元素,这种方法非常有用。
for line in lines_list:
print(line, end='')
总结

本节课中,我们一起学习了在Python中读取文本文件的四种主要方法:使用 readline 逐行读取、使用 for 循环遍历、使用 read 一次性读取整个文件,以及使用 readlines 将文件内容读取到列表中。每种方法都有其适用场景,你可以根据具体需求选择最合适的一种。
038:写入文件 📝


在本节课中,我们将要学习如何向文件中写入数据。写入文件的操作与读取文件几乎一样简单。我们将通过一个具体的例子,演示如何复制一个文件,并在新文件的开头添加一行文本。
概述
上一节我们介绍了如何从文件中读取数据。本节中我们来看看如何向文件中写入数据。我们将学习使用 open 函数以写入模式打开文件,并使用文件的 write 方法写入内容。核心操作包括打开文件、写入字符串以及关闭文件。
打开文件与写入模式
与读取文件类似,写入文件的第一步是使用 open 函数。该函数接收两个主要参数:文件名和模式。
- 文件名:可以是文件的完整路径,也可以是文件名(如果文件与程序在同一目录下)。
- 模式:指定文件的打开方式。对于写入,我们主要使用:
'w':写入模式。如果文件已存在,会覆盖原有内容;如果文件不存在,则创建新文件。'a':追加模式。在文件末尾添加新内容,不会覆盖原有数据。
打开文件后,我们可以使用 write 方法向文件中写入字符串。需要注意的是,write 方法不会自动在字符串末尾添加换行符 \n,这与 Python 的 print 函数不同。
文件复制与修改示例

我们将编写一段代码,复制一个文件,并在新复制的文件第一行添加单词 “copy”。

获取源文件路径
首先,我们需要获取用户想要复制的源文件路径。Python 的 tkinter 模块提供了一个方便的文件对话框功能。
以下是导入和使用文件对话框的步骤:

- 从
tkinter导入filedialog子模块。 - 调用
filedialog.askopenfilename()函数。这会弹出一个系统文件选择器,让用户选择文件。 - 该函数会返回一个字符串,即所选文件的完整路径。

import tkinter.filedialog
# 弹出文件选择对话框,让用户选择要复制的文件
from_filename = tkinter.filedialog.askopenfilename()
执行上述代码后,会切换到 Python 界面并弹出文件选择器。例如,我们选择名为 flanders_fields.txt 的文件。选择后,函数返回该文件的路径字符串,并赋值给变量 from_filename。
获取目标文件路径
接下来,我们需要确定将文件复制到哪里,并指定新文件名。我们使用 filedialog.asksaveasfilename() 函数。
# 弹出“另存为”对话框,让用户指定新文件的保存位置和名称
to_filename = tkinter.filedialog.asksaveasfilename()
执行这行代码会再次弹出对话框,这次是“另存为”对话框。我们可以输入新文件名,例如 flanders_copy.txt。如果指定的文件名已存在,系统会提示是否覆盖。函数返回新文件的路径字符串,并赋值给变量 to_filename。
执行文件复制与写入
现在,我们有了源文件路径 from_filename 和目标文件路径 to_filename。可以开始执行复制和写入操作了。
操作步骤如下:
- 打开源文件并读取内容:以读取模式
'r'打开源文件,读取其全部内容到一个字符串变量中,然后关闭文件。 - 打开目标文件并写入:以写入模式
'w'打开目标文件。 - 写入新内容:首先使用
write方法写入字符串"copy\n"。注意,我们需要手动添加换行符\n。 - 写入原内容:接着,将之前读取的源文件内容字符串写入目标文件。
- 关闭目标文件:完成写入后,关闭文件以确保所有数据都保存到磁盘。
# 1. 读取源文件内容
read_file = open(from_filename, 'r')
contents = read_file.read()
read_file.close()

# 2. 打开目标文件用于写入
write_file = open(to_filename, 'w')
# 3. 写入第一行“copy”
chars_written1 = write_file.write('copy\n')
# 4. 写入源文件的所有内容
chars_written2 = write_file.write(contents)
# 5. 关闭目标文件
write_file.close()
重要提示:write 方法在成功写入后会返回一个整数,表示写入的字符数。例如,写入 'copy\n' 会返回 5,因为 c, o, p, y, \n 共5个字符。这个返回值可以用来验证写入是否按预期完成。

验证结果
完成代码执行后,我们可以在文件系统中找到新创建的文件 flanders_copy.txt。打开该文件,可以看到第一行是 “copy”,之后是原文件 flanders_fields.txt 的全部内容,这证实了我们的操作是成功的。
总结
本节课中我们一起学习了如何向文件写入数据。关键点包括:
- 使用
open(filename, 'w')以写入模式打开文件。 - 使用文件对象的
write(string)方法写入字符串,该方法不会自动添加换行符。 write方法会返回成功写入的字符数,可用于调试。- 操作完成后,务必使用
close()方法关闭文件。 - 通过结合
tkinter.filedialog模块,可以方便地实现图形化界面下的文件选择功能,使程序更友好。


通过复制文件并添加前缀行的例子,我们完整实践了读取、处理和写入文件的流程。
039:开发一个程序 🚀


在本节课中,我们将学习如何从头开始开发一个完整的程序。我们将通过一个具体案例——分析学生成绩分布并生成直方图——来了解程序设计的完整思考过程。本节课不会引入新的Python工具,但会展示如何将已知知识组合起来解决实际问题。


概述 📋
我们的目标是编写一个程序,读取一个包含学生成绩的文件,统计不同分数段(如0-9分、10-19分等)的人数,并将结果以直方图(用星号*表示)的形式输出到一个新文件中。
第一步:理解问题与数据格式
首先,我们需要明确输入文件的格式和期望的输出。
输入文件 a1.txt 的格式如下:
- 文件开头有一个标题行。
- 接着是一个空行。
- 之后是每行一个学生的学号和成绩(浮点数)。
例如:
Assignment 1 Grades
12345678 77.5
23456789 37.5
34567890 62.5
我们希望生成的输出文件 a1_hist.txt 内容类似这样:
0 - 9:
10 - 19:
20 - 29: *
30 - 39: **
...
100: *
其中,每个星号代表一个落在该分数段内的成绩。
第二步:高层设计(自顶向下设计)
面对一个复杂任务时,将其分解为几个独立的步骤是很好的方法。这个过程称为自顶向下设计。
对于本程序,我们可以将其分解为三个主要步骤:
- 读取成绩:将文件中的成绩读入一个列表。
- 统计分布:计算每个分数段(0-9, 10-19, ..., 100)中有多少个成绩。
- 写入直方图:将统计结果按照指定格式写入新的文件。
这些步骤相对独立,我们可以分别实现和测试它们。
第三步:实现步骤一 - 读取成绩
上一节我们介绍了程序的整体框架,本节中我们来实现第一个核心功能:从文件中读取成绩。
我们将创建一个函数 read_grades 来完成这个任务。它的输入是一个已打开用于读取的文件对象,输出是一个浮点数列表。
核心代码:
def read_grades(file):
""" (file open for reading) -> list of float
读取文件并返回成绩列表。
前置条件:文件格式正确,包含标题、空行,然后是学号和成绩。
"""
# 跳过标题行,直到遇到空行
line = file.readline()
while line != '\n':
line = file.readline()
grades = [] # 初始化累加器列表
line = file.readline()
while line != '': # 尚未到达文件末尾
# 找到行中最后一个空格(分隔学号和成绩)
last_space_index = line.rfind(' ')
# 提取空格后的部分(即成绩字符串),并转换为浮点数
grade = float(line[last_space_index + 1:])
grades.append(grade)
line = file.readline()
return grades
实现要点:
- 使用
while循环跳过文件开头的标题部分。 - 使用另一个
while循环读取剩余的所有行。 - 在每行中,使用
str.rfind(‘ ’)找到最后一个空格的位置,然后通过切片line[last_space_index + 1:]提取成绩字符串。 - 使用
float()将字符串转换为浮点数,并添加到累加器列表grades中。
第四步:实现步骤二 - 统计成绩分布
现在我们已经将成绩读入了列表,接下来需要统计每个分数段的数量。
我们将创建函数 count_grade_ranges。它的输入是成绩列表,输出是一个包含11个整数的列表,分别对应0-9, 10-19, ..., 90-99, 100这11个分数段的数量。
核心概念与公式:
如何确定一个成绩 grade(如77.5)属于哪个分数段?我们可以利用整数除法。
grade // 10的结果是7(因为77.5 // 10 = 7.0,转换为int后是7)。- 这个结果(7)正好可以作为我们统计列表的索引,对应70-79这个分数段。
- 特殊处理满分100分:当
grade == 100时,grade // 10结果是10,对应索引10。
核心代码:
def count_grade_ranges(grades):
""" (list of float) -> list of int
返回一个长度为11的列表,索引i处的元素表示成绩在[i*10, i*10+9]区间内的数量(i从0到9),索引10表示成绩为100的数量。
"""
# 初始化一个包含11个0的列表
range_counts = [0] * 11
for grade in grades:
# 确定该成绩所属分数段的索引
which_range = int(grade // 10)
# 特殊情况:100分属于索引10
if which_range == 10:
which_range = 10
# 增加对应分数段的计数
range_counts[which_range] += 1
return range_counts
第五步:实现步骤三 - 写入直方图文件
最后一步是将统计结果格式化为直方图并写入文件。
我们将创建函数 write_histogram。它的输入是分数段统计列表和一个已打开用于写入的文件对象。
以下是该函数需要完成的任务列表:
- 写入第一行 “0 - 9:” 以及对应数量的星号。
- 循环写入第1到第9个分数段(10-19 到 90-99)的行。
- 写入最后一行 “100:” 以及对应数量的星号。
- 注意对齐格式,
0-9和100前面需要额外添加空格以使冒号对齐。
核心代码:
def write_histogram(range_counts, file):
""" (list of int, file open for writing) -> None
将分数段统计列表以直方图格式写入文件。
"""
# 写入 0-9 分数段
file.write(' 0 - 9: ' + '*' * range_counts[0] + '\n')
# 写入 10-19 到 90-99 分数段
for i in range(1, 10):
low = i * 10
high = i * 10 + 9
file.write(str(low) + ' - ' + str(high) + ': ' + '*' * range_counts[i] + '\n')
# 写入 100 分数段
file.write('100: ' + '*' * range_counts[10] + '\n')
第六步:整合主程序并测试
现在,我们将所有函数整合到一个主程序中,并使用文件对话框让用户选择输入和输出文件。
核心代码:
import tkinter.filedialog
import grade_functions # 假设上述函数保存在此模块中
# 1. 让用户选择输入文件
input_filename = tkinter.filedialog.askopenfilename()
input_file = open(input_filename, 'r')
# 2. 读取成绩
grades = grade_functions.read_grades(input_file)
input_file.close()
# 3. 统计分数段
range_counts = grade_functions.count_grade_ranges(grades)
# 4. 让用户选择输出文件并写入直方图
output_filename = tkinter.filedialog.asksaveasfilename()
output_file = open(output_filename, 'w')
grade_functions.write_histogram(range_counts, output_file)
output_file.close() # 重要:关闭文件以确保数据写入磁盘
测试要点:
- 在开发过程中,应频繁运行和测试代码,即使它还不完整。例如,在打开文件后立即打印几行内容,可以快速发现文件路径或格式错误。
- 分别测试每个函数,确保其行为符合预期,然后再将它们组合起来。
- 注意文件操作后要调用
.close()方法,否则数据可能不会立即写入磁盘。


总结 🎉

本节课中,我们一起完成了一个完整程序的开发流程:
- 明确需求:理解输入输出格式。
- 高层设计:使用自顶向下设计将问题分解为“读取”、“统计”、“写入”三个步骤。
- 逐步实现:
- 实现了
read_grades函数,使用循环和字符串处理读取数据。 - 实现了
count_grade_ranges函数,利用整数除法//和列表索引进行统计。 - 实现了
write_histogram函数,格式化字符串并写入文件。
- 实现了
- 整合测试:将函数整合到主程序,通过文件对话框与用户交互,并强调了频繁测试和关闭文件的重要性。

这个案例展示了程序设计的核心思维:分解问题、设计数据表示、实现独立模块、逐步测试验证。希望这个过程能帮助你理解如何将想法转化为可运行的代码。
040:元组 🧱

在本节课中,我们将要学习一种新的序列类型——元组。我们将了解元组与列表的相似之处,以及它们之间最关键的区别:可变性。
概述

上一节我们介绍了可变的列表。本节中我们来看看另一种序列类型:元组。元组是不可变的序列。
元组的基本操作
元组与列表使用相同的索引和切片表示法。

以下代码创建了一个包含三个元素的元组,并将其赋值给变量 tup:
tup = (3, 4, -0.2)
我们可以像访问列表一样访问元组的元素:
tup[0] # 返回 3
tup[1] # 返回 4
tup[2] # 返回 -0.2
tup[-1] # 返回 -0.2
切片元组会生成一个新的元组:
tup[:2] # 返回 (3, 4)
tup[1:3] # 返回 (4, -0.2)
元组的不可变性
元组与列表最核心的区别在于其不可变性。这意味着元组一旦创建,其内容就不能被修改。

尝试修改元组会导致类型错误:
tup[0] = 5 # 会引发 TypeError: 'tuple' object does not support item assignment
错误信息明确指出:元组对象不支持元素赋值。

元组与列表的方法对比
由于元组不可变,它支持的方法比列表少得多。


以下是列表和元组支持的方法对比:
- 列表方法:
append,count,extend,index,insert,pop,remove,reverse,sort。 - 元组方法:
count,index。
列表的其他方法(如 append, sort)都会修改列表本身,因此不适用于不可变的元组。
元组的创建与迭代
元组在许多操作上与列表类似。

我们可以遍历元组中的元素:
for item in tup:
print(item)
len() 函数同样适用于元组:
len(tup) # 返回 3
我们也可以遍历元组的索引:
for i in range(len(tup)):
print(tup[i])
创建元组的注意事项
创建元组的基本方法是将一个由逗号分隔的值列表放在圆括号 () 中。
my_tuple = (1, 2, 3)
创建只包含一个元素的元组需要特别注意。如果只在括号中放一个值,Python会将其解释为普通的数学表达式括号。
not_a_tuple = (5) # 这只是一个整数 5
要创建单元素元组,必须在值后面加上一个逗号:
single_element_tuple = (5,) # 这是一个元组,包含元素 5
创建空元组则不需要这个逗号:
empty_tuple = () # 这是一个空元组
总结

本节课中我们一起学习了元组。我们了解到元组是一种不可变的序列,使用圆括号 () 创建。它与列表共享索引、切片和迭代等操作,但因其不可变性,不支持任何会修改自身内容的方法。记住创建单元素元组时需要在值后加逗号 (value,)。元组适用于存储不应被程序意外修改的数据集合。
041:类型字典 📚

在本节课中,我们将要学习Python中的字典类型。我们将探索如何使用字典来存储和访问信息,并比较字典与之前学过的列表在存储数据集合时的不同之处。

概述
之前我们已经学习了使用列表来存储数据集合,并通过索引来访问列表中的项目。本节中,我们来看看Python的另一种数据结构——字典。字典提供了一种通过“键”来存储和查找“值”的更直观方式。

从列表到字典

在之前的课程中,我们使用了一个嵌套列表来记录作业名称及其对应的分数。

grades = [['A1', 80], ['A2', 70], ['A3', 90]]

每个内部列表都是一个包含两个项目的列表:作业名称(字符串)和分数(数字)。我们可以使用索引来访问列表中的项目。例如,grades[0] 指向包含 A1 及其分数的列表。要获取特定作业的名称,我们需要使用该双元素列表的第一个索引。例如,要获取 grades[1] 的作业名称,我们使用索引 0。要访问与该作业关联的分数,我们使用内部列表的索引 1。
现在,我们将使用字典来存储相同的分数信息。我们将看到,在处理字典时,访问特定作业的分数会变得更简单。
创建字典
变量 assignment_to_grade 将引用这个新字典。字典的表示符号是花括号 {}。字典中的每个条目都有两部分,由冒号 : 分隔。
assignment_to_grade = {'A1': 80, 'A2': 70, 'A3': 90}
这个字典有三个键值对。A1、A2 和 A3 是键。80 是与键 A1 关联的值,70 是与键 A2 关联的值,90 是与键 A3 关联的值。
访问字典中的值
要查找与作业 A2 关联的分数,我们可以使用与嵌套列表类似的方括号表示法。但是,我们不是在括号内放入索引,而是放入一个键。
assignment_to_grade['A2']
这将返回值 70。这种查找与作业关联的分数的表示法,比使用嵌套列表所需的两个索引要简单得多。然而,这也揭示了字典的一个特性:为了能够查找与特定键关联的值,该键必须是唯一的,在字典中只能出现一次。因此,作业名称 A1、A2 和 A3 是唯一的名称。与这些作业关联的分数(即字典的值)可以重复。例如,键 A1 和 A2 可以具有相同的值,比如 80。

处理不存在的键

如果我们尝试访问一个不存在的列表索引,会发生错误。字典也是如此。如果我们尝试使用一个不存在的键访问字典,则会发生 KeyError。
# 这将引发 KeyError,因为 ‘A4’ 不存在
# assignment_to_grade[‘A4’]
在访问字典的键之前,我们可以检查该键是否存在于字典中。
‘A4’ in assignment_to_grade # 返回 False
‘A2’ in assignment_to_grade # 返回 True
这个 in 运算符检查的是键的存在,而不是值。因此,检查 80 in assignment_to_grade 也会返回 False,因为 80 不是该字典中的键。
字典的长度
目前,字典中有三个键值对。内置函数 len() 返回键值对的数量。
len(assignment_to_grade) # 返回 3
字典的可变性
与列表一样,字典是可变的。这意味着可以向字典中添加键值对,可以更改与键关联的值,也可以从字典中删除键值对。
添加键值对
让我们首先向这个字典添加另一个键值对。我们将添加作业 A4。
assignment_to_grade[‘A4’] = 85
我们使用与访问特定键的值相同的方括号表示法,但将其与赋值语句结合使用。现在,len(assignment_to_grade) 返回 4。
更新值
使用相同的表示法,我们也可以更新与键关联的值。
assignment_to_grade[‘A4’] = 90
因为 A4 已经存在于字典中,所以这是一个更改。如果键 A4 之前不存在,那么新的键值对就会被添加。现在,键 A4 关联的值从 85 变成了 90。请注意,值 90 同时与键 A3 和 A4 关联。值可以重复,但键不能。
删除键值对

现在,让我们从字典中删除一个键值对。为此,我们将使用 del 运算符,然后是字典引用和键(使用相同的方括号表示法)。
del assignment_to_grade[‘A4’]

字典的长度现在变回 3。字典中不再有键值对 A4: 90。
遍历字典
我们可以使用 for 循环来遍历字典的键,就像我们遍历字符串的字符或列表的项目一样。
for assignment in assignment_to_grade:
print(assignment)
因为我们在遍历键,所以我选择的变量名 assignment 代表字典的键。这将打印出键 A1、A3 和 A2。然而,这个顺序与我们向字典添加键值对的顺序(先 A1,然后 A2,然后 A3)并不匹配。键值对添加到字典的顺序,不影响 for 循环访问键的顺序。因此,在遍历字典的键时,我们不能依赖键以任何特定顺序被访问。
这次,我们不打印字典的键,而是打印值。
for assignment in assignment_to_grade:
print(assignment_to_grade[assignment])
所有我们为字典编写的 for 循环都是对键的循环,因此我们保持循环头完全相同。我们不是打印键 assignment,而是在字典中查找与该键关联的值并打印该值。这将打印出值 80、90 和 70。
最后,让我们同时打印键和值。
for assignment in assignment_to_grade:
print(assignment, assignment_to_grade[assignment])
当我们调用 print 时,可以传递两个参数:键 assignment 和与该键关联的值。这将打印每个作业及其关联的分数。
其他重要概念
我们对字典的介绍即将完成,但还有几个重要事项需要指出。
空字典
首先,字典可以是空的。我们只用一对空的花括号 {} 来表示。
empty_dict = {}
len(empty_dict) # 返回 0
键和值的类型
其次,我们需要注意字典键和值可以具有的类型。键和值可以是不同的类型。例如,一个键可以是字符串,另一个键可以是整数。然而,键的类型必须是不可变的。
由于列表是可变的,因此列表不能用作字典键。以下尝试将列表 [1, 2] 作为键并关联值 ‘banana’ 的赋值语句将导致错误。
# 这将引发 TypeError
# d = {[1, 2]: ‘banana’}
当我们想使用序列作为键时,可以使用元组而不是列表,因为元组是不可变的。
# 这是可以接受的
d = {(1, 2): ‘banana’}
总结

本节课中,我们一起学习了Python的字典类型。我们了解了如何创建字典、通过键访问和修改值、添加和删除键值对,以及遍历字典。我们还学习了字典键必须是唯一且不可变的这一重要特性。与列表相比,字典在通过特定标识符(键)快速查找关联信息时提供了更大的便利性和可读性。
042:反转字典 🔄

在本节课中,我们将学习如何使用字典作为累加器,并解决一个特定的问题:反转一个字典。我们将从一个简单的字典反转开始,逐步深入到处理更复杂的情况,即当多个键映射到同一个值时。

使用字典作为累加器
上一节我们介绍了字典的基本操作。本节中,我们来看看如何将字典用作一个累加器,来构建新的数据结构。
我们首先创建一个将水果映射到颜色的字典。

fruit_to_color = {
'banana': 'yellow',
'cherry': 'red',
'orange': 'orange',
'peach': 'orange',
'pomegranate': 'red',
'strawberry': 'red'
}
这个字典的条目没有按键或值排序。我们可以通过水果名称来索引它,以查找其颜色。
例如:
fruit_to_color['orange']的结果是'orange'。fruit_to_color['banana']的结果是'yellow'。

我们也可以遍历这个字典的键。
for fruit in fruit_to_color:
color = fruit_to_color[fruit]
print(fruit, color)
这段代码允许我们依次获取每个水果,并使用该水果作为字典的索引来查找对应的颜色值。
反转字典的问题
现在,我们进入今天要探讨的核心问题:将这个字典“里外翻转”或反转。我们想构建一个新的字典,将颜色映射到水果。

以下是实现此目标的一种方法:

color_to_fruit = {}
for fruit in fruit_to_color:
color = fruit_to_color[fruit]
color_to_fruit[color] = fruit
变量 color_to_fruit 是我们的累加器。运行这段代码后,我们检查 fruit_to_color 和 color_to_fruit。
然而,color_to_fruit 丢失了一些信息。字典中没有石榴、草莓和桃子。


问题在于,与这三个缺失水果相关的颜色,每个都对应着多个水果。例如,石榴是红色的,但樱桃也是红色的。当我们添加樱桃到新字典时,它覆盖了石榴的条目。
改进的反转方案
因此,我们不能简单地直接反转,需要更谨慎一些。我们将构建一个字典,将每种颜色映射到具有该颜色的水果列表。
以下是实现该算法的逻辑:
- 如果颜色还不是累加器字典中的键,我们就添加一个条目,将该颜色映射到一个只包含当前水果的列表。
- 否则,如果颜色已经是键,这意味着它已经有一个该颜色的水果列表。我们需要将当前水果追加到那个现有列表中。
以下是表达该算法的代码:
color_to_fruit = {}
for fruit in fruit_to_color:
color = fruit_to_color[fruit]
if color not in color_to_fruit:
color_to_fruit[color] = [fruit]
else:
color_to_fruit[color].append(fruit)
让我们看看效果如何。fruit_to_color 保持不变。现在的 color_to_fruit 如下:
{
'yellow': ['banana'],
'red': ['cherry', 'pomegranate', 'strawberry'],
'orange': ['orange', 'peach']
}
它的键是字符串,值是字符串列表。
-
如何找到橙色的水果?
我们使用字符串'orange'索引到字典中,这会返回一个列表['orange', 'peach']。然后我们可以使用常规的列表索引方式(例如color_to_fruit['orange'][0])来获取'orange'。 -
如何找到樱桃?
我们使用'red'索引到字典中,樱桃是返回列表['cherry', 'pomegranate', 'strawberry']中的第一个元素(索引 0)。
总结

本节课中,我们一起学习了如何反转字典。我们从简单的键值对调开始,发现了当原字典中多个键对应相同值时会导致信息丢失的问题。为了解决这个问题,我们引入了使用列表作为值的改进方案,从而完整地保留了所有映射关系。这个例子展示了如何根据具体需求,灵活地使用字典和列表来构建复杂的数据结构。
043:填充字典 📚



在本节课中,我们将结合之前学过的多个主题,学习如何从文件中读取数据,并将其存储在字典中。我们将通过一个具体的例子——读取学生作业1的成绩文件——来实践这一过程。


概述
我们将编写一个名为 read_grades 的函数。该函数接收一个已打开用于读取的文件作为参数。文件开头是一个不含空行的标题部分,接着是一个空行,之后才是包含学生学号和成绩的数据行。我们的任务是读取这些成绩信息,并构建一个字典。

构建字典的逻辑


这个字典的结构如下:
- 键:一个具体的成绩(浮点数)。
- 值:获得该成绩的所有学生学号(字符串)组成的列表。

例如,对于成绩 77.5 这个键,其对应的值可能是一个包含学号 0052 和 1311 的列表,因为这两位学生都获得了这个成绩。文件中后续可能还有其他获得 77.5 分的学生,他们也会被添加到这个列表中。

实现步骤

为了实现这个函数,我们需要完成两个主要任务:
- 跳过文件的标题部分。
- 读取成绩数据,并将其累积到字典中。

第一步:跳过标题

由于我们只需要处理文件的一部分数据,我们将使用 readline 方法来逐行读取。以下是具体步骤:

- 读取文件的第一行。
- 只要当前行不是空行(即只包含换行符
\n),就继续读取下一行。这确保了我们会读完整个标题部分,直到遇到那个作为分隔的空行。
第二步:累积成绩到字典


标题跳过之后,我们就可以开始处理真正的数据行了。我们将使用一个名为 grade_to_ids 的字典作为累加器,并初始化为空字典 {}。
以下是处理数据行的循环逻辑:

- 读取一行:使用
readline读取下一行。 - 循环条件:只要读取的行不是空字符串(
''),就继续循环。当readline读到文件末尾时,会返回空字符串,循环便会终止。 - 提取信息:对于每一行数据,我们需要从中提取学生学号和成绩。
- 学号固定为4个字符,因此可以通过切片获取:
student_id = line[:4]。 - 成绩是行中剩余的部分。由于成绩前后可能有空格和换行符,我们使用
strip方法清理:grade = line[4:].strip()。 - 此时
grade是字符串,我们需要用float()函数将其转换为浮点数:grade = float(grade)。
- 学号固定为4个字符,因此可以通过切片获取:
- 更新字典:根据成绩是否已存在于字典中,我们有两种处理情况:
- 情况一:成绩是新的键。如果
grade不在grade_to_ids中,我们需要为它创建一个新条目。值是一个仅包含当前学生学号的列表。if grade not in grade_to_ids: grade_to_ids[grade] = [student_id] - 情况二:成绩已存在。如果
grade已经是字典的键,我们只需要将当前学生学号追加到该键对应的值(列表)中。else: grade_to_ids[grade].append(student_id)
- 情况一:成绩是新的键。如果
- 继续循环:处理完当前行后,回到循环开始,读取下一行。

当所有行都处理完毕,循环结束,函数返回构建好的 grade_to_ids 字典。
代码示例与运行
将上述逻辑整合,read_grades 函数的完整实现如下:
def read_grades(gradefile):
"""从gradefile中读取标题后的行,返回一个字典。
键是成绩(浮点数),值是获得该成绩的学生学号列表。"""
# 第一步:跳过标题(直到遇到第一个空行)
line = gradefile.readline()
while line != '\n':
line = gradefile.readline()
# 第二步:初始化累加字典并开始读取数据
grade_to_ids = {}
line = gradefile.readline()
while line != '':
# 提取学号和成绩
student_id = line[:4]
grade = float(line[4:].strip())
# 更新字典
if grade not in grade_to_ids:
grade_to_ids[grade] = [student_id]
else:
grade_to_ids[grade].append(student_id)
# 读取下一行
line = gradefile.readline()
return grade_to_ids
运行这个函数,我们传入一个已打开的成绩文件对象:
grades_file = open('path/to/your/grades.txt', 'r')
result_dict = read_grades(grades_file)
grades_file.close()
print(result_dict)
输出结果将是一个字典,例如,键 77.5 对应的值列表中会包含学号 0052 和 1311 等。

总结
本节课中,我们一起学习了如何从结构化文本文件中读取数据并填充到字典中。我们回顾了文件读取(readline)、字符串处理(切片和 strip)、类型转换(float)以及字典的两种更新方式(添加新键和修改现有键的值)。通过将多个基础概念组合应用,我们实现了一个实用的数据处理函数。

浙公网安备 33010602011771号