20. 调试
调试程序时, 应当区分不同类型的错误, 以便更快地查找出错误的原因.* 语法错误(semantic error): 在将源代码翻译成字节码的过程中有解释器发现.它们通常表示有程序结构错误.例如, 在def语句的末尾漏掉冒号, 会产生一个有些冗余的错误信息SyntaxErroe: invalid syntax* 运行时错误(runtime erroe): 由解释器在程序运行的过程中发现错误后产生.大部分错误消息都包含了错误发生的位置以及正在执行的函数的信息.例如, 一个无线递归最终会导致运行时错误maximum recursion depth exceeded(超过最大递归深度).* 语义错误(semantic error): 是程序运行中没有产生错误信息, 但做的事情却不正确的情况(设计与预期不一致). 例如, 一个表达式求值的顺序和你预想的不同, 因此产生了不正确的结果.
调试的第一步就是弄清楚你面对的到底是哪种类型的错误.
虽然下面的几节是按照错误类型来组织的, 但有些技巧其实可以适用于多种情形.
20.1 语法错误
语法错误, 在弄清楚它们是什么之后, 通常都很容易修正.
不幸的是, 错误信息往往没什么帮助.
最常见的错误信息是:
SyntaxError: invalid syntax 和 SyntaxErro invalid token, 这两种都没多少信息量.另一方面, 信息也确实告诉你问题在程序中发生的位置.
实际上, 它告诉你的是Python发现错误的位置, 而并不一定总和错误发生的位置相同.
有时候错误发生在错误信息指明的位置之前, 往往是前前一行.如果你递增地构建程序, 应当很清楚错误发生的位置. 它常常在你最后添加的那行代码上.如果你是从书中赋值代码, 则最好先仔细比较自己的代码个书中的代码. 检查每一个字母.
同时请记得书本也可能是错误, 所以如果你看到一个像是语法错误的东西, 那么它有可能就是.下面是一些可以避免最常见的语法错误的方法:
* 1. 确保你没有使用Python关键字作为变量名称.* 2. 检测在没一个复合语句的语句头结尾, 都有一个冒号, 包括for, while, if和def语句.* 3. 确保程序中每个字符串都有前后匹配的引号. 确定每个括号都是直引号(如"), 而不是弯引号(如”).* 4. 如果有三引号(单引号或双引号)多行字符串, 确保你正确结束了字符串.没有正确结束的字符串, 会导致程序结尾处产生invalid token错误, 或者它会将接下来的程序看作字符串的一部分, 直到遇到下一个字符串为止.这种情况下, 可能都不会产生错误信息!* 5. 没有关闭的开始符号( (, {或[) 会让Python继续解析下一行, 并当作前语句的一部分.通常来说, 会在下一行立即产生一个错误.* 6. 检查再条件判断时'=='写成'='的经典错误.* 7. 检查缩进, 确保它们是按照设想正确排布的.Python可以处理空格和制表符, 但如果混合使用他们, 则可能产生问题.避免这种问题做好的办法是使用一个懂得Python的编辑器, 并由它产生一致的缩进.* 8. 如果你的代码中有非ASCII字符串(包括字符串和注释中), 虽然Python3通常能处理好非ASCII字符,但还是可能导致问题. 当你从网页或其他来源直接复制文本是, 需要格外注意.如果上面的办法都没用, 请继续看下一节.
20.1.1 我一直进行修改, 但没有什么区别
如果解释器保存一个错误而你又找不到, 有可能是因为解释器和你用的并不是同一套代码.
检查你的编辑环境, 确保你正在编辑的代码和Python运行的是同一个.如果不确定, 可以尝试在程序开头加上一个明显而故意的错误. 再运行一次
如果解释器并没有发现新的错误, 那么说明你运行的不是新代码.
可能有以下几种原因.* 你编辑了代码, 但忘了保存更改就直接运行了. 有的编辑环境会帮你自动保存, 有的不会.* 你修改了文件名, 返任然在使用旧文件名运行程序.* 你的编程环境可能没有正确配置.* 如果你在编写一个模块, 并使用import, 请确保你的模块名称没有和Python标准模块冲突.* 如果你在使用import来读入模块, 请记得重载一个修改过的文件时, 需要重启解释器或者使用reload.如果你直接重新导入这个模块, 它并不会做任何事.如果你遇到困难被卡住, 而且弄不清楚到底怎么回事,
一个办法是重新以最简单的类似'Hello World!'的程序开始, 并确保你能让一个已知的程序正确运行.
然后逐渐添加原先程序的部分到新程序中.
20.2 运行时错误
一旦你的程序已经确保语法正确, Python可以读取它, 并且至少可以开始运行它.
这时候可能发生哪些错误?
20.2.1 我的程序什么都不做
这个问题最常见的原因是你的文件包含了各种函数的类的定义, 但没有实际调用函数来启动执行.
如果你是为了导入模块使用它们提供的类和函数, 那么这么做可能是故意的如果你是故意的, 则确保在程序中有一个函数调用, 并确保执行流程能到底这一函数调用(参见20.2.5节).
20.2.2 我的程序卡死了
如果一个程序突然停止并看起来什么事情都没做, 它就'卡死了'.
通常这意味着程序掉入一个死循环或者无线递归中.* 如果怀疑一个特别的循环可能是问题所在, 可以在循环开始前添加一个print语句, 打出'进入循环', 在循环的结尾处之后也添加一个, 打出'退出循环'.再次运行程序. 如果你看到第一个输出, 而没有看到第二个, 说明你确实遇到一个死循环了.无限循环的内容参加20.2.3节.* 大部分情况下, 无限递归都会让程序运行一会儿, 然后产生'RuntimeError: Maximum recursion depth exceeded'错误.如果发生这种情况, 参见20.2.4节.如果你没有看到这个错误, 但怀疑可能是递归方法或函数产生的问题, 也同样可以使用20.2.4节中的技巧.* 如果上面两步都没用, 尝试其他循环或其他递归方法与函数.* 如果这些都没用, 说明可能是你没理解你的程序的执行流程. 执行流程的内容参加20.2.5节.
20.2.3 无限循环
如果你觉得有一个无限循环并知道哪个循环导致的问题, 可以在循环的结尾处添加一个print语句,
打印出循环条件中的变量值, 以及条件的值. 例如:
while x > 0 and y < 0:# do something to x# do something to yprint('x': x)print('y': y)print('condition: ', (x > 0 and y < 0))
现在当你再次运行程序时, 能够看到每次循环中打印出的3行输出.
最后一次循环时, 添加应该变成Fasle.
如果循环一直进行, 你应当可以看到x和y的值, 并可能弄清楚为什么它们没有被正确更新.
20.2.4 无限递归
大部分情况下, 无限递归会导致程序运行一会儿, 然后产生Maximum recursion depth exceeded的错误.如果你怀疑一个函数导致了无限递归, 保证递归确实有一个基准情形.
因该有一个条件能导致函数直接返回而不再继续递归调用.
如果没有, 那么你可能需要重新思考算法, 并定义一个基准情形.如果有一个基准情形, 但程序似乎没有到达它, 可以在函数的开头加一个print语句来打印参数.
现在当你重新运行程序时, 会看到每次函数调用时都会打出几行输出, 并能看到每次调用的参数值.
如果参数并没有向基准情形变化, 你大概能发现为何如此.
20.2.5 执行流程
如果你不确认程序中的执行流程如何走向, 可以在每个函数的开头添加一个print语句,
打印类似'进入函数foo'之类的输出. 这里foo是函数名.现在如果你重新运行程序, 它会打印出每个函数调用的轨迹.
20.2.6 当我运行程序, 会得到一个异常
如果在运行时遇到一个问题, Python会打印出一个信息, 包含错误的名称,
程序中发生这个错误的位置, 以及一个回溯.回溯时标明了当前执行的函数, 以及调用它的函数, 以及调用这个调用者的函数, 以此类推.
换句话说, 它回溯了从程序开头知道错误发生所在位置的整个调用轨迹, 包括了每个函数所在文件中的行号.第一步是检查程序中错误发生的位置, 并尝试弄清楚问题所在.
下面是一些常见的运行时错误:NameError
你在试图使用一个当前环境中并不存在的变量. 检测变量名是否有拼写正确, 或至少是一致.
请记得局部变量是局部的, 不能再定义它们的函数之外使用.TypeErroe
有3中可能的原因.
* 你在尝试错误的使用一个值. 例如, 使用不是整数的值来引用字符串, 列表或元组.* 格式字符串中, 内部的格式和传入的参数不匹配. 当格式项的数目不对或者转换的类型不对时都可能发生.* 调用函数的使用了错误数量的参数. 对于方法来说, 查看方法定义并检查第一个参数是否为self.接着查看方法调用; 确保你是在正确类型的对象上调用方法, 并正确提供了其他参数.KeyError
你在试图用一个字典并不包含的键来查找字典的元素. 如果键是字符串, 请注意大小写问题.AttributeError
你在尝试访问一个不存在的属性或方法. 检查拼写! 你可以使用内置的vars函数来列出存在的属性.
如果AttributeError指明一个对象是NoneType, 则意味着它是None. 那么问题不是属性名而是对象.对象为None的原因可能是你忘了从函数返回值; 如果函数执行肺癌结尾都没有遇到return语句, 那么它会返回None.
另一个常见的原因是使用了一个返回None的列表方法作为结果, 如sort. (可变类型的方法大多数返回值是None).IndexError
你在访问列表, 字符串或元组时使用索引大于它的长度减一.
在错误的发生的前一行, 添加一个print语句展示索引的值和数组的长度. 数组长度是否正确? 索引大小是否正确?
Python调用器(pdb)在查找异常时很有用, 因为它让你可能在错误发生之前的地方查看程序的状态.
可以在http://docs.python.org/3library/pdb.html阅读pdb的相关资料.
20.2.7 我添加了太多print语句, 被输出掩没了
使用print语句进行调试的问题之一是你可能被太多的输出所埋没.
有两种方法可以继续: 简化输出, 或者简化程序.要简化输出, 可能删除或注释掉没用的print语句, 或者将它们合并起来, 或者格式化输出让它们更容易看懂.要简化程序, 有几件事情可做. 首先, 简化程序所处理的问题.
例如, 如果你在搜索一个列表, 就改为搜索一个很小的列表.
如果程序从用户获得输入, 则输入可以产生错误的最简单的输入.其次, 清理程序. 删除无效的代码, 并重新组织代码让它尽可能更可读.
例如, 如果你怀疑问题出在程序的一个很深的嵌套部分中, 则应当尝试重写那部分, 让它的结构更简单.
如果你怀疑一个很大的函数, 则尝试将它拆分为多个更小的函数, 并分别测试它们.寻找最简测试用例的过程往往能带你找到问题所在.
如果发现程序在一种情况下正确工作, 而在另一种情况下则不能, 那这些情况本身就给你一些线索.类似地, 重写一部分代码可以帮你找打细微的bug.
如果你做出一个认为不该影响程序的改变, 而它确实出问题了, 这就给了具体的提示.
20.3 语义错误
从某种角度看, 语义错误更难调试, 因为解释器并不提供任何信息.
只有你自己知道程序到底因该怎么做.解决语义错误的第一步是在程序文本和你看到的程序行为之间建立一个连接.
你需要对程序实际在做什么有一个假设.
让这件事情很难的原因之一是计算机运行得太快.你常常会希望程序能够佳能慢到人的速度, 而使用调试器时你可以做到.
但往程序里插入几条精确放置的print语句, 比起设置调试器, 插入或删除断掉,
并'单步'执行到程序出错的地方, 往往花费的时间更少.
20.3.1 我的程序运行不正确
你因该问自己如下几个问题.
* 程序中没有地方你期望它去做而实际上没有发生的? 找到运行那段功能的代码, 并确保它确实如你所期望的那样运行了.* 有没有一些不应该发生的事情? 找到程序中运行了某种不该出现的功能的代码.* 有没有一段代码产生的效果和你所期望的不一致?确保你完全明白该断代码, 特别是当它牵涉到其他Python模块的函数或方法是时阅读你调用的函数的文本. 使用简单的测试用例测试它们并检查结果.为了能够编程, 你需要程序如何工作的一个思维模型.
如果编写出一段和你预期不同的代码, 常常问题不是在程序本身, 而是你的思维模型上.修正你的思维模型的最佳方法是将程序划分成不同部分(通常是函数和方法)并独立测试每一个部分,
一旦找到你的模型和真实世界的偏差, 就能够解决问题了.当然, 在开发程序时你应当分组进行构建和测试.
如果发现一个问题, 应该只需要检查一小部分新的不确认是否正确的代码.
20.3.2 w有一个巨大而复杂的表达式, 而它和我预料的不同
编写复杂的表达式没有问题, 只要能保证它们还可读. 但它们也会变得更难调试. 但它们也会变得更难调试.
将复杂的表达式拆分成一系列的赋值到临时变量的语句, 常常是个好注意. 例如:
self.hands[i].addCard(self.hands[self.findNeighbor(i)].popCard())
这个表达式可以写作:
neighbor = self.findNeighbor(i)
pickedCard = self.hands[neighbor].popCard()
self.hands[i].addCard(pickedCard)
后面更清晰的版本也更加可读, 因为变量名称提供了附加的文档信息, 它也更加容易调试,
因为你可以检查中间变量的类型, 并打印它们的值.复杂表达式的另一个问题是求值的顺序可能和你所期望的不同.
例如, 如果你将表达式x/2π翻译成Python, 可能会这么写:
y = x / 2 **math.pi
任何时候如果不确定求值的顺序, 都可以使用括号.
这样不但会让程序更加正确(从按照你的设想来做的角度说),
也会让其他人更容易阅读你的代码, 因为不需要去记忆操作的顺序.
20.3.3 我有一个函数, 返回值和预期不同
如果你在程序中有return语句返回一个复杂的表达式, 则没有机会在返回之前打印结果.
这时候, 也可以使用临时变量. 例如, 这个语句:
return self.hands[i].removeMatches()
可以写作:
count = self.hands[i].renmoveMatches()
return count
现在你有几乎在返回之前显示count的值来.
20.3.4 我真的真的卡住了, 我需要帮助
首先, 试着离开计算机几分钟. 计算机会发射辐射影响大脑, 产生下列症状
* 挫败感和愤怒感.* 迷信的信念('我的计算机恨我')和神奇的想法('程序只有在我反戴帽子时才正确运行').* 随机行走编程(尝试这看下所有可能的程序并选择运行正确的那个).如果你发现自己正在遭受这些症状之一, 请马上站起来出去散个步. 当你平静下来后, 再思考程序.
它在做什么?
产生那种行为的可能原因有哪些?
上一次程序还正确运行时什么时候, 之后你做了什么?有时候发现一个bug确实需要时间. 我常常能够在远离计算机并让思维休息之后找到bug.
找到错误的最佳地点有火车上, 浴缸中及将入睡之前在床上.
20.3.5 不行, 我真的需要帮助
这种确实会发生. 即使最好的程序员也会偶尔卡住.
有时候你在一段程序上工作太久了所以反而看不到错误. 你需要一双新的眼睛.在叫人帮忙之前, 请确保你已经准备好. 你的程序尽量简单, 而你应当使用最小的输入来复现错误.
你应当在合适的地方放好了print语句(并且它们的输出应当容易理解).
你应当足够理解这个问题, 因此能够简明扼要地描述它.当你找人帮忙是, 请确保给他们需要的信息.
* 如果有错误信息, 它是什么, 它代表了程序的哪部分?* 在这个错误发生之前, 你做的最后一件事情是什么? 你写的最后一段代码是什么? 失败的新测试用例是什么?* 面前为止你做了哪些尝试, 并从中得到了什么?当你找寻bug是, 思考一下如何做才能找的更快. 下异常见到类似的情形时, 就能够快速地找打问题了.记住, 目标不只是让程序正确运行. 目标是学会如何让程序正确运行.