第6 章 测试与调试

我真的不想指出这一点,但潘格洛斯博士①确实错了,我们并未生活在一个“可能是最好的
世界”中。有的地方滴雨不落,有的地方大雨倾盆;有的地方天寒地冻,有的地方赤日炎炎,而
有的地方则冬日严寒、夏季酷热。有时股市会呈现断崖式暴跌。还有,令人恼火的是,我们的程
序在第一次运行时总不正常。
关于如何处理最后一个问题的书已经够多了,读读这些书你会学到很多。但为了给你一些启
示,帮助你及时解决遇到的问题,我们在本章会高度精简地讨论程序的测试与调试。尽管所有编
程示例都是使用Python编写的,但其中的基本原则可以应用于任何复杂系统的调试。
测试指通过运行程序以确定它是否按照预期工作。调试则指修复已知的未按预期工作的
程序。
测试和调试不是你编写完程序之后才开始考虑的问题,优秀的程序员在设计程序时,就已经
开始考虑如何使程序易于测试和调试了。关键就是将程序分解成独立的部件,可以在不受其他部
件影响的情况下实现、测试和调试。本书至此为止只讨论了一种程序模块化的机制——函数,所
以现在我们的所有示例都基于函数。当我们讲到其他模块化机制,特别是类的时候,会继续讨论
本章的一些主题。
使程序正确工作的第一步是让语言系统允许程序运行,也就是说,要消除不需运行程序就可
以检测到的语法错误和静态语义错误。如果你还没有解决程序中的这些问题,那么就还没有做好
准备学习这一章。再在你的程序上花点时间吧,然后回来继续阅读。
6.1 测试
关于测试,最重要的是清楚它的目的是证明错误的存在,而不是证明程序没有错误。艾兹赫
尔·戴克斯特拉说过:“程序测试是用来证明错误的存在,而不是展示没有错误!”②据说爱因斯
坦也说过:“多少次实验也不能证明我是对的,但一次实验就可以证明我是错的。”
为什么会这样呢?因为即使是最简单的程序也有无数种可能的输入。例如,考虑一个想满足
以下规范的程序:

def isBigger(x, y):
"""假设x和y是整数
如果x小于y则返回True,否则返回False。"""
毫不夸张地说,使用所有整数对运行这个程序会非常枯燥乏味。最好的方式是,只使用一些
特殊的整数对来运行程序,如果程序中有错误,那么这些整数对就应该极有可能产生错误答案。
测试的关键就是找到这样一组输入,可以称之为测试套件。它有很大可能发现程序错误,又
不需要程序运行太长时间。找到这样的输入的关键是,对所有可能的输入空间进行分区,将其划
分为对程序正确性提供相同信息的多个子集,然后构建测试套件,使其包含来自每个分区的至少
一个输入。(一般情况下,构建这样一个测试套件实际上是不可能的,可以把它看成是一个不能
达到的理想状态。)
集合的分区可以将集合分割为多个子集,并使得初始集合中的每个元素都恰好属于一个子
集。例如,对于isBigger(x, y),可能的输入集合为所有整数的成对组合。对这个集合我们可
以将其划分为7个子集:
x为正,y为正 x为负,y为负
x为正,y为负 x为负,y为正
x = 0,y = 0 x = 0,y≠0 x≠0,y≠0
如果使用来自每个子集的至少一个值对函数实现进行测试,就非常有可能(不一定)暴露可
能存在的错误。
对于多数程序,适当地划分输入空间说起来容易,做起来就太难了。我们通常需要将代码和
规范结合起来,进行各种路径探索,并在此基础上发展出一种启发式方法。基于代码探索路径的
启发式方法称为白盒测试,基于规范探索路径的启发式方法称为黑盒测试。
6.1.1 黑盒测试
理论上,构建黑盒测试时不需要查看要测试的代码。黑盒测试允许测试者和开发者来自不同
人群。当我们这些讲授编程课程的教师为分配给学生的问题集合生成测试案例时,就是在建立黑
盒测试套件。商业软件的开发者经常要配备一个质量保证团队,这个团队在很大程度上是独立于
开发团队的。
生成测试套件时,代码中的错误可能会潜伏到测试套件中,上面这种团队之间的独立性可以
减少这种可能性。举例来说,程序编写者可能做了一个错误的隐含假设,即不能使用负数调用函
数,那么如果由这个人构建程序的测试套件,就很有可能继续重复这个错误,不使用负数参数测
试这个函数。
黑盒测试的另外一个好处是,具体实现发生变化时,这种测试仍然适用。因为生成测试数据
与具体实现没有关系,所以具体实现改变时,测试不需随之改变。
我们前面说过,生成黑盒测试数据的有效方法是通过规范探索测试路径。看一下下面的规范:
def sqrt(x, epsilon):
"""假设 x 和 epsilon 是浮点数
x >= 0

epsilon > 0
如果存在满足x-epsilon <= result*result <= x+epsilon的result,
就返回result"""
这个规范看上去只有两条路径:一条对应x = 0,一条对应x > 0。但常识告诉我们,尽管
测试这两种情形是必要的,但绝对不够。
还需要测试边界条件。测试列表时,边界条件包括空列表、只有一个元素的列表以及包含列
表的列表。测试数值时,典型的边界条件就是非常小的值、非常大的值和“正常”值。对于例子
中的sqrt函数,使用图6-1中的x和epsilon值应该比较理想。

图6-1 测试边界条件

前四行是一些典型的测试用例。请注意,x值包括一个完全平方数、一个小于1的数和一个根
为无理数的数。如果这些测试有任何一种没有通过,那么程序中肯定有错误需要修复。
其余几行测试了x和epsilon取特别大的值和特别小的值的情形。如果有任何一种测试失败,
说明程序有需要修改的地方。可能是程序中有错误需要修复,也可能需要修改规范以使它更容易
被满足。例如,当epsilon特别小的时候,还希望能找到合适的平方根近似值,那对程序的要求
就太高了。
还需要考虑的一个重要边界条件是别名。例如,下面的代码:

def copy(L1, L2):
"""假设L1和L2是列表
使L2和L1元素相同"""
删除L2中的所有元素
删除L2的第一个元素
向空列表L2添加L1的元素
L2.append(e)


多数情况下是有效的,但L1和L2引用同一个列表时,它就失效了。如果测试套件中没有包括像
copy(L, L)这样的函数调用,就永远不会发现这个错误。

6.1.2 白盒测试
黑盒测试是必需的,但通常也是不够的。不检查代码内部结构,就不可能知道哪种测试用例
能提供新的信息。看看下面这个普通的例子:
def isPrime(x):
"""假设x是非负整数
如果x是素数,则返回True,否则返回False"""
if x <= 2:
return False
for i in range(2, x):
if x%i == 0:
return False
return True
查看代码可知,因为测试条件为if x <= 2,所以0、1和2都可以作为一种特殊情形,都需
要测试。如果不看这段代码,可能就不会测试isPrime(2),也就不会发现isPrime(2)这个函数
调用会返回False,错误地认为2不是质数。
构建白盒测试套件要比黑盒测试套件容易得多。规范经常是不完整的,也十分简单,这使得
我们很难估计黑盒测试套件对输入空间的覆盖程度。相比之下,代码中反映的路径则定义得非常
清楚,白盒测试套件对输入空间的覆盖程度相对也比较容易。实际上,现在就有一些商业工具可
以比较客观地测量白盒测试的完备程度。
如果一个白盒测试套件可以测试程序中所有潜在路径,那我们就可以认为它是路径完备的。
一般来说,路径完备不可能达成,因为这取决于程序中循环的次数和递归的深度。例如,一个阶
乘函数的递归实现对于每种可能的输入都有不同路径(因为不同输入的递归深度不一样)。
而且,即使一个路径完备的测试套件也不能保证发现程序中的所有错误。看下面的代码:
def abs(x):
"""假设x是整数
如果x>=0返回x,否则返回-x"""
if x < -1:
return -x
else:
return x
从规范中可知,有两种可能的情形:x为负数,或者不为负数。这说明输入集合{2, -2}足
以覆盖规范中的所有路径。这个测试套件对我们来说还有一个额外的惊喜:它同时也测试了程序
代码中的所有路径,所以看起来也是一个路径完备的白盒测试套件。美中不足的是,这个测试套
件忽略了这样一个事实:abx(-1)会返回-1。
尽管白盒测试有很多局限性,但它提供的一些经验准则仍然值得我们参考。
 测试所有if语句的所有分支。
 必须测试每个except子句(参见第7章)。
 对于每个for循环,需要以下测试用例:
 未进入循环(例如,如果使用循环遍历列表中的所有元素,则必须测试空列表);

 循环体只被执行一次;
 循环体被执行多于一次;
 对于每个while循环:
 包括上面for循环中的所有用例;
 还要包括对应于所有跳出循环的方式的测试用例。例如,对于以while len(L) > 0 and
not L[i] == e开始的循环,测试用例应该包括因为len(L)不大于0和因为L[i] == e
而跳出循环的情况。
 对于递归函数,测试用例应该包括函数没有递归调用就返回、只执行一次递归调用和执
行多次递归调用的情况。
6.1.3 执行测试
测试一般分为两个阶段。第一个阶段称为单元测试。在这个阶段中,测试者构建并执行测试,
用来确定代码的每个独立单元(例如,函数)是否正常工作。第二个阶段称为集成测试,用来确
定整个程序能否按预期运行。在实际工作中,测试者会不断重复这两个阶段,因为如果集成测试
没有通过,那就还需要对单个模块做出修改。
一般来说,集成测试比单元测试更具挑战性。原因之一就是与描述单个模块的预期行为相比,
描述整个程序的预期行为要困难得多。例如,描述一个字处理软件的预期行为,其困难程度要远
远超过描述一个计算文档中字符数量的函数的预期行为。规模问题也会使集成测试更加困难。花
费数小时甚至数天时间执行集成测试是常有的事。
很多业内软件开发组织都配备有独立于软件开发团队的软件质量保证团队。这个团队的任务
就是确保软件功能在发布之前达到预期要求。在一些组织中,开发团队负责单元测试,QA团队
负责集成测试。
在工业界,测试过程通常是高度自动化的。测试者①不会坐在终端前面手动输入用例并检查
输出。他们会使用测试驱动程序,这些程序会自动进行以下工作:
 建立调用待测试程序(或单元)所需的环境;
 使用一个预先定义的或自动生成的输入序列来调用待测试程序(或单元);
 保存以上调用结果;
 检查测试结果是否可以接受;
 自动生成一个合适的报告。
在单元测试中,除了建立测试驱动程序之外,我们还经常需要建立测试桩。测试驱动程序可
以模拟使用待测试单元的那部分程序,测试桩则用来模拟待测试单元要使用的那部分程序。测试桩
的用处非常大,因为它使我们可以测试那些需要某些软件才能运行的单元,有时候甚至是需要某些
还不存在的硬件才能运行的单元。这样,程序员团队就可以同时开发并测试一个系统的多个部分。
理想情况下,测试桩应该具有以下功能:

 检查调用者提供的环境和参数是否合理(使用不恰当的参数调用函数是很常见的错误);
 修改实参和全局变量,使它们符合规范;
 返回与规范一致的值。
建立合适的测试桩通常比较困难。如果测试桩要模拟的是一个执行复杂任务的单元,那么要
建立这样一个测试桩,而且它的行为要完全符合该单元的规范,其工作量差不多相当于重写这个
要模拟的单元。解决这个问题的一种方法是,限制测试桩可以接受的参数集合并创建一个表格,
在其中列出测试套件使用的每种参数组合及其对应的返回值。
测试过程自动化的一个显著优点是更易于进行回归测试。程序员调试程序时,非常容易破坏
其他正常工作的部分。每次对程序做出修改之后,不论修改有多么小,我们都应该确保程序还能
通过它以前已经通过的测试。

6.2 调试
修复软件缺陷的过程被称为调试,关于这个词的由来有一个迷人的坊间传说。图6-2的照片
是哈佛大学的一个研究组1947年9月9日实验室记录中的一页,这个研究组当时正致力于研究艾肯
继电器计算机“马克二号”。

图6-2 不是第一个bug

有人断定,正是因为发现了那只被困在“马克二号”中的倒霉的蛾子,才导致debugging这
个术语的出现。但从照片中“发现虫子(bug)的第一个真实案例”这句话可以看出,bug的引申
含义已经常用于描述系统问题。“马克二号”项目负责人之一格蕾丝·莫里·霍珀明确表示,bug
这个词在二战时期就经常用于描述电子系统中的问题。而且在那之前,一本1896年的电气手册Hawkins’s New Catechism of Electricity中就已经包含了这个条目:“术语bug在有限范围内表示电气
设备连接或运行方面的错误和问题。”在英语中,bugbear这个词表示“导致不必要的或过分的恐
惧或焦虑的事情”①。当哈姆雷特抱怨“我生命中的虫子和小妖精”的时候,莎士比亚将这个词
简写成了bug。
bug这个词有时会使人们忽略这样一个基本事实:如果你写的程序中有bug,那肯定是你自己
的问题。完美无瑕的程序中,问题不会不请自来。如果你的程序中有问题,那一定是你的错。错
误不会在程序中繁衍,如果你的程序中有很多错误,那肯定是因为你犯了很多错误。运行时错误
可以按照以下两个维度进行分类。
 显性→隐性:显性错误有明显的表现,如程序崩溃或运行时间异常长(可能永不停止)。
隐性错误没有明显的表现,程序会正常结束,不出任何问题——除了给出一个错误答案。
多数错误都在二者之间,一个错误是否是显性的取决于你检查程序行为的周密程度。
 持续→间歇:持续性错误在程序每次使用相同的输入运行时都会发生。间歇性错误仅在
某些时候出现,即使程序使用相同输入并在相同条件下运行。进行到第14章时,我们会
开始编写程序为随机情形建模,在这种类型的程序中,间歇性错误是很常见的。
显性的和持续性的错误是最好的情况。开发者不会对部署这种程序的可行性抱任何幻想,如
果有其他人愚蠢到想使用这种程序,他会立刻发现自己蠢到不可救药。这种程序在崩溃之前可能
会做一些可怕的事情,比如删除文件,但这种错误至少会引起用户的重视(如果没有惊慌失措的
话)。优秀的程序员编写程序时,会尽量使程序错误是显性的和持续性的,这种编程方式通常称
为防御性编程。
下一种我们不希望见到的错误是显性的间歇性错误。一个几乎所有时候都能计算出飞机正确
位置的空中交通管制系统,要比一个一直在犯明显错误的系统危险得多。部署一个带有错误程序
的系统确实可以维持一段时间,但这只不过是一场黄粱美梦,问题迟早会暴露。如果发生错误的
条件很容易重现,那么跟踪和修复错误也相对比较容易。如果引起错误的条件不是很清楚的话,
问题就非常难以解决。
以隐性方式出错的程序特别危险。因为它们表面看来没有问题,人们使用它们,并且相信它
们在做正确的事。逐渐地,我们的社会将对软件产生依赖,这些软件用来执行超出人类能力的关
键计算,我们甚至不能判断软件执行的这些计算是否正确。因此,程序可以在很长一段时间内给
出一个错误答案,而我们根本意识不到这个情况。这样的程序可能而且已经造成了严重危害。②一
个评价抵押债券投资组合风险的程序如果出现错误,会给银行(甚至整个社会)带来极大的麻烦。
一台放射治疗仪如果比正常情况发射出哪怕多一点或少一点的射线,对癌症病人来说都是生与死
的区别。一个只是偶尔出现隐性错误的程序造成的危害总是远大于一直出错的程序。既是隐性又
是间歇性的错误始终是最难发现和修复的。

6.2.1 学习调试
调试是一种需要学习的技能,没有人天生就会调试程序。好消息是,学习调试并不难,而且
一通百通。调试程序的能力可以用来找出其他复杂系统中的错误,如科学实验和病人。
人们至少用了40年时间建立被称为“调试器”的工具,所有流行的Python IDE中也带有调试
器工具。设计者认为这些调试工具可以帮助人们找到程序中的错误,它们确实有帮助,但说实话,
帮助不大。更重要的是如何接近问题。很多经验丰富的程序员甚至根本不用调试工具。多数程序
员认为最重要的调试工具是print语句。
当测试发现程序运行不正常时,就应该进行程序调试。调试就是搜索程序异常行为原因的过
程。保持良好调试能力的关键就是系统化地执行这个搜索过程。
调试从研究现有数据开始,这些数据包括测试结果和程序文本。要研究所有测试结果,不但
要检查那些发现问题的测试,还要检查那些没有发现问题的测试。尽量弄清楚为什么这个测试通
过了而另一个测试没有通过,这经常会给你一些启发。查看程序文本时,请记住你不需要完全理
解它。如果完全理解了程序,就不会有错误发生了。
然后,建立一个符合所有现有数据的假设。这个假设可以非常具体,比如“如果我将第403
行代码从x < y修改成x <= y,那么问题就会解决”;这个假设也可以非常宽泛,比如“程序不
会结束的原因是某些while循环中的跳出条件出现了错误”。
接下来,设计并运行一个能够推翻上述假设的可重复实验。例如,你可以在每个while循环
的前面和后面都加上一个print语句。如果这些语句完全匹配成对,那么因为某个while循环出
错导致程序无法终止的假设就被推翻了。运行实验之前,你就应该明确如何解释每种可能的结果。
如果直到实验结束才对结果进行解释,就很可能陷入“事后诸葛亮”的尴尬境地。
最后,要将你的实验过程记录下来,这一点非常重要。当你花费数小时修改代码,并努力追
踪那些难以捉摸的错误的时候,非常容易忘掉你已经做了什么。如果不仔细记录,非常容易浪费
大量时间,一遍又一遍地重复同样的实验(更可能的是,那些看上去不同但会向你传达同样信息
的实验)。请记住,就像很多人说过的,“非常愚蠢的人会一遍又一遍地重复同样的事情,却期待
不同的结果。”①
6.2.2 设计实验
如果将调试看作一个搜索过程,那么每次实验就要尽力缩减搜索空间。缩减搜索空间的一种
方法是,设计一个实验,确定代码的一个具体区域是否是造成某个问题的原因,这个问题是在集
成测试中被发现的。另外一种缩减搜索空间的方法是,减少导致错误出现所需的测试数据量。
我们故意设计了一个例子,来演示如何进行调试。假设你编写了图6-3中的回文检测代码。

def isPal(x):
"""假设x是列表
如果列表是回文,则返回True,否则返回False"""
temp = x
temp.reverse
if temp == x:
return True
else:
return False
def silly(n):
"""假设n是正整数
接受用户的n个输入
如果所有输入组成一个列表,则返回'Yes'
否则返回'No'
for i in range(n):
result = []
elem = input('Enter element: ')
result.append(elem)
if isPal(result):
print 'Yes'
else:
print 'No'

图6-3 带有bug的程序

假设你对自己的编程技能非常自信,所以未经测试就将这段代码发布到互联网上。很快你将
收到一封信:“我用以下1000个字符串作为输入测试了你的这个(哔——)程序,每次它都输出
Yes,就算一个傻瓜都能看出这不是回文,赶快修改一下吧!”
你可以使用邮件中提供的1000个字符串作为输入进行测试,但更明智的做法是先从更简单一
些的输入开始。实际上,应该先使用一个最短的非回文字符串来测试,如:

>>> silly(2)
Enter element: a
Enter element: b

好消息是,程序连这个最简单的测试也没有通过,所以你不用输入那1000个字符串了。坏消
息是,你根本不知道哪里出错。
对于这个例子,代码非常少,所以你可以一直盯着它看,直到找出那个错误(或那些错误)。
但是,假设这个程序非常大,不能使用上面的方法,那下面就来系统地缩减搜索空间。
一般来说,最好的方法是执行二分查找。先找出代码中间点,然后设计一个实验,确定是否
因为中间点前面存在问题才导致程序出现这种症状。(当然,中间点后面也可能存在问题,但最
好一次只解决一个问题。)中间点最好选在能够提供某些中间值的地方,这些中间值应该既易于
检查,又能提供有价值的信息。如果某个中间值与你的预期不符,那么中间点之前就很可能存在
问题。如果中间值都没有问题,那么错误就可能在代码后半部分的某个地方。可以一直重复这个过程,直到将存在问题的区域缩减到几行代码。
我们看一下silly函数,它的中间点大致在if isPal(result)这行附近。很明显,我们需要
检查result的值是否是['a', 'b']。我们在silly函数中的if语句前面插入print(result)来检
查result的值。这个实验运行的结果是程序输出了['b'],这说明已经出现了错误。下一步是在
循环的中间点输出result,这次很快发现,result中的元素从来不会多于1个,这说明对result
的初始化应该移到for循环的外面。
“正确的”silly函数代码应该是:

def silly(n):
"""假设n是正整数
接受用户的n个输入
如果所有输入组成了一个回文列表,则返回'Yes'
否则返回'No'
result = []
for i in range(n):
elem = input('Enter element: ')
result.append(elem)
print (result)
if isPal(result):
print ('Yes')
else:
print ('No')

我们试一下这段代码,看看在for循环之后result取值是否正确。没有问题,但不幸的是,
程序仍然输出Yes。于是,我们有理由相信print语句后面还有第二个错误。所以,我们再看一
下isPal函数。代码if temp == x:看上去是这个函数的中心点,所以我们在这行代码前面插入:
print(temp, x)
运行代码时,我们看到temp的值与预期一致,但x则不然。先看isPal函数的前半部分,我们在
temp = x这行代码后面插入一条print语句,发现temp和x的值都是['a', 'b']。快速检查代码,
发现在isPal函数中,我们将temp.reverse()错误地写成了temp.reverse,后者会返回一个内
置的列表reverse方法,但不调用它。①
我们再运行一下测试,这次发现temp和x的值都是['b', 'a']。现在可以将错误限定在一行
代码上了。看上去,temp.reverse()意外地改变了x的值。就是这个别名错误将我们害惨了:temp
和x是同一个列表的两个名称,在列表翻转前后都是这样。修复这个错误的一种方法是使用temp =
x[:]替换isPal中的第一个赋值语句,新代码可以为x制作一个副本。
isPal函数的正确代码是:

def isPal(x):
"""假设x是列表
如果列表是回文,则返回True,否则返回False"""
temp = x[:]
temp.reverse()
if temp == x:
return True
else:
return False

6.2.3 遇到麻烦时
据说,肯尼迪总统的父亲约瑟夫·P. 肯尼迪曾经这样教导他的孩子们:“艰难之路,唯勇者
行。”①然而他从来没有调试过一段代码。调试遇到困难时,我们该怎么做呢?以下给出了几条实
用的提示。
 排除常见错误。例如,看看你是否犯了以下错误:
 以错误的顺序向函数传递实参;
 拼错一个名称,如将大写字母写成小写;
 变量重新初始化失败;
 检验两个浮点数是否相等(==),而不是近似相等(请记住,浮点数的运算与学校里学
的运算不一样);
 在应该检验对象相等(如id(L1) == id(L2))的时候,检验值相等(例如,使用表达
式 L1 == L2比较两个列表);
 忘记了一些内置函数具有副作用;
 忘记使用()将对function类型对象的引用转换为函数调用;
 意外地创建了一个别名;
 其他一些你常犯的错误。
 不要问自己为什么程序没有按照你的想法去做,而要问自己程序为什么像现在这样做。
后者应该更容易回答,要想弄清楚如何修复程序,这可能是一个很好的开始。
 记住,错误可能不在你认为会出错的地方。如果在那里,你早就应该发现它了。确定错
误位置的一种实用方法是,看看那些你认为不会出错的地方。就像夏洛克·福尔摩斯所
说:“排除所有其他因素,最后剩下来的一定就是真相。”②
 试着向其他人解释程序的问题。每个人都会有盲点。经常有这样的情况,试图向别人解
释问题的时候,你会突然发现自己忽略的地方。向其他人解释为什么程序中某个地方不
会出现错误是个很好的选择。
 不要盲目相信任何书面上的东西。特别是,不要相信文档。代码行为可能与注释不一样。
 暂停调试,开始编写文档。这会帮助你从不同视角接近问题所在。
 出去散散步,明天接着做。这可能意味着与你坚持工作相比,修复问题的时间要晚一些,
但花费的总时间会大大减少。也就是说,我们使用时间上的一点延迟换取了效率上的大
幅提升。(同学们,开始习题集中的编程练习吧,宁早勿晚,这是个绝好的理由!)

6.2.4 找到“目标”错误之后
当你认为找到代码中的错误时,很可能要立刻开始编写并测试一个修复程序,这种诱惑通常
难以抑制。但你这时最好冷静一下。请记住,我们的目标不是修复一个错误,而是快速有效地得
到一个没有错误的程序。
你应该扪心自问,这个错误能够解释所有观测到的症状,还是只是冰山一角。如果是后者,
最好将对这个错误的处理与其他修改结合考虑。举例来说,假设你发现错误是因为意外修改列表
导致的,那么可以复制列表,先在本地绕开这个错误;也可以考虑使用一个元组代替列表(因为
元组是不可变的),这样可能消除代码其他部分中的类似错误。
做出任何修改之前,一定要想清楚提交“修复”的后果。它会破坏其他程序吗?它会使程序
过度复杂吗?它会使我们有机会优化其他部分的代码吗?
请一定确保你可以回到修复前的状态。如果经过一系列长长的修改之后你才发现,离最初设
定的目标越来越远,而且已经没有机会回到原点,那么没什么事情比这更令人沮丧了。磁盘空间
一般足够大,请一定注意保存程序修改前的版本。
最后,如果还存在很多无法解释的错误,那么你应该反思,这种按部就班查找并修复错误是
否正确。你最好考虑一下,是否有更好的方法来组织程序,或者有更简单的、更容易正确实现的
算法。


 

  • 15
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

___Y1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值