异常

当你的程序出现例外情况时就会发生异常(Exception)。例如,当你想要读取一个文件时,而那个文件却不存在,怎么办?又或者你在程序执行时不小心把它删除了,怎么办?这些通过使用异常来进行处理。

类似地,如果你的程序中出现了一些无效的语句该怎么办?Python 将会对此进行处理,举起(Raises)[^1]它的小手来告诉你哪里出现了一个错误(Error)

错误

你可以想象一个简单的 print 函数调用。如果我们把 print 误拼成 Print 会怎样?你会注意到它的首字母是大写。在这一例子中,Python 会抛出(Raise)一个语法错误。

  1. >>> Print("Hello World")
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. NameError: name 'Print' is not defined
  5. >>> print("Hello World")
  6. Hello World

你会注意到一个 NameError 错误被抛出,同时 Python 还会打印出检测到的错误发生的位置。这就是一个错误错误处理器(Error Handler)[^2] 为这个错误所做的事情。

异常

我们将尝试(Try)去读取用户的输入内容。按下 [ctrl-d] 来看看会发生什么事情。

  1. >>> s = input('Enter something --> ')
  2. Enter something --> Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. EOFError

此处 Python 指出了一个称作 EOFError 的错误,代表着它发现了一个文件结尾(End of File)符号(由 ctrl-d 实现)在不该出现的时候出现了。

处理异常

我们可以通过使用 try..except 来处理异常状况。一般来说我们会把通常的语句放在 try 代码块中,将我们的错误处理器代码放置在 except 代码块中。

案例(保存文 exceptions_handle.py):

  1. {% include "./programs/exceptions_handle.py" %}

输出:

  1. {% include "./programs/exceptions_handle.txt" %}

它是如何工作的

我们将所有可能引发异常或错误的语句放在 try 代码块中,并将相应的错误或异常的处理器(Handler)放在 except 子句或代码块中。except 子句可以处理某种特定的错误或异常,或者是一个在括号中列出的错误或异常。如果没有提供错误或异常的名称,它将处理所有错误与异常。

要注意到必须至少有一句 except 字句与每一句 try 字句相关联。不然,有一个 try 代码块又有什么意义?

如果没有任何错误或异常被处理,那么将调用 Python 默认处理器,它只会终端程序执行并打印出错误信息。我们已经在前面的章节里见过了这种处理方式。

你还可以拥有一个 else 子句与 try..except 代码块相关联。else 子句将在没有发生异常的时候执行。

在下一个案例中,我们还将了解如何获取异常对象以便我们可以检索其他信息。

抛出异常[^3]

你可以通过 raise 语句来引发一次异常,具体方法是提供错误名或异常名以及要抛出(Thrown)异常的对象。

你能够引发的错误或异常必须是直接或间接从属于 Exception(异常) 类的派生类。

案例(保存为 exceptions_raise.py):

  1. {% include "./programs/exceptions_raise.py" %}

输出:

  1. {% include "./programs/exceptions_raise.txt" %}

它是如何工作的

在本例中,我们创建了我们自己的异常类型。这一新的异常类型叫作 ShortInputException。它包含两个字段——获取给定输入文本长度的 length,程序期望的最小长度 atleast

except 子句中,我们提及了错误类,将该类存储 as(为) 相应的错误名或异常名。这类似于函数调用中的形参与实参。在这个特殊的 except 子句中我们使用异常对象的 lengthatleast 字段来向用户打印一条合适的信息。

Try … Finally {#try-finally}

假设你正在你的读取中读取一份文件。你应该如何确保文件对象被正确关闭,无论是否会发生异常?这可以通过 finally 块来完成。

保存该程序为 exceptions_finally.py

  1. {% include "./programs/exceptions_finally.py" %}

输出:

  1. {% include "./programs/exceptions_finally.txt" %}

它是如何工作的

我们按照通常文件读取进行操作,但是我们同时通过使用 time.sleep 函数任意在每打印一行后插入两秒休眠,使得程序运行变得缓慢(在通常情况下 Python 运行得非常快速)。当程序在处在运行过过程中时,按下 ctrl + c 来中断或取消程序。

你会注意到 KeyboardInterrupt 异常被抛出,尔后程序退出。不过,在程序退出之前,finally 子句得到执行,文件对象总会被关闭。

另外要注意到我们在 print 之后使用了 sys.stout.flush(),以便它能被立即打印到屏幕上。

with 语句 {#with}

try 块中获取资源,然后在 finally 块中释放资源是一种常见的模式。因此,还有一个 with 语句使得这一过程可以以一种干净的姿态得以完成。

保存为 exceptions_using_with.py

  1. {% include "./programs/exceptions_using_with.py" %}

它是如何工作的

程序输出的内容应与上一个案例所呈现的相同。本例的不同之处在于我们使用的是 open 函数与 with 语句——我们将关闭文件的操作交由 with open 来自动完成。

在幕后发生的事情是有一项 with 语句所使用的协议(Protocol)。它会获取由 open 语句返回的对象,在本案例中就是“thefile”。

总会在代码块开始之前调用 thefile.__enter__ 函数,并且总会在代码块执行完毕之后调用 thefile.__exit__

因此,我们在 finally 代码块中编写的代码应该格外留心 __exit__ 方法的自动操作。这能够帮助我们避免重复显式使用 try..finally 语句。

有关该话题的更多讨论已经超出了本书所能涉及的范围,因此请参考 PEP 343 来了解更加全面的解释。

总结

我们已经讨论了 try..excepttry..finally 语句的用法。同时我们也已经看到了如何创建我们自己的异常类型,还有如何抛出异常。

接下来,我们将探索 Python 的标准库。


[^1]: 在本章中 Raise 一词会经常出现,沈洁元译本大都将其译作“引发”,此处将按照具体的语境对该词的译法作出调整。

[^2]: 此处采用沈洁元译本的翻译。但是在其它教程或有关 Python 的讨论文章中,Handler 大都保留原文而不作翻译,这点需读者知悉。

[^3]: 原文作 Raising Exceptions,沈洁元译本译作“引发异常”,此处采用更流行的译法。