目录
A.3.2 我写了一个超大的密密麻麻的表达式,结果它运行得不正确。
注:本文选择性摘自《Think Python》 Allen Downey著
如何高效调试代码
1、程序错误分类
程序一般会有三种错误:语法错误,运行错误和语义错误。区分这三种错误有助于更快速地追踪错误。
- 语法错误Syntax error:
语法是指程序的结构和规则。比如括号要成对用。如果你的程序有某个地方出现了语法错误,Python会显示出错信息并退出,程序就不能运行了。最开始学习编程的这段时间,你遇到的最常见的估计就是这种情况。等你经验多了,基本就犯的少了,而且也很容易发现了。
- 运行错误Runtime error:
第二种错误就是运行错误,显而易见了,就是直到运行的时候才会出现的错误。这种错误也被叫做异常,因为一般表示一些意外的尤其是比较糟糕的情况发生了。
- 语义错误Semantic error:
第三种就是语义错误,顾名思义,是跟意义相关。这种错误是指你的程序运行没问题,也不产生错误信息,但不能正确工作。程序可能做一些和设计目的不同的事情。发现语义错误特别不容易,需要你仔细回顾代码和程序输出,要搞清楚到底程序做了什么。
2、调试与编程
给程序调试是你应当掌握的最关键技能之一了。尽管调试的过程会有挫败感,也依然是最满足智力,最有挑战性,也是编程过程中最有趣的一个项目了。
某种程度上,调试像是侦探工作一样。你面对着很多线索,必须推断出导致当前结果的整个过程和事件。
调试也有点像一门实验科学。一旦你有了一个关于所出现的错误的想法,你就修改一下程序再试试看。如果你的假设是正确的,你就能够预料到修改导致的结果,这样在编程的水平上,你就上了一层台阶了,距离让程序工作起来也更近了。
如果你的推测是错误的,你必须提出新的来。就像夏洛克.福尔摩斯之处的那样,『当你剔除了所有那些不可能,剩下的无论多么荒谬,都必然是真相。』(引自柯南道尔的小说《福尔摩斯探案:四签名》)
对于一些人来说,编程和调试是一回事。也就是说,编程就是对一个程序逐渐进行调试,一直到程序按照设想工作为止。这种思想意味着你要从一段能工作的程序来起步,一点点做小修改和调试。
例如,Linux是一个有上百万行代码的操作系统,但最早它起源于Linus Torvalsd的一段小代码。这个小程序是作者用来探索Intel的80386芯片的。根据Larry Greenfield回忆,『Linus早起的项目就是很小的一个程序,这个程序能够在输出AAAA和BBBB之间进行转换。这后来就发展除了Linux了。』(引用自Linux用户参考手册beta1版)
3、交互接口与函数
一个交互接口,就像是函数和调用者的一个中间人。调用者提供特定的参数,函数完成特定的任务。
例如,polyline这个多段线函数,需要四个实际参数:t必须是一个Turtle小乌龟;n(边数)必须是一个整形;length(长度)应该是一个正数;angle(角度)必须是一个以度为单位的角度值。
这些要求叫做『前置条件』,因为要在函数开始运行之前就要实现才行。相应的在函数的结尾那里的条件叫『后置条件』。后置条件包含函数的预期效果(如画线段)和其他作用(如移动海龟或进行其他改动)。
前置条件是准备给函数调用者的。如果调用者违背了(妥当标注的)前置条件,然后函数不能正常工作,这个bug就会反馈在函数调用者上,而不是函数本身。
如果前置条件得到了满足,而后置条件未能满足,这个bug就是函数的了。所以如果你的前后置条件都弄清晰,对调试很有帮助。
4、错误信息分析
当语法错误或者运行错误出现的时候,错误信息会包含很多有用的信息,不过信息量太大,太繁杂。最有用的也就下面这两类:
-
错误的类型是什么,以及
-
错误的位置在哪里。
>>> x = 5 >>> y = 6 File "<stdin>", line 1 y = 6 ^ IndentationError: unexpected indent
这个例子里面,错误的地方是第二行开头用一个空格来缩进了。但这个错误是指向y的,这就有点误导了。一般情况下,错误信息都会表示出发现问题的位置,但具体的错误可能是在此位置之前的代码引起的,有的时候甚至是前一行。
同样情况也发生在运行错误的情况下。假设你试着用分贝为单位来计算信噪比
import math signal_power = 9 noise_power = 10 ratio = signal_power // noise_power decibels = 10 * math.log10(ratio) print(decibels)
运行这个程序,你就会得到如下错误信息:
Traceback (most recent call last): File "snr.py", line 5, in ? decibels = 10 * math.log10(ratio) ValueError: math domain error
这个错误信息提示第五行,但那一行实际上并没有错。要找到真正的错误,就要输出一下ratio的值来看一下,结果发现是0了。那问题实际是在第四行,应该用浮点除法,结果多打了一个右斜杠,弄成了地板除法,才导致的错误。
所以你得花点时间仔细阅读错误信息,但不要轻易就认为出错信息说的内容都是完全正确可靠的。
5、增量式开发
写一些复杂函数的时候,你会发现要花很多时间调试。
要应对越来越复杂的程序,你不妨来试试增量式开发的办法。增量式开发的目的是避免长时间的调试过程,一点点对已有的小规模代码进行增补和测试。
你动手的时候,每次建议只添加一两行代码。等你经验更多了,你发现自己可能就能够驾驭大块代码了。不论如何,增量式开发总还是能帮你节省很多调试消耗的时间。
这个过程的核心如下:
-
一定要用一个能工作的程序来开始,每次逐渐添加一些细小增补。在任何时候遇到错误,都应该弄明白错误的位置。
-
用一些变量来存储中间值,这样你可以显示一下这些值,来检查一下。
-
程序一旦能工作了,你就应该把一些发挥『脚手架作用』的代码删掉,并且把重复的语句改写成精简版本,但尽量别让程序变得难以阅读。
把大的程序切分成小块的函数,就自然为我们调试建立了一个个的检查点。在不工作的函数里面,有几种导致错误的可能:
-
函数接收的参数可能有错,前置条件没有满足。
-
函数本身可能有错,后置条件没有满足。
-
返回值或者返回值使用的方法可能有错。
要去除第一种情况,你要在函数开头的时候添加一个print语句,来输出一下参数的值(最好加上类型)。或者你可以写一份代码来明确检查一下前置条件是否满足。
如果形式参数看上去没问题,在每一个返回语句之前都加上print语句,显示一下要返回的值。如果可以的话,尽量亲自去检查一下这些结果,自己算一算。尽量调用有值的函数,这样检查结果更方便些。
如果函数看着没啥问题,就检查一下函数的调用,来检查一下返回值是不是有用到,确保返回值被正确使用。
在函数的开头结尾添加输出语句,能够确保整个执行流程更加可视化。如果你对执行流程比较困惑,这种输出会有一定帮助。有效率地进行脚手架开发是需要时间的,但稍微利用一下这种思路,反而能够节省调试用的时间。
6、思维跳跃
跟随着运行流程是阅读程序的一种方法,但很快就容易弄糊涂。另外一个方法,我称之为『思维跳跃』。当你遇到一个函数调用的时候,你不用去追踪具体的执行流程,而是假设这个函数工作正常并且返回正确的结果。
实际上你已经联系过这种思维跳跃了,就在你使用内置函数的时候。当你调用math.cos或者math.exp的时候,你并没有仔细查看这些函数的函数体。你就假设他们都工作,因为写这些内置函数的人都是很可靠的编程人员。
你调用自己写的函数时候也是同样道理。比如在6.4部分,我们谢了这个叫做is_divisible的函数来判断一个数能否被另外一个数整除。一旦我们通过分析代码和做测试来确定了这个函数没有问题,我们就可以直接使用这个函数了,不用去理会函数体内部细节了。
对于递归函数而言也是同样道理。当你进行递归调用的时候,并不用追踪执行流程,你只需要假设递归调用正常工作,返回正确结果,然后你可以问问自己:『假设我能算出来n-1的阶乘,我能否计算出n的阶乘呢?』很显然你是可以的,乘以n就可以了。
当然了,当你还没写完一个函数的时候就假设它正常工作确实有点奇怪,不过这也是我们称之为『思维飞跃』的原因了,你总得飞跃一下。
7、对分调试
当你开始写更为复杂的程序时,你会发现大部分时间都花费在调试上。更多的代码意味
着更高的出错概率,并且会有更多隐藏 bug 的地方。
减少调试时间的一个方法就是“对分调试”。例如,如果程序有 100 行,你一次检查一行,
就需要 100 步。
相反,试着将问题拆为两半。在代码中间部分或者附近的地方,寻找一个可以检查的中
间值。加上一行 print 语句 (或是其他具有可验证效果的代码),然后运行程序。
如果中间点检查出错了,那么就说明程序的前半部分存在问题。如果没问题,则说明是
后半部分出错了。
每次你都这样检查,就可以将需要搜索的代码行数减少一半。经过 6 步之后 (这比 100
小多了),你将会找到那或者两行出错的代码,至少理论上是这样。
在实践中,可能并不能很好的确定程序的 ‘‘中间部分” 是什么,也有可能并不是那么好
检查。计算行数并且取其中间行是没有意义的。相反,多考虑下程序中哪些地方比较容
易出问题,或者哪些地方比较容易进行检查。然后选定一个检查点,在这个断点前后出
现 bug 的概念差不多。
8、提高调试效率
当你操作较大的数据集时,通过打印并手工检查数据来调试很不方便。下面是针对调试
大数据集的一些建议:
缩小输入 (Scale down the input): 如果可能,减小数据集合的大小。例如,如果程序
读入一个文本文件,从前 10 行开始分析,或是找到更小的样例。你可以选择编辑
读入的文件,或是 (最好) 修改程序使它只读入前 n 行。
如果出错了,你可以将 n 缩小为会导致该错误的最小值,然后在查找和解决错误
的同时,逐步增加 n 的值。
检查摘要和类型 (Check summaries and types): Instea 考虑打印数据的摘要,而不是
打印并检查全部数据集合:例如,字典中项的数目或者数字列表的总和。
运行时错误的一个常见原因,是值的类型不正确。为了调试此类错误,打印值的
类型通常就足够了。
编写自检代码 (Write self-checks): 有时你可以写代码来自动检查错误。例如,如果
你正在计算数字列表的平均数,你可以检查其结果是不是大于列表中最大的元素,
或者小于最小的元素。这被称作 ‘‘合理性检查’’,因为它能检测出 ‘‘不合理的’’ 结
果。
另一类检查是比较两个不同计算的结果,来看一下它们是否一致。这被称作 ‘‘一
致性检查’’。
格式化输出 (Format the output): 格式化调试输出能够更容易定位一个错误。
重申一次,你花在搭建脚手架上的时间能减少你花在调试上的时间。
9、如何找到bug
在调试一个程序的时候,特别是调试一个很难的错误时,应该做到以下五点:
细读: 检查你的代码,仔细地阅读,并且检查是否实现了你的期望。
运行: 通过修改和运行不同的版本来不断试验。通常,如果你在程序中正确的地方打
印了正确的东西,问题会变得很明显,但是有时你不得不搭建一些脚手架。
思考: 花些时间思考!错误的类型是什么:语法、运行时、语义?你从错误信息或者
程序的输出中能获得什么信息?什么类型的错误能引起你看到的问题?问题出现
前,你最后的修改是什么?
小黄鸭调试法 (rubberducking): 如果将你的问题解释给别人听,有时你会发现在解
释完问题之前就能找到答案。你通常并不需要真的去问另外一个人;你可以对着
一个小黄鸭说。这就是著名的小黄鸭调试法 (rubber duck debugging) 的由来。这
可不是我编造的,看看维基的解释。
回退: 有时候,最好的做法是回退,撤销最近的修改,直到你回到一个能运行并且你
能理解的程序。然后你可以开始重建。
初级程序员有时陷入这些步骤之一,忘记了还可以做其他的事情。事实上,每种方法都
有失败的可能。
例如,如果程序是一个排版错误,读代码可能有帮助,但是如果问题是概念理解错误,
则未必是这样。如果你不理解程序要做什么,可能读 100 遍程序都不会发现错误,因为
错误在你的头脑中。
试验可能会有帮助,特别是如果你运行简单短小的测试。但是,如果你不思考或者阅读
你的代码,就直接进行实验,你可能陷入一种我称为“随机游走编程”的模式。这指的是
随机修改,直到程序通过测试。不用说,随机游走编程会花费很长的时间。
你必须花时间思考。调试就像是一门实验科学。你应该至少有一个关于问题是什么的假
设。如果有两个或者更多的可能,试着考虑利用测试消除其中一个可能。
但是,如果有太多的错误,或者你正试图修复的代码太大、太复杂,即使最好的调试技
巧也会失败。有时,最好的选择是回退,简化程序,直到你获得一个正常运行并且能理
解的程序。
初级程序员经常不愿意回退,因为他们舍不得删除一行代码 (即使它是错误的)。如果能
让你好受些,在你开始精简之前,可以将你的代码拷贝到另一个文件中。然后你再把修
改后的代码一块一块地拷贝回去。
发现一个错误,需要阅读、运行、沉思、和时而的回退。如果其中某个步骤没有进展,
试一下其它的。
FAQ
A.1 语法错误
通常一旦找出是哪种语法错误,就容易修正。不幸的是,抛出的错误消息通常没什么帮
助。最常见的错误消息是 SyntaxError: invalid syntax 和 SyntaxError: invalid token ,都没
有提供很多信息。
另一方面,这些错误消息会告诉你程序的哪里出现了错误。实际上,它告诉你 Python
是在哪里发现的问题,但这并一定就是出错的地方。有时,错误出现在错误消息出现的
位置之前,通常就在前一行。
如果你是一点一点地增量式地写的代码,你应该能够知道错误在哪里。一般就在你最后
添加的那行代码里。
如果你是从书上复制的代码,那请仔细地从头和书中的代码对照。一个一个字母地比
照。同时,记住也可能是书上就错了,所以如果你发现看上去像语法错误的地方,那可
能就是了。下面是避免大部分常见语法错误的一些方法:
1. 确保你没有使用 Python 的关键字作为变量名称。
2. 检查你在每个复合语句首行的末尾都加了冒号,包括 for,while,if,和 def 语句。
3. 确保代码中的字符串都有匹配地引号。确保所有的引号都是 ‘‘直引号1’’,而不是
‘‘花引号2’’。
4. 如果你有带三重引号的多行字符串,确保你正确地结束了字符串。一个没有结束
的字符串会在程序的末尾产生 invalid token 错误,或者它会把剩下的程序看作字
符串的一部分,直到遇到下一个字符串。第二种情况下,可能根本不会产生错误!
5. 一个没有关闭的操作符((,{ 以及 [ )使得 Python 把下一行继续看作当前语句
的一部分。通常下一行会马上提示错误消息。
6. 检查条件语句里面的 == 是不是写成了 = 。
7. 确保每行的缩进是符合要求。Python 能够处理空格和制表符,但是如果混用则会
出错。避免该问题的最好方法是使用一个了解 Python 语法、能够产生一致缩进
的纯文本编辑器。
8. 如果代码中包含有非 ASCII 字符串(包括字符串和注释),可能会出错,尽管
Python 3 一般能处理非 ASCII 字符串。从网页或其他源粘贴文本时,要特别注意。
如果上面的方法都不想,请接着看下一节...
A.1.1 我不断地改代码,但似乎一点用都没有。
如果解释器说有一个错误但是你怎么也看不出来,可能是因为你和解释器看的不是同
一个代码。检查你的编码环境,确保你正在编辑的就是 Python 试图要运行的程序。
如果你不确定,试着在程序开始时制造一些明显、故意的语法错误。再运行一次。如果
解释器没有提示新错误,说明你没有运行新修改的代码。
有可能是以下原因:
• 你编辑了文件,但是忘记了在运行之前保存。有一些编程环境会在运行前自动保
存,有些则不会。
• 你更改了文件的名称,但是你仍然在运行旧名称的文件。
• 开发环境的配置不正确。
• 如果你在编写一个模块,使用了 import 语句,确保你没有使用标准 Python 模块的
名称作为模块名。
• 如果你使用 import 来载入一个模块,记住你必须重启解释器或者使用 reload 才能
重新载入一个修改了的文件。如果你导入一个模块两次,第二次是无效的。
如果你依然解决不了问题,不知道究竟是怎么回事,有一种办法是从一个类似“Hello,
World!”这样的程序重头开始,确保你能运行一个已知的程序。然后逐渐地把原来程序
的代码粘贴到新的程序中。
A.2 运行时错误
一旦你的程序语法正确,Python 就能够编译它,至少可以正常运行它。接下来,可能
会出现哪些错误?
A.2.1 我的程序什么也没有做。
在文件由函数和类组成,但并没有实际调用函数执行时,这个问题是最常见的。你也可
能是故意这么做的,因为你只打算导入该模块,用于提供类和函数。
如果你不是故意的,确保你调用了一个函数来开始执行,请确保执行流能够走到函数调
用处(参见下面“执行流”一节)。
A.2.2 我的程序挂死了。
如果一个程序停止了,看起来什么都没有做,这就是“挂死”了。通常这意味着它陷入了
无限循环或者是无限递归。
• 如果你怀疑问题出在某个循环,在该循环之前添加一个打印语句,输出“进入循环”,
在循环之后添加一个打印“退出循环”的语句。
运行程序。如果打印了第一条,但没有打印第二条,那就是进入了无线循环。跳
到下面“无限循环”一节。
• 大多数情况下,无限递归会造成程序运行一会儿之后输出
RuntimeError:Maximum recursion depth exceeded
错误。如果发生了这个错误,跳到下面 ‘‘无限递归’’ 一节。
如果没有出现这个错误,但你怀疑某个递归方法或函数有问题,你仍可以使用“无
线递归”一节中的技巧。
• 如果上面两种方法都没用,开始测试其他的循环和递归函数或方法是否存在问题。
• 如果这也没有用,那有可能你没有弄懂程序的执行流。跳到下面 ‘‘执行流’’ 一节。
A.2.3 执行流
如果你不确定程序执行的过程,在每个函数的开始处添加打印语句,打印类似 ‘‘进入函
数 foo’’ 这样的信息,foo 是你的函数名。
现在运行程序时,就会打印出每个函数调用的轨迹。
运行程序时产生了异常。
如果在运行时出现了问题,Python 会打印出一些信息,包括异常的名称、产生异常的
行号和一个回溯 (traceback)。
回溯会指出正在运行的函数、调用它的上层函数以及上上层函数等等。换言之,它追踪
进行到目前函数调用所调用过的函数,包括每次函数的调用所在的行号。
第一步是检查程序中发生错误的位置,看你能不能找出问题所在。下面是一些常见的运
行时错误:
命名错误 (NameError): 你正在使用当前环境中不存在的变量名。检查下名称是否拼写
正确,或者名称前后是否一致。还要记住局部变量是局部的。你不能在定义它们
的函数的外面引用它们。
类型错误 (TypeError): 有几种可能的原因:
• 值的使用方法不对。例如:使用除整数以外的东西作为字符串、列表或元组
的索引下标。
• 格式化字符串中的项与传入用于转换的项之间不匹配。如果项的数量不同或
是调用了无效的转换,都会出现这个问题。
• 传递给函数的参数数量不对。如果是方法,查看方法定义是不是以 self 作为
第一个参数。然后检查方法调用;确保你在一个正确的类型的对象上调用方
法,并且正确地提供了其它参数。
键错误 (KeyError): 你尝试用字典没有的键来访问字典的元素。如果键是字符串,记住
它是区分大小写的。
属性错误 (AttributeError): 你尝试访问一个不存在的属性或方法。检查一下拼写!你
可以使用内建函数 dir 来列出存在的属性。
如果一个属性错误表明一个对象是 NoneType ,那意味着它就是 None 。因此问题不
在于属性名,而在于对象本身。
对象是 None 的一个可能原因,是你忘记从函数返回一个值;如果程序执行到函数
的末尾没有碰到 return 语句,它就会返回 None 。另一个常见的原因是使用了列表
方法的结果,如 sort ,这种方法返回的是 None 。
索引错误 (IndexError): 用来访问列表、字符串或元组的索引要大于访问对象长度减一。
在错误之处的前面加上一个打印语句,打印出索引的值和数组的长度。数组的大
小是否正确?索引值是否正确?
Python 调试器 (pdb) 有助于追踪异常,因为它可以让你检查程序出现错误之前的状态。
你可以阅读了解更多关于 pdb 的细节。
A.2.4 我加入了太多的打印语句以至于输出刷屏。
使用打印语句来调试的一个问题,是你可能会被泛滥的输出所埋没。有两种途径来处
理:简化输出或者是简化程序。
为了简化输出,你可以移除或注释掉不再需要的打印语句,或者合并它们,或者格式化
输出便于理解。
为了简化程序,有几件事情可以做的。首先,缩减当前求解问题的规模。例如,如果你
在检索一个列表,使用一个小列表来检索。如果程序从用户获得输入,给一个会造成问
题的最简单的输入。
其次,清理程序。移除死代码,并且重新组织程序使其易于理解。例如,如果你怀疑问
题来自程序深度嵌套的部分,尝试使用简单的结构重写它。如果你怀疑是一个大函数的
问题,尝试分解它为小函数并分别测试。
通常,寻找最小化测试用例的过程能够引出 bug。如果你发现一个程序在一种条件下运
行正确,在另外的条件下运行不正确,这能够给你提供一些解决问题的线索。
类似的,重写代码能够让你发现难找的 bug。如果你做了一处改变,认为不会影响程序
但是却事实证明相反,这也可以给你线索。
A.3 语义错误
在某些程度上,语义错误是最难调试的,因为解释器不能提供错误的信息。只有你知道
程序本来应该是怎么样做的。
第一步是在程序代码和你看到的表现之间建立连接。你需要首先假设程序实际上干了
什么事情。这种调试的难处之一,是电脑运行的太快了。
你会经常希望程序能够慢下来好让你能跟上它的速度,通过一些调试器 (debugger) 就
能做到这点。但是有时候,插入一些安排好位置的打印语句所需的时间,要比你设置好
调试器、插入和移除断点,然后“步进”程序到发生错误的地方要短。
A.3.1 我的程序不能工作。
你应该问自己下面这些问题:
• 是不是有你希望程序完成的但是并没有出现的东西?找到执行这个功能的代码,
确保它是按照你认为的方式工作的。
• 是不是有些本不该执行的代码却运行了?找到程序中执行这个功能的代码,然后
看看它是不是本不应该执行却执行了。
• 是不是有一些代码的效果和你预期的不一样?确保你理解了那部分的代码,特别
是当它涉及调用其它模块的函数或者方法。阅读你调用的函数的文档。尝试写一
些简单的测试用例,来测试他们是不是得到了正确的结果。
在编程之前,你需要先建立程序是怎样工作的思维模型。如果你写出来的代码并非按照
你预期的工作,问题经常不是在程序本身,而是你的思维模型。
纠正思维模型最好的方,是把程序切分成组件(就是通常的函数和方法),然后单独测
试每个组件。一旦你找到了模型和现实的不符之处,你就能解决问题了。
当然,你应该在写代码的过程中就编写和测试组件。如果你遇到了一个问题,那只能是
刚写的一小段代码才有可能出问题。
A.3.2 我写了一个超大的密密麻麻的表达式,结果它运行得不正确。
写复杂的表达式是没有问题的,前提是可读,但是它们很难调试。通常把复杂的表达式
打散成一系列临时变量的赋值语句,是一个好做法。因为变量名提供了额外的信息,也更容易调试,因为你可以检查中间变量的类型和值。调试表达式的一个好办法,是添加括号来显式地指定计算顺序 只要你不太确定计算的顺序,就用括号。这样不仅能确保程序正确(按照你认为的方式
工作),而且对于那些记不住优先级的人来说更加易读。
A.3.3 我真的是没办法了,我需要帮助。
首先,离开电脑几分钟吧。电脑发出的辐射会影响大脑,容易造成以下症状:
• 焦躁易怒
• 迷信 (‘‘电脑就是和我作对”) 和幻想 ( ‘‘只有我反着带帽子程序才会正常工作”)。
• 随机漫步编程(试图编写所有可能的程序,选择做了正确的事情的那个程序)。
如果你发现你自己出现上述的症状,起身走动走动。当你冷静之后,再想想程序。它在
做什么?它异常表现的一些可能的原因是什么?上次代码正确运行时什么时候,你接
下来做了什么?
有时,找到一个 bug 就是需要花很长的时间。我经常都是在远离电脑、让我的思绪飞
扬时才找到 bug 的。一些寻找 bug 的绝佳地点是火车上、洗澡时、入睡之前在床上。
A.3.4我不干了,我真的需要帮助。
这个经常发生。就算是最好的程序员也偶尔被难住。有时你在一个程序上工作的时间太
长了,以至于你看不到错误。那你该是休息一下双眼了。
当你拉某人来帮忙之前,确保你已经准备好了。你的程序应该尽量简单,你应该应对造
成错误的最小输入。你应该在合适的地方添加打印语句(打印输出应该容易理解)。你
应该对程序足够理解,能够简洁地对其进行描述。
当你拉某人来帮忙时,确保提供他们需要的信息:
• 如果有错误信息,它是什么以及它指出程序的错误在哪里?
• 在这个错误发生之前你最后做的事情是什么?你写的最后一行代码是什么,或者
失败的新的测试样例是怎样的?
• 你至今都尝试了哪些方法,你了解到了什么?
你找到了 bug 之后,想想你要怎样才能更快的找到它。下次你看到相似的情况时,你
就可以更快的找到 bug 了。
记住,最终目标不是让程序工作,而是学习如何让程序正确工作。