高阶函数与 lambda
学到这你大概已经能写"命令式"程序了:定义几个函数、调来调去、用
if/for控制流程。
但 CS61A 真正的第一个分水岭在这一篇——当你意识到"函数"可以像数字一样被传来传去、被装进盒子、被工厂批量生产,"编程"这件事的维度就升了一级。
这种能"吃函数、拉函数"的函数,叫高阶函数(higher-order function)。它是后面装饰器、回调、甚至整个函数式编程的地基。
一、什么是"一等公民":函数不是语法糖,是值
是什么:在 Python 里,函数和数字、字符串一样,是货真价实的对象(object)。这意味着函数可以:被赋值给变量、当参数传给别的函、作为返回值、存进容器。具备这种待遇的,叫一等公民(first-class citizen)。
1 | def square(x): |
输出:
1 | 16 |
注意 f = square 后面没有括号——这把"函数对象本身"交给了 f,而不是把调用结果交出去。很多初学者在这里栽:分不清"函数"和"函数的返回值"。
坑:函数名加括号才是调用,不加括号是引用。下面这行 bug 极隐蔽:
1 | buttons = [] |
为什么三个都打 2?因为三个 lambda 共享同一个变量 i,等它们真正执行时,循环早跑完、i 已是 2。这叫闭包的延迟绑定(late binding),我们下面专门拆。
本质一句话:函数是一等公民 = 它能像普通值一样被传、被存、被返回;分清"加括号调用"和"不加括号引用"是第一道坎。
二、把函数当参数:自己造一遍 map / filter
是什么:高阶函数最经典的三种用法就是 map(映射)、filter(过滤)、reduce(归约)。Python 内置了前两个,但自己写一遍才能真正理解"函数当参数"意味着什么。
1 | def my_map(fn, seq): |
输出:
1 | [1, 4, 9, 16, 25] |
my_map 根本不在乎 fn 是平方还是开方——它只负责"把序列里每个元素喂给 fn"。把变化的部分(fn)抽出来当参数,不变的部分(遍历)留下来,这就是"策略模式"的雏形,也是高阶函数最大的价值:复用骨架、定制行为。
坑:别为了"显得函数式"而强行 map/filter 嵌套。当逻辑一复杂,清晰的 for 循环往往比三层 map(lambda...) 更易读。可读性是第一优先级。
本质一句话:高阶函数 = 把"变化的行为"参数化;map/filter/reduce 是这套思想最干净的三种表达。
三、lambda:一次性匿名函数
是什么:lambda 用来写没有名字的小函数,语法是 lambda 参数: 表达式。它适合只在一处使用、逻辑只有一行的情况。
1 | add = lambda a, b: a + b |
坑一:lambda 体只能是一个表达式,不能写 if/else 语句块、for、赋值等。需要多行逻辑就得老老实实 def。
坑二(最重要):回到篇头那个延迟绑定。要修复循环里的 lambda,得立刻把当前值"冻"进默认参数:
1 | buttons = [] |
lambda i=i: 利用"默认参数在定义时就绑定"的特性,把循环变量 i 的当时值快照进形参 i,从而切断了和外界 i 的联系。这是面试和作业里超高发的翻车点,记住它。
本质一句话:lambda 是"一行的匿名函数",只能写表达式;要避坑就用默认参数冻结外层变量。
四、函数返回函数:闭包登场
是什么:高阶函数不仅能"收"函数,还能返回函数——这引出了 CS61A 最重要的概念之一:闭包(closure)。返回的函数会"记住"它定义时所在环境里的变量,哪怕那个环境早已"返回"了。
1 | def make_adder(n): # 返回一个"加 n"的函数 |
输出:
1 | 8 |
make_adder(5) 返回的 adder 记住了 n=5;make_adder(10) 记住了 n=10。这就是函数工厂——用一份模板批量造出"行为各异"的函数。结合篇一的环境模型看:adder 的帧里挂着定义时的 n,所以 add5 和 add10 是两只独立的闭包,互不干扰。
坑:闭包"记住的是变量,不是值"。如果外部变量后来变了,闭包看到的是变后的:
1 | def counter(): |
这里用 nonlocal 明确"我要改外层 count",否则 count += 1 会报 UnboundLocalError。nonlocal 是 Python 闭包里修改外层可变状态的钥匙。
本质一句话:闭包 = 返回的函数带着定义时的环境;它记的是变量引用,所以能"记住"也能"改写"外层状态(需 nonlocal)。
五、C 对照:把函数当值有多麻烦
CS61A 用 Python,但底层世界(OS、数据库)多是 C。C 里函数不是一等公民——没有"函数对象"这种东西,要传函数得靠函数指针,而且语法相当拧巴:
1 |
|
对照要点:Python 原生把函数当值,随手传;C 必须用函数指针 +
typedef,且没有闭包——想"记住外部变量"得自己手写结构体把状态打包传进去。这正是动态语言在"描述行为"上比 C 省力一个量级的原因。
六、常见坑速查(面试高频)
| 坑 | 现象 | 修法 |
|---|---|---|
f = square() 多写括号 |
把返回值(常是 None)当函数传 |
去掉括号,传 square |
lambda 循环延迟绑定 |
全拿到最后一个值 | lambda x=x: 冻结 |
滥用 map/filter 嵌套 |
可读性崩塌 | 复杂逻辑用 for |
闭包改外层变量忘 nonlocal |
UnboundLocalError |
加 nonlocal |
小结
- 一等公民:函数可赋值、传参、返回、入容器;分清"加括号调用"与"不加括号引用"。
- 高阶函数:把变化的行为参数化;
map/filter/reduce是三种经典形态。 - lambda:一行匿名函数,只写表达式;用默认参数冻结外层变量避坑。
- 闭包:返回的函数带着定义时的环境,记的是变量引用;改外层状态需
nonlocal。 - C 对照:函数指针语法拧巴且无闭包,动态语言在"传行为"上省力得多。

