Lecture 4
Iteration Example
Overview
这篇是深入讲解一个著名的迭代(Iteration)示例:斐波那契数列(Fibonacci sequence)。视频通过 Python 编程语言,展示了如何使用 while 循环(while statement)来构建一个函数,以计算出斐波那契数列中任意位置(第 n 个)的数字。视频不仅详细拆解了迭代算法的逻辑、关键的状态变量以及代码的执行过程,还通过对比一个初始实现和其改进版本,强调了在设计算法时,处理“边界情况”(edge cases)的重要性。其核心结论是,一个更优的实现不仅要能处理一般情况,更要能稳健地、正确地处理诸如 n=0 这样的起始条件。
按照主题来梳理
1. 斐波那契数列:定义、索引与背景
视频首先介绍了我们即将研究的主题——斐波那契数列 [00:00]。这是一个在数学乃至自然界中都非常著名的序列。
-
序列的定义
斐波那契数列有一个非常明确的生成规则。它以两个初始数字 0 和 1 开始 [00:10]。从第三个数字开始,序列中的每一个后续元素(element)都是其前两个元素之和 [00:19]。
-
序列开始是:0, 1
-
0 + 1 = 1
-
1 + 1 = 2
-
1 + 2 = 3
-
2 + 3 = 5
-
3 + 5 = 8
-
…
-
因此,这个序列展开后是:0, 1, 1, 2, 3, 5, 8, 13, 21, 34… 视频提到,你可以看到这个序列在短时间内就开始变得相当大 [00:26]。
-
-
历史与命名
视频特别澄清了一个历史事实:斐波那契(Fibonacci)这位数学家,并非这个数列的发明者 00:35。这个序列在他之前的很长时间里,就早已被其他数学家讨论和描述过了。但是,斐波那契在他的工作中使得这个序列在西方世界(in the west)变得广为人知,起到了普及的作用 00:43。因此,为了纪念他的贡献,后人习惯性地将这个序列称为斐波那契数列。
-
索引惯例(Indexing Convention)
在计算机科学和数学中,当我们讨论序列时,“位置”或“索引”(index)的定义至关重要。对于斐波那契数列,视频强调了一个通用的惯例(convention):序列从第 0 位开始索引 00:52。
-
0 是“第 0 个”(zeroth)斐波那契数。
-
1 是“第 1 个”(first)斐波那契数。
-
1 是“第 2 个”(second)斐波那契数。
-
2 是“第 3 个”(third)斐波那契数。
-
…
-
视频举例说明 [01:10]:序列中的数字 5(0, 1, 1, 2, 3, 5),虽然它出现在序列的第 6 个“位置”(position six),但我们称它为“第 5 个”(fifth)斐波那契数(因为我们从 0 开始计数)。这只是一个需要共同遵守的约定。
-
-
斐波那契数列的意义:黄金螺旋
为什么人们会关心这个序列?视频提到了它一个非常有趣的特性:它与**“黄金螺旋”(golden spiral)**紧密相关 01:27。
-
你可以通过将一系列边长为斐波那契数的正方形拼贴(tiling)在一起来构建这个螺旋。
-
想象一下,你从一个边长为 1 的小正方形开始,旁边再放一个边长为 1 的正方形。然后拼上一个边长为 2 的正方形(1+1),接着是一个边长为 3 的(1+2),一个边长为 5 的(2+3),一个边长为 8 的(3+5),一个边长为 13 的(5+8)… [01:38]。
-
当你以这种方式将这些正方形螺旋式地排列组合后,如果你在这些正方形的对角交叉点之间绘制一条平滑的曲线(螺旋线),你会得到一个不断向外扩展(ever-expanding)的螺旋 [01:49]。
-
这个螺旋在人类的视觉中,被认为具有一种特别的平衡感(looks particularly well balanced to the human eye)[01:58]。
-
人们也热衷于在自然界中寻找这种螺旋的踪迹。视频展示了一张卷心菜(cabbage)的照片,有人认为他们在这颗卷心菜上发现了黄金螺旋的结构 [02:06]。
-
2. 通过迭代计算:fib(n) 函数的实现
在介绍了背景知识后,视频转向了核心的计算问题:我们如何编写一个函数 fib(n),当给定一个索引 n 时,它能计算出第 n 个斐波那契数 [02:15]。视频将使用 while 循环(while statement)来实现这个功能。
-
迭代设计的核心:追踪状态
视频强调,在设计一个迭代函数(iterative function)时,最重要的事情之一,就是思考你需要在迭代的每一步中“追踪”哪些信息(what information you need to keep track of)02:40。
-
为了计算 下一个 斐波那契数,我们必须知道 当前的 数和它 前一个 的数(因为定义是“前两项之和”)[02:54]。
-
此外,我们还需要知道我们 当前算到了第几个数,以便判断何时停止。
-
-
初始实现:fib(n) (for n >= 1)
视频中展示的第一个 fib 函数实现,其设计目标是计算 n >= 1 的情况 02:31。
-
- 定义状态变量与初始化:
我们从序列的“开端”开始。我们需要三个变量来追踪状态:
-
- 定义循环条件(终止条件):
我们的目标是找到第 n 个数。我们当前的 k 是 1,我们要让 k 一直增长到 n 为止。因此,循环(iteration)应该在 k < n 这个条件成立时持续执行 03:39。当 k 最终等于 n 时,循环将停止。
-
- 定义状态转移规则(循环体):
在循环的每一步中,我们需要更新我们的三个状态变量,让它们“前进”到序列的下一个位置。
-
- 定义返回值(提取结果):
while 循环的结构允许我们重复执行这个“状态转移”04:18。当 k < n 这个条件不再满足时,意味着 k 已经等于 n 了(因为 k 每次只加 1)04:33。
-
此时,我们已经找到了 第 n 个 斐波那契数。
-
根据我们的设计,这个“第 n 个”斐波那契数被存储在
cur变量中 [04:39]。 -
因此,函数最后返回(return)
cur。
-
-
执行示例:fib(5) 的环境图
视频通过一个环境图(environment diagram)的例子,生动地展示了 fib(5)(目标是找到第 5 个斐波那契数,即 5)的执行过程 04:49。
-
初始状态 (k=1):
n=5,pred=0,cur=1,k=1。 -
k < n(1 < 5) 为 True,进入循环。 -
循环 1 (k=2):
pred变为 1 (旧的cur),cur变为 1 (0 + 1)。k变为 2 [05:18]。 -
k < n(2 < 5) 为 True,进入循环。 -
循环 2 (k=3):
pred变为 1 (旧的cur),cur变为 2 (1 + 1)。k变为 3 [05:33]。 -
k < n(3 < 5) 为 True,进入循环。 -
循环 3 (k=4):
pred变为 2 (旧的cur),cur变为 3 (1 + 2)。k变为 4。 -
k < n(4 < 5) 为 True,进入循环。 -
循环 4 (k=5):
pred变为 3 (旧的cur),cur变为 5 (2 + 3)。k变为 5 [05:40]。 -
k < n(5 < 5) 为 False,循环终止 [05:45]。 -
返回: 函数返回
cur的当前值,即 5 [05:45]。
-
3. 实现的优化:一个关于初始值的讨论
视频的最后一部分,提出了一个关键的“讨论题”(discussion question),通过这个讨论,引出了一个更优、更稳健的实现方式 [05:54]。
-
问题:如果改变初始值会怎样?
问题是:如果我们将初始化代码修改为:pred, cur = 1, 0 以及 k = 0,这个“替代版本”(alternative definition)的 fib 函数与原始版本是相同还是不同?06:02
-
分析:一个更优的实现
视频在短暂停顿后揭晓了答案:这个替代版本不仅是正确的(对于 n >= 1 的情况),而且它甚至更好(even better)06:20。因为它能正确地计算原始版本无法处理的 n = 0 的情况。
-
优势 1:正确处理“边界情况”
n = 0 -
优势 2:依然能处理一般情况
n = 5-
那么这个改动是否会破坏
n = 5时的计算呢?[07:04] -
初始化:
pred=1,cur=0,k=0。 -
循环: 因为
k从 0 开始,而n是 5,while k < n这个循环的循环体将执行 5 次(当 k=0, 1, 2, 3, 4 时),而不是像原始版本中那样执行 4 次 [07:10]。 -
追踪
cur的变化 [07:16]:-
初始:
cur = 0 -
k=0 (循环 1):
cur=pred+cur= 1 + 0 = 1 -
k=1 (循环 2):
cur= 1 + 1 = 2 -
k=2 (循环 3):
cur= 2 + 1 = 3 -
k=3 (循环 4):
cur= 3 + 2 = 5 -
k=4 (循环 5):
cur= 5 + 3 = 8 (译者注:此处视频口误,cur的序列应为 0, 1, 1, 2, 3, 5。当 k=0,cur=1+0=1; k=1,cur=1+1=2; k=2,cur=2+1=3; k=3,cur=3+2=5; k=4,cur=5+3=8。不,我错了,让我们重新追踪)
-
-
让我们在
k=0时,严格按照pred, cur = cur, pred + cur规则重新追踪:-
初始:
n=5,pred=1,cur=0,k=0。 -
循环 1 (k=0):
k < n(0<5) 为 True。-
pred, cur = 0, 1 + 0(即pred=0,cur=1) -
k = 1
-
-
循环 2 (k=1):
k < n(1<5) 为 True。-
pred, cur = 1, 0 + 1(即pred=1,cur=1) -
k = 2
-
-
循环 3 (k=2):
k < n(2<5) 为 True。-
pred, cur = 1, 1 + 1(即pred=1,cur=2) -
k = 3
-
-
循环 4 (k=3):
k < n(3<5) 为 True。-
pred, cur = 2, 1 + 2(即pred=2,cur=3) -
k = 4
-
-
循环 5 (k=4):
k < n(4<5) 为 True。-
pred, cur = 3, 2 + 3(即pred=3,cur=5) -
k = 5
-
-
终止:
k < n(5<5) 为 False。 -
返回: 返回
cur,即 5 [07:16]。
-
-
结论: 结果依然正确。这个改进后的版本,通过调整初始化状态,实现了对
n=0边界情况的兼容,同时没有破坏一般情况的计算,因此是一个更稳健、更优秀的设计。
-
框架 & 心智模型 (Framework & Mindset)
从这个仅有 7 分钟的教学视频中,我们可以抽象出两个非常重要且通用的框架和心智模型,它们是计算思维(Computational Thinking)的基石。
1. 框架:迭代式问题解决的状态管理(State Management in Iterative Problem-Solving)
视频的核心(从 [02:40] 开始)其实是在教授一个解决问题的通用框架:如何通过迭代(一步一步地)来解决一个问题。这个框架的核心在于“状态管理”,它可以被分解为以下几个步骤:
-
步骤一:识别问题状态 (Identify the State)
在开始解决问题前,首先要自问:如果我要一步一步地计算,为了能算出“下一步”的结果,我必须知道“这一步”的哪些信息?这些信息就是“状态”。
-
在斐波那契问题中,视频点明 [02:54],为了计算 下一个 数,你必须知道 当前 的数和它 前一个 的数。这就是计算所需的核心状态。
-
此外,你还需要一个“计步器”状态,来知道你进行到第几步了,以及何时该停下。
-
-
步骤二:定义状态变量 (Define State Variables)
将第一步中识别出的“状态”信息,用具体的变量来命名和承载。
-
pred(predecessor):用于存储“前一个值”。 -
cur(current):用于存储“当前值”。 -
k(index):用于充当“计步器”,记录cur对应的是第几个数。 -
n(target):虽然n不变,但它也是状态的一部分,代表我们的“目标状态”。
-
-
步骤三:确定初始状态 (Determine Initial State)
这是迭代设计的关键一步,也是视频后半部分重点讨论的地方。你必须定义这些状态变量在“第 0 步”(迭代开始前)的值是什么。
-
步骤四:定义状态转移规则 (Define State Transition Rules)
这是迭代的“引擎”。你必须明确定义:状态变量如何从“上一步”变化到“下一步”?这通常是循环体(loop body)中的核心逻辑。
-
在视频中 [03:58],这个规则被精炼地表达为:
-
pred的新值 =cur的旧值 -
cur的新值 =pred的旧值 +cur的旧值 -
k的新值 =k的旧值 + 1
-
-
这个规则确保了计算可以按照斐波那契数列的定义,一步一步地“向前滚动”。
-
-
步骤五:确定终止条件 (Determine Termination Condition)
你必须定义一个清晰的条件,告诉这个“引擎”什么时候该“熄火”了。这通常是 while 循环的“条件”部分。
-
步骤六:提取最终结果 (Extract the Final Result)
当循环终止时,你的“答案”存储在哪个状态变量中?
- 根据这个算法的设计,当
k最终等于n时,cur变量中存储的正好就是“第 n 个”斐波那契数。因此,函数返回cur[04:39]。
- 根据这个算法的设计,当
这个六步框架不仅适用于计算斐波那契数列,它适用于几乎所有可以通过迭代解决的问题(例如计算阶乘、数组求和、模拟系统变化等)。
2. 心智模型:边界情况优先的思维模式 (Edge-Case-First Mindset)
视频的后半部分(从 [05:54] 开始)通过一个“讨论题”,着重培养了一种在工程和算法设计中至关重要的心智模型:永远要优先考虑边界情况(Edge Cases)。
-
“一般情况”的陷阱(The “Typical Case” Fallacy)
视频中的第一个实现(k 从 1 开始),是为一个“典型”或“一般”的输入(如 n=5)而设计的。它在 n=5 的情况下表现完美 05:45。但这种“只为一般情况设计”的思维,恰恰是许多程序错误的来源。这个实现是建立在 n >= 1 的假设之上的 02:31。
-
识别“边界”在哪里?
“边界情况”是指那些处于输入范围“边缘”的、特殊的、最简单的或最极端的情况。
-
在斐波那契问题中,
n=5是一般情况,而n=1和n=0则是“边界情况”,它们是序列的开端。 -
这个心智模型要求我们,在设计算法时,不能只想着
n=5,而是要主动去想:-
“如果输入是
n=1会怎样?” -
“如果输入是
n=0会怎样?” -
(如果允许)“如果输入是负数会怎样?”
-
-
-
用边界情况测试和驱动设计
视频通过 n=0 这个边界情况,来“攻击”第一个实现。
-
测试失败:第一个实现,当
n=0时,k=1,循环条件k < n(1 < 0) 立刻为 False,循环不执行,返回cur的初始值 1。这是错误答案(第 0 项应为 0)。 -
驱动改进:这个失败的测试,驱动我们去寻找一个更好的设计。
-
测试成功:视频中的第二个实现(
k从 0 开始,cur从 0 开始),在面对n=0的测试时,k < n(0 < 0) 同样为 False,循环不执行,但它返回cur的初始值 0 [06:55]。这是正确答案。
-
-
稳健性(Robustness)的胜利
这个“边界情况优先”的心智模型,其最终目标是追求“稳健性”(Robustness)。
Designing Functions
Overview
这篇内容的核心论题是探讨在编程中如何设计出优秀的函数 (Functions)。视频首先强调了函数设计作为计算机科学的一项基本技能的重要性,它能帮助我们更好地组织大型程序 [00:31]。优秀的函数设计不仅能让代码更易于阅读和理解,还能使其在更多情境下更具可用性 [00:08]。视频的核心结论是,优秀的设计应遵循三个关键的指导方针(或启发式原则):单一职责(一个函数只做一件事)、不要重复自己(DRY 原则)以及定义通用函数(使其具有广泛适用性)[02:36]。视频通过类比(如剪刀、瑞士军刀、电源插座)来强化这些原则,旨在帮助开发者建立起一种清晰、高效、可复用的编程心智模型。
按照主题来梳理
主题一:理解函数的核心特性:定义域、值域与行为
在深入探讨如何“设计”函数之前,我们必须首先清晰地定义一个函数由什么构成。视频从概念层面(而非特定于 Python 语法的层面)[02:05] 阐述了函数的三个基本特性,它们共同决定了一个函数“是什么”以及“能用在哪里”。
-
1. Domain (定义域)
-
2. Range (值域)
-
3. Behavior (行为)
-
定义: 对于一个“纯函数” (pure function) 而言(视频中特别提到了“纯函数”),其行为是“它在输入和输出之间建立的关系” [01:11]。
-
示例:
-
意义: 行为描述了函数的核心逻辑,即“转换”的规则。它连接了定义域和值域,说明了一个特定的输入是如何映射到一个特定的输出的。
-
综上所述,这三个特性(Domain, Range, Behavior)共同构成了对一个函数最完整的概念性描述 [01:03]。
主题二:优秀函数设计的三大指导方针
视频接着提出,设计函数是一项需要“基于经验的启发式” (heuristics based on experience) 的技能 [02:36]。虽然这些不是绝对的定律,但在大多数情况下,它们是设计出优秀函数的基石 [02:36]。
-
1. Give each function exactly one job (一个函数只做一件事)
-
核心理念: 这是最重要的原则之一 [02:44]。你的函数应该目标明确、职责单一。
-
类比:剪刀 vs 瑞士军刀 [03:03]
-
视频建议我们应该“将函数建模成剪刀” (model your functions after scissors) [03:03]。剪刀的“工作” (job) 非常明确:就是“切割” (cut things) [03:09]。它简单、高效、易于理解。
-
相反,我们应该避免设计“瑞士军刀” (Swiss army knife) 式的函数 [03:09]。一把瑞士军刀试图同时做很多不同的事情(切割、开瓶、拧螺丝等)。
-
关键转折: 如果一个东西(一个软件功能)确实需要实现多种不同的功能,正确的做法_不_是把所有功能塞进一个庞大而复杂的函数里,而是应该“让它包含许多不同的函数” (it should probably involve many different functions) [03:17]。换言之,用多个职责单一的“剪刀”来_组合_成一个“瑞士军刀”的功能集。
-
-
-
2. Don’t repeat yourself (DRY) (不要重复自己)
-
核心理念: “DRY 原则” [02:47]。如果你发现自己在不同的地方写了几乎一样的代码逻辑,你就应该警惕了。
-
实施: “将一个过程只实现一次” (Implement a process just once) [02:47],然后你就可以在任何需要的地方“多次执行它” (execute it many times) [02:54]。这个“过程”就是函数。
-
类比: 视频用了一张有四只小狗的图片 [03:27] 来反讽(“小狗也是很多的工作”),其核心隐喻是,重复性的事物(无论是代码还是小狗)都会创造“大量的工作” (a lot of work) [03:17]。通过函数封装重复的逻辑,可以极大减少维护成本。
-
-
3. Define functions generally (定义通用的函数)
-
核心理念: 在设计函数时,你应该追求“通用性” [02:54]。思考你的函数是否可以被更广泛地应用,而不仅仅是针对当前这一个狭窄的特例。
-
类比:混乱的电源插座 [03:27]
-
视频使用全球的“电源插座” (electrical sockets) [03:36] 作为一个“反面教材” (counter example) [03:27]。
-
现实世界中,人们用“很多不同的方式” (lots of different ways) 解决了“插电”这个相同的问题,其结果就是我们现在拥有“所有这些疯狂的适配器” (all these crazy adapters) [03:36],导致“插电变得非常困难” (it’s really hard to plug stuff in) [03:45]。
-
理想状态: “如果只有一种通用的插电方式,那该多好?” (wouldn’t it be nice if there was just one General way to plug something in) [03:51]。
-
-
应用: 这就是你在设计函数时应该“渴望” (aspire) 达到的状态 [03:56]。不要制造“需要适配器”的函数,而要努力设计那个“通用的插座”,避免陷入“电源插座世界中存在的混乱” (the chaos that exists in the electrical outlet world today) [03:56]。
-
框架 & 心智模型 (Framework & Mindset)
函数设计的“单一、复用、通用”心智模型
视频中提出的三个指导方针 [02:36],共同构筑了一个在进行函数设计时可以依赖的强大心智模型 (Mindset) 或框架 (Framework)。这个框架帮助我们将编程从简单的“编写语句” (writing statements) [00:16] 提升到“组织大型程序” (organize large programs) [00:31] 的层面。这个心智模型可以被提炼为“单一”、“复用”和“通用”。
-
1. 单一职责 (Single Job) - “剪刀”思维
-
这不仅是一个原则,更是一种解耦 (Decoupling) 的心智模型。当你面对一个复杂问题时,你的第一反应不应该是构建一个庞大的、试图解决所有问题的“瑞士军刀” [03:09]。
-
相反,你应该像一个外科医生一样,精确地将问题“切割” (cut) [03:09] 成一系列更小的、独立的子问题。每一个子问题都应该由一个专用的“剪刀”(即函数)来处理。
-
这种思维方式的好处是显而易见的,正如视频所暗示的(“更容易阅读和理解” [00:08]):
-
可读性: 一个只做一件事的函数,其名称(例如
calculate_area)和其行为(计算面积)是高度一致的。阅读者(包括未来的你)可以快速理解其意图,而不需要钻研一个包含了几百行逻辑的复杂函数。 -
可测试性: 只做一个小事情的函数非常容易测试。你知道它的输入(Domain)和输出(Range),可以轻易地为其编写单元测试。而一个“瑞士军刀”函数,你必须测试它所有功能的排列组合,这极其困难。
-
可维护性: 当“切割”功能(剪刀)坏了,你只需要修理或替换这把剪刀。你不需要动开瓶器或螺丝刀。在代码中,这意味着 bug 修复和功能迭代被限制在非常小的范围内。
-
-
视频中提到“如果一个东西需要很多功能,它应该包含很多函数” [03:17],这正是组合 (Composition) 思想的体现。这个心智模型的核心是:优先拆分,然后组合。
-
-
2. 避免重复 (DRY) - “复用”思维
-
这是一种抽象 (Abstraction) 的心智模型。“不要重复自己” [02:47] 的本质是识别出代码中的“模式” (Patterns)。
-
当你第二次或第三次写下相似的代码块时,DRY 心智模型应该触发警报。这表明你发现了一个可复用的“过程” (process) [02:47]。
-
此时的任务就是将这个过程“只实现一次” [02:47],将其从具体的情境中“抽象”出来,放进一个函数里。然后,在所有需要它的地方,你不再是复制代码,而是“执行” (execute) [02:54](即调用)这个函数。
-
这种思维方式的好处是(视频中提到的“避免大量的工作” [03:17]):
-
效率: 显而易见,你只写一次,但用N次。
-
健壮性: 这是最大的好处。如果那个被重复的逻辑(那个“过程”)有一个错误,或者需要更新(例如,税率计算规则改变了),你只需要在一个地方——那个唯一的函数——进行修改。所有调用它的地方都会自动获得更新。如果你是复制粘贴的,你将陷入“在所有副本中查找并修复”的噩梦,这极易出错。
-
-
-
3. 通用定义 (General) - “适配器”规避思维
-
这是一种前瞻性 (Forward-Looking) 的心智模型。它要求设计师在解决当前问题时,稍微“抬头”思考一下未来。
-
“电源插座的混乱” [03:56] 是缺乏通用设计的典型后果。在编程中,这相当于:
-
你为“用户 A”写了一个函数
calculate_sales_tax_for_user_A。 -
然后为“用户 B”写了
calculate_sales_tax_for_user_B。 -
很快你就会有十个这样的函数,它们 90% 的逻辑是重复的,但因为微小的差异(比如税率不同)而无法统一。你被迫制造了大量的“代码适配器” [03:45]。
-
-
“通用”心智模型会指导你采用不同的方法。你应该设计一个
calculate_sales_tax(user, amount)函数。这个函数足够“通用” [03:51],它可以处理_任何_用户和_任何_金额。它可能会在内部查询用户的税率,但函数的“接口” (interface) 是通用的。 -
这种思维方式的好处是“在更多情况下更有用” [00:08]:
-
这要求设计者在函数参数(定义域)的设置上具有一定的远见,思考“什么在变,什么不变”,并将“变化的部分”(如税率、特定配置)作为参数传入,而不是将其硬编码 (hard-code) 在函数体中。
-
Higher-Order Functions
Overview
本视频的核心论题是“高阶函数”(Higher-Order Functions)是编程中一种强大的抽象工具。它探讨了我们如何通过“泛化”(generalize)代码模式来设计函数,从最开始的重复性代码出发,识别出“共同结构”与“特定部分”,并将其分离。视频通过两个核心案例——计算几何图形面积和计算数列求和——逐步展示了泛化的过程:首先是泛化“数据”(如形状常量),然后是泛化“计算过程”本身。最终的结论是,通过定义一个接受其他函数作为参数(即“高阶函数”)的通用函数,我们可以极大地减少代码重复,将通用的计算方法(如循环、累加)与特定的计算逻辑(如对每一项进行立方或取恒等)解耦,从而写出更具复用性、更清晰、更易于维护的代码。
按照主题来梳理
第一部分:通过计算面积理解“泛化数据”
视频的第一个部分通过一个“计算几何图形面积”的例子,向我们展示了“泛化”(Generalization)的初级阶段:即如何通过抽象来分离“共同的计算结构”和“特定的数据”。
-
最初的问题:重复的代码
视频首先展示了三个独立的、用于计算不同形状面积的函数 [00:31]。这三个形状分别是正方形(Square)、圆形(Circle)和六边形(Hexagon)。它们的面积计算公式都依赖于一个“相关长度 R”——对于正方形和六边形是边长(side length),对于圆形是半径(radius) [00:41]。
-
正方形面积:1 * R²
-
圆形面积:π * R²
-
六边形面积:(3√3 / 2) * R²
在代码实现中,这意味着我们需要定义三个几乎独立的函数 [01:45]:
1
2
3
4
5
6
7
8
9
10def area_square(r):
return r * r * 1
def area_circle(r):
from math import pi
return r * r * pi
def area_hexagon(r):
from math import sqrt
return r * r * 3 * sqrt(3) / 2这个阶段的问题非常明显:代码之间存在大量重复。
-
-
识别“共同点”与“不同点”
通过观察这三个公式 01:05,我们可以清晰地识别出模式:
-
共同点 (Common Structure):所有的计算都包含了
R * R(即 R 的平方)这一项。这是它们共享的“计算面积”的核心逻辑的一部分。 -
不同点 (Specific Parts):真正区分这三个形状的是它们各自的“形状常量”(shape constant)[01:12]:正方形是
1,圆形是pi,六边形是(3 * sqrt(3) / 2)。
-
-
引入问题:输入校验与重复劳动
在进行泛化之前,视频引入了一个现实中的编程问题:输入校验(validation)。如果我们传入一个负数的边长(例如 -10),当前的函数会返回一个正数面积,这在逻辑上是错误的[02:42]。
为了解决这个问题,视频介绍了 Python 中的
assert(断言)语句 [02:55]。assert后面跟着一个表达式,如果该表达式为 False,程序将抛出一个AssertionError(断言错误)并显示一条消息。我们可以用它来确保长度R必须是正数:1
assert r > 0, "A length must be positive"
[03:43]
但是,如果我们要修复这个问题,我们就必须把这行
assert语句复制粘贴到area_square、area_circle和area_hexagon三个函数中 [04:03]。这立刻凸显了“重复自己”(repeat myself)的弊端 [04:14]:如果未来我们想修改这个校验逻辑(比如改成r >= 0),我们就必须修改三个地方,这非常容易出错。 -
泛化:封装共同点,参数化不同点
这个“重复”的问题,无论是 R * R 还是 assert 语句,都指向了同一个解决方案:泛化 04:24。我们可以定义一个通用的 area(面积)函数,它封装所有“共同点”,并把“不同点”作为参数传入。
在这个例子中:
-
共同点 是:
assert r > 0, "A length must be positive"和r * r * ...。 -
不同点 是:那个特定的“形状常量”。
于是,一个泛化的
area函数诞生了 [04:31]:1
2
3def area(r, shape_constant):
assert r > 0, "A length must be positive"
return r * r * shape_constant这个函数接收两个参数:长度
r和shape_constant(形状常量)。它内部包含了共享的校验逻辑和r * r的计算。 -
-
重构:使用泛化函数
有了这个通用的 area 函数,我们现在可以重新定义之前那三个独立的函数,但这一次它们不再包含重复的逻辑,而是简单地调用 area 函数并传入“特定”的参数 05:05:
1
2
3
4
5
6
7
8def area_square(r):
return area(r, 1)
def area_circle(r):
return area(r, pi)
def area_hexagon(r):
return area(r, 3 * sqrt(3) / 2)现在,所有的核心逻辑(包括校验)都集中在 area 函数中。如果我们想修改校验规则,只需修改一个地方。我们就此完成了第一层泛化:通过将“特定数据”(形状常量)参数化,来共享“通用的计算实现” 01:27。
第二部分:高阶函数:泛化“计算过程”
第一部分展示了如何泛化“数据”(一个数字常量),而视频的第二部分则将这个理念提升到了一个全新的、更强大的层次:我们不仅可以泛化数据,还可以泛化“计算过程”(computational processes)本身 [06:03]。这就是“高阶函数”的用武之地。
-
新的问题:重复的“求和”模式
视频提出了一个新的场景:计算不同类型的数列求和 06:12。
-
自然数求和 (Sum Naturals):
1 + 2 + 3 + ... + n -
立方数求和 (Sum Cubes):
1³ + 2³ + 3³ + ... + n³ -
一个更复杂的、用于逼近 Pi 的级数求和 [06:48]。
我们先关注前两个。为了实现它们,我们可能会写出如下两个函数:
sum_naturals(n) 07:55:
1
2
3
4
5def sum_naturals(n):
total, k = 0, 1
while k <= n:
total, k = total + k, k + 1 # 关键差异点
return totalsum_cubes(n)[09:10]:1
2
3
4
5def sum_cubes(n):
total, k = 0, 1
while k <= n:
total, k = total + pow(k, 3), k + 1 # 关键差异点
return total -
-
再次识别“共同点”与“不同点”
和面积的例子一样,我们来比较这两个函数 10:11。
-
关键的飞跃:“不同点”是一个“过程”
在面积的例子中,“不同点”是一个简单的数字(如 pi)。但在这里,“不同点”是一个计算(computation)或表达式(expression)07:29。我们需要的不是传入 k 或 k^3 的结果,而是需要传入“如何根据 k 计算出下一项”的这个“过程”本身。
-
解决方案:将“过程”封装为函数
我们如何才能把一个“过程”作为参数传递呢?答案是:将这个过程定义为一个函数。
视频中定义了两个小函数,它们分别代表了那两个“特定的部分”10:35:
-
高阶函数:封装“共同点”,参数化“过程”
现在,我们可以像泛化 area 函数一样,来泛化求和的过程。我们将定义一个名为 summation(求和)的通用函数 11:09。
这个函数需要两个参数:
-
n:要累加多少项。 -
term(项):一个函数,它告诉summation在每一步(即每个k)应该累加什么 [11:17]。
这个
summation函数如下所示 [12:06]:1
2
3
4
5
6def summation(n, term):
total, k = 0, 1
while k <= n:
# 这里的 term(k) 就是在调用那个被传入的函数
total, k = total + term(k), k + 1
return total请注意
total + term(k)[12:28] 这一行。summation函数本身并不知道也不关心它是在计算k还是k的立方。它只是执行了通用的“迭代-累加”逻辑,并在每一步“委托”(defer)那个特定的计算给它通过term参数接收到的那个函数 [12:22]。这就是高阶函数:
summation是一个高阶函数,因为它接收了另一个函数(term)作为其参数 [14:44]。 -
-
重构:使用高阶函数
有了这个极其通用的 summation 函数,我们现在可以重写 sum_naturals 和 sum_cubes,使它们变得极其简洁 13:00:
1
2
3
4
5
6
7def sum_naturals(n):
# 调用 summation,并把 "identity" 函数作为参数传给 term
return summation(n, identity)
def sum_cubes(n):
# 调用 summation,并把 "cube" 函数作为参数传给 term
return summation(n, cube)当我们调用 sum_cubes(5) 时 11:50,它会调用 summation(5, cube)。在 summation 函数内部,n 是 5,term 这个名字被绑定到了 cube 这个函数对象上 14:06。因此,在 while 循环内部,每次执行 term(k) 时,它实际上执行的就是 cube(k) 14:29,从而实现了立方的累加。
框架 & 心智模型 (Framework & Mindset)
Framework: “抽象与泛化”的两步法 (The Two-Step Abstraction & Generalization Framework)
视频通过两个案例,反复演示了一个从“具体”走向“通用”的强大框架。这个框架可以帮助我们识别重复,并写出更优雅、更可复用的代码。
步骤一:识别模式 (Identify Patterns)
这是框架的第一步,也是最关键的一步,它要求我们从“观察”开始。
-
并置具体实现 (Juxtapose Implementations):不要一上来就试图设计一个“完美”的通用函数。相反,先去解决几个具体的问题。就像视频中,我们先写了
area_square和area_circle[01:45],或者先写了sum_naturals和sum_cubes[07:55]。把这些“具体”的实现并排放在一起。 -
寻找“共同点” (Find the Common Structure):仔细比较这些代码。它们中一定有完全相同或结构上等价的部分。
-
寻找“不同点” (Find the Specific Parts):在剥离出共同点之后,剩下的就是让这几个具体实现“彼此不同”的关键所在。
步骤二:提取与封装 (Extract & Encapsulate)
一旦完成了识别,我们就进入了“重构”的阶段。
-
封装“共同点” (Encapsulate the Common):将你在上一步识别出的“共同结构”提取到一个全新的、更通用的函数中(例如
area函数 [04:31] 或summation函数 [11:09])。这个新函数代表了那个“通用计算方法”的骨架。 -
“参数化”不同点 (Parameterize the Specific):这是整个框架的核心。你需要一种方法,在调用通用函数时,能把“不同点”告诉它。
-
重构 (Refactor):最后,回到你最初的那些“具体”函数(
area_square,sum_cubes等),将它们的实现完全替换为对你新创建的“通用”函数的调用。在调用时,传入它们各自“特定”的参数(是pi这个“数据”[05:11],还是cube这个“行为”[13:10])。
这个两步框架的最终产物是分层清晰的代码:顶层是高度具体、意图清晰的函数(如 sum_cubes),底层是一个(或多个)高度通用、可复用的“引擎”(如 summation),两者通过“参数”(无论是数据还是函数)连接起来。
Mindset: “万物皆可为参数” (Everything Can Be an Argument Mindset)
这个视频所倡导的,是一种根本性的思维模式转变,即“万物皆可为参数”。
我们通常的编程思维习惯于将“数据”(Data)作为参数传递给“函数”(Function),函数则包含了“行为”(Behavior)。例如,我们调用 area(10, pi),10 和 pi 是数据,area 是行为。
但是,高阶函数打破了这种“数据”与“行为”的固定划分。它告诉我们:“行为”本身也可以是一种“数据”,因此“行为”也可以作为参数被传递。
-
从“行动者”到“可传递的值”:
在 sum_cubes 的例子中,cube 函数(def cube(k): … 10:51)是一个“行动者”,它知道如何执行“立方”这个动作。然而,当我们执行 summation(n, cube) 13:10 时,我们并没有 调用 cube,我们只是把 cube 这个函数对象本身(一个包含了“如何立方”这个行为的值)传递给了 summation。
-
“委托”而非“包办”:
这种思维模式让我们能够编写“委托型”的通用函数。summation 函数 11:09 就是一个典型的“委托者”。它负责了它最擅长的事情:设置循环、管理 total 和 k 12:06。但它并不“包办”所有的工作,它不知道也不关心“具体要加什么”。当它运行到 total + term(k) 12:28 这一行时,它是在说:“好了,到了我不知道该怎么办的特定部分了,我要把我拿到的这个 term(它恰好是一个函数)叫来,让它用 k 来完成这项工作”。
-
分离“变”与“不变”:
这个心智模型的核心是分离“变化”与“不变”。
采用这种“万物皆可为参数”的心智模型,意味着我们不再将函数仅仅视为执行命令的动词,而是将其也视为可以被传递、组合和操作的名词。这使得我们可以构建出更高层次的抽象,将通用的算法框架(如求和、映射、过滤)与特定的业务逻辑(如“立方”、“身份认证”、“格式化字符串”)分离开来,这正是现代编程中许多最强大功能(如 map, filter, reduce)的基石。
Functions as Return Value
Overview
本视频的核心论题是“函数作为返回值”(Functions as Return Values),这是“高阶函数”(Higher-Order Functions)的一个关键特性。视频通过一个核心示例 make_adder(制造一个加法器)清晰地展示了:一个函数(make_adder)可以被定义为在其内部创建并“返回”另一个函数(adder)。这个被返回的内部函数(adder)具有一种特殊的能力,它能够“记住”其创建时所在环境(即外部函数 make_adder)中的变量(例如变量 n)。视频的结论是,这种将函数作为“一等公民值”(First-Class Values)来传递和返回的编程范式,极大地增强了代码的抽象能力、复用性,并有助于实现“关注点分离”(Separation of Concerns)这一重要的软件设计原则。
按照主题来梳理
章节一:延续:使用高阶函数计算 Pi
在深入探讨“函数作为返回值”这一新概念之前,视频首先回顾了“高阶函数”的另一个方面:将函数作为 参数 传递。视频延续了之前讨论过的 summation(求和)函数,这是一个通用的求和工具。它的强大之处在于,它将“求和”这一 行为模式(即从第 1 项加到第 n 项)与 具体要加什么(即每一项的具体内容)分离开来。summation 函数接受另一个函数作为参数,这个参数函数(在视频中被称为 term)的任务就是定义序列中的“第 k 项”是什么。
本次的示例是计算一个收敛于 (圆周率)的序列。
-
首先,定义了一个名为
pi_term(Pi 项)的函数 [00:31]。这个函数接受一个参数k(代表序列中的第 k 项)。 -
pi_term的工作是计算并返回一个特定公式的值,该公式为:8 / ((4 * k - 3) * (4 * k - 1))[00:31]。这个公式定义了序列的每一项。 -
有了这个
pi_term函数,就可以将其作为参数传递给通用的summation函数。 -
视频中执行了
summation(1000000, pi_term)[00:58],意为“使用pi_term函数作为项的定义,对这个序列求和一百万(1,000,000)次”。 -
执行的结果得到了
3.141592...[00:58],这是一个非常接近 真实值的近似值。
这个开场示例的意义在于,它生动地展示了高阶函数的价值。我们不需要为求和 序列重写一个新的求和循环,我们只需要定义一个全新的、专用于计算 序列项的 pi_term 函数,然后把它“喂”给已有的、通用的 summation 函数即可。这完美地体现了代码复用和抽象。
章节二:核心概念:返回函数的函数 make_adder
在展示了函数作为 参数 的威力后,视频转向了本节的核心主题:函数作为 返回值 [01:18]。为了演示这一点,视频定义了一个名为 make_adder(制造加法器)的函数 [01:26]。
make_adder 的定义如下:
-
它接受一个参数,我们称之为
n[01:26]。这个n将是“我们想要加上的那个数”。 -
关键在于:在
make_adder函数的 内部,定义了 另一个 函数,名为adder(加法器) [02:27]。 -
这个内部的
adder函数接受一个参数,我们称之为k[01:45]。 -
adder函数的函数体非常简单:它返回k + n[01:45]。 -
最后,外部的
make_adder函数 返回(return)的 不是 一个数字,而是adder这个 函数本身 [02:56]。
视频通过一个实例来演示这是如何工作的 [01:58]:
-
我们调用
make_adder(3),并将其结果赋给一个新变量add_three。 -
发生了什么?
-
make_adder被调用,此时n被绑定为3。 -
在
make_adder内部,adder函数被定义。在adder的定义中,k是一个占位符(等待被调用时传入),但n已经从其“外部环境”中获取,并被确定为3。 -
make_adder执行完毕,它返回了adder这个函数对象。
-
-
因此,
add_three这个变量现在指向的 就是 那个内部的adder函数。更准确地说,它是一个“特制版”的adder函数,这个版本“记住”了n等于3。 -
现在,如果我们调用
add_three(4)[02:06]:-
这等同于调用那个被返回的
adder函数,并传入4作为参数k。 -
函数执行
k + n,也就是4 + 3。 -
最终结果返回
7[02:14]。
-
这个例子最神奇的地方在于 [02:20],当我们调用 add_three(4) 时,make_adder(3) 的执行 早就已经结束了。但 add_three(即内部的 adder 函数)仍然能够访问并“记住”那个值为 3 的变量 n。这种现象——一个函数(adder)和它被创建时所在的环境(make_adder 的作用域,包含了 n=3)的组合——就是编程中著名的“闭包”(Closure)。
视频强调了这种嵌套的 def(定义函数)结构 [02:49]:
-
make_adder(外部函数)返回一个 函数。 -
adder(内部函数)返回一个 数字。 -
最重要的是,内部函数
adder可以自由地使用它自己的参数(k)和其 外部 函数make_adder的参数(n)[03:03]。
章节三:make_adder 的求值过程与“闭包”
理解 make_adder 的关键在于理解 Python 如何对一个看似奇怪的表达式求值,例如:make_adder(1)(2) [04:04]。
这个表达式看起来像是函数后面跟了两个括号。视频解释了它的求值步骤 [04:35]:
-
第一步:求值“操作符”(Operator)
-
在 Python 中,
function(argument)这种形式被称为“调用表达式”(Call Expression)。 -
在
make_adder(1)(2)中,Python 首先会把make_adder(1)视为一个整体,即“操作符”部分(因为它在另一对括号(2)的左边)。而(2)则是“操作数”(Operand)部分。 -
一个操作符可以是任何“求值结果为函数”的表达式 [04:21]。
-
因此,Python 必须先求值
make_adder(1)。
-
-
第二步:执行
make_adder(1)-
这又是一个调用表达式。Python 查找
make_adder函数,并用参数1来调用它。 -
根据
make_adder的定义,n被绑定为1。 -
make_adder创建了内部的adder函数(此时adder知道n=1)。 -
make_adder(1)调用结束,它 返回adder函数(这个版本“记住”了n=1)。
-
-
第三步:执行返回的函数
-
现在,第一步中的“操作符”
make_adder(1)已经成功求值得到了结果——即那个“记住了n=1”的adder函数。 -
于是,原来的表达式
make_adder(1)(2)就变成了adder(2)(这里的adder是上一步返回的那个特定函数)。 -
Python 现在执行这个新的调用。
adder函数被调用,参数k被绑定为2。 -
adder函数执行它的函数体:return k + n。 -
此时,
k是2,n是它“记住”的1。 -
2 + 1等于3。
-
-
第四步:返回最终结果
- 表达式
make_adder(1)(2)的最终求值结果为3[05:07]。
- 表达式
视频还在 Python 交互式环境中演示了这一点 [05:16],证明 make_adder(1)(2) 确实得到 3。
为了进一步强化这个概念,视频还展示了分两步执行的过程 [05:26]:
-
add_2000 = make_adder(2000)- 这创建并返回了一个“记住”了
n=2000的adder函数,并将其命名为add_2000。
- 这创建并返回了一个“记住”了
-
add_2000(13)-
这会调用
add_2000函数,传入k=13。 -
它执行
k + n,即13 + 2000。 -
结果为
2013。
-
这个两步的过程清晰地表明,make_adder 的真正工作是 制造和配置 新的函数。它返回的函数是一个独立的值,可以被存储在变量中,也可以被立刻调用。
框架 & 心智模型 (Framework & Mindset)
框架一:高阶函数 (Higher-Order Functions)
视频的最终目的,是利用 make_adder 的例子来建立一个强大的编程心智模型,即“高阶函数”(Higher-Order Functions)。
要理解高阶函数,首先必须接受一个前提:函数是“一等公民值”(First-Class Values) [05:43]。
-
“一等公民”是一个比喻,意思是函数在编程语言中的地位,与数字(如
1,3.14)、字符串(如"hello")或列表等其他类型的数据是平等的。 -
这种平等的地位体现在:
-
可以作为参数传递给其他函数:就像
summation函数接受pi_term函数作为参数一样。 -
可以作为函数的返回值:就像
make_adder函数返回adder函数一样。 -
(视频未明确提及,但隐含)可以被赋值给变量(如
add_three = make_adder(3))或存储在数据结构中。
-
基于“函数是一等公民”这个前提,高阶函数 的定义就水到渠成了:一个函数如果满足以下 至少一个 条件,它就是高阶函数 [05:58]:
-
接受至少一个函数作为其参数(例如
summation函数)。 -
返回一个函数作为其结果(例如
make_adder函数)。
这个框架是函数式编程的核心思想。它将“动作”或“计算过程”(即函数)本身数据化、变量化了。在 make_adder 的例子中,我们操作的不再是具体的数字,而是“加法”这个 概念 本身。我们通过 make_adder 制造 出了一个“加 3”的函数和一个“加 2000”的函数。这种对“计算过程”进行抽象和操作的能力,是高阶函数框架提供的最强大的力量。
心智模型二:高阶函数的价值:抽象、复用与“关注点分离”
为什么高阶函数如此重要?视频在结尾总结了它的三大价值 [06:07],这构成了我们应该建立的“心智模型”:
-
表达通用的计算方法 (Express General Methods of Computation)
- 高阶函数允许我们编写“方法的方法”。
summation函数就是最好的例子 [06:07]。它不关心 具体 加什么,它只关心“如何加”(即累加的模式)。它将“累加”这个通用的计算方法(summation)与“要累加的具体项”(pi_term或cubes)分离开来。这是一种强大的抽象能力,让我们能够站在更高的维度去思考计算。
- 高阶函数允许我们编写“方法的方法”。
-
消除程序中的重复 (Remove Repetition)
-
关注点分离 (Separation of Concerns)
-
这是一个极其重要的软件设计原则。它的核心思想是:每个函数(或模块)都应该只做好一件事 [06:24]。
-
高阶函数是实现这一原则的利器。
-
在
summation的例子中:summation函数的 唯一关注点 是“如何执行累加”;而pi_term函数的 唯一关注点 是“如何计算第 k 项 序列”。两者各司其职,互不干扰。 -
在
make_adder的例子中:make_adder的 唯一关注点 是“如何制造一个新的加法函数”;而被制造出来的adder函数(例如add_three)的 唯一关注点 则是“如何将传入的数字加上 3”。 -
这种分离使得我们的程序被分解为一系列高度专业化、功能单一的小组件。这些组件可以被独立地开发、测试、复用和组合,从而极大地提高了代码的清晰度、可维护性和健壮性。
-
Lambda Expressions
Overview
本视频的核心论题是介绍 Python 中的 lambda expression(Lambda 表达式):一种其计算结果为“函数”本身的表达式。视频通过对比实验,首先展示了为何简单的变量赋值无法创建函数([00:42]),进而引出了 lambda 表达式的正确语法和用法,即它允许我们以内联(inline)的方式定义一个函数,并将其像普通数值一样绑定(bind)到一个名称上([01:10])。视频的结论是,lambda 表达式与传统的 def 语句在功能上(如创建函数的行为、作用域等)几乎完全相同([04:50]),但存在两个关键区别:lambda 表达式只能包含单个表达式(不能包含语句,如 while)([04:13]),并且 def 语句会赋予函数一个“固有名称”(intrinsic name),而 lambda 表达式创建的是匿名函数,其名称绑定完全依赖于赋值语句([05:37])。
按照主题来梳理
主题一:什么是 Lambda 表达式及其限制
在 Python 中,lambda 表达式是一种特殊的语法,它允许你创建并“返回”一个函数对象,这一切都发生在一个表达式中。
-
核心目的:将函数作为值来处理
视频一开始提出了一个问题:我们已经知道如何用赋值语句将一个值(比如数字 10)绑定到一个名称(比如 x)上,即 x = 10(00:13)。那么,我们能否用类似的语法,将一个 函数 绑定到一个名称上呢?(00:23)
-
一个失败的尝试
一个直观但错误的尝试是,如果我们想创建一个名为 square(平方)的函数,我们可能会尝试写
square = x * x(00:42)。但问题在于,Python 在执行这句赋值时,会立即 计算 (evaluate) 右侧的x * x。在视频的上下文中,x 已经被绑定为 10,所以x * x的计算结果是 100(00:56)。因此,square 这个名称最终被绑定到了数字 100 上,而不是一个函数(00:48)。 -
Lambda 表达式的解决方案
lambda 表达式正是为了解决这个问题。它允许我们实现最初的设想:使用赋值语句,将一个新创建的函数绑定到一个名称上(01:10)。
正确的语法是:square = lambda x: x * x(01:18)。
-
lambda关键字:这个关键字标志着一个lambda表达式的开始,告诉 Python “我正在定义一个函数”([02:29])。 -
x:这是函数的formal parameter(形式参数)([02:41])。它可以是任何合法的变量名。 -
:(冒号):分隔参数和函数体。 -
x * x:这是函数体,也是函数的return expression(返回表达式)([03:13])。
当你执行这行代码时,
lambda x: x * x这一部分会首先被 计算,其计算的 结果 是一个函数对象([01:29])。然后,赋值语句 (=) 将这个函数对象绑定到名称square上。此时,square就成了一个真正的函数,我们可以像调用其他函数一样调用它,例如square(4)会返回16([01:35])。 -
-
关键限制:单一表达式
lambda 表达式有一个非常重要的特性:它没有 return 关键字(03:05)。你不需要(也不能)写 lambda x: return x * x。在冒号之后的部分 只能 是一个单独的表达式(03:13)。
这个表达式的计算结果就是函数的返回值。这个设计极大地限制了 lambda 函数的复杂性。它们只能用于创建“简单的函数”,这些函数除了计算并返回一个表达式的结果外,什么也不做(03:31)。
因此,lambda 表达式中 不能包含语句(statements)(04:02)。例如,你不能在 lambda 函数体内使用 while 循环语句(04:19)。如果你需要定义包含循环、条件判断(if 语句,而非 if 表达式)或多个步骤的复杂函数,你必须使用 def 语句(04:24)。
-
匿名使用
由于 lambda 是一个表达式,你甚至不需要将它绑定到任何名称上就可以立即使用它。视频中展示了 (lambda x: x * x)(10) 这样的用法(01:58)。在这个调用表达式(call expression)中,操作符(operator)本身就是 lambda 表达式(它评估为一个函数),然后这个函数被立即调用,参数是 10。
主题二:Lambda 表达式 vs. Def 语句
视频用了一个“Face off time”(对决时刻)来详细比较 lambda 和 def([04:28])。
-
对比案例:
-
二者的相同点
视频强调,在绝大多数情况下,这两者是“几乎完全相同”的(04:50)。
-
二者的关键区别
尽管功能几乎一致,但它们在创建和命名机制上存在细微但重要的差别。
1. 绑定名称的方式不同([05:05])
-
lambda:这是一个两步过程。首先,lambda表达式本身被评估,创建 了一个 没有名字 的函数对象([05:13])。然后,assignment statement(赋值语句,即=)将这个函数对象 绑定 到了square这个名称上([05:20])。 -
def:def语句是一个复合操作。它会 自动 完成创建函数和绑定名称两个步骤([05:29])。def语句在执行时,会创建一个函数对象,并 立即 将它绑定到def语句中指定的名称(square)上。
- 固有名称(Intrinsic Name)的区别(05:37)
这是上述绑定机制差异导致的直接后果。
-
def:def语句会赋予函数一个“固有名称”。如果你在 Python 环境中定义了def square...,然后你查看square变量,它的显示会是<function square ...>([06:10])。这个square就是它的固有名称。 -
lambda:lambda表达式创建的是 匿名函数。当你查看通过lambda绑定的square变量时,它的显示会是<function <lambda> ...>([05:54])。它没有一个叫square的固有名称,它只有一个占位符<lambda>。
- 在环境图(Environment Diagram)上的体现(06:37)
这个“固有名称”的差异在可视化执行过程的环境图中体现得最清楚:
-
def的情况:当我们执行def square...时,在全局帧(global frame)中,名称square会指向一个函数对象。这个函数对象 内部 会被标记为square([07:43])。当我们调用square(4)时,系统会创建一个新的帧(frame)来执行函数体,这个新帧也会被标记为square([07:51])。 -
lambda的情况:当我们执行square = lambda...时,在全局帧中,名称square同样会指向一个函数对象([06:57])。但是,这个函数对象 内部 不会被标记为square,而是被标记为希腊字母lambda(λ)([07:17])。当我们调用square(4)时,创建的新帧也会被标记为lambda([07:24])。
视频强调,这只是一个“微小的差异”([07:58]),在实际代码执行中几乎没有影响。它只是反映了函数被创建时的“历史”:
def创建时就有名字,而lambda直到赋值完成才获得一个指向它的名字([08:05])。 -
框架 & 心智模型 (Framework & Mindset)
心智模型:函数是“一等公民” (First-Class Citizens)
本视频(特别是 Lambda 表达式)最核心的心智模型,是帮助理解 Python 中“函数是一等公民”这一概念。
“一等公民”(或“一等对象”)在编程语言中意味着某个“事物”(比如函数)与其他“事物”(比如数字、字符串)具有同等的地位。它们可以被:
-
存储在变量中(即被名称绑定)。
-
作为参数传递给其他函数。
-
作为其他函数的返回值。
这个8分钟的视频通过 lambda 表达式,完美地展示了第1点。
-
将函数视为“值”
视频的开场(00:13)建立了一个基准:x = 10。这是一个赋值语句,它的工作方式是:右侧的 10 是一个 value(值),左侧的 x 是一个 name(名称),= 运算符将这个名称绑定到这个值。
视频中的“失败尝试”
square = x * x([00:48])强化了这一点。这里的核心问题是,x * x也是一个 表达式,它被 Python 立即求值(evaluated)为100。所以square被绑定到了100这个值上。 -
Lambda:产生“函数值”的表达式
lambda 表达式的革命性在于,它也是一个 表达式,但它求值(evaluate)后产生的结果不是一个数字或字符串,而是一个 函数对象(02:29)。
lambda x: x * x当 Python 看到这个表达式时,它不会去计算
x * x,因为它知道这是一个lambda。相反,它会“打包”这个逻辑(“接受一个参数x,并返回x*x”),并创建一个代表这个逻辑的“函数对象”。 -
统一的赋值模型
有了这个心智模型,赋值操作就变得完全一致了:
-
x = 10-
右侧是一个字面量表达式 (literal expression),其值为
10。 -
名称
x被绑定到值10。
-
-
square = lambda x: x * x-
右侧是一个 Lambda 表达式 (lambda expression),其值为一个函数对象([01:10])。
-
名称
square被绑定到这个函数对象。
-
lambda表达式允许我们将函数无缝地整合到 Python 的变量赋值系统中。这与def语句形成了鲜明对比。def square(x): ...是一个statement(语句),而不是一个expression(表达式)。语句是用来 执行动作 的(比如创建函数并绑定名称),而表达式是用来 计算值 的。lambda的精髓在于它是一个 表达式,这就是为什么它能出现在def语句不能出现的地方(例如,作为另一个函数的参数)。 -
-
匿名性:解耦“创建”与“命名”
这个心智模型还引出了“匿名性”的概念。
-
def语句是 原子性 的:它在一条语句中同时完成了“创建函数”和“命名函数”两件事([05:29])。 -
lambda表达式将这两件事解耦了。lambda x: x * x只负责“创建函数”([05:13])。它创建的函数是匿名的(在视频的环境图中用 λ 表示)([07:17])。 -
你(程序员)可以选择是否以及如何命名它。
因此,
lambda的心智模型是:它是一个“函数工厂”表达式,其唯一的(且受限的)工作就是制造一个简单的、匿名的函数对象,你可以像对待任何其他值(如10)一样对待这个对象——将它存入变量、传递它,或者立即“消费”它。 -
Return
Overview
这篇内容的核心论题是,Python 中的 return 语句并不仅仅是函数用来“返回一个值”的工具;它更是一种强大且关键的控制流机制 [00:00]。它的核心功能在于立即终止当前函数的执行,并将控制权交还给调用方。视频的结论是,通过利用 return 的这种立即终止特性,我们可以将其与循环(甚至是刻意构造的无限循环)相结合,用一种看似简单却极其有效的方式来实现复杂的逻辑,例如实现“搜索”算法——即找到第一个满足特定条件的元素,乃至实现一个通用的“反函数”计算框架 [05:49]。
按照主题来梳理
函数的基石:return 语句的运作机制
在 Python(以及许多其他编程语言)中,return 语句是理解函数如何工作的核心。我们常常将其简单地理解为“给出一个结果”,但它的实际机制远比这更深刻,它直接关系到程序执行的流程和环境的切换。
-
return 的双重使命
return 语句有两个职责:第一,它标志着一个函数调用的结束;第二,它确定了调用该函数的表达式(call expression)的最终值 00:00。当我们编写如 result = f(x) 这样的代码时,我们真正在乎的是 f(x) 这个表达式的值是什么。return 语句就是用来回答这个问题的。
-
函数调用的生命周期
要理解 return,我们必须先理解调用一个用户自定义函数(user-defined function)时发生了什么 00:15:
-
发起调用:当程序执行到
f(x)这样的调用表达式时,它会暂停在当前环境(我们称之为“调用方环境”)中的执行。 -
创建新环境:程序会创建一个全新的、隔离的“本地环境”,用于执行函数
f的主体(body)代码。f的参数(在这个例子中是x)会在这个新环境中被赋值。 -
执行主体:程序开始逐行执行函数
f内部的代码 [00:23]。 -
等待终止:这个执行过程会一直持续下去,直到遇到两种情况之一:要么是执行到了函数体内的某条
return语句,要么是执行到了函数体的末尾(即所有代码都运行完了)。
-
-
return 触发的“返回”过程
当函数执行过程中遇到一条 return 语句时(例如 return x + 1),一个关键的切换发生了 00:49:
-
立即终止:函数体中位于
return语句之后的任何代码都将被跳过。函数的执行在这一刻被立即终止 [01:24]。这是return最核心的控制流特性。一个函数体中可能有很多条return语句(例如在不同的if分支里),但只要有任何一条被执行,整个函数就结束了。 -
计算返回值:程序会计算
return关键字后面的表达式(即x + 1)的值。 -
销毁环境:为该函数调用创建的那个“本地环境”被销毁。
-
交还控制权:程序执行流程“跳回”到之前的“调用方环境”中,回到它当初暂停的地方。
-
赋值:
f(x)这个调用表达式本身,现在“变成”了return语句计算出的那个值 [00:55]。如果代码是result = f(x),那么这个值现在就被赋给了result;如果代码是print(f(x)),这个值就会被传递给print函数 [01:03]。
-
如果函数执行到末尾都没有遇到 return 语句,Python 会隐式地执行一个 return None。None 是一个特殊的对象,代表“没有值”。这解释了为什么一个只打印(print)而不返回(return)的函数,在被赋值时,结果总是 None。
实践应用:使用 return 提前退出循环
return 的“立即终止函数”特性,使其成为控制循环(尤其是 while 循环)的强大工具。它不是“退出循环”(像 break 语句那样),而是“退出整个函数”,这在很多情况下是更简洁、更直接的解决方案。
-
问题场景:寻找特定数字
视频中提出了一个具体问题:编写一个函数,它接收一个非负整数 n 和一个数字 d,要求从 n 的个位数开始,反向逐个打印数字,直到打印到 d 为止 01:32。
- 示例:对于
n = 34567和d = 5,函数应该打印7,然后打印6,然后打印5。当它找到5时,它就应该停止,不应该再继续打印4和3[01:40]。
- 示例:对于
-
常规循环的局限
一个常规的 while 循环可以用来打印所有数字 02:00。逻辑如下:
-
当
n大于 0 时,持续循环。 -
last = n % 10(获取n的最后一位数字,即个位数)。 -
n = n // 10(通过整除 10 来“砍掉”n的最后一位)。 -
print(last)(打印出最后一位)。
- 问题:如果
n = 34567,这个循环会打印 7, 6, 5, 4, 3。它无法在找到 5 之后停下来。
-
-
return 的解决方案
我们可以利用 return 来解决这个问题。我们在循环内部增加一个条件判断 [02:19]:
1
2
3
4
5
6
7def print_reverse_until(n, d):
while n > 0:
last = n % 10
n = n // 10
print(last)
if last == d: # 检查当前打印的数字是否是我们要找的 d
return None # 或者仅仅是 'return'-
工作原理:
-
循环开始,打印 7。
7 == 5不成立,循环继续。 -
循环继续,打印 6。
6 == 5不成立,循环继续。 -
循环继续,打印 5。
5 == 5成立,if条件为真。 -
程序执行
return None语句 [02:34]。 -
return语句立即终止print_reverse_until整个函数的执行。 -
while循环(以及函数体内的任何其他代码)因为函数已经终止,所以自然也就停止了。
-
-
关键点:我们不需要引入额外的“标志变量”(flag variable,例如
found_d = False)来控制while循环的条件。return提供了一种更清晰、更直接的方式来表达“我们的工作已经完成,请立即停止”的意图 [02:41]。
-
高级应用:在无限循环中使用 return 实现搜索
return 的控制能力在与“无限循环”结合时,展现出了更强大的威力。这是一种非常常见的编程模式,用于实现各种“搜索”算法。
-
“搜索”的抽象定义
视频定义了一个名为 search(f) 的高阶函数(higher-order function,即接收函数作为参数的函数)02:57。
-
使用 while True: 和 return 实现搜索
search(f) 函数的实现巧妙地利用了一个无限循环:
1
2
3
4
5
6
7def search(f):
x = 0 # 从第一个非负整数 0 开始
while True: # 这是一个刻意构造的“无限循环” [00:03:32]
if f(x): # 检查 f(x) 的结果是不是一个“真值”
return x # 如果是,立即返回当前的 x,函数终止 [00:03:41]
else:
x = x + 1 # 如果不是,尝试下一个 x [00:03:48]- 执行流程:这个循环会永远运行下去,除非
if f(x):这条语句为真。一旦为真,return x语句就会被执行,这不仅“跳出”了while True循环,更是终止了search函数,并把x作为结果返回。
- 执行流程:这个循环会永远运行下去,除非
-
示例 1:找到 3
-
示例 2:找到使 x*x - 100 为正数的最小 x
这是一个更复杂的例子,它依赖于“真值”不仅仅是 True 这一特性。
-
定义一个函数
positive(x),它返回max(0, x*x - 100)[04:37]。 -
分析
positive(x)的行为: -
调用
search(positive)。 -
search内部开始循环:-
x = 0,positive(0)返回0(假值)。x变为 1。 -
…
-
x = 10,positive(10)返回0(假值)。x变为 11。 -
x = 11,positive(11)返回21。 -
21是一个非零数字,所以它是一个“真值” [05:19]。if条件满足。 -
return 11被执行。search函数终止,返回值是 11。
-
-
结论:我们通过这个通用的
search函数,找到了一个与 100 的平方根(10)紧密相关的数字(11)[05:24]。我们实际上实现了一种通过“暴力搜索”(brute force search)来求解问题的方法 [04:29]。
-
框架 & 心智模型 (Framework & Mindset)
框架:「无限循环 + 条件返回」搜索模式 (The “Infinite Loop + Return” Search Pattern)
这个视频的核心思想可以被抽象为一个强大且通用的编程框架:“无限循环 + 条件返回”模式。这是一种用于解决“找到满足条件的第一个元素”这类问题的标准模式。
-
核心心智模型:
我们应该克服对“无限循环”(while True:)的恐惧。while True: 本身并不是一个错误,它是一种有用的工具,它清晰地表达了“持续执行,直到我明确告诉你停止”的意图 03:32。在这个模式中,return 语句就是那个“明确的停止信号”。这种模式将“如何推进搜索”和“何时停止搜索”这两个关注点完全分离开来。while 循环只负责推进,而 if 和 return 语句只负责判断和停止。
-
框架的构成步骤:
-
初始化状态 (Initialize State):设置搜索的起点。在视频的例子中,这就是
x = 0[03:18]。在其他问题中,它可能是一个指针、一个索引、或者一个队列中的第一个元素。 -
启动无限循环 (Create Infinite Loop):使用
while True:语句块,表明搜索过程将无限持续下去,直到找到解。 -
检查条件 (Check Condition):在循环体内部,使用
if语句检查当前状态是否满足我们的“成功条件”。例如if f(x):[03:41] 或if last == d:[02:25]。 -
成功则返回 (Return on Success):如果条件满足,立即使用
return语句返回当前状态(即我们的“解”)[03:41]。return会强制终止循环和函数,确保我们返回的是_第一个_满足条件的解。 -
推进状态 (Advance State):如果条件_不_满足,那么更新状态,为下一次循环做准备。例如
x = x + 1[03:48] 或n = n // 10[02:13]。
-
-
框架的变体:
视频最后还展示了这种模式的一个更紧凑的变体 07:24。search 函数也可以被写成:
1
2
3
4
5def search_alternative(f):
x = 0
while not f(x): # 循环的条件是“当 f(x) 还是假值时”
x = x + 1
return x # 循环一旦停止,就意味着 f(x) 已经是真值了- 对比:这个变体在逻辑上是等价的,它把“成功条件”放到了
while循环的判断中。第一个版本(while True:)在语义上可能更清晰地表达了“搜索”的意图:循环体负责“检查与推进”,而return负责“终止”。而这个变体则更符合“循环直到…”(loop until…)的语义。选择哪一种取决于个人风格和问题的复杂度。
- 对比:这个变体在逻辑上是等价的,它把“成功条件”放到了
框架:通过「搜索」实现「反函数」 (Computing Inverse Functions via Search)
视频的第二个重要框架,是展示了如何将第一个“搜索”框架进行封装和抽象,用来解决一个更高级的数学问题:计算一个函数的“反函数”(Inverse Function)。
-
核心心智模型:
什么是反函数?如果我们有一个函数 f(例如 square,平方),它的反函数 g(例如 sqrt,平方根)应该能“撤销”f 的操作。更准确地说,如果我们有一个值 y(例如 256),我们想找到一个 x,使得 f(x) = y(即 square(x) = 256)。那个 x(即 16)就是 g(y)(即 sqrt(256))的结果 06:21。
- 关键洞察:“找到一个
x使得f(x) = y” 这句话,本质上就是一个搜索问题!
- 关键洞察:“找到一个
-
框架的构成步骤:
我们可以构建一个高阶函数 inverse(f),它接收一个函数 f,并返回它的反函数 g 05:49。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21def inverse(f):
# 1. 定义那个即将被返回的“反函数” g
# g 需要一个参数 y (我们想要“撤销”到的值)
def g(y):
# 2. g 的工作是“找到 x”,它如何找?
# 它使用我们之前构建的 search 框架!
# 3. 定义 search 需要的“条件函数”。
# 这个条件函数接收一个 x,
# 它需要检查 f(x) 是否等于我们要找的 y
def condition(x):
return f(x) == y
# 4. 调用 search,并把这个“条件函数”传给它
# search 会返回第一个满足 f(x) == y 的 x
# (视频中使用了 lambda 表达式来内联地定义这个 condition)
# return search(condition)
return search(lambda x: f(x) == y) [00:06:21]
# 5. inverse 函数的最终工作,就是返回 g 这个函数对象
return g -
框架的应用:实现 sqrt
现在我们可以用这个框架来“制造”平方根函数,而不需要知道平方根的任何数学知识,只需要知道“平方”是什么:
-
定义原函数
f:def square(x): return x * x -
制造反函数
g:sqrt = inverse(square)[06:30]-
inverse(square)调用返回了上面定义的g函数,并将g赋值给sqrt。 -
重要的是,此时
g函数“记住”了它需要使用的f就是square。
-
-
使用反函数:当我们调用
sqrt(256)时 [06:40]:-
这实际上是在调用
g(256)(y = 256)。 -
g内部开始执行search(lambda x: square(x) == 256)。 -
search开始搜索:-
x=0,square(0) == 256? 否。 -
…
-
x=15,square(15) == 256? 否。 -
x=16,square(16) == 256? 是。
-
-
search返回16。 -
g(256)返回16。 -
sqrt(256)的调用者得到结果16。
-
-
-
框架的局限性:
这个基于整数搜索的 inverse 框架是简单而强大的,但它有其局限性 06:55。它只能找到使 f(x) == y 成立的非负整数 x。如果我们尝试 sqrt(2),search 会去寻找一个整数 x 使得 x * x == 2。由于不存在这样的整数,search 函数将陷入它自己的 while True 无限循环中,永远不会返回 07:03。视频也提到,要解决这个问题需要更高级的方法(例如牛顿法,Newton’s method)07:10,但这已超出了 return 语句本身的主题。
Control
Overview (概述)
本视频的核心论题在于论证为什么现代编程语言(如 Python)必须包含 if 这样的控制语句 (Control statements),而不能仅仅依赖函数调用表达式 (Call expressions) 来实现逻辑分支。视频得出的结论是:控制语句是不可或缺的,因为它们提供了选择性执行 (Selective Execution) 的能力,即根据条件只评估和执行两个或多个代码块中的一个。相比之下,函数调用表达式遵循“全部评估” (Eager Evaluation) 的规则,在函数体执行 之前 就必须计算出 所有 参数的值。视频通过一个 real_sqrt(实数平方根)的实例,清晰地展示了如果试图用函数来模拟 if 语句,将在处理如 sqrt(-4) 这样的无效输入时导致程序崩溃,而 if 语句则能通过跳过无效计算来完美规避这个问题。
按照主题来梳理
核心问题:为什么我们需要 if 语句?
视频一开始便提出了一个具有挑战性的问题:在课程的早些时候,曾提到“调用表达式 (Call expressions) 是我们唯一需要的表达式类型”[00:00]。但事实证明,这是不完整的 [00:10]。程序设计还需要一个至关重要的概念,那就是控制 (Control) [00:13]。
控制语句,主要包括 if 语句和 while 语句,其核心作用是描述程序在执行过程中“接下来应该发生什么”[00:26]。我们不仅要学习它们如何工作,更要深入探究一个根本问题:它们为什么必须存在?[00:34]
为了解构这个问题,视频引导我们进行一个思想实验:我们能否编写一个函数,来完全替代 if 语句的功能? [00:43]
我们来聚焦于 if 语句最常见的形式:一个 if 子句 (if Clause) 加上一个 else 子句 (else clause) [00:51]。在 Python 中,这样的一个完整结构被称为一个“条件语句 (conditional statement)”[00:59],它由多个部分组成:
-
子句 (Clauses):在这个例子中,包含
if子句和else子句 [01:21]。 -
头部表达式 (Header expression):
if语句后面跟着的那个用于判断的条件(例如x > 0),else子句则没有头部表达式 [01:08]。 -
套件 (Suite):在 Python 中,指代跟在冒号后面、被缩进的代码块 [01:38]。我们有
if套件和else套件。
这个结构的核心价值在于其独特的执行规则 (execution rule) [00:59]:
-
程序按顺序考量每个子句。
-
它首先评估
if子句的头部表达式。 -
如果该表达式的值为真 (a true value) [01:15],程序就 执行 其对应的
if套件,并且(这是关键)跳过 (skip) 所有剩余的子句(即else块)[01:21]。 -
如果
if的头部表达式为假,程序跳过if套件,移动到else子句,并 总是 执行else套件(如果它存在的话)[02:01]。
这个规则确保了一个根本事实:if 套件和 else 套件 永远只有一个会被执行 [01:54]。
理解了 if 语句的语法和规则后,我们回到那个挑衅性的问题 [02:07]:为什么 Python 语言需要设计这样一套独立的语法(冒号、缩进等),而不是提供一个简单的函数呢?[02:13] 我们可以想象一个(不存在的)if_function [02:40]:
if_function(predicate, consequent, alternative)
-
predicate(谓词):代表条件。 -
consequent(结果):代表条件为真时要执行的表达式。 -
alternative(替代):代表条件为假时要执行的表达式。
这个函数实现起来似乎非常直接 [02:40]:
1 | def if_function(predicate, consequent, alternative): |
如果能这样实现,那么所有的程序逻辑都可以统一为函数调用表达式,代码会变得更简洁,甚至可以在一行内写完,免去了记住缩进和冒号的麻烦 [02:59, 03:11]。那么,为什么 Python(以及几乎所有主流语言)没有 提供这样的函数呢?答案在于函数调用表达式自身的一个根本局限。
根本局限:函数调用 (Call Expression) 的评估规则
视频指出,那个看似完美的 if_function 之所以不存在,完全是因为函数调用表达式的评估规则 (evaluation rule for call expressions) [03:14]。
当我们执行一次函数调用时,比如 if_function(x > 0, sqrt(x), 0),计算机的执行步骤 [03:24] 如下:
-
评估操作符 (Operator):首先,评估
if_function这个名字本身,找到它所代表的函数定义。 -
评估所有操作数 (Operands):在 调用 函数体内的代码 之前 [03:33],程序必须准备好传递给函数的所有参数(即操作数)。这意味着它会 无条件地、从左到右 评估所有三个参数表达式:
-
评估
x > 0 -
评估
sqrt(x) -
评估
0
-
-
应用 (Apply):只有在 所有 参数都评估完毕,获得了它们的 值 之后 [03:41],程序才会把这些值(例如
False,[一个错误],0)传递给函数,并开始执行函数体内的代码 [03:48]。
这个规则——先评估所有参数,再执行函数体——就是函数调用的核心特征。
现在,我们对比一下 if 语句和 if_function 的差异:
-
if语句:在其语法结构中,sqrt(x)和0这两个表达式,只有一个会被 执行 (executed) [03:52]。 -
if_function:在其函数调用中,sqrt(x)和0这两个表达式,两者都 会被 评估 (evaluated) [04:03],然后才轮到函数体内的if开始工作。
这个差异在大多数时候可能无伤大雅,但在某些关键场景下,它会导致灾难性的后果。
决定性反例:real_sqrt (实数平方根) 函数
为了证明“执行”与“评估”之间的差异是致命的,视频构建了一个非常实用且清晰的案例:计算一个数的平方根的实部 (real part of the square root) [04:43]。
在 Python 中,内置的 math.sqrt 函数无法处理负数。如果你尝试计算 sqrt(-4),它会立即抛出一个 math domain error (数学域错误) [04:13, 04:21]。
然而,在数学中,负数的平方根是存在的,它们是虚数 (imaginary numbers) [04:34]。例如,-4 的平方根是 2i。如果我们只关心这个结果的“实部”,那么 -4 的平方根的实部就是 0 [05:09]。
我们的任务是定义一个 real_sqrt(x) 函数,它能做到:
-
如果
x是正数(如 4),返回其平方根(即 2)。 -
如果
x是负数(如 -4),返回其平方根的实部(即 0)[05:01]。
实现版本一:使用 if 控制语句 [05:01]
我们首先使用 Python 的标准 if 语句来编写这个函数:
1 | from math import sqrt |
我们来测试这个函数:
-
real_sqrt_if(4):程序检查4 > 0,结果为真。它进入if套件,执行return sqrt(4),返回 2。else套件被 跳过。程序正常工作 [05:24]。 -
real_sqrt_if(-4):程序检查-4 > 0,结果为假。它 跳过if套件(因此sqrt(-4)永远不会被执行),移动到else子句,执行return 0,返回 0 [05:24]。
这个函数完美地完成了任务。if 语句的控制能力是保护程序免于崩溃的关键 [05:43]:它确保了只有在 x 大于 0 的安全情况下,sqrt(x) 才会被 执行 [05:51]。
实现版本二:使用(假设的)if_function [05:59]
现在,我们尝试用那个“更简洁”的 if_function 来实现 完全相同 的逻辑 [06:09]:
1 | # 假设 if_function 存在 |
我们来测试这个版本:
-
real_sqrt_func(4):程序评估real_sqrt_func(4)。根据函数调用规则,它必须先评估所有参数:4 > 0(得True),sqrt(4)(得2.0),0(得0)。然后它调用if_function(True, 2.0, 0)。函数体内的if判断True为真,返回2.0。程序正常工作 [06:30]。 -
real_sqrt_func(-4):程序评估real_sqrt_func(-4)。根据函数调用规则,它必须先评估所有参数 [06:48]:
尽管我们的 意图 是“如果 x 不大于 0,就只使用 0”,但函数调用表达式的“全部评估”规则使得 sqrt(-4) 在 if_function 有机会做出判断 之前 就被强制执行了 [06:37]。
结论:控制语句的不可替代性
这个 real_sqrt 案例有力地证明了 if 语句的不可替代性。
函数调用表达式只能在不同的 值 (values) 之间进行选择(例如在 2.0 和 0 之间),但在做出选择之前,这些值必须已经存在(即已经被计算出来)[07:14]。
而 if 这样的控制语句 (Control statements),它提供了一种更强大的能力:它可以在不同的 表达式 (expressions) 或 计算路径 之间进行选择,并且 只评估 (evaluate) 被选中的那一个 [07:06]。
这就是为什么 Python 语言(以及 C, Java, Lisp 等几乎所有语言)必须区分“函数调用”和“条件语句”。控制语句的核心价值在于它们能够 跳过 (skip) 或 重复 (repeat,例如 while 循环) 某些代码块,而这是函数调用表达式(它总是评估其所有组件)在根本上无法做到的 [07:37, 07:46]。
框架 & 心智模型 (Framework & Mindset)
1. 程序评估心智模型:调用表达式 (Eager Evaluation) vs 控制语句 (Selective Execution)
这个视频的核心是对比了两种截然不同的程序执行心智模型。理解这两种模型的根本差异,是理解为什么编程语言如此设计的关键。
模型 A:调用表达式 (Call Expression) 的“全部评估” (Eager Evaluation) 模型
“Eager Evaluation”(也可称为“急切求值”或“严格求值”)是大多数编程语言中函数调用的默认行为。其核心心智模型是**“先准备好所有材料,再开始工作”**。
-
工作流程:
-
识别任务:程序遇到一个函数调用,如
f(A, B, C)[03:24]。 -
准备所有参数:在真正开始执行
f函数体内的任何指令 之前,程序必须首先获得A,B,C三个参数的 值。 -
强制评估:为了获得值,程序必须 立即、无条件地 评估
A,B,C这三个表达式 [03:33]。-
如果
A是1+1,它被评估为2。 -
如果
B是sqrt(4),它被评估为2.0。 -
如果
C是sqrt(-4),它被评估… 然后程序在此刻崩溃 [06:57]。
-
-
应用函数:只有当 所有参数都成功评估出结果后,程序才会将这些结果(例如
2,2.0和…哦,程序已经崩溃了)作为 值 传递给f函数,并开始执行f内部的逻辑 [03:41]。
-
-
real_sqrt 案例分析:
当我们尝试 if_function(x > 0, sqrt(x), 0) [05:59] 时,我们是站在程序员的角度思考:“我希望这个函数能帮我选择 sqrt(x) 或 0”。但计算机是站在“全部评估”模型的角度执行:“我收到了一个 if_function 的调用任务。我的规则是,必须先准备好它的三个参数。”
当 x 为 -4 时,它的评估列表是:
-
x > 0->False(值) -
sqrt(x)->sqrt(-4)->Math Domain Error(错误) -
0 -> 0 (值)
评估过程在第二个参数处就失败了 [06:48]。if_function 内部的 if … else … 逻辑根本没有机会运行,因为它在等待它的参数值,而其中一个参数值的 计算过程本身 就失败了。这个模型无法处理“这个参数可能根本不应该被计算”的情况。
-
模型 B:控制语句 (Control Statement) 的“选择性执行” (Selective Execution) 模型
“Selective Execution”(选择性执行,或“条件执行”)是 if-else 等控制语句提供的核心心智模型。其核心理念是**“先检查条件,再决定走哪条路”**。
-
工作流程:
-
遇到岔路口:程序遇到
if语句。它不把后面的代码块(套件)当作需要立即评估的参数 [00:59]。 -
评估“路标”:它只做一件必要的事:评估
if旁边的 头部表达式(条件)[01:08],例如x > 0。 -
做出选择:根据上一步评估得到的布尔值(
True或False),程序做出 路径选择 [01:15]。-
如果为
True:选择if套件 (Suite) 所在的路径。 -
如果为
False:选择else套件所在的路径(或者elif,或什么也不做)。
-
-
执行并忽略:程序 只 进入被选中的路径(套件)并执行其中的代码。最关键的是,所有其他路径(套件)被 完全跳过 (skip) [01:21]。它们内部的代码 永远不会被评估或执行。
-
-
real_sqrt 案例分析:
当我们使用 if x > 0: … else: … [05:01] 时,计算机是站在“选择性执行”模型的角度工作。
当 x 为 -4 时:
-
遇到岔路口:
if语句。 -
评估“路标”:评估
x > 0(即-4 > 0),得到False[05:37]。 -
做出选择:因为条件为
False,程序选择else路径。 -
执行并忽略:程序 完全跳过 if 套件。return sqrt(x) 这行代码仿佛从一开始就不存在于这次执行中 [05:51],因此 sqrt(-4) 永远不会被评估。程序进入 else 套件,执行 return 0。
程序安全、正确地返回了 0 [05:24]。
-
-
总结:
“全部评估”模型(函数调用)在 值 的层面上操作,它要求所有值必须先准备好。“选择性执行”模型(控制语句)在 执行流 (Control Flow) 的层面上操作 [07:06],它允许程序 选择性地跳过 那些可能产生无效值或错误的代码块。这就是为什么 if 是语言的基石,而不是一个库函数 [07:28]。
Control Expressions
Overview
本视频的核心论题是介绍 Python 等编程语言中的“控制表达式”(Control Expressions),特别是逻辑运算符 and(与)和 or(或)所展现的“短路求值”(Short-circuiting)特性 [00:09]。视频的结论是,这种短路行为是一种非常有用的控制形式 [04:58],它允许程序员通过有策略地安排表达式的求值顺序,来避免潜在的运行时错误(例如程序崩溃),从而编写出更健壮和安全的代码。
按照主题来梳理
1. 揭秘 “and” 与 “or” 的短路求值 (Short-Circuiting)
视频首先指出,Python 中存在一些特殊的表达式,它们允许解释器在某些条件下“跳过”对部分子表达式的求值 [00:00]。逻辑运算符 and 和 or 就是这种行为的典型代表,这种行为被称为“短路”(Short-circuiting)[00:09]。
and 运算符的求值规则 [00:21]
当 Python 解释器遇到一个 left and right(左表达式 与 右表达式)的结构时,它并不会立即计算两边的值,而是遵循一个严格的顺序:
-
步骤一:求值左表达式。 解释器首先计算
and左侧的子表达式 [00:28]。 -
步骤二:检查左侧结果。
-
步骤三:返回最终值。
-
如果发生了短路(左侧为假),则返回左侧的假值。
-
如果没有短路(左侧为真),则返回右侧表达式的计算结果 [00:37]。
-
视频特别强调,这里的“真值”和“假值”并不仅仅指布尔值 True 和 False [00:56]。在 Python 中,任何值都可以被视为“真值”或“假值” [01:02]。例如,0、空字符串 ""、空列表 [] 都是假值;而像 2、3、非空字符串(如 “hello”)等都是真值。因此,视频中举例 2 and 3 [01:02],2 是一个真值,所以 Python 必须继续计算右侧,最终整个表达式的值是右侧的 3。
or 运算符的求值规则 [01:12]
or 运算符的逻辑与 and 相反,但同样遵循短路原则:
-
步骤一:求值左表达式。 解释器首先计算
or左侧的子表达式 [01:12]。 -
步骤二:检查左侧结果。
-
步骤三:返回最终值。
-
如果发生了短路(左侧为真),则返回左侧的真值。
-
如果没有短路(左侧为假),则返回右侧表达式的计算结果 [01:27]。
-
这种看似微小的求值顺序差异,实际上为程序控制提供了强大的能力,尤其是在处理可能引发错误的操作时 [01:36]。
2. “短路” 的实战应用:避免程序崩溃
视频接着通过两个具体的编程实例,展示了短路求值在实践中为何如此有用 [01:36]。
应用一:and 的“前置守卫”
-
场景设定: 假设我们需要编写一个函数
has_big_square_root(x),用于判断一个数字x的平方根是否大于 10 [01:36]。一个直接的想法是return sqrt(x) > 10[01:43]。 -
潜在风险: 这个实现在
x为正数时工作良好(例如x=1000)[02:18]。但是,如果x是一个负数(例如x=-1000),尝试计算sqrt(-1000)会导致程序崩溃(在某些数学库中会引发ValueError)[01:59]。视频中提到,我们可能只关心平方根的实部 [02:05],但即便如此,直接的计算依然存在风险。 -
解决方案: 利用
and的短路特性,我们可以将代码修改为:return x > 0 and sqrt(x) > 10[02:10]。 -
工作原理:
应用二:or 的“例外优先”
-
场景设定: 假设我们要定义一个函数
is_reasonable(n),用于判断一个数n是否“合理” [03:42]。这里的“合理”定义为:1 / n的结果不等于0。这个定义的背景是,当n变得极大(如10的1000次方)时,1 / n的结果会因为精度限制而被“舍入”(Rounded)为0[03:14],我们认为这种极大数是“不合理”的 [03:22]。 -
潜在风险: 一个直接的想法是
return 1 / n != 0[03:59]。这个实现在处理大数字时(如10的100次方)是有效的 [03:07]。但是,如果n恰好等于0,1 / 0操作会导致程序因“除零错误”(ZeroDivisionError)而崩溃 [04:05]。而视频指出,0本身应该被视为一个“合理”的数字 [04:05]。 -
解决方案: 利用
or的短路特性,我们将代码修改为:return n == 0 or 1 / n != 0[04:12]。 -
工作原理:
-
当
n = 0时,解释器首先计算or左侧的n == 0,结果为True[04:19]。 -
根据
or的短路规则,由于左侧为“真值”(True),解释器 立即停止 求值,将True作为整个表达式的最终结果返回 [04:50]。 -
关键在于,右侧的
1 / n != 0(即1 / 0 != 0) 根本没有被执行 [04:44]。 -
通过这种方式,
n == 0充当了一个“例外情况”(Exception Case)的优先处理。它在“除零”这个风险操作发生之前将其拦截,确保了n=0时函数能安全返回True[04:28],同时也正确处理了n为极大数时(如10的10000次方)返回False的情况 [04:36]。
-
框架 & 心智模型 (Framework & Mindset)
从视频的讲解中,我们可以抽象出两个关键的编程框架和心智模型:
1. “防御性” 顺序求值框架 (Defensive Sequential Evaluation Framework)
这个框架的核心思想是:在编写逻辑表达式时,不仅仅要考虑最终的逻辑真假,更要考虑表达式的 求值顺序 及其 副作用(Side Effects),特别是那些可能导致错误的副作用。 短路求值为我们提供了一种低成本、高效率的“防御性编程”手段。
框架应用一:and 的“前置条件” (Pre-condition) 检查
-
结构:
[安全的前置检查] and [有风险的操作] -
心智模型: 当一个操作(如
sqrt(x))依赖于某个必须满足的“前置条件”(如x > 0)才能安全执行时,就应该使用and框架。 -
详细展开:
-
我们将“前置检查”放在
and的左侧。这个检查本身必须是绝对安全的(例如x > 0只是一个比较,不会崩溃)[02:10]。 -
我们将“有风险的操作”放在
and的右侧 [02:10]。 -
利用
and的短路特性,只有当左侧的“安全检查”通过时(即值为“真”),右侧的“风险操作”才会被执行 [00:37]。如果检查失败(值为“假”),风险操作将被完美跳过 [02:43]。 -
这在实际编程中极为常见,例如在访问一个对象的方法前,先检查该对象是否为
None(if obj is not None and obj.do_something());或者在访问列表元素前,先检查索引是否越界(if index < len(my_list) and my_list[index] == ...)。
-
框架应用二:or 的“例外情况” (Exception Case) 优先
-
结构:
[安全的例外情况] or [通用的风险规则] -
心智模型: 当我们有一个适用于大多数情况的“通用规则”(如
1 / n != 0),但存在一个或多个已知的“例外情况”(如n == 0)会导致该规则崩溃时,就应该使用or框架。 -
详细展开:
-
我们将“安全的例外情况”检查放在
or的左侧 [04:12]。这个检查必须能安全地识别出那些会导致右侧规则崩溃的特定值。 -
我们将“通用的风险规则”放在
or的右侧 [04:12]。 -
利用
or的短路特性,当左侧的“例外情况”被触发时(即值为“真”),解释器会立即短路,返回“真”(表示这个例外情况是可接受的),而右侧的“风险规则”将被跳过 [04:50]。 -
只有当左侧的“例外情况”不成立时(值为“假”),程序才会去评估右侧的“通用规则”[01:27]。
-
这个框架确保了那些“已知的、特殊的、但合法的”值(如
0)能够被安全地处理 [04:44],而不会“污染”或破坏我们为一般情况设计的逻辑。
-
2. “真值” 与 “假值” (Truthy & Falsey) 的泛化心智模型
视频中一个非常重要的补充点,是 and 和 or 并非只操作布尔值 True 和 False,而是操作 Python 中所有的值 [00:56]。这要求我们建立一个超越简单布尔逻辑的“真值/假值”心智模型。
-
核心概念: 在 Python 中,任何数据类型的值都可以被放入一个逻辑上下文中(如
if语句或and/or表达式),它们会被隐式地判断为“真值”(Truthy)或“假值”(Falsey)[01:02]。-
假值 (Falsey): 包括
False、None、所有类型的数字零(0、0.0)、以及所有空的“容器”(如""、[]、{}、())。 -
真值 (Truthy): 包括
True以及上述假值之外的 一切。例如2,3,"hello",[1],{"a": 1}都是真值。
-
-
对
and/or的影响:-
这个模型解释了为什么
2 and 3的结果是3[01:02]。因为2是“真值”,and必须继续求值,于是它返回了右侧的值3。 -
同理,
0 and 3的结果会是0。因为0是“假值”,and立即短路,返回左侧的假值0。 -
2 or 3的结果是2。因为2是“真值”,or立即短路,返回左侧的真值2。 -
0 or 3的结果是3。因为0是“假值”,or必须继续求值,于是它返回了右侧的值3。
-
-
心智模型的转变:
-
我们不应再将
and和or简单地视为“逻辑判断”工具,而应将它们视为“值选择”或“流程控制”工具。 -
A and B的意思是:“如果 A 是假值,则选择 A;否则,选择 B。” -
A or B的意思是:“如果 A 是真值,则选择 A;否则,选择 B。” -
视频中展示的“避免崩溃”[04:58] 只是这个“值选择”框架的一种应用。它还可以用于提供默认值(例如
username = input() or "Guest",如果用户输入空字符串,username就会是"Guest"),这是对同一底层短路机制的更广泛应用。这个心智模型是理解 Pythonic 代码风格(如链式比较和默认值赋值)的基础。
-
Q&A
Overview
本视频是 CS 61A(一门计算机科学导论课程)第 4 讲的问答(Q&A)环节。核心议题源于一个教学疏漏:讲师 John DeNero 忘记录制和上传关于 lambda 表达式的教学视频 [00:00],导致学生在后续内容中遇到了这个未曾谋面的概念,引发了大量困惑 [00:25]。因此,本次 Q&A 的核心论题就是彻底厘清 lambda 表达式是什么、为什么用以及如何用。视频的结论是,lambda 表达式本质上是一种创建“匿名函数”的简洁语法糖,其本身不提供任何超越传统 def 语句的新功能 [28:09],但它在与“高阶函数”(Higher-Order Functions)配合使用时极其便利。讲师们通过 search(搜索)、inverse(求逆)、compose1(函数复合)和 make_adder(创建加法器)等一系列精妙的示例,将 lambda 置于高阶函数、嵌套函数、作用域和环境模型(Environment Diagrams)的宏观背景中,最终揭示了函数作为“一等公民”的核心思想以及“闭包”(Closure)这一关键机制。
按照主题来梳理
主题一:揭秘 lambda 表达式——匿名函数的“语法糖”
本视频的首要主题,是为困惑的学生“补上”关于 lambda 表达式的一课。
-
lambda究竟是什么? -
lambda与def的语法对比-
lambda是一种表达式(Expression),而def是一种语句(Statement)[02:14]。这是它们最核心的区别。 -
def语句(传统方式):1
2def f(x):
return x + 2这创建了一个函数,并将其“绑定”到名称
f上。 -
lambda表达式(匿名方式):1
lambda x: x + 2
这同样创建了一个函数,它接受一个参数
x,并返回x + 2的值 [01:51]。 -
语法解析:
-
lambda:关键字,宣告你正在创建一个匿名函数。 -
x:函数的形式参数(Formal Parameter)。 -
:(冒号):分隔参数和函数体。 -
x + 2:函数体,但它必须是一个单一的表达式。这个表达式的计算结果就是函数的返回值。
-
-
-
lambda作为表达式的意义-
因为
lambda是一个表达式,它会“评估”为一个值(这个值就是那个匿名函数对象)。 -
这意味着你可以将它用在任何需要值的地方。最常见的用法是:
-
将其赋值给一个变量 [02:22]:
1
2g = lambda x: x + 2
# 此后,g(7) 的行为就和 f(7) 完全一样虽然这在语法上可行,但它完全丧失了
lambda的意义(你给一个“匿名”函数起了个名字g),Python 社区通常不推荐这种用法,而是建议直接使用def。 -
将其作为参数传递给另一个函数 [19:37]:
这是 lambda 最主要、最合理的用途。当一个函数(高阶函数)需要你传入另一个“功能简单”的函数作为参数时,使用 lambda 可以在“原地”快速定义这个小函数,而无需在代码的其他地方用 def 专门定义一个只用一次的具名函数。
-
-
-
为什么要使用
lambda?(优点) -
lambda的严格限制(缺点)
主题二:高阶函数的威力——将函数作为积木
视频的第二个主要部分,是将 lambda 置于其真正发挥作用的上下文中——高阶函数 (Higher-Order Functions, HOFs)。HOF 指的是那些“以函数为参数”或“返回一个函数”的函数。
-
示例 1:
search(f)(以函数为参数)-
讲师定义了一个 HOF
search[03:52]。 -
功能:
search(f)接受一个函数f作为参数。f必须是一个接受单个整数并返回布尔值(True/False)的函数。search会从 0, 1, 2, … 开始依次测试,返回第一个使得f(x)为True的非负整数x。 -
应用:
-
寻找 4 的平方:
1
2
3def is_4_squared(x):
return 4**2 == x
result = search(is_4_squared) # result 会是 16 -
寻找 16 的平方根:
1
2
3def is_sqrt_of_16(x):
return x**2 == 16
result = search(is_sqrt_of_16) # result 会是 4
-
-
关键点 [06:04]:在调用
search(is_sqrt_of_16)时,我们传递的是函数名is_sqrt_of_16(函数对象本身),而不是is_sqrt_of_16()(函数的_调用结果_)。我们是把“计算蓝图”交给search,让search内部去调用它。
-
-
示例 2:
inverse(f)(返回一个函数)-
这是视频中一个更高级的例子,展示了 HOF 如何用于抽象和泛化。
-
目标:创建一个通用的求逆函数
inverse。它接受一个函数f(例如square),并返回f的反函数(例如sqrt)。 -
思路(从具体到抽象):
-
我们已经知道如何用
search找 16 的平方根 [07:20]。 -
我们可以将其泛化,写一个
square_root(y)函数,用于寻找 任意y的平方根 [08:00]。 -
实现这个
square_root(y)的方法是嵌套函数(Nested Function):1
2
3
4
5
6def square_root(y):
# 定义一个 *仅在内部* 使用的辅助函数
def is_sqrt_of_y(x):
return x**2 == y
# 返回用 search 查找的结果
return search(is_sqrt_of_y)注意,内部函数
is_sqrt_of_y可以访问外部函数square_root的参数y。这就是“闭包”的雏形。 -
最终泛化:inverse(f) [11:09]
square_root 函数其实就是 square 函数的 inverse(逆)。我们可以把 square 替换为一个通用参数 f:
1
2
3
4
5
6
7
8def inverse(f):
# inverse(f) 返回的是 *另一个函数*
def inverse_of_f(y):
def f_equals_y(x):
return f(x) == y
return search(f_equals_y)
# 返回这个新定义的函数,注意没有 ()
return inverse_of_finverse是一个 HOF,因为它接受f作为参数,并返回inverse_of_f这个新函数。
-
-
lambda 的用武之地:
上面那段 inverse 函数,用 lambda 可以写得极其“简洁”(但可能也极其“晦涩”):
1
2def inverse(f):
return lambda y: search(lambda x: f(x) == y)讲师们承认,虽然
lambda版本 [12:46] 很“优雅”,但def版本 [12:09] 因为有明确的函数命名(如inverse_of_f),对于阅读者来说通常更清晰易懂。
-
-
示例 3:
make_adder(n)(y)与双括号语法-
视频中讨论了另一个 HOF
make_adder(加法器制造者),并解释了一个奇怪的语法make_adder(1)(2)[20:23]。 -
make_adder的定义:1
2
3
4def make_adder(n):
def adder(y):
return n + y
return adder # 返回内部的 adder 函数 -
make_adder(1)(2) 的含义 [20:47]:
这不是一个“有两个参数的函数调用”。Python 会从左到右评估。
-
第一步:
make_adder(1)-
调用
make_adder函数,n被绑定为1。 -
此调用执行完毕,返回了内部的
adder函数。在这个adder函数中,n的值(即1)被“记住”了(这就是闭包)。 -
所以,
make_adder(1)表达式的“值”是一个“加一函数”。
-
-
第二步:
... (2)-
上一步返回的“加一函数”现在被立即调用,参数是
2。 -
它执行
n + y,即(被记住的)1 + 2,得到3。
-
-
-
等价的“慢动作” [22:42]:
以下代码与 make_adder(1)(2) 完全等价,只是分了两步写,更易读:
1
2add_one = make_adder(1) # add_one 现在是那个 "加一函数"
result = add_one(2) # result 是 3
-
-
示例 4:
compose1(f, g)(函数复合)
主题三:Python 关键机制解析
在讨论 HOF 和 lambda 的过程中,自然地引出了一些 Python 的核心工作机制。
-
return addervsreturn adder(x)-
return adder(不带括号):-
返回的是函数对象本身(the function itself)。
-
你返回的是一个“配方”或“蓝图”。
-
这是 HOF(如
make_adder)想要的。
-
-
return adder(x)(带有括号):-
这是在调用(call)
adder函数,并返回adder函数的执行结果(the result of calling the function)。 -
你返回的是一个“计算结果”,例如一个数字(
int)。 -
如果你在
make_adder中这么做,代码会崩溃。首先,x在那里没有定义;其次,即使它有定义,make_adder(1)也会返回一个_数字_(比如 2)。 -
那么
make_adder(1)(2)就会变成2(2)[24:01],Python 会报错TypeError: 'int' object is not callable(错误:整数不可调用)。
-
-
多重赋值(Multiple Assignment)
-
视频最后讨论了
fibonacci斐波那契数列中常见的pred, curr = curr, pred + curr这种多重赋值语句是如何工作的 [31:53]。 -
关键机制 [32:11]:Python 会首先评估(evaluate)等号右侧的所有表达式。
-
执行步骤:
-
假设
pred是 3,curr是 5。 -
Python 看到
pred, curr = curr, pred + curr。 -
第 1 步(评估右侧):
-
评估第一个值
curr,得到 5。 -
评估第二个值
pred + curr,得到 3 + 5 = 8。 -
此时,Python 在“内部”持有了两个新值:(5, 8)。
-
-
第 2 步(执行赋值):
-
将第一个值 5 赋给第一个变量
pred。 -
将第二个值 8 赋给第二个变量
curr。
-
-
执行完毕,
pred变成了 5,curr变成了 8。
-
-
重点:这个过程确保了赋值操作的“同步性”,右侧的计算(
pred + curr)使用的是pred_旧_的值(3),而不是在同一步中被赋了新值(5)的pred[32:36]。
-
框架 & 心智模型 (Framework & Mindset)
从这场 Q&A 中,我们可以抽象出两个对于理解现代编程(尤其是函数式编程)至关重要的心智模型。
模型一:“函数即数据”——高阶函数的心智模型
-
核心思维:在 Python(以及许多其他现代语言)中,函数是“一等公民”(First-class Citizens)。这个比喻意味着,函数与其他数据类型(如整数
int、字符串str、列表list)的地位完全平等。 -
推论:一个函数(一个值),就像任何其他值一样,可以被:
-
赋值给变量:
-
g = lambda x: x + 2 -
my_adder = make_adder
-
-
作为参数传递给另一个函数:
-
search(is_sqrt_of_16) -
这是
lambda的主要用途:search(lambda x: x**2 == 16)。
-
-
作为另一个函数的返回值:
-
return adder(在make_adder内部) -
return inverse_of_f(在inverse内部)
-
-
-
关键区分(再次强调):
-
带来的力量:
-
这种心智模型是**抽象(Abstraction)**的基石。
-
search函数是通用的。它不关心你到底在“搜索”什么(是平方根?还是某个日志文件中的特定行?)。它的逻辑(“从 0 开始一个一个试”)是抽象的。 -
你通过传入一个_特定_的函数
f,来_具体化_search的行为。f成了search行为的“配置”。 -
这使得我们可以编写出更小、更通用、可复用性更强的代码(例如
search),然后通过lambda或def定义的小函数将其“组装”起来,解决复杂问题。
-
模型二:“函数如何‘记忆’”——闭包与环境模型
-
核心问题:当
make_adder(1)执行完毕并返回adder函数后,make_adder的本地(local)变量n(其值为 1)应该已经随着函数调用的结束而“死亡”了。那么,为什么之后调用add_one(2)(即adder(2))时,adder函数内部仍然能够访问到n = 1呢?[16:46] -
心智模型(答案):闭包(Closure)。
-
通俗解释:函数在被定义时,会“记住”它被定义时所处的环境(Environment)。
-
环境模型(Environment Diagram)的逐步解析 [17:32]:
-
调用
make_adder(1):-
Python 创建一个帧(Frame)(我们称之为
F1),用于make_adder的本地变量。 -
在
F1中,n被绑定到1。
-
-
定义
adder:-
在
make_adder(1)执行期间,def adder(y): ...语句被执行。 -
Python 创建了一个函数对象(
adder)。 -
关键:这个
adder函数对象内部包含一个“指针”,指向它被创建时的环境,即F1帧 [17:40]。这个指针就是它与生俱来的“记忆”。
-
-
return adder:-
make_adder将这个“携带记忆”的adder函数对象返回。 -
make_adder(1)调用结束,F1帧从“调用栈”中移除。但是,由于adder函数对象(现在被add_one变量引用)仍然“指向”F1,F1帧在内存中并不会被销毁。
-
-
调用
add_one(2)(即adder(2)) [21:56]:-
Python 创建一个新的帧(
F2),用于adder的调用。 -
在
F2中,参数y被绑定到2。
-
-
执行
return n + y:-
Python 在
F2中查找y,找到了,y=2。 -
Python 在
F2中查找n,没找到。 -
Python 沿着
adder函数的“记忆”指针(即F1帧) [17:37],去F1中查找n。 -
在
F1中找到了n=1。 -
计算
1 + 2,返回3。
-
-
-
总结:
adder函数“封闭”了(Enclosed)其定义时环境中的变量n,这就是“闭包”的含义。这个机制是 HOF(尤其是返回函数的 HOF)能够工作的根本原因。它使得inverse(f)返回的函数能“记住”f是什么,compose1(f, g)返回的h函数能“记住”f和g分别是什么 [17:04]。

