第六章. 异常和文件处理

在本章中,您将深入了解异常、文件对象、for 循环以及 ossys 模块。如果您在其他编程语言中使用过异常,则可以浏览第一部分以了解 Python 的语法。请务必再次收听文件处理部分。

6.1. 处理异常

与许多其他编程语言一样,Python 通过 try...except 块进行异常处理。

Note
Python 使用 try...except 处理异常,并使用 raise 生成异常。JavaC++ 使用 try...catch 处理异常,并使用 throw 生成异常。

异常在 Python 中无处不在。标准 Python 库中的几乎每个模块都使用它们,并且 Python 本身会在许多不同的情况下引发它们。在本书中,您已经多次看到它们。

在每种情况下,您都只是在 Python IDE 中玩耍:发生错误,打印异常(取决于您的 IDE,可能是有意 jarring 的红色阴影),仅此而已。这称为 未处理 异常。引发异常时,没有代码可以明确注意到它并处理它,因此它会冒泡回到 Python 中内置的默认行为,即吐出一些调试信息并放弃。在 IDE 中,这没什么大不了的,但是如果在您的实际 Python 程序运行时发生这种情况,整个程序就会突然停止。

但是,异常并不一定会导致程序完全崩溃。引发异常时,可以 处理 异常。有时,异常确实是因为您的代码中存在错误(例如访问不存在的变量),但很多时候,异常是您可以预料到的。如果您要打开一个文件,它可能不存在。如果您要连接到数据库,则数据库可能不可用,或者您可能没有正确的安全凭据来访问它。如果您知道某行代码可能会引发异常,则应使用 try...except 块处理该异常。

示例 6.1. 打开不存在的文件

>>> fsock = open("/notthere", "r")      1
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
IOError: [Errno 2] No such file or directory: '/notthere'
>>> try:
...     fsock = open("/notthere")       2
... except IOError:                     3
...     print "The file does not exist, exiting gracefully"
... print "This line will always print" 4
The file does not exist, exiting gracefully
This line will always print
1 使用内置的 open 函数,您可以尝试打开文件进行读取(有关 open 的更多信息,请参见下一节)。但是该文件不存在,因此会引发 IOError 异常。由于您没有提供对 IOError 异常的任何显式检查,因此 Python 只是打印出有关发生情况的一些调试信息,然后放弃。
2 您正在尝试打开同一个不存在的文件,但是这次您是在 try...except 块中执行此操作。
3 open 方法引发 IOError 异常时,您已准备好处理它。except IOError: 行捕获异常并执行您自己的代码块,在这种情况下,该代码块仅打印一条更友好的错误消息。
4 处理完异常后,处理将在 try...except 块之后的第一个人行上正常继续。请注意,无论是否发生异常,此行都将始终打印。如果您的根目录中确实有一个名为 notthere 的文件,则对 open 的调用将成功,except 子句将被忽略,并且此行仍将被执行。

异常可能看起来不友好(毕竟,如果您没有捕获到异常,则整个程序都会崩溃),但请考虑另一种情况。您是否希望将不可用的文件对象返回到不存在的文件?无论如何,您都需要以某种方式检查其有效性,如果忘记了,在某个时候,您的程序会在某个地方给您奇怪的错误,您需要追溯到源头。我相信您已经经历过这种情况,并且知道这并不好玩。使用异常,错误会立即发生,并且您可以使用标准方法在问题根源处处理它们。

6.1.1. 将异常用于其他目的

除了处理实际的错误情况外,异常还有许多其他用途。标准 Python 库中的一种常见用法是尝试导入模块,然后检查它是否有效。导入不存在的模块将引发 ImportError 异常。您可以使用它根据运行时可用的模块来定义多个级别的功能,或者支持多个平台(其中特定于平台的代码被分离到不同的模块中)。

您还可以通过创建一个继承自内置 Exception 类的类来定义自己的异常,然后使用 raise 命令引发异常。如果您有兴趣这样做,请参阅进一步阅读部分。

下一个示例演示了如何使用异常来支持特定于平台的功能。此代码来自 getpass 模块,该模块是一个用于从用户获取密码的包装器模块。在 UNIX、Windows 和 Mac OS 平台上获取密码的方式有所不同,但是此代码封装了所有这些差异。

示例 6.2. 支持特定于平台的功能

  # Bind the name getpass to the appropriate function
  try:
      import termios, TERMIOS                     1
  except ImportError:
      try:
          import msvcrt                           2
      except ImportError:
          try:
              from EasyDialogs import AskPassword 3
          except ImportError:
              getpass = default_getpass           4
          else:                                   5
              getpass = AskPassword
      else:
          getpass = win_getpass
  else:
      getpass = unix_getpass
1 termios 是一个特定于 UNIX 的模块,它提供对输入终端的低级控制。如果此模块不可用(因为它不在您的系统上,或者您的系统不支持它),则导入失败,并且 Python 引发 ImportError,您将捕获该错误。
2 好的,您没有 termios,所以让我们尝试 msvcrt,这是一个特定于 Windows 的模块,它提供了一个 API 来访问 Microsoft Visual C++ 运行时服务中的许多有用函数。如果此导入失败,Python 将引发 ImportError,您将捕获该错误。
3 如果前两个不起作用,请尝试从 EasyDialogs 导入函数,这是一个特定于 Mac OS 的模块,它提供弹出各种类型对话框的函数。再次,如果此导入失败,Python 将引发 ImportError,您将捕获该错误。
4 这些特定于平台的模块均不可用(这是可能的,因为 Python 已被移植到许多不同的平台上),因此您需要依靠默认的密码输入函数(在 getpass 模块的其他地方定义)。请注意您在此处执行的操作:将函数 default_getpass 分配给变量 getpass。如果您阅读了官方的 getpass 文档,它会告诉您 getpass 模块定义了一个 getpass 函数。它通过将 getpass 绑定到适合您平台的正确函数来实现此目的。然后,当您调用 getpass 函数时,您实际上是在调用此代码已为您设置的特定于平台的函数。您不需要知道或关心您的代码在哪个平台上运行 - 只需调用 getpass,它就会始终做正确的事情。
5 try...except 块可以有一个 else 子句,就像 if 语句一样。如果在 try 块期间没有引发异常,则之后将执行 else 子句。在这种情况下,这意味着 from EasyDialogs import AskPassword 导入有效,因此您应该将 getpass 绑定到 AskPassword 函数。其他每个 try...except 块都有类似的 else 子句,用于在找到有效的 import 时将 getpass 绑定到适当的函数。

有关异常处理的进一步阅读