错误和异常

异常中断代码块的正常控制流程以便处理 错误其他异常条件 。它是 Python 中的一类对象。一个异常表示一个或一类错误。

异常会在错误被检测到的位置引发,它可以被 当前包围发生错误的代码块 或是 任何直接或间接调用发生错误的代码块的其他代码块 所处理。

Python 解析器会在检测到代码运行错误的时候引发异常。也可以通过 raise 语句显式地引发异常。

异常处理通过 try 语句 来指定。该语句的 finally 子句可被用来指定清理代码,它并不处理异常,而是无论之前的代码是否发生异常都会被执行。

Python 的错误处理采用的是 “终止” 模型:异常处理器可以找出发生了什么问题,并在外层继续执行,但它不能修复错误的根源并重试失败的操作(除非通过从顶层重新进入出错的代码片段)。

当一个异常完全未被处理时,解释器会终止程序的执行,或者返回交互模式。无论是哪种情况,它都会打印栈回溯信息,除非是当异常为 SystemExit 的时候。

异常是通过类实例来标识的。except 子句必须引用实例的类或是其所属的基类。实例可通过处理器被接收,并可携带有关异常条件的附加信息。

在 Python 中,所有异常必须为一个派生自 BaseException 的类的实例。

内置异常类可以被子类化以定义新的异常。鼓励从 Exception 类或它的某个子类而不是从 BaseException 来派生新的异常。

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

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

处理异常

异常处理通过 try 语句(详细语法及使用规则见 try 语句)来实现。

通过 try 语句处理程序已经出现或可能出现异常,使代码能够继续执行,否则异常未被处理,程序终止执行。

例如下列代码要求用户输入有效的整数:

while True:
    try:
        x = int(input("Please enter a number: "))
        break
    except ValueError:
        print("Oops! That was no valid number. Try again...")
Please enter a number:  3.14


Oops! That was no valid number. Try again...


Please enter a number:  314

try 语句的工作原理如下:

  • 首先执行 try 子句;

  • 如果没有异常发生,则跳过 except 子句并完成 try 语句的执行;

  • 如果在执行 try 子句时发生了异常,则跳过该子句中剩下的部分。然后,如果异常的类型和 except 关键字后面的异常匹配,则执行 except 子句,然后继续执行 try 语句之后的代码;

  • 如果发生的异常和 except 子句中指定的异常不匹配,则将其传递到外部的 try 语句中;如果没有找到处理程序,则它是一个未处理异常,执行将停止并显示异常信息。

一个 try 语句可能有多个 except 子句,以指定不同异常的处理程序,但最多会执行一个处理程序。

处理程序只处理相应的 try 子句中发生的异常。如果发生的异常和 except 子句中的类是同一个类或者是它的基类,则异常和 except 子句中的类是兼容的。如果首先处理了基类,子类不再被处理(子类也被基类处理了):

class B(Exception):
    pass

class C(B):
    pass

class D(C):
    pass

for cls in [B, C, D]:
    try:
        raise cls()
    except D:
        print("D")
    except C:
        print("C")
    except B:
        print("B")
B
C
D
class B(Exception):
    pass

class C(B):
    pass

class D(C):
    pass

for cls in [B, C, D]:
    try:
        raise cls()
    except B: # 下列都是 B 的子类,不再被处理
        print("B")
    except C:
        print("C")
    except D:
        print("D")
B
B
B

最后的 except 子句可以省略异常名,以用作通配符。但请谨慎使用,因为以这种方式很容易掩盖真正的编程错误!它还可用于打印错误消息,然后重新引发异常:

import sys

try:
    3/0
except ValueError as v:
    print(v)
except:
    print("Unexpected error:", sys.exc_info()[0])
    raise
Unexpected error: <class 'ZeroDivisionError'>


---------------------------------------------------------------------------

ZeroDivisionError                         Traceback (most recent call last)

<ipython-input-13-f0b3e840c5ab> in <module>
      2 
      3 try:
----> 4     3/0
      5 except ValueError as v:
      6     print(v)


ZeroDivisionError: division by zero

可选的 else 子句,在使用时必须放在所有的 except 子句后面。对于在 try 子句不引发异常时必须执行的代码来说很有用:

try:
    f = open('../11_built-in_function/test.txt',
             'r',encoding='utf-8')
except OSError:
    print('cannot open')
else:
    print(len(f.readlines()), 'lines')
    f.close()
3 lines

异常处理程序不仅处理 try 子句中遇到的异常,还处理 try 子句中调用(即使是间接地)的函数内部发生的异常。

如果 try 子句中有多个可能的异常,只处理最先引发的:

def f():
    return 3/0

try:
    f() * int('a')
except ValueError as v:
    print(v)
except  ZeroDivisionError as z:
    print(z)
division by zero

抛出异常

raise 语句 允许强制发生指定的异常。

raise NameError('HiThere')
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

<ipython-input-1-72c183edb298> in <module>
----> 1 raise NameError('HiThere')


NameError: HiThere

如果你需要确定是否引发了异常但不打算处理它,则可以使用更简单的 raise 语句形式重新引发异常:

try:
    raise NameError('HiThere')
except NameError:
    print('An exception flew by!')
    raise
An exception flew by!


---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

<ipython-input-2-bf6ef4926f8c> in <module>
      1 try:
----> 2     raise NameError('HiThere')
      3 except NameError:
      4     print('An exception flew by!')
      5     raise


NameError: HiThere

自定义异常

可以通过创建新的异常类来自定义代码执行错误引发的异常。许多标准模块定义了它们自己的异常,以报告它们定义的函数中可能出现的错误。

异常通常应该直接或间接地从 Exception 类派生。定义的异常类,可以执行任何其他类可以执行的任何操作,但通常保持简单,只提供一些属性,这 些属性允许处理程序为异常提取有关错误的信息。

在创建可能引发多个不同错误的模块时,通常的做法是为该模块定义的异常创建基类,并为不同错误条件创建特定异常类的子类。大多数异常都定义为名称以 Error 结尾,类似于标准异常的命名:

class Error(Exception):
    """Base class for exceptions in this module."""
    pass

class InputError(Error):
    """Exception raised for errors in the input.
    
    Attributes:
        expression -- input expression in which the error occurred
        message -- explanation of the error
    """
    
    def __init__(self, expression, message):
        self.expression = expression
        self.message = message
    
class TransitionError(Error):
    """Raised when an operation attempts a state transition that's not allowed.
    
    Attributes:
        previous -- state at beginning of transition
        next -- attempted new state
        message -- explanation of why the specific transition is not allowed
    """
    
    def __init__(self, previous, next, message):
        self.previous = previous
        self.next = next
        self.message = message
        
raise InputError(3/1,'分母不能为 1')
---------------------------------------------------------------------------

InputError                                Traceback (most recent call last)

<ipython-input-2-d0c6938bc1c4> in <module>
     29         self.message = message
     30 
---> 31 raise InputError(3/1,'分母不能为 1')


InputError: (3.0, '分母不能为 1')

finally 清理操作

try 语句有另一个可选子句 finally,用于定义必须在所有情况下执行的清理操作(详见 try 语句)。

例如,在 finally 子句中关闭打开的文件:

def read_file():
    try:
        f = open('../11_built-in_function/test.txt',
                encoding='utf-8')
        return f.read()
    except OSError:
        print('不能打开')
    finally:
        print('执行清理操作')
        f.close()
        
read_file() # 先执行关闭,在执行返回
执行清理操作


'xue.cn\n\n自学是门手艺'

如果 finally 子句中引发了新的异常,清理操作本身无效,则达不到清理目的:

def read_file():
    try:
        f = open('../11_built-in_function/test.txt',
                encoding='utf-8')
        return f.read()
    except OSError:
        print('不能打开')
    finally:
        print(执行清理操作) # 清理操作引发异常
        f.close()
        
read_file() 
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

<ipython-input-13-af7cdb6d88f0> in <module>
     10         f.close()
     11 
---> 12 read_file() # 先执行关闭,在执行返回


<ipython-input-13-af7cdb6d88f0> in read_file()
      7         print('不能打开')
      8     finally:
----> 9         print(执行清理操作) # 清理操作引发异常
     10         f.close()
     11 


NameError: name '执行清理操作' is not defined

某些对象(例如文件对象)定义了在不再需要该对象时,要执行的标准清理操作,无论使用该对象的操作是成功还是失败,清理操作都会被执行。此时使用 with 语句允许像文件这样的对象能够以一种确保它们得到及时和正确的清理的方式使用。

with 语句相当于将引发异常情况下的清理操作放到了 except 子句中,正常情况下的清理操作放到了 finally 子句中。详情见 with 语句

def read_file():
    with open('../11_built-in_function/test.txt',
                encoding='utf-8') as f:
        return f.read()
    
read_file()
'xue.cn\n\n自学是门手艺'