编程错误
编写程序时遇到的错误可大致分为 2 类,分别为语法错误和运行时错误。
语法错误
语法错误,也就是解析代码时出现的错误。当代码不符合Python
语法规则时,Python
解释器在解析时就会报出SyntaxError
语法错误,与此同时还会明确指出最早探测到错误的语句。例如:
print "Hello,World!"
我们知道,Python 3
已不再支持上面这种写法,所以在运行时,解释器会报如下错误:
SyntaxError: Missing parentheses in call to 'print'
运行时错误
运行时错误,即程序在语法上都是正确的,但在运行时发生了错误。例如:
a = 1/0
上面这句代码的意思是:用 1 除以 0,并赋值给 a 。因为 0 作除数是没有意义的,所以运行后会产生如下错误:
>>> a = 1/0
Traceback (most recent call last):
File "<pyshell#2>", line 1, in <module>
a = 1/0
ZeroDivisionError: division by zero
在Python
中,把这种运行时产生错误的情况叫做异常(Exceptions
)。
异常处理机制
异常处理机制己经成为衡量一门编程语言是否成熟的标准之一,使用异常处理机制的 Python
程序具有更好的容错性,更加健壮。
try except
异常处理
Python
提供了try except
语句捕获并处理异常,该异常处理语句的基本语法结构如下:
try:
可能产生异常的代码块
except [(Error1, Error2, ...) [as e]]:
处理异常的代码块1
except [(Error3, Error4, ...) [as e]]:
处理异常的代码块2
该格式中,[] 括起来的部分可以使用,也可以省略;(Error1,Error2,...
) 、(Error3,Error4,...
) 表示各自的except
块可以处理异常的具体类型;[as e]
表示将异常类型赋值给变量e
(方便在 except 块中调用异常类型)。
try except
语句的执行流程如下:
- 首先执行
try
中的代码块,如果执行过程中出现异常,系统会自动生成一个异常对象,该异常对象会提交给Python
解释器,此过程被称为引发异常。 - 当
Python
解释器收到异常对象时,会寻找能处理该异常对象的except
块,如果找到合适的except
块,则把该异常对象交给该except
块处理,这个过程被称为捕获异常。如果Python
解释器找不到捕获异常的except
块,则程序运行终止,Python
解释器也将退出。
try:
a = int(input("输入被除数:"))
b = int(input("输入除数:"))
c = a / b
print("您输入的两个数相除的结果是:", c )
except (ValueError, ArithmeticError):
print("程序发生了数字格式异常、算术异常之一")
except :
print("未知异常")
print("程序继续运行")
"""运行结果
输入被除数:a
程序发生了数字格式异常、算术异常之一
程序继续运行
"""
访问异常信息
如果程序需要在except
块中访问异常对象的相关信息,可以通过为except
块添加as a
来实现。当Python
解释器决定调用某个except
块来处理该异常对象时,会将异常对象赋值给except
块后的异常变量,程序即可通过该变量来获得异常对象的相关信息。
所有的异常对象都包含了如下几个常用属性和方法:
args
:该属性返回异常的错误编号和描述字符串。errno
:该属性返回异常的错误编号。strerror
:该属性返回异常的描述宇符串。with_traceback()
:通过该方法可处理异常的传播轨迹信息。
def foo():
try:
fis = open("a.txt");
except Exception as e:
# 访问异常的错误编号和详细信息
print(e.args)
# 访问异常的错误编号
print(e.errno)
# 访问异常的详细信息
print(e.strerror)
foo()
"""运行结果
(2, 'No such file or directory')
2
No such file or directory
"""
try except else
详解
try except else
语句是在原来try except
语句的基础上再添加一个else
子句,其作用是指定当try
块中没有发现异常时要执行的代码。换句话说,当try
块中发现异常,则else
块中的语句将不会被执行。
s = input('请输入除数:')
try:
result = 20 / int(s)
print('20除以%s的结果是: %g' % (s , result))
except ValueError:
print('值错误,您必须输入数值')
except ArithmeticError:
print('算术错误,您不能输入0')
else:
print('没有出现异常')
print("程序继续运行")
"""
请输入除数:3
20 除以3 的结果是:6.66667
没有出现异常
程序继续运行
"""
try except finally
资源回收
finally
语句是与try
和except
语句配合使用的,其通常是用来做清理工作的。无论try
中的语句是否跳入except
中,最终都要进入finally
语句,并执行其中的代码块。
在异常处理语法结构中,只有 try 块是必需的,也就是说:
- 如果没有
try
块,则不能有后面的except
块和finally
块; except
块和finally
块都是可选的,但except
块和finally
块至少出现其中之一,也可以同时出现;- 可以有多个
except
块,但捕获父类异常的except
块应该位于捕获子类异常的except
块的后面; - 不能只有
try
块,既没有except
块,也没有finally
块; - 多个
except
块必须位于try
块之后,finally
块必须位于所有的except
块之后。
finally语句块和else语句块的区别是,else语句块只有在没有异常发生的情况下才会执行,而finally语句则不管异常是否发生都会执行。不仅如此,无论是正常退出、异常退出,还是通过break、continue、return语句退出,finally语句块都会执行。
import os
def test():
fis = None
try:
fis = open("a.txt")
except OSError as e:
print(e.strerror)
# return语句强制方法返回
return # ①
#os._exit(1) # ②
finally:
# 关闭磁盘文件,回收资源
if fis is not None:
try:
# 关闭资源
fis.close()
except OSError as ioe:
print(ioe.strerror)
print("执行finally块里的资源回收!")
test()
"""运行结果
No such file or directory
执行finally里的资源回收!
"""
总结
到本节为止,读者已经学习了整个Python
的异常处理机制的结构,接下来带领大家回顾一下,在此过程还会讲解一些新的知识。
首先,Python
完整的异常处理语法结构如下:
try:
#业务实现代码
except Exception1 as e:
#异常处理块1
...
except Exception2 as e:
#异常处理块2
...
#可以有多个 except
...
else:
#正常处理块
finally :
#资源回收块
...
整个异常处理结构的执行过程,如图 1 所示。
图 1 异常处理语句块的执行流程
注意,在整个异常处理结构中,只有try
块是必需的,也就是说:
- 如果没有
try
块,则不能有后面的except
块、else
块和finally
块。但是也不能只使用try
块,要么使用try except
结构,要么使用try finally
结构; except
块、else
块、finally
块都是可选的,当然也可以同时出现;- 可以有多个
except
块,但捕获父类异常的except
块应该位于捕获子类异常的except
块的后面; - 多个
except
块必须位于try
块之后,finally
块必须位于所有的except
块之后。 - 要使用
else
块,其前面必须包含try
和except
。
其中,很多初学者分不清 finally
和else
的区别,这里着重说一下。else
语句块只有在没有异常发生的情况下才会执行,而finally
语句则不管异常是否发生都会执行。不仅如此,无论是正常退出、遇到异常退出,还是通过break
、continue
、return
语句退出,finally
语句块都会执行。
注意,如果程序中运行了强制退出Python
解释器的语句(如os._exit(1)
),则finally
语句将无法得到执行。例如:
import os
try:
os._exit(1)
finally:
print("执行finally语句")
运行程序,没有任何输出。因此,除非在try
块、except
块中调用了退出Python
解释器的方法,否则不管在try
块、except
块中执行怎样的代码,出现怎样的情况,异常处理的finally
块总会被执行。
另外在通常情况下,不要在finally
块中使用如return
或raise
等导致方法中止的语句(raise
语句将在后面介绍),一旦在finally
块中使用了return
或raise
语句,将会导致try
块、except
块中的return
、raise
语句失效。看如下程序:
def test():
try:
# 因为finally块中包含了return语句
# 所以下面的return语句失去作用
return True
finally:
return False
print(test())
上面程序在finally
块中定义了一条return False
语句,这将导致try
块中的return true
失去作用。运行上面程序,输出结果为:
False
同样,如果Python
程序在执行try
块、except
块包含有return
或raise
语句,则Python
解释器执行到该语句时,会先去查找finally
块,如果没有finally
块,程序才会立即执行return
或raise
语句;反之,如果找到finally
块,系统立即开始执行finally
块,只有当finally
块执行完成后,系统才会再次跳回来执行try
块、except
块里的return
或raise
语句。
但是,如果在 finally 块里也使用了return
或raise
等导致方法中止的语句,finally
块己经中止了方法,系统将不会跳回去执行try
块、except
块里的任何代码。
尽量避免在finally块里使用return或raise等导致方法中止的语句,否则可能出现一些很奇怪的情况。
raise
用法
在前面章节的学习中,遗留过一个问题,即是否可以在程序的指定位置手动抛出一个异常?答案是肯定的,Python
允许我们在程序中手动设置异常,使用raise
语句即可。
读者可能会感到疑惑,即我们从来都是想方设法地让程序正常运行,为什么还要手动设置异常呢?首先要分清楚程序发生异常和程序执行错误,它们完全是两码事,程序由于错误导致的运行异常,是需要程序员想办法解决的;但还有一些异常,是程序正常运行的结果,比如用raise
手动引发的异常。
raise
语句的基本语法格式为:
raise [exceptionName [(reason)]]
其中,用[]
括起来的为可选参数,其作用是指定抛出的异常名称,以及异常信息的相关描述。如果可选参数全部省略,则raise
会把当前错误原样抛出;如果仅省略 (reason
),则在抛出异常时,将不附带任何的异常描述信息。
也就是说,raise
语句有如下三种常用的用法:
raise
:单独一个raise
。该语句引发当前上下文中捕获的异常(比如在except
块中),或默认引发RuntimeError
异常。raise
异常类名称:raise
后带一个异常类名称,表示引发执行类型的异常。raise
异常类名称(描述信息):在引发指定类型的异常的同时,附带异常的描述信息。
显然,每次执行raise
语句,都只能引发一次执行的异常。首先,我们来测试一下以上 3 种raise
的用法:
>>> raise
Traceback (most recent call last):
File "<pyshell#1>", line 1, in <module>
raise
RuntimeError: No active exception to reraise
>>> raise ZeroDivisionError
Traceback (most recent call last):
File "<pyshell#0>", line 1, in <module>
raise ZeroDivisionError
ZeroDivisionError
>>> raise ZeroDivisionError("除数不能为零")
Traceback (most recent call last):
File "<pyshell#2>", line 1, in <module>
raise ZeroDivisionError("除数不能为零")
ZeroDivisionError: 除数不能为零
当然,我们手动让程序引发异常,很多时候并不是为了让其崩溃。事实上,raise
语句引发的异常通常用try except
(else finally
)异常处理结构来捕获并进行处理。例如:
try:
a = input("输入一个数:")
#判断用户输入的是否为数字
if(not a.isdigit()):
raise ValueError("a 必须是数字")
except ValueError as e:
print("引发异常:",repr(e))
程序运行结果为:
输入一个数:a
引发异常: ValueError('a 必须是数字',)
可以看到,当用户输入的不是数字时,程序会进入if
判断语句,并执行raise
引发ValueError
异常。但由于其位于try
块中,因为raise
抛出的异常会被try
捕获,并由except
块进行处理。
因此,虽然程序中使用了raise
语句引发异常,但程序的执行是正常的,手动抛出的异常并不会导致程序崩溃。
raise
不需要参数
正如前面所看到的,在使用raise
语句时可以不带参数,例如:
try:
a = input("输入一个数:")
if(not a.isdigit()):
raise ValueError("a 必须是数字")
except ValueError as e:
print("引发异常:",repr(e))
raise
程序执行结果为:
输入一个数:a
引发异常: ValueError('a 必须是数字',)
Traceback (most recent call last):
File "D:\python3.6\1.py", line 4, in <module>
raise ValueError("a 必须是数字")
ValueError: a 必须是数字
这里重点关注位于except
块中的raise
,由于在其之前我们已经手动引发了ValueError
异常,因此这里当再使用raise
语句时,它会再次引发一次。
当在没有引发过异常的程序使用无参的raise
语句时,它默认引发的是RuntimeError
异常。例如:
try:
a = input("输入一个数:")
if(not a.isdigit()):
raise
except RuntimeError as e:
print("引发异常:",repr(e))
程序执行结果为:
输入一个数:a
引发异常: RuntimeError('No active exception to reraise',)
获取异常信息
sys.exc_info()
方法:获取异常信息
在实际调试程序的过程中,有时只获得异常的类型是远远不够的,还需要借助更详细的异常信息才能解决问题。
捕获异常时,有 2 种方式可获得更多的异常信息,分别是:
- 使用
sys
模块中的exc_info
方法; - 使用
traceback
模块中的相关函数。
本节首先介绍如何使用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)
其他异常...
可以看到,输出信息中包含了更多的异常信息,包括文件名、抛出异常的代码所在的行数、抛出异常的具体代码。
traceback
模块:获取异常信息
除了使用sys.exc_info()
方法获取更多的异常信息之外,还可以使用traceback
模块,该模块可以用来查看异常的传播轨迹,追踪异常触发的源头。
下面示例显示了如何显示异常传播轨迹:
class SelfException(Exception):
pass
def main():
firstMethod()
def firstMethod():
secondMethod()
def secondMethod():
thirdMethod()
def thirdMethod():
raise SelfException("自定义异常信息")
main()
上面程序中main()
函数调用firstMethod()
,firstMethod()
调用secondMethod()
,secondMethod()
调用thirdMethod()
,thirdMethod()
直接引发一个SelfException
异常。运行上面程序,将会看到如下所示的结果:
Traceback (most recent call last):
File "C:\Users\mengma\Desktop\1.py", line 11, in <module>
main()
File "C:\Users\mengma\Desktop\1.py", line 4, in main <--mian函数
firstMethod()
File "C:\Users\mengma\Desktop\1.py", line 6, in firstMethod <--第三个
secondMethod()
File "C:\Users\mengma\Desktop\1.py", line 8, in secondMethod <--第二个
thirdMethod()
File "C:\Users\mengma\Desktop\1.py", line 10, in thirdMethod <--异常源头
raise SelfException("自定义异常信息")
SelfException: 自定义异常信息
从输出结果可以看出,异常从thirdMethod()
函数开始触发,传到secondMethod()
函数,再传到firstMethod()
函数,最后传到main()
函数,在main()
函数止,这个过程就是整个异常的传播轨迹。
在实际应用程序的开发中,大多数复杂操作都会被分解成一系列函数或方法调用。这是因为,为了具有更好的可重用性,会将每个可重用的代码单元定义成函数或方法,将复杂任务逐渐分解为更易管理的小型子任务。由于一个大的业务功能需要由多个函数或方法来共同实现,在最终编程模型中,很多对象将通过一系列函数或方法调用来实现通信,执行任务。
所以,当应用程序运行时,经常会发生一系列函数或方法调用,从而形成“函数调用战”。异常的传播则相反,只要异常没有被完全捕获(包括异常没有被捕获,或者异常被处理后重新引发了新异常),异常就从发生异常的函数或方法逐渐向外传播,首先传给该函数或方法的调用者,该函数或方法的调用者再传给其调用者,直至最后传到 Python
解释器,此时Python
解释器会中止该程序,并打印异常的传播轨迹信息。
很多初学者一看到输出结果所示的异常提示信息,就会惊慌失措,他们以为程序出现了很多严重的错误,其实只有一个错误,系统提示那么多行信息,只不过是显示异常依次触发的轨迹。
其实,上面程序的运算结果显示的异常传播轨迹信息非常清晰,它记录了应用程序中执行停止的各个点。最后一行信息详细显示了异常的类型和异常的详细消息。从这一行向上,逐个记录了异常发生源头、异常依次传播所经过的轨迹,并标明异常发生在哪个文件、哪一行、哪个函数处。
使用traceback
模块查看异常传播轨迹,首先需要将traceback
模块引入,该模块提供了如下两个常用方法:
traceback.print_exc()
:将异常传播轨迹信息输出到控制台或指定文件中。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
块捕获异常,并在其中打印异常传播信息,包括把它输出到文件中。例如如下程序:
# 导入trackback模块
import traceback
class SelfException(Exception): pass
def main():
firstMethod()
def firstMethod():
secondMethod()
def secondMethod():
thirdMethod()
def thirdMethod():
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
允许用户自定义异常类型。实际开发中,有时候系统提供的异常类型不能满足开发的需求。这时就可以创建一个新的异常类来拥有自己的异常。
其实,在前面章节中,已经涉及到了异常类的创建,例如:
class SelfExceptionError(Exception):
pass
try:
raise SelfExceptionError()
except SelfExceptionError as err:
print("捕捉到自定义异常")
运行结果为:
捕捉到自定义异常
可以看到,此程序中就自定义了一个名为SelfExceptionError
的异常类,只不过该类是一个空类。
由于大多数Python内置异常的名字都以"Error"结尾,所以实际命名时尽量跟标准的异常命名一样。
需要注意的是,自定义一个异常类,通常应继承自Exception
类(直接继承),当然也可以继承自那些本身就是从Exception
继承而来的类(间接继承Exception
)。
图 1
Python
异常类继承图
注意,虽然所有类同时继承自BaseException,但它是为系统退出异常而保留的,假如直接继承 BaseException,可能会导致自定义异常不会被捕获,而是直接发送信号退出程序运行,脱离了我们自定义异常类的初衷。
另外,系统自带的异常只要触发会自动抛出(比如NameError
、ValueError
等),但用户自定义的异常需要用户自己决定什么时候抛出。也就是说,自定义的异常需要使用raise
手动抛出。
下面也是自定义的异常类,和上面的异常类相比,其内部实现了__init__()
方法和__str__()
方法:
class InputError(Exception):
'''当输出有误时,抛出此异常'''
#自定义异常类型的初始化
def __init__(self, value):
self.value = value
# 返回异常类对象的说明信息
def __str__(self):
return ("{} is invalid input".format(repr(self.value)))
try:
raise InputError(1) # 抛出 MyInputError 这个异常
except InputError as err:
print('error: {}'.format(err))
运行结果为:
error: 1 is invalid input
注意,只要自定义的类继承自Exception
,则该类就是一个异常类,至于此类中包含的内容,并没有做任何规定。
异常机制使用细则
前面介绍了使用异常处理的优势、便捷之处,本节将进一步从程序性能优化、结构优化的角度给出异常处理的一般规则。
成功的异常处理应该实现如下 4 个目标:
- 使程序代码混乱最小化。
- 捕获并保留诊断信息。
- 通知合适的人员。
- 采用合适的方式结束异常活动。
下面介绍达到这些效果的基本准则。
不要过度使用异常
不可否认,Python
的异常机制确实方便,但滥用异常机制也会带来一些负面影响。过度使用异常主要表现在两个方面:
- 把异常和普通错误混淆在一起,不再编写任何错误处理代码,而是以简单地引发异常来代苦所有的错误处理。
- 使用异常处理来代替流程控制。
熟悉了异常使用方法后,程序员可能不再愿意编写烦琐的错误处理代码,而是简单地引发异常。实际上这样做是不对的,对于完全己知的错误和普通的错误,应该编写处理这种错误的代码,增加程序的健壮性。只有对于外部的、不能确定和预知的运行时错误才使用异常。
对比前面五子棋游戏中,处理用户输入坐标点己有棋子的两种方式。如果用户试图下棋的坐标点己有棋子:
#如果要下棋的点不为空
if board[int(y_str) - 1) [int(x_str) - 1] !="╋" :
inputStr = input ("您输入的坐标点己有棋子了,请重新输入\n")
continue
上面这种处理方式检测到用户试图下棋的坐标点己经有棋子,立即打印一条提示语句,并重新开始下一次循环。这种处理方式简洁明了、逻辑清晰,程序的运行效率也很好程序进入if
块后,即结束了本次循环。
如果将上面的处理机制改为如下方式:
#如果要下棋的点不为空
if board[int(y_str) - 1) [int(x_str) - 1) != "╋":
#引发默认的RuntimeError 异常
raise
上面这种处理方式没有提供有效的错误处理代码,当程序检测到用户试图下棋的坐标点己经有棋子时,并没有提供相应的处理,而是简单地引发一个异常。这种处理方式虽然简单,但Python
解释器接收到这个异常后,还需要进入相应的except
块来捕获该异常,所以运行效率要差一些。而且用户下棋重复这个错误完全是可预料的,所以程序完全可以针对该错误提供相应的处理,而不是引发异常。
必须指出,异常处理机制的初衷是将不可预期异常的处理代码和正常的业务逻辑处理代码分离,因此绝不要使用异常处理来代替正常的业务逻辑判断。
另外,异常机制的效率比正常的流程控制效率差,所以不要使用异常处理来代替正常的程序流程控制。例如,对于如下代码:
#定义一个字符串列表
my_list =["Hello", "Python", "Spring"]
#使用异常处理来遍历arr数组的每个元素
try:
i = 0
while True:
print (my_list [i])
i += 1
except:
pass
运行上面程序确实可以实现遍历 my_list 列表的功能,但这种写法可读性较差,而且运行效率也不高。程序完全有能力避免产生indexError
异常,程序“故意”制造这种异常,然后使用except
块去捕获该异常,这是不应该的。将程序改为如下形式肯定要好得多:
i = 0
while i < len(my_list):
print(my_list[i])
i += 1
注意,异常只应该用于处理非正常的情况,不要使用异常处理来代替正常的流程控制。对于一些完全可预知,而且处理方式清楚的错误,程序应该提供相应的错误处理代码,而不是将其笼统地称为异常。
不要使用过于庞大的try
块
很多初学异常机制的读者喜欢在try
块里放置大量的代码,这看上去很“简单”,但这种“简单”只是一种假象,只是在编写程序时看上去比较简单。但因为try
块里的代码过于庞大,业务过于复杂,就会造成try
块中出现异常的可能性大大增加,从而导致分析异常原因的难度也大大增加。
而且当时块过于庞大时,就难免在try
块后紧跟大量的except
块才可以针对不同的异常提供不同的处理逻辑。在同一个 try 块后紧跟大量的except
块则需要分析它们之间的逻辑关系,反而增加了编程复杂度。
正确的做法是,把大块的try
块分割成多个可能出现异常的程序段落,并把它们放在单独的try
块中,从而分别捕获并处理异常。
不要忽略捕获到的异常
不要忽略异常!既然己捕获到异常,那么except
块理应做些有用的事情,及处理并修复异常。except
块整个为空,或者仅仅打印简单的异常信息都是不妥的!
except
块为空就是假装不知道甚至瞒天过海,这是最可怕的事情,程序出了错误,所有人都看不到任何异常,但整个应用可能已经彻底坏了。仅在except
块里打印异常传播信息稍微好一点,但仅仅比空白多了几行异常信息。通常建议对异常采取适当措施,比如:
- 处理异常。对异常进行合适的修复,然后绕过异常发生的地方继续运行;或者用别的数据进行计算,以代替期望的方法返回值;或者提示用户重新操作……总之,程序应该尽量修复异常,使程序能恢复运行。
- 重新引发新异常。把在当前运行环境下能做的事情尽量做完,然后进行异常转译,把异常包装成当前层的异常,重新传给上层调用者。
- 在合适的层处理异常。如果当前层不清楚如何处理异常,就不要在当前层使用
except
语句来捕获该异常,让上层调用者来负责处理该异常。
logging
模块
无论使用哪种编程语言,最常用的调试代码的方式是:使用输出语句(比如C
语言中使用printf
,Python
中使用print()
函数)输出程序运行过程中一些关键的变量的值,查看它们的值是否正确,从而找到出错的地方。
启用logging
模块很简单,直接将下面的代码复制到程序开头:
import logging
logging.basicConfig(level=logging.DEBUG,
format=' %(asctime)s - %(levelname)s - %(message)s')
假如我们编写了如下一个函数,其设计的初衷是用来计算一个数的阶乘,但该函数有些问题,需要调试:
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')
"""运行结果
2019-09-11 14:14:56,928 - DEBUG - Start of program
2019-09-11 14:14:56,945 - DEBUG - Start of factorial(5%)
2019-09-11 14:14:56,959 - DEBUG - i is 0, total is 0
2019-09-11 14:14:56,967 - DEBUG - i is 1, total is 0
2019-09-11 14:14:56,979 - DEBUG - i is 2, total is 0
2019-09-11 14:14:56,991 - DEBUG - i is 3, total is 0
2019-09-11 14:14:57,000 - DEBUG - i is 4, total is 0
2019-09-11 14:14:57,013 - DEBUG - i is 5, total is 0
2019-09-11 14:14:57,024 - DEBUG - End of factorial(5%)
0
2019-09-11 14:14:57,042 - DEBUG - End of program
"""
可以看到,通过logging.debug()
函数可以打印日志信息,这个 debug() 函数将调用basicConfig()
打印一行信息,这行信息的格式是在basicConfig()
函数中指定的,并且包括传递给debug()
的消息。
分析程序的运行结果,factorial(5)
返回 0 作为 5 的阶乘的结果,这显然是不对的。for 循环应该用从 1 到 5 的数,乘以total
的值,但logging.debug()
显示的日志信息表明,i 变量从 0 开始,而不是 1。因为 0 乘任何数都是 0,所以接下来的迭代中,total
的值都是错的。日志消息提供了可以追踪的痕迹,帮助我们弄清楚程序运行过程哪里不对。
logging
日志级别
“日志级别”提供了一种方式,按重要性对日志消息进行分类。
级别 | 对应的函数 | 描述 |
---|---|---|
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-09-11 14:32:34,249 - DEBUG - Some debugging details.
>>> logging.info('The logging module is working.')
2019-09-11 14:32:47,456 - INFO - The logging module is working.
>>> logging.warning('An error message is about to be logged.')
2019-09-11 14:33:02,391 - WARNING - An error message is about to be logged.
>>> logging.error('An error has occurred.')
2019-09-11 14:33:14,413 - ERROR - An error has occurred.
>>> logging.critical('The program is unable to recover!')
2019-09-11 14:33:24,071 - CRITICAL - The program is unable to recover!
日志级别的好处在于,我们可以改变想看到的日志消息的优先级。比如说,向basicConfig()
函数传入logging.DEBUG
作为level
关键字参数,这将显示所有级别为 DEBUG
的日志消息。当开发了更多的程序后,我们可能只对错误感兴趣,在这种情况下,可以将basicConfig()
的level
参数设置为logging.ERROR
,这将只显示ERROR
和CRITICAL
消息,跳过DEBUG
、INFO
和WARNING
消息。
将日志消息输出到文件中
将日志消息输出到文件中的实现方法很简单,只需要设置logging.basicConfig()
函数中的filename
关键字参数即可,例如:
>>> import logging
>>> logging.basicConfig(filename='demo.txt', level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s')
此程序中,将日志消息存储到了demo.txt
文件中,该文件就位于运行的程序文件所在的目录。