您的位置:首页 > 编程语言 > Python开发

Python基础--第7章 错误与异常——调教出听话的程序

2018-03-07 09:51 363 查看
任何开发人员,在编写程序时,都会遇到各种不同的错误。程序的错误有很多种,有的是编写人员疏忽;有的是程序运行时与系统的规则冲突、或与其他外部环境不一致导致的。所有的错误都可以归纳为两类:
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  标准异常
[align=center]
异常名称
描述
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
用户代码生成的警告
[/align] 
 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基础