📄 一句话概括
纯函数调 100 次只有 1 种结果,带状态的调 100 次有 100 种;差别不在快慢,在【能不能预测】——不能预测的代码,测试只能靠猜。
📝 本节和前面几节的关系
A.6 讲能拼,这一节讲凭什么拼了不出事——因为纯函数不碰外面。
一、实测:100 次调用两种结果
纯函数 pure(5) 调 100 次:结果集合只有 1 种(永远 10)。
带状态 impure(5) 调 100 次:结果集合有 100 种(每次加一)。
追加操作对照:纯版本返回新列表、原列表长度仍是 3;非纯版本把原列表改成了 4。
二、什么叫副作用
改了函数外面的东西:全局变量、传入的对象、文件、数据库、屏幕。 还有一类容易漏:读外面的东西(读时间、读随机数)——也算不纯,因为同输入不同输出。 纯函数只有一条规矩:只看参数、只返回值。
三、为什么这是分水岭
能预测就能测:一个用例抵一百个,因为不用摆环境。 能预测就能并行:谁先谁后都不影响结果。 能预测就能缓存:同输入直接给上次的答案。 反过来,只要有一处改了外面,这三件事全部失效。
四、动手 Demo:特征5:没有副作用
纯函数与带状态函数各调 100 次,数结果种类。 再看追加操作有没有改动原列表。
def pure(x):
return x * 2
state = {"n": 0}
def impure(x):
state["n"] += 1
return x * 2 + state["n"]
print(len({pure(5) for _ in range(100)})) # 1
print(len({impure(5) for _ in range(100)})) # 100
def pure_append(xs, v):
return xs + [v]
src = [1, 2, 3]
pure_append(src, 4)
print(len(src)) # 3
实跑输出:
--------------------------------------------------------------------
特征5:没有副作用
--------------------------------------------------------------------
【无副作用】=同输入必同输出,**且不碰外部世界**。实测两种函数各调 100 次:
测什么 实测
------------------------------------------
纯函数 pure(5) 调 100 次 1 种结果
带状态 impure(5) 调 100 次 100 种结果
纯函数追加后原列表长度 3
非纯追加后原列表长度 4
纯函数的结果种类 1 种(永远同一个值)
非纯函数的结果种类 100 种(每次都不同)
纯版本没动原列表 True(长度仍是 3)
**纯函数调 100 次只有 1 种结果,非纯调 100 次有 100 种。**
**差别不在快慢,在【能不能预测】**——**不能预测的代码,测试只能靠猜。**
**注意纯版本返回的是新列表**(`xs + [v]`),**原列表一个字节没动。**
实测:纯函数 100 次 1 种结果,非纯 100 次 100 种。 纯版本返回新列表,原列表长度仍是 3——一个字节没动。 差别不在快慢,在能不能预测;不能预测,测试只能靠猜。
30 秒实验
- 找出你项目里一个【改传入参数】的函数,改成返回新对象。
- 再找出一个读当前时间的函数,想想它为什么天生不纯。
常见误区
- 以为不改全局变量就叫纯。改传入的对象、读时间、读随机数都不纯。
- 以为纯函数一定更快。纯讲的是可预测,不是性能。
自测三问(点开对答案)
1. 纯函数调 100 次几种结果? —— 1 种;带状态的函数 100 种。 2. 哪些算副作用? —— 改外部变量、改传入对象、写文件、读时间或随机数。 3. 为什么这是分水岭? —— 能预测才能测、才能并行、才能缓存;有一处改外面,三件事全失效。- ☐ A.8 特征6:使用模式匹配和递归来编写循环处理
相关笔记
- 特征4:可以灵活组合函数和参数 — 上一条
- 函数式语言的课题 — 纯的代价
- 实例变量是限定访问范围的全局变量 — 状态的另一面
- 🧩 面向对象是怎样工作的 · 索引