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

      1. 定义状态变量与初始化:

      我们从序列的“开端”开始。我们需要三个变量来追踪状态:

      • pred (predecessor,前一个值) 和 cur (current,当前值)。

      • 我们将其初始化为序列的最开始两个值:pred 被赋值为 0,cur 被赋值为 1 [03:00]。

      • k (索引计数器)。

      • k 用来追踪我们当前 cur 变量对应的是“第几个”斐波那契数 [03:11]。

      • 由于 cur 此时是 1,它是“第 1 个”斐波那契数,所以 k 被初始化为 1 [03:30]。

      1. 定义循环条件(终止条件):

      我们的目标是找到第 n 个数。我们当前的 k 是 1,我们要让 k 一直增长到 n 为止。因此,循环(iteration)应该在 k < n 这个条件成立时持续执行 03:39。当 k 最终等于 n 时,循环将停止。

      1. 定义状态转移规则(循环体):

      在循环的每一步中,我们需要更新我们的三个状态变量,让它们“前进”到序列的下一个位置。

      • 更新 predcur [03:48]:

        • 在计算 新值 之前,旧的 cur(当前值)将成为 下一个 迭代中的 pred(前一个值)。

        • cur,将根据斐波那契的定义,等于 旧的 pred 和 旧的 cur 之和。

        • 在 Python 中,这可以通过一个简洁的赋值语句 pred, cur = cur, pred + cur 来完成 [03:58]。

      • 更新 k [04:11]:

        • 因为我们已经计算出了序列中的下一个数,所以我们的索引计数器 k 也必须相应地增加 1。

        • 这通过 k = k + 1 来实现 [04:18]。

      1. 定义返回值(提取结果):

      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

    • 让我们分析当输入 n = 0 时,这个新版本会发生什么 [06:35]。

    • 初始化: pred=1, cur=0, k=0

    • 检查循环条件: 此时 n 也是 0。while 循环检查 k < n(即 0 < 0)[06:49]。

    • 循环终止: 0 < 0 为 False,因此循环体(body)一次也不会被执行 [06:49]。

    • 返回: 函数直接跳转到 return cur 语句,返回 cur 的当前值,即 0 [06:55]。

    • 结论: 0 正是“第 0 个”斐波那契数,所以这个实现是正确的。而原始版本(k 从 1 开始)无法处理这种情况。

  • 优势 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 步”(迭代开始前)的值是什么。

    • 视频中的第一个实现([03:00])选择了:pred = 0, cur = 1, k = 1。这是一个有效的初始状态,但它隐含了一个假设(n >= 1)。

    • 第二个实现([06:02])选择了:pred = 1, cur = 0, k = 0。这是一个更“基础”(更接近 n=0)的初始状态。

    • 选择哪个初始状态,将直接影响你的“状态转移规则”和“终止条件”的设计,以及算法能处理的输入范围。

  • 步骤四:定义状态转移规则 (Define State Transition Rules)

    这是迭代的“引擎”。你必须明确定义:状态变量如何从“上一步”变化到“下一步”?这通常是循环体(loop body)中的核心逻辑。

    • 在视频中 [03:58],这个规则被精炼地表达为:

      1. pred 的新值 = cur 的旧值

      2. cur 的新值 = pred 的旧值 + cur 的旧值

      3. k 的新值 = k 的旧值 + 1

    • 这个规则确保了计算可以按照斐波那契数列的定义,一步一步地“向前滚动”。

  • 步骤五:确定终止条件 (Determine Termination Condition)

    你必须定义一个清晰的条件,告诉这个“引擎”什么时候该“熄火”了。这通常是 while 循环的“条件”部分。

    • 视频中的条件是 k < n [03:39]。

    • 这意味着:“只要我的计步器 k 还没有达到目标 n,就请继续执行状态转移规则”。

    • 一旦 k 等于或大于 n(在这个例子中是等于 n [04:33]),循环就停止。

  • 步骤六:提取最终结果 (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=1n=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)。

    • 第二个实现之所以“更好”(better)[06:20],不是因为它更快(事实上它们的时间复杂度相同),而是因为它更稳健

    • 它用一套统一的逻辑(同一个 while 循环和状态转移规则),同时正确地处理了“边界情况”(n=0)和“一般情况”(n=5)[07:04]。

    • 在设计任何流程或算法时,都应该主动去寻找和测试那些“最边缘”、“最简单”的输入,并确保你的设计能够优雅地处理它们,而不是为它们打上“输入无效”的补丁。这是一种能极大提升代码质量和系统可靠性的高级思维模式。

Designing Functions

Overview

这篇内容的核心论题是探讨在编程中如何设计出优秀的函数 (Functions)。视频首先强调了函数设计作为计算机科学的一项基本技能的重要性,它能帮助我们更好地组织大型程序 [00:31]。优秀的函数设计不仅能让代码更易于阅读和理解,还能使其在更多情境下更具可用性 [00:08]。视频的核心结论是,优秀的设计应遵循三个关键的指导方针(或启发式原则):单一职责(一个函数只做一件事)、不要重复自己(DRY 原则)以及定义通用函数(使其具有广泛适用性)[02:36]。视频通过类比(如剪刀、瑞士军刀、电源插座)来强化这些原则,旨在帮助开发者建立起一种清晰、高效、可复用的编程心智模型。


按照主题来梳理

主题一:理解函数的核心特性:定义域、值域与行为

在深入探讨如何“设计”函数之前,我们必须首先清晰地定义一个函数由什么构成。视频从概念层面(而非特定于 Python 语法的层面)[02:05] 阐述了函数的三个基本特性,它们共同决定了一个函数“是什么”以及“能用在哪里”。

  • 1. Domain (定义域)

    • 定义: “一个函数的定义域是它可能接受的所有输入的集合” [00:47]。换言之,这是你“可以”传递给函数的值的范围。

    • 示例:

      • 以一个我们熟悉的 square (平方) 函数为例 [01:20],它的定义域是“任何实数 X” (any real number X) [01:26]。这意味着你可以给它 2, -10, 0.5 等任意实数。

      • 对于 Fibonacci (斐波那契) 函数,其目的是计算第 n 个斐波那契数 [01:20]。在视频的示例中,其定义域被设定为“大于或等于 1 的整数” (an integer greater than or equal to one) [01:51]。这明确了该函数不接受 0、-5 或 2.5 这样的输入。

    • 意义: 理解定义域至关重要,因为它划定了函数功能的边界。在实际编程中,虽然像 Python 这样的语言可能不会在代码中强制你(在类型提示之外)严格声明定义域 [02:05],但这个概念在程序文档中极其重要 [02:19]。文档必须清楚地告诉使用者:这个函数期望什么样的输入?例如,斐波那契函数的文档会明确指出 n 必须大于等于 1 [02:19]。

  • 2. Range (值域)

    • 定义: “一个函数的值域是它可能返回的所有输出值的集合” [00:56]。这是函数“产出”的值的范围。

    • 示例:

      • 对于 square (平方) 函数,无论输入是正还是负,其输出(即平方值)永远是“一个非负实数” (a non-negative real number) [01:35]。它的值域不包括 -10 或 -0.2。

      • 对于 Fibonacci (斐波那契) 函数,其值域自然是“一个斐波那契数” (a Fibonacci number) [02:01],也就是 1, 2, 3, 5, 8… 这样的整数。

    • 意义: 值域告诉我们调用这个函数后,可以期望得到什么类型或范围的数据。

  • 3. Behavior (行为)

    • 定义: 对于一个“纯函数” (pure function) 而言(视频中特别提到了“纯函数”),其行为是“它在输入和输出之间建立的关系” [01:11]。

    • 示例:

      • square (平方) 函数的行为非常直白:“返回值是输入的平方” (the return value is the square of the input) [01:35]。

      • Fibonacci (斐波那契) 函数的行为是:“返回值是第 n 个斐波那契数” (the return value is the nth Fibonacci number) [02:01]。

    • 意义: 行为描述了函数的核心逻辑,即“转换”的规则。它连接了定义域和值域,说明了一个特定的输入是如何映射到一个特定的输出的。

综上所述,这三个特性(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]:

      • 可扩展性: 当“用户 C”出现时,你不需要写新代码。这个通用函数天然就可以支持他。

      • 简洁性: 你最终得到的是一个“通用”的解决方案 [03:51],而不是一堆需要“疯狂的适配器” [03:45] 才能协同工作的特殊情况代码。

    • 这要求设计者在函数参数(定义域)的设置上具有一定的远见,思考“什么在变,什么不变”,并将“变化的部分”(如税率、特定配置)作为参数传入,而不是将其硬编码 (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
    10
    def 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_squarearea_circlearea_hexagon 三个函数中 [04:03]。这立刻凸显了“重复自己”(repeat myself)的弊端 [04:14]:如果未来我们想修改这个校验逻辑(比如改成 r >= 0),我们就必须修改三个地方,这非常容易出错。

  • 泛化:封装共同点,参数化不同点

    这个“重复”的问题,无论是 R * R 还是 assert 语句,都指向了同一个解决方案:泛化 04:24。我们可以定义一个通用的 area(面积)函数,它封装所有“共同点”,并把“不同点”作为参数传入。

    在这个例子中:

    1. 共同点 是:assert r > 0, "A length must be positive"r * r * ...

    2. 不同点 是:那个特定的“形状常量”。

    于是,一个泛化的 area 函数诞生了 [04:31]:

    1
    2
    3
    def area(r, shape_constant):
    assert r > 0, "A length must be positive"
    return r * r * shape_constant

    这个函数接收两个参数:长度 rshape_constant(形状常量)。它内部包含了共享的校验逻辑和 r * r 的计算。

  • 重构:使用泛化函数

    有了这个通用的 area 函数,我们现在可以重新定义之前那三个独立的函数,但这一次它们不再包含重复的逻辑,而是简单地调用 area 函数并传入“特定”的参数 05:05

    1
    2
    3
    4
    5
    6
    7
    8
    def 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)

    05:11

    现在,所有的核心逻辑(包括校验)都集中在 area 函数中。如果我们想修改校验规则,只需修改一个地方。我们就此完成了第一层泛化:通过将“特定数据”(形状常量)参数化,来共享“通用的计算实现” 01:27


第二部分:高阶函数:泛化“计算过程”

第一部分展示了如何泛化“数据”(一个数字常量),而视频的第二部分则将这个理念提升到了一个全新的、更强大的层次:我们不仅可以泛化数据,还可以泛化“计算过程”(computational processes)本身 [06:03]。这就是“高阶函数”的用武之地。

  • 新的问题:重复的“求和”模式

    视频提出了一个新的场景:计算不同类型的数列求和 06:12

    1. 自然数求和 (Sum Naturals)1 + 2 + 3 + ... + n

    2. 立方数求和 (Sum Cubes)1³ + 2³ + 3³ + ... + n³

    3. 一个更复杂的、用于逼近 Pi 的级数求和 [06:48]。

    我们先关注前两个。为了实现它们,我们可能会写出如下两个函数:

    sum_naturals(n) 07:55

    1
    2
    3
    4
    5
    def sum_naturals(n):
    total, k = 0, 1
    while k <= n:
    total, k = total + k, k + 1 # 关键差异点
    return total

    sum_cubes(n) [09:10]:

    1
    2
    3
    4
    5
    def sum_cubes(n):
    total, k = 0, 1
    while k <= n:
    total, k = total + pow(k, 3), k + 1 # 关键差异点
    return total
  • 再次识别“共同点”与“不同点”

    和面积的例子一样,我们来比较这两个函数 10:11

    • 共同点 (Common Structure):它们几乎是完全相同的。它们都初始化了 totalk [08:21], [09:35],都有一个 while k <= n 的循环,都在循环的最后执行 k + 1,最后都返回 total。这个共同的部分代表了一个“从 1 迭代到 n 并进行累加”的通用计算过程

    • 不同点 (Specific Parts):唯一的区别在于 total 累加上的是什么 [10:19]。在 sum_naturals 中是 k 本身;在 sum_cubes 中是 k 的立方,即 pow(k, 3)

  • 关键的飞跃:“不同点”是一个“过程”

    在面积的例子中,“不同点”是一个简单的数字(如 pi)。但在这里,“不同点”是一个计算(computation)或表达式(expression)07:29。我们需要的不是传入 k 或 k^3 的结果,而是需要传入“如何根据 k 计算出下一项”的这个“过程”本身。

  • 解决方案:将“过程”封装为函数

    我们如何才能把一个“过程”作为参数传递呢?答案是:将这个过程定义为一个函数。

    视频中定义了两个小函数,它们分别代表了那两个“特定的部分”10:35

    1. identity(k)(恒等函数):它接收 k 并原封不动地返回 k [10:44]。

      1
      2
      def identity(k):
      return k
    2. cube(k)(立方函数):它接收 k 并返回 k 的立方 [10:51]。

      1
      2
      def cube(k):
      return pow(k, 3)
  • 高阶函数:封装“共同点”,参数化“过程”

    现在,我们可以像泛化 area 函数一样,来泛化求和的过程。我们将定义一个名为 summation(求和)的通用函数 11:09

    这个函数需要两个参数:

    1. n:要累加多少项。

    2. term(项):一个函数,它告诉 summation 在每一步(即每个 k)应该累加什么 [11:17]。

    这个 summation 函数如下所示 [12:06]:

    1
    2
    3
    4
    5
    6
    def 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
    7
    def sum_naturals(n):
    # 调用 summation,并把 "identity" 函数作为参数传给 term
    return summation(n, identity)

    def sum_cubes(n):
    # 调用 summation,并把 "cube" 函数作为参数传给 term
    return summation(n, cube)

    13:10

    当我们调用 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)

这是框架的第一步,也是最关键的一步,它要求我们从“观察”开始。

  1. 并置具体实现 (Juxtapose Implementations):不要一上来就试图设计一个“完美”的通用函数。相反,先去解决几个具体的问题。就像视频中,我们先写了 area_squarearea_circle [01:45],或者先写了 sum_naturalssum_cubes [07:55]。把这些“具体”的实现并排放在一起。

  2. 寻找“共同点” (Find the Common Structure):仔细比较这些代码。它们中一定有完全相同或结构上等价的部分。

    • 在面积案例中,“共同点”是 r * r 这个计算 [01:05] 以及后来增加的 assert r > 0 这个校验逻辑 [04:24]。

    • 在求和案例中,“共同点”是整个 while 循环的结构、totalk 的初始化与递增 [10:11]。

    • 这个“共同点”就是“通用的计算方法”(general methods of computation)[00:11],是我们可以“共享的实现” [01:27]。

  3. 寻找“不同点” (Find the Specific Parts):在剥离出共同点之后,剩下的就是让这几个具体实现“彼此不同”的关键所在。

    • 在面积案例中,“不同点”是那个形状常量:1pi3 * sqrt(3) / 2 [01:12]。

    • 在求和案例中,“不同点”是在循环中被累加的那一项:是 k 还是 pow(k, 3) [10:19]。

    • 这个“不同点”是“模式中的特定实例”(specific instances of those patterns)[00:21]。

步骤二:提取与封装 (Extract & Encapsulate)

一旦完成了识别,我们就进入了“重构”的阶段。

  1. 封装“共同点” (Encapsulate the Common):将你在上一步识别出的“共同结构”提取到一个全新的、更通用的函数中(例如 area 函数 [04:31] 或 summation 函数 [11:09])。这个新函数代表了那个“通用计算方法”的骨架。

  2. “参数化”不同点 (Parameterize the Specific):这是整个框架的核心。你需要一种方法,在调用通用函数时,能把“不同点”告诉它。

    • 级别 1:参数化“数据”。如果“不同点”是一个简单的值(如数字、字符串),那就把它变成一个普通的参数。这就是 area 函数所做的,它增加了一个 shape_constant 参数 [04:39]。

    • 级别 2:参数化“行为”。这是视频中的关键飞跃。如果“不同点”是一个“计算过程”或“动作”(比如“如何从 k 计算出下一项”),那么你就需要将这个“行为”也变成参数。在支持高阶函数的语言中,实现这一点的方法就是:将这个“行为”定义成一个函数,然后将这个函数作为参数传递。这就是 summation 所做的,它增加了一个 term 函数参数 [11:17]。

  3. 重构 (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)10pi 是数据,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 来完成这项工作”。

  • 分离“变”与“不变”:

    这个心智模型的核心是分离“变化”与“不变”。

    • 不变 (The General):在求和的例子中,“不变”的是“从 1 迭代到 n 并累加”这个模式 [08:21], [09:35]。

    • 变化 (The Specific):“变化”的是“在第 k 步累加的到底是什么”这个行为 [10:19]。

    • 高阶函数(summation)通过将“不变”的模式硬编码(hardcode)在自己内部,同时通过一个函数参数(term)为“变化”的行为留下一个“插槽”(slot),从而完美地将两者解耦。

采用这种“万物皆可为参数”的心智模型,意味着我们不再将函数仅仅视为执行命令的动词,而是将其也视为可以被传递、组合和操作的名词。这使得我们可以构建出更高层次的抽象,将通用的算法框架(如求和、映射、过滤)与特定的业务逻辑(如“立方”、“身份认证”、“格式化字符串”)分离开来,这正是现代编程中许多最强大功能(如 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(圆周率)的序列。

  • 首先,定义了一个名为 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 真实值的近似值。

这个开场示例的意义在于,它生动地展示了高阶函数的价值。我们不需要为求和 π\pi 序列重写一个新的求和循环,我们只需要定义一个全新的、专用于计算 π\pi 序列项的 pi_term 函数,然后把它“喂”给已有的、通用的 summation 函数即可。这完美地体现了代码复用和抽象。

章节二:核心概念:返回函数的函数 make_adder

在展示了函数作为 参数 的威力后,视频转向了本节的核心主题:函数作为 返回值 [01:18]。为了演示这一点,视频定义了一个名为 make_adder(制造加法器)的函数 [01:26]。

make_adder 的定义如下:

  1. 它接受一个参数,我们称之为 n [01:26]。这个 n 将是“我们想要加上的那个数”。

  2. 关键在于:在 make_adder 函数的 内部,定义了 另一个 函数,名为 adder(加法器) [02:27]。

  3. 这个内部的 adder 函数接受一个参数,我们称之为 k [01:45]。

  4. adder 函数的函数体非常简单:它返回 k + n [01:45]。

  5. 最后,外部的 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]:

  1. 第一步:求值“操作符”(Operator)

    • 在 Python 中,function(argument) 这种形式被称为“调用表达式”(Call Expression)。

    • make_adder(1)(2) 中,Python 首先会把 make_adder(1) 视为一个整体,即“操作符”部分(因为它在另一对括号 (2) 的左边)。而 (2) 则是“操作数”(Operand)部分。

    • 一个操作符可以是任何“求值结果为函数”的表达式 [04:21]。

    • 因此,Python 必须先求值 make_adder(1)

  2. 第二步:执行 make_adder(1)

    • 这又是一个调用表达式。Python 查找 make_adder 函数,并用参数 1 来调用它。

    • 根据 make_adder 的定义,n 被绑定为 1

    • make_adder 创建了内部的 adder 函数(此时 adder 知道 n=1)。

    • make_adder(1) 调用结束,它 返回 adder 函数(这个版本“记住”了 n=1)。

  3. 第三步:执行返回的函数

    • 现在,第一步中的“操作符”make_adder(1) 已经成功求值得到了结果——即那个“记住了 n=1”的 adder 函数。

    • 于是,原来的表达式 make_adder(1)(2) 就变成了 adder(2)(这里的 adder 是上一步返回的那个特定函数)。

    • Python 现在执行这个新的调用。adder 函数被调用,参数 k 被绑定为 2

    • adder 函数执行它的函数体:return k + n

    • 此时,k2n 是它“记住”的 1

    • 2 + 1 等于 3

  4. 第四步:返回最终结果

    • 表达式 make_adder(1)(2) 的最终求值结果为 3 [05:07]。

视频还在 Python 交互式环境中演示了这一点 [05:16],证明 make_adder(1)(2) 确实得到 3

为了进一步强化这个概念,视频还展示了分两步执行的过程 [05:26]:

  1. add_2000 = make_adder(2000)

    • 这创建并返回了一个“记住”了 n=2000adder 函数,并将其命名为 add_2000
  2. 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")或列表等其他类型的数据是平等的。

  • 这种平等的地位体现在:

    1. 可以作为参数传递给其他函数:就像 summation 函数接受 pi_term 函数作为参数一样。

    2. 可以作为函数的返回值:就像 make_adder 函数返回 adder 函数一样。

    3. (视频未明确提及,但隐含)可以被赋值给变量(如 add_three = make_adder(3))或存储在数据结构中。

基于“函数是一等公民”这个前提,高阶函数 的定义就水到渠成了:一个函数如果满足以下 至少一个 条件,它就是高阶函数 [05:58]:

  1. 接受至少一个函数作为其参数(例如 summation 函数)。

  2. 返回一个函数作为其结果(例如 make_adder 函数)。

这个框架是函数式编程的核心思想。它将“动作”或“计算过程”(即函数)本身数据化、变量化了。在 make_adder 的例子中,我们操作的不再是具体的数字,而是“加法”这个 概念 本身。我们通过 make_adder 制造 出了一个“加 3”的函数和一个“加 2000”的函数。这种对“计算过程”进行抽象和操作的能力,是高阶函数框架提供的最强大的力量。

心智模型二:高阶函数的价值:抽象、复用与“关注点分离”

为什么高阶函数如此重要?视频在结尾总结了它的三大价值 [06:07],这构成了我们应该建立的“心智模型”:

  1. 表达通用的计算方法 (Express General Methods of Computation)

    • 高阶函数允许我们编写“方法的方法”。summation 函数就是最好的例子 [06:07]。它不关心 具体 加什么,它只关心“如何加”(即累加的模式)。它将“累加”这个通用的计算方法(summation)与“要累加的具体项”(pi_termcubes)分离开来。这是一种强大的抽象能力,让我们能够站在更高的维度去思考计算。
  2. 消除程序中的重复 (Remove Repetition)

    • 这是通用性的直接好处 [06:17]。在没有 summation 函数之前,我们可能需要为“自然数求和”写一个循环,为“立方求和”写一个循环,为“π\pi 序列求和”再写一个循环。

    • 通过使用高阶函数 summation,我们只需要定义一次“求和”这个行为 [06:24]。之后,我们只需提供不同的 term(项)函数即可复用该行为。这极大地减少了冗余代码,使程序更简洁、更易于维护。

  3. 关注点分离 (Separation of Concerns)

    • 这是一个极其重要的软件设计原则。它的核心思想是:每个函数(或模块)都应该只做好一件事 [06:24]。

    • 高阶函数是实现这一原则的利器。

    • summation 的例子中:summation 函数的 唯一关注点 是“如何执行累加”;而 pi_term 函数的 唯一关注点 是“如何计算第 k 项 π\pi 序列”。两者各司其职,互不干扰。

    • 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 * x00: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”(对决时刻)来详细比较 lambdadef([04:28])。

  • 对比案例:

    • 左侧 (Lambda): square = lambda x: x * x([04:37])

    • 右侧 (Def): def square(x): return x * x([04:43])

  • 二者的相同点

    视频强调,在绝大多数情况下,这两者是“几乎完全相同”的(04:50)。

    1. 功能相同:它们都创建了一个函数。

    2. 行为相同:这两个 square 函数具有完全相同的 domain(定义域)、range(值域)和 behavior(行为)([04:50])。

    3. 父框架相同:它们都遵循相同的作用域规则,即函数的 parent(父级)都是它们被定义的那个 frame(帧)([04:59])。

  • 二者的关键区别

    尽管功能几乎一致,但它们在创建和命名机制上存在细微但重要的差别。

    1. 绑定名称的方式不同([05:05])

    • lambda:这是一个两步过程。首先,lambda 表达式本身被评估,创建 了一个 没有名字 的函数对象([05:13])。然后,assignment statement(赋值语句,即 = )将这个函数对象 绑定 到了 square 这个名称上([05:20])。

    • defdef 语句是一个复合操作。它会 自动 完成创建函数和绑定名称两个步骤([05:29])。def 语句在执行时,会创建一个函数对象,并 立即 将它绑定到 def 语句中指定的名称(square)上。

    1. 固有名称(Intrinsic Name)的区别(05:37

    这是上述绑定机制差异导致的直接后果。

    • defdef 语句会赋予函数一个“固有名称”。如果你在 Python 环境中定义了 def square...,然后你查看 square 变量,它的显示会是 <function square ...>([06:10])。这个 square 就是它的固有名称。

    • lambdalambda 表达式创建的是 匿名函数。当你查看通过 lambda 绑定的 square 变量时,它的显示会是 <function <lambda> ...>([05:54])。它没有一个叫 square 的固有名称,它只有一个占位符 <lambda>

    1. 在环境图(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 中“函数是一等公民”这一概念。

“一等公民”(或“一等对象”)在编程语言中意味着某个“事物”(比如函数)与其他“事物”(比如数字、字符串)具有同等的地位。它们可以被:

  1. 存储在变量中(即被名称绑定)。

  2. 作为参数传递给其他函数。

  3. 作为其他函数的返回值。

这个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”),并创建一个代表这个逻辑的“函数对象”。

  • 统一的赋值模型

    有了这个心智模型,赋值操作就变得完全一致了:

    1. x = 10

      • 右侧是一个字面量表达式 (literal expression),其值为 10

      • 名称 x 被绑定到值 10

    2. 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])。

    • (程序员)可以选择是否以及如何命名它。

      • square = lambda ... 是通过赋值语句来命名它([05:20])。

      • (lambda x: x * x)(10) 则是完全不命名,立即使用 它([01:58])。

    因此,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

    1. 发起调用:当程序执行到 f(x) 这样的调用表达式时,它会暂停在当前环境(我们称之为“调用方环境”)中的执行。

    2. 创建新环境:程序会创建一个全新的、隔离的“本地环境”,用于执行函数 f 的主体(body)代码。f 的参数(在这个例子中是 x)会在这个新环境中被赋值。

    3. 执行主体:程序开始逐行执行函数 f 内部的代码 [00:23]。

    4. 等待终止:这个执行过程会一直持续下去,直到遇到两种情况之一:要么是执行到了函数体内的某条 return 语句,要么是执行到了函数体的末尾(即所有代码都运行完了)。

  • return 触发的“返回”过程

    当函数执行过程中遇到一条 return 语句时(例如 return x + 1),一个关键的切换发生了 00:49

    1. 立即终止:函数体中位于 return 语句之后的任何代码都将被跳过。函数的执行在这一刻被立即终止 [01:24]。这是 return 最核心的控制流特性。一个函数体中可能有很多条 return 语句(例如在不同的 if 分支里),但只要有任何一条被执行,整个函数就结束了。

    2. 计算返回值:程序会计算 return 关键字后面的表达式(即 x + 1)的值。

    3. 销毁环境:为该函数调用创建的那个“本地环境”被销毁。

    4. 交还控制权:程序执行流程“跳回”到之前的“调用方环境”中,回到它当初暂停的地方。

    5. 赋值f(x) 这个调用表达式本身,现在“变成”了 return 语句计算出的那个值 [00:55]。如果代码是 result = f(x),那么这个值现在就被赋给了 result;如果代码是 print(f(x)),这个值就会被传递给 print 函数 [01:03]。

如果函数执行到末尾都没有遇到 return 语句,Python 会隐式地执行一个 return NoneNone 是一个特殊的对象,代表“没有值”。这解释了为什么一个只打印(print)而不返回(return)的函数,在被赋值时,结果总是 None

实践应用:使用 return 提前退出循环

return 的“立即终止函数”特性,使其成为控制循环(尤其是 while 循环)的强大工具。它不是“退出循环”(像 break 语句那样),而是“退出整个函数”,这在很多情况下是更简洁、更直接的解决方案。

  • 问题场景:寻找特定数字

    视频中提出了一个具体问题:编写一个函数,它接收一个非负整数 n 和一个数字 d,要求从 n 的个位数开始,反向逐个打印数字,直到打印到 d 为止 01:32

    • 示例:对于 n = 34567d = 5,函数应该打印 7,然后打印 6,然后打印 5。当它找到 5 时,它就应该停止,不应该再继续打印 43 [01:40]。
  • 常规循环的局限

    一个常规的 while 循环可以用来打印所有数字 02:00。逻辑如下:

    1. n 大于 0 时,持续循环。

    2. last = n % 10 (获取 n 的最后一位数字,即个位数)。

    3. n = n // 10 (通过整除 10 来“砍掉” n 的最后一位)。

    4. print(last) (打印出最后一位)。

    • 问题:如果 n = 34567,这个循环会打印 7, 6, 5, 4, 3。它无法在找到 5 之后停下来。
  • return 的解决方案

    我们可以利用 return 来解决这个问题。我们在循环内部增加一个条件判断 [02:19]:

    1
    2
    3
    4
    5
    6
    7
    def print_reverse_until(n, d):
    while n > 0:
    last = n % 10
    n = n // 10
    print(last)
    if last == d: # 检查当前打印的数字是否是我们要找的 d
    return None # 或者仅仅是 'return'
    • 工作原理

      1. 循环开始,打印 7。7 == 5 不成立,循环继续。

      2. 循环继续,打印 6。6 == 5 不成立,循环继续。

      3. 循环继续,打印 5。5 == 5 成立,if 条件为真。

      4. 程序执行 return None 语句 [02:34]。

      5. return 语句立即终止 print_reverse_until 整个函数的执行。

      6. while 循环(以及函数体内的任何其他代码)因为函数已经终止,所以自然也就停止了。

    • 关键点:我们不需要引入额外的“标志变量”(flag variable,例如 found_d = False)来控制 while 循环的条件。return 提供了一种更清晰、更直接的方式来表达“我们的工作已经完成,请立即停止”的意图 [02:41]。

高级应用:在无限循环中使用 return 实现搜索

return 的控制能力在与“无限循环”结合时,展现出了更强大的威力。这是一种非常常见的编程模式,用于实现各种“搜索”算法。

  • “搜索”的抽象定义

    视频定义了一个名为 search(f) 的高阶函数(higher-order function,即接收函数作为参数的函数)02:57

    • 目标search(f) 的任务是找到第一个非负整数 x(从 0, 1, 2, … 开始尝试),使得 f(x) 的计算结果是一个“真值”(true value)[03:04]。

    • 什么是“真值”? [03:12] 在 Python 的条件判断中,不仅仅是布尔值 True 被认为是“真”。任何非零数字(如 21)、非空字符串(如 “hello”)或非空列表,在 if 语句中都会被当作“真”来处理。相反,False、数字 0、空字符串 ""、空列表 []None 对象,都被认为是“假值”(false value)[05:19]。

  • 使用 while True: 和 return 实现搜索

    search(f) 函数的实现巧妙地利用了一个无限循环:

    1
    2
    3
    4
    5
    6
    7
    def 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

    1. 定义一个简单的测试函数 is_three(x),它当 x 等于 3 时返回 True,否则返回 False [04:01]。

    2. 调用 search(is_three) [04:11]。

    3. search 内部开始循环:

      • x = 0is_three(0) 返回 Falsex 变为 1。

      • x = 1is_three(1) 返回 Falsex 变为 2。

      • x = 2is_three(2) 返回 Falsex 变为 3。

      • x = 3is_three(3) 返回 Trueif 条件满足。

      • return 3 被执行。search 函数终止,返回值是 3。

  • 示例 2:找到使 x*x - 100 为正数的最小 x

    这是一个更复杂的例子,它依赖于“真值”不仅仅是 True 这一特性。

    1. 定义一个函数 positive(x),它返回 max(0, x*x - 100) [04:37]。

    2. 分析 positive(x) 的行为:

      • 如果 x 是 0 到 10,x*x - 100 都是小于或等于 0 的。max 函数会返回 0

      • x = 10 时,10*10 - 100 = 0positive(10) 返回 0 [04:54]。0 是一个“假值”。

      • x = 11 时,11*11 - 100 = 21positive(11) 返回 21 [05:03]。

    3. 调用 search(positive)

    4. search 内部开始循环:

      • x = 0positive(0) 返回 0(假值)。x 变为 1。

      • x = 10positive(10) 返回 0(假值)。x 变为 11。

      • x = 11positive(11) 返回 21

      • 21 是一个非零数字,所以它是一个“真值” [05:19]。if 条件满足。

      • return 11 被执行。search 函数终止,返回值是 11。

    5. 结论:我们通过这个通用的 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 语句只负责判断和停止。

  • 框架的构成步骤

    1. 初始化状态 (Initialize State):设置搜索的起点。在视频的例子中,这就是 x = 0 [03:18]。在其他问题中,它可能是一个指针、一个索引、或者一个队列中的第一个元素。

    2. 启动无限循环 (Create Infinite Loop):使用 while True: 语句块,表明搜索过程将无限持续下去,直到找到解。

    3. 检查条件 (Check Condition):在循环体内部,使用 if 语句检查当前状态是否满足我们的“成功条件”。例如 if f(x): [03:41] 或 if last == d: [02:25]。

    4. 成功则返回 (Return on Success):如果条件满足,立即使用 return 语句返回当前状态(即我们的“解”)[03:41]。return 会强制终止循环和函数,确保我们返回的是_第一个_满足条件的解。

    5. 推进状态 (Advance State):如果条件_不_满足,那么更新状态,为下一次循环做准备。例如 x = x + 1 [03:48] 或 n = n // 10 [02:13]。

  • 框架的变体:

    视频最后还展示了这种模式的一个更紧凑的变体 07:24。search 函数也可以被写成:

    1
    2
    3
    4
    5
    def search_alternative(f):
    x = 0
    while not f(x): # 循环的条件是“当 f(x) 还是假值时”
    x = x + 1
    return x # 循环一旦停止,就意味着 f(x) 已经是真值了
    • 对比:这个变体在逻辑上是等价的,它把“成功条件”放到了 while 循环的判断中。第一个版本(while True:)在语义上可能更清晰地表达了“搜索”的意图:循环体负责“检查与推进”,而 return 负责“终止”。而这个变体则更符合“循环直到…”(loop until…)的语义。选择哪一种取决于个人风格和问题的复杂度。

视频的第二个重要框架,是展示了如何将第一个“搜索”框架进行封装和抽象,用来解决一个更高级的数学问题:计算一个函数的“反函数”(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
    21
    def 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

    现在我们可以用这个框架来“制造”平方根函数,而不需要知道平方根的任何数学知识,只需要知道“平方”是什么:

    1. 定义原函数 fdef square(x): return x * x

    2. 制造反函数 gsqrt = inverse(square) [06:30]

      • inverse(square) 调用返回了上面定义的 g 函数,并将 g 赋值给 sqrt

      • 重要的是,此时 g 函数“记住”了它需要使用的 f 就是 square

    3. 使用反函数:当我们调用 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]:

  1. 程序按顺序考量每个子句。

  2. 它首先评估 if 子句的头部表达式。

  3. 如果该表达式的值为真 (a true value) [01:15],程序就 执行 其对应的 if 套件,并且(这是关键)跳过 (skip) 所有剩余的子句(即 else 块)[01:21]。

  4. 如果 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
2
3
4
5
def if_function(predicate, consequent, alternative):
if predicate:
return consequent
else:
return 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] 如下:

  1. 评估操作符 (Operator):首先,评估 if_function 这个名字本身,找到它所代表的函数定义。

  2. 评估所有操作数 (Operands):在 调用 函数体内的代码 之前 [03:33],程序必须准备好传递给函数的所有参数(即操作数)。这意味着它会 无条件地、从左到右 评估所有三个参数表达式:

    • 评估 x > 0

    • 评估 sqrt(x)

    • 评估 0

  3. 应用 (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
2
3
4
5
6
7
from math import sqrt

def real_sqrt_if(x):
if x > 0:
return sqrt(x)
else:
return 0

我们来测试这个函数:

  • 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
2
3
# 假设 if_function 存在
def real_sqrt_func(x):
return if_function(x > 0, sqrt(x), 0)

我们来测试这个版本:

  • 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]:

    1. 评估 -4 > 0,得到 False

    2. 评估 sqrt(-4)程序在此处崩溃 [06:57],抛出 math domain error [06:30]。

    3. (程序甚至没有机会评估 0,更没有机会进入 if_function 的函数体)。

尽管我们的 意图 是“如果 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”(也可称为“急切求值”或“严格求值”)是大多数编程语言中函数调用的默认行为。其核心心智模型是**“先准备好所有材料,再开始工作”**。

  • 工作流程

    1. 识别任务:程序遇到一个函数调用,如 f(A, B, C) [03:24]。

    2. 准备所有参数:在真正开始执行 f 函数体内的任何指令 之前,程序必须首先获得 A, B, C 三个参数的

    3. 强制评估:为了获得值,程序必须 立即无条件地 评估 A, B, C 这三个表达式 [03:33]。

      • 如果 A1+1,它被评估为 2

      • 如果 Bsqrt(4),它被评估为 2.0

      • 如果 Csqrt(-4),它被评估… 然后程序在此刻崩溃 [06:57]。

    4. 应用函数只有当 所有参数都成功评估出结果后,程序才会将这些结果(例如 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 等控制语句提供的核心心智模型。其核心理念是**“先检查条件,再决定走哪条路”**。

  • 工作流程

    1. 遇到岔路口:程序遇到 if 语句。它不把后面的代码块(套件)当作需要立即评估的参数 [00:59]。

    2. 评估“路标”:它只做一件必要的事:评估 if 旁边的 头部表达式(条件)[01:08],例如 x > 0

    3. 做出选择:根据上一步评估得到的布尔值(TrueFalse),程序做出 路径选择 [01:15]。

      • 如果为 True:选择 if 套件 (Suite) 所在的路径。

      • 如果为 False:选择 else 套件所在的路径(或者 elif,或什么也不做)。

    4. 执行并忽略:程序 进入被选中的路径(套件)并执行其中的代码。最关键的是,所有其他路径(套件)被 完全跳过 (skip) [01:21]。它们内部的代码 永远不会被评估或执行

  • real_sqrt 案例分析:

    当我们使用 if x > 0: … else: … [05:01] 时,计算机是站在“选择性执行”模型的角度工作。

    当 x 为 -4 时:

    1. 遇到岔路口if 语句。

    2. 评估“路标”:评估 x > 0(即 -4 > 0),得到 False [05:37]。

    3. 做出选择:因为条件为 False,程序选择 else 路径。

    4. 执行并忽略:程序 完全跳过 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]。逻辑运算符 andor 就是这种行为的典型代表,这种行为被称为“短路”(Short-circuiting)[00:09]。

and 运算符的求值规则 [00:21]

当 Python 解释器遇到一个 left and right(左表达式 与 右表达式)的结构时,它并不会立即计算两边的值,而是遵循一个严格的顺序:

  • 步骤一:求值左表达式。 解释器首先计算 and 左侧的子表达式 [00:28]。

  • 步骤二:检查左侧结果。

    • 如果左侧的值是一个“假值”(False Value): 此时,整个 and 表达式的结果已经确定为“假”,Python 会立刻“短路”。它将左侧的这个“假值”作为整个表达式的最终结果返回 [00:28],而 完全不会 去计算右侧的子表达式 [02:43]。

    • 如果左侧的值是一个“真值”(True Value): 此时,整个表达式的结果尚未确定,它取决于右侧的值。因此,解释器会继续去计算右侧的子表达式 [00:37]。

  • 步骤三:返回最终值。

    • 如果发生了短路(左侧为假),则返回左侧的假值。

    • 如果没有短路(左侧为真),则返回右侧表达式的计算结果 [00:37]。

视频特别强调,这里的“真值”和“假值”并不仅仅指布尔值 TrueFalse [00:56]。在 Python 中,任何值都可以被视为“真值”或“假值” [01:02]。例如,0、空字符串 ""、空列表 [] 都是假值;而像 23、非空字符串(如 “hello”)等都是真值。因此,视频中举例 2 and 3 [01:02],2 是一个真值,所以 Python 必须继续计算右侧,最终整个表达式的值是右侧的 3

or 运算符的求值规则 [01:12]

or 运算符的逻辑与 and 相反,但同样遵循短路原则:

  • 步骤一:求值左表达式。 解释器首先计算 or 左侧的子表达式 [01:12]。

  • 步骤二:检查左侧结果。

    • 如果左侧的值是一个“真值”(True Value): 此时,整个 or 表达式的结果已经确定为“真”,Python 会立刻“短路”。它将左侧的这个“真值”作为整个表达式的最终结果返回 [01:20],而 完全不会 去计算右侧的子表达式 [04:50]。

    • 如果左侧的值是一个“假值”(False Value): 此时,整个表达式的结果尚未确定,它取决于右侧的值。因此,解释器会继续去计算右侧的子表达式 [01:27]。

  • 步骤三:返回最终值。

    • 如果发生了短路(左侧为真),则返回左侧的真值。

    • 如果没有短路(左侧为假),则返回右侧表达式的计算结果 [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]。

  • 工作原理:

    • x = -1000 时,解释器首先计算 and 左侧的 x > 0,结果为 False [02:18]。

    • 根据 and 的短路规则,由于左侧为“假值”(False),解释器 立即停止 求值,将 False 作为整个表达式的结果返回 [02:43]。

    • 关键在于,右侧的 sqrt(x) > 10(即 sqrt(-1000) > 10根本没有被执行 [02:35]。

    • 通过这种方式,x > 0 充当了一个“守卫”(Guard),它保护了后续的 sqrt(x) 操作,使其永远不会在 x 为负数时被调用,从而完美避免了程序崩溃 [02:28]。

应用二:or 的“例外优先”

  • 场景设定: 假设我们要定义一个函数 is_reasonable(n),用于判断一个数 n 是否“合理” [03:42]。这里的“合理”定义为:1 / n 的结果不等于 0。这个定义的背景是,当 n 变得极大(如 101000 次方)时,1 / n 的结果会因为精度限制而被“舍入”(Rounded)为 0 [03:14],我们认为这种极大数是“不合理”的 [03:22]。

  • 潜在风险: 一个直接的想法是 return 1 / n != 0 [03:59]。这个实现在处理大数字时(如 10100 次方)是有效的 [03:07]。但是,如果 n 恰好等于 01 / 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 为极大数时(如 1010000 次方)返回 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]。

    • 这在实际编程中极为常见,例如在访问一个对象的方法前,先检查该对象是否为 Noneif 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) 的泛化心智模型

视频中一个非常重要的补充点,是 andor 并非只操作布尔值 TrueFalse,而是操作 Python 中所有的值 [00:56]。这要求我们建立一个超越简单布尔逻辑的“真值/假值”心智模型。

  • 核心概念: 在 Python 中,任何数据类型的值都可以被放入一个逻辑上下文中(如 if 语句或 and/or 表达式),它们会被隐式地判断为“真值”(Truthy)或“假值”(Falsey)[01:02]。

    • 假值 (Falsey): 包括 FalseNone、所有类型的数字零(00.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

  • 心智模型的转变:

    • 我们不应再将 andor 简单地视为“逻辑判断”工具,而应将它们视为“值选择”或“流程控制”工具。

    • 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 究竟是什么?

    • 讲师 John DeNero [01:13] 和 Hani [03:01] 都给出了明确的定义:它是一种创建匿名函数(Anonymous Function)的方式。

    • “匿名”是关键,意味着它被创建时没有绑定一个正式的名称(例如 f)。

    • 它在功能上与 Python 的 def(函数定义)语句是等价的,只是语法不同。

  • lambdadef 的语法对比

    • lambda 是一种表达式(Expression),而 def 是一种语句(Statement)[02:14]。这是它们最核心的区别。

    • def 语句(传统方式):

      1
      2
      def f(x):
      return x + 2

      这创建了一个函数,并将其“绑定”到名称 f 上。

    • lambda 表达式(匿名方式):

      1
      lambda x: x + 2

      这同样创建了一个函数,它接受一个参数 x,并返回 x + 2 的值 [01:51]。

    • 语法解析:

      1. lambda:关键字,宣告你正在创建一个匿名函数。

      2. x:函数的形式参数(Formal Parameter)。

      3. :(冒号):分隔参数和函数体。

      4. x + 2函数体,但它必须是一个单一的表达式。这个表达式的计算结果就是函数的返回值

  • lambda 作为表达式的意义

    • 因为 lambda 是一个表达式,它会“评估”为一个值(这个值就是那个匿名函数对象)。

    • 这意味着你可以将它用在任何需要值的地方。最常见的用法是:

      1. 将其赋值给一个变量 [02:22]:

        1
        2
        g = lambda x: x + 2
        # 此后,g(7) 的行为就和 f(7) 完全一样

        虽然这在语法上可行,但它完全丧失了 lambda 的意义(你给一个“匿名”函数起了个名字 g),Python 社区通常不推荐这种用法,而是建议直接使用 def

      2. 将其作为参数传递给另一个函数 [19:37]:

        这是 lambda 最主要、最合理的用途。当一个函数(高阶函数)需要你传入另一个“功能简单”的函数作为参数时,使用 lambda 可以在“原地”快速定义这个小函数,而无需在代码的其他地方用 def 专门定义一个只用一次的具名函数。

  • 为什么要使用 lambda?(优点)

    1. 简洁:当函数体非常简单(仅一行表达式)时,lambda 能用一行代码完成 def 需要两行(defreturn)才能做到的事,使代码更“紧凑”(compact)[19:16]。

    2. 避免命名空间污染(Namespace Pollution) [03:11], [27:57]:如果你只是需要一个“加一”的函数,并将其传递给 mapfilter 之类的函数,你不需要为了这个简单的功能而“污染”你的全局或局部命名空间,专门给它起一个(可能很难想的)名字,如 add_one_functionlambda 可以“用完即走”。

  • lambda 的严格限制(缺点)

    • 这是 Python 中 lambda 的一个关键特性:它的函数体必须是一个单一的表达式 [24:22]。

    • 你不能做

      • 你不能在 lambda 中写 return 语句(它是隐式的)[24:54]。

      • 你不能写多行代码 [24:29]。

      • 你不能写循环(如 whilefor)。

      • 你不能写常规的 if-else 语句。

    • 你可以做(但不推荐)

      • 你可以调用其他复杂的函数(只要这个调用本身是个表达式)[25:05]。

      • 你可以使用 Python 的“三元运算符”(A if Condition else B),因为它是一个表达式。但讲师们指出,这通常会使代码变得极其晦涩难懂 [27:06],违背了简洁的初衷。

    • 结论lambda 并没有带来任何新的计算能力 [28:09]。任何 lambda 能做到的事,def 都能做到,并且 def 的功能(可以包含多行、循环、判断)远比 lambda 强大。lambda 仅仅是在特定场景下(作为参数传递的、极其简单的函数)的一种便利的(convenience)速记法 [27:44]。

主题二:高阶函数的威力——将函数作为积木

视频的第二个主要部分,是将 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

    • 应用

      1. 寻找 4 的平方

        1
        2
        3
        def is_4_squared(x):
        return 4**2 == x
        result = search(is_4_squared) # result 会是 16
      2. 寻找 16 的平方根

        1
        2
        3
        def 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)。

    • 思路(从具体到抽象)

      1. 我们已经知道如何用 search 找 16 的平方根 [07:20]。

      2. 我们可以将其泛化,写一个 square_root(y) 函数,用于寻找 任意 y 的平方根 [08:00]。

      3. 实现这个 square_root(y) 的方法是嵌套函数(Nested Function)

        1
        2
        3
        4
        5
        6
        def 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。这就是“闭包”的雏形。

      4. 最终泛化:inverse(f) [11:09]

        square_root 函数其实就是 square 函数的 inverse(逆)。我们可以把 square 替换为一个通用参数 f:

        1
        2
        3
        4
        5
        6
        7
        8
        def 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_f

        inverse 是一个 HOF,因为它接受 f 作为参数,并返回 inverse_of_f 这个新函数。

    • lambda 的用武之地:

      上面那段 inverse 函数,用 lambda 可以写得极其“简洁”(但可能也极其“晦涩”):

      1
      2
      def 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
      4
      def make_adder(n):
      def adder(y):
      return n + y
      return adder # 返回内部的 adder 函数
    • make_adder(1)(2) 的含义 [20:47]:

      这不是一个“有两个参数的函数调用”。Python 会从左到右评估。

      1. 第一步:make_adder(1)

        • 调用 make_adder 函数,n 被绑定为 1

        • 此调用执行完毕,返回了内部的 adder 函数。在这个 adder 函数中,n 的值(即 1)被“记住”了(这就是闭包)。

        • 所以,make_adder(1) 表达式的“值”是一个“加一函数”。

      2. 第二步:... (2)

        • 上一步返回的“加一函数”现在被立即调用,参数是 2

        • 它执行 n + y,即(被记住的)1 + 2,得到 3

    • 等价的“慢动作” [22:42]:

      以下代码与 make_adder(1)(2) 完全等价,只是分了两步写,更易读:

      1
      2
      add_one = make_adder(1) # add_one 现在是那个 "加一函数"
      result = add_one(2) # result 是 3
  • 示例 4:compose1(f, g)(函数复合)

    • 另一个 HOF 示例,用于演示 f(g(x)) [13:04]。

    • compose1(f, g) 接受两个函数 fg,并返回一个的函数 h。这个 h 函数在被调用时(例如 h(x)),会先计算 g(x),然后将结果再传给 f,即 f(g(x))

    • 当用 lambda 调用它时 h = compose1(lambda x: x*x, lambda y: y+1) [14:16],意味着 f 是“平方”函数,g 是“加一”函数。

    • 那么调用 h(12) 时,会先执行 g(12)(即 12+1,得 13),再执行 f(13)(即 13*13,得 169)[15:30]。

主题三:Python 关键机制解析

在讨论 HOF 和 lambda 的过程中,自然地引出了一些 Python 的核心工作机制。

  • return adder vs return adder(x)

    • 这是一个新手常见的、致命的错误,讲师在 [23:07] 和 [23:34] 处特别强调。

    • 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)等号右侧的所有表达式

    • 执行步骤

      1. 假设 pred 是 3,curr 是 5。

      2. Python 看到 pred, curr = curr, pred + curr

      3. 第 1 步(评估右侧)

        • 评估第一个值 curr,得到 5。

        • 评估第二个值 pred + curr,得到 3 + 5 = 8。

        • 此时,Python 在“内部”持有了两个新值:(5, 8)。

      4. 第 2 步(执行赋值)

        • 将第一个值 5 赋给第一个变量 pred

        • 将第二个值 8 赋给第二个变量 curr

      5. 执行完毕,pred 变成了 5,curr 变成了 8。

    • 重点:这个过程确保了赋值操作的“同步性”,右侧的计算(pred + curr)使用的是 pred _旧_的值(3),而不是在同一步中被赋了新值(5)的 pred [32:36]。


框架 & 心智模型 (Framework & Mindset)

从这场 Q&A 中,我们可以抽象出两个对于理解现代编程(尤其是函数式编程)至关重要的心智模型。

模型一:“函数即数据”——高阶函数的心智模型

  • 核心思维:在 Python(以及许多其他现代语言)中,函数是“一等公民”(First-class Citizens)。这个比喻意味着,函数与其他数据类型(如整数 int、字符串 str、列表 list)的地位完全平等。

  • 推论:一个函数(一个值),就像任何其他值一样,可以被:

    1. 赋值给变量

      • g = lambda x: x + 2

      • my_adder = make_adder

    2. 作为参数传递给另一个函数

      • search(is_sqrt_of_16)

      • 这是 lambda 的主要用途:search(lambda x: x**2 == 16)

    3. 作为另一个函数的返回值

      • return adder(在 make_adder 内部)

      • return inverse_of_f(在 inverse 内部)

  • 关键区分(再次强调)

    • 你必须在脑中清晰地区分 f(函数对象)f(x)(函数调用结果) [23:34]。

    • f 是“配方”本身。

    • f(x) 是“按照配方做出来的蛋糕”。

    • 高阶函数是对“配方”进行操作:search 需要一个“配方”作为输入;make_adderinverse 则是“制造新配方”的工厂。当你需要传递“配方”时,你绝不能带上括号,否则你就把“蛋糕”(一个数字)传递过去了,这会导致类型错误 [24:01]。

  • 带来的力量

    • 这种心智模型是**抽象(Abstraction)**的基石。

    • search 函数是通用的。它不关心你到底在“搜索”什么(是平方根?还是某个日志文件中的特定行?)。它的逻辑(“从 0 开始一个一个试”)是抽象的。

    • 你通过传入一个_特定_的函数 f,来_具体化_ search 的行为。f 成了 search 行为的“配置”。

    • 这使得我们可以编写出更小、更通用、可复用性更强的代码(例如 search),然后通过 lambdadef 定义的小函数将其“组装”起来,解决复杂问题。

模型二:“函数如何‘记忆’”——闭包与环境模型

  • 核心问题:当 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]:

    1. 调用 make_adder(1)

      • Python 创建一个帧(Frame)(我们称之为 F1),用于 make_adder 的本地变量。

      • F1 中,n 被绑定到 1

    2. 定义 adder

      • make_adder(1) 执行期间,def adder(y): ... 语句被执行。

      • Python 创建了一个函数对象adder)。

      • 关键:这个 adder 函数对象内部包含一个“指针”,指向它被创建时的环境,即 F1 帧 [17:40]。这个指针就是它与生俱来的“记忆”。

    3. return adder

      • make_adder 将这个“携带记忆”的 adder 函数对象返回。

      • make_adder(1) 调用结束,F1 帧从“调用栈”中移除。但是,由于 adder 函数对象(现在被 add_one 变量引用)仍然“指向”F1F1 帧在内存中并不会被销毁

    4. 调用 add_one(2)(即 adder(2) [21:56]:

      • Python 创建一个的帧(F2),用于 adder 的调用。

      • F2 中,参数 y 被绑定到 2

    5. 执行 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 函数能“记住”fg 分别是什么 [17:04]。