什么是调试?

“当我们开始编程时,我们惊讶地发现,程序不像我们想的那样容易编写正确。调试必须被发现。我记得清楚地记得那一刻,当时我意识到,从那时起,我的大部分时间将花在寻找自己程序中的错误上。” — 莫里斯·威尔克斯(Maurice Wilkes)发现调试,1949年

到现在为止,如果你一直在玩程序,你可能已经发现,有时候程序做的事情并不是你想要的。这是很常见的。调试是找出计算机在做什么,然后让它做你想要它做的事情的过程。这可能很棘手。我曾经花了将近一周的时间来追踪并修复一个错误,那个错误是因为某人把“x”放在了应该是“y”的位置。

本章将比以前的章节更为抽象。

程序应该做什么?

做的第一件事(这听起来很显而易见)是弄清楚程序如果运行正确应该做什么。想出一些测试用例,看看会发生什么。例如,假设我有一个程序用于计算矩形的周长(所有边长的总和)。我有以下测试用例:

height width perimeter
3 4 14
2 3 10
4 4 16
2 2 8
5 1 12

现在我对所有的测试用例运行程序,看看程序是否按照我预期的方式工作。如果没有,那么我需要找出计算机到底在做什么。

更常见的情况是,有些测试用例能通过,有些不能通过。如果是这种情况,你应该尝试找出通过的用例有什么共同点。例如,下面是一个计算周长的程序的输出(你稍后会看到代码):

Height: 3
Width: 4
perimeter = 15
Height: 2
Width: 3
perimeter = 11
Height: 4
Width: 4
perimeter = 16
Height: 2
Width: 2
perimeter = 8
Height: 5
Width: 1
perimeter = 8

注意,前两个输入不正确,接下来的两个是正确的,最后一个不正确。试着找出哪些是正确的,哪些不正确。找出工作正常的测试用例有什么共同点。一旦你有了一些想法,找出问题的根源就会变得更容易。对于你自己的程序,如果需要,你应该尝试更多的测试用例。

程序在做什么?

接下来需要做的事是查看源代码。编程过程中最重要的一件事就是阅读源代码。主要的方式就是代码走查(Code Walkthrough)。

代码走查从程序的第一行开始,逐行检查直到程序结束。while 循环和 if 语句意味着某些行可能永远不会运行,而某些行可能会运行多次。每一行,你都要弄明白 Python 到底在做什么。

让我们从一个简单的计算周长的程序开始。不要直接输入代码,你要读它,而不是运行它。源代码是:

height = int(input("Height: "))
width = int(input("Width: "))
print("perimeter =", width + height + width + width)

问题: Python运行的第一行是什么? 回答: 第一行始终是首先运行的。这里是:height = int(input("Height: "))

这一行做什么? 它打印 Height: ,等待用户输入一个字符串,然后将该字符串转换为一个整数并赋值给变量 height

接下来运行的是哪一行? 通常情况下,接下来运行的是下面的一行:width = int(input("Width: "))

这一行做什么? 它打印 Width: ,等待用户输入一个数字,并将用户输入的数字赋值给变量 width

接下来运行的是哪一行? 因为下一行没有缩进的变化,所以接下来运行的是:print("perimeter =", width + height + width + width)

这一行做什么? 它首先打印 perimeter = ,然后打印 width + height + width + width 的总和。

width + height + width + width 计算周长正确吗? 让我们来看,矩形的周长是底边(width)加上左边(height)加上上边(width)再加上右边(huh?)。最后一项应该是右边的长度,也就是 height。

你理解为什么某些时候周长被计算“正确”了吗? 当 width 和 height 相等时,周长计算是正确的。

接下来我们将对一个程序进行代码走查,这个程序本该在屏幕上打印出5个点。然而,程序输出的是:

. . . .

这里是程序代码:

number = 5
while number > 1:
    print(".",end=" ")
    number = number - 1
print()

这个程序的走查会更复杂,因为它有缩进部分(控制结构)。让我们开始吧。

问题: 第一行运行的是什么? 回答: 文件的第一行:number = 5

它做什么? 将数字 5 放入变量 number

接下来的行是什么? 接下来的行是:while number > 1:

它做什么? while 语句通常检查其表达式,如果为真,它会执行接下来的缩进代码块;如果为假,它会跳过接下来的缩进代码块。

那现在它做什么? 如果 number > 1 为真,接下来的两行代码将被执行。

number > 1 是否为真? 最后赋给 number 的值是5,而 5 > 1 所以是的。

接下来运行的行是什么? 由于 while 为真,接下来运行的是:print(".",end=" ")

这一行做什么? 打印一个点,并且因为 end=" " 这个额外参数的存在,下一次打印的文本不会换行。

接下来运行的行是什么? number = number - 1 因为它是下一行并且没有缩进变化。

它做什么? 它计算 number - 1,即当前的 number(或者是5)减去1,赋值给 number。所以它把 number 的值从5变为4。

接下来运行的行是什么? 由于缩进级别下降,所以我们需要回到 while 循环,检查:while number > 1:

它做什么? 它检查 number 的值,这里是4,判断 4 > 1,为真,继续执行。

接下来运行的行是什么? 由于 while 循环为真,接下来运行的是:print(".",end=" ")

它做什么? 打印第二个点,并且继续在同一行。

接下来运行的行是什么? number = number - 1

它做什么? 它计算 number - 1,即 4 - 1,然后把新值3赋给 number

接下来运行的行是什么? 返回到 while 循环,检查 while number > 1:

它做什么? 它检查 number 的值,这里是3,判断 3 > 1,为真,继续执行。

接下来运行的行是什么? 由于 while 循环为真,接下来运行的是:print(".",end=" ")

它做什么? 打印第三个点。

接下来运行的行是什么? number = number - 1

它做什么? 它计算 number - 1,即 3 - 1,然后把新值2赋给 number

接下来运行的行是什么? 返回到 while 循环,检查 while number > 1:

它做什么? 它检查 number 的值,这里是2,判断 2 > 1,为真,继续执行。

接下来运行的行是什么? 由于 while 循环为真,接下来运行的是:print(".",end=" ")

它做什么? 打印第四个点。

接下来运行的行是什么? number = number - 1

它做什么? 它计算 number - 1,即 2 - 1,然后把新值1赋给 number

接下来运行的行是什么? 返回到 while 循环,检查 while number > 1:

它做什么? 它检查 number 的值,这里是1,判断 1 > 1 为假,所以跳

while 循环。

接下来运行的行是什么? 跳出 while 循环后,执行接下来的行:print()

它做什么? 让屏幕换行。

为什么程序没有打印5个点? 循环多执行了1次,少打印了1个点。

如何修复? 让循环少执行1次。可以通过多种方式来实现。可以将 while 循环修改为:while number > 0:,或者修改条件为:number >= 1,还有其他几种方式。

如何修复我的程序?

你需要弄明白程序在做什么。你需要弄明白程序应该做什么。找出两者之间的区别。调试是一项需要练习才能掌握的技能。如果你一个小时后还没弄明白,休息一下,和别人讨论问题,或者凝视着肚脐沉思。过一会儿再回来,你很可能会对问题有新的思路。祝你好运。

Last modified: Saturday, 11 January 2025, 11:26 AM