疯狂Python讲义-李刚编著-第7章异常处理

第7章 异常处理

Python 的异常机制主要依赖 try、except、else、finally 和 raise 五个关键字,其中在 try 关键字后缩进的代码简称 try 块,它里面放置的是可能引发异常的代码;在 except 后对应的是异常类型和一个代码块,用于表明该 except 块处理这种类型的代码块;在多个 except 块之后可以放一个 else 块,表明程序不出现异常时还要执行 else 块;最后还可以跟一个 finally 块,finally 块用于回收在 try 块里打开的物理资源,异常机制会保证 finally 块总被执行;而 raise 用于引发一个实际的异常,raise 可以单独作为语句使用,引发一个具体的异常对象。

7.1.异常概述

7.2.异常处理机制

Python 的异常处理机制可以让程序具有极好的容错性,让程序更加健壮。当程序运行出现意外情况时,系统会自动生成一个 Error 对象来通知程序,从而实现将 "业务实现代码" 和 "错误处理代码" 分离,提供更好的可读性。

7.2.1.使用 try...except 捕获异常

Python 异常处理机制的语法结构

try:
    # 业务实现代码
    ...
except(Error1, Error2, ...) as e:
    alert 输入不合法
    goto retry

如果在执行 try 块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给 Python 解释器,这个过程被称为引发异常。

当 Python 解释器收到异常对象时,会寻找能处理该异常对象的 except 块,如果找到合适的 except 块,则把该异常对象交给该 except 块处理,这个过程被称为捕获异常。如果 Python 解释器找不到捕获异常的 except 块,则运行时环境终止,Python 解释器也将退出。

提示:不管程序代码块是否处于 try 块中,甚至包括 except 块中的代码,只要执行该代码块时出现异常,系统总会自动生成一个 Error 对象。如果程序没有为这段代码定义任何的 except 块,则 Python 解释器无法找到处理该异常的 except 块,程序就在此退出。

inputStr = input("请输入您下棋的坐标,应以 x,y 的格式:\n")
while inputStr != None:
    try:
        x_str, y_str = inputStr.split(sep = ",")
        if board[int(y_str) - 1][int(x_str) - 1] != "+":
            inputStr = input("您输入的坐标点已有棋子了,请重新输入")
            continue
        board[int(y_str) - 1][int(x_str) - 1] == "."
    except Exception:
        inputStr = input("请输入的坐标不合法,请重新输入,下棋坐标应以 x,y 的格式:\n")
        continue

7.2.2.异常类的继承体系

当 Python 解释器接收到异常对象时,如何为该异常对象寻找 except 块呢?注意上面程序中 except 块的 except Exception:,这意味着每个 except 块都是专门用于处理该异常类及其子类的异常实例。

当 Python 解释器接收到异常对象后,会依次判断该异常对象是否是 except 块后的异常类或其子类的实例,如果是,Python 解释器将调用该 except 块来处理该异常;否则,再次拿该异常对象和下一个 except 块里的异常类进行比较。

Python 的所有异常类都从 BaseException 派生而来,提供了丰富的异常类,这些异常类之间有严格的继承关系。

Python 的常见异常类之间的继承关系
第1层 第2层 第3层 第4层
BaseException GeneratorExit    
  Exception ArithmeticError ZeroDivisionError
      FloatingPointError
      OverflowError  
    BufferError  
    LookupError IndexError
      KeyError
  SystemExit    
  KeyboardInterrupt     

BaseException 的主要子类就是 Exception,不管是系统的异常类,还是用户自定义的异常类,都应该从 Exception 派生。

import sys
try:
    a = int(sys.argv[1])
    b = int(sys.argv[2])
    c = a / b
    print('您输入的两个数相除的结果是:', c)
except IndexError:
    print('索引错误:运行程序时输入的参数个数不够')
except ValueError:
    print('数值错误:程序只能接收整数参数')
except ArithmeticError:
    print('算术错误')
except Exception:
    print('未知异常')

实际上,在进行异常捕获时不仅应该把 Exception 类对应的 except 块放在最后,而且所有父类异常的 except 块都应该排在子类异常的 except 块的后面(即:先处理小异常,再处理大异常)。

7.2.3.多异常捕获

Python 的一个 except 块可以捕获多种类型的异常。

在使用一个 except 块捕获多种类型的异常时,只要将多个异常类用圆括号括起来,中间用逗号隔开即可——其实就是构建多个异常类的元组。

import sys
try:
    a = int(sys.argv[1])
    b = int(sys.argv[2])
    c = a / b
    print('请输入的两个数相除的结果是:', c)
except (IndexError, ValueError, ArithmeticError):
    print('程序发生了数组越界、数字格式异常、算术异常之一')
except:
    print('未知异常')

7.2.4.访问异常信息

如果程序需要在 except 块中访问异常对象的相关信息,则可通过为异常对象声明变量来实现。当 Python 解释器决定调用某个 except 块来处理该异常对象时,会将异常对象赋值给 except 块后的异常变量,程序即可通过该变量来获得异常对象的相关信息。

所有的异常对象都包含了如下几个常用属性和方法

1.args:该属性返回异常的错误编号和描述字符串

2.errno:该属性返回异常的错误编号

3.strerror:该属性返回异常的描述字符串

4.with_traceback():通过过该方法可处理异常的传播轨迹信息

def foo():
    try:
        fis = open("a.txt")
    except Exception as e:
        print(e.args)
        print(e.errno)
        print(e.strerror)
foo()  

7.2.5.else 块

在 Python 的异常处理流程中还可添加一个 else 块,当 try 块没有出现异常时,程序会执行 else 块。

s = input('请输入除数:')
try:
    result = 20 / int(s)
    print('20除以%s的结果是:%g' % (s, result))
except ValueError:
    print('值错误,您必须输入数值')
except ArithmeticError:
    print('算术错误,您不能输入0')
else:
    print('没有出现异常')

但 Python 的异常处理使用 else 块绝不是多余的语法,当 try 块没有异常,而 else 块有异常时,就能体现出 else 块的作用了。

如果希望某段代码的异常能被后面的 except 块捕获,那么就应该将这段代码放在 try 块的代码之后;如果希望某段代码的异常能向外传播(不被 except 块捕获),那么就应该将这段代码放在 else 块中。 

7.2.6.使用 finally 回收资源

有些时候,程序在 try 块里打开了一些物理资源(例如数据库连接、网络连接和磁盘文件等)。这些物理资源都必须被显式回收。

提示:Python 的垃圾回收机制不会回收任何物理资源,只能回收堆内存中对象所占用的内存。

为了保证一定能回收在 try 块中打开的物理资源,异常处理机制提供了 finally 块。不管 try 块中的代码是否出现异常,也不管哪一个 except 块被执行,甚至在 try 块或 except 块中执行了 return 语句,finally 块总会被执行。

语法结构如下

try:
    # 业务实现代码
    ...
except SubException as e:
    # 异常处理块 1
    ...
except SubException2 as e:
    # 异常处理块 2
    ...
...
else:
    # 正常处理块
finally:
    # 资源回收块
    ...

 多个 except 块必须位于 try 块之后,finally 块必须位于所有的 except 块之后

import os

def test():
    fis = None
    try:
        fis = open("a.txt")
    except OSError as e:
        print(e.strerror)
        return
        # os._exit(1)
    finally:
        if fis is not None:
            try:
                fis.close()
            except OSError as ioe:
                print(ioe.strerror)
        print("执行 finally 块里的资源回收!")
test()


# 输出
No such file or directory
执行 finally 块里的资源回收!

如果将 except OSError as e 下的 return 语句注释掉,取消 os.exit(1) 的注释,即在异常处理的 except 块中使用 os.exit(1) 语句来退出 Python 解释器。

No such file or directory

运行结果表明 finally 块没有被执行。如果在异常处理代码中使用 os.exit(1) 语句来退出 Python 解释器,则 finally 块将失去执行的机会。

注意:除非在 try 块、except 块中调用了退出 Python 解释器的方法,否则不管在 try 块、except 块中执行怎样的代码,出现怎样的情况,异常处理的 finally 块总会被执行。调用 sys.exit(1) 方法退出程序不能阻止 finally 块的执行,这是因为 sys.exit() 方法本身就是通过引发 SystemExit 异常来退出程序的。

在通常情况下,不要在 finally 块中使用如 return 或 raise 等导致方法中止的语句,一旦在 finally 块中使用了 return 或 raise 语句,将会导致 try 块、except 块中的 return、raise 语句失效。

def test():
    try:
        return True
    finally:
        return False
a = test()
print(a)

# 输出
False

如果 Python 程序在执行 try 块、except 块时遇到了 return 或 raise 语句,这两条语句都会导致该方法立即结束,那么系统执行这两条语句并不会结束该方法,而是去寻找该异常处理流程 finally 块,如果没有找到 finally 块,程序立即执行 return 或 raise 语句,方法中止;如果找到 finally 块,系统立即开始执行 finally 块——只有当 finally 块执行完成后,系统才会再次跳回来执行 try 块,except 块里的 return 或 raise 语句;如果在 finally 块也使用了 return 或 raise 等导致方法中止的语句,finally 块已经终止了方法,系统将不会跳回去执行 try 块、except 块里的任何代码。

注意:尽量避免在 finally 块里使用 return 或 raise 等导致方法中止的语句,否则可能出现一些很奇怪的情况。  

7.2.7.异常处理嵌套

对异常处理嵌套的深度没有很明确的限制,但通常没有必要使用超过两层的嵌套异常处理,使用层次太深的嵌套异常处理没有太大必要,而且容易导致程序的可读性降低。

7.3.使用 raise 引发异常

Python 也允许程序自发引发异常,自行引发异常使用 raise 语句来完成。

7.3.1.引发异常

raise 语句有如下三种常用的用法

1.raise:单独一个 raise。该语句引发当前上下文中捕获的异常(比如在 except 块中),或默认引发 RuntimeError 异常。

2.raise 异常类:raise 后带一个异常类。该语句引发指定异常类的默认实例。

3.raise 异常对象:引发指定的异常对象。

raise 语句每次只能引发一个异常实例。

try:
    x_str, y_str = inputStr.split(sep = ",")
    if board[int(x_str) - 1][int(y_str) - 1] != "+":
        raise
    board[int(x_str) - 1][int(y_str) - 1] = "."
except Exception as e:
    print(type(e))
    inputStr = input("请输入的坐标不合法,请重新输入,下棋坐标应以 x,y 的格式\n")
    continue

代码使用 raise 语句来自行引发异常,程序认为当用户试图向一个已有旗子的坐标点下棋时就是异常。当 Python 解释器接收到开发者自行引发的异常时,同样会中止当前的执行流,跳到该异常对应的 except 块,由该 except 块来处理该异常。也就是说,不管是系统自动引发的异常,还是程序员手动引发的异常,Python 解释器对异常的处理没有任何差别。

def main():
    try:
        mtd(3)
    except Exception as e:
        print('程序出现异常:', e)
    mtd(3)
def mtd(a):
    if a > 0:
        raise ValueError("a 的值大于0,不符合要求")
main()

# 输出
程序出现异常: a 的值大于0,不符合要求
Traceback (most recent call last):
  File "D:/django/test/20200203/1421.py", line 14, in <module>
    main()
  File "D:/django/test/20200203/1421.py", line 8, in main
    mtd(3)
  File "D:/django/test/20200203/1421.py", line 12, in mtd
    raise ValueError("a 的值大于0,不符合要求")
ValueError: a 的值大于0,不符合要求

 第一次调用 mtd(3) 的结果,该方法引发的异常被 except 块捕获并处理。后面的大段输出则是第二次调用 mtd(3) 的结果,由于该异常没有被 except 块捕获,因此该异常一直向上传播,知道传给 Python 解释器导致程序中止。

提示:第二调用 mtd(3) 引发的以 "File" 开头的三行输出,其实显示的就是异常的传播轨迹信息,也就是说,如果程序不对异常进行处理,Python 默认会在控制台输出异常的传播轨迹信息。

7.3.2.自定义异常类

用户自定义异常都应该继承 Exception 基类或 Exception 的子类

class AuctionException(Exception):pass

7.3.3.except 和 raise 同时使用

在实际应用中对异常可能需要更复杂的处理方式——当一个异常出现时,单靠某个方法无法完成处理该异常,必须由几个方法协作才可完全处理该异常。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次引发异常,让该方法的调用者也能捕获异常。

为了实现这种通过多个方法协作处理同一个异常的情形,可以在 except 块中结合 raise 语句来完成。

class AuctionException(Exception):pass
class AuctionTest:
    def __init__(self, init_price):
        self.init_price = init_price
    def bid(self, bid_price):
        d = 0.0
        try:
            d = float(bid_price)
        except Exception as e:
            print("转换出异常:", e)
            # 再次引发自定义异常
            raise AuctionException("竞拍价必须是数值,不能包含其他字符!")
        if self.init_price > d:
            raise AuctionException("竞拍价比起拍价低,不允许竞拍!")
        initPrice = d
def main():
    at = AuctionTest(20.4)
    try:
        at.bid("df")
    except AuctionException as ae:
        # 再次捕获到 bid() 方法中的异常,并对异常进行处理
        print('main函数捕获的异常:', ae)
main()


# 输出
转换出异常: could not convert string to float: 'df'
main函数捕获的异常: 竞拍价必须是数值,不能包含其他字符!

代码对应的 except 块捕获到异常后,系统打印了该异常的字符串信息,接着引发一个 AuctionException 异常,通过该方法的调用者再次处理该 AuctionException 异常。所以程序中的 main() 函数,也就是 bid() 方法的调用者还可以再次捕获 AuctionException 异常,并将该异常的详细描述信息打印出来。

这种 except 和 raise 结合使用的情况在实际应用中非常常用。

实际应用对异常的处理通常分成两部分

1.应用后台需要通过日志来记录异常发生的详细情况

2.应用还需要根据异常向应用使用者传达某种提示

如果程序需要将原始异常的详细信息直接传播出去,Python 也允许用自定义异常对原始异常进行包装,修改如下

raise AuctionException(e)

上面就是把原始异常 e 包装成了 AuctionException 异常,这种方式也被称为异常包装或异常转译

7.3.4.raise 不需要参数

在使用 raise 语句时可以不带参数,此时 raise 语句处于 except 块中,它将会自动引发当前上下文激活的异常;否则,通常默认引发 RuntimeError 异常

class AuctionException(Exception):pass
class AuctionTest:
    def __init__(self, init_price):
        self.init_price = init_price
    def bid(self, bid_price):
        d = 0.0
        try:
            d = float(bid_price)
        except Exception as e:
            print("转换出异常:", e)
            # 再次引发当前激活的异常
            raise 
        if self.init_price > d:
            raise AuctionException("竞拍价比起拍价低,不允许竞拍!")
        initPrice = d
def main():
    at = AuctionTest(20.4)
    try:
        at.bid("df")
    except Exception as ae:
        # 再次捕获到 bid() 方法中的异常,并对异常进行处理
        print('main函数捕获的异常:', type(ae))
main()


# 输出
转换出异常: could not convert string to float: 'df'
main函数捕获的异常: <class 'ValueError'>

7.4.Python 的异常传播轨迹

异常对象提供了一个 with_traceback 用于处理异常的传播轨迹,查看异常的传播轨迹可追踪异常触发的源头,也可看到异常一路触发的轨迹。

class SelfException(Exception):pass
def main():
    firstMethod()
def firstMethod():
    secondMethod()
def secondMethod():
    thirdMethod()
def thirdMethod():
    raise SelfException("自定义异常信息")
main()

# 输出
Traceback (most recent call last):
  File "D:/django/test/20200203/1522.py", line 12, in <module>
    main()
  File "D:/django/test/20200203/1522.py", line 5, in main
    firstMethod()
  File "D:/django/test/20200203/1522.py", line 7, in firstMethod
    secondMethod()
  File "D:/django/test/20200203/1522.py", line 9, in secondMethod
    thirdMethod()
  File "D:/django/test/20200203/1522.py", line 11, in thirdMethod
    raise SelfException("自定义异常信息")
__main__.SelfException: 自定义异常信息

异常从 thirdMethod() 函数开始触发,传到 secondMethod() 函数,再传到 firstMethod() 函数,最后传到 main() 函数为止,这个过程就是 Python 的异常传播轨迹。

Python 专门提供了 traceback 模块来处理异常传播轨迹,使用 traceback 可以方便地处理 Python 的异常传播轨迹。

导入 traceback 模块之后,traceback 提供了如下两个常用方法

1.traceback.print_exec():将异常传播轨迹信息输出到控制台或指定文件中

2.format_exec():将异常传播轨迹信息转换成字符串

而 print_exc([limit[,file]]) 的完整形式是 print_exception(etype, value, tb[,limit[,file]])

1.etype:指定异常类型

2.value:指定异常值

3.tb:指定异常的 trackback 信息

当程序处于 except 块中时,该 except 块所捕获的异常信息可通过 sys 对象来获取,其中 sys.exc_type、sys.exc_value、sys.exc_traceback 就代表当前 except 块内的异常类型、异常值和异常传播轨迹。

简单地说,print_exc([limit[,file]]) 相当于如下

print_exception(sys.exc_type, sys.exc_value, sys.exc_tb[, limit[, file]])

4.limit:用于限制显示异常传播的层数,比如函数 A 调用函数 B,函数 B 发生了异常,如果指定 limit=1,则只显示函数 A 里面发生的异常。如果不设置 limit 参数,则默认全部显示

5.file:指定将异常传播轨迹信息输出到指定文件中。如果不指定参数,则默认输出到控制台

import traceback
class SelfException(Exception):pass

def main():
    firstMethod()
def firstMethod():
    secondMethod()
def secondMethod():
    thirdMethod()
def thirdMethod():
    raise SelfException("自定义异常信息")
try:
    main()
except:
    traceback.print_exc()
    traceback.print_exc(file=open('log.txt', 'a'))

# 输出
Traceback (most recent call last):
  File "D:/django/test/20200203/1542.py", line 15, in <module>
    main()
  File "D:/django/test/20200203/1542.py", line 7, in main
    firstMethod()
  File "D:/django/test/20200203/1542.py", line 9, in firstMethod
    secondMethod()
  File "D:/django/test/20200203/1542.py", line 11, in secondMethod
    thirdMethod()
  File "D:/django/test/20200203/1542.py", line 13, in thirdMethod
    raise SelfException("自定义异常信息")
SelfException: 自定义异常信息

7.5.异常处理规则

成功的异常处理应该实现如下4个目标:

1.使程序代码混乱最小化

2.捕获并保留诊断信息

3.通知合适的人员

4.采用合适的方式结束异常活动

7.5.1.不要过度使用异常

过度使用异常主要表现在

1.把异常和普通错误混淆在一起,不再编写任何错误处理代码,而是以简单地引发异常来代替所有的错误

2.使用异常处理来代替流程控制

7.5.2.不要使用过于庞大的 try 块

正确的做法是,把大块的 try 块分割成多个可能出现异常的程序段落,并把它们放在单独的 try 块中,从而分别捕获并处理异常。

7.5.3.不要忽略捕获到的异常

7.6.本章小结

posted on 2020-02-02 12:16  herisson_pan  阅读(20)  评论(0)    收藏  举报

导航