既然你已学习了足够的内容,可以编写更复杂的程序,可能就会在程序中发现不那么简单的缺陷。本章介绍了一些工具和技巧,用于寻找程序中缺陷的根源,帮助你更快更容易地修复缺陷。
程序员之间流传着一个老笑话:“编码占了编程工作量的90%,调试占了另外90%。”
计算机只会做你告诉它做的事情,它不会读懂你的心思,做你想要它做的事情。即使专业的程序员也一直在制造缺陷,所以如果你的程序有问题,不必感到沮丧。
好在,有一些工具和技巧可以确定你的代码在做什么,以及哪儿出了问题。首先,你要查看日志和断言。这两项功能可以帮助你尽早发现缺陷。一般来说,缺陷发现的越早,就越容易修复。
其次,你要学习如何使用调试器。调试器是IDLE的一项功能,它可以一次执行一条指令,在代码运行时,让你有机会检查变量的值,并追踪程序运行时值的变化。这比程序全速运行要慢得多,但可以帮助你查看程序运行时其中实际的值,而不是通过源代码推测值可能是什么。
10.1 抛出异常
当Python试图执行无效代码时,就会抛出异常。在第 3 章中,你已看到如何使用try和except语句来处理Python的异常,这样程序就可以从你预期的异常中恢复。但你也可以在代码中抛出自己的异常。抛出异常相当于是说:“停止运行这个函数中的代码,将程序执行转到except语句”。
抛出异常使用raise语句。在代码中,raise语句包含以下部分:
- raise关键字;
- 对Exception函数的调用;
- 传递给Exception函数的字符串,包含有用的出错信息。
例如,在交互式环境中输入以下代码:
>>> raise Exception('This is the error message.')Traceback (most recent call last): File "", line 1, in raise Exception('This is the error message.')Exception: This is the error message.
如果没有try和except语句覆盖抛出异常的raise语句,该程序就会崩溃,并显示异常的出错信息。
通常是调用该函数的代码知道如何处理异常,而不是该函数本身。所以你常常会看到raise语句在一个函数中,try和except语句在调用该函数的代码中。例如,打开一个新的文件编辑器窗口,输入以下代码,并保存为boxPrint.py:
def boxPrint(symbol, width, height): if len(symbol) != 1:❶ raise Exception('Symbol must be a single character string.') if width <= 2:❷ raise Exception('Width must be greater than 2.') if height <= 2:❸ raise Exception('Height must be greater than 2.') print(symbol * width) for i in range(height - 2): print(symbol + (' ' * (width - 2)) + symbol) print(symbol * width) for sym, w, h in (('*', 4, 4), ('O', 20, 5), ('x', 1, 3), ('ZZ', 3, 3)): try: boxPrint(sym, w, h)❹ except Exception as err:❺ print('An exception happened: ' + str(err))
这里我们定义了一个boxPrint 函数,它接受一个字符、一个宽度和一个高度。它按照指定的宽度和高度,用该字符创建了一个小盒子的图像。这个盒子被打印到屏幕上。
假定我们希望该字符是一个字符,宽度和高度要大于2。我们添加了if语句,如果这些条件没有满足,就抛出异常。稍后,当我们用不同的参数调用boxPrint时,try/except语句就会处理无效的参数。
这个程序使用了except语句的except Exception as err形式❹。如果boxPrint返回一个Exception对象❶❷❸,这条语句就会将它保存在名为err的变量中。Exception对象可以传递给str,将它转换为一个字符串,得到用户友好的出错信息❺。运行boxPrint.py,输出看起来像这样:
***** ** *****OOOOOOOOOOOOOOOOOOOOO OO OO OOOOOOOOOOOOOOOOOOOOOAn exception happened: Width must be greater than 2.An exception happened: Symbol must be a single character string.
使用try和except语句,你可以更优雅地处理错误,而不是让整个程序崩溃。
10.2 取得反向跟踪的字符串
如果Python遇到错误,它就会生成一些错误信息,称为“反向跟踪”。反向跟踪包含了出错消息、导致该错误的代码行号,以及导致该错误的函数调用的序列。这个序列称为“调用栈”。
在IDLE中打开一个新的文件编辑器窗口,输入以下程序,并保存为error Example.py:
def spam: bacondef bacon: raise Exception('This is the error message.')spam
如果运行errorExample.py,输出看起来像这样:
Traceback (most recent call last): File "errorExample.py", line 7, in <module> spam File "errorExample.py", line 2, in spam bacon File "errorExample.py", line 5, in bacon raise Exception('This is the error message.')Exception: This is the error message.
通过反向跟踪,可以看到该错误发生在第5行,在bacon 函数中。这次特定的bacon 调用来自第2行,在spam 函数中,它又在第7行被调用的。在从多个位置调用函数的程序中,调用栈就能帮助你确定哪次调用导致了错误。
只要抛出的异常没有被处理,Python 就会显示反向跟踪。但你也可以调用traceback.format_exc,得到它的字符串形式。如果你希望得到异常的反向跟踪的信息,但也希望except语句优雅地处理该异常,这个函数就很有用。在调用该函数之前,需要导入Python的traceback模块。
例如,不是让程序在异常发生时就崩溃,可以将反向跟踪信息写入一个日志文件,并让程序继续运行。稍后,在准备调试程序时,可以检查该日志文件。在交互式环境中输入以下代码:
>>> import traceback>>> try: raise Exception('This is the error message.')except: errorFile = open('errorInfo.txt', 'w') errorFile.write(traceback.format_exc) errorFile.close print('The traceback info was written to errorInfo.txt.') 116The traceback info was written to errorInfo.txt.
write 方法的返回值是116,因为116个字符被写入到文件中。反向跟踪文本被写入errorInfo.txt。
Traceback (most recent call last): File "<pyshell#28>", line 2, in <module>Exception: This is the error message.
10.3 断言
“断言”是一个心智正常的检查,确保代码没有做什么明显错误的事情。这些心智正常的检查由assert语句执行。如果检查失败,就会抛出异常。在代码中,assert语句包含以下部分:
- assert关键字;
- 条件(即求值为True或False的表达式);
- 逗号;
- 当条件为False时显示的字符串。
例如,在交互式环境中输入以下代码:
>>> podBayDoorStatus = 'open'>>> assert podBayDoorStatus == 'open', 'The pod bay doors need to be "open".'>>> podBayDoorStatus = 'I/'m sorry, Dave. I/'m afraid I can't do that.''>>> assert podBayDoorStatus == 'open', 'The pod bay doors need to be "open".'Traceback (most recent call last): File "< pyshell#10>", line 1, in < module> assert podBayDoorStatus == 'open', 'The pod bay doors need to be "open".'AssertionError: The pod bay doors need to be "open".
这里将podBayDoorStatus设置为 'open',所以从此以后,我们充分期望这个变量的值是 'open'。在使用这个变量的程序中,基于这个值是 'open' 的假定,我们可能写下了大量的代码,即这些代码依赖于它是 'open',才能按照期望工作。所以添加了一个断言,确保假定podBayDoorStatus是 'open' 是对的。这里,我们加入了信息 'The pod bay doors need to be "open".',这样如果断言失败,就很容易看到哪里出了错。
稍后,假如我们犯了一个明显的错误,把另外的值赋给podBayDoorStatus,但在很多行代码中,我们并没有意识到这一点。这个断言会抓住这个错误,清楚地告诉我们出了什么错。
在日常英语中,assert语句是说:“我断言这个条件为真,如果不为真,程序中什么地方就有一个缺陷。”不像异常,代码不应该用try和except处理assert语句。如果assert失败,程序就应该崩溃。通过这样的快速失败,产生缺陷和你第一次注意到该缺陷之间的时间就缩短了。这将减少为了寻找导致该缺陷的代码,而需要检查的代码量。
断言针对的是程序员的错误,而不是用户的错误。对于那些可以恢复的错误(诸如文件没有找到,或用户输入了无效的数据),请抛出异常,而不是用assert语句检测它。
10.3.1 在交通灯模拟中使用断言
假定你在编写一个交通信号灯的模拟程序。代表路口信号灯的数据结构是一个字典,以 'ns' 和 'ew' 为键,分别表示南北向和东西向的信号灯。这些键的值可以是 'green'、'yellow' 或 'red' 之一。代码看起来可能像这样:
market_2nd = {'ns': 'green', 'ew': 'red'}mission_16th = {'ns': 'red', 'ew': 'green'}
这两个变量将针对Market街和第2街路口,以及Mission街和第16街路口。作为项目启动,你希望编写一个switchLights 函数,它接受一个路口字典作为参数,并切换红绿灯。
开始你可能认为,switchLights 只要将每一种灯按顺序切换到下一种顔色:'green' 值应该切换到 'yellow','yellow' 应该切换到 'red','red' 应该切换到'green'。实现这个思想的代码看起来像这样:
def switchLights(stoplight): for key in stoplight.keys: if stoplight[key] == 'green': stoplight[key] = 'yellow' elif stoplight[key] == 'yellow': stoplight[key] = 'red' elif stoplight[key] == 'red': stoplight[key] = 'green'switchLights(market_2nd)
你可能已经发现了这段代码的问题,但假设你编写了剩下的模拟代码,有几千行,但没有注意到这个问题。当最后运行时,程序没有崩溃,但虚拟的汽车撞车了!
因为你已经编写了剩下的程序,所以不知道缺陷在哪里。也许在模拟汽车的代码中,或者在模拟司机的代码中。可能需要花几个小时追踪缺陷,才能找到switchLights 函数。
但如果在编写switchLights 时,你添加了断言,确保至少一个交通灯是红色,可能在函数的底部添加这样的代码:
assert 'red' in stoplight.values, 'Neither light is red! ' + str(stoplight)
有了这个断言,程序就会崩溃,并提供这样的出错信息:
Traceback (most recent call last): File "carSim.py", line 14, in <module> switchLights(market_2nd) File "carSim.py", line 13, in switchLights assert 'red' in stoplight.values, 'Neither light is red! ' + str(stoplight)❶ AssertionError: Neither light is red! {'ns': 'yellow', 'ew': 'green'}
这里重要的一行是AssertionError❶。虽然程序崩溃并非如你所愿,但它马上指出了心智正常检查失败:两个方向都没有红灯,这意味着两个方向的车都可以走。在程序执行中尽早快速失败,可以省去将来大量的调试工作。
10.3.2 禁用断言
在运行Python时传入-O选项,可以禁用断言。如果你已完成了程序的编写和测试,不希望执行心智正常检测,从而减慢程序的速度,这样就很好(尽管大多数断言语句所花的时间,不会让你觉察到速度的差异)。断言是针对开发的,不是针对最终产品。当你将程序交给其他人运行时,它应该没有缺陷,不需要进行心智正常检查。如何用-O选项启动也许并不疯狂的程序,详细内容请参考附录B。
10.4 日志
如果你曾经在代码中加入print 语句,在程序运行时输出某些变量的值,你就使用了记日志的方式来调试代码。记日志是一种很好的方式,可以理解程序中发生的事,以及事情发生的顺序。Python的logging模块使得你很容易创建自定义的消息记录。这些日志消息将描述程序执行何时到达日志函数调用,并列出你指定的任何变量当时的值。另一方面,缺失日志信息表明有一部分代码被跳过,从未执行。
10.4.1 使用日志模块
要启用logging模块,在程序运行时将日志信息显示在屏幕上,请将下面的代码复制到程序顶部(但在Python的#!行之下):
import logginglogging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s- %(message)s')
你不需要过于担心它的工作原理,但基本上,当 Python 记录一个事件的日志时,它会创建一个LogRecord对象,保存关于该事件的信息。logging模块的函数让你指定想看到的这个LogRecord对象的细节,以及希望的细节展示方式。
假如你编写了一个函数,计算一个数的阶乘。在数学上,4 的阶乘是1 × 2 × 3 × 4,即24。7的阶乘是1 × 2 × 3 × 4 × 5 × 6 × 7,即5040。打开一个新的文件编辑器窗口,输入以下代码。其中有一个缺陷,但你也会输入一些日志信息,帮助你弄清楚哪里出了问题。将该程序保存为factorialLog.py。
import logginglogging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s- %(message)s')logging.debug('Start of program')def factorial(n): logging.debug('Start of factorial(%s%%)' % (n)) total = 1 for i in range(n + 1): total *= i logging.debug('i is ' + str(i) + ', total is ' + str(total)) logging.debug('End of factorial(%s%%)' % (n)) return totalprint(factorial(5))logging.debug('End of program')
这里,我们在想打印日志信息时,使用logging.debug 函数。这个debug 函数将调用basicConfig,打印一行信息。这行信息的格式是我们在 basicConfig函数中指定的,并且包括我们传递给 debug 的消息。print(factorial(5))调用是原来程序的一部分,所以就算禁用日志信息,结果仍会显示。
这个程序的输出就像这样:
2015-05-23 16:20:12,664 - DEBUG - Start of program2015-05-23 16:20:12,664 - DEBUG - Start of factorial(5)2015-05-23 16:20:12,665 - DEBUG - i is 0, total is 02015-05-23 16:20:12,668 - DEBUG - i is 1, total is 02015-05-23 16:20:12,670 - DEBUG - i is 2, total is 02015-05-23 16:20:12,673 - DEBUG - i is 3, total is 02015-05-23 16:20:12,675 - DEBUG - i is 4, total is 02015-05-23 16:20:12,678 - DEBUG - i is 5, total is 02015-05-23 16:20:12,680 - DEBUG - End of factorial(5)02015-05-23 16:20:12,684 - DEBUG - End of program
factorial 函数返回0作为5的阶乘,这是不对的。for循环应该用从1到5的数,乘以total的值。但logging.debug 显示的日志信息表明,i变量从0开始,而不是1。因为0乘任何数都是0,所以接下来的迭代中,total的值都是错的。日志消息提供了可以追踪的痕迹,帮助你弄清楚何时事情开始不对。
将代码行for i in range(n + 1):改为for i in range(1,n + 1):,再次运行程序。输出看起来像这样:
2015-05-23 17:13:40,650 - DEBUG - Start of program2015-05-23 17:13:40,651 - DEBUG - Start of factorial(5)2015-05-23 17:13:40,651 - DEBUG - i is 1, total is 12015-05-23 17:13:40,654 - DEBUG - i is 2, total is 22015-05-23 17:13:40,656 - DEBUG - i is 3, total is 62015-05-23 17:13:40,659 - DEBUG - i is 4, total is 242015-05-23 17:13:40,661 - DEBUG - i is 5, total is 1202015-05-23 17:13:40,661 - DEBUG - End of factorial(5)1202015-05-23 17:13:40,666 - DEBUG - End of program
factorial(5)调用正确地返回120。日志消息表明循环内发生了什么,这直接指向了缺陷。
你可以看到,logging.debug 调用不仅打印出了传递给它的字符串,而且包含一个时间戳和单词DEBUG。
10.4.2 不要用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 调用。我们不希望禁用日志消息之后,让用户看不到有用的信息。
10.4.3 日志级别
“日志级别”提供了一种方式,按重要性对日志消息进行分类。5个日志级别如表10-1所示,从最不重要到最重要。利用不同的日志函数,消息可以按某个级别记入日志。
表10-1 Python中的日志级别
级别
日志函数
描述
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.')2015-05-18 19:04:26,901 - DEBUG - Some debugging details.>>> logging.info('The logging module is working.')2015-05-18 19:04:35,569 - INFO - The logging module is working.>>> logging.warning('An error message is about to be logged.')2015-05-18 19:04:56,843 - WARNING - An error message is about to be logged.>>> logging.error('An error has occurred.')2015-05-18 19:05:07,737 - ERROR - An error has occurred.>>> logging.critical('The program is unable to recover!')2015-05-18 19:05:45,794 - CRITICAL - The program is unable to recover!
日志级别的好处在于,你可以改变想看到的日志消息的优先级。向basicConfig函数传入logging.DEBUG作为level关键字参数,这将显示所有日志级别的消息(DEBUG是最低的级别)。但在开发了更多的程序后,你可能只对错误感兴趣。在这种情况下,可以将basicConfig 的level参数设置为logging.ERROR,这将只显示ERROR和CRITICAL消息,跳过DEBUG、INFO和WARNING消息。
10.4.4 禁用日志
在调试完程序后,你可能不希望所有这些日志消息出现在屏幕上。logging. disable 函数禁用了这些消息,这样就不必进入到程序中,手工删除所有的日志调用。只要向logging.disable 传入一个日志级别,它就会禁止该级别和更低级别的所有日志消息。所以,如果想要禁用所有日志,只要在程序中添加logging. disable(logging.CRITICAL)。例如,在交互式环境中输入以下代码:
>>> import logging>>> logging.basicConfig(level=logging.INFO, format=' %(asctime)s -%(levelname)s - %(message)s')>>> logging.critical('Critical error! Critical error!')2015-05-22 11:10:48,054 - CRITICAL - Critical error! Critical error!>>> logging.disable(logging.CRITICAL)>>> logging.critical('Critical error! Critical error!')>>> logging.error('Error! Error!')
因为logging.disable 将禁用它之后的所有消息,你可能希望将它添加到程序中接近import logging代码行的位置。这样就很容易找到它,根据需要注释掉它,或取消注释,从而启用或禁用日志消息。
10.4.5 将日志记录到文件
除了将日志消息显示在屏幕上,还可以将它们写入文本文件。logging.basic Config 函数接受filename关键字参数,像这样:
import logginglogging.basicConfig(filename='myProgramLog.txt', level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
日志信息将被保存到myProgramLog.txt文件中。虽然日志消息很有用,但它们可能塞满屏幕,让你很难读到程序的输出。将日志信息写入到文件,让屏幕保持干净,又能保存信息,这样在运行程序后,可以阅读这些信息。可以用任何文件编辑器打开这个文本文件,诸如Notepad或TextEdit。
10.5 IDLE的调试器
“调试器”是IDLE的一项功能,让你每次执行一行程序。调试器将运行一行代码,然后等待你告诉它继续。像这样让程序运行“在调试器之下”,你可以随便花多少时间,检查程序运行时任意一个时刻的变量的值。对于追踪缺陷,这是一个很有价值的工具。
要启用IDLE的调试器,就在交互式环境窗口中点击DebugDebugger。这将打开调试控制(Debug Control)窗口,如图10-1所示。
图10-1 调试控制窗口
当调试控制窗口出现后,勾选全部4个复选框:Stack、Locals、Source和Globals。这样窗口将显示全部的调试信息。调试控制窗口显示时,只要你从文件编辑器运行程序,调试器就会在第一条指令之前暂停执行,并显示下面的信息:
- 将要执行的代码行;
- 所有局部变量及其值的列表;
- 所有全局变量及其值的列表。
你会注意到,在全局变量列表中,有一些变量你没有定义,诸如 builtins 、 doc 、 file ,等等。它们是Python在运行程序时,自动设置的变量。这些变量的含义超出了本书的范围,你可以暂时忽略它们。
程序将保持暂停,直到你按下调试控制窗口的5个按钮中的一个:Go、Step、Over、Out或Quit。
10.5.1 Go
点击Go按钮将导致程序正常执行至终止,或到达一个“断点”(断点在本章稍后介绍)。如果你完成了调试,希望程序正常继续,就点击Go按钮。
10.5.2 Step
点击Step按钮将导致调试器执行下一行代码,然后再次暂停。如果变量的值发生了变化,调试控制窗口的全局变量和局部变量列表就会更新。如果下一行代码是一个函数调用,调试器就会“步入”那个函数,跳到该函数的第一行代码。
10.5.3 Over
点击Over按扭将执行下一行代码,与Step按钮类似。但是,如果下一行代码是函数调用,Over按钮将“跨过”该函数的代码。该函数的代码将以全速执行,调试器将在该函数返回后暂停。例如,如果下一行代码是print 调用,你实际上不关心内建print 函数中的代码,只希望传递给它的字符串打印在屏幕上。出于这个原因,使用Over按钮比使用Step按钮更常见。
10.5.4 Out
点击Out按钮将导致调试器全速执行代码行,直到它从当前函数返回。如果你用Step按钮进入了一个函数,现在只想继续执行指令,直到该函数返回,那就点击Out按钮,从当前的函数调用“走出来”。
10.5.5 Quit
如果你希望完全停止调试,不必继续执行剩下的程序,就点击Quit按钮。Quite按钮将马上终止该程序。如果你希望再次正常运行你的程序,就再次选择DebugDebugger,禁用调试器。
10.5.6 调试一个数字相加的程序
打开一个新的文件编辑器窗口,输入以下代码:
print('Enter the first number to add:')first = inputprint('Enter the second number to add:')second = inputprint('Enter the third number to add:')third = inputprint('The sum is ' + first + second + third)
将它保存为buggyAddingProgram.py,不启用调试器,第一次运行它。程序的输出像这样:
Enter the first number to add:5Enter the second number to add:3Enter the third number to add:42The sum is 5342
这个程序没有崩溃,但求和显然是错的。让我们启用调试控制窗口,再次运行它,这次在调试器控制之下。
当你按下F5或选择RunRun Module(启用DebugDebugger,选中调试控制窗口的所有4个复选框),程序启动时将暂停在第1行。调试器总是会暂停在它将要执行的代码行上。调试控制窗口看起来如图10-2所示。
图10-2 程序第一次在调试器下运行时的调试控制窗口
点击一次Over按钮,执行第一个print 调用。这里应该使用Over按钮,而不是Step,因为你不希望进入到print 函数的代码中。调试控制窗口将更新到第2行,文件编辑器窗口的第2行将高亮显示,如图10-3所示。这告诉你程序当前执行到 哪里。
图10-3 点击Over按钮后的调试控制窗口
再次点击Over按钮,执行input 函数调用,当IDLE等待你在交互式环境窗口中为input 调用输入内容时,调试控制窗口中的按钮将被禁用。输入5并按回车。调试控制窗口按钮将重新启用。
继续点击Over按钮,输入3和42作为接下来的两个数,直到调试器位于第7行,程序中最后的print 调用。调试控制窗口应该如图10-4所示。可以看到,在全局变量的部分,第一个、第二个和第三个变量设置为字符串值,而不是整型值。当最后一行执行时,这些字符串连接起来,而不是加起来,导致了这个缺陷。
用调试器单步执行程序很有用,但也可能很慢。你常常希望程序正常运行,直到它到达特定的代码行。你可以使用断点,让调试器做到这一点。
10.5.7 断点
“断点”可以设置在特定的代码行上,当程序执行到达该行时,它迫使调试器暂停。在一个新的文件编辑器窗口中,输入以下程序,它模拟投掷1000次硬币。将它保存为coinFlip.py。
图10-4 在最后一行的调试控制窗口。这些变量被设置为字符串,导致了这个缺陷
import random heads = 0 for i in range(1, 1001):❶ if random.randint(0, 1) == 1: heads = heads + 1 if i == 500:❷ print('Halfway done!') print('Heads came up ' + str(heads) + ' times.')
在半数时间里,random.randint(0,1)调用❶将返回0,在另外半数时间将返回1。这可以用来模拟50/50的硬币投掷,其中1代表正面。当不用调试器运行该程序时,它很快输出下面的内容:
Halfway done!Heads came up 490 times.
如果启用调试器运行这个程序,就必须点击几千次Over按钮,程序才能结束。如果你对程序执行到一半时heads的值感兴趣,等1000次硬币投掷完500次,可以在代码行print('Halfway done!')❷上设置断点。要设置断点,在文件编辑器中该行代码上点击右键,并选择Set Breakpoint,如图10-5所示。
图10-5 设置断点
你不会在if语句上设置断点,因为if语句会在循环的每次迭代中都执行。通过在if语句内的代码上设置断点,调试器就会只在执行进入if语句时才中断。
带有断点的代码行会在文件编辑器中以黄色高亮显示。如果在调试器下运行该程序,开始它会暂停在第一行,像平时一样。但如果点击Go,程序将全速运行,直到设置了断点的代码行。然后可以点击Go、Over、Step或Out,正常继续。
如果希望清除断点,在文件编辑器中该行代码上点击右键,并从菜单中选择Clear Breakpoint。黄色高亮消失,以后调试器将不会在该行代码上中断。
10.6 小结
断言、异常、日志和调试器,都是在程序中发现和预防缺陷的有用工具。用Python语句实现的断言,是实现心智正常检查的好方式。如果必要的条件没有保持为True,它将尽早给出警告。断言所针对的错误,是程序不应该尝试恢复的,而是应该快速失败。否则,你应该抛出异常。
异常可以由try和except语句捕捉和处理。logging模块是一种很好的方式,可以在运行时查看代码的内部,它比使用print 函数要方便得多,因为它有不同的日志级别,并能将日志写入文本文件。
调试器让你每次单步执行一行代码。或者,可以用正常速度运行程序,并让调试器暂停在设置了断点的代码行上。利用调试器,你可以看到程序在运行期间,任何时候所有变量的值。
这些调试工具和技术将帮助你编写正确工作的程序。不小心在代码中引入缺陷,这是不可避免的,不论你有多少年的编码经验。
10.7 习题
1.写一条 assert 语句,如果变量 spam 是一个小于 10 的整数,就触发AssertionError。
2.编写一条assert语句,如果eggs和bacon包含的字符串彼此相同,而且不论大小写如何,就触发AssertionError(也就是说,'hello' 和 'hello' 被认为相同,'goodbye' 和 'GOODbye' 也被认为相同)。
3.编写一条assert语句,总是触发AssertionError。
4.为了能调用logging.debug,程序中必须加入哪两行代码?
5.为了让logging.debug 将日志消息发送到名为programLog.txt的文件中,程序必须加入哪两行代码?
6.5个日志级别是什么?
7.你可以加入哪一行代码,禁用程序中所有的日志消息?
8.显示同样的消息,为什么使用日志消息比使用print 要好?
9.调试控制窗口中的Step、Over和Out按钮有什么区别?
10.在点击调试控制窗口中的Go按钮后,调试器何时会停下来?
11.什么是断点?
12.在IDLE中,如何在一行代码上设置断点?
10.8 实践项目
作为实践,编程完成下面的任务。
调试硬币抛掷
下面程序的意图是一个简单的硬币抛掷猜测游戏。玩家有两次猜测机会(这是一个简单的游戏)。但是,程序中有一些缺陷。让程序运行几次,找出缺陷,使该程序能正确运行。
import randomguess = ''while guess not in ('heads', 'tails'): print('Guess the coin toss! Enter heads or tails:') guess = inputtoss = random.randint(0, 1) # 0 is tails, 1 is headsif toss == guess: print('You got it!')else: print('Nope! Guess again!') guesss = input if toss == guess: print('You got it!') else: print('Nope. You are really bad at this game.')