函数、调用表达式与环境模型
很多人学编程是从"记语法"开始的:先背
if怎么写、for怎么写,再背一堆库函数。CS61A 偏不这么干。
它开篇就抛出一个看似哲学的问题:计算机到底是怎么把"名字"和"值"对应起来的?
把这个想明白,后面高阶函数、递归、面向对象都不是新东西,只是同一套"名字—值"规则的变体。
这一篇,我们先把最底层的"调用"和"环境"拆开看透。
一、调用表达式:你以为的"先算哪个"其实是铁律
写 square(3 + 4) 时,大脑下意识就知道要先算 3+4 再平方。但"下意识"在编程里不够——它是一条求值规则,而且顺序不能乱。
是什么:Python 里 操作符(操作数, 操作数, ...) 这种结构叫调用表达式(call expression)。求值分两步,顺序很关键:
- 先按从左到右的顺序,把每个操作数(operand)求值成"值";
- 再把操作符(也就是那个函数对象)作用在已经求好值的操作数上。
1 | def square(x): |
输出:
1 | 49 |
嵌套调用不是特例,只是把规则从最内层一层层往外套。
坑:很多人以为"函数会先执行"。错。解释器在"跳进函数体"之前,会先把所有参数算完。看这个反直觉的例子:
1 | def noisy(x): |
输出:
1 | 进入函数,x = 2 |
注意 print 的打印顺序:两个参数 noisy(1+1) 和 noisy(2*3) 是先从左到右各自求值完(所以先打出两行),最后才把 2+6 加起来打印 8。函数体里的打印发生在"传参阶段",不在"相加阶段"。
本质一句话:调用表达式 = 先求值所有参数、再套用函数;顺序永不变,嵌套只是递归套用同一条规则。
二、环境:名字不是"存在变量里",而是指向一张表
是什么:环境(environment) 是一张"名字 → 值"的映射表。你写下 x = 10,并不是把 10 塞进了叫 x 的盒子里,而是让名字 x 指向 值 10。
每次调用函数,解释器会新建一个局部帧(frame),函数里的形参在这个帧里绑定到实参。查找名字时:
- 先在当前帧找;
- 找不到,就沿环境链(environment chain) 一直往上走到全局环境。
1 | 全局环境 (Global) |
1 | x = 10 # 全局环境里的 x |
输出:
1 | 15 |
坑:x 到底取哪个值,取决于"调用时"的环境链,不取决于"写代码时"的位置。下面这个例子是面试和作业里最高频的翻车点:
1 | def make_counter(): |
inc 里没有 count,它沿着定义时所在的环境链,找到 make_counter 帧里的 count。这就是"闭包"的雏形——我们后面专门讲,这里先建立直觉:名字的值由"定义处的环境链"决定,不是由"调用处的位置"决定。
本质一句话:环境是一张名字→值的表;每次调用开新帧,找名字沿链往上,找不到才去全局。
三、画环境图:让"看不见的绑定"变成图
是什么:环境图(environment diagram)是 CS61A 最核心的武器——它把抽象的"名字绑定"画成可见的框框,让你能一步步推演程序跑完之后每个名字指向哪。
画图的铁律只有三条:
- 函数被定义时,记录它"定义在哪个环境里"(决定它日后找名字的链);
- 函数被调用时,开一个新帧,形参在帧里绑定实参;
- 每个名字查找,从当前帧沿链向上。
1 | 调用 square(7) 之后: |
坑:初学者常把"函数定义"和"函数调用"画混。定义只是把函数对象和环境绑在一起存进全局;只有调用才开帧。一个函数定义十次、调用一次,图上就只有一帧。
本质一句话:环境图 = 定义记环境、调用开帧、查找沿链;它是你调试"名字到底指向哪"的显微镜。
四、赋值 = 绑定,不是"等于"
是什么:= 在编程里是绑定(binding)——让名字指向某个值。它不是数学里的等号(数学等号表示两边恒等)。这个区别看似文字游戏,却是后面"可变性"几乎一切坑的根源。
1 | a = [1, 2, 3] |
输出:
1 | [1, 2, 3, 4] |
b = a 没有复制列表,只是让 b 也指向 a 指向的那个对象。所以通过 b 改,从 a 看也变了。这张图能救命:
1 | a ──┐ |
坑:如果你想要"独立的一份",必须显式复制:b = a.copy() 或 b = a[:]。否则你会花一下午找"谁改了我的列表"。(这个话题我们在篇五"可变性"里展开。)
本质一句话:= 是"让名字指向值",不是"让两边相等";别名让多个名字共享同一个对象。
五、控制流:决定走哪条路
是什么:前面讲的都是"表达式求值"(算出一个值)。程序之所以是程序,是因为有控制流——根据条件选择路径、或重复执行。
| 结构 | 作用 | 易错点 |
|---|---|---|
if/elif/else |
按条件选一条分支 | 多个 elif 只进第一个为真的 |
while |
条件为真就重复 | 忘记改循环变量 → 死循环 |
for |
遍历序列每个元素 | 遍历中修改序列长度会乱 |
1 | def abs_val(n): |
输出:
1 | 5 3 |
坑:return 一旦执行,函数立刻结束,后面的代码都不跑。很多 bug 是"把 return 写在了 if 里,忘了 else 分支也有返回值",导致某些路径返回 None。
本质一句话:控制流决定"走哪条路、走几遍";表达式算值,控制流定路径。
小结
- 调用表达式:先从左到右求值所有参数,再套用函数;嵌套只是同规则递归套用。
- 环境:名字→值的映射表;调用开局部帧,找名字沿链向上,找不到去全局。
- 环境图:定义记环境、调用开帧——调试名字绑定的显微镜。
=是绑定不是等于:别名让多个名字共享同一对象,改一个看全变。- 控制流:
if/while/for决定路径与次数,return立即终止函数。

