Python下的异常处理及错误日志记录

 Python
 

Python使用被称为异常的特殊对象来表达执行期间发现的错误。当这些异常没有被捕获并处理时,程序将停止,并向控制台打印错误信息。这个错误信息通常是一个traceback,包含了异常的类型,以及诱发这个异常的代码位置及调用栈细节。

版权声明

本文可以在互联网上自由转载,但必须:注明出处(作者:海洋饼干叔叔)并包含指向本页面的链接。

本文不可以以纸质出版为目的进行改编、摘抄。

1. 曾经的异常

在本书的前半部分,我们已经遇到过很多异常:

异常类 说明
ValueError 值与期望的不符
IndentationError 代码缩进错误
IndexError 序列索引不存在
AssertionError 断言失败
NameError 名字不存在
KeyError 映射(比如字典)中的键不存在
AttributeError 属性错误(对象无指定名字的属性)
TypeError 类型出错
SyntaxError 代码语法错误
OSError 操作系统未能执行指定任务
ZeroDivisionError 除0错误

这些异常类,都继承自Exception类型。在本书的前半部分,我们对于这些异常采取了放任的态度: 程序直接报错停止。但一个严谨的程序,应该捕获并处理这些异常。

异常发生后
- 捕获并处理异常,尝试将程序从异常中拯救出来,继续正常运行。
- 捕获并处理异常,至少做一些必要的紧急操作,避免严重后果的发生。比如,汽车的车载控制系统发现发动机的“异常高温”(可能意味着起火)异常,应尝试切断油路,迫使汽车减速停车;一个股票交易系统发现无法恢复的异常,应尝试关闭数据库连接,并将没有保存的文件全部存盘。
- 捕获并处理异常,最低限度,作者认为应该将异常信息保存在错误日志中,以便程序员查找错误发生的原因。

2. try except else finally

下述代码展示了一个完整的异常处理程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#try.py
def divide(a, b):
return a/b


while True:
sFirst = input("First number:")
sSecond = input("Second number:")
if sFirst == "q" or sSecond == "q":
break
try:
iFirst = int(sFirst)
iSecond = int(sSecond)
fResult = divide(iFirst,iSecond)
except (ZeroDivisionError) as e:
print("You can not divide by 0:",e)
except (ValueError,TypeError) as e:
print("Illegal value been inputted:",e)
print(type(e))
except (Exception) as e:
print("An exception found, I do not know how to process it.")
raise
else:
print( sFirst, "/", sSecond, "=", fResult)
finally:
print("'Finally' will be executed what ever happens.")

上述程序试图让用户输入两个整数,然后相除并将相除的结果打印出来。当除数为0或者操作者输入的字符串不是一个整数时,均会发生异常。上述try … except … else … finally语句将会捕获并恰当地处理这些异常。这个try…finally语句的工作过程可以概述如下:

首先,解释器将会执行try子句内的代码。在上例中,作者故意把try子句写得比较复杂,还加入了一个本不必要的函数,目的是想告诉读者:try子句内的代码以及间接被try子句内代码调用执行的代码,都受try子句的管辖。

如果try子句的执行没有发生异常,在try子句执行完毕后将执行else子句,然后再执行finally语句。下述输入及其执行结果证明了这一点。作者输入了15和3,字符串到整数的转换没有任何问题,else子句打印了除法运算的结果,finally子句接下来“强行”补充:”不管发生什么,我都来刷存在感”。

1
2
3
4
First number:15
Second number:3
15 / 3 = 5.0
'Finally' will be executed what ever happens.

如果try子句的执行发生了异常,则Python解释器会放弃try子句内后续代码的执行,并根据异常的类型创建一个异常对象。然后,解释器将从前往后逐一检查except子句括号里所包括的异常类型,当实际发生的异常属于该except子句括号内的异常类型时,该except子句将会被执行。最后,finally子句也会被执行。

下例中,我们输入了15和0x38。由于0x38不是一个十进制整数,所以iSecond = int(sSecond)这行代码诱发了一个 ValueError异常。解释器放弃了后续try子句的执行,并执行了第二个except子句以及finally子句。

1
2
3
4
5
First number:15
Second number:0x38
Illegal value been inputted: invalid literal for int() with base 10: '0x38'
<class 'ValueError'>
'Finally' will be executed what ever happens.

下例中,我们输入了15和0。此时,divide函数内的a/b产生了ZeroDivisionError异常。解释器放弃了try子句后续代码的执行,并执行了第一个except子句以及finally子句。

当异常发生后,如果异常类型既不属于第一条except语句指定的类型,也不属于第二条except语句指定的类型,此时,第三条except语句多半可以捕获并处理异常。因为,绝大多数异常都是Exception类的子类型。注意,作者说的是绝大多数,也有一些异常,比如SystemExit,KeyboardInterrupt,不是Exception的子类。如果希望捕获并处理这些异常,可以直接使用下述型式的except子句:

1
2
except:
print("I found an exception that is not sub-class of Exception.")

如果捕获的异常在当前情境下处理不了,也可以接着向外抛:上述代码中第三条except子句中的raise即为该用途。如果直接raise,抛出的是原有异常。当然,也可以欺骗或者加工一下异常:raise ValueError(“值错了!”) 。

连续输入两个”q”, 上述程序将会正常结束。

所谓的向外抛出异常,有必要解释一下。程序中的try…except…else…finally语句很可能处于另外一个try…finally语句的try子句中。所谓,抛出,就是本try…finally语句不处理该异常,扔给外面那个try…finally来处理。参见下述伪代码,我们看到,dummy函数内的try…except间接处于外部的try…except的try子句管辖内。

1
2
3
4
5
6
7
8
9
10
11
12
def dummy():
try:
doing something here
except:
raise

try:
doing something before
dummy()
doing something more
except:
print("Exception catched...")
try…finally语句总结
- 当try子句的执行没有发生异常时,else子句将被执行。
- 当try子句的执行发生异常时,解释器会放弃执行try子句后续代码,并根据异常的类型选择执行except子句,顺序为从上到下。
- except子句捕获异常后,可以尝试将程序从异常中恢复,或者做一些最低限度的后处理,以避免“灾难”性结果。如果处理不了,也可以通过raise语句将异常外抛。
- 不管有没有异常发生,finally子句总会在最后阶段被执行。这使得finally子句特别适合于处理一些善后工作,比如关闭因为异常未及关闭的文件,断开网络连接,关闭数据库连接等。
- 语法上,else子句以及finally子句是可选的,而except子句可以有无限多条。
- Python允许程序员定义自己的异常类型,比如车载电脑的控制程序可能需要一个“发动机转速过高”的异常。这很简单:定义一个Exception的子类就好了。

3. 警告

如果有些情况的发生还不是那么严重,可以尝试发出警告。

1
2
3
4
5
6
7
8
9
10
11
#warn.py
from warnings import warn

def divide(a,b):
fResult = a / b
if fResult < 0.0000001:
warn("The result is very close to Zero!")
return fResult

print(divide(0.1, 10000000000))
print("Something else.")

执行结果:

1
2
3
4
d:\pylearn\C12_UnitTestException\warn.py:6: UserWarning: The result is very close to Zero!
warn("The result is very close to Zero!")
1.0000000000000001e-11
Something else.

从执行结果可以看出,warn()函数发出了警告,该警告会被打印至屏幕,但程序的执行不会因为该警告而停止:print(“Something else.”)在警告之后继续执行了。

如果你使用别人的模块,还可以通过filterwarnings()函数来过滤该模块产生的警告信息。你可以选择忽略-“ignore”,也可以上纲上线地把该模块的警告转化成异常-“error”。具体请查询Python文档。

4. 出错日志

异常发生后,如果异常是预料中的类型,程序员直接处理这些异常并挽救程序即可。而程序发生非意料的异常几乎是历史的必然:程序员是人不是神! 没有bug的应用程序不存在。这些意料之外的异常发生后,把相关情况记入出错日志(通常是一个文本文件),对于解决问题,十分重要。下述代码中的UserExceptHook函数来自于作者编写的一个实际的应用软件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#excepthook.py
import sys,traceback
from datetime import datetime

fError = open("except_error.log", 'a')
def UserExceptHook(tp, val, tb):
traceList = traceback.format_tb(tb)
html = repr(tp) + "\n"
html += (repr(val) + "\n")
for line in traceList:
html += (line + "\n")
print(html, file=sys.stderr)
print(datetime.now(), file=fError)
print(html, file=fError)
fError.close()

def main():
sFirst = input("First number:")
sSecond = input("Second number:")
try:
fResult = int(sFirst) / int(sSecond)
except Exception:
print("发现异常,但我不处理,抛出去.")
raise
else:
print( sFirst, "/", sSecond, "=", fResult)

sys.excepthook = UserExceptHook
main()
fError.close()

sys模块下的excepthook是一个勾子-hook函数。当有程序没有捕获的异常,或者捕获后又抛出来的异常时,解释器就会执行这个勾子函数,然后停止运行。

上述代码自行定义了一个勾子函数,并将其赋值给sys.excepthook。这个函数有三个参数:tp-异常类型、val-异常值、tb-异常跟踪栈。异常跟踪栈可以通过traceback模块的format_tb()函数转换成一个字符串列表,这些字符串表明了异常发生时的程序调用关系。

可以看到,UserExceptHook()函数把异常转换成一个多行字符串,其中,repr()函数将一个对象转换成一个可以打印的表示字符串。然后这个多行字符串被打印至sys.stderr标准错误输出,然后再被打印至fError文件。请注意,fError文件在程序开始执行时即被以”a”-附加模式打开,附加模式保证了后续发生的错误信息不会覆盖原有文件内容。此外,在UserExceptHook()函数的最后一行,fError文件被关闭。因为,该勾子函数执行完成后,程序将会终止,这里是唯一关闭文件确保文件内容被正确写入外存的最后机会。

datetime.now()返回当前系统日期和时间。

作者多次运行上述程序,故意输入一些不恰当的值来诱发异常,最后得到下述错误日志-except_error.log:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2018-11-03 18:05:58.305337
<class 'ZeroDivisionError'>
ZeroDivisionError('division by zero')
File "D:/pylearn/C12_UnitTestException/excepthook.py", line 28, in <module>
main()

File "D:/pylearn/C12_UnitTestException/excepthook.py", line 20, in main
fResult = int(sFirst) / int(sSecond)


2018-11-03 18:06:04.097457
<class 'ValueError'>
ValueError("invalid literal for int() with base 10: '3.2'")
File "D:/pylearn/C12_UnitTestException/excepthook.py", line 28, in <module>
main()

File "D:/pylearn/C12_UnitTestException/excepthook.py", line 20, in main
fResult = int(sFirst) / int(sSecond)

终于,当使用你编写的软件的客户通过电话向你抱怨程序有时会运行出错时,你可以要求客户把错误日志文件发送给你。通过错误日志,你可以了解软件出错的相关细节,而不必通过客户那些“不准确”甚至“夸大其词”的描述去推断错误发生的原因。


 评论