CS 61A Lab 2 笔记:短路求值、高阶函数与 Lambda 表达式

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 / 0ZeroDivisionError。结果 Error

复制代码
>>> True or 1 / 0

True 为真,or 短路 ,立刻返回 True1 / 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 12 为真 → 继续求右边 → 返回最后一个值 1

依次尝试了 2(左边的值)、True(bool 转换)、Nothing(None 才是 Nothing),都错。答案是 1


Suite 2 Case 2:print 与逻辑运算

复制代码
>>> print(3) or ""
3
''

print(3) 有两件事要分清:

  1. 副作用 :把 3 打印到屏幕 → 第一行输出 3
  2. 返回值print 永远返回 None

None or ""None 为假,or 继续求右边 → 返回 "",REPL 显示 ''

初次第二行答了 None,错。Noneprint 的返回值,它参与了逻辑运算,但 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() 函数体:

  1. print('beets') → 打印 beets(有输出)
  2. 定义内层函数 pie
  3. return pie → 返回 pie 函数对象(注意:不是调用它)

chocolate 现在指向 pie 函数。

beets,正确。

chocolate

在 REPL 里输入变量名,只是"看一眼"它是什么,不调用它。chocolate 是一个函数对象。

依次尝试 sweets(认为会执行函数体)、sweets cake,都错。答案是 Function

chocolate()

现在才真正调用 pie()

  1. print('sweets') → 打印 sweets

  2. 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_cakeTrue

snake(10, 20) :条件成立,返回 chocolate(即 pie 函数对象)→ Function

snake(10, 20)() :调用返回的 pie

  1. print('sweets') → 打印 sweets

  2. 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

前置概念:lambdadef 的区别

复制代码
lambda <参数>: <返回表达式>
对比项 def 语句 lambda 表达式
类型 语句,改变环境(创建绑定) 表达式,求值为函数对象,不改变环境
命名 自动绑定到函数名 匿名,不自动绑定任何名字
函数体 可多行,支持任意语句 只能是单个表达式
返回值 return 冒号后的表达式就是返回值

关键理解lambda 被求值时,只是创建函数对象 ,函数体不执行。只有调用这个函数时,函数体才运行。

Suite 1:概念题

Case 1deflambda 的区别是什么?

正确答案:lambda 不会自动把返回的函数绑定到名字。

def square(x): ... 会在当前环境里创建名字 square;而 lambda x: x*x 只是产生一个函数对象,不创建任何名字。

Case 2lambda a, b: c + d 有几个形式参数?

答案:2 个(ab)。

cd 是函数体里引用的变量,不是参数。注意:cd 需要来自外部作用域(闭包),否则调用时会报 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):调用 bx=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)。传入 squaresquare(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

思路

这是一个返回函数的高阶函数 。外层接收 fg,返回一个单参数函数。内层函数对某个 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)。fg 通过闭包被内层函数捕获。

验证

复制代码
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 规则

函数体里用到的变量名,按以下顺序查找:

  1. Local:当前帧(函数自身)
  2. Enclosing:外层函数的帧(闭包)
  3. Global:全局帧
  4. 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 帧里找到 → 7
  • n 在 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 = 15

    Global 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)。

画环境图的正确步骤

  1. 每遇到 def:在当前帧画出函数对象,标注名字、参数、父帧
  2. 每遇到函数调用:开新帧,标注函数名和父帧,绑定参数
  3. 查找变量时:先看当前帧 → 没有就去父帧 → 再没有去 Global
  4. return 执行后:关闭当前帧,把返回值给调用者
  5. 赋值语句:在当前帧更新绑定(覆盖旧的)

八、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) 返回函数 gg(n) 返回函数 hh(x)x 依次循环应用 f1, f2, f3n 次。

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  ✓
相关推荐
qq_422828622 小时前
android图形学之SurfaceControl和Surface的关系 五
android·开发语言·python
weixin_444012932 小时前
c++如何将std--vector直接DUMP到二进制文件_指针地址直写【附代码】
jvm·数据库·python
woxihuan1234562 小时前
Go语言中--=运算符详解:位右移赋值操作的原理与应用
jvm·数据库·python
石山代码3 小时前
Python 数据分析三大库:NumPy + Pandas + Matplotlib
python·数据分析·numpy
如竟没有火炬3 小时前
用队列实现栈
开发语言·数据结构·python·算法·leetcode·深度优先
yivifu3 小时前
CustomTkinter的布局管理器介绍及应用
python·gui·customtkinter·pdf去水印
m0_690825823 小时前
如何备份被破坏的数据表_强制跳过错误的导出尝试
jvm·数据库·python
m0_733565463 小时前
JavaScript中Reflect-ownKeys获取所有键名的优势
jvm·数据库·python
水木流年追梦4 小时前
大模型入门-应用篇3-Agent智能体
开发语言·python·算法·leetcode·正则表达式