CS 61A Lab 2 笔记:短路求值、高阶函数与 Lambda 表达式
对应资料 :CS 61A Lab 2 · Composing Programs 1.6
内容范围:Q1 短路求值、Q2 高阶函数 WWPD、Q3 Lambda 表达式 WWPD
一、Q1:短路求值(Short Circuiting)
核心规则
Python 的 and / or 采用惰性求值:一旦能确定整个表达式的结果,就立刻停止,不再计算剩余部分。
| 运算符 | 停止时机 | 返回值 |
|---|---|---|
and |
遇到第一个假值 | 那个假值本身 |
and |
全为真,走到最后 | 最后一个值 |
or |
遇到第一个真值 | 那个真值本身 |
or |
全为假,走到最后 | 最后一个值 |
最容易记错的地方 :and / or 返回的不一定是 True / False,而是某个操作数的原始值。
测试题逐条解析
Suite 1 Case 1
>>> True and 13
True 是真值,and 继续求右边,返回最后一个值 13(而非 True)。
初次答了
True,错。规则:and 返回最后一个被求值的操作数本身。
>>> False or 0
False 是假值,or 继续求右边,返回最后一个值 0。结果 0。
>>> not 10
10 是真值,not 取反 → False。
>>> not None
None 是假值,not 取反 → True。
Suite 2 Case 1
>>> True and 1 / 0
True 为真,and 必须继续求右边 → 执行 1 / 0 → ZeroDivisionError。结果 Error。
>>> True or 1 / 0
True 为真,or 短路 ,立刻返回 True,1 / 0 根本没被执行 。结果 True。
初次答了
Error,错。短路的关键:右边的代码可能完全不运行。
>>> -1 and 1 > 0
运算符优先级:> 高于 and,等价于 (-1) and (1 > 0)。
-
-1为真(非零即真)→ 继续求右边 →1 > 0=True→ 返回True。-1 or 5
-1 为真 → or 短路,返回 -1 本身(不是 True!)。
初次答了
True,错。or返回的是操作数原始值,不做 bool 转换。
>>> (1 + 1) and 1
2 and 1:2 为真 → 继续求右边 → 返回最后一个值 1。
依次尝试了
2(左边的值)、True(bool 转换)、Nothing(None 才是 Nothing),都错。答案是1。
Suite 2 Case 2:print 与逻辑运算
>>> print(3) or ""
3
''
print(3) 有两件事要分清:
- 副作用 :把
3打印到屏幕 → 第一行输出3 - 返回值 :
print永远返回None
None or "" → None 为假,or 继续求右边 → 返回 "",REPL 显示 ''。
初次第二行答了
None,错。None是or最终返回的是"",而不是None。
Suite 3 Case 1:函数返回值参与逻辑运算
def f(x):
if x == 0: return "zero"
elif x > 0: return "positive"
else: return ""
>>> 0 or f(1)
0 为假 → 继续求右边 → f(1) 返回 "positive" → 返回 "positive"。
初次答
positive(没有引号),错。REPL 显示字符串时带引号:'positive'。
>>> f(0) or f(-1)
f(0) 返回 "zero","zero" 为真 → 短路,返回 "zero"。
>>> f(0) and f(-1)
f(0) 返回 "zero",为真 → 继续求右边 → f(-1) 返回 "" → and 返回 ""(空字符串是假值,是 and 遇到的第一个假值)。
二、Q2:高阶函数 WWPD
前置概念:print vs return,函数对象 vs 函数调用
| 概念 | 含义 |
|---|---|
f |
函数对象本身,不执行任何代码,REPL 显示 Function |
f() |
调用函数,执行函数体,得到返回值 |
print(x) |
把 x 显示到屏幕(副作用),返回 None |
return x |
把 x 传出函数,调用者可以拿到这个值 |
测试题逐条解析
def cake():
print('beets')
def pie():
print('sweets')
return 'cake'
return pie
chocolate = cake()
执行 cake() 函数体:
print('beets')→ 打印beets(有输出)- 定义内层函数
pie return pie→ 返回pie函数对象(注意:不是调用它)
chocolate 现在指向 pie 函数。
答
beets,正确。
chocolate
在 REPL 里输入变量名,只是"看一眼"它是什么,不调用它。chocolate 是一个函数对象。
依次尝试
sweets(认为会执行函数体)、sweets cake,都错。答案是Function。
chocolate()
现在才真正调用 pie():
-
print('sweets')→ 打印sweets -
return 'cake'→ 返回字符串'cake',REPL 显示'cake'sweets
'cake'
初次答了
beets(以为还在 cake 的上下文里),错。chocolate已经是pie,调用它只执行pie的函数体。
more_chocolate, more_cake = chocolate(), cake
chocolate()= 调用pie(),打印sweets,返回'cake'cake= cake 函数对象本身(没有调用)
这一行的副作用:打印 sweets,仅此一行输出。
more_chocolate
more_chocolate = 'cake'(字符串),REPL 显示 'cake'。
more_cake
more_cake = cake 函数对象,REPL 显示 Function。
snake 函数
def snake(x, y):
if cake == more_cake:
return chocolate
else:
return x + y
此时:cake 是函数(全局定义的 cake 函数),more_cake 也是函数(cake 的引用)→ cake == more_cake 为 True。
snake(10, 20) :条件成立,返回 chocolate(即 pie 函数对象)→ Function。
snake(10, 20)() :调用返回的 pie:
-
print('sweets')→ 打印sweets -
return 'cake'→ REPL 显示'cake'sweets
'cake'
初次答
'cake'(一行),错。print('sweets')是额外的输出,不能省略。
cake = 'cake'
把全局的 cake 重新绑定为字符串 'cake'。
再次 snake(10, 20) :cake(字符串 'cake')== more_cake(函数对象)→ False → 执行 else,返回 x + y = 10 + 20 = 30。
三、Q3:Lambda 表达式 WWPD
前置概念:lambda 与 def 的区别
lambda <参数>: <返回表达式>
| 对比项 | def 语句 |
lambda 表达式 |
|---|---|---|
| 类型 | 语句,改变环境(创建绑定) | 表达式,求值为函数对象,不改变环境 |
| 命名 | 自动绑定到函数名 | 匿名,不自动绑定任何名字 |
| 函数体 | 可多行,支持任意语句 | 只能是单个表达式 |
| 返回值 | 用 return |
冒号后的表达式就是返回值 |
关键理解 :lambda 被求值时,只是创建函数对象 ,函数体不执行。只有调用这个函数时,函数体才运行。
Suite 1:概念题
Case 1 :def 和 lambda 的区别是什么?
正确答案:
lambda不会自动把返回的函数绑定到名字。
def square(x): ... 会在当前环境里创建名字 square;而 lambda x: x*x 只是产生一个函数对象,不创建任何名字。
Case 2 :lambda a, b: c + d 有几个形式参数?
答案:2 个(
a和b)。
c 和 d 是函数体里引用的变量,不是参数。注意:c 和 d 需要来自外部作用域(闭包),否则调用时会报 NameError。
Case 3:lambda 的返回表达式什么时候执行?
正确答案:当 lambda 返回的函数被调用时。
初次选了"当 lambda 被求值时",错。lambda求值只创建函数对象,body 在调用时才运行------和def完全一致。
Suite 2 Case 1:WWPD
>>> lambda x: x
单独一个 lambda 表达式,没有被赋值也没有被调用,求值得到函数对象 → Function。
>>> a = lambda x: x
>>> a(5)
a 是恒等函数,a(5) 返回 5。
>>> (lambda: 3)()
lambda: 3 是无参数的 lambda,(...)() 立即调用它 → 返回 3。
lambda 返回 lambda:核心难点
>>> b = lambda x, y: lambda: x + y
>>> c = b(8, 4)
>>> c
拆解 b:
# b 等价于:
def b(x, y):
def inner():
return x + y # x, y 捕获进闭包
return inner
c = b(8, 4):调用 b,x=8, y=4 被捕获,返回内层 lambda: x + y 的函数对象。c 是那个内层函数,还没被调用。
c:在 REPL 里查看 c,它是函数对象 → Function。
依次尝试:
12(调用结果)、lambda: 8 + 4(源码不是显示方式)、c()(表达式不是值)、<function <lambda> at ...(太接近了!但题目规定显示<function...>就答Function),共尝试 7 次后答对。
>>> c()
现在调用内层函数,执行 x + y = 8 + 4 = 12。
>>> d = lambda f: f(4)
>>> def square(x):
... return x * x
>>> d(square)
d 接受函数 f,调用 f(4)。传入 square,square(4) = 16。→ 16。
Suite 2 Case 2:复合 lambda
>>> higher_order_lambda = lambda f: lambda x: f(x)
>>> g = lambda x: x * x
>>> higher_order_lambda(2)(g)
higher_order_lambda(2):用 f=2 调用外层 lambda,得到 lambda x: 2(x)。然后 (g) 表示 x=g,执行 2(g)------用整数 2 作为函数来调用 → TypeError,结果 Error。
初次答了
4(误以为g(2)),错。参数顺序至关重要:higher_order_lambda(2)先把2绑定给f,结果f=2不是函数。
>>> higher_order_lambda(g)(2)
higher_order_lambda(g):f=g,得到 lambda x: g(x)。再调用 (2):g(2) = 4。→ 4。
>>> call_thrice = lambda f: lambda x: f(f(f(x)))
>>> call_thrice(lambda y: y + 1)(0)
call_thrice(lambda y: y + 1) 返回 lambda x: f(f(f(x))),其中 f = lambda y: y + 1。
调用 (0):f(f(f(0))) = f(f(1)) = f(2) = 3。→ 3。
print 作为 lambda 函数体
>>> print_lambda = lambda z: print(z)
>>> print_lambda
print_lambda 是函数对象 → Function。
初次答
None,错。没有调用它,print(z)根本没执行。
>>> one_thousand = print_lambda(1000)
1000
调用 print_lambda(1000):执行 print(1000),打印 1000(这是副作用/输出)。print 返回 None,所以 one_thousand = None。
>>> one_thousand
one_thousand 的值是 None。在 REPL 里,None 不显示任何内容 (REPL 只显示非 None 的返回值)→ Nothing。
初次答
None,错。None是值,但 REPL 对None特殊处理------什么都不显示,所以答Nothing。
四、核心认知总结
经过这三组 WWPD 练习,有几个反复踩坑的地方值得专门记下来:
1. and / or 返回操作数本身,不做 bool 转换
-1 or 5 # → -1,不是 True
2 and 1 # → 1,不是 True
2. print 的副作用(输出)和返回值(None)是两回事
print(3) or ""
# 输出:3 ← print 的副作用
# 返回值:'' ← or 的最终结果(None 为假,取右边的 "")
3. 函数对象 vs 函数调用
chocolate # 函数对象,REPL 显示 Function,函数体不执行
chocolate() # 调用函数,函数体执行,得到返回值
4. lambda 的求值时机
lambda 被求值(包括赋值给变量、传入函数时)只是创建函数对象 ,函数体不执行。只有加了 () 调用时,函数体才运行。
5. lambda 嵌套时的参数绑定顺序
higher_order_lambda = lambda f: lambda x: f(x)
higher_order_lambda(g)(2) # f=g,x=2 → g(2) = 4 ✓
higher_order_lambda(2)(g) # f=2,x=g → 2(g) = Error ✗
每一次 () 调用只绑定最外层那个函数的参数,从左到右一层一层剥开。
五、Q4:Composite Identity Function
题目要求
实现 composite_identity(f, g),返回一个函数,该函数对输入 x 返回 True 如果 f(g(x)) == g(f(x)),否则返回 False。
思路
这是一个返回函数的高阶函数 。外层接收 f 和 g,返回一个单参数函数。内层函数对某个 x,分别计算两种复合顺序的结果,比较是否相等。
def composite_identity(f, g):
return lambda x: f(g(x)) == g(f(x))
用 lambda 一行完成:接受 x,计算 f(g(x)) 和 g(f(x)),返回比较结果(True/False)。f 和 g 通过闭包被内层函数捕获。
验证
add_one = lambda x: x + 1
square = lambda x: x ** 2
b1 = composite_identity(square, add_one)
b1(0) # square(add_one(0)) = (0+1)² = 1
# add_one(square(0)) = 0²+1 = 1 → 1 == 1 → True
b1(4) # square(add_one(4)) = (4+1)² = 25
# add_one(square(4)) = 4²+1 = 17 → 25 ≠ 17 → False
六、Q5:Count Cond ------ 抽象的核心思路
先看两个具体实现
def count_fives(n):
i, count = 1, 0
while i <= n:
if sum_digits(n * i) == 5: # ← 判断条件(使用了 n 和 i)
count += 1
i += 1
return count
def count_primes(n):
i, count = 1, 0
while i <= n:
if is_prime(i): # ← 判断条件(只使用了 i)
count += 1
i += 1
return count
相同 vs 不同
| 部分 | 说明 |
|---|---|
| 相同:循环框架 | 从 1 到 n 遍历,用计数器累加,最后返回 |
不同 :if 里的条件 |
每个函数判断"什么算符合条件"各不相同 |
文档的问题指引:
- "相同的部分"→ 告诉我们
count_cond内层函数的固定逻辑:循环框架 - "不同的部分"→ 告诉我们在哪里留出可变性 :那个
if判断条件作为参数传入
设计 count_cond
count_cond 是一个高阶函数,它的结构是:
-
外层 接收
condition(两参数的谓词函数) -
返回 一个接受
n的函数(闭包捕获condition) -
内层 包含两个函数共同的循环框架,用
condition(n, i)替换原来的具体判断def count_cond(condition):
def counter(n):
count, i = 0, 1
while i <= n:
if condition(n, i): # 把"判断什么"交给外部决定
count += 1
i += 1
return count
return counter
为什么 condition 接受两个参数 (n, i)?
count_fives 的条件用了 n * i(两个都需要),count_primes 的条件只用了 i,但为了统一接口,condition 固定接受 (n, i) 两个参数。count_primes 的条件写成 lambda n, i: is_prime(i) 来适配这个接口(n 接收了但不使用)。
使用方式
# 用 lambda 直接内联条件
count_fives = count_cond(lambda n, i: sum_digits(n * i) == 5)
count_primes = count_cond(lambda n, i: is_prime(i))
count_fives(10) # 1
count_primes(20) # 8
count_cond 把"循环计数"这个通用框架提取出来,condition 参数决定"数什么"。这是 1.6 节高阶函数"将通用模式与具体行为分离"思想的直接体现。
七、Q6:HOF 环境图 ------ 理解混乱的根源与解法
代码
n = 7
def f(x):
n = 8
return x + 1
def g(x):
n = 9
def h():
return x + 1
return h
def f(f, x): # 注意:这里重新定义了 f,参数也叫 f!
return f(x + n)
f = f(g, n)
g = (lambda y: y())(f)
混乱的根源:三件事必须分清
1. 名字(Name)和对象(Object)是分开的
def f(x): ... 和 def f(f, x): ... 都叫 f,但它们是两个不同的函数对象。第二个 def 执行后,全局的 f 被重新绑定到新函数。名字只是标签,可以换,对象本身不变。
2. 变量查找遵循 LEGB 规则
函数体里用到的变量名,按以下顺序查找:
- Local:当前帧(函数自身)
- Enclosing:外层函数的帧(闭包)
- Global:全局帧
- Built-in:内置
每个帧只负责它自己定义的变量,不会"污染"其他帧。
3. 函数的父帧(Parent Frame)决定了闭包查找的起点
函数在哪个帧里被定义,那个帧就是它的父帧。调用时产生的新帧,向上查找变量时,从父帧开始,而不是从调用者的帧开始。
逐步追踪
步骤 1~4:全局准备
Global frame:
n → 7
f → func f(x) # 第一个 f
Global frame:
n → 7
f → func f(x)
g → func g(x)
Global frame:
n → 7
f → func f(f, x) # 第二个 f 覆盖了第一个!
g → func g(x)
第一个
f(x)已经从全局环境消失,但它的函数对象还存在(只是没有名字指向它了)。
步骤 5:f = f(g, n)
调用第二个 f(f, x) = f(g, 7),开一个新帧:
f1: f(f, x) [parent=Global]
f → func g(x) # 参数 f 绑定到 g 函数
x → 7
执行 return f(x + n):
f在 f1 帧里找到 →func g(x)x在 f1 帧里找到 →7n在 f1 帧里找不到 → 去父帧 Global 找 →n = 7- 所以执行
g(7 + 7)=g(14)
调用 g(14),开一个新帧:
f2: g(x) [parent=Global]
x → 14
n → 9 # g 内部定义了自己的 n=9,遮蔽了全局的 n=7
执行 g 的函数体:定义内层函数 h(此时 h 的父帧是 f2,捕获了 x=14),然后 return h。
f(g, 7) 最终返回 h 函数对象。
Global frame:
n → 7
f → func h() # f 现在指向 h!
g → func g(x)
步骤 6:g = (lambda y: y())(f)
lambda y: y() 被立即调用,参数是 f(即 h):
y绑定为h- 执行
y()=h()
调用 h(),开新帧:
f3: h() [parent=f2] # h 的父帧是 f2!
执行 return x + 1:
-
x在 f3 里找不到 → 去父帧 f2 里找 →x = 14 -
返回
14 + 1 = 15Global frame:
n → 7
f → func h()
g → 15 # g 现在是整数 15!
截图对应的时刻(Step 10 of 20)
截图展示的是正在执行 g(14) 的函数体 (f2 帧已打开,x=14, n=9),下一步即将定义 h。此时:
- f1 帧展示了
f=func g, x=7 - f2 帧展示了
x=14, n=9
这是图示最密集的时刻:两个帧同时活跃,n 有三个不同的值(全局 7,f1 的参数名 f 恰好叫 f,f2 内部 9)。
画环境图的正确步骤
- 每遇到
def:在当前帧画出函数对象,标注名字、参数、父帧 - 每遇到函数调用:开新帧,标注函数名和父帧,绑定参数
- 查找变量时:先看当前帧 → 没有就去父帧 → 再没有去 Global
return执行后:关闭当前帧,把返回值给调用者- 赋值语句:在当前帧更新绑定(覆盖旧的)
八、Q7(Optional):Multiple
def multiple(a, b):
"""Return the smallest number n that is a multiple of both a and b."""
n = a
while n % b != 0:
n += a
return n
从 a 开始,每次加 a(保证是 a 的倍数),直到同时也是 b 的倍数为止。本质是枚举 a 的倍数序列:a, 2a, 3a, ...,找到第一个能被 b 整除的。
九、Q8(Optional):Cycle ------ 三层嵌套函数
题目
cycle(f1, f2, f3) 返回函数 g,g(n) 返回函数 h,h(x) 对 x 依次循环应用 f1, f2, f3 共 n 次。
| n | h(x) 的计算 |
|---|---|
| 0 | x |
| 1 | f1(x) |
| 2 | f2(f1(x)) |
| 3 | f3(f2(f1(x))) |
| 4 | f1(f3(f2(f1(x)))) |
| 6 | f3(f2(f1(f3(f2(f1(x)))))) |
关键 :用 n % 3 映射到 {0, 1, 2} 对应三个函数,再用列表 [f1, f2, f3] 按下标取。
def cycle(f1, f2, f3):
def g(n):
def h(x):
funcs = [f1, f2, f3]
i = 0
while i < n:
x = funcs[i % 3](x)
i += 1
return x
return h
return g
f1, f2, f3 被最外层闭包捕获,n 被中间层闭包捕获,最内层 h 执行实际的循环应用。
验证
my_cycle = cycle(add1, times2, add3)
my_cycle(3)(2)
# i=0: funcs[0](2) = add1(2) = 3
# i=1: funcs[1](3) = times2(3) = 6
# i=2: funcs[2](6) = add3(6) = 9 ✓