程序员之间流传着一个老笑话:“编码占了编程工作量的90%,调试占了剩余工作量的90%”。
计算机只会做你告诉他的事情,他不会读懂你的心思,做你想要让他做的事情。即使是专业的程序员也一直在制造bug,因此如果你的程序有问题,不必感到沮丧。
好在有一些工具和技巧可以确定你的代码在做什么,以及哪儿出了问题。首先,你要查看日志和断言,这两项功能可以帮助你尽早发现bug。一般来说,bug发现的越早,就越容易修复。
其次,你要学习使用调试器。调试器是IDLE的一项功能,他可以依次执行一条指令,在代码运行时,让你有机会检查变量的值,并追踪程序运行时值的变化。这比程序全速运行要慢得多,但可以帮助你查看程序运行时其中实际的值,而不是通过源代码推测值可能是什么。
抛出异常
抛出异常使用Raise语句。包含以下部分:
- raise关键字
- 对Exception()函数的调用
- 传递给Exception()函数的字符串,包含有用的错误信息
取得回溯字符串
如果Python遇到错误,他就会生成一些错误信息,成为“回溯”。回溯中包含了错误信息、导致该错误的代码行号,以及导致该错误的函数调用的序列。这个序列成为“调用栈”。
断言
断言是健全性检查,用于确保代码没有做什么明显错误的事情。这些健全性检查由assert语句执行。如果检查失败,就会抛出异常。在代码中,assert语句包含以下部分:
- assert关键字
- 条件(即求值为True或False的表达式)
- 逗号
- 当条件为False时显示的字符串
不像异常,代码不应该用try和except处理assert语句。如果assert失败,那么程序就应该崩溃。通过这样的“快速失败”,产生bug和你第一次注意到该bug之间的时间就缩短了。这将减少为了寻找bug的原因而需要检查的代码量。
断言针对的是程序员的错误,而不是用户的错误。断言只能在程序正在开发时失败,用户永远都不会在完成的程序中看到断言错误。对于程序在正常运行中可能遇到的那些错误(如文件没有找到,或用户输入了无效的数据),请抛出异常,而不是用assert语句检测他。不应使用assert语句来引发异常,因为用户可以选择关闭断言。如果你使用python -O xxx.py而不是python xxx.py运行Python脚本,那么Python将跳过assert语句。如果用户开发的程序需要在具有最佳性能的生产环境运行,可能会禁用断言。(尽管在很多时候,即使在这种情况下他们也会使断言保持启用状态。=。)
断言不能替代全面测试。例如,如示例ages=[10, 3, 2, 1, 20],则断言ages[0] <= ages[-1]不会注意到列表未排序,因为刚好第一个年龄小于等于最后一个年龄,这是断言所做的唯一检查。
assert ages[0] > ages[-1], 'Bad!'
在程序执行中尽早失败,可以省去将来大量的调试工作。
日志
如果你曾经在代码中加入print()语句,以在程序运行中输出某些变量的值,那么你就使用了写日志的方式来调试代码。写日志是一种很好的方式,可以理解程序中发生的事,以及事情发生的顺序。Python的logging模块使你很容易创建自定义的消息记录。这些日志消息将描述程序何时调用日志函数,并列出你指定的任何变量当时的值。另一方面,确实日志消息表明有一部分代码被跳过,从未执行。
使用logging模块
要启用logging模块以在程序运行时将日志消息显示在屏幕上,将下面的代码复制到程序顶部(但在Python的#!行之下)
import logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)')
当Python记录一个事件的日志时,它都会创建一个LogRecord对象以保存关于该事件的信息。logging模块的函数让你指定想看到的这个LogRecord对象的细节,以及希望的细节展示方式。
不要用print()调试
输入import logging和logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')有一点不方便。你可能想使用print()代替,但不要屈服于这种诱惑。因为在调试完成后,你需要花很多时间从代码中清除每条日志消息的print()调用。你甚至可能不小心删除一些print()调用,而他们并不是用来产生日志消息的。使用日志消息的好处在于,你可以在程序中想加多少就加多少,稍后只要加入一次logging.disable(logging.CRITICAL)调用就可以禁止日志,不像print()需逐条清除。logging模块使得显示和隐藏日志信息之间的切换变得容易。
日志消息是给程序员的,不是给用户的。用户不会因为便于调试而想看到字典值的内容。对于用户希望看到的信息,例如“文件未找到”或者“无效的输入,请输入一个数字”,应该使用print()调用。我们不希望禁用日志消息之后。让用户看不到有用的信息。
日志级别
“日志级别”提供了一种方式:按重要性对日志消息进行分类。5个日志级别如下表所示,从最不重要到最重要。利用不同的日志函数,消息可以按某个级别记入日志。
级别 | 日志函数 | 描述 |
---|---|---|
DEBUG | Logging.debug() | 最低级别。用于小细节。通常只有在诊断问题时,你才会关心这些消息。 |
INFO | logging.info() | 用于记录程序中一般事件的信息,或确认一切工作正常。 |
WARNING | Logging.warning() | 用于表示可能的问题,它在当前不会阻止程序的工作,但将来可能会用于记录错误,它导致程序做某事失败。 |
ERROR | logging.error() | 用于记录错误,它导致程序做某事失败 |
CRITICAL | logging.critical() | 最高级别。用于表示致命的错误,它导致或将会导致程序完全停止工作。 |
日志消息可作为一个字符串传递给这些函数。日志级别只是一种建议,归根到底,还是有你来决定消息类型是哪一种类型。
import logging
logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s')
logging.debug('Some debugging details.')
logging.info('The logging module is working.')
logging.warning('An error message is about to be logged.')
logging.error('An error has occured.')
logging.critical('The program is unable to recover!')
划分日志级别的好处在于,你可以改变希望看到的日志消息的优先级。向basicConfig()函数传入logging.DEBUG作为level关键字参数,这将显示所有日志级别的消息(DEBUG是最低的级别)。但在开发了更多的程序后,你可能只对ERROR感兴趣。在这种情况下,可以将basicConfig()的level参数设置为logging.ERROR,这将只显示ERROR和CRITICAL消息,跳过DEBUG、INFO和WARNING消息。
禁用日志
在调试完程序后,你可能不希望所有日志消息出现在屏幕上。使用logging.disable()函数禁用这些消息,这样就不必进入程序中手动删除所有的日志调用。只要向logging.disable()传入一个日志级别,他就会禁止该级别和更低级别的所有日志消息。因此,如果想要禁用所有的日志,只要在程序中添加logging.disable(logging.CRITICAL)即可。i.e. :
import logging
logging.basicConfig(level=logging.INFO, format = ' %(asctime)s - %(levelname)s - %(message)s')
logging.crital('Critical error! Critical error!')
logging.disable(logging.CRITICAL)
logging.critical('Critical error! Critical error!')
logging.error('Error! Error!')
因为logging.disable()将禁用它之后的所有消息,所以你可以将他添加到程序中接近import logging代码行的位置。这样就很容易找到他,根据需要注释掉他或取消注释,从而禁用或启用日志消息。
将日志记录到文件
Logging.basicConfig()函数接收filename关键字参数,like this:
import logging
logging.basicConfig(filename='myProgramLog.txt', level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s')
日志消息将保存到myProgramLog.txt文件中。
虽然日志消息很有用,但他们可能充满屏幕,让你很难读到程序的输出。将日志消息写入文件,可以在屏幕保持干净的同时又能保存信息,这样在运行程序后也可以阅读这些信息。
调试器
IDE 设置断点