当一个异常在你的代码中被引发时,Python会打印一个traceback(回溯)。如果你是第一次看到回溯输出,或者你不知道它在告诉你什么,那么它可能会让你不知所措。但是Python回溯具有丰富的信息,可以帮助你诊断和修复代码中引发异常的原因。理解Python回溯提供了什么信息对于成为一个更好的Python程序员至关重要。
在本教程结束时,你将能够:
回溯是一个报告,其中包含在你的代码中某个特定点上执行的函数调用。回溯有很多名称,包括堆栈跟踪、堆栈回溯、向后追溯,也许还有其他名称。在Python中,使用的术语是回溯。
当你的程序引发一个异常时,Python将打印当前回溯信息以帮助你知道哪里出错了。下面是一个例子来说明这种情况:
在这里,我们使用参数someone调用greet()。但是,在greet()中,这个变量名没有被使用。相反,它在print()调用中被错误拼写为someon。
注意:本教程假设你理解Python异常。如果你不熟悉或者只是想复习一下,那么你应该查看《Python异常:介绍》。
当你运行这个程序时,你会得到以下回溯:
此回溯输出包含你诊断问题所需的所有信息。回溯输出的最后一行告诉你引发的是什么类型的异常,以及关于此异常的一些相关信息。回溯的前几行指出了引发异常的代码。
在上面的回溯中,该异常是一个NameError,这意味着有一个对未定义的名称(变量、函数、类)的引用。在本例中,被引用的名称是someon。
本例中的最后一行有足够的信息来帮助你解决问题。在代码中搜索名称someon时(这是一个拼写错误)将为你指明正确的方向。然而,你的代码通常要比这个例子复杂得多。
当你试图确定代码中引发异常的原因时,Python回溯中包含许多有用的信息。在本节中,你将浏览不同的回溯,以便理解回溯中包含的不同信息。
Python回溯概述
每个Python回溯都有几个重要的部分。下图突出显示了各个部分:
在Python中,最好从底部往上阅读回溯:
当你在命令行中执行代码和在REPL中运行代码时,回溯输出会有一些不同。下面是在REPL中执行的与上一节相同的代码以及执行后产生的回溯输出:
注意用 "<stdin>"替代文件名的地方。这是有意义的,因为你是通过标准输入来输入代码的。而且,执行的代码行不会显示在回溯中。
注意: 如果你经常在其他编程语言中查看回溯,那么你会注意到它与Python的回溯方式相比有一个主要不同。大多数其他语言在顶部打印异常,然后从顶部到底部,从最近的调用到最远的调用。
前面已经说过,但这里还是要重申一下,你应该从底部到顶部来阅读Python回溯。这是非常有用的,因为回溯会被打印出来,而你的终端(或你正在阅读回溯的任何地方)通常会在输出的底部结束,这为你提供了开始阅读回溯的最佳位置。
具体的回溯浏览
浏览一些具体的回溯输出,可以帮助你更好地理解并查看回溯将为你提供什么信息。
下面代码被用在例子中来说明Python回溯为你提供的信息:
在这里,who_to_greet()接受一个值person,并返回它或提示输入一个值返回来代替。
然后,greet()接受一个要打招呼的名字someone和一个可选的greeting值,并传入someone值来调用print(). who_to_greet()。
最后,greet_many()将遍历people列表并调用greet()。如果调用greet()时引发异常,则打印出一个简单的备份问候语。
只要你提供了正确的输入,这段代码没有任何bug会引发异常。
如果你在greetings.py的底部添加一个greet()调用,然后指定一个它无法预期的关键字参数(例如,greet('Chad', greting='Yo')),那你将得到以下回溯:
同样,对于一个Python回溯,最好是向后处理,向上移动输出。从回溯的最后一行开始,你可以看到异常是一个TypeError。异常类型后面的消息(冒号后面的所有内容)为你提供了一些很好的信息。它告诉你,greet()被调用时带有一个它没有预料到的关键字参数,并为你提供了未知参数的名称:greting。
继续向上移动,你可以看到导致异常的行。在本例中,这个行就是我们在greetings .py的底部添加的greet()调用。
向上的下一行给出了代码所在文件的路径、代码所在文件的行号以及代码所在的模块。在本例中,因为我们的代码没有使用任何其他Python模块,所以这里我们只会看到<module>,这意味着这就是正在被执行的文件。
使用不同的文件和不同的输入,你可以看到回溯实际上会向你指出正确的方向来找到问题。如果你正在进行跟踪,请从greetings.py底部删除有问题的greet()调用,并将以下文件添加到你的目录中:
这里,你已经设置了另一个Python文件,该文件将导入前面的模块greetings.py,并从中使用greet()。下面是运行example.py时会发生的事情:
在本例中捕获的异常同样是一个TypeError,但这一次消息的帮助要小一些。它告诉你,在代码的某个地方,它期望使用一个字符串,但是传入了一个整数。
向上移动,你可以看到被执行的代码行。然后是文件和代码的行号。不过,这一次我们得到的不是<module>,而是正在被执行的函数的名称greet()。
转到下一个被执行的代码行,我们看到传入了一个整数的有问题的greet()调用。
有时在异常引发之后,另一段代码会捕获该异常并导致另一个异常。在这些情况下,Python将按照接收异常的顺序输出所有异常回溯,同样会以最近一次抛出的异常的回溯结束。
这可能有点令人困惑,这里有一个例子。在greetings.py的底部添加一个对greet_many()的调用。
这将会打印出对所有三个人的问候语。但是,如果你运行这段代码,你会看到一个输出多个回溯的例子:
注意上面输出中以During handling开始的高亮显示的行。在所有回溯之间,你将看到这一行。它的消息非常清楚,当你的代码试图处理前一个异常时,又引发了另一个异常。
注意: Python的显示以前异常回溯的特性是在Python 3中添加的。在Python2中,你只会得到最后一个异常的回溯。
你之前已经看到过前面的异常,就在你使用一个整数调用greet()时。因为我们在要打招呼的人员列表中添加了一个1,所以我们可以预期得到相同的结果。但是,函数greet_many()将greet()调用封装在一个try和except块中。这样,当greet()引发异常时,greet_many()会打印一个默认的问候语。
greetings.py的相关部分在这里被重复:
因此,当greet()由于错误的整数输入而导致TypeError时,greet_many()会处理该异常并尝试打印一个简单的问候语。这里的代码最终会导致另一个类似的异常。它仍然试图添加一个字符串和一个整数。
查看所有的回溯输出可以帮助你了解异常的真正原因。有时,当你看到最后一个异常被引发,并由此产生回溯时,你仍然看不出哪里出错了。在这些情况下,向上移动到前面的异常通常会让你更好地了解根本原因。
在编程时,了解如何在程序引发异常时阅读Python回溯可能非常有用,但是了解一些更常见的回溯也可以提升编程的进程。
下面是一些你可能会遇到的常见异常,它们被引发的原因和它们的含义,以及你可以在它们的回溯中找到的信息。
AttributeError
当你试图访问一个对象上没有被定义的属性时,会引发AttributeError。Python文档中定义了此异常何时被引发:
当属性引用或赋值失败时引发。
下面是一个引发AttributeError的例子:
AttributeError的错误消息行告诉你,在本例中,具体的对象类型,在本例中是int,没有访问的an_attribute属性。在错误消息行中看到AttributeError可以帮助你快速确定你尝试访问的是哪个属性,以及要到哪里去修复它。
大多数情况下,获得这个异常表明你正在处理的对象可能不是你期望的类型:
在上面的例子中,你可能期望a_list是list类型的,它有一个名为.Append()的方法。当你接收到AttributeError异常并看到它是在你尝试调用.append()时引发的,这说明你正在处理的对象类型可能不是你所期望的。
通常,当你期望从一个函数或方法调用返回一个特定类型的对象时,会出现这种情况,你最终会得到一个类型为None的对象。在本例中,错误消息行将写到,AttributeError: 'None类型'对象没有属性'append'。
ImportError
当一个import语句出错时,ImportError会被引发。如果你试图导入的模块找不到,或者你试图从一个模块中导入模块中不存在的内容时,你将得到这个异常,或者它的子类ModuleNotFoundError。Python文档定义了此异常何时被引发:
当import语句在尝试加载模块时遇到困难时引发。当from…import中的“from list”中存在一个无法被找到的名称时也会引发。
下面是一个ImportError 和ModuleNotFoundError被引发的例子。
在上面的例子中,你可以看到,当我们试图导入不存在的模块asdf时会导致ModuleNotFoundError。当试图从一个存在的模块(这里是collections)中导入不存在的asdf时,就会导致ImportError。回溯底部的错误消息行告诉你,在这两种情况下都不能导入asdf。
IndexError
当你试图从一个序列(如列表或元组)中检索一个索引时,而该索引在这个序列中找不到时,就会引发一个IndexError。Python文档定义了此异常何时会被引发:
当一个序列的下标超出范围时引发。
下面是一个引发IndexError的例子:
IndexError的错误消息行不会给你提供很好的信息。你可以看到有一个超出范围的序列引用以及此序列的类型,在本例中是一个列表。这些信息,加上其他回溯信息,通常足以帮助你快速确定如何修复此问题。
KeyError
与IndexError类似,当你试图访问映射(通常是dict)中没有的键时,会引发KeyError。你可以把它看作是IndexError,只不过是针对字典的。Python文档定义了此异常何时被引发:
当在现有键集合中找不到一个映射(字典)键时引发。
下面是一个KeyError被引发的例子:
KeyError的错误消息行会给出找不到的键。这并没有太多的内容,但是,结合回溯的其他内容,但对于修复这个问题来说通常是足够了。
要深入了解KeyError,请查看《Python KeyError异常以及如何处理它们》。
NameError
当你引用了一个代码中未定义的变量、模块、类、函数或其他名称时,将引发一个NameError。Python文档定义了此异常何时被引发:
当本地或全局名称未被找到时引发。
在下面的代码中,greet()接受一个参数person。但在函数本身中,该参数被错误拼写为persn:
NameError 回溯的错误消息行给出了缺失的名称。在上面的例子中,它是一个传入函数的拼写错误的变量或参数。
如果它是你拼写错误的参数,那么NameError也会被引发:
在这里,你似乎没有做错什么。在回溯中被执行和引用的最后一行看起来不错。如果你发现自己处于这种情况,那么你要做的事情就是查看代码,确定person变量在哪里被使用和定义。在这里,你可以很快看到参数名称拼错了。
SyntaxError
当你的代码中有不正确的Python语法时,就会引发SyntaxError。Python文档定义了此异常何时被引发:
当解析器产生语法错误时引发。
下面代码的问题是函数定义行末尾缺少一个冒号。在Python%20REPL中,这个语法错误在按下回车键后会立即被引发:
SyntaxError的错误消息行只告诉你代码的语法有问题。查看上面的行可以得到问题所在的行,通常用a ^(插入符号)指向问题点。这里,函数的def语句中缺少冒号。
同样,使用SyntaxError回溯,常规的第一行Traceback (most recent call last:也丢失了。这是因为当Python试图解析你的代码时,SyntaxError会被引发,而实际上这些行并没有被执行。
TypeError
当你的代码试图对一个对象执行某些不能执行的操作时,例如试图将一个字符串相加到一个整数中,或者在一个没有定义其长度的对象上调用len(),TypeError就会被引发。Python文档中定义了此异常何时被引发:
当一个操作或函数被应用于一个不合适类型的对象时引发。
下面是TypeError被引发的几个示例:
以上所有引发TypeError的示例都会产生一个包含不同消息的错误消息行。每一条消息都能很好地告诉你哪里出了问题。
前两个示例尝试将字符串和整数相加。然而,它们有细微的不同:
错误消息行反映了这些不同。
最后一个例子尝试在一个int上调用len()。错误消息行告诉你不能对一个int类型执行此操作。
ValueError
当对象的值不正确时,ValueError将被引发。你可以将其视为一个IndexError,当索引值不在序列范围之内时会被引发,只不过ValueError用于更一般的情况。Python文档中定义了此异常何时被引发:
当一个操作或函数接收到一个具有正确类型但值不合适的参数时引发,并且这种情况不能被一个更精确的异常(比如IndexError)描述。
下面是ValueError被引发的两个例子:
在这些例子中,ValueError错误消息行会准确地告诉你这些值存在什么问题:
获得异常及其生成的Python回溯意味着你需要决定如何处理它。通常,修复代码是第一步,但有时问题出在未预期的或不正确的输入上。虽然在代码中提供这些情况很好,但有时通过记录回溯和执行其他操作来隐藏异常也很有意义。
下面是一个更真实的代码示例,它需要让一些Python回溯保持静默。本例使用了requests库。你可以在Python的requests库(指南)中获取更多信息:
这段代码运行得很好。当你运行此脚本时,你将一个URL作为命令行参数提供给它,它将调用该URL,然后打印出HTTP状态码和响应中的内容。甚至在响应是一个HTTP错误状态时,它也可以工作:
但是,有时你的脚本提供的用于检索的URL不存在,或者主机服务器关闭。在这些情况下,这个脚本现在就会引发一个未捕获的ConnectionError异常,并打印一个回溯:
这里的Python回溯可能非常长,还会引发许多其他异常,最终导致ConnectionError被requests库本身引发。如果你向上移动到最后的异常回溯,你就可以看到问题都是从我们的代码urlcall .py中的第5行开始的。
如果你将非法行封装在一个try和except块中,那么捕获适当的异常将允许你的脚本继续处理更多的输入:
上面的代码使用了一个带有try和except块的else子句。如果你不熟悉Python的这一特性,那么请在Python Exceptions:An Introduction中查看else子句。
现在,当你使用一个URL来运行此脚本时,将引发一个ConnectionError,系统会打印一个状态码-1,以及Connection Error的内容:
这运行的很好。然而,在大多数实际系统中,你并不希望只是静默化异常和生成的回溯,而是希望去记录回溯。记录回溯可以让你更好地理解程序中哪些地方出错了。
注意: 要了解更多关于Python日志系统的信息,请查看Python中的logging。
你可以通过导入logging包,获取一个日志记录器并在try和except块的except部分中调用该日志记录器的.exception()来在你的脚本中记录回溯。你的最终脚本应该会类似于以下代码:
现在,当你对一个有问题的URL运行此脚本时,它会打印预期的-1和Connection Error,同时也会记录回溯:
默认情况下,Python将向标准错误(stderr)发送日志消息。看起来我们根本没有抑制回溯输出。但是,如果你在重定向stderr时再次调用它,你可以看到日志系统正在工作,我们可以将日志保存起来,以备以后使用:
Python回溯包含了大量的信息,可以帮助你发现你的Python代码中出现的错误。这些回溯看起来有点吓人,但是一旦你把它分解开来,看看它想向你展示什么,它们就会非常有用。逐行浏览一些回溯将会使你更好地理解它们包含的信息,并帮助你最大限度地利用它们。
在运行代码时获得Python回溯输出是改进代码的一个机会。这是Python试图帮助你的一种方式。
既然你已经了解了如何阅读Python回溯,那么你可以从学习更多有关诊断回溯输出所告诉你的问题的一些工具和技术中获益。Python的内置traceback模块可用于处理和检查回溯。当你需要从回溯输出中获得更多信息时,traceback模块是很有用的。了解更多有关调试Python代码的技术也会很有帮助。
英文原文:https://realpython.com/python-traceback/ 译者:野生大熊猫