Python编程轻松进阶
上QQ阅读APP看书,第一时间看更新

在面对错误信息中的一大段技术性文本时,很多程序员会下意识地选择忽略。但程序出错的原因就在其中,我们需要用以下两个步骤找到它:检查回溯信息1,以及在网上搜索错误信息。

1 traceback是一个内置模块。如果程序报错,那么traceback模块会提供异常发生时的程序栈。——译者注

如果代码抛出的异常未被except语句捕获,那么Python程序就会崩溃。此时,Python会展示异常消息和回溯。回溯也被称为调用栈,它展示了异常在程序中的具体位置,以及引发异常的函数调用链路。

我们来做个阅读回溯信息的练习,输入以下程序(有bug)并保存为abcTraceback.py。注意,行号仅起参考作用,不属于程序的一部分。

 1. def a():
 2.     print('Start of a()')
 3.     b() # 调用b() ❶
 4.
 5. def b():
 6.     print('Start of b()')
 7.     c() # 调用c() ❷
 8.
 9. def c():
10.     print('Start of c()')
11.     42 / 0 # 该行会导致以0为除数的错误 ❸
12.
13. a() # 调用a()

这段程序中,函数a()调用了b()❶,b()调用了c()❷,调用关系是a()→b()→c()。在函数c()中,表达式42 / 0❸引发了一个以0为除数的错误。该程序运行时的输出是:

Start of a()
Start of b()
Start of c()
Traceback (most recent call last):
  File "abcTraceback.py", line 13, in <module>
    a() # 调用a()
  File "abcTraceback.py", line 3, in a
    b() #调用b()
  File "abcTraceback.py", line 7, in b
    c() # 调用c()
  File "abcTraceback.py", line 11, in c
    42 / 0 # 该行会导致以0为除数的错误
ZeroDivisionError: division by zero

让我们从下面这行开始,逐行检查信息:

Traceback (most recent call last):

这行信息说明接下来是回溯信息。“most recent call last”表明将按照调用顺序列出所有函数调用,从第一个函数调用开始,到最后一个调用为止。

下一行展示了回溯的第一个函数调用:

File "abcTraceback.py", line 13, in <module>
  a() # 调用a()

这两行是栈帧摘要,它展示了栈帧对象的内部信息,其中包括函数调用时的局部变量、函数结束后返回的位置以及其他与函数调用相关的数据。栈帧对象随着函数的调用而创建,直到函数返回时被销毁。回溯显示了导致崩溃的每一个函数调用栈帧的摘要信息。这次函数调用发生在abcTraceback.py文件的第13行,<module>表示这一行位于全局作用域。接下来是第13行的代码,行首有两个空格的缩进。

再往后4行分别是接下来两个栈帧的摘要。

File "abcTraceback.py", line 3, in a
  b() # 调用b()
File "abcTraceback.py", line 7, in b
  c() # 调用a()

我们可以发现,嵌套在a()函数中的b()函数在第3行被调用,接着导致嵌套在b()函数中的c()函数在第7行被调用。注意,尽管在b()函数和c()函数调用之前,第2、6、10行的print()函数也被调用了,但它并未展示在回溯信息中,这是因为只有导致异常的函数调用的所在行才会被展示在回溯信息中。

最后一个栈帧摘要显示了未被处理的异常的名字和信息。

File "abcTraceback.py", line 11, in c
  42 / 0 # 该行会导致以0为除数的错误
ZeroDivisionError: division by zero

需要注意的是,回溯给出的行号是Python最终检测到错误的地方,引起bug的罪魁祸首可能在这行之前。

错误信息像谜语一样,是出了名地又短又难懂。在数学上,0不能作为除数,这是一个常见的bug。你得知道这一点,否则“division by zero”(以0为除数)这几个字对你而言没有任何意义。这个示例程序中的bug还不算很难查找。只要看看栈帧摘要指出的那行代码,就能轻易地发现是表达式42 / 0引发了以0为除数的错误。

让我们再看一个难一点的案例。输入以下代码,保存为zeroDivideTraceback.py:

def spam(number1, number2):
    return number1 / (number2 - 42)

spam(101, 42)

运行时的输出如下:

Traceback (most recent call last):
  File "zeroDivideTraceback.py", line 4, in <module>
    spam(101, 42)
  File "zeroDivideTraceback.py", line 2, in spam
    return number1 / (number2 - 42)
ZeroDivisionError: division by zero

错误信息跟上一个示例相同,但从返回值number1 / (number2 - 42)很难直接看出以0为除数的错误。推断过程是这样的:由于/运算符的出现发生了除法运算,除数,也就是表达式number2 - 42一定等于0了。结论显而易见:一旦参数number2被设置为42,spam()函数就会失败。

有时候,回溯信息可能指出错误出现在真正造成bug的行后的那行代码。例如接下来的程序,第一行的print()函数调用语句缺少闭合的括号:

print('Hello.'
print('How are you?')

但程序的错误信息指出问题是在第2行:

File "example.py", line 2
  print('How are you?')
      ^
SyntaxError: invalid syntax

这是因为Python解释器直到读到第2行时才意识到存在语法错误。回溯可以表明程序从哪一行开始出问题,但这一行并不一定是罪魁祸首。如果栈帧摘要没能提供足够的信息排查出bug,又或者造成bug的罪魁祸首在回溯指出的位置前的某一行,那么只能退而求其次,即使用调试器逐行调试程序或者检查日志信息,这可能会多花不少时间。另一种方式是在网上搜索错误消息,没准儿会更快地找到解决问题的关键线索。

错误信息通常短得根本构不成一个完整的句子。因为对程序员而言很常见,所以它们是作为提示信息而非完整的解释出现的。如果某个错误之前没遇到过,那么你可以直接将其复制并粘贴到网上搜索,大概率会得到错误信息的具体含义及其可能的原因等详细解释。图1-1显示了搜索python“ZeroDivisionError: division by zero”的结果。这里有两个技巧可以帮助精确查找:一是使用引号包裹错误信息作为关键词;二是将python一起作为关键词。

图1-1 复制错误信息,将其粘贴到在线搜索工具中,可以快速得到相关解释和解决方案

在网上搜索错误信息并不是耍小聪明,没有人可以记住一门编程语言可能出现的所有错误信息。在网上搜索编程问题的答案对职业程序员而言是家常便饭。

你可能想去除代码中特有的错误信息。来看下面这个例子:

>>> print(employeRecord)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'employeRecord' is not defined ❶
>>> 42 - 'hello'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for -: 'int' and 'str' ❷

这个例子中的变量employeRecord存在拼写错误,导致了错误❶。由于错误信息“NameError: name 'employeRecord' is not defined”中的employeRecord是你的代码所特有的,因此最好搜索python “NameError: name”“is not defined”

在最后一行,错误信息❷中的int和str似乎分别指向42和hello这两个值。所以将搜索词缩减为python“TypeError: unsupported operand type(s) for”可以避免包括代码中的特有部分。

如果通过这些搜索没能得到有价值的结果,那么可以尝试搜索完整的错误信息。