5.3 处理异常

在一段程序中,为了能够让程序健壮,有时还要处理异常。举例:

  1. #!/usr/bin/env python
  2. # coding=utf-8
  3.  
  4. while 1:
  5. print "this is a division program."
  6. c = raw_input("input 'c' continue, otherwise logout:")
  7. if c == 'c':
  8. a = raw_input("first number:")
  9. b = raw_input("second number:")
  10. try:
  11. print float(a)/float(b)
  12. print "*************************"
  13. except ZeroDivisionError:
  14. print "The second number can't be zero!"
  15. print "*************************"
  16. else:
  17. break

运行这段程序,显示如下过程:

  1. $ python 21601.py
  2. this is a division program.
  3. input 'c' continue, otherwise logout:c
  4. first number:5
  5. second number:2
  6. 2.5
  7. *************************
  8. this is a division program.
  9. input 'c' continue, otherwise logout:c
  10. first number:5
  11. second number:0
  12. The second number can't be zero!
  13. *************************
  14. this is a division program.
  15. input 'c' continue, otherwise logout:d
  16. $

从运行情况看,当在第二个数,即除数为0时,程序并没有因为这个错误而停止,而是给用户一个友好的提示,让用户有机会改正错误。这完全得益于程序中“处理异常”的设置,如果没有“处理异常”,则当异常出现时就会导致程序中止。

5.3.1 try…except…

对于前述举例程序,只看try和except部分,如果没有异常发生,except子句在try语句执行之后被忽略;如果try子句中有异常发生,该部分的其他语句被忽略,直接跳到except部分,执行其后面指定的异常类型及其子句。

except后面也可以没有任何异常类型,即无异常参数。如果这样,不论try部分发生什么异常,都会执行except。

在except子句中,可以根据异常或者别的需要,进行更多的操作。比如:

  1. #!/usr/bin/env python
  2. # coding=utf-8
  3.  
  4. class Calculator(object):
  5. is_raise = False
  6. def calc(self, express):
  7. try:
  8. return eval(express)
  9. except ZeroDivisionError:
  10. if self.is_raise:
  11. print "zero can not be division."
  12. else:
  13. raise

先解释函数eval(),它的含义是:

  1. eval(...)
  2. eval(source[, globals[, locals]]) -> value
  3.  
  4. Evaluate the source in the context of globals and locals.
  5. The source may be a string representing a Python expression
  6. or a code object as returned by compile().
  7. The globals must be a dictionary and locals can be any mapping,
  8. defaulting to the current globals and locals.
  9. If only globals is given, locals defaults to it.

例如:

  1. >>> eval("3+5")
  2. 8

另外,在except子句中,还有一个孤零零的raise,作为单独一个语句,它的含义是将异常信息抛出,并且except子句用了一个判断语句,根据不同的情况确定走不同的分支。

  1. if __name__ == "__main__":
  2. c = Calculator()
  3. print c.calc("8/0")

故意出现0做分母的情况,就是要让is_raise=False,则会:

  1. $ python 21602.py
  2. Traceback (most recent call last):
  3. File "21602.py", line 17, in <module>
  4. print c.calc("8/0")
  5. File "21602.py", line 8, in calc
  6. return eval(express)
  7. File "<string>", line 1, in <module>
  8. ZeroDivisionError: integer division or modulo by zero

如果将is_raise的值改为True,会是这样:

  1. if __name__ == "__main__":
  2. c = Calculator()
  3. c.is_raise = True
  4. print c.calc("8/0")

运行结果:

  1. $ python 21602.py
  2. zero can not be division.
  3. None

最后的None是c.calc("8/0")的返回值,因为有print c.calc("8/0"),所以被打印出来。

5.3.2 处理多个异常

处理多个异常并不是因为同时报出多个异常,程序在运行中,只要遇到一个异常就会有反应,所以,每次捕获到的异常一定是一个。所谓处理多个异常的意思是可以容许捕获不同的异常,由不同的except子句处理。

  1. #!/usr/bin/env python
  2. # coding=utf-8
  3.  
  4. while 1:
  5. print "this is a division program."
  6. c = raw_input("input 'c' continue, otherwise logout:")
  7. if c == 'c':
  8. a = raw_input("first number:")
  9. b = raw_input("second number:")
  10. try:
  11. print float(a)/float(b)
  12. print "*************************"
  13. except ZeroDivisionError:
  14. print "The second number can't be zero!"
  15. print "*************************"
  16. except ValueError:
  17. print "please input number."
  18. print "************************"
  19. else:
  20. break

修改一下程序,增加了一个except子句,目的是当用户输入的不是数字时,捕获并处理这个异常。测试如下:

  1. $ python 21701.py
  2. this is a division program.
  3. input 'c' continue, otherwise logout:c
  4. first number:3
  5. second number:"hello" #输入了一个不是数字的东西
  6. please input number. #对照上面的程序,捕获并处理了这个异常
  7. ************************
  8. this is a division program.
  9. input 'c' continue, otherwise logout:c
  10. first number:4
  11. second number:0
  12. The second number can't be zero!
  13. *************************
  14. this is a division program.
  15. input 'c' continue, otherwise logout:4

如果有多个except,try里面遇到一个异常,就转到相应的except子句,其他的忽略。如果except没有相应的异常,该异常也会抛出,不过这时程序就要中止了,因为异常“浮出”程序顶部。

除了用多个except之外,还可以在一个except后面放多个异常参数,比如上面的程序,可以将except部分修改为:

  1. except (ZeroDivisionError, ValueError):
  2. print "please input rightly."
  3. print "********************"

运行的结果就是:

  1. $ python 21701.py
  2. this is a division program.
  3. input 'c' continue, otherwise logout:c
  4. first number:2
  5. second number:0 #捕获异常
  6. please input rightly.
  7. ********************
  8. this is a division program.
  9. input 'c' continue, otherwise logout:c
  10. first number:3
  11. second number:a #异常
  12. please input rightly.
  13. ********************
  14. this is a division program.
  15. input 'c' continue, otherwise logout:d

需要注意的是,except后面如果是多个参数,一定要用圆括号包裹起来。否则,后果自负。

在对异常的处理中,前面都是自己写一个提示语,但自己写的不如内置的异常错误提示好,如果希望把默认错误提示打印出来,但程序还不能中断,怎么办?Python提供了一种方式,将上面的代码修改如下:

  1. while 1:
  2. print "this is a division program."
  3. c = raw_input("input 'c' continue, otherwise logout:")
  4. if c == 'c':
  5. a = raw_input("first number:")
  6. b = raw_input("second number:")
  7. try:
  8. print float(a)/float(b)
  9. print "*************************"
  10. except (ZeroDivisionError, ValueError), e:
  11. print e
  12. print "********************"
  13. else:
  14. break

运行一下,看看提示信息。

  1. $ python 21702.py
  2. this is a division program.
  3. input 'c' continue, otherwise logout:c
  4. first number:2
  5. second number:a #异常
  6. could not convert string to float: a
  7. ********************
  8. this is a division program.
  9. input 'c' continue, otherwise logout:c
  10. first number:2
  11. second number:0 #异常
  12. float division by zero
  13. ********************
  14. this is a division program.
  15. 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子句。其实,人类的自然语言何尝不是如此呢?总要根据需要添加不少东西。

  1. >>> try:
  2. ... print "I am try"
  3. ... except:
  4. ... print "I am except"
  5. ... else:
  6. ... print "I am else"
  7. ...
  8. I am try
  9. I am else

这段演示能够帮助读者理解else的执行特点。如果执行了try,则except被忽略,但是else被执行。

  1. >>> try:
  2. ... print 1/0
  3. ... except:
  4. ... print "I am except"
  5. ... else:
  6. ... print "I am else"
  7. ...
  8. I am except

这时候else就不被执行了。

理解了else的执行特点,可以写这样一段程序,还是类似于前面的计算,只是如果输入的有误,就不断要求重新输入,直到输入正确并得到了结果,才不再要求输入内容,然后程序结束。

在看下面的参考代码之前,读者是否可以先自己写一段并调试?看看结果如何。

  1. #!/usr/bin/env python
  2. # coding=utf-8
  3. while 1:
  4. try:
  5. x = raw_input("the first number:")
  6. y = raw_input("the second number:")
  7.  
  8. r = float(x)/float(y)
  9. print r
  10. except Exception, e:
  11. print e
  12. print "try again."
  13. else:
  14. break

先看运行结果:

  1. $ python 21703.py
  2. the first number:2
  3. the second number:0 #异常,执行except
  4. float division by zero
  5. try again. #循环
  6. the first number:2
  7. the second number:a #异常
  8. could not convert string to float: a
  9. try again.
  10. the first number:4
  11. the second number:2 #正常,执行try
  12. 2.0 #然后else:break,退出程序

相当满意的执行结果。

程序中的“except Exception,e”的含义是不管什么异常,这里都会捕获,并且传给变量e,然后用print e把异常信息打印出来。

5.3.4 finally子句

finally子句,一听这个名字,就感觉它是做善后工作的。的确如此,如果有了finally,不管前面执行的是try,还是except,最终都要执行它。因此有一种说法是将finally用在可能的异常后进行清理。比如:

  1. >>> x = 10
  2.  
  3. >>> try:
  4. ... x = 1/0
  5. ... except Exception, e:
  6. ... print e
  7. ... finally:
  8. ... print "del x"
  9. ... del x
  10. ...
  11. integer division or modulo by zero
  12. del x

看一看x是否被删除?

  1. >>> x
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. NameError: name 'x' is not defined

当然,在应用中可以将上面的各个子句都综合起来使用,写成如下样式:

  1. try:
  2. do something
  3. except:
  4. do something
  5. else:
  6. do something
  7. finally
  8. do something

看到这里,你是不是觉得这个“try…except…”跟“if…else…”有点相似呢?但不要误以为某一个就可以取代另外一个,他们的存在都是有道理的。

5.3.5 assert语句

  1. >>> assert 1==1
  2. >>> assert 1==0
  3. Traceback (most recent call last):
  4. File "<stdin>", line 1, in <module>
  5. AssertionError

从上面的举例中可以基本了解assert的特点。

assert,翻译过来是“断言”之意。assert是语句等价于布尔真的判定,发生异常就意味着表达式为假。

assert的应用情景与其翻译的意思“断言”一样,即当程序运行到某个节点的时候,就断定某个变量的值必然是什么,或者对象必然拥有某个属性等,简单说就是断定什么东西必然是什么,如果不是,就抛出错误。

  1. #!/usr/bin/env python
  2. # coding=utf-8
  3.  
  4. class Account(object):
  5. def __init__(self, number):
  6. self.number = number
  7. self.balance = 0
  8.  
  9. def deposit(self, amount):
  10. assert amount > 0
  11. self.balance += balance
  12.  
  13. def withdraw(self, amount):
  14. assert amount > 0
  15. if amount <= self.balance:
  16. self.balance -= amount
  17. else:
  18. print "balance is not enough."

程序中,deposit()和withdraw()方法的参数amount值必须是大于零的,这里就用断言,如果不满足条件就会报错。比如这样来运行:

  1. if __name__ == "__main__":
  2. a = Account(1000)
  3. a.deposit(-10)

出现的结果是:

  1. $ python 21801.py
  2. Traceback (most recent call last):
  3. File "21801.py", line 22, in <module>
  4. a.deposit(-10)
  5. File "21801.py", line 10, in deposit
  6. assert amount > 0
  7. AssertionError

这就是断言assert的作用。什么是使用断言的最佳时机?

  • 如果没有特别的目的,断言应该用于如下情况:
  • 防御性的编程。
  • 运行时对程序逻辑的检测。
  • 合约性检查(比如前置条件,后置条件)。
  • 程序中的常量。
  • 检查文档。(上述要点来自《Python使用断言的最佳时机》网址:http://www.oschina.net/translate/when-to-use-assert)

不论是否理解,都要先看看,请牢记,在具体的开发过程中,有时间就看看本书,不断加深对这些概念的理解,这也是master的成就之法。

最后,引用危机百科中对“异常处理”词条的说明,作为对“错误和异常”部分的总结(有所删改):

异常处理是编程语言或计算机硬件里的一种机制,用于处理软件或信息系统中出现的异常状况(即超出程序正常执行流程的某些特殊条件)。

各种编程语言在处理异常方面具有非常显著的不同点(错误检测与异常处理的区别在于:错误检测是在正常的程序流中,处理不可预见问题的代码,例如一个调用操作未能成功结束)。某些编程语言有这样的函数:当输入存在非法数据时不能被安全地调用,或者返回值不能与异常进行有效的区别。例如,C语言中的atoi函数(从ASCII串到整数的转换)在输入非法时可以返回0。在这种情况下编程者需要另外进行错误检测(可能通过某些辅助全局变量,如C的errno),或进行输入检验(如通过正则表达式),或者共同使用这两种方法。

通过异常处理,我们可以对用户在程序中的非法输入进行控制和提示,以防程序崩溃。

从进程的视角来看,硬件中断相当于可恢复异常,虽然中断一般与程序流本身无关。

从子程序编程者的视角来看,异常是很有用的一种机制,用于通知外界该子程序不能正常执行,如输入的数据无效(例如除数是0),或所需资源不可用(例如文件丢失)。如果系统没有异常机制,则编程者需要用返回值来标示发生了哪些错误。

Python语言对异常处理机制是非常普遍深入的,所以想写出不含try、except的程序非常困难。