← 返回周历 Python 起步 视频 DeepInfer 101
起步 3

起步 3:用状态表示已经完成的工作

浏览文字约 4 分钟 · 手算与练习请按正文分段完成 源文件:Markdown 本地渲染版 · 无需联网
本页目录 · 5 节

先修:前三节,能写遍历列表的循环。目标:独立实现 Q4,解释每轮的累计值,写出一个反例测试。

1. 先观察一段完整的累计程序

本节路线:先填累计状态表,再把“只要最终总数”改成“保留每个边界”,独立完成foundations Q4。先修为起步2的循环与append;命令目录见 起步入口。出口应有长度为0、含零长度段、一般长度三种例子;能说明输出比输入多一项的原因。

total = 0
for length in [2, 0, 3]:
    total = total + length
print(total)

循环结束时得到 5。total 表示到目前为止处理过的长度之和。每一轮之前都成立的这种描述,叫循环不变式。它不是说变量的数值不变,而是说变量所代表的含义一直成立。

如果把 total=0 放进循环,每轮都会忘掉先前的累计值。如果放到函数外面并不断修改全局变量,下一次调用又可能带上上一次的结果。要让每次函数调用独立,状态应在函数内部、循环外部初始化。

2. 从一个总数迁移到全部边界

现在把三段数据接在一起,长度为 [2, 0, 3]。第一段占位置 0、1;第二段为空;第三段占位置 2、3、4。我们需要记录 [0, 2, 2, 5]:每段起点,加最后一段的终点。

已处理段数本段长度累计长度已记录边界
0尚未开始0[0]
122[0, 2]
202[0, 2, 2]
335[0, 2, 2, 5]

第 i 段位于 [offsets[i], offsets[i+1]),方括号表示包含起点,圆括号表示不包含终点。两端相等表示空段,合法且不能丢失。段长度就是两个相邻边界的差。

3. 独立实现 Q4

这次不提供循环框架。先自己写一句不变式,然后决定初始化、循环更新、何时 append、何时 return。输入空列表时也有边界 0,所以输出是 [0],不是 []。

python course/tools/grade.py foundations --question q4 --unlock
python course/tools/grade.py foundations --question q4 --failfast

长度为 n 的输入需要处理 n 项,输出 n+1 个边界;时间与输出空间都随 n 线性增长。这里的计数针对这段 Python 算法,不是 GPU 性能测量。

4. 测试要能推翻一个错误假设

assert actual == expected 表示检查两个值相等;不相等时产生 AssertionError。在 course/foundations/ 新建自己的 check_offsets.py,写入下面完整示例。from ... import ... 表示从同目录的 foundations.py 取出这个函数来调用:

from foundations import prefix_offsets

assert prefix_offsets([1, 2]) == [0, 1, 3]

从仓库根目录运行 python course/foundations/check_offsets.py。没有输出且正常结束表示这个断言通过;有 traceback 就从最后一行看起。

先写期望,再运行代码。期望应来自手算或独立规则,不能直接把程序本次输出抄成期望。

有人把最后的终点漏掉,得到 [0, 1]。有人遇到零长度就跳过,认为“不占空间就不需要记录”。为后者写一个最小反例,并解释除了最终总数以外,还要检查哪些东西。

核对思路(尝试后再看)

输入 [0] 应输出 [0, 0]。虽然总长度仍为 0,但它表示一段空数据,边界数量必须为 2。只检查最终总长度会漏掉这个错误。完整性质包括:第一项为 0,长度为输入长度加一,每个相邻差等于对应段长,最后一项等于所有段长之和。

5. 离开起步单元前

给自己 10 分钟,关闭参考解,换一个输入重新解释代码;再运行全单元测试。现在你已经具备做 调试预备课 的先修能力。那里会第一次出现二维列表;请把它理解为“每一项还是列表”,不必预先学完线性代数。