Python 1.5 中的标准异常类
Python 1.5 中的标准异常类
(已更新至 Python 1.5.2 -baw)用户定义的 Python 异常可以是字符串或 Python 类。由于类在用作异常时具有许多优良特性,因此期望迁移到仅使用类的情况。在 Python 1.5 alpha 4 之前,Python 的标准异常(IOError、TypeError 等)被定义为字符串。将这些异常更改为类带来了一些特别棘手的向后兼容性问题。
在 Python 1.5 及更高版本中,标准异常是 Python 类,并且添加了一些新的标准异常。废弃的 AccessError 异常已被删除。由于此更改可能会(尽管不太可能)破坏现有代码,因此可以使用命令行选项 -X
调用 Python 解释器以禁用此功能,并像以前一样使用字符串异常。此选项是临时措施——最终基于字符串的标准异常将从语言中完全删除。Python 2.0 中是否允许用户定义的字符串异常尚未决定。
标准异常层次结构
请看标准异常层次结构。它在新的标准库模块 exceptions.py 中定义。Python 1.5 以来新增的异常标有 (*)。
Exception(*) | +-- SystemExit +-- StandardError(*) | +-- KeyboardInterrupt +-- ImportError +-- EnvironmentError(*) | | | +-- IOError | +-- OSError(*) | +-- EOFError +-- RuntimeError | | | +-- NotImplementedError(*) | +-- NameError +-- AttributeError +-- SyntaxError +-- TypeError +-- AssertionError +-- LookupError(*) | | | +-- IndexError | +-- KeyError | +-- ArithmeticError(*) | | | +-- OverflowError | +-- ZeroDivisionError | +-- FloatingPointError | +-- ValueError +-- SystemError +-- MemoryError
所有异常的根类是新异常 Exception。由此派生出另外两个类:StandardError,它是所有标准异常的根类;以及 SystemExit。建议新代码中用户定义的异常派生自 Exception,尽管出于向后兼容性原因,这不是必需的。最终此规则将收紧。
SystemExit 派生自 Exception,因为它虽然是异常,但它不是错误。
大多数标准异常都是 StandardError 的直接后代。一些相关的异常使用从 StandardError 派生的中间类进行分组;这使得可以在一个 except 子句中捕获多个不同的异常,而无需使用元组表示法。
我们曾考虑引入更多相关的异常组,但未能决定最佳分组方式。在像 Python 这样动态的语言中,很难说 TypeError 是“程序错误”、“运行时错误”还是“环境错误”,所以我们决定不予决定。可以说 NameError 和 AttributeError 应该派生自 LookupError,但这是有争议的,并且完全取决于应用程序。
异常类定义
标准异常的 Python 类定义是从标准模块“exceptions”导入的。您无法通过修改此文件来使其更改自动显示在标准异常中;内置模块期望 exceptions.py 中定义的当前层次结构。
有关标准异常类的详细信息可在 Python 库参考手册中关于 exceptions 模块的条目中找到。
对 raise
的更改
raise
语句已扩展,允许在不显式实例化的情况下引发类异常。允许以下形式,称为 raise
语句的“兼容性形式”:
raise
异常raise
异常, 参数raise
异常, (参数, 参数, ...)
当 异常 是一个类时,这些等同于以下形式:
raise
异常()raise
异常(参数)raise
异常(参数, 参数, ...)
请注意,这些都是以下形式的示例
raise
实例
这本身就是以下形式的简写:
raise
类, 实例
其中 类 是 实例 所属的类。在 Python 1.4 中,只允许以下形式:
raise
类, 实例 和raise
实例
在 Python 1.5(从 1.5a1 开始)中,添加了以下形式:
raise
类 和raise
类, 参数(s)
字符串异常的允许形式保持不变。
出于各种原因,将 None 作为 raise 的第二个参数传递等同于省略它。特别是,语句
raise
类,None
等同于
raise
类()
而不是
raise
类(None
)
同样,语句
raise
类, 值
其中 值 恰好是一个元组,等同于将元组的项作为单个参数传递给类构造函数,而不是将 值 作为单个参数传递(空元组调用不带参数的构造函数)。这会产生差异,因为 f(a, b)
和 f((a, b))
之间存在差异。
这些都是折衷方案——它们与标准异常通常采用的参数类型(例如简单字符串)配合得很好。为了在新代码中清晰明了,建议采用以下形式:
raise
类(参数, ...)
(即,显式调用构造函数)。
这有什么帮助?
引入兼容性形式的动机是为了允许向后兼容引发标准异常的旧代码。例如,一个 __getattr__ 钩子可能会在所需属性未定义时调用以下语句:
raise AttributeError
, attrname
使用新的类异常,应引发的正确异常是 AttributeError
(attrname);兼容性形式确保旧代码不会中断。(事实上,希望与 -X 选项兼容的新代码 必须 使用兼容性形式,但强烈不鼓励这样做。)
(事实上,希望与 -X 选项兼容的新代码 必须 使用兼容性形式,但强烈不鼓励这样做。)
对 except
的更改
对 try
语句的 except
子句没有进行任何用户可见的更改。
在内部,发生了许多变化。例如,从 C 语言引发的类异常在捕获时实例化,而不是在引发时实例化。这是一种性能优化,以便完全在 C 语言中引发和捕获的异常无需承担实例化成本。例如,在 for
语句中遍历列表时,列表对象会在列表末尾引发 IndexError,但该异常在 C 语言中被捕获,因此从未实例化。
可能会出现什么问题?
新设计尽力不破坏旧代码,但在某些情况下,为了避免破坏代码,不值得妥协新语义。换句话说,一些旧代码可能会被破坏。这就是为什么有 -X 开关;然而,这不应该成为不修复代码的借口。
有两种类型的破坏:有时,当代码捕获类异常但期望字符串异常时,它会打印一些略微奇怪的错误消息。有时,但频率低得多,代码实际上会在其错误处理中崩溃或以其他方式做错事。
非致命性破坏
第一种破坏的例子是试图打印异常名称的代码,例如:
对于基于字符串的异常,这会打印类似:try: 1/0 except: print "Sorry:", sys.exc_type, ":", sys.exc_value
对于基于类的异常,它将打印:Sorry: ZeroDivisionError : integer division or modulo
奇怪的Sorry: exceptions.ZeroDivisionError : integer division or modulo
exceptions.ZeroDivisionError
出现是因为当异常类型是类时,它被打印为 模块名.类名。这由 Python 内部处理。致命性破坏
更严重的是破坏错误处理代码。这通常发生是因为错误处理代码期望异常或与异常关联的值具有特定类型(通常是字符串或元组)。在新方案中,类型是一个类,值是一个类实例。例如,以下代码会中断:
因为它试图将异常类型(一个类对象)与字符串连接。一个修复方法(也适用于前面的示例)是这样写:try: raise Exception() except: print "Sorry:", sys.exc_type + ":", sys.exc_value
请注意这个例子如何避免了显式类型测试!相反,它只是捕获了当 __name__ 属性未找到时引发的(新)异常。为了绝对确保我们正在连接字符串,应用了内置函数 str()。try: raise Exception() except: etype = sys.exc_type # Save it; try-except overwrites it! try: ename = etype.__name__ # Get class name if it is a class except AttributeError: ename = etype print "Sorry:", str(ename) + ":", sys.exc_value
另一个例子涉及对与异常关联的值类型假设过多的代码。例如:
这段代码知道 IOError 通常会引发一个 (errorcode, message) 形式的元组,有时也只引发一个字符串。然而,由于它显式地测试值的元组性质,当值是一个实例时,它会崩溃!try: open('file-doesnt-exist') except IOError, v: if type(v) == type(()) and len(v) == 2: (code, message) = v else: code = 0 message = v print "I/O Error: " + message + " (" + str(code) + ")" print
同样,补救措施是直接尝试解包元组,如果失败,则使用备用策略:
这之所以有效,是因为元组解包语义已经放宽,可以与右侧的任何序列一起使用(参见下面的“序列解包”部分),并且标准异常类可以像序列一样访问(通过它们的 __getitem__ 方法,参见上文)。try: open('file-doesnt-exist') except IOError, v: try: (code, message) = v except: code = 0 message = v print "I/O Error: " + str(message) + " (" + str(code) + ")" print
请注意,第二个 try-except 语句没有指定要捕获的异常——这是因为对于字符串异常,引发的异常是“TypeError: unpack non-tuple”,而对于类异常,它是“ValueError: unpack sequence of wrong size”。这是因为字符串是一个序列;我们必须假设错误消息总是超过两个字符长!
(另一种方法是使用 try-except 来测试 errno 属性是否存在;将来,这将是有意义的,但目前为了与字符串异常兼容,需要更多的代码。)
C API 的变更
XXX 待详细描述
int PyErr_ExceptionMatches(PyObject *); int PyErr_GivenExceptionMatches(PyObject *, PyObject *); void PyErr_NormalizeException(PyObject**, PyObject**, PyObject**);
应优先使用 PyErr_ExceptionMatches(exception) 而不是 PyErr_Occurred()==exception,因为当引发的异常是所测试异常的派生类时,后者将返回不正确的结果。
PyErr_GivenExceptionMatches(raised_exception, exception) 执行与 PyErr_ExceptionMatches() 相同的测试,但允许您显式传递引发的异常。
PyErr_NormalizeException() 主要用于内部使用。
其他变更
作为同一项目的一部分,语言进行了一些更改。
新的内置函数
引入了两个用于类测试的新内置函数(因为该功能必须在 C API 中实现,所以没有理由不让 Python 程序员访问它)。
issubclass(D, C)
当且仅当类 D 直接或间接派生自类 C 时返回 true。issubclass(C, C) 始终返回 true。两个参数都必须是类对象。
isinstance(x, C)
当且仅当 x 是 C 的实例或 C 的(直接或间接)子类的实例时返回 true。第一个参数可以是任何类型;如果 x 不是任何类的实例,isinstance(x, C) 始终返回 false。第二个参数必须是类对象。
序列解包
以前的 Python 版本要求“解包”赋值的左侧和右侧之间进行精确的类型匹配,例如:
要求 x 是一个包含三个项的元组,而(a, b, c) = x
要求 x 是一个包含三个项的列表。[a, b, c] = x
作为同一项目的一部分,两个语句的右侧可以是任何具有正好三个项的序列。这使得可以以向后兼容的方式从 IOError 异常中提取例如 errno 和 strerror 值:
try: f = open(filename, mode) except IOError, what: (errno, strerror) = what print "Error number", errno, "(%s)" % strerror
同样的方法也适用于 SyntaxError 异常,但有一个前提条件是 info 部分并非总是存在:
try: c = compile(source, filename, "exec") except SyntaxError, what: try: message, info = what except: message, info = what, None if info: "...print source code info..." print "SyntaxError:", msg