5.3 处理异常
在一段程序中,为了能够让程序健壮,有时还要处理异常。举例:
- #!/usr/bin/env python
- # coding=utf-8
- while 1:
- print "this is a division program."
- c = raw_input("input 'c' continue, otherwise logout:")
- if c == 'c':
- a = raw_input("first number:")
- b = raw_input("second number:")
- try:
- print float(a)/float(b)
- print "*************************"
- except ZeroDivisionError:
- print "The second number can't be zero!"
- print "*************************"
- else:
- break
运行这段程序,显示如下过程:
- $ python 21601.py
- this is a division program.
- input 'c' continue, otherwise logout:c
- first number:5
- second number:2
- 2.5
- *************************
- this is a division program.
- input 'c' continue, otherwise logout:c
- first number:5
- second number:0
- The second number can't be zero!
- *************************
- this is a division program.
- input 'c' continue, otherwise logout:d
- $
从运行情况看,当在第二个数,即除数为0时,程序并没有因为这个错误而停止,而是给用户一个友好的提示,让用户有机会改正错误。这完全得益于程序中“处理异常”的设置,如果没有“处理异常”,则当异常出现时就会导致程序中止。
5.3.1 try…except…
对于前述举例程序,只看try和except部分,如果没有异常发生,except子句在try语句执行之后被忽略;如果try子句中有异常发生,该部分的其他语句被忽略,直接跳到except部分,执行其后面指定的异常类型及其子句。
except后面也可以没有任何异常类型,即无异常参数。如果这样,不论try部分发生什么异常,都会执行except。
在except子句中,可以根据异常或者别的需要,进行更多的操作。比如:
- #!/usr/bin/env python
- # coding=utf-8
- class Calculator(object):
- is_raise = False
- def calc(self, express):
- try:
- return eval(express)
- except ZeroDivisionError:
- if self.is_raise:
- print "zero can not be division."
- else:
- raise
先解释函数eval(),它的含义是:
- eval(...)
- eval(source[, globals[, locals]]) -> value
- Evaluate the source in the context of globals and locals.
- The source may be a string representing a Python expression
- or a code object as returned by compile().
- The globals must be a dictionary and locals can be any mapping,
- defaulting to the current globals and locals.
- If only globals is given, locals defaults to it.
例如:
- >>> eval("3+5")
- 8
另外,在except子句中,还有一个孤零零的raise,作为单独一个语句,它的含义是将异常信息抛出,并且except子句用了一个判断语句,根据不同的情况确定走不同的分支。
- if __name__ == "__main__":
- c = Calculator()
- print c.calc("8/0")
故意出现0做分母的情况,就是要让is_raise=False,则会:
- $ python 21602.py
- Traceback (most recent call last):
- File "21602.py", line 17, in <module>
- print c.calc("8/0")
- File "21602.py", line 8, in calc
- return eval(express)
- File "<string>", line 1, in <module>
- ZeroDivisionError: integer division or modulo by zero
如果将is_raise的值改为True,会是这样:
- if __name__ == "__main__":
- c = Calculator()
- c.is_raise = True
- print c.calc("8/0")
运行结果:
- $ python 21602.py
- zero can not be division.
- None
最后的None是c.calc("8/0")的返回值,因为有print c.calc("8/0"),所以被打印出来。
5.3.2 处理多个异常
处理多个异常并不是因为同时报出多个异常,程序在运行中,只要遇到一个异常就会有反应,所以,每次捕获到的异常一定是一个。所谓处理多个异常的意思是可以容许捕获不同的异常,由不同的except子句处理。
- #!/usr/bin/env python
- # coding=utf-8
- while 1:
- print "this is a division program."
- c = raw_input("input 'c' continue, otherwise logout:")
- if c == 'c':
- a = raw_input("first number:")
- b = raw_input("second number:")
- try:
- print float(a)/float(b)
- print "*************************"
- except ZeroDivisionError:
- print "The second number can't be zero!"
- print "*************************"
- except ValueError:
- print "please input number."
- print "************************"
- else:
- break
修改一下程序,增加了一个except子句,目的是当用户输入的不是数字时,捕获并处理这个异常。测试如下:
- $ python 21701.py
- this is a division program.
- input 'c' continue, otherwise logout:c
- first number:3
- second number:"hello" #输入了一个不是数字的东西
- please input number. #对照上面的程序,捕获并处理了这个异常
- ************************
- this is a division program.
- input 'c' continue, otherwise logout:c
- first number:4
- second number:0
- The second number can't be zero!
- *************************
- this is a division program.
- input 'c' continue, otherwise logout:4
如果有多个except,try里面遇到一个异常,就转到相应的except子句,其他的忽略。如果except没有相应的异常,该异常也会抛出,不过这时程序就要中止了,因为异常“浮出”程序顶部。
除了用多个except之外,还可以在一个except后面放多个异常参数,比如上面的程序,可以将except部分修改为:
- except (ZeroDivisionError, ValueError):
- print "please input rightly."
- print "********************"
运行的结果就是:
- $ python 21701.py
- this is a division program.
- input 'c' continue, otherwise logout:c
- first number:2
- second number:0 #捕获异常
- please input rightly.
- ********************
- this is a division program.
- input 'c' continue, otherwise logout:c
- first number:3
- second number:a #异常
- please input rightly.
- ********************
- this is a division program.
- input 'c' continue, otherwise logout:d
需要注意的是,except后面如果是多个参数,一定要用圆括号包裹起来。否则,后果自负。
在对异常的处理中,前面都是自己写一个提示语,但自己写的不如内置的异常错误提示好,如果希望把默认错误提示打印出来,但程序还不能中断,怎么办?Python提供了一种方式,将上面的代码修改如下:
- while 1:
- print "this is a division program."
- c = raw_input("input 'c' continue, otherwise logout:")
- if c == 'c':
- a = raw_input("first number:")
- b = raw_input("second number:")
- try:
- print float(a)/float(b)
- print "*************************"
- except (ZeroDivisionError, ValueError), e:
- print e
- print "********************"
- else:
- break
运行一下,看看提示信息。
- $ python 21702.py
- this is a division program.
- input 'c' continue, otherwise logout:c
- first number:2
- second number:a #异常
- could not convert string to float: a
- ********************
- this is a division program.
- input 'c' continue, otherwise logout:c
- first number:2
- second number:0 #异常
- float division by zero
- ********************
- this is a division program.
- input 'c' continue, otherwise logout:d
在Python 3.x中,常常这样写:except(ZeroDivisionError,ValueError)as e:
在上面的程序中,只处理了两个异常,还可能有更多的异常,如果要处理,怎么办?可以这样:execpt:或者except Exception,e,后面什么参数也不写就好了。
5.3.3 else子句
有了try…except…,在一般情况下是够用的,但总有不一般的时候出现,所以,就增加了一个else子句。其实,人类的自然语言何尝不是如此呢?总要根据需要添加不少东西。
- >>> try:
- ... print "I am try"
- ... except:
- ... print "I am except"
- ... else:
- ... print "I am else"
- ...
- I am try
- I am else
这段演示能够帮助读者理解else的执行特点。如果执行了try,则except被忽略,但是else被执行。
- >>> try:
- ... print 1/0
- ... except:
- ... print "I am except"
- ... else:
- ... print "I am else"
- ...
- I am except
这时候else就不被执行了。
理解了else的执行特点,可以写这样一段程序,还是类似于前面的计算,只是如果输入的有误,就不断要求重新输入,直到输入正确并得到了结果,才不再要求输入内容,然后程序结束。
在看下面的参考代码之前,读者是否可以先自己写一段并调试?看看结果如何。
- #!/usr/bin/env python
- # coding=utf-8
- while 1:
- try:
- x = raw_input("the first number:")
- y = raw_input("the second number:")
- r = float(x)/float(y)
- print r
- except Exception, e:
- print e
- print "try again."
- else:
- break
先看运行结果:
- $ python 21703.py
- the first number:2
- the second number:0 #异常,执行except
- float division by zero
- try again. #循环
- the first number:2
- the second number:a #异常
- could not convert string to float: a
- try again.
- the first number:4
- the second number:2 #正常,执行try
- 2.0 #然后else:break,退出程序
相当满意的执行结果。
程序中的“except Exception,e”的含义是不管什么异常,这里都会捕获,并且传给变量e,然后用print e把异常信息打印出来。
5.3.4 finally子句
finally子句,一听这个名字,就感觉它是做善后工作的。的确如此,如果有了finally,不管前面执行的是try,还是except,最终都要执行它。因此有一种说法是将finally用在可能的异常后进行清理。比如:
- >>> x = 10
- >>> try:
- ... x = 1/0
- ... except Exception, e:
- ... print e
- ... finally:
- ... print "del x"
- ... del x
- ...
- integer division or modulo by zero
- del x
看一看x是否被删除?
- >>> x
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- NameError: name 'x' is not defined
当然,在应用中可以将上面的各个子句都综合起来使用,写成如下样式:
- try:
- do something
- except:
- do something
- else:
- do something
- finally
- do something
看到这里,你是不是觉得这个“try…except…”跟“if…else…”有点相似呢?但不要误以为某一个就可以取代另外一个,他们的存在都是有道理的。
5.3.5 assert语句
- >>> assert 1==1
- >>> assert 1==0
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- AssertionError
从上面的举例中可以基本了解assert的特点。
assert,翻译过来是“断言”之意。assert是语句等价于布尔真的判定,发生异常就意味着表达式为假。
assert的应用情景与其翻译的意思“断言”一样,即当程序运行到某个节点的时候,就断定某个变量的值必然是什么,或者对象必然拥有某个属性等,简单说就是断定什么东西必然是什么,如果不是,就抛出错误。
- #!/usr/bin/env python
- # coding=utf-8
- class Account(object):
- def __init__(self, number):
- self.number = number
- self.balance = 0
- def deposit(self, amount):
- assert amount > 0
- self.balance += balance
- def withdraw(self, amount):
- assert amount > 0
- if amount <= self.balance:
- self.balance -= amount
- else:
- print "balance is not enough."
程序中,deposit()和withdraw()方法的参数amount值必须是大于零的,这里就用断言,如果不满足条件就会报错。比如这样来运行:
- if __name__ == "__main__":
- a = Account(1000)
- a.deposit(-10)
出现的结果是:
- $ python 21801.py
- Traceback (most recent call last):
- File "21801.py", line 22, in <module>
- a.deposit(-10)
- File "21801.py", line 10, in deposit
- assert amount > 0
- AssertionError
这就是断言assert的作用。什么是使用断言的最佳时机?
- 如果没有特别的目的,断言应该用于如下情况:
- 防御性的编程。
- 运行时对程序逻辑的检测。
- 合约性检查(比如前置条件,后置条件)。
- 程序中的常量。
- 检查文档。(上述要点来自《Python使用断言的最佳时机》网址:http://www.oschina.net/translate/when-to-use-assert)
不论是否理解,都要先看看,请牢记,在具体的开发过程中,有时间就看看本书,不断加深对这些概念的理解,这也是master的成就之法。
最后,引用危机百科中对“异常处理”词条的说明,作为对“错误和异常”部分的总结(有所删改):
异常处理是编程语言或计算机硬件里的一种机制,用于处理软件或信息系统中出现的异常状况(即超出程序正常执行流程的某些特殊条件)。
各种编程语言在处理异常方面具有非常显著的不同点(错误检测与异常处理的区别在于:错误检测是在正常的程序流中,处理不可预见问题的代码,例如一个调用操作未能成功结束)。某些编程语言有这样的函数:当输入存在非法数据时不能被安全地调用,或者返回值不能与异常进行有效的区别。例如,C语言中的atoi函数(从ASCII串到整数的转换)在输入非法时可以返回0。在这种情况下编程者需要另外进行错误检测(可能通过某些辅助全局变量,如C的errno),或进行输入检验(如通过正则表达式),或者共同使用这两种方法。
通过异常处理,我们可以对用户在程序中的非法输入进行控制和提示,以防程序崩溃。
从进程的视角来看,硬件中断相当于可恢复异常,虽然中断一般与程序流本身无关。
从子程序编程者的视角来看,异常是很有用的一种机制,用于通知外界该子程序不能正常执行,如输入的数据无效(例如除数是0),或所需资源不可用(例如文件丢失)。如果系统没有异常机制,则编程者需要用返回值来标示发生了哪些错误。
Python语言对异常处理机制是非常普遍深入的,所以想写出不含try、except的程序非常困难。