要做出最小的可重现示例,复制程序,删掉所有不必要的部分,并保留错误的结果。写下你预期的结果和实际得到的结果。当问题能缩进五行之内,原因通常就看得见了。
这是最有用的调试(debugging)习惯之一,因为它用测试取代猜测。它也适用于 Computer Science 里的追踪和解释题,这类题要求你说出一段代码做了什么。
为什么 bug 会藏在长程序里?
程序越长,bug 越容易躲起来。你也容易最信任自己写得最仔细的那几行,而错误常常就在那里。
缩小就是把不相干的行清走,剩下的几行没有地方可以躲。
怎样缩小程序
- 各用一行写下预期结果和实际结果。
- 复制程序,以免弄坏原件。
- 把真实输入换成很小的自编输入,但仍然出错。
- 每次删除一部分,每删一次就重新运行。
- 如果错误的结果消失了,就撤销这次删除。你删掉的部分很重要。
- 当再删任何东西都会改变结果时就停下来,用一句话解释原因。
例题
原程序。老师规定及格分是 50,刚好 50 分算及格:
names = ["Aiman", "Bee Ling", "Chandran", "Dina"]
marks = [72, 50, 38, 65]
pass_mark = 50
passed = 0
total = 0
for i in range(len(marks)):
total = total + marks[i]
if marks[i] > pass_mark:
passed = passed + 1
average = total / len(marks)
print("Average:", average)
print("Passed:", passed)
预期:3 人及格(72、50 和 65)。实际:2。平均分 225 ÷ 4 = 56.25 是对的,所以计算平均分的代码没有问题。
删掉 names 列表、total 和 average。及格人数仍然是错的。再把四个分数换成一个:50。
marks = [50]
passed = 0
for m in marks:
if m > 50:
passed = passed + 1
print(passed)
预期:1。实际:0。只剩五行,而且只有一行可以怪罪:m > 50 把刚好 50 分排除在外,但题目说 50 分算及格。
要留意的错误
有学生随意删行,直到错误答案不再出现,就断定最后删掉的那一行是 bug。但他同时也把分数列表里的 50 删掉了,问题正是因此才消失。
修正方法:每删一次就重新运行,检查是否仍得到同样的错误结果。如果结果变了,就撤销这次删除。可重现的示例必须和原程序以同样的方式出错。
自我检测
1. 为什么缩小之前要先写下预期结果?
显示答案
没有它,你就无法判断某次删除是否保住了 bug。你需要一个固定的目标,来和每一次重新运行的结果比较。
2. 删掉一个循环后,程序打印的是 1,而不是之前错误的 0。你该怎么做?
显示答案
撤销这次删除。错误的结果改变了,说明这个循环是问题的一部分。
3. 某程序统计 50 个分数的列表中有多少个至少是 60,结果不对。写出两个缩小的步骤。
显示答案
改用只有两三个分数的列表,其中包含一个刚好 60 的分数。然后删掉不影响统计的打印语句和额外计算。
接下来
这个习惯与修正差一错误(off-by-one)以及运行代码前先说明预期结果相连。接着,用改代码之前先设计边界测试把 bug 变成测试。你可以在安全的 Python 推理沙盒里练习自己的小题目。
如果你能缩小 bug,却仍难以解释,一对一 Computer Science 补习可以让老师陪你追踪那个小示例。Computer Science 学习指南显示了调试在课程大纲中的位置。