强基初中数学&学Python——第二十五课 Python异常处理

    请看下面的计算整数算术平方根浮点值的程序:

输入“9.8”后:

。由于9.8不是整数,发生了异常(ValueError),退出了!有没有什么方法,即使发生异常也不退出运行呢?有!就是Python的异常处理机制。把代码改为:

,输入"9.8"后:

。虽然发生了异常,但程序没有终止运行,这种语法结构就是异常处理机制:

    把程序改成算分数的平方根:

错误把0输给分母后:

发生了除以0的错误,由于没有这个异常的处理,程序还是退出了。怎么办呢?

    第一种方法是捕捉多个异常,捕捉多个异常有两种语法格式。其一:在except关键词后加入括号,括号中列举可能要发生的异常。程序代码如下:

输入1和0后:

显然,除以0的异常已经被捕捉处理,但对应读者,不知道具体是哪个异常发生和处理了。有没有分开捕捉和处理的方法呢?有!其二:每一个异常使用一条except语句。程序修改如下:

程序运行结果:

上面只知道捕捉到了什么类的异常,但不知道哪个异常的对象,从异常的对象中打出描述。有没有办法,获取异常的具体对象并打印它呢?

    有!把程序改为这样:

运行结果:

在异常类名称后加关键词"as"后跟着一个名词(自取)的,这里用e,用其它名称也一样的。

    现在输入一个正确的分数,结果如下:

计算结果前多了一些不想要的空格,把程序改为下面样子试试:

结果打印:

显然,有个异常没被处理而造成程序退出。有没有一种异常类名可以捕获所有的异常的呢?

    有!不过在介绍这个类名之前,要了解一下异常的继承关系。

    except关键字后的异常类名,可以捕获这个类和它的派生类(子孙类)的对象。

    内置异常的类层级结构如下:

BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- StopAsyncIteration
      +-- ArithmeticError
      |    +-- FloatingPointError
      |    +-- OverflowError
      |    +-- ZeroDivisionError
      +-- AssertionError
      +-- AttributeError
      +-- BufferError
      +-- EOFError
      +-- ImportError
      |    +-- ModuleNotFoundError
      +-- LookupError
      |    +-- IndexError
      |    +-- KeyError
      +-- MemoryError
      +-- NameError
      |    +-- UnboundLocalError
      +-- OSError
      |    +-- BlockingIOError
      |    +-- ChildProcessError
      |    +-- ConnectionError
      |    |    +-- BrokenPipeError
      |    |    +-- ConnectionAbortedError
      |    |    +-- ConnectionRefusedError
      |    |    +-- ConnectionResetError
      |    +-- FileExistsError
      |    +-- FileNotFoundError
      |    +-- InterruptedError
      |    +-- IsADirectoryError
      |    +-- NotADirectoryError
      |    +-- PermissionError
      |    +-- ProcessLookupError
      |    +-- TimeoutError
      +-- ReferenceError
      +-- RuntimeError
      |    +-- NotImplementedError
      |    +-- RecursionError
      +-- SyntaxError
      |    +-- IndentationError
      |         +-- TabError
      +-- SystemError
      +-- TypeError
      +-- ValueError
      |    +-- UnicodeError
      |         +-- UnicodeDecodeError
      |         +-- UnicodeEncodeError
      |         +-- UnicodeTranslateError
      +-- Warning
           +-- DeprecationWarning
           +-- PendingDeprecationWarning
           +-- RuntimeWarning
           +-- SyntaxWarning
           +-- UserWarning
           +-- FutureWarning
           +-- ImportWarning
           +-- UnicodeWarning
           +-- BytesWarning
           +-- ResourceWarning

BaseException是所有内置异常类的根类,根类有四个子类SystemExit(系统退出)、KeyboardInterrupt(键盘中断)、GeneratorExit(普通退出)和Exception(应用异常)。对应程序编写人员,一般只要捕捉Exception类及其派生类异常;Python也建议程序编写人员自定义异常类时都是继承Exception类及其派生类异常。从上面的异常树中找出我们刚遇到的三个异常:

BaseException
 +-- Exception
      +-- ArithmeticError
      |    +-- ZeroDivisionError
      +-- TypeError
      +-- ValueError

    有了异常类树,我们就可以按2个类之间有无直接或间接的继承关系,把类的关系分为2种,有继承关系的是派生关系,没有继承关系的是非派生关系。可见ZeroDivisionError、ValueError和TypeError之间都是非派生关系。如果所有的类之间都是非派生关系,那么异常类名称在except子句中的位置或except子句的排列顺序对程序的执行结果没有影响;如果有派生关系的类,因为系统是按顺序查找匹配的类名,所以异常类名称在except子句中的位置或except子句的排列顺序对程序的执行结果可能会产生影响。

    根据异常类树,可以知道BaseException可以捕捉所有的异常。但考虑到我们在程序中一般只遇到Exception类或它的派生类异常,所以我们一般都用Exception类作为通配异常。把程序修改为:

,测试异常打印如下:

可见,除了第一个except字句起作用外,其它的都不起作用,因为异常都被Exception捕获了。谨记:Exception总是放在最后。修改程序代码:

,测试异常打印如下:

除了TypeError没在程序中列出,其它的都对上了。

    从上面的测试异常打印中可以看到,有两处出现ValueError。这程序不同地方产生同样名称的异常是很常见的,那么怎样才能知道,究竟是哪里产生了异常呢?

    要找traceback模块来帮忙。把程序改为下面的样子:

测试异常结果打印:

。可见追踪提供更多的异常发生信息,这些信息依次:文件全路径,行号,发生在模块还是对象,发生异常的那一行代码,异常名称,异常简单描述。因为异常的类名太多,也不太清楚一些代码会发生什么异常,使用Exception和traceback模块作为异常处理非常实用。

    上面例子中的异常,除了输入整数目前我们还不能避免发生异常,但分母不为零和平方根的底不可以小于0都可以用if语句判断避免发生异常。也许你会发问:用异常处理代码那么简单,为什么要用if语句来自行检查呢?这是编程风格的问题,一般情况异常处理耗费多点系统资源,使程序运行效率降低一些。因此,如果能通过少量代码避免产生异常,就要用代码来解决;如果少量代码无法避免的,就用异常处理。
    这样,可能产生异常的代码如果能和不产生异常代码分开,就好了。

    Python真贴心,在try-except基础上,添加了else子句,就成了try-except-else代码块,这就可以达到这效果。程序代码:

测试结果打印:

    如果不管发不发生异常,重新输入前都加个标题:码老师分数平方根计算器,那应该怎样做呢?那就用finally子句。程序修改如下:

测试结果打印:

    至此,try语句的三种子句except、else和finally都出场了,再也没有别的子句了。前面已经知道只用except子句是允许的,那只用else和finally子句会怎样呢?

    测试只用else子句,代码如下:

可见,只用else子句是不行的,只有用except子句的情况下才能用else子句。

    测试只用finally子句,代码如下:

测试结果打印:

可见,finally子句是可以单独用的。看结果打印中,即使发生异常程序要退出,退出前还是执行finally块的代码。finally子句的这种性质非常有用!finally子句代码块一般用与关闭打开的文件、断开网络连接、释放内存等资源回收操作。

    简单地说try语句的语法:except子句和finally子句至少要有一个,有except子句的情况下才能用else子句。

    用代码解决运行发生异常的另一个办法就是通过代码检测后主动抛出异常。把程序修改为:

测试结果打印:

主动抛出异常,追踪的信息中,异常类名和简述都是自定义的。虽然看起来和被动抛出异常没有太大差别,但代码的运行效率会大大提高。

练习题:把本课的测试输入电脑自己测试一次。