Automate the Boring Stuff with Python: Practical Programming for Total Beginners (2nd Edition)
Copyright © 2020 by Al Sweigart.
有一些工具和技巧可以确定代码在做什么,以及哪儿出了问题。
首先,查看日志记录和断言,这两个功能可以帮助及早发现缺陷。通常,发现缺陷的时间越早,越容易修复。
其次,学习如何使用调试器。调试器是 Mu 的一项功能,可一次执行一条指令,这样可以在代码运行时检查变量中的值,并跟踪值在程序过程中的变化。
11.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 "<pyshell#191>", line 1, in <module>
raise Exception('This is the error message.')
Exception: This is the error message.
通常,是调用函数的代码,而不是函数本身的代码,知道如何处理异常。这意味着通常会在函数内看到一个 raise
语句,并且在调用该函数的代码中会看到 try
和 except
语句。
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: # stores an Exception object in a variable named err
print('An exception happened: ' + str(err))
可以在 https://autbor.com/boxprint 上查看该程序的执行。
11.2 以字符串形式获取回溯信息
当 Python 遇到错误时,它会产生一些错误信息,称为回溯(traceback)。回溯包含了出错消息、导致该错误的代码行号,以及导致该错误的函数调用的序列。此调用序列称为调用堆栈(call stack)。
# errorExample.py
def spam():
bacon()
def bacon():
raise Exception('This is the error message.')
spam()
运行上面的代码,输出看起来像这样:
Traceback (most recent call last):
File "errorExample.py", line 8, in <module>
spam()
File "errorExample.py", line 3, in spam
bacon()
File "errorExample.py", line 6, in bacon
raise Exception('This is the error message.')
Exception: This is the error message.
每当抛出的异常未处理时,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.')
111
The traceback info was written to errorInfo.txt.
write()
方法的返回值是111,因为 111 个字符被写入到文件中。回溯文本被写入errorInfo.txt。
Traceback (most recent call last):
File "<pyshell#28>", line 2, in <module>
Exception: This is the error message.
11.3 断言
断言(assertion)是一项健全性检查,以确保代码没有执行明显错误的操作。这些健全性检查由 assert
语句执行。如果健全性检查失败,则会抛出 AssertionError
异常。在代码中,一个 assert
语句包含以下内容:
① assert
关键字;
② 条件(即,一个求值为 True
或 False
的表达式);
③ 逗号
④ 当条件为 False
时,显示的字符串。
assert
语句是说:“我断言该条件成立,否则,某处存在错误,因此请立即停止该程序。”
>>> ages = [26, 57, 92, 54, 22, 15, 17, 80, 47, 73]
>>> ages.sort()
>>> ages
[15, 17, 22, 26, 47, 54, 57, 73, 80, 92]
>>> assert
ages[0] <= ages[-1] # Assert that the first age is <= the last age.
这是一个健全性检测;如果 sort()
中的代码没有出错,这个断言为真。assert
语句不做任何事。
如果代码中有缺陷,assert
语句抛出 AssertionError
:
>>> ages = [26, 57, 92, 54, 22, 15, 17, 80, 47, 73]
>>> ages.reverse()
>>> ages
[73, 47, 80, 17, 15, 22, 54, 92, 57, 26]
>>> assert ages[0] <= ages[-1] # Assert that the first age is <= the last age.
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
与异常不同,代码不应该使用 try
和 except
处理 assert
语句。如果 assert
失败,则程序应该崩溃。通过这样的“快速失败”,可以缩短第一次发现错误的时间。这将减少在找到错误原因之前必须检查的代码量。
断言是针对程序员错误,而不是用户错误。断言只会在程序正在开发时失败;用户永远都不会在完成的程序中看到断言错误。对于程序可能会在其正常运行过程中遇到的错误(例如找不到文件或用户输入无效数据),请抛出异常,而不是使用 assert
语句检测。
不应使用断言语句来引发异常,因为用户可以选择关闭断言。如果使用 python -O myscript.py
而不是 python myscript.py
运行 Python 脚本,Python 将跳过 assert
语句。
断言也不能替代全面测试。例如,如果先前的 ages
示例设置为 [10,3,2,1,20],则 assert ages[0] <= ages[-1]
断言不会注意到列表未排序。
在交通灯模拟中使用断言
假设你正在构建一个交通信号灯模拟程序。表示交叉路口交通信号灯的数据结构是一个字典,键为 'ns'
和 'ew'
,分别表示南北向和东西向的信号灯。这些键上的值是字符串 'green'
,'yellow'
或 'red'
之一。
market_2nd = {'ns': 'green', 'ew': 'red'}
mission_16th = {'ns': 'red', 'ew': 'green'}
要启动项目,需要编写一个 switchLights()
函数,该函数以路口字典作为参数,并切换红绿灯。
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()
时添加一个断言,确保至少一个灯是红色的,则可能在函数底部包括了以下内容:
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'}
11.4 日志
日志是了解程序中发生的事情以及发生的顺序的好方法。Python 的 logging
模块可创建自定义消息的记录。这些日志消息描述,程序执行何时到达日志记录函数调用,并列出在该时间点指定的所有变量。另一方面,缺少日志消息表示部分代码已跳过且从未执行。
使用 logging 模块
要使 logging
模块能够在程序运行时在屏幕上显示日志消息,请将以下内容复制到程序顶部(但在 #!
行下方):
import logging
logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s')
基本上,Python 在记录事件时会创建一个 LogRecord 对象,该对象保存有关该事件的信息。logging
模块的 basicConfig()
函数可指定要查看的 LogRecord 对象的详细信息,以及如何显示这些详细信息。
import logging
logging.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 total
print(factorial(5))
logging.debug('End of program')
如果想要打印日志信息,使用 logging.debug()
函数。debug()
函数调用 basicConfig()
,并打印一行信息。此信息采用在 basicConfig()
中指定的格式,并包括传递给 debug()
的消息。
程序的输出看起来像这样:
2019-05-23 16:20:12,664 - DEBUG - Start of program
2019-05-23 16:20:12,664 - DEBUG - Start of factorial(5)
2019-05-23 16:20:12,665 - DEBUG - i is 0, total is 0
2019-05-23 16:20:12,668 - DEBUG - i is 1, total is 0
2019-05-23 16:20:12,670 - DEBUG - i is 2, total is 0
2019-05-23 16:20:12,673 - DEBUG - i is 3, total is 0
2019-05-23 16:20:12,675 - DEBUG - i is 4, total is 0
2019-05-23 16:20:12,678 - DEBUG - i is 5, total is 0
2019-05-23 16:20:12,680 - DEBUG - End of factorial(5)
0
2019-05-23 16:20:12,684 - DEBUG - End of program
将代码行 for i in range(n + 1):
改为 for i in range(1,n + 1):
,再次运行程序。输出看起来像这样:
2019-05-23 17:13:40,650 - DEBUG - Start of program
2019-05-23 17:13:40,651 - DEBUG - Start of factorial(5)
2019-05-23 17:13:40,651 - DEBUG - i is 1, total is 1
2019-05-23 17:13:40,654 - DEBUG - i is 2, total is 2
2019-05-23 17:13:40,656 - DEBUG - i is 3, total is 6
2019-05-23 17:13:40,659 - DEBUG - i is 4, total is 24
2019-05-23 17:13:40,661 - DEBUG - i is 5, total is 120
2019-05-23 17:13:40,661 - DEBUG - End of factorial(5)
120
2019-05-23 17:13:40,666 - DEBUG - End of program
不要使用 print() 函数进行调试
如果使用 print()
函数进行调试,那么调试完成后,需要花大量时间从代码中删除每个日志消息的 print()
调用。甚至有可能不小心删除一些不是用来产生日志消息的 print()
调用。
使用 logging
模块,可以轻松地在显示和隐藏日志消息之间进行切换。
日志消息仅供程序员使用,而不是用户使用。
对于用户想要查看的消息,比如“找不到文件”或“输入无效,请输入一个数字”,应该使用 print()
调用。
日志级别
日志级别(logging levels)提供了一种按重要性对日志消息进行分类的方法。有 5 个日志级别,如表11-1 所示,从最不重要到最重要。可以使用不同的日志函数按每个级别记录消息。
表11-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.')
2019-05-18 19:04:26,901 - DEBUG - Some debugging details.
>>> logging.info('The logging module is working.')
2019-05-18 19:04:35,569 - INFO - The logging module is working.
>>> logging.warning('An error message is about to be logged.')
2019-05-18 19:04:56,843 - WARNING - An error message is about to be logged.
>>> logging.error('An error has occurred.')
2019-05-18 19:05:07,737 - ERROR - An error has occurred.
>>> logging.critical('The program is unable to recover!')
2019-05-18 19:05:45,794 - CRITICAL - The program is unable to recover!
日志级别的好处是,可以更改想要查看的日志消息的优先级。将 logging.DEBUG
传递给 basicConfig()
函数的 level
关键字参数,显示来自所有日志级别的消息(DEBUG 是最低级别)。
如果只对错误感兴趣,可以将 basicConfig()
的 level
参数设置为 logging.ERROR
。这将仅显示 ERROR 和 CRITICAL 消息,并跳过 DEBUG,INFO 和 WARNING 消息。
禁用日志
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!')
2019-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
行附近。这样,可以根据需要启用或禁用日志消息,轻松地找到它,注释掉该调用或取消注释。
将日志记录到文件
logging.basicConfig()
函数接受 filename
关键字参数。
import logging
logging.basicConfig(filename='myProgramLog.txt', level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
日志消息将保存到 myProgramLog.txt。将日志消息写入文件,让屏幕保持干净,并存储消息,以便在运行程序后可以阅读它们。
11.5 Mu 的调试器
调试器(debugger)是 Mu 编辑器,IDLE 和其他编辑器软件的功能,可以一次一行执行程序。这是用于跟踪错误的有价值的工具。
要在 Mu 的调试器下运行程序,请点击按钮行顶部的 Debug 按钮,它在 Run 按钮旁边。常规输出窗格在底部,Debug Inspector 窗格将在窗口的右侧打开。此窗格列出了程序中变量的当前值。
在下图中,调试器在程序运行第一行代码之前就暂停了程序的执行。可以在文件编辑器中看到此行突出显示。
调试模式还将以下新按钮添加到编辑器的顶部: Continue,Step Over,Step In 和 Step Out。通常的 Stop 按钮也可用。
Continue
单击 Continue 按钮将使程序正常执行,直到终止或到达断点。如果完成调试并希望程序正常运行,请单击 Continue 按钮。
Step In
单击 Step In 按钮将使调试器执行下一行代码,然后再次暂停。如果下一行代码是函数调用,则调试器将“进入”该函数并跳转到该函数的第一行代码。
Step Over
单击 Step Over 按钮将执行下一行代码,类似于 Step In 按钮。但是,如果下一行代码是函数调用,则 Step Over 按钮将“跳过”函数中的代码。该函数的代码将全速执行,并且一旦函数调用返回,调试器将暂停。
Step Out
单击 Step Out 按钮将使调试器以全速执行代码行,直到从当前函数返回为止。如果已使用 Step In 按钮进入函数调用,而现在只想继续执行指令直到退出,请单击 Step Out 按钮以“跳出”当前函数调用。
Stop
如果要完全停止调试并且不想继续执行程序的其余部分,请单击 Stop 按钮。Stop 按钮将立即终止程序。
调试一个数字相加的程序
print('Enter the first number to add:')
first = input()
print('Enter the second number to add:')
second = input()
print('Enter the third number to add:')
third = input()
print('The sum is ' + first + second + third)
将其保存为 buggyAddingProgram.py,不启用调试器,首先运行它。该程序将输出像这样:
Enter the first number to add:
5
Enter the second number to add:
3
Enter the third number to add:
42
The sum is 5342
该程序没有崩溃,但是总和显然是错误的。在调试器下,再次运行程序。
当单击 Debug 按钮时,程序将在第 1 行暂停,这是将要执行的代码行。
单击 Step Over 按钮一次,执行第一个 print()
调用。
再次单击 Step Over,执行 input()
函数调用。输入 5 并按 Enter。
继续单击 Step Over,然后输入 3 和 42 作为接下来的两个数字。
当执行最后一行时,Python 将这些字符串连接起来,而不是将数字加在一起,从而导致错误。
断点
断点(breakpoint)可以设置在特定的代码行上,并在程序执行到该行时强制调试器暂停。
# coinFlip.py - Simulates flipping a coin 1,000 times.
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.')
在没有调试器的情况下运行该程序时,它会迅速输出像下面这样的内容:
Halfway done!
Heads came up 490 times.
如果对程序执行到一半时 heads
的值感兴趣,则当完成 1000 次硬币翻转中的 500 个时,可以在行 print('Halfway done!')
设置一个断点。要设置断点,请在文件编辑器中单击行号,以使出现一个红点。
带有断点的行旁边将有一个红点。如果要删除断点,请再次单击行号。红点将消失,并且调试器将来不会在该行上中断。
11.6 IDLE 的调试器
要启用 IDLE 的调试器,请在交互式 Shell 窗口中单击 Debug▸Debugger。这将打开 Debug Control 窗口,如下图所示。
出现 Debug Control 窗口时,选中 Stack,Locals,Source 和 Globals 这 4 个复选框,以便该窗口显示全套的调试信息。显示 Debug Control 窗口时,无论何时从文件编辑器运行程序,调试器都会在第一条指令之前暂停执行并显示以下内容:
① 将要执行的代码行;
② 所有局部变量及其值的列表;
③ 所有全局变量及其值的列表。
该程序将保持暂停状态,直到按 Debug Control 窗口中的 5 个按钮之一:Go,Step,Over,Out 或 Quit。
Go
单击 Go 按钮将使程序正常执行,直到终止或到达断点。
Step
单击 Step 按钮将使调试器执行下一行代码,然后再次暂停。Debug Control 窗口的全局和局部变量列表会随着值的改变而更新。如果下一行代码是函数调用,则调试器将“进入”该函数并跳转到该函数的第一行代码。
Over
单击 Over 按钮将执行下一行代码,类似于 Step 按钮。但是,如果下一行代码是函数调用,则 Over 按钮将“跳过”函数中的代码。该函数的代码将全速执行,并且一旦函数调用返回,调试器将暂停。
Out
单击 Out 按钮将使调试器全速执行代码行,直到从当前函数返回为止。如果已经使用 Step 按钮进入了一个函数调用,而现在只是想继续执行指令直到退出,请单击 Out 按钮以“跳出”当前函数调用。
Quit
如果要完全停止调试并且不想继续执行程序的其余部分,请单击 Quit 按钮。
如果要再次正常运行程序,请再次选择 Debug▸Debugger 以禁用调试器。
调试一个数字相加的程序
在未启用调试器的情况下,先运行 buggyAddingProgram.py。
启用 Debug Control 窗口,然后在调试器下,再次运行它。
当按 F5 键或选择 Run▸Run Module(启用 Debug▸Debugger 并选中 Debug Control 窗口上的所有 4 个复选框)时,程序启动,在第 1 行暂停。
继续单击 Over,然后输入数字。
当执行最后一行时,Python 将这些字符串连接起来,而不是将数字加在一起,从而导致错误。
断点
要设置断点,请在文件编辑器中右键单击该行,然后选择 Set Breakpoint。
带有断点的行将在文件编辑器中以黄色突出显示。
如果要删除断点,请在文件编辑器中右键单击该行,然后从菜单中选择 Clear Breakpoint。黄色突出显示将消失,并且调试器将来不会在该行上中断。
11.7 实践项目
调试硬币抛掷
以下程序旨在成为一个简单的掷硬币猜谜游戏。玩家有两个猜测(这是一个简单的游戏)。但是,该程序存在一些错误。多次运行该程序,以查找使程序无法正常运行的错误。
import random
guess = ''
while guess not in ('heads', 'tails'):
print('Guess the coin toss! Enter heads or tails:')
guess = input()
toss = random.randint(0, 1) # 0 is tails, 1 is heads
if 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.')
该程序没有崩溃,但结果错误。
在调试器下运行程序,可以看出 guess
只能是 'heads'
和 'tails'
中的一个,toss
只能是 0
和 1
中的一个,两者永远不会相等。
使用字典将整数和字符串对应起来。修改程序如下:
import random
checkDict = {0:'heads', 1:'tails'}
guess = ''
while guess not in ('heads', 'tails'):
print('Guess the coin toss! Enter heads or tails:')
guess = input()
toss = random.randint(0, 1) # 0 is tails, 1 is heads
if checkDict[toss] == guess:
print('You got it!')
else:
print('Nope! Guess again!')
guesss = input()
if checkDict[toss] == guess:
print('You got it!')
else:
print('Nope. You are really bad at this game.')
学习网站:
https://automatetheboringstuff.com/2e/chapter11/
https://automatetheboringstuff.com/chapter10/