# 异常

# 异常类型

  • C 语言程序运行时,可能在终端打印一些报错信息。而 Python 程序运行时,将各种报错信息,表示为异常(exception)的形式。

  • Python 内置了几十种异常。每种异常,由一个 class 定义。

    • 常见的一些异常:
      - BaseException         # 这是所有异常类的祖先类
        - SystemExit          # 关于系统的严重异常
        - KeyboardInterrupt   # 当用户通过键盘输入 Ctrl + C 打断 Python 程序运行时,会抛出这种异常
        - Exception           # 普通的异常
          - AssertionError
          - AttributeError    # 访问对象的一个属性时,如果该属性不存在,则抛出这种异常
          - ImportError       # 用关键字 import 导入一个 Python 模块时,如果导入失败,则抛出这种异常
          - KeyError          # 访问 dict 对象中的一个 key 时,如果该 key 不存在,则抛出这种异常
          - NameError         # 如果 Python 代码中访问了一个标识符,但当前作用域中没有找到该标识符,则抛出这种异常
          - OSError           # 表示与操作系统相关的异常,例如 FileNotFoundError 是 OSError 的子类
          - RuntimeError      # 表示 Python 运行时的一般错误。如果用户想创建一个异常,但不属于 NameError 等已有的异常类型,则可以粗略地归类为 RuntimeError
          - SyntaxError       # 语法错误
          - SystemError       # 表示 Python 解释器的内部错误
          - TypeError         # 如果某个对象的类型错误,则抛出这种异常。例如期望某个变量为 int 类型,实际发现为 str 类型
          - ValueError        # 如果某个对象的取值错误,则抛出这种异常
          - Warning           # 表示警告,不是严重的报错
            - DeprecationWarning  # 表示某个功能被弃用
            - RuntimeWarning      # 表示 Python 运行时的一般警告
      
    • 例:
      >>> NameError           # 这是一种异常类型,或者说一种异常名称
      <class 'NameError'>
      >>> NameError.mro()     # 查看这种异常的祖先类
      [<class 'NameError'>, <class 'Exception'>, <class 'BaseException'>, <class 'object'>]
      >>> NameError()         # 这是一个 NameError 类型的异常,或者说 NameError 类的一个实例
      NameError()
      
  • 用户可以自己定义一个异常类,如下:

    >>> class MyException(Exception):   # 建议继承 Exception 类
    ...     def __init__(self, msg=''):
    ...         self.msg = msg
    ...     def __repr__(self):
    ...         return 'MyException({})'.format(repr(self.msg))
    ...     def __str__(self):
    ...         return self.__repr__()
    ...
    >>> raise MyException('test')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    __main__.MyException: MyException('test')
    

# 抛出异常

# raise

  • 用关键字 raise 可以产生一个异常,又称为抛出异常。

  • 抛出异常的影响:

    • 打开 Python 交互式终端时,如果抛出一个异常,则会在终端打印报错信息。例:
      >>> a                         # 在终端输入一个未定义的变量,就会产生异常
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
      NameError: name 'a' is not defined
      >>> raise NameError('test')   # 这是创建 NameError 类型异常的一个实例,传入一个字符串作为描述,然后用 raise 抛出
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
      NameError: test
      
      • 如果抛出的异常,属于 SystemExit 类型,则打印报错信息之后,还会导致 Python 终端退出。
    • 运行 Python 脚本时,如果抛出异常到全局作用域,则会导致 Python 脚本退出。
      • 为了让 Python 脚本保持运行,
        • 用户可以捕捉异常,避免它被抛到全局作用域。
        • 用户可以在 Python 脚本退出之后,重新启动 Python 脚本。

# assert

  • 用关键字 assert 可以执行一个条件表达式,如果表达式结果为 False ,则抛出 AssertionError 异常。这种语法称为断言。
  • 例:
    >>> assert 1 == 0
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AssertionError
    >>> assert 1 == 0, 'not equal'        # 可以给该异常,添加字符串描述
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AssertionError: not equal
    

# 捕捉异常

# except

  • 用关键字 except 可以捕捉异常。

    • 语法格式如下:
      try:          # 有且仅有一个 try 语句块,用于尝试执行一段代码
          ...
      except ...:   # 可以输入任意个 except 语句块,用于处理某些异常
          ...
      else:
          ...
      finally:
          ...
      
    • 运行逻辑:
      • 首先执行 try 语句块,如果产生了异常,则终止执行 try 语句块,然后,
        • 如果当前异常类型,属于某个 except 指定的异常类或其子孙类,则执行该 except 语句块。
        • 如果存在多个匹配的 except 子句,则只执行最先出现的那个 except 语句块。
        • 如果不存在匹配的 except 子句,则抛出当前异常。(从 try 语句块,抛到 try 之外的语句块)
      • 在 try-except 语句块之后,可选添加一个 else 语句块。如果 try 语句块没有产生异常,则会执行 else 语句块。
  • 例:

    >>> try:
    ...     1 + 'A'
    ... except Exception as e:  # 捕捉一个 Exception 类型的异常,用关键字 as 赋值给变量 e
    ...     print(type(e))      # 通过变量 e ,可以知道当前异常的具体信息
    ...     print(str(e))
    ...
    <class 'TypeError'>
    unsupported operand type(s) for +: 'int' and 'str'
    
    • Exception 是大部分类型的异常的祖先类。因此,如果在 except 语句块中指定 Exception 类,则可以匹配大部分类型的异常。
    • 实践中,建议不指定 Exception 类,而是指定一些具体的异常类型,从而准确地捕捉异常。
  • 例:

    >>> import sys
    >>> try:
    ...     1 + 'A'
    ... except TypeError:             # 捕捉异常时,可以不用关键字 as 赋值给变量 e
    ...     print(sys.exc_info()[0])  # 此时没有变量 e ,调用 sys.exc_info() 才能知道当前异常的具体信息,该函数会返回一个三元组
    ...     print(sys.exc_info()[1])
    ...     print(sys.exc_info()[2])
    ...
    <class 'TypeError'>                                 # 异常的类型
    unsupported operand type(s) for +: 'int' and 'str'  # 异常的字符串描述
    <traceback object at 0x000001B326D554C0>            # 异常实例
    
  • 例:

    >>> import sys
    >>> try:
    ...     1 + 'A'
    ... except (NameError, KeyError) as e:  # 同一个 except 语句中,可以尝试捕捉多种异常
    ...     print(type(e))
    ...     print(str(e))
    ... except:                             # 最后一个 except 语句,可以不指定异常类型,从而匹配所有类型的异常
    ...     print('Unexpected error:', sys.exc_info()[0], sys.exc_info()[1])
    ...
    Unexpected error: <class 'TypeError'> unsupported operand type(s) for +: 'int' and 'str'
    
  • 在 except 语句块中使用 raise :

    • 直接执行 raise ,会报错说没有指定异常:
      >>> raise
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
      RuntimeError: No active exception to reraise
      
    • 在 except 语句块中,可以直接执行 raise ,这会将当前捕捉的异常,重新抛出:
      >>> try:
      ...     1 + 'A'
      ... except:
      ...     raise
      ...
      Traceback (most recent call last):
        File "<stdin>", line 2, in <module>
      TypeError: unsupported operand type(s) for +: 'int' and 'str'
      
    • 在 except 语句块中,已经捕捉了一个异常。如果用 raise 再抛出一个异常,则同时存在两个异常。这些异常被 Python 解释器暂存在 traceback 堆栈中,称为异常链。
      >>> try:
      ...     1 + 'A'
      ... except:
      ...     raise RuntimeError('an error')
      ...
      Traceback (most recent call last):
        File "<stdin>", line 2, in <module>
      TypeError: unsupported operand type(s) for +: 'int' and 'str'
      
      During handling of the above exception, another exception occurred:
      
      Traceback (most recent call last):
        File "<stdin>", line 4, in <module>
      RuntimeError: an error
      
    • 可用 raise ... from ... 的语法,表示当前异常,是由某个异常造成的。
      >>> try:
      ...     1 + 'A'
      ... except TypeError as e:
      ...     raise RuntimeError('an error') from e
      ...
      Traceback (most recent call last):
        File "<stdin>", line 2, in <module>
      TypeError: unsupported operand type(s) for +: 'int' and 'str'
      
      The above exception was the direct cause of the following exception:    # 这里说明了因果关系
      
      Traceback (most recent call last):
        File "<stdin>", line 4, in <module>
      RuntimeError: an error
      
    • 可用 raise ... from None 的语法,删除已经产生的异常,只留下当前异常。
      >>> try:
      ...     1 + 'A'
      ... except TypeError as e:
      ...     raise RuntimeError('an error') from None
      ...
      Traceback (most recent call last):
        File "<stdin>", line 4, in <module>
      RuntimeError: an error
      

# finally

  • 在 try-except 语句块之后,可选添加一个 finally 语句块。不管 try-except 语句块是否产生异常,都会执行 finally 语句块。
  • finally 语句块的优先级很高,即使在 try-except 语句块中执行了 break、continue、return 语句来退出,也会在退出之前,先执行 finally 语句块。
    >>> def fun1():
    ...     try:
    ...         print('try start')
    ...         raise RuntimeError('try error')   # 执行到这里时,跳转到 except 语句块
    ...         return 'try return'
    ...         print('try end')
    ...     except:
    ...         print('except start')
    ...         raise RuntimeError('except error')
    ...         return 'except return'            # 执行到这里时,跳转到 finally 语句块
    ...         print('except end')
    ...     finally:
    ...         return 'finally return'
    ...
    >>> fun1()
    try start
    except start
    'finally return'
    

# with

  • 使用关键字 finally ,可以不管是否产生异常,都保证在最后执行一段代码,比如清理现场、记录日志。
  • 使用关键字 with ,也可以实现这种效果:创建某个类的一个实例,对该实例进行任意操作,最后保证对该实例执行清理操作。
    • 前提条件:被关键字 with 操作的这个类,必须实现了魔法方法 __enter__()__exit__()
    • 例:
      class Test:
          def __init__(self, filename):
              self.filename = filename
          def __enter__(self):
              self.f = open(self.filename, self.mode)
              return self.f
          def __exit__(self, name, value, trace): # 产生异常时,会调用该方法,并传入异常名称、异常内容、traceback 对象
              self.f.close()
      
      with Test('1.txt') as t:
          t.read()
      
      • Python 解释器执行 with Test('1.txt') as t时,会先创建对象 t = Test('1.txt') ,然后自动调用 t.__enter__()
      • 当 with 语句块执行完毕,或者抛出异常时,都会自动调用 t.__exit__()