Lecture 3
Print and None
1. 概述 (Overview)
本视频的核心论题在于辨析 Python 中“表达式求值”和“print() 函数调用”之间的根本区别。虽然两者在交互式环境中可能展现出相似的输出,但其底层的机制截然不同。视频通过对比不同类型的值(数字、字符串,尤其是特殊值 None)的两种输出行为,揭示了 print() 的本质:它是一个“非纯函数 (Non-Pure Function)”。它的主要功能是产生“副作用 (Side Effect)”(即将内容显示到屏幕),而它本身的“返回值 (Return Value)”永远是 None。视频最终通过一个嵌套 print() 调用的复杂案例,清晰地展示了理解这一区别对于掌握 Python 执行流程和调试代码至关重要。
2. 按照主题来梳理
主题一:print 与“自动显示”—— 看似相同,实则不同
在 Python 的交互式解释器(Interactive Interpreter)中,当我们输入一个表达式(如数字 -2)并按回车时,解释器会自动显示该表达式的值 [00:09]。然而,如果我们使用 print(-2),屏幕上也会显示 -2 [00:09]。这两种方式产生了完全相同的视觉输出,但它们是两个截然不同的过程 [00:17]。
这种差异在处理字符串时变得更加明显:
-
表达式求值: 如果我们输入一个字符串字面量,如
"go Bears",解释器会显示该字符串的值,包含着引号(例如'go Bears')[00:25]。这是因为解释器在显示这个值,而这个值的类型是字符串。 -
print调用: 如果我们调用print("go Bears"),解释器会执行print函数。这个函数的功能是获取字符串的内容,并将其显示出来,不带引号(即go Bears)[00:34]。
这证明了:print 是一个主动的行为(一个函数调用),而解释器在行尾自动显示结果是被动的规则。
主题二:特殊值 None 及其引发的常见错误
None 是 Python 中一个非常特殊的值,它用来代表“虚无”或“什么都没有” [00:48, 01:51]。None 的行为进一步放大了 print 和自动显示之间的区别:
-
表达式求值: 如果在解释器中单独输入
None,什么都不会显示 [00:57]。这是 Python 解释器的一条特殊规则:它会自动显示所有表达式的值,唯一的例外就是None值 [01:14]。 -
print调用: 如果我们调用print(None),屏幕上会确实地显示出 “None” 这个词 [00:57]。
None 作为隐式返回值
None 最常见的出现场景是作为函数的默认返回值。在 Python 中,一个函数如果没有显式地使用 return 语句返回值,它将在执行完毕后自动(隐式地)返回 None [02:06]。
视频中使用了一个例子来说明这个概念,定义了一个名为 does_not_square(不会平方)的函数:
1 | def does_not_square(x): |
当我们调用这个函数时,例如 does_not_square(4) [02:32]:
-
函数内部确实计算了
4 * 4,得到了 16。 -
但是,由于缺少
return语句,函数执行结束后返回了None。 -
这个
None值被交还给解释器。 -
根据我们刚学到的规则,解释器不会自动显示
None。 -
因此,调用
does_not_square(4)的结果是屏幕上什么也不出现 [02:50]。
None 如何导致程序错误
这种隐式返回 None 的行为是常见的 bug 来源。如果我们试图将这个“什么都没有”的结果赋值给一个变量,并用它进行后续计算,就会出错 [03:04]:
1 | # 1. 赋值 |
在执行第 3 步时,Python 会尝试计算 None + 4,这在逻辑上是不成立的。程序会立即崩溃,并抛出一个 TypeError (类型错误),提示无法对 NoneType (None 类型) 和 int (整数) 执行 + (加法) 操作 [03:26]。
主题三:解构嵌套 print—— 副作用与返回值的赛跑
视频提出了一个终极谜题:print(print(1), print(2)) [05:34]。
要理解这段代码的输出,我们必须结合前面学到的所有知识,特别是 print 的“副作用”和“返回值”。
当我们运行 print(print(1), print(2)) 时,输出结果是:
1 | 1 |
这个输出可以通过一个清晰的、分步的“表达式树 (Expression Tree)” [05:51] 求值过程来理解:
-
第 1 步:解析最外层
Python 解释器首先遇到最外层的 print(…) 调用。在执行它之前,Python 必须先确定它括号内所有参数的值。
-
第 2 步:求值第一个参数 print(1)
解释器开始执行第一个参数 print(1)。
-
第 3 步:求值第二个参数 print(2)
解释器接着执行第二个参数 print(2)。
-
第 4 步:执行最外层
print- 现在,解释器已经获得了最外层 print 所需的所有参数。在第 2 步和第 3 步中,它分别收到了 None 和 None 作为返回值。
- 因此,最外层的调用现在变成了 print(None, None) 。
-
第 5 步:执行 print(None, None)
解释器执行 print(None, None)。
-
第 6 步:最终收尾
整个嵌套表达式 print(print(1), print(2)) 已经完全执行完毕。它返回的最终值是 None(来自第 5 步)。
这个最终的 None 值被交还给交互式解释器。
-
第 7 步:解释器规则
根据我们在主题二中学到的规则,解释器不会自动显示作为表达式最终结果的 None 值。
因此,在 None None 之后,一切都结束了,不会再有任何额外输出。
3. 框架 & 心智模型 (Framework & Mindset)
心智模型:纯函数 (Pure Functions) vs. 非纯函数 (Non-Pure Functions)
要真正理解 print 和 None 的行为,核心在于建立一个关于“函数纯度”的心智模型 [03:56]。函数大致可以分为两类:
1. 纯函数 (Pure Functions) [04:03]
纯函数是行为最简单、最可预测的函数。你可以将其想象成一个“封闭的管道 (closed pipe)” [04:28]。
-
定义: 纯函数唯一的任务就是根据其输入(参数)计算出一个输出(返回值)。
-
特性:
-
它不与外界进行任何“交流”。它不会在执行过程中在屏幕上打印任何东西,不会修改传入的参数(如果参数是可变类型),不会读写文件,也不会改变任何全局变量。
-
给定相同的输入,它永远会返回相同的输出。
-
-
视频中的例子:
2. 非纯函数 (Non-Pure Functions) [04:03]
非纯函数,顾名思义,就是“不纯净的”函数。它们在返回值之外,还会做一些额外的事情。这种额外的事情被称为“副作用 (Side Effect)”。
-
定义: 非纯函数在执行时,除了(可能)返回一个值之外,还会对函数外部的状态产生可观察到的影响 [04:56]。
-
“副作用”的含义: 副作用是函数调用的一种行为或后果,它不是返回值 [05:19]。它是函数与“世界”互动的方式。
-
视频中的核心例子:
print()[04:47]
应用此模型:
当我们理解了这个模型,print(print(1), print(2)) 的谜题就迎刃而解了。我们看到的输出 1、2 和 None None 都是 print 调用的副作用。而这些调用所产生的 None 返回值,则作为参数被传递给了上一层调用,最终驱动了 None None 这个副作用的产生。
这个心智模型(纯 vs. 非纯)是区分“一个函数做了什么(副作用)”和“一个函数是什么(返回值)”的关键
Multiple Environment
1. Overview (总览)
本视频的核心论题是:Python 程序在执行时,不同的表达式(Expressions)会在不同的“环境”(Environments)中被求值。这意味着在同一个程序的执行过程中,会同时存在多个环境。视频通过“环境图”(Environment Diagram)这一工具,详细拆解了用户定义函数(User-defined functions)的定义、调用、嵌套调用过程,以及“帧”(Frame)的创建和“名称查找”(Name Lookup)规则,最终得出的结论是:一个“环境”就是一系列“帧”的序列,而任何一个“名称”(Name,例如变量名或函数名)的真正含义,都必须在特定的环境中才能被确定。
2. 按照主题来梳理
主题一:Python 如何定义和调用函数
在 Python 中,理解“环境”的第一步是理解两个最基本的操作:定义一个函数和调用一个函数。这两步分别对应 def 语句(def statement)和调用表达式(call expression)。
a. def 语句:创建函数
def 语句(def statement)是 Python 中用来创建函数的语法。视频中以一个简单的平方函数为例 [00:37]:
1 | def square(x): |
我们可以将这个语句解构成几个关键部分:
-
def关键字:表明这是一个函数定义。 -
名称 (Name):即函数名,这里是
square。 -
形式参数 (Formal Parameter):括号内的名称,这里是
x。这是函数在定义时给它未来会接收到的参数所起的名字。如果有多个参数,会用逗号隔开 [00:51]。 -
函数体 (Body):在首行冒号后、所有缩进的部分 [01:06]。这里是
return x * x。 -
返回语句 (Return Statement):函数体中的
return语句。 -
返回表达式 (Return Expression):
return语句后面的表达式,这里是x * x。
当 Python 执行这个 def 语句时 [01:24],它会在内存中创建一个新的“函数对象”(function object)。然后,它会将 square 这个“名称”与这个新创建的函数对象“绑定”(bind)在一起。这个绑定关系被存储在“当前帧”(current frame)中。
至关重要的一点是:在 def 语句被执行时,函数体(return x * x)并不会被执行 [01:43]。Python 只是创建了这个函数并给它命了名,但此时没有任何乘法计算发生。
b. 调用表达式:执行函数
要真正执行函数的逻辑,我们必须使用“调用表达式”(call expression)[01:43]。例如,在定义了上述函数后,我们可能会在程序的稍后部分写下这样的代码 [01:52]:
1 | square(2 + 2) |
这是一个调用表达式。它也有两个关键部分:
-
操作符 (Operator):括号左边的部分,这里是
square。 -
操作数 (Operand):括号内的部分,这里是
2 + 2。
当 Python 遇到这个调用表达式时,它的求值过程是 [02:16]:
-
求值操作符:Python 查找
square这个名称。在当前环境中,它发现square绑定到了我们刚刚定义的那个平方函数对象。 -
求值操作数:Python 计算
2 + 2这个表达式,得到值4。 -
应用 (Apply) 函数:Python 将第 1 步中得到的函数(平方函数)“应用”到第 2 步中得到的值(
4)上。这个值4就被称为函数的“实参”(Argument)。
c. 应用(调用)用户定义函数的过程
“应用函数”是整个流程中最核心的部分,它涉及到“环境”的切换 [02:35]。当一个用户定义的函数被调用时,会发生以下三个步骤 [02:49]:
-
创建 F1:一个新的帧 (Frame):Python 会立刻创建一个全新的“帧”。
-
绑定参数:在这个新创建的帧中,Python 会将函数定义中的“形式参数”(
x)与调用时传入的“实参值”(4)进行绑定。于是在这个新帧中,名称x现在的值是4。 -
在新环境中执行函数体:Python 接着进入函数的函数体(
return x * x),并在这个新的环境(即这个新创建的帧)中执行它。-
当它遇到表达式
x * x时,它会查找x的含义。 -
它首先在当前的新帧(F1)中查找,立刻就找到了
x,其值为4。 -
因此,表达式变为
4 * 4,计算结果为16。 -
return语句将这个结果16作为整个调用表达式square(2 + 2)的最终值返回。
-
主题二:嵌套调用与“多帧环境”的诞生
情况在函数调用嵌套时变得更有趣,这也真正揭示了为什么需要“多重环境”。视频中的第二个例子是 [03:20]:
1 | # 假设我们已经执行了 |
这个表达式 square(square(3)) 是一个单一的、嵌套的调用表达式。让我们来分解它的执行过程 [03:45]:
步骤一:处理“外部调用” square(...)
- 操作符 (Operator):
square(即平方函数)。 - 操作数 (Operand):
square(3)(这是另一个调用表达式)。
根据规则,Python 必须先“求值操作数”,才能执行“外部调用”。因此,Python 必须暂停对外部调用的处理,转而先完整地计算出 square(3) 的值。
步骤二:处理“内部调用” square(3)
这是一个独立的函数调用。Python 会严格执行我们之前提到的“应用函数三步骤” [04:06]:
- 创建 F1:第一个本地帧:Python 创建一个新帧,我们称之为
F1[05:19]。 - 绑定参数:在这个
F1帧中,将形式参数x绑定到实参值3。(此时F1: x -> 3)。 - 在 F1 环境中执行函数体:Python 执行
return mul(x, x)。- 在这个 F1 环境中,
x被查找到为3。 mul在F1中未找到,Python 会“向外”查找到“全局帧”(Global Frame),在那里找到mul绑定的乘法函数。- 计算
mul(3, 3),得到9。 return 9。
- 在这个 F1 环境中,
现在,内部调用 square(3) 已经执行完毕,它返回了值 9。
步骤三:返回“外部调用” square(9)
Python 回到在步骤一中暂停的地方。此时,原始的操作数 square(3) 已经被其返回值 9 所替代。整个表达式现在等价于 square(9) [04:46]。
这是一个新的函数调用。Python 会再一次严格执行“应用函数三步骤” [04:56]:
- 创建 F2:第二个本地帧:Python 又创建了一个全新的帧,我们称之为
F2。 - 绑定参数:在这个
F2帧中,将形式参数x绑定到新的实参值9。(此时F2: x -> 9)。 - 在 F2 环境中执行函数体:Python 再次执行
return mul(x, x)。- 在这个 F2 环境中,
x被查找到为9。 mul在F2中未找到,向外查找到“全局帧”,找到乘法函数。- 计算
mul(9, 9),得到81。 return 81。
- 在这个 F2 环境中,
结论
这个 square(square(3)) 的执行过程清晰地表明 [05:11]:
- 我们只定义了一个
square函数。 - 但我们通过两次调用它,创建了两个完全不同的本地帧(
F1和F2)。 - 这两个帧是不同的,因为它们绑定了不同的实参值(
x在F1中是3,在F2中是9),这也导致了它们产生了不同的返回值(9和81)。
3. 框架 & 心智模型 (Framework & Mindset)
框架一:什么是“环境” (Environment)?
视频的核心是建立了“环境”的心智模型。一个“环境”(Environment)被严格定义为一个“帧”的序列 (a sequence of frames) [05:32]。
- 帧 (Frame):一个“帧”是存储“名称”到“值”的绑定关系(Bindings)的地方。例如,
F1帧中存储了x -> 3的绑定。 - 全局帧 (Global Frame):在程序开始执行时,就存在一个“全局帧”。我们定义的
square函数和导入的mul函数,它们的名称绑定就存储在这里。在没有定义任何函数之前,整个程序的环境只有全局帧 [05:42]。 - 本地帧 (Local Frame):每次调用用户定义函数时,都会创建一个新的“本地帧”(如
F1,F2)。 - 环境 = 帧的序列:当一个函数被调用时,就创建了一个新的“多帧环境”(multi-frame environment)[05:50]。
- 例如,在执行
square(3)的函数体时(在F1帧中),“当前环境”是:序列[F1, Global Frame]。 - 在执行
square(9)的函数体时(在F2帧中),“当前环境”是:序列[F2, Global Frame]。
- 例如,在执行
在 square(square(3)) 的例子中,整个执行图里实际存在三个不同的环境 [05:57]:
- 环境一:
[Global Frame](在执行顶层代码时) - 环境二:
[F1, Global Frame](在执行square(3)的函数体时) - 环境三:
[F2, Global Frame](在执行square(9)的函数体时)
这个模型告诉我们,一个环境总是从“当前帧”开始,然后通过“父级”(parent)链接到下一个帧,直到“全局帧”为止(全局帧没有父级)[06:23]。
框架二:名称查找 (Name Lookup) 的黄金法则
这个“环境”模型最重要的作用,是提供了一个清晰的“名称查找”规则。
心智模型:名称本身(如 x 或 square)没有任何意义 [06:44]。是“环境”赋予了它们意义(即它们绑定的值)。每一个表达式都是在特定环境的上下文中被求值的。
黄金法则 [07:02]:当查找一个名称的值时,Python 会在“当前环境”中,按照“帧”的序列顺序,查找第一个(最早的)包含该名称绑定的帧。
我们可以在 square(9) 的执行过程中(即在 [F2, Global Frame] 环境中执行 mul(x, x))来验证这条法则 [07:19]:
-
查找名称
x:- Python 查看当前环境的第一帧:
F2。 F2中是否包含x?是,x绑定到9。- 查找停止。
x的值是9。
- Python 查看当前环境的第一帧:
-
查找名称
mul:- Python 查看当前环境的第一帧:
F2。 F2中是否包含mul?否。- Python 移动到环境的下一帧:
Global Frame。 Global Frame中是否包含mul?是,它绑定到乘法函数。- 查找停止。
mul的值是乘法函数。
- Python 查看当前环境的第一帧:
这个规则完美地解释了为什么函数体内部可以访问到“本地变量”(如 x)和“全局变量”(如 mul)。
心智模型三:名称的相对性 (Relativity of Names)
视频的最后一个例子 [08:15],是这个框架的终极考验,它建立了一个关于“名称相对性”的强大心智模型:同一个名称,在不同的环境中,可以代表完全不同的事物。
考虑以下这段(不推荐、但完全合法)的代码:
1 | def square(square): |
这里,square 这个词被同时用作“函数名”和“形式参数名”。直觉上这似乎会产生混淆,但环境模型清晰地解释了为什么它能准确地返回 16 [08:49]。
1. 调用表达式的环境:全局帧
square(4) 这一行代码本身(调用表达式)是在“全局环境”([Global Frame])中被求值的 [09:13]。
- 当 Python 评估这个调用的“操作符”(即括号前的
square)时,它在[Global Frame]中查找square。 - 它找到了:
square绑定到我们刚刚定义的那个函数对象。 - 因此,Python 准备调用这个函数,并传入实参
4。
2. 函数体的环境:本地帧
根据函数调用规则,Python 立即执行三步骤:
- 创建 F1:创建一个新的本地帧
F1。 - 绑定参数:将形式参数(在
def语句中定义)绑定到实参值(在call语句中提供)。- 形式参数是
square。 - 实参值是
4。 - 因此,在
F1帧中,square绑定到4。(F1: square -> 4)
- 形式参数是
- 在新环境中执行函数体:Python 开始在新的环境
[F1, Global Frame]中执行函数体return mul(square, square)[09:30]。
3. 函数体内的名称查找
现在 Python 需要评估 mul(square, square)。它必须查找 mul 和 square 这两个名称:
- 查找
mul:- 在
F1中查找mul?否。 - 在
Global Frame中查找mul?是(乘法函数)。
- 在
- 查找
square:- Python 在当前环境的第一帧(即
F1)中查找square[09:51]。 F1中是否包含square?是,它绑定到数值4。- 查找立即停止。
- Python 在当前环境的第一帧(即
Python 永远不会“继续”查找到全局帧中的 square(那个函数对象),因为它已经在本地帧 F1 中找到了它所需要的 square(数字 4)[09:58]。
因此,函数体 mul(square, square) 被评估为 mul(4, 4),最终返回 16。
这个例子最终证明了:square(4) 中的 square 和 mul(square, square) 中的 square,虽然拼写相同,但是因为它们在不同的环境中被求值,所以它们代表了两个完全不同的东西(一个是函数,一个是数字)。
Miscellaneous Python Features
Overview
本视频的核心内容是介绍 Python 编程中一系列实用但零散的功能特性(Miscellaneous Python Features)。讲演者(John DeNero)通过实时的编码演示,逐一讲解了运算符的本质、两种除法(真除法与地板除法)及模运算、函数的多重返回值、在源文件中编写代码的标准流程、如何使用文档字符串(Docstrings)和文档测试(DocTests)来规范和验证代码,以及如何为函数设置默认参数值。视频的结论是,掌握这些功能将对学习者开发项目和完成作业大有裨益。
按照主题来梳理
主题一:理解 Python 运算符的本质
在 Python 中,我们从第一天起就在使用各种运算符(Operators),例如加法 + 和乘法 *。但视频指出,我们并未深入探讨它们的工作原理。
一个核心的观点是,这些中缀运算符(infix operators,即放置在操作数之间的符号)实际上是一种“速记”(shorthand),它们本质上是调用了 Python 内置函数的等价形式。
- 运算符与函数的对应:
为了说明这一点,视频引入了 operator 模块。2 + 3的操作,等同于add(2, 3)。3 * 4的操作,等同于mul(3, 4)。- 你必须先从
operator模块中导入这些函数(例如from operator import add, mul)才能使用它们。
- 运算优先级(Precedence):
当我们在一个表达式中混合使用运算符时,Python 会遵循标准的运算优先级规则。例如,乘法(*)的优先级高于加法(+)。- 示例:
2 + 3 * 4 + 5 - Python 会先计算
3 * 4得到12。 - 然后表达式变为
2 + 12 + 5,最终结果是19。
- 示例:
- 用函数调用模拟优先级:
如果我们想用 operator 模块中的函数来重写这个带优先级的表达式,我们必须自己来处理这个运算顺序。add(add(2, mul(3, 4)), 5)- 我们必须明确地将
mul(3, 4)作为内层调用,将其结果(12)再传递给add函数与2相加,最后将该结果(14)再与5相加。 - 这清晰地表明,中缀运算符
+和*的优先级规则,是在语言层面为我们处理了函数调用的嵌套逻辑。
- 使用括号覆盖优先级:
与数学运算一样,我们可以使用括号 () 来覆盖(override)默认的运算优先级。- 示例:
(2 + 3) * (4 + 5) - 在这个表达式中,Python 会先计算括号内的加法。
2 + 3得到5。4 + 5得到9。- 最后计算
5 * 9,得到45。 - 函数调用版本: 如果用
operator函数表达,它将是mul(add(2, 3), add(4, 5))。这再次证明了括号和函数调用在组织运算顺序上的等价性。
- 示例:
总之,将运算符理解为内置函数调用的“速记”或“语法糖”(Syntactic Sugar,视频中未明确使用此术语,但含义相近),是理解 Python 运算逻辑的一个重要心智模型。
主题二:Python 中的两种除法与模运算
在 Python 中,除法运算被明确区分为两种类型,并且提供了一个补充的模(mod)运算符来获取余数。这对于需要精确控制数值(尤其是整数)计算的场景至关重要。
-
真除法 (True Division):
/- 这是我们在数学中通常理解的除法,它总是返回一个浮点数(float)结果,即使两个操作数都是整数且可以整除。
- 示例:
2013 / 10 - 结果是
201.3。 - 在
operator模块中,它对应的函数是truediv(例如truediv(2013, 10))。
-
地板除法 (Floor Division) / 整数除法 (Integer Division):
//- 地板除法只计算除数(divisor)能完整地“装入”被除数(dividend)多少次,结果会“向下取整”(floor)到最接近的整数。它不关心任何小数部分的余数。
- 示例:
2013 // 10 - 结果是
201。它丢掉了.3的部分。 - 在
operator模块中,它对应的函数是floordiv(例如floordiv(2013, 10))。
-
模运算 (Mod Operator):
%- 模运算(或称“取余”)用于获取除法运算中的“余数”(remainder)。
- 示例:
2013 % 10 - 结果是
3。 - 在
operator模块中,它对应的函数是mod(例如mod(2013, 10))。
-
为什么需要地板除法和模运算?
视频强调,// 和 % 运算符的主要优势在于它们的结果是精确的(exact)。- 当你使用真除法
/时,你经常得到的是一个近似值(approximation)。例如,5 / 3的结果是1.66666...7,这是一个有限精度的浮点数,它只是无限循环小数1.666...的一个近似。 - 相比之下,使用地板除法和模运算来处理
5和3,我们可以得到两个精确的整数:5 // 3结果是1(3 在 5 中只完整出现了 1 次)。5 % 3结果是2(余数是 2)。
- 这种精确性在很多算法和数据处理中(例如分解数字的各个位数)非常有用。
- 当你使用真除法
地板除法 // 和模运算 % 共同构成了一对完整的整数除法运算,它们一个提供商(quotient),一个提供余数(remainder),二者结合可以完整地描述两个整数相除的结果。
主题三:函数的多重返回值
Python 函数允许通过一种非常简洁的语法返回多个值,并在函数调用时用多个变量来接收它们。
-
背景: 在许多其他编程语言中,函数通常只能返回一个值。如果需要返回多个信息,往往需要将它们打包成一个数组、列表或自定义对象。
-
Python 的实现方式:
Python 允许你在 return 语句后简单地用逗号 , 分隔多个值。 -
示例:同时返回商和余数
视频演示了一个名为 divide_exact 的函数,它接受两个参数 n(被除数)和 d(除数),并希望同时返回 n 除以 d 的商和余数。
- 定义函数:
1
2
3# 假设已经 from operator import floordiv, mod
def divide_exact(n, d):
return floordiv(n, d), mod(n, d) - 调用与赋值:
当调用这个函数时,你可以使用一个同样以逗号分隔的变量列表来“解包”(unpack)返回的多个值。1
quotient, remainder = divide_exact(2013, 10)
- 结果:
floordiv(2013, 10)的结果201被赋值给了变量quotient。mod(2013, 10)的结果3被赋值给了变量remainder。
- 定义函数:
-
与多重赋值的联系:
视频提到,这种多重返回值的语法,与 Python 中已有的多重赋值(multiple assignment)语法是一致的。
- 多重赋值:
x, y = 1, 2 - 多重返回与接收:
q, r = divide_exact(n, d) - 从技术上讲,函数返回的是一个“元组”(Tuple),而多重赋值操作是将这个元组“解包”到对应的变量中。但从使用者的角度看,它提供了一种直接返回和接收多个独立值的便捷途径。
- 多重赋值:
这个功能在项目中非常有用,例如当一个函数需要同时返回一个计算结果以及一个表示计算状态(如是否成功或错误信息)的标志时。
主题四:Python 源文件、文档字符串与文档测试
视频的后半部分详细演示了从使用交互式解释器(Interactive Interpreter)到在源文件中编写可执行、可测试代码的完整流程。
-
从交互式到源文件:
-
到目前为止,视频中的演示都是在 Python 交互式解释器中完成的(即输入一行代码,立即看到结果)。这种方式适合“玩耍”(play with ideas)。
-
但如果想编写永久性的代码,你需要将代码保存在一个文件中,通常是以
.py结尾的 Python 源文件(source file)。 -
视频中展示了使用
Vim编辑器(并提到了实验课 Lab 1 中有更多编辑器选项)创建了一个ex.py文件。
-
-
在文件中定义函数:
讲演者在 ex.py 文件中编写了 divide_exact 函数的定义,包括 from operator import … 和 def divide_exact…。
1
2
3
4
5# ex.py
from operator import floordiv, mod
def divide_exact(n, d):
return floordiv(n, d), mod(n, d) -
执行源文件:
在命令行(shell)中,使用 python3 ex.py 来执行这个文件。
-
第一次执行(无输出): 执行后,控制台什么也没发生。
-
原因:
def语句只是定义了一个函数,并将divide_exact这个名字绑定(bind)到了新创建的函数对象上。它并没有调用(call)这个函数,也没有执行任何打印操作。 -
添加调用与打印: 讲演者接着在
ex.py中添加了调用代码:1
2
3
4
5# ... (函数定义) ...
q, r = divide_exact(2013, 10)
print("the quotient is", q)
print("the remainder is", r) -
第二次执行(有输出): 再次运行
python3 ex.py,此时控制台成功打印出了 “the quotient is 201” 和 “the remainder is 3”。
-
-
交互式加载文件 ( -i 标志):
视频指出,一种更常见的做法是使用 -i (interactive) 标志来运行文件:python3 -i ex.py。-
这会先执行
ex.py文件中的所有代码(包括函数定义和变量赋值)。 -
执行完毕后,它不会退出,而是进入交互式模式。
-
此时,你可以在解释器中访问文件中定义的所有内容,例如查看
q和r的值,或者再次调用divide_exact函数。
-
-
文档字符串 (Docstrings):
为了让其他人(以及未来的自己)理解函数的功能,我们需要添加文档。
-
在
def语句的下一行,使用三引号("""或''')括起来的字符串,就是该函数的“文档字符串”(Docstring)。 -
示例:
1
2
3
4
5
6def divide_exact(n, d):
"""Return the quotient and remainder of dividing N by D.
(更多描述...)
"""
return floordiv(n, d), mod(n, d) -
视频提到,一个约定俗成的规范(convention)是使用全大写字母(如
N和D)来指代函数签名中的形式参数(formal parameters)。
-
-
文档测试 (DocTests):
Docstrings 不仅可以包含人类可读的描述,还可以包含机器可执行的示例。
-
这些示例被写成模拟的 Python 交互式会话(
>>>提示符)。 -
示例(在 Docstring 内部):
1
2
3
4
5
6
7
8
9"""...
Here's what would happen if you called divide_exact:
q, r = divide_exact(2013, 10)
q
201
r
3
""" -
这不仅是文档,更是一个“测试用例”。它声明:“如果我运行
q, r = divide_exact(2013, 10),我期望q的值是201,r的值是3。”
-
-
运行 DocTests:
doctest 是 Python 的一个标准模块,可以用来运行这些文档测试。
-
命令:
python3 -m doctest ex.py -
-m标志告诉 Python 运行一个模块(这里是doctest模块)作为脚本,并将ex.py作为参数传递给它。 -
成功(无输出): 如果所有测试都按预期通过,该命令不会产生任何输出。
-
详细输出 (
-v): 使用python3 -m doctest -v ex.py(-v代表 verbose,详细模式),它会打印出它尝试的每一个测试用例及其结果。 -
失败: 视频中演示了如果故意改错期望值(例如,期望
r为2,但实际得到3),doctest会报告一个详细的失败信息(Failed example: r,Expected: 2,Got: 3),帮助你定位错误。
-
主题五:函数的默认参数值
最后,视频介绍了一个小而有用的功能:为函数的参数指定默认值(Default Values)。
-
语法: 在
def语句定义函数签名时,使用=为一个或多个参数提供默认值。1
2def divide_exact(n, d=10):
# (函数体...)- 注意: 视频强调,
d=10不是一个赋值语句(assignment statement)。它只是一个占位符(placeholder),用于声明d的默认值。
- 注意: 视频强调,
-
工作机制:
-
d=10的意思是:如果在调用divide_exact函数时,没有为参数d传递第二个参数,那么 Python 将自动把10绑定到d。 -
如果调用者提供了第二个参数,那么该参数会覆盖默认值。
-
-
示例:
-
加载文件:
python3 -i ex.py -
调用(提供两个参数):
divide_exact(2013, 10)或divide_exact(2013, 5)。这与以前的行为完全相同。 -
调用(使用默认值):
q, r = divide_exact(2013)
-
在此调用中,只提供了
n的值 (2013)。 -
由于
d没有被提供,Python 使用了默认值10。 -
因此,
q得到了201,r得到了3,就好像调用者明确写了divide_exact(2013, 10)一样。
-
这个功能在定义那些具有“可选”配置或常用值的函数时非常方便。
框架 & 心智模型 (Framework & Mindset)
Framework: Python 代码的规范化编写与测试工作流
从视频的演示中,我们可以抽象出一个在 Python 中编写可靠代码的标准化工作流程(Workflow)。这个流程从简单的“能运行”的代码,演进到“可维护、可验证”的健壮代码。
-
构思与探索 (Idea & Exploration) - 交互式解释器
-
固化代码 (Solidifying Code) - 编写
.py源文件-
当你需要编写永久性的、可重复执行的逻辑时(例如一个
divide_exact函数),你需要将其写入一个.py源文件中(如ex.py)([04:52])。 -
在这个阶段,你的主要目标是将逻辑(如函数定义
def ...)从临时的交互式会话中迁移出来。 -
此时的代码是“功能性”的,但还不是“规范”的。
-
-
验证执行 (Verification) - 运行文件与
-i模式 -
文档化 (Documentation) - 编写 Docstrings
-
自动化测试 (Automated Testing) - 嵌入 Doctests
-
验证与回归 (Verification & Regression) - 运行
doctest模块-
最后一步是让 Python 自动执行你的“文档测试”。
-
通过运行
python3 -m doctest ex.py,doctest模块会解析你文件中所有 Docstrings 里的>>>示例,像真人一样执行它们,然后比对实际输出和你在文档中写的期望输出([09:04])。 -
如果无输出:恭喜,你的代码实现与你的文档(期望)完全一致。
-
如果输出
Failed example:说明你的代码([09:55] 中得到的3)和你的文档([09:41] 中期望的2)不匹配。这表明你的代码或你的文档(测试用例)中有一个是错的。 -
这提供了一个强大的“回归测试”框架:每当你修改了
divide_exact函数的实现时,你都可以重新运行doctest,以确保你的修改没有破坏(break)之前写下的任何一个示例(期望)。
-
这个从“交互”到“文件”再到“文档测试”的流程,是 Python 开发中一个重要且实用的微型框架,它强制你思考代码的预期行为,并将其固化为可自动验证的文档。
Mindset: 运算符是函数调用的“语法糖”
视频开篇([00:21])就提出了一个重要的心智模型:将 Python 中的运算符(如 +, *, /, //, %)视为底层函数调用的“速记”或“语法糖”(Shorthand)。
-
表象:中缀运算符 (Infix Operators)
-
我们习惯的写法是
2 + 3或3 * 4。 -
这种写法的优点是符合人类的数学直觉,非常简洁易读。
-
当表达式变复杂时,如
2 + 3 * 4 + 5,Python 语言会“在幕后”为我们应用一套复杂的“优先级规则”(Precedence Rules)([00:55]),自动决定先算乘法再算加法。
-
-
本质:函数调用 (Function Calls)
-
视频揭示了这种“速记”的等价实现:
from operator import add, mul([00:44])。 -
2 + 3等价于add(2, 3)。 -
3 * 4等价于mul(3, 4)。 -
这种写法(
add(2, 3))被称为“前缀表示法”(Prefix Notation),即运算符(函数名)在操作数(参数)之前。
-
-
建立该心智模型的意义
-
消除魔法,理解优先级:
当我们使用函数调用来重写复杂表达式
2 + 3 * 4 + 5时,我们被迫自己处理优先级。我们必须将其写成add(add(2, mul(3, 4)), 5)。这让我们清楚地看到,
3 * 4必须先被计算(它是最内层的mul调用),然后才能被外层的add调用使用。这证明了“运算符优先级”并不是什么魔法,它只是中缀表示法(+, *)如何翻译为函数调用嵌套(add, mul)的一套规则。
-
理解括号的真正作用:
当我们使用括号
(2 + 3) * (4 + 5)时,我们实际上是在改变这种嵌套结构。其等价的函数调用是
mul(add(2, 3), add(4, 5))。我们强制 add 运算(2+3 和 4+5)先发生(它们是 mul 的参数,必须先被求值),然后 mul 才对它们的结果进行运算。
因此,括号和中缀运算符的关系,等同于函数调用中“参数”和“外层函数”的关系。
-
统一语言设计:
这个心智模型帮助我们将 Python 的设计看得更统一。当视频讲到除法时,它立刻展示了
/、//和%对应的函数:truediv、floordiv和mod。这表明 Python 的设计者在设计语言时,是先有了这些函数的概念(如 floordiv),然后再为其提供一个便捷的“速记”符号(//)。
-
应用(超越本视频):
虽然视频没有深入,但这个心智模型是理解 Python 更高级特性的基础。例如,在面向对象编程中,当你对自定义对象(如“向量”或“矩阵”)使用 + 运算符时,Python 内部调用的就是你在这个对象上定义的特殊方法 add。add 就像是 operator.add 的一个可自定义版本。
-
综上所述,将运算符视为函数调用的“速记”,能帮助我们穿透语法的表象,更深刻地理解代码的实际执行顺序(求值顺序),并欣赏 Python 语言设计的内在一致性。
Conditional statements
Overview
本视频的核心论题是 Python 中的“条件语句” (Conditional statements)。它首先通过定义“语句” (statement) 的基本概念,特别是“复合语句” (compound statement) 的结构(包含“头部” header 和“套件” suite),为理解条件语句奠定了基础。视频的核心内容详细介绍并演示了条件语句的语法、执行规则和实际应用(以编写一个计算绝对值的函数为例)。其最终结论是:条件语句通过 if、elif 和 else 关键字,允许程序按顺序评估一系列表达式。一旦找到第一个为“真值” (True Value) 的表达式,程序将执行其对应的“套件” (suite),并且(这一点至关重要)跳过所有剩余的子句,从而实现在不同条件下仅执行一个代码路径的控制流。
按照主题来梳理
主题一:先导概念:理解“语句” (Statements)
在深入探讨条件语句之前,视频首先厘清了几个关键的编程基础概念,这些概念是理解 if 语句如何工作的前提。
-
什么是“语句” (Statement)?
-
在编程中,“语句”是由解释器(Interpreter)执行以执行某个动作的基本单元 [00:14]。
-
这些动作的例子包括:
-
将一个名称(变量)绑定到一个值(例如,赋值语句
x = 10)。 -
定义一个新函数(例如,使用
def语句)。
-
-
-
什么是“复合语句” (Compound Statement)?
-
语句并不总是单行。复合语句是一种可以跨越多行的结构化语句 [00:22]。
-
它具有特定的层次结构,通常由一个“头部” (header) 和一个缩进的“套件” (suite) 组成。
-
一个复合语句中可能包含多个部分,例如一个
if...elif...else...结构,它整体上是一个复合语句。
-
-
复合语句的结构
-
视频将复合语句的结构拆解为以下几个部分,这对于理解
if语句的语法至关重要: -
头部 (Header):
-
子句 (Clause):
-
一个“子句”由一个头部及其后面跟随的一个缩进的语句块(套件)组成 [00:50]。
-
一个复合语句(如
if-elif-else)是由多个子句构成的。
-
-
套件 (Suite):
-
-
如何“执行一个套件” (Execute a Suite)?
主题二:条件语句 (if) 详解:语法与执行
在铺垫了“语句”和“套件”的概念后,视频转向了核心主题:条件语句。这是一种允许程序根据不同条件执行不同代码块的复合语句。
-
实例演示:计算绝对值
-
为了展示条件语句的用法,视频现场编写了一个名为
absolute_value(x)的函数,用于返回x的绝对值 [02:05]。 -
其逻辑是检查
x是小于零、等于零还是大于零。 -
下面是视频中演示的 Python 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# 定义一个计算绝对值的函数
def absolute_value(x):
# 返回 x 的绝对值
if x < 0:
# 如果 x 小于 0,其绝对值为 -x
return -x
elif x == 0:
# "elif" 是 "else if" (否则如果) 的缩写
# 如果 x 等于 0,其绝对值为 0
return 0
else:
# 视频中的表述是 "finally if neither of those are true" (最后,如果前两个都不成立) [00:02:58]
# 这在 Python 中对应一个 "else" 块
# 如果 x 既不小于0也不等于0,那它一定大于0,其绝对值为 x
return x -
-
条件语句的结构分析
-
视频指出,上述
absolute_value函数体(def语句的“套件”)中,只包含了一个语句,即一个条件语句 [03:33]。 -
但这一个复合语句,内部由三个“子句” (clauses) 构成:
-
if子句 (头部:if x < 0:, 套件:return -x) -
elif子句 (头部:elif x == 0:, 套件:return 0) -
else子句 (头部:else:, 套件:return x)
-
-
-
条件语句的执行规则(核心)
-
视频给出了条件语句执行的明确规则 [03:59],这是理解其工作方式的关键:
-
按顺序评估 (Consider each clause in order): 解释器从第一个子句(
if)开始,按顺序向下检查每个子句。 -
评估头部表达式 (Evaluate the header’s expression): 计算头部中的表达式(例如
x < 0)。 -
判断真假 (If it is a True Value):
-
如果表达式的值是一个“真值” (True Value)(详见主题三),则执行该子句的“套件” (suite)。
-
关键一步: 在执行完该套件后,跳过所有剩余的子句 (skip the remaining clauses) [04:11]。
-
-
如果表达式的值是一个“假值” (False Value),则跳过当前子句的套件,移动到 下一个 子句,并重复步骤 2。
-
-
这个规则最重要的推论是:在一个条件语句块中,最多只有一个套件会被执行 [04:19]。
-
-
条件语句的语法
主题三:布尔上下文 (Boolean Contexts) 与“真值” (Truthiness)
在主题二中,我们知道条件语句的执行取决于表达式是“真值”还是“假值”。主题三深入探讨了 Python 是如何做出这个判断的。
-
乔治·布尔 (George Boole)
- 视频引入了乔治·布尔 [04:52],他是一位逻辑学家,也是计算机科学的早期奠基人之一。这个名字用于引出“布尔” (Boolean) 逻辑的概念,即“真”与“假”。
-
什么是“布尔上下文” (Boolean Contexts)?
-
Python 中的“真值”与“假值”
-
乔治·布尔(作为比喻)“监视”着这些布尔上下文 [06:02]。为了判断如何执行,Python 定义了哪些值被视为“假”,哪些被视为“真”。
-
假值 (False Values): [05:44]
-
False(布尔值False本身) -
0(数字零) -
""(空字符串) -
None(Python 中的空值对象) -
视频提到 “some more to come” (未来还会学到更多),暗示这个列表并不完整(例如,空的列表
[]或字典{}也是假值),但这是很好的起点。
-
-
真值 (True Values): [06:02]
-
规则非常简单:任何不被视为“假值”的值,都是“真值”。
-
这意味着
True、数字1、-1、非空字符串(如"hello")等,在布尔上下文中都会被评估为“真”。
-
-
-
总结
-
当 Python 遇到
if x < 0:这样的语句时,它会计算x < 0的结果(例如,如果x是-2,结果是True;如果x是3,结果是False)。 -
然后,它在“布尔上下文”中检查这个结果(
True或False)。True是一个“真值”,而False是一个“假值”,从而决定是执行套件还是跳到下一个子句。
-
框架 & 心智模型 (Framework & Mindset)
从这个视频中,我们可以抽象出两个关键的编程心智模型:一个是关于代码的 结构 ,另一个是关于代码的 执行流 。
1. 复合语句的结构心智模型 (The Structure of Compound Statements)
这个心智模型帮助我们将代码视为具有层次结构的积木,而不是一长串扁平的指令。
-
核心思想: 代码块(如函数或条件逻辑)是“复合语句”,它们由“子句” (Clauses) 构成,而每个“子句”又由一个“头部” (Header) 和一个“套件” (Suite) 组成。
-
组件解析:
-
头部 (Header):
-
这是“控制器”,以关键字(
def,if,elif,else)开头,以冒号 (:) 结尾。 -
它“声明”了接下来要发生的事情的类型(例如
def声明了一个函数,if声明了一个条件)。 -
头部“拥有”并“控制”着它下方的缩进代码块 [01:07]。
-
-
套件 (Suite):
-
这是“执行体”,是 从属于 头部的一组缩进语句 [00:50]。
-
同一个套件中的所有语句必须保持相同的缩进级别。
-
这个模型强调了缩进在 Python 中的重要性——缩进 定义 了套件的范围。
-
-
子句 (Clause):
- 这是一个“头部-套件”对。一个子句是一个独立的逻辑单元(例如,“如果
x < 0,则return -x”)。
- 这是一个“头部-套件”对。一个子句是一个独立的逻辑单元(例如,“如果
-
复合语句 (Compound Statement):
-
-
为何这个模型重要?
-
它澄清了一个常见的误解。
if、elif和else不是三个独立的语句。它们是* 一个单一的复合语句 *的三个不同部分(三个子句)。 -
理解了这一点,就能明白为什么当
if满足时,elif和else会被 整个 跳过——因为它们同属于一个语句,而这个语句的执行规则(见下一个框架)是“只执行一个套件”。
-
2. 条件执行的“瀑布流”框架 (The “Waterfall” Framework for Conditional Execution)
这个心智模型帮助我们可视化程序在遇到条件语句时的执行路径。
-
核心思想: 条件语句的执行就像一个“瀑布”或一系列“关卡”。代码流从上到下,按顺序尝试通过每个关卡(子句)。一旦成功通过一个关卡(条件为真),它就会顺着该关卡的特定路径(套件)流下,并 完全绕过 所有后续的关卡。
-
执行步骤:
-
从顶部开始 (Start at the Top): 执行流到达
if子句。 -
检查布尔上下文 (Check Boolean Context): [05:12] 评估
if头部的表达式。 -
决策点:真 (Decision Point: True): [03:59]
-
行动: 表达式为“真值”。
-
路径: 执行
if子句的“套件” (suite)。 -
结束: 立即退出整个
if-elif-else复合语句。执行流跳转到整个条件语句块之后的第一行代码 [04:11]。
-
-
决策点:假 (Decision Point: False):
-
行动: 表达式为“假值”。
-
路径: 跳过
if子句的套件。 -
继续: 移动到下一个子句。
-
-
重复检查 (Repeat Check):
-
如果下一个是
elif子句: 回到步骤 2,评估elif的头部表达式。 -
如果下一个是
else子句:else没有布尔上下文,它是一个“接住所有” (catch-all) 的子句。直接执行else的套件,然后退出整个语句。 -
如果没有下一个子句: (例如,一个只有
if的语句,或者if-elif链的末尾):什么也不做,退出整个语句。
-
-
-
为何这个模型重要?
-
它强调了条件执行的两个关键特性:
-
有序性 (Ordered): 检查的顺序(从上到下)至关重要。如果
x=0,if x <= 0:和elif x == 0:的顺序会产生不同的执行路径(尽管结果可能相同,但逻辑不同)。 -
排他性 (Exclusive): 最多只有一个 套件会被执行 [04:19]。程序不会评估所有子句;它在找到第一个“真值”时就会停止。这与“瀑布”的比喻一致——水一旦找到了一个出口流了下去,就不会再流过同一层瀑布的其他出口。
-
-
Iteration
Overview
本视频的核心论题是讲解编程中的“迭代” (iteration) 概念,并深入探讨了 Python 语言中实现迭代的一种方式:while 语句 [00:01]。视频通过一个简单的累加示例(计算 1+2+3),详细拆解了 while 语句的精确执行规则,并通过逐行追踪变量变化,展示了计算机是如何一步步执行循环的 [01:26, 02:16]。最终的结论是,while 循环的执行是一个严格遵循“检查条件 -> 执行完整代码块 -> 返回重新检查”这一固定流程的过程,理解这一规则是掌握迭代的关键。
按照主题来梳理
1. 什么是迭代 (Iteration) 与 while 语句?
迭代 (Iteration) 在编程中的基本含义是“重复”——即一遍又一遍地执行相同的操作 [00:01]。在 Python 中,while 语句(或称 while 循环)是实现这种重复的一种重要方式。
while 语句是一种复合语句 (compound statement) [00:13]。这意味着它由一个“头部” (header) 和一个“主体” (body,在视频中也称为 suite) 组成。
-
头部 (Header): 头部以
while关键字开始,后面跟着一个“表达式” (expression),这个表达式最终会被求值为一个布尔值(True或False)。 -
主体 (Suite): 主体是
while头部下方的一个缩进代码块。这个代码块中包含我们希望重复执行的一条或多条语句 [00:13]。
while 语句的核心逻辑是:只要头部表达式的值为 True(真),它就会一遍又一遍地执行其主体代码块中的所有语句 [01:42]。当头部表达式的值变为 False(假)时,循环才会停止,程序将继续执行 while 语句之后的代码 [04:16]。这种机制允许我们编写代码来处理那些需要重复多次直到满足某个特定条件为止的任务。
2. 代码演示:累加 1 到 3
为了具体展示 while 循环的工作原理,视频中使用了一个简单的 Python 示例,目标是计算从 1 到 3 的总和(即 1 + 2 + 3),期望得到的结果是 6 [00:37]。
实现这个目标的代码如下:
1 | i = 0 |
-
初始化变量 [00:28]:
-
首先,设置了两个变量:
i和total。 -
i = 0:变量i将作为我们的计数器或索引。 -
total = 0:变量total用于存储累加的结果,初始值为 0。
-
-
while循环结构 [00:43]:
当这段代码在 Python 解释器中执行完毕后,我们去查看变量的值,会发现 i 最终变为 3,而 total 最终变为 6 [01:11]。这个 total 为 6 的结果,正是 1 + 2 + 3 的和。要理解这个过程是如何发生的,我们必须了解 while 语句的严格执行规则。
3. while 语句的执行规则
while 语句的执行流程(Execution Rule)非常严格,它决定了计算机如何处理循环。视频中将其总结为以下几个关键步骤 [01:34]:
-
第一步:求值 (Evaluate)
- 计算机首先会评估
while语句头部 (header) 中的表达式 [01:42]。在我们的例子中,就是评估i < 3。
- 计算机首先会评估
-
第二步:检查 (Check) & 执行 (Execute)
这个规则是理解 while 循环,尤其是理解变量如何在循环中变化的(如“差一错误”,off-by-one errors)的核心。
4. 逐行追踪:while 循环如何计算出 6
视频通过一个“环境帧” (frame) 的概念,一步步展示了变量 i 和 total 是如何变化的。让我们来模拟这个过程 [02:16]:
-
初始状态 [02:25]:
-
程序开始,执行前两行赋值语句。
-
在全局帧 (global frame) 中:
i被绑定到 0,total被绑定到 0。
-
-
第一次循环 (Iteration 1)
-
[第一步:求值] [02:33]:检查
while条件i < 3。- 当前
i是 0。0 < 3的结果是True。
- 当前
-
[第二步:执行] [02:39]:条件为
True,执行循环体。 -
[返回步骤一]: 循环体执行完毕,返回顶部。
- 循环 1 结束时状态:
i = 1,total = 1
-
-
第二次循环 (Iteration 2)
-
[第一步:求值] [03:08]:再次检查
while条件i < 3。- 当前
i是 1。1 < 3的结果是True。
- 当前
-
[第二步:执行] [03:17]:条件为
True,执行循环体。 -
[返回步骤一]: 循环体执行完毕,返回顶部。
- 循环 2 结束时状态:
i = 2,total = 3
-
-
第三次循环 (Iteration 3)
-
[第一步:求值] [03:34]:再次检查
while条件i < 3。- 当前
i是 2。2 < 3的结果是True。
- 当前
-
[第二步:执行] [03:41]:条件为
True,执行循环体。 -
[返回步骤一]: 循环体执行完毕,返回顶部。
- 循环 3 结束时状态:
i = 3,total = 6
-
-
第四次检查 (Final Check)
-
最终状态 [04:35]:
- 程序执行完毕后,
i的值是 3,total的值是 6。
- 程序执行完毕后,
框架 & 心智模型 (Framework & Mindset)
while 循环的“检查-全执行-返回”心智模型
要真正理解 while 循环并避免出错,我们不能凭直觉“感觉”它在做什么,而必须建立一个严格的、基于规则的心智模型。从视频的详细追踪中 [02:16],我们可以抽象出以下这个“检查-全执行-返回” (Check-ExecuteAll-Return) 的心智框架:
1. 初始状态设定 (Initial State Setup)
-
在进入
while循环之前,你的“世界”(即程序的环境帧)必须被明确定义。 -
心智活动:明确写下所有相关变量的初始值 [02:25]。在我们的例子中,就是在大脑里或纸上写下:
i = 0,total = 0。这是你的“零点” (Ground Zero)。
2. 守卫检查点 (The Guard Checkpoint)
-
while语句的头部 (header) 是一个严格的“守卫检查点” [01:42]。这是循环 唯一 的入口。 -
心智活动:每次你的执行流“到达”
while这一行时(无论是第一次还是从循环体底部返回),你 必须 停下来。获取条件表达式所需变量的 当前值,并计算表达式的布尔结果 [02:33]。 -
分支:
-
如果为
True:你获得了“通行证”,可以进入主体代码块(步骤 3)。 -
如果为
False:你被“拒绝”通行。你必须 立即 绕过整个主体代码块,跳到while循环结束后的第一行代码 [04:24]。循环的使命至此结束。
-
3. “全有或全无”的主体执行 (The “All-or-Nothing” Suite Execution)
-
一旦你通过了检查点(条件为
True),你就进入了循环体 (suite) [02:39]。 -
心智活动:你 必须 从上到下,依次执行循环体中的 每一条 语句,直到最后一条 [03:57]。
-
关键陷阱:在执行循环体 期间,变量的值可能会发生变化,甚至可能导致
while的条件“看起来”已经变成了False(例如,在我们的例子中,i在循环体内部变成了 3)[03:50]。你 必须 忽略这一点。检查点只在“顶部”;一旦进入了循环体,就必须执行到底,中途不能退出或回头。你不能在执行完i = i + 1后立刻跳回顶部去检查i < 3,你必须继续执行total = total + i[03:57]。
4. 强制返回 (The Mandatory Return)
-
当循环体的 最后一条 语句执行完毕后 [04:05](在我们的例子中是
total = total + i),你的任务 不是 继续往下走。 -
心智活动:你的执行流必须 立即、无条件地“传送”回 步骤 2,即
while语句的头部检查点 [01:51, 03:08]。 -
这个“返回”是自动发生的。你将带着 更新后 的变量状态(例如,
i = 3,total = 6)[04:16],重新回到那个“守卫检查点”,开始新一轮的评估。
这个“检查 -> 全执行 -> 返回”的循环,是 while 语句的本质。通过在脑中严格模拟这个过程,而不是凭直觉猜测,我们才能准确预测任何 while 循环(无论多么复杂)的行为。
Example: Prime Factorization
Overview
本视频的核心论题是“如何通过编程解决一个经典的数学问题:计算任意正整数的质因数分解”。视频通过一个具体的 Python 编程实例,不仅展示了如何将一个数学概念(质因数分解)[00:08] 转化为一个清晰的、分步骤的算法(Algorithm)[01:58],还演示了如何将这个算法实现为代码。视频的最终结论是,通过“功能抽象”(Functional Abstraction)[08:55],即将复杂的逻辑拆分为多个职责单一的辅助函数,可以显著提高代码的可读性和清晰度 [11:15],即便存在其他“有效”但更难理解的实现方式(例如嵌套循环)[11:08]。
按照主题来梳理
第 1 节:理解与定义:质因数分解 (Prime Factorization)
在深入探讨编程实现之前,视频首先清晰地界定了核心的数学概念:质因数分解(Prime Factorization)[00:08]。这个概念是整个编程任务的基石。
-
核心定义:视频指出,每一个正整数
n都可以被表示为一系列质数(Prime Numbers)的乘积。这些质数就被称为n的质因数(Prime Factors)[00:17]。 -
唯一性:一个关键的数学特性是,这种分解方式是唯一的(unique)[00:24]。前提是,你需要将这些质因数按照从小到大(from least to greatest)的顺序排列 [00:30]。例如,数字 12 的质因数分解永远是 2、2、3,无论你通过什么方法计算,最终都会得到这两个 2 和一个 3。
-
特殊情况:1:视频提到了数字 1 是一个“有点滑稽的例子”(funny case)[00:30],因为它在技术上(technically)没有任何质因数。不过,视频很快将重点转移到大于 1 的整数上,因为这才是算法真正需要处理的情况 [00:35]。
为了让这个抽象的定义更具体,视频给出了一系列实例 [00:41]:
-
8的质因数分解是:2 * 2 * 2 -
9的质因数分解是:3 * 3 -
10的质因数分解是:2 * 5 -
11的质因数分解是:11(因为 11 本身就是质数) -
12的质因数分解是:2 * 2 * 3
这些例子是后续算法测试(Test Cases)的基础。视频特别提到了 10 的例子(2 和 5),并就“顺序”问题进行了补充说明 [02:40]。当例子中出现 8 = 2 * 2 * 2 这样的情况时,这些数字是相同的,所以“递增”(increasing)这个词并不完全准确。因此,视频将其修正为“非递减顺序”(non-decreasing order)[02:52],这意味着序列中的下一个数字要么大于等于前一个数字,这在数学上更为严谨。
第 2 节:算法的构建(Algorithm Construction):如何逐步找到所有质因数?
在明确了“是什么”(What)之后,视频转向了“怎么办”(How)的问题:我们该如何为任意给定的数字 n 找到它的质因数分解?[00:46]
视频介绍了一种直观的、作者(John DeNero)自称“小时候学到的”算法 [00:52]。这个过程是迭代(Iterative)和递归(Recursive in nature)的,其核心逻辑可以分解为以下步骤:
-
起始:给定一个正整数
n。 -
寻找:找到
n的“最小质因数”(smallest prime factor)。 -
分割:用
n除以这个最小质因数,得到一个新的、更小的数。 -
重复:对这个新得到的数,重复执行第 2 步和第 3 步。
-
终止:一直持续这个过程,直到这个数最终变为 1。
视频通过一个更复杂的例子 858 [01:05],生动地演示了这个算法的执行流程:
-
第 1 轮:
-
n = 858。 -
858是一个偶数,所以它的最小质因数显然是2[01:11]。 -
我们记录下
2。 -
n变为858 / 2 = 429。
-
-
第 2 轮:
-
n = 429。 -
它不是偶数,所以
2不再是它的因数。 -
我们尝试下一个质数
3。429可以被3整除 [01:18]。 -
我们记录下
3。 -
n变为429 / 3 = 143。
-
-
第 3 轮:
-
第 4 轮:
-
n = 13。 -
我们尝试寻找
13的最小质因数。但13本身就是一个质数 [01:50]。 -
我们记录下
13。 -
n变为13 / 13 = 1。
-
-
终止:
n现在等于1。算法结束。
最终,我们得到的质因数序列是:2, 3, 11, 13。这些也恰好是按非递减顺序排列的 [03:09]。
这个清晰的计算过程,视频称之为“算法”(Algorithm)[01:58]。现在,任务变成了将这个算法翻译成 Python 代码。
1 | def prime_factors(n): |
这段代码精准地复现了算法的宏观流程。while n > 1 [03:31] 完美对应了“重复直到 n 变为 1”的规则。在循环内部,代码执行了“寻找”(k = smallest_prime_factor(n))、“分割”(n = n // k)和“记录”(print(k))[04:21]。
当然,这段代码还不能运行,因为它依赖一个尚不存在的辅助函数 smallest_prime_factor [04:43]。
第 3 节:核心辅助函数:实现 smallest_prime_factor
上一步,我们构建了 prime_factors 函数的“骨架”,但它依赖于一个“黑盒”——smallest_prime_factor(n) 函数。现在,我们的任务就是实现这个黑盒 [04:43]。
这个函数的目标很明确:给定一个 n(n 总是大于 1,因为主循环的条件是 n > 1),找到能整除 n 的“最小”的数 k(这个 k 必须大于 1)。
视频指出,这个“最小的因数”(smallest factor)[04:51] 必定是“最小的质因数”(smallest prime factor)。为什么呢?因为如果这个最小因数 k 是一个合数(比如 6),那么 k 必然有一个更小的质因数(比如 2 或 3),而这个更小的质因数也必然能整除 n,这就与 k 是“最小因数”的前提相矛盾了。因此,我们只需要找到 n 的最小因数(k > 1)即可。
如何找到这个最小因数 k?视频采用了最朴素的“暴力搜索”(Brute Force)或“试除法”:
-
起始:我们从最小的可能因数
k = 2开始尝试 [05:03]。 -
检查:检查当前的
k能否“整除”n。在 Python 中,检查方式是n % k == 0(n除以k的余数是否为 0)。 -
迭代:如果当前的
k不能 整除n(即n % k != 0)[05:15],说明k不是我们要找的因数,我们就尝试下一个数,即k = k + 1[05:33]。 -
重复:回到第 2 步,用新的
k继续检查。 -
找到:当循环停止时,意味着
n % k == 0(即while循环的条件n % k != 0变为 False),我们便找到了那个能整除n的最小k。 -
返回:返回这个
k[05:46]。
这个逻辑被翻译成如下的 Python 代码:
1 | def smallest_prime_factor(n): |
视频特别强调了 return k 语句的缩进(indentation)[05:54]。return 必须放在 while 循环的外面。如果错误地将 return k 放在 while 循环的内部,那么函数会在第一次检查时(k=2)就立刻返回,无论 k 是否能整除 n,这完全破坏了算法的逻辑 [06:08]。return 必须在循环“完成”其搜索任务之后才执行。
至此,两个函数都已定义完毕,整个程序可以工作了。视频通过运行 prime_factors(858) 进行了验证,正确地输出了 2, 3, 11, 13 [06:25],证明了算法和代码的正确性。
框架 & 心智模型 (Framework & Mindset)
框架 1:通过“占位符”实现增量式开发 (Incremental Development via Placeholders)
视频在编写代码的过程中,无形中演示了一种强大且高效的编程心智模型:增量式开发(Incremental Development),其核心技巧是使用“功能占位符”。
这个框架在 prime_factors 函数的开发过程中体现得淋漓尽致。当作者写到 while n > 1: 循环时,他遇到了一个子问题:“如何找到 n 的最小质因数?”[03:38]
面对这个子问题,作者并没有停下主流程的开发转而立即去解决它。他的处理方式是 [03:46]:
-
承认子问题:他知道这是一个需要解决的问题(“who knows” [03:38]),但他选择“暂时不要担心”(“let’s not worry about how to do that”)。
-
定义“契约”:他假设已经存在一个函数,这个函数能完美地解决这个子问题。
-
命名与调用:他给这个虚构的函数起了一个清晰的名字
smallest_prime_factor,然后就像它已经存在一样去调用它:k = smallest_prime_factor(n)[03:53]。 -
完成主流程:基于这个“占位符”或“契约”,他继续完成了
prime_factors函数的剩余逻辑(n = n // k和print(k))[04:00]。
这种方法论,在视频的后半段被明确称为“功能抽象”(Functional Abstraction)[08:55]。
这个心智模型的价值在于“分离关注点”(Separation of Concerns):
-
降低认知负荷:在任何时候,开发者都只需要专注于一个层面的问题。在编写
prime_factors时,你只需要关注“分解”这个高阶(High-level)逻辑,而不需要同时担心“如何寻找因数”这个低阶(Low-level)的实现细节。 -
自顶向下设计(Top-Down Design):它允许你从最宏观的算法骨架开始构建程序。你首先搭建起程序的“脚手架”(
prime_factors),然后再去填充“脚手架”中每一个具体的实现细节(smallest_prime_factor)。 -
清晰的程序结构:这种方法自然而然地将代码分解为多个职责单一的小函数。
prime_factors的工作是“执行分解流程”,smallest_prime_factor的工作是“找到最小因数”。每个函数都有一个明确且单一的职责。
当作者在 [04:43] 说“我不能运行这段代码,直到我定义了 smallest_prime_factor”时,这标志着第一阶段(高阶逻辑)的完成,和第二阶段(低阶实现)的开始。这个无缝衔接的过程,就是增量式开发的精髓。它把一个复杂的大问题(实现质因数分解)拆解成了两个更小、更易于管理和验证的子问题。
框架 2:功能抽象 vs. 嵌套循环:代码可读性的权衡 (Functional Abstraction vs. Nested Loops: The Readability Trade-off)
在视频的后半部分 [08:38],作者提出了一个关键问题:既然我们已经有了 smallest_prime_factor 的实现,我们是否真的需要一个单独的函数?我们能不能把它的代码直接“内联”(inline)或“粘贴”(pasting)到 prime_factors 函数中,从而合并成一个函数?[09:01]
作者尝试了这种做法,即把 smallest_prime_factor 的 while k... 循环逻辑,直接嵌套(nesting)在 prime_factors 的 while n > 1 循环内部 [09:12]。这种尝试揭示了一个关于程序设计的核心心智模型:代码的清晰度(Clarity)往往比代码的紧凑性(Conciseness)更重要。
1. 内联代码的直接问题 (The Technical Problem):
-
作者首先指出,简单的复制粘贴会导致
return语句出错 [09:19]。在smallest_prime_factor中,return k的含义是“返回k给调用者prime_factors”。 -
但当
return k被内联到prime_factors内部时,它的含义变成了“立即终止 整个prime_factors函数并返回值”[09:33]。这会导致prime_factors(12)只打印出一个2就提前退出了,因为内层循环的return终止了外层循环。
2. 修复后(丑陋)的内联代码 (The “Fixed” but Ugly Version):
-
作者展示了如何通过移除
return语句来“修复”这个问题 [09:50]。修复后的代码在逻辑上是“正确”的,它能通过所有的测试用例。 -
然而,作者明确表示,这是一个“更差的程序”(worse program)[11:01]。
3. 为什么内联版本“更差”?(Why is it worse?)
-
认知混乱:修复后的代码包含一个“
while循环嵌套在另一个while循环内部”(a while statement within a while statement)[11:08]。这在视觉上和逻辑上都制造了巨大的复杂度。读者需要同时追踪两个(n和k)在不同循环层级中不断变化的变量。 -
变量名失效:
k这个变量的含义变得极其模糊 [10:12]。它一开始是2,然后作为内层循环的计数器不断递增,最后它又代表“找到的最小因数”。一个变量在短时间内扮演了太多的角色。 -
“修复”变量名也无济于事:作者尝试将
k重命名为smallest_prime(最小质因数)[10:18],但这让情况更糟。因为在k = k + 1的过程中,这个变量在绝大部分时间里并不是最小质因数 [10:32]。它只是一个“候选者”。这使得代码具有误导性,更难阅读。
4. 功能抽象版本的优越性 (Why is Functional Abstraction better?)
-
职责单一:原始的两函数版本 [10:44] 中,
prime_factors负责高阶流程,smallest_prime_factor负责低阶搜索。它们各自的“工作”(job)非常清晰 [10:50]。 -
代码即文档:
prime_factors中的k = smallest_prime_factor(n)这一行代码,读起来就像一句通顺的英文。函数名smallest_prime_factor本身就是最好的“注释”。 -
易于调试和验证:你可以单独测试
smallest_prime_factor函数,确保它在所有情况下(例如n是 2、是 13、是 9)都能正确返回。而将所有逻辑混杂在一起,则很难进行单元测试。
最终的心智模型(Moral of the Story)[11:15]:
解决一个问题的方法不止一种。Python 解释器并不在乎你的代码是否清晰,它只在乎语法是否正确 [11:08]。但是,人类 开发者在乎。视频的结论是,使用函数(和它们的命名)来清晰地划分和组织逻辑,几乎总是比试图将所有东西塞进一个冗长、嵌套的程序体中更为明智 [11:22]。这(功能抽象)是写出可维护、可理解代码的核心。
Q&A
1. Overview
本视频是CS 61A课程第3讲的配套问答(Q&A)录播。它并非一个结构化的授课,而是教授(John DeNero)针对学生们在学习了Python基础(如函数、环境模型)后提出的具体疑问进行解答。视频的核心论题是澄清Python中关于函数、作用域、环境模型和控制流的常见误解。结论是,深入理解代码的“环境模型”(Environment Model)——即程序如何跟踪变量名(names)和值(values)——是掌握编程的关键,而许多Python的语法细节(如“Truthy”值或if/elif的区别)需要精确记忆,即使它们在某些情况下看起来不那么直观。
2. 按照主题来梳理
由于这是一个Q&A视频,内容在不同主题间跳转。为了便于阅读,我已将相关问题重新组合为以下几个核心主题。
主题一:函数的行为:纯粹性、返回值与副作用
这个主题围绕着函数(function)的定义、行为及其对程序状态的影响展开。
-
什么是非纯函数 (Non-pure function)? [00:12]
-
纯函数 (Pure function) 的特点是,给定相同的输入,永远返回相同的输出,并且不产生任何“副作用” (side effects)。
-
非纯函数的定义则相反。视频中提到的非纯行为包括:
-
-
print函数的特殊性 [01:41] -
None值是什么? [26:35]
主题二:算法与控制流:素数分解 (Prime Factorization)
这是一个关于代码重构(refactoring)以及 return 语句如何影响循环控制流的案例研究。
-
问题背景: [06:35] 任务是编写一个函数
prime_factors(n),打印出整数n的所有素数因子(例如,n=12应打印2,2,3)。 -
原始算法: [06:48] 视频中采用的算法是:
-
当
n仍然大于 1 时,执行循环。 -
找到
n的“最小素数因子” (smallest prime factor),称之为k。 -
打印
k。 -
将
n更新为n除以k的结果 (n = n // k)。 -
重复此过程,直到
n变为 1。
-
-
清晰的实现 (使用辅助函数): [07:08]
-
最初的实现包含两个函数:
-
prime_factors(n): 主函数,包含while n > 1循环。 -
smallest_prime_factor(n): 一个辅助函数 (helper function),它找到并returnn的最小因子k。
-
-
在
while循环中,主函数调用辅助函数k = smallest_prime_factor(n),然后print(k)和更新n。这个版本运行良好 [10:21]。
-
-
失败的重构 (“内联”代码): [08:13]
-
教授尝试将
smallest_prime_factor函数的内部代码直接复制粘贴到prime_factors函数的while循环内部,试图将两个函数合并为一个。 -
问题所在: [09:26] 辅助函数的核心逻辑包含一个
return k语句。当这个语句被复制到while循环内部时,它仍然是一个return语句。 -
return的致命影响: [09:46]return语句的含义是“立即终止当前函数的执行,并返回这个值”。 -
导致Bugs: 当
prime_factors(12)运行时,while循环在第一次迭代时,计算出最小因子k=2。然后它遇到了(复制过来的)return k语句。程序立即退出了prime_factors函数,返回了 2。它再也没有机会打印,也没有机会继续while循环来寻找下一个 2 和 3 [09:58]。
-
-
教训: [10:06] 虽然删除那个
return语句可以修复这个bug,但合并后的代码变得非常难以阅读。这个例子说明了return语句的绝对控制权,以及使用辅助函数来分解复杂逻辑、提升代码可读性的重要性。
主题三:Python 语法细节与控制流
这部分涵盖了学生们对Python特定语法规则的疑问。
-
if...ifvs.if...elif[37:26] -
if/elif/else的语法解析 [29:16] -
缩进 (Indentation): 空格 vs. Tab
-
Python 如何知道函数何时结束? [23:37] 答案是缩进。Python 不像其他语言使用
{}或end关键字 [23:42]。函数体、循环体和条件体都是由它们相对于周围代码的缩进级别来定义的 [23:57]。 -
空格 (Spaces) vs. Tab 键: [24:34] 这是一个常见的陷阱。
Tab键(制表符)和“一组空格”在计算机看来是不同的字符 [25:26]。 -
一致性是关键: [24:45] Python 要求你在一个代码块中必须一致地使用缩进。如果你在一个函数中,有时用 Tab 键缩进,有时用4个空格缩进,Python 解释器会报错(例如
unindent does not match outer indentation level) [25:02]。 -
最佳实践: [25:41] 现代代码编辑器通常被配置为:当你按下
Tab键时,它不会插入一个 Tab 字符,而是自动插入4个空格符。这是推荐的约定 [25:47]。
-
3. 框架 & 心智模型 (Framework & Mindset)
从这场Q&A中,我们可以提取出两个至关重要的、用于理解Python执行的“心智模型”和一个关于代码风格的“思维模式”。
框架一:Python 环境模型 (The Python Environment Model)
这是本视频中讨论最深入、最核心的框架。它是一个可视化的心智模型,用于精确追踪程序执行时发生的一切,尤其是在涉及函数调用和变量时。
-
核心组件:
-
帧 (Frame): [16:40] “帧”是一个“盒子”,它存储了“名称” (names)(即变量名)到“值” (values) 的绑定 (bindings)。
-
程序开始时,只有一个“全局帧” (Global frame)。
-
关键规则: 每当一个函数被调用 (called) 时,Python 就会创建一个新的“本地帧” (local frame) [02:49]。
-
-
环境 (Environment): [16:45] “环境”是一连串 (sequence) 的帧 [16:55]。它总是从一个“当前帧”(比如你所在的本地帧)开始,并链接到它的父帧(对于简单的程序,父帧就是全局帧)[17:05]。
-
-
执行流程 (The Lookup Rule):
-
该模型解释的常见问题:
-
嵌套调用 (Nested Calls): [02:16] 如果函数
f调用了函数g(例如f内部有y = g(x+1)): -
UnboundLocalError(本地变量在赋值前被引用): [18:29] 这是一个经典的陷阱。-
假设:全局帧中
x = 2。你写了一个函数f:1
2
3def f():
print(x) # 试图在赋值前使用
x = 3 # 赋值 -
环境模型的解释: 当Python_编译*这个函数时,它会扫描整个函数体。它看到了
x = 3这行 [20:08]。 -
Python 的决定: 因为在函数体内部存在对
x的赋值操作,Python 判定x在这个函数中是一个本地变量 (local variable) [20:08]。 -
执行时的后果: 当你调用
f()时,它执行到print(x)。根据 Python 的决定,它只会在f的本地帧中查找x。它不会去全局帧查找 [19:54]。 -
错误: 在那一刻,本地帧中的
x还没有被x = 3赋值,所以 Python 报错:UnboundLocalError: local variable 'x' referenced before assignment(本地变量 ‘x’ 在赋值前被引用) [19:48]。 -
教训: [20:35] 这就是为什么强烈建议不要依赖全局变量,而是通过参数 (parameters) 传入数据,通过
return传出数据 [20:42]。
-
-
框架二:Python 的 “Truthy” (真值) 和 “Falsy” (假值) 规则
这是一个在 Python 中用于控制流(如 if 和 while 语句)的核心规则集。

