异常信息分析与收集
在实际调试程序的过程中,有时只获得异常的类型是远远不够的,还需要借助更详细的异常信息才能解决问题。
捕获异常时,常用的有2种方式可获得更多的异常信息,分别是:
- 使用 sys 模块中的 exc_info 方法;
- 使用 traceback 模块中的相关函数。
Python sys.exc_info()方法:获取异常信息
模块 sys 中,有两个方法可以返回异常的全部信息,分别是 exc_info() 和 last_traceback(),这两个函数有相同的功能和用法,这里仅以 exc_info() 方法为例。
exc_info() 方法会将当前的异常信息以元组的形式返回,该元组中包含 3 个元素,分别为 type、value 和 traceback,它们的含义分别是:
- type:异常类型的名称,它是 BaseException 的子类
- value:捕获到的异常实例。
- traceback:是一个 traceback 对象。
举个例子:
#使用 sys 模块之前,需使用 import 引入
import sys
try:
x = int(input("请输入一个被除数:"))
print("30除以",x,"等于",30/x)
except:
print(sys.exc_info())
print("其他异常...")
当输入 0 时,程序运行结果为:
请输入一个被除数:0
(<class 'ZeroDivisionError'>, ZeroDivisionError('division by zero',), <traceback object at 0x000001FCF638DD48>)
其他异常...
输出结果中,第 2 行是抛出异常的全部信息,这是一个元组,有 3 个元素,第一个元素是一个 ZeroDivisionError 类;第 2 个元素是异常类型 ZeroDivisionError 类的一个实例;第 3 个元素为一个 traceback 对象。其中,通过前 2 个元素可以看出抛出的异常类型以及描述信息,对于第 3 个元素,是一个 traceback 对象,无法直接看出有关异常的信息,还需要对其做进一步处理。
要查看 traceback 对象包含的内容,需要先引进 traceback 模块,然后调用 traceback 模块中的 print_tb 方法,并将 sys.exc_info() 输出的 traceback 对象作为参数参入。例如:
#使用 sys 模块之前,需使用 import 引入
import sys
#引入traceback模块
import traceback
try:
x = int(input("请输入一个被除数:"))
print("30除以",x,"等于",30/x)
except:
#print(sys.exc_info())
traceback.print_tb(sys.exc_info()[2])
print("其他异常...")
输入 0,程序运行结果为:
请输入一个被除数:0
File "C:\Users\mengma\Desktop\demo.py", line 7, in <module>
print("30除以",x,"等于",30/x)
其他异常...
可以看到,输出信息中包含了更多的异常信息,包括文件名、抛出异常的代码所在的行数、抛出异常的具体代码。
print_tb 方法也仅是 traceback 模块众多方法中的一个,更多情况下,我们会用traceback模块的其他方法来获取异常信息。
Python traceback模块:获取异常信息
简单讲,traceback模块可以用来查看异常的传播轨迹,追踪异常触发的源头,就像你不捕获异常时,pycharm里面的异常报错一样。
#举例说明,什么是异常传播轨迹
def main():
first_method()
def first():
second_method()
def second():
third_method()
def third():
raise ZeroDivisionError("自定义异常信息")
main()
上面程序中 main() 函数调用 first(),first() 调用 second(),second() 调用 third(),third() 引发一个 ZeroDivisionError 异常。运行上面程序,将会看到如下所示的异常信息:
Traceback (most recent call last):
File "D:\PyGuide\错误和异常\zy_exception.py", line 12, in <module>
main()
File "D:\PyGuide\错误和异常\zy_exception.py", line 4, in main
first()
File "D:\PyGuide\错误和异常\zy_exception.py", line 6, in first
second()
File "D:\PyGuide\错误和异常\zy_exception.py", line 8, in second
third()
File "D:\PyGuide\错误和异常\zy_exception.py", line 10, in third
raise ZeroDivisionError("自定义异常信息")
ZeroDivisionError: 自定义异常信息
该输出结果要从下往上看,12行是报出来的异常信息,10和11行是报出来的最早开始发生异常的位置(哪个文件,哪行代码),继续往上可以看出,异常从 third() 函数开始触发,传到 second() 函数,再传到 firstMethod() 函数,最后传到 main() 函数,在 main() 函数终止,这个过程就是整个异常的传播轨迹,实际上,除了third这个函数里有异常,其他调用者本身并没有异常,只不过是调用了有异常的third(),所以被追踪到。换句话说,结果显示的异常传播轨迹信息非常清晰,它记录了应用程序中执行停止的各个点。最后一行信息详细显示了异常的类型和异常的详细消息。从这一行向上,逐个记录了异常发生源头、异常依次传播所经过的轨迹,并标明异常发生在哪个文件、哪一行、哪个函数处。
在实际的开发中,大多数复杂操作都会被分解成一系列函数或方法调用。这是因为,为了具有更好的可重用性,会将每个可重用的代码单元定义成函数或方法,将复杂任务逐渐分解为更易管理的小型子任务。由于一个大的业务功能需要由多个函数或方法来共同实现,在最终编程模型中,很多对象将通过一系列函数或方法调用来实现通信,执行任务。所以,当应用程序运行时,经常会发生一系列函数或方法调用,从而形成“函数调用栈”。
异常的传播则路径相反,只要异常没有被完全捕获(异常没有被捕获(包括主动引发异常后不进行捕获处理),或者异常被处理后重新引发了新异常),异常就从发生异常的函数或方法逐渐向外传播,首先传给该函数或方法的调用者,该函数或方法的调用者再传给其调用者,直至最后传到Python解释器,此时 Python 解释器会中止该程序,并打印异常的传播轨迹信息。
很多同学一看到输出结果所示的异常提示信息,就会惊慌失措,他们以为程序出现了很多严重的错误,其实只有一个错误,系统提示那么多行信息,只不过是显示异常依次触发的轨迹,你只需要拉到最后一行,看看是什么问题,去解决掉,很可能就把异常全部消灭。
问题来了,我们在pycharm运行时,只要报错(或者说报异常),程序就会告诉我们这个异常传播轨迹,为什么还要引入traceback模块呢?实际上,如果你没有捕获异常,程序确实会默认告诉你异常传播轨迹,可一旦你捕获了异常,异常信息里就没有这部分内容了,不信你看:
def main():
first()
def first():
second()
def second():
third()
def third():
try:
raise ZeroDivisionError("自定义异常信息")
except Exception as e:
print(repr(e))
main()
返回结果是:
ZeroDivisionError('自定义异常信息')
所以问题来了,如果我们既想要捕获异常,又想要查看异常传播轨迹,找到发生异常的位置,就可以用traceback模块。
常用的格式如下:
import traceback
try:
block
except:
traceback.print_exc()
举例:
import traceback
try:
1/0
except Exception as e:
# print(repr(e)) #只返回一行异常信息: ZeroDivisionError('division by zero'),我们并不知道是在哪个文件哪个函数哪一行出的错。
traceback.print_exc() #返回更加详细的异常信息,并且打印
#traceback.format_exc() #和traceback.print_exc返回的信息一样,只是它不打印,只返回字符串
print("处理完异常后继续运行")
#执行结果:
处理完异常后继续运行
Traceback (most recent call last):
File "D:\PyGuide\错误和异常\zy_exception.py", line 6, in <module>
1/0
ZeroDivisionError: division by zero
traceback提供了如下两个常用方法:
- traceback.print_exc():将异常传播轨迹信息输出到控制台或指定文件中(通过file参数)。
- format_exc():将异常传播轨迹信息转换成字符串。
可能有读者好奇,从上面方法看不出它们到底处理哪个异常的传播轨迹信息。实际上我们常用的 print_exc() 是 print_exc([limit[, file]]) 省略了 limit、file 两个参数的形式。而 print_exc([limit[, file]]) 的完整形式是 print_exception(etype, value, tb[,limit[, file]])
,在完整形式中,前面三个参数用于分别指定异常的如下信息:
- etype:指定异常类型;
- value:指定异常值;
- tb:指定异常的traceback 信息;
当程序处于 except 块中时,该 except 块所捕获的异常信息可通过 sys 对象来获取,其中 sys.exc_type、sys.exc_value、sys.exc_traceback 就代表当前 except 块内的异常类型、异常值和异常传播轨迹。
简单来说, print_exc([limit[, file]]) 相当于如下形式:
print_exception(sys.exc_etype, sys.exc_value, sys.exc_tb[, limit[, file]])
也就是说,使用 print_exc([limit[, file]]) 会自动处理当前 except 块所捕获的异常。该方法还涉及两个参数:
- limit:用于限制显示异常传播的层数,比如函数 A 调用函数 B,函数 B 发生了异常,如果指定 limit=1,则只显示函数 A 里面发生的异常。如果不设置 limit 参数,则默认全部显示。
- file:指定将异常传播轨迹信息输出到指定文件中。如果不指定该参数,则默认输出到控制台。
借助于 traceback 模块的帮助,我们可以使用 except 块捕获异常,并在其中打印异常传播信息,包括把它输出到文件中。例如如下程序:
import traceback
def main():
first()
def first():
second()
def second():
third()
def third():
raise SelfException("自定义异常信息")
try:
main()
except:
# 捕捉异常,并将异常传播信息输出控制台
traceback.print_exc()
# 捕捉异常,并将异常传播信息输出指定文件中
traceback.print_exc(file=open('log.txt', 'a'))
上面程序第一行先导入了 traceback 模块,接下来程序使用 except 捕获程序的异常,并使用 traceback 的 print_exc() 方法输出异常传播信息,分别将它输出到控制台和指定文件中。运行上面程序,同样可以看到在控制台输出异常传播信息,而且在程序目录下生成了一个 log.txt 文件,该文件中同样记录了异常传播信息。
通过以上的介绍,再来看下面这段话,应该会更容易理解了:
发生异常时,Python 能记住引发的异常以及程序的当前状态。Python 还维护着 traceback(跟踪)对象,其中含有异常发生时与函数调用堆栈有关的信息。记住,异常可能在一系列嵌套较深的函数调用中引发。程序调用每个函数时,Python 会在“函数调用堆栈”的起始处插入函数名。一旦异常被引发,Python 会搜索一个相应的异常处理程序。如果当前函数中没有异常处理程序,当前函数会终止执行,Python 会搜索当前函数的调用函数,并以此类推,直到发现匹配的异常处理程序,或者 Python 抵达主程序为止。这一查找合适的异常处理程序的过程就称为“堆栈辗转开解”(StackUnwinding)。python解释器一方面维护着与放置堆栈中的函数有关的信息,另一方面也维护着与已从堆栈中“辗转开解”的函数有关的信息。
friendly-traceback,帮你快速定位代码语法错误
初学者在编写代码时,时常会犯一些低级的语法错误,由于知识掌握的不够扎实,往往一时间无法找到是哪里出了问题。python提供了异常和错误信息,也就是traceback,对于有经验的人,可以根据这些提示信息很快便定位到问题只所在,但对于初学者来说,这些错误信息用处不大,一方面是因为初学者还不够熟练,更重要的是,这些提示信息所提供的有价值的信息太少了,比如下面的错误
if len('hello') = 5:
print('ok')
错误提示内容是
File "C:/Users/zhangdongsheng/PycharmProjects/liepin/test.py", line 1
if len('hello') = 5:
^
SyntaxError: invalid syntax
这段错误信息指出了错误的具体位置,可初学者正是因为基础不扎实才会犯错误,上面的信息对于他们来说,提示性仍然不够。
那么,有没有什么办法,可以获得更加具有提示性的错误信息呢?强烈建议安装使用friendly-traceback模块,一切语法错误都将不再是问题
pip install friendly-traceback
如果你的某个脚本里有语法错误,依靠python原生提供的错误信息不足以定位问题,那么就可以使用该模块,上面的代码写在脚本test.py中,那么你可以这样来执行
python -m friendly_traceback test.py
得到的错误提示为
┌─────────────────────────────────────────────────────────────────── Python exception ───────────────────────────────────────────────────────────────────┐
│ Traceback (most recent call last): │
│ File "test.py", line 1 │
│ if len('hello') = 5: │
│ ^ │
│ SyntaxError: invalid syntax │
│ │
│ Perhaps you needed == instead of =. │
│ │
│ A SyntaxError occurs when Python cannot understand your code. │
│ │
│ Python could not understand the code in the file 'test.py' beyond the location indicated by ^. │
│ │
│ -->1: if len('hello') = 5: │
│ ^ │
│ │
│ You used an assignment operator = instead of an equality operator ==. │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
注意看最后一行
You used an assignment operator = instead of an equality operator ==.
几乎就等于告诉你应该怎样去修改,这就厉害了呀,不仅仅是之处问题在哪,而且还会给出非常准确的修改方案,即便是初学小白,也能快速修复问题了。
不只是语法错误,这个库对异常和运行时的错误都能给出非常具体的有参考意义的提示,如果python发行版能将这个模块内置其中就好了。