美文网首页
《python基础教程(第三版)》第八章 异常

《python基础教程(第三版)》第八章 异常

作者: Colleen_oh | 来源:发表于2019-08-07 17:03 被阅读0次

    编写计算机程序时,通常能够区分正常和异常(不正常)情况。异常事件可能是错误(如试图除以零),也可能是通常不会发生的事情。为处理这些异常事件,可在每个可能发生这些事件的地方都使用条件语句。例如,对于每个除法运算,都检查除数是否为零。然而,这样做不效率低下、缺乏灵活性,还可能导致程序难以卒读。你可能很想忽略这些异常事件,希望它们不会发生,但Python提供功能强大的替代解决方案——异常处理机制。

    8.1 异常是什么

    Python使用异常对象来表示异常状态,并在遇到错误时引发异常。异常对象未被处理(或捕获)时,程序将终止并显示一条错误消息(traceback)。

    >>> 1 / 0
    Traceback (most recent call last):
     File "<stdin>", line 1, in ?
    ZeroDivisionError: integer division or modulo by zero 
    

    如果异常只能用来显示错误消息,就没多大意思了。但事实上,每个异常都是某个类(这里是ZeroDivisionError)的实例。你能以各种方式引发和捕获这些实例,从而逮住错误并采取措施,而不是放任整个程序失败。

    8.2 让事情沿你指定的轨道出错

    正如你看到的,出现问题时,将自动引发异常。先来看看如何自主地引发异常,还有如何创建异常,然后再学习如何处理这些异常。

    8.2.1 raise语句

    要引发异常,可使用raise语句,并将一个类(必须是Exception的子类)或实例作为参数。将类作为参数时,将自动创建一个实例。下面的示例使用的是内置异常类Exception:

    >>> raise Exception 
    Traceback (most recent call last):
     File "<stdin>", line 1, in ?
    Exception
    >>> raise Exception('hyperdrive overload')
    Traceback (most recent call last):
     File "<stdin>", line 1, in ?
    Exception: hyperdrive overload 
    

    在第一个示例(raise Exception)中,引发的是通用异常,没有指出出现了什么错误。在第二个示例中,添加了错误消息hyperdrive overload。


    8.2.2 自定义的异常类

    虽然内置异常涉及的范围很广,能够满足很多需求,但有时你可能想自己创建异常类。
    那么如何创建异常类呢?就像创建其他类一样,但务必直接或间接地继承Exception(这意味着从任何内置异常类派生都可以)。因此,自定义异常类的代码类似于下面这样:

    class SomeCustomException(Exception): pass 
    

    8.3 捕获异常

    前面说过,异常比较有趣的地方是可对其进行处理,通常称之为捕获异常。为此,可使用try/except语句。假设你创建了一个程序,让用户输入两个数,再将它们相除,如下所示:

    x = int(input('Enter the first number: '))
    y = int(input('Enter the second number: '))
    print(x / y) 
    

    这个程序运行正常,直到用户输入的第二个数为零。

    Enter the first number: 10
    Enter the second number: 0
    Traceback (most recent call last):
     File "exceptions.py", line 3, in ?
     print(x / y)
    ZeroDivisionError: integer division or modulo by zero 
    

    为捕获这种异常并对错误进行处理(这里只是打印一条对用户更友好的错误消息),可像下
    面这样重写这个程序:

    try:
       x = int(input('Enter the first number: '))
       y = int(input('Enter the second number: '))
       print(x / y)
    except ZeroDivisionError:
       print("The second number can't be zero!") 
    

    8.3.1 不用提供参数

    捕获异常后,如果要重新引发它(即继续向上传播),可调用raise且不提供任何参数。
    为说明这很有用,来看一个能够“抑制”异常ZeroDivisionError的计算器类。如果启用了这种功能,计算器将打印一条错误消息,而不让异常继续传播。在与用户交互的会话中使用这计算器时,抑制异常很有用;但在程序内部使用时,引发异常是更佳的选择(此时应关闭“抑制”功能)。下面是这样一个类的代码:

    class MuffledCalculator:
       muffled = False
       def calc(self, expr):
           try:
                return eval(expr)
           except ZeroDivisionError:
                if self.muffled:
                     print('Division by zero is illegal')
                else:
                     raise
    

    下面的示例演示了这个类的用法(包括启用和关闭了抑制功能的情形):

    >>> calculator = MuffledCalculator()
    >>> calculator.calc('10 / 2')
    5.0
    >>> calculator.calc('10 / 0') # 关闭了抑制功能
    Traceback (most recent call last): File "<stdin>", line 1, in ?
     File "MuffledCalculator.py", line 6, in calc
     return eval(expr)
     File "<string>", line 0, in ?
    ZeroDivisionError: integer division or modulo by zero
    >>> calculator.muffled = True
    >>> calculator.calc('10 / 0')
    Division by zero is illegal
    

    如你所见,关闭抑制功能时,捕获了异常ZeroDivisionError,但继续向上传播它。
    如果无法处理异常,在except子句中使用不带参数的raise通常是不错的选择,但有时你可能想引发别的异常。在这种情况下,导致进入except子句的异常将被作为异常上下文存储来,并出现在最终的错误消息中,如下所示:

    >>> try:
    ... 1/0
    ... except ZeroDivisionError:
    ... raise ValueError
    ...
    Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
    ZeroDivisionError: division by zero 
    

    在处理上述异常时,引发了另一个异常:

    Traceback (most recent call last):
     File "<stdin>", line 4, in <module>
    ValueError
    

    你可使用raise ... from ...语句来提供自己的异常上下文,也可使用None来禁用上下文。

    >>> try:
    ... 1/0
    ... except ZeroDivisionError:
    ... raise ValueError from None
    ...
    Traceback (most recent call last):
     File "<stdin>", line 4, in <module>
    ValueError
    

    8.3.2 多个except子句

    如果你运行前一节的程序,并在提示时输入一个非数字值,将引发另一种异常。

    Enter the first number: 10
    Enter the second number: "Hello, world!"
    Traceback (most recent call last):
     File "exceptions.py", line 4, in ?
     print(x / y)
    TypeError: unsupported operand type(s) for /: 'int' and 'str' 
    

    由于该程序中的except子句只捕获ZeroDivisionError异常,这种异常将成为漏网之鱼,导致
    程序终止。为同时捕获这种异常,可在try/except语句中再添加一个except子句。

    try:
       x = int(input('Enter the first number: '))
       y = int(input('Enter the second number: '))
       print(x / y)
    except ZeroDivisionError:
       print("The second number can't be zero!")
    except TypeError:
       print("That wasn't a number, was it?") 
    

    8.3.3 一箭双雕

    如果要使用一个except子句捕获多种异常,可在一个元组中指定这些异常,如下所示:

    try:
       x = int(input('Enter the first number: '))
       y = int(input('Enter the second number: '))
       print(x / y)
    except (ZeroDivisionError, TypeError, NameError):
       print('Your numbers were bogus ...') 
    

    在except子句中,异常两边的圆括号很重要。一种常见的错误是省略这些括号,这可能导致你不想要的结果。

    8.3.4 捕获对象

    要在except子句中访问异常对象本身,可使用两个而不是一个参数。(请注意,即便是在你捕获多个异常时,也只向except提供了一个参数——一个元组。)需要让程序继续运行并录错误(可能只是向用户显示)时,这很有用。下面的示例程序打印发生的异常并继续运行:

    try:
       x = int(input('Enter the first number: '))
       y = int(input('Enter the second number: '))
       print(x / y)
    except (ZeroDivisionError, TypeError) as e:
       print(e) 
    

    在这个小程序中,except子句也捕获两种异常,但由于你同时显式地捕获了对象本身,因此可将其打印出来,让用户知道发生了什么情况。

    8.3.5 一网打尽

    如果你就是要使用一段代码捕获所有的异常,只需在except语句中不指定任何异常类即可。

    try:
        x = int(input('Enter the first number: '))
        y = int(input('Enter the second number: '))
        print(x / y)
    except:
        print('Something wrong happened ...') 
    

    8.3.6 万事大吉时

    在有些情况下,在没有出现异常时执行一个代码块很有用。为此,可像条件语句和循环一样,给try/except语句添加一个else子句。

    >>> try:
    ...  print('A simple task')
    ... except:
    ...  print('What? Something went wrong?')
    ... else:
    ...  print('Ah ... It went as planned.')
    ...
    A simple task
    Ah ... It went as planned.
    

    8.3.7 最后

    最后,还有finally子句,可用于在发生异常时执行清理工作。这个子句是与try子句配套的。

    x = None
    try:
        x = 1 / 0
    finally:
        print('Cleaning up ...')
        del x 
    

    在上述示例中,不管try子句中发生什么异常,都将执行finally子句。为何在try子句之前初始化x呢?因为如果不这样做,ZeroDivisionError将导致根本没有机会给它赋值,进而导在finally子句中对其执行del时引发未捕获的异常。
    如果运行这个程序,它将在执行清理工作后崩溃。

    Cleaning up ...
    Traceback (most recent call last):
     File "C:\python\div.py", line 4, in ?
     x = 1 / 0
    ZeroDivisionError: integer division or modulo by zero 
    

    虽然使用del来删除变量是相当愚蠢的清理措施,但finally子句非常适合用于确保文件或网络套接字等得以关闭。
    也可在一条语句中同时包含try、except、finally和else(或其中的3个)。

    try:
        1 / 0
    except NameError:
        print("Unknown variable")
    else:
        print("That went well!")
    finally:
        print("Cleaning up.") 
    

    8.4 异常和函数

    异常和函数有着天然的联系。如果不处理函数中引发的异常,它将向上传播到调用函数的地方。如果在那里也未得到处理,异常将继续传播,直至到达主程序(全局作用域)。如果程序中也没有异常处理程序,程序将终止并显示栈跟踪消息。来看一个示例:

    >>> def faulty():
    ...      raise Exception('Something is wrong')
    ...
    >>> def ignore_exception():
    ...      faulty()
    ...
    >>> def handle_exception():
    ...    try:
    ...       faulty()
    ...    except:
    ...       print('Exception handled')
    ...
    >>> ignore_exception()
    Traceback (most recent call last):
     File '<stdin>', line 1, in ?
     File '<stdin>', line 2, in ignore_exception
     File '<stdin>', line 2, in faulty
    Exception: Something is wrong
    >>> handle_exception()
    Exception handled
    

    如你所见,faulty中引发的异常依次从faulty和ignore_exception向外传播,最终导致显示一条栈跟踪消息。调用handle_exception时,异常最终传播到handle_exception,并被这的try/except语句处理。

    8.5 异常之禅

    假设有一个字典,你要在指定的键存在时打印与之相关联的值,否则什么都不做。实现这种功能的代码可能类似于下面这样:

    def describe_person(person):
          print('Description of', person['name'])
          print('Age:', person['age'])
          if 'occupation' in person:
                 print('Occupation:', person['occupation']) 
    

    如果你调用这个函数,并向它提供一个包含姓名Throatwobbler Mangrove和年龄42(但不包含职业)的字典,输出将如下:

    Description of Throatwobbler Mangrove
    Age: 42 
    

    如果你在这个字典中添加职业camper,输出将如下:

    Description of Throatwobbler Mangrove
    Age: 42
    Occupation: camper 
    

    这段代码很直观,但效率不高(虽然这里的重点是代码简洁),因为它必须两次查找'occupation'键:一次检查这个键是否存在(在条件中),另一次获取这个键关联的值,便将其打印出来。下面是另一种解决方案:

    def describe_person(person):
          print('Description of', person['name'])
          print('Age:', person['age'])
          try:
              print('Occupation:', person['occupation'])
          except KeyError: pass 
    

    8.6 不那么异常的情况

    如果你只想发出警告,指出情况偏离了正轨,可使用模块warnings中的函数warn。

    >>> from warnings import warn
    >>> warn("I've got a bad feeling about this.")
    __main__:1: UserWarning: I've got a bad feeling about this.
    >>> 
    

    警告只显示一次。如果再次运行最后一行代码,什么事情都不会发生。
    如果其他代码在使用你的模块,可使用模块warnings中的函数filterwarnings来抑制你发出的警告(或特定类型的警告),并指定要采取的措施,如"error"或"ignore"。

    >>> from warnings import filterwarnings
    >>> filterwarnings("ignore")
    >>> warn("Anyone out there?")
    >>> filterwarnings("error")
    >>> warn("Something is very wrong!")
    Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
    UserWarning: Something is very wrong! 
    

    如你所见,引发的异常为UserWarning。发出警告时,可指定将引发的异常(即警告类别),但必须是Warning的子类。如果将警告转换为错误,将使用你指定的异常。另外,可根据异常来过滤掉特定类型的警告。

    >>> filterwarnings("error")
    >>> warn("This function is really old...", DeprecationWarning)
    Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
    DeprecationWarning: This function is really old...
    >>> filterwarnings("ignore", category=DeprecationWarning)
    >>> warn("Another deprecation warning.", DeprecationWarning)
    >>> warn("Something else.")
    Traceback (most recent call last):
     File "<stdin>", line 1, in <m
    

    相关文章

      网友评论

          本文标题:《python基础教程(第三版)》第八章 异常

          本文链接:https://www.haomeiwen.com/subject/jopodctx.html