很多人学编程是从"记语法"开始的:先背 if 怎么写、for 怎么写,再背一堆库函数。CS61A 偏不这么干。
它开篇就抛出一个看似哲学的问题:计算机到底是怎么把"名字"和"值"对应起来的?
把这个想明白,后面高阶函数、递归、面向对象都不是新东西,只是同一套"名字—值"规则的变体。
这一篇,我们先把最底层的"调用"和"环境"拆开看透。

一、调用表达式:你以为的"先算哪个"其实是铁律

square(3 + 4) 时,大脑下意识就知道要先算 3+4 再平方。但"下意识"在编程里不够——它是一条求值规则,而且顺序不能乱。

是什么:Python 里 操作符(操作数, 操作数, ...) 这种结构叫调用表达式(call expression)。求值分两步,顺序很关键:

  1. 按从左到右的顺序,把每个操作数(operand)求值成"值";
  2. 把操作符(也就是那个函数对象)作用在已经求好值的操作数上。
1
2
3
4
5
def square(x):
return x * x

print(square(3 + 4)) # 先算 3+4=7,再 square(7) = 49
print(square(square(2))) # 内层 square(2)=4,再 square(4)=16

输出:

1
2
49
16

嵌套调用不是特例,只是把规则从最内层一层层往外套

:很多人以为"函数会先执行"。错。解释器在"跳进函数体"之前,会先把所有参数算完。看这个反直觉的例子:

1
2
3
4
5
def noisy(x):
print("进入函数,x =", x)
return x

print(noisy(1 + 1) + noisy(2 * 3))

输出:

1
2
3
进入函数,x = 2
进入函数,x = 6
8

注意 print 的打印顺序:两个参数 noisy(1+1)noisy(2*3)先从左到右各自求值完(所以先打出两行),最后才把 2+6 加起来打印 8。函数体里的打印发生在"传参阶段",不在"相加阶段"。

本质一句话:调用表达式 = 先求值所有参数、再套用函数;顺序永不变,嵌套只是递归套用同一条规则。

二、环境:名字不是"存在变量里",而是指向一张表

是什么环境(environment) 是一张"名字 → 值"的映射表。你写下 x = 10,并不是把 10 塞进了叫 x 的盒子里,而是让名字 x 指向10

每次调用函数,解释器会新建一个局部帧(frame),函数里的形参在这个帧里绑定到实参。查找名字时:

  • 先在当前帧找;
  • 找不到,就沿环境链(environment chain) 一直往上走到全局环境。
1
2
3
4
5
6
7
8
9
10
11
全局环境 (Global)
┌──────────────────────────┐
│ square ──▶ <function> │
│ x ──▶ 10 │
└──────────────────────────┘
▲ 调用 f(5) 时,挂上局部帧

┌──────────────────────────┐
│ 局部帧 f: y = 5 │
│ 返回 x + y = 15 │
└──────────────────────────┘
1
2
3
4
x = 10                 # 全局环境里的 x
def f(y):
return x + y # 函数体里没有 x,沿环境链到全局拿到 10
print(f(5)) # 15

输出:

1
15

x 到底取哪个值,取决于"调用时"的环境链,不取决于"写代码时"的位置。下面这个例子是面试和作业里最高频的翻车点:

1
2
3
4
5
6
7
8
def make_counter():
count = 0
def inc():
return count + 1 # 这里的 count 沿链找到外层 count=0
return inc

c = make_counter()
print(c()) # 1

inc 里没有 count,它沿着定义时所在的环境链,找到 make_counter 帧里的 count。这就是"闭包"的雏形——我们后面专门讲,这里先建立直觉:名字的值由"定义处的环境链"决定,不是由"调用处的位置"决定

本质一句话:环境是一张名字→值的表;每次调用开新帧,找名字沿链往上,找不到才去全局。

三、画环境图:让"看不见的绑定"变成图

是什么:环境图(environment diagram)是 CS61A 最核心的武器——它把抽象的"名字绑定"画成可见的框框,让你能一步步推演程序跑完之后每个名字指向哪。

画图的铁律只有三条:

  1. 函数被定义时,记录它"定义在哪个环境里"(决定它日后找名字的链);
  2. 函数被调用时,开一个新帧,形参在帧里绑定实参;
  3. 每个名字查找,从当前帧沿链向上。
1
2
3
4
5
6
7
8
9
10
11
调用 square(7) 之后:
全局环境
┌──────────────────────────┐
│ square ──▶ [func: 定义在全局] │
└──────────────────────────┘

│ 新建局部帧
┌──────────────────────────┐
│ square 帧: x = 7 │
│ return x * x = 49 │
└──────────────────────────┘

:初学者常把"函数定义"和"函数调用"画混。定义只是把函数对象和环境绑在一起存进全局;只有调用才开帧。一个函数定义十次、调用一次,图上就只有一帧。

本质一句话:环境图 = 定义记环境、调用开帧、查找沿链;它是你调试"名字到底指向哪"的显微镜。

四、赋值 = 绑定,不是"等于"

是什么= 在编程里是绑定(binding)——让名字指向某个值。它不是数学里的等号(数学等号表示两边恒等)。这个区别看似文字游戏,却是后面"可变性"几乎一切坑的根源。

1
2
3
4
a = [1, 2, 3]
b = a # b 和 a 指向同一个列表对象(别名)
b.append(4)
print(a) # [1, 2, 3, 4]

输出:

1
[1, 2, 3, 4]

b = a 没有复制列表,只是让 b 也指向 a 指向的那个对象。所以通过 b 改,从 a 看也变了。这张图能救命:

1
2
3
4
5
a ──┐

[1, 2, 3, 4] ← 同一个对象

b ──┘

:如果你想要"独立的一份",必须显式复制:b = a.copy()b = a[:]。否则你会花一下午找"谁改了我的列表"。(这个话题我们在篇五"可变性"里展开。)

本质一句话= 是"让名字指向值",不是"让两边相等";别名让多个名字共享同一个对象。

五、控制流:决定走哪条路

是什么:前面讲的都是"表达式求值"(算出一个值)。程序之所以是程序,是因为有控制流——根据条件选择路径、或重复执行。

结构 作用 易错点
if/elif/else 按条件选一条分支 多个 elif 只进第一个为真的
while 条件为真就重复 忘记改循环变量 → 死循环
for 遍历序列每个元素 遍历中修改序列长度会乱
1
2
3
4
5
6
def abs_val(n):
if n < 0:
return -n
return n

print(abs_val(-5), abs_val(3))

输出:

1
5 3

return 一旦执行,函数立刻结束,后面的代码都不跑。很多 bug 是"把 return 写在了 if 里,忘了 else 分支也有返回值",导致某些路径返回 None

本质一句话:控制流决定"走哪条路、走几遍";表达式算值,控制流定路径。

小结

  • 调用表达式:先从左到右求值所有参数,再套用函数;嵌套只是同规则递归套用。
  • 环境:名字→值的映射表;调用开局部帧,找名字沿链向上,找不到去全局。
  • 环境图:定义记环境、调用开帧——调试名字绑定的显微镜。
  • = 是绑定不是等于:别名让多个名字共享同一对象,改一个看全变。
  • 控制流if/while/for 决定路径与次数,return 立即终止函数。