Python基础--第7章 错误与异常——调教出听话的程序
任何开发人员,在编写程序时,都会遇到各种不同的错误。程序的错误有很多种,有的是编写人员疏忽;有的是程序运行时与系统的规则冲突、或与其他外部环境不一致导致的。所有的错误都可以归纳为两类:
l 语法错误,也就是解析时错误。代码不符合Python语法规则时,在解析过程中会报SyntaxError。报错的同时会显示出哪一行出错,并且用小箭头指明最早探测到错误的位置。例如:
print'hello' #在Python3中,这种写法是不允许的,'hello'外面必须有括号
上面是错误写法。因为在Python3中,函数print被调用时,参数必须放入括号里。于是运行的时候,就会报如下错误:
print'hello'
File "<iPython-input-1-bfbe230352b8>",line 1
print 'hello'
^
SyntaxError:Missing parentheses in call to 'print'
这种错误属于编写人员疏忽导致的,与基本的代码规则违背。这种错误在Python中属于真正意义上的错误,是程序不能容忍的,程序执行到这时就会终止。
l 运行时的错误,即语句或表达式在语法上都是正确的,但在运行时发生了错误。例如:
a=1/0 # 让1除以0,结果赋值给a
该句代码在运行时就会出错。因为它执行了1除以0,0做被除数是没有意义的。于是运行后会产生如下错误:
Traceback(most recent call last):
File "<iPython-input-2-82f326788aeb>",line 1, in <module>
a= 1/0
ZeroDivisionError:division by zero
上面显示的内容中,前两段是指出错误的位置。最后一句是报出出错的类型。在Python中,会把这种运行时产生错误的情况叫做异常(Exceptions)。对于这种异常情况还有很多。例如:
a=3+t #将3与t的加和赋值给a
上面的代码中t没有定义,会报如下异常:
Traceback(most recent call last):
File "<iPython-input-3-01cf05da9aff>",line 1, in <module>
a=3+t
NameError:name 't' is not defined
这种就是属于NameError异常。NameError就是代表异常的类型。
例子中的NameError与前面的ZeroDivisionError都属于内置的异常名称,是Python中用来代表异常类型的标准异常名字。是全局认可的内建的标识符。
当一个程序发生异常时,代表着该程序的执行出现了一个非正常的情况。无法再执行下去。默认情况下,程序是要终止的。为了避免程序退出,在Python中可以通过捕获异常的方式,获取到这个异常名称。再依靠其他的逻辑让程序继续运行。这种根据异常做出的逻辑处理叫做异常处理。
通过异常的处理,使开发者可以更全面的控制自己的程序。不仅仅能够管理正常的流程运行,还能够在程序出错时,对程序进行必要的处理。大大提高程序的健壮性和人机交互的友好性。
7.1 异常的基本语法
Python语法会把异常当作一个对象,通过使用try/except语句来捕捉该异常对象。try/except语句后面都会跟着与其对应的代码块。用来检测的错误代码放到try对应的代码块中,当检测到错误时,就会进入except代码块来执行相应的逻辑处理。出现异常之后是要让程序自我调整继续运行,还是要终止运行,全看except代码块的逻辑要如何处理。
其语法定义如下:
try:
<语句> #运行别的代码
except<名字>:
<语句> #如果在try部份引发了'name'异常
except<名字>,<数据>:
<语句> #如果引发了'name'异常,获得附加的数据
else:
<语句> #如果没有异常发生
一个 try 语句可以有多条的 except 语句,用以指定不同的异常,但至多只有一个会被执行。例如:
try:
x = int(input('请输入一个被除数:')) #等待输入一个数
print('30除以',x,'等于',30/x) #输出30处于该输入的数字
exceptValueError: #捕获ValueError异常
print('输入了无效的整数. 重新输入...')
exceptZeroDivisionError: #捕获ZeroDivisionError异常
print('被除数不等于0, 重新输入...')
except: #捕获其他异常
print('其他异常...')
上面这段代码,当输入a(非数字)时,将抛出ValueError异常;当输入0时,将抛出ZeroDivisionError异常;如果再出现其它异常,将执行except:后的处理语句。
7.1.1 同时处理多个异常
一个 except 语句可以同时包括多个异常名,但需要用括号括起来,例如:
try:
x = int(input('请输入一个被除数:')) #等待输入一个数
print('30除以',x,'等于',30/x) #输出30处于该输入的数字
except(ZeroDivisionError,ValueError): #同时捕获ZeroDivisionError与ValueError异常
print('输入错误,重新输入...')
except: #捕获其他异常
print('其他异常...')
上面这段代码,ZeroDivisionError与ValueError异常是被放到一个异常处理分支下的。当输入a(非数字)或0时,都会输出“输入错误, 重新输入...”信息。
7.1.2 配合else语句
try...except...语句后面还可以跟else 语句。当没有异常发生的时候,else 从句将被执行。else语句是个可选语句,必须要放在所有 except 语句后面。例如:
try:
x = int(input('请输入一个被除数:')) #等待输入一个数
print('30除以',x,'等于',30/x) #输出30处于该输入的数字
except(ZeroDivisionError,ValueError): #同时捕获ZeroDivisionError与ValueError异常
print('输入错误,重新输入...')
except: #捕获其他异常
print('其他异常...')
else:
print("再见")
执行上面程序,当输入整数6时。显示如下:
请输入一个被除数:6
30除以 6 等于 5.0
再见
程序走完了正常流程,没有产生异常。执行了else中的内容,于是输出了“再见”。
7.1.3 输出未知异常
前面的7.1.1中的例子,直接捕获了指定的异常类型。这是因为程序比较简单,可以预知会发生哪些异常。如果在一些复杂的程序中捕获异常时,往往很难预测会有什么异常情况发生。这时可以使用如下语法输出未知异常。
try:
...some functions...
exceptException as e:
print(e)
通过except后面跟着Exception as e的语句,即可得到异常类型e。下面通过代码演示:
try:
x = int(input('请输入一个被除数:')) #等待输入一个数
print('30除以',x,'等于',30/x) #输出30处于该输入的数字
exceptException as e: #捕获未知异常
print(e )
print('其他异常...')
执行上面程序,当输入整数0时。显示如下:
divisionby zero
其他异常...
可以看到程序打印出了异常类型的原因,是因为除数为0。
7.1.4 输出异常详细信息
前面的7.1.3中的例子,可以知道未知异常的原因,但是在某些情况下还是远远不够的。为了调试方便,还需要获得更多详细的异常信息,来辅助开发人员解决问题。。
要想得到异常的全部信息,可以使用sys模块的exe_info函数,也可以使用traceback模块的相关函数。详细介绍如下:
1. sys的exc_info函数
确切的说sys中有两个函数都可以返回异常的全部信息:一个是exc_info;另一个是last_traceback。两个函数的用法及功能一样。这里以exc_info为例。
exc_info将获取到当前的异常信息,返回一个元组,元组内包含3个元素,内容为 type、value,、traceback。其中:
l Type:异常的类型名称,它是BaseException 的子类(类与子类见第9章的详细介绍);
l value:捕获到的异常实例(实例的概念见第9章的详细介绍);
l traceback: 是一个 traceback 对象,下面会详述。
具体用法,见下面代码:
importsys
try:
x = int(input('请输入一个被除数:')) #等待输入一个数
print('30除以',x,'等于',30/x) #输出30处于该输入的数字
except:
print(sys.exc_info() ) #捕获其他异常
print('其他异常...')
运行程序上面的程序。输入0,令程序发生异常。输出如下:
请输入一个被除数:0
(<class'ZeroDivisionError'>, ZeroDivisionError('division by zero',), <tracebackobject at 0x000000000C745748>)
其他异常...
可以看到第二行显示的就是异常的全部信息。是一个元组。元组的第一个元素是一个'ZeroDivisionError'类;第二个元素是异常类型ZeroDivisionError类的一个实例;第三个元素为一个traceback对象。
这种输出可以看到异常类型的信息。还可以从traceback对象里面找到异常位置的调用堆栈信息。见下文的traceback模块介绍。
2. traceback对象的显示
当traceback模块被引入之后就可以把sys.exc_info输出的traceback对象信息显示出来。具体是实现是通过调用traceback的print_tb对象。例如
importtraceback
importsys
try:
x = int(input('请输入一个被除数:')) #等待输入一个数
print('30除以',x,'等于',30/x) #输出30处于该输入的数字
except: #捕获其他异常
traceback.print_tb(sys.exc_info()[2]) #用来打印trackback 对象
print('其他异常...')
else:
print("再见")
运行程序上面的程序。输入0,令程序发生异常。输出如下:
其他异常...
File "<iPython-input-6-74537bd39c61>",line 5, in <module>
print('30除以',x,'等于',30/x) #输出30处于该输入的数字
可以看到显示的内容就是报错异常的那句代码的位置。
3. traceback模块的其他函数
通过traceback模块的载入可以更简单的获得更多异常信息显示。比如调用traceback的print_exc方法可以直接将异常内容打印出来。例如:
importtraceback
try:
x = int(input('请输入一个被除数:')) #等待输入一个数
print('30除以',x,'等于',30/x) #输出30处于该输入的数字
except: #捕获其他异常
traceback.print_exc()
print('其他异常...')
else:
print("再见")
运行程序上面的程序。输入0,令程序发生异常。输出如下:
请输入一个被除数:0
其他异常...
Traceback(most recent call last):
File "<iPython-input-4-a9ad416eade4>",line 4, in <module>
print('30除以',x,'等于',30/x)
ZeroDivisionError:division by zero
可以看到打印的信息中包含了程序出错的位置(第4、5行),异常类型(最后一行)、异常类型的说明(最后一行)。
在traceback模块中,还有类似功能的print_exception函数。但是print_exception需要传入sys.exc_info()的结果。即,下面两句代码是等价的:
traceback.print_exc()
traceback.print_exception(*sys.exc_info())
7.2 异常的捕获与处理
异常处理的过程中,try与except语句都做了哪些操作呢?这个得从异常的处理流程与try的工作原理说起。
7.2.1 异常处理流程
在 try 语句执行时,如果出现了一个异常,该语句的剩下部分将被跳过。并且如果该异常的类型匹配到了 except 后面的异常名,那么该 except 后的语句将被执行。注意,如果 except 后面没有跟异常名,表示它匹配任何类型的异常,except:必须放在最后。
7.2.2 try的工作原理
try的工作原理如下:
(1)当开始一个try语句后,Python就在当前程序的上下文中作标记,这样当异常出现时就可以回到这里,try子句先执行,接下来会发生什么依赖于执行时是否出现异常。
(2)如果当try后的语句执行时发生异常,Python就跳回到try并执行第一个匹配该异常的except子句,异常处理完毕,控制流就通过整个try语句(除非在处理异常时又引发新的异常)。
(3)如果在try后的语句里发生了异常,却没有匹配的except子句,异常将被递交到上层的try,或者到程序的最上层(这样将结束程序,并打印缺省的出错信息)。
(4)如果在try子句执行时没有发生异常,Python将执行else语句后的语句(如果有else的话),然后控制流通过整个try语句。
7.2.3 标准异常
在Python中,内置了一些常见的异常,称之为标注异常。标准异常的具体类型下表7-1:
表7-1 标准异常
异常名称 | 描述 |
BaseException | 所有异常的基类 |
SystemExit | 解释器请求退出 |
KeyboardInterrupt | 用户中断执行(通常是输入^C) |
Exception | 常规错误的基类 |
StopIteration | 迭代器没有更多的值 |
GeneratorExit | 生成器(generator)发生异常来通知退出 |
StandardError | 所有的内建标准异常的基类 |
ArithmeticError | 所有数值计算错误的基类 |
FloatingPointError | 浮点计算错误 |
OverflowError | 数值运算超出最大限制 |
ZeroDivisionError | 除(或取模)零 (所有数据类型) |
AssertionError | 断言语句失败 |
AttributeError | 对象没有这个属性 |
EOFError | 没有内建输入,到达EOF 标记 |
EnvironmentError | 操作系统错误的基类 |
IOError | 输入/输出操作失败 |
OSError | 操作系统错误 |
WindowsError | 系统调用失败 |
ImportError | 导入模块/对象失败 |
LookupError | 无效数据查询的基类 |
IndexError | 序列中没有此索引(index) |
KeyError | 映射中没有这个键 |
MemoryError | 内存溢出错误(对于Python 解释器不是致命的) |
NameError | 未声明/初始化对象 (没有属性) |
UnboundLocalError | 访问未初始化的本地变量 |
ReferenceError | 弱引用(Weak reference)试图访问已经垃圾回收了的对象 |
RuntimeError | 一般的运行时错误 |
NotImplementedError | 尚未实现的方法 |
SyntaxError | Python 语法错误 |
IndentationError | 缩进错误 |
TabError | Tab 和空格混用 |
SystemError | 一般的解释器系统错误 |
TypeError | 对类型无效的操作 |
ValueError | 传入无效的参数 |
UnicodeError | Unicode 相关的错误 |
UnicodeDecodeError | Unicode 解码时的错误 |
UnicodeEncodeError | Unicode 编码时错误 |
UnicodeTranslateError | Unicode 转换时错误 |
Warning | 警告的基类 |
DeprecationWarning | 关于被弃用的特征的警告 |
FutureWarning | 关于构造将来语义会有改变的警告 |
OverflowWarning | 旧的关于自动提升为长整型(long)的警告 |
PendingDeprecationWarning | 关于特性将会被废弃的警告 |
RuntimeWarning | 可疑的运行时行为(runtime behavior)的警告 |
SyntaxWarning | 可疑的语法的警告 |
UserWarning | 用户代码生成的警告 |
Python内置的标准异常大多数都是实例化的类(类的细节,请参考第9章)。
7.3 创建异常(raise)
前面7.1与7.2小节讲的内容都是由于程序运行过程中发生了异常情况时,被动的报出了异常。这里再介绍一种主动生成一个异常的方法——创建异常。
创建异常又叫抛出异常,也叫触发异常。是主动向系统报出异常的方法。使用关键字raise来实现,其定义如下:
raise[Exception [, args [, traceback]]]
l Exception是异常的类型(例如,NameError)参数是一个异常参数值。该参数是可选的,如果不提供,异常的参数是"None";
l 最后一个参数是可选的(在实践中很少使用),如果存在,是跟踪异常对象。
注意:
raise抛出的异常必须是一个异常实例或类(派生自 Exception 的类)。类有关的知识在第9章可以看到,读者先有个印象即可。
当程序运行时,无论当前代码是否正常,只要执行到有raise开头的语句时,就会抛出一个异常。例如:
try:
x = int(input('请输入一个被除数:')) #等待输入一个数
if 0==x: #判断输入是否为0。如果为0,主动生成异常。
raise ValueError('输入错误:0不能做被除数')
print('30除以',x,'等于',30/x) #输出30处于该输入的数字
exceptException as e:
print(e )
exceptZeroDivisionError: #捕获ZeroDivisionError异常
print('被除数不等于0, 重新输入...')
except: #捕获其他异常
print('其他异常...')
这段代码是让用户输入一个被除数,并用30除以这个输入的被除数。由于0不能为除数,于是在得到输入数值之后加了一个判断。发现输入为0时,由程序主动生成一个异常。在后面代码中通过except关键字来捕获该异常。
将程序运行起来,输入0。会有如下输出:
请输入一个被除数:0
输入错误:0不能做被除数
可以看到在捕获异常时,程序走到了ValueError异常分支。与代码中通过raise生成的异常类型(ValueError)一致。在ValueError异常分支中,执行的代码是将异常信息打印出来。于是,程序就输出:“输入错误:0不能做被除数” 。
使用raise语句,会使程序更加严谨。可以在程序走入不该发生的分支情况,主导报出异常。使得程序在逻辑层面的健壮性增强。同时,在出错时,还可以提供自己定义的人机交互信息。使调试起来更为方便。
7.4 清理动作(finally)
Python中,与try和except语句配合使用的,还有finally语句。其作用是try中的语句无论是否跳入except中,最终都要进入finally语句,执行finally语句的分支代码。finally语句中的内容一般都是用来做清理工作的。
7.4.1 finally的使用场景
由于try和except语句的出现,使得程序的执行流程发生了改变。代码在没有运行的前提下,并不知道会进入哪个分支。当在复杂的逻辑关系处理中,这种情况很容易导致某些已经占用的资源没有释放。引起资源泄漏或内存泄漏的情况。
为了解决这种问题,一般会在except的最后,加个finally语句。并在finally中将某些资源、内存进行统一的清理处理。这样无论程序走哪个分支,最终都会进入finally分支的代码,进行资源的收尾工作。避免了资源或内存泄漏的问题。例如:
try:
print('打开一个文件') #伪码:打开一个文件
print('读取内容') #伪码:假设打开成功,开始读取文件
raise IOError('读取出错') #伪码:假设在读取过程中发生了错误
exceptException as e: #将错误异常捕获
print(e )
finally: #无论程序是否错误,在执行完前面代码后,都要在这里关闭文件
print("关闭文件")
这里通过伪码来描述一个打开文件并读取的过程(文件的真实操作在第8章会有介绍)。在这段代码中,try里面的代码,打开了一个文件,并开始读取。在真正执行的情况下,会发生正常读取和读取出错两种情况。无论哪种情况,都会进入finally分支。在finally分支中,将已经打开的文件关闭,确保资源得到释放。
7.4.2 finally与else的区别
与 else 从句的区别在于: else 语句只在没有异常发生的情况下执行,而 finally 语句则不管异常发生与否都会执行。准确的说,finally 语句总是在退出 try 语句前被执行,无论是正常退出、异常退出,还是通过break、continue、return退出。
7.5 断言(assert)
异常通常是在程序运行时与系统或环境间的作用下产生的。Python中的try与except语句一般是用来捕捉用户或者环境的错误。而断言是对某个条件确定性的判断,即,检验自己的判断是对还是错。
断言是使用assert关键字,后面接着一个条件表达式。如果条件表达式为真,意味着程序当前的条件与开发人员自己断言的情况一致,则程序继续运行;如果为假,则表明一定是前面发生错误了,程序停止运行报出异常。例如:
assert1!=1 #断言1不等于1
这句代码断言1不等于1,这显然是个错误的条件。于是报错:
File "<iPython-input-4-c5230ec50e34>",line 1, in <module>
assert 1!=1
AssertionError
外表看上去貌似没有什么意义,但是在检查开发人员的自身错误时,就变得非常有用了。断言常常被用在单元测试和参数检查中。避免在程序运行时发生的由于开发人员编写上的逻辑错误。
内容来源于《python带我起飞——入门、进阶、商业实战》一书。
购买链接:http://t.cn/RBNS8fD
配套免费视频:http://v.qq.com/vplus/1ea7e3c40fd64cd5a25e9827b38c171e/foldervideos/xvp0024019vk2to