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 异常已被删除。由于这种更改可能(尽管不太可能)破坏现有代码,因此可以调用 Python 解释器的命令行选项 -X
来禁用此功能,并像以前一样使用字符串异常。此选项是一项临时措施 - 最终将从该语言中完全删除基于字符串的标准异常。尚未决定在 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
exceptionraise
exception, argumentraise
exception, (argument, argument, ...)
当 exception 是一个类时,这些等效于以下形式
raise
exception()raise
exception(argument)raise
exception(argument, argument, ...)
请注意,这些都是以下形式的示例
raise
instance
它本身是以下形式的简写
raise
class, instance
其中 class 是 instance 所属的类。在 Python 1.4 中,只允许以下形式
raise
class, instance 和raise
instance
在 Python 1.5(从 1.5a1 开始)中,添加了以下形式
raise
class 和raise
class, argument(s)
字符串异常的允许形式保持不变。
由于各种原因,将 None 作为 raise 的第二个参数传递等同于省略它。特别是,语句
raise
class,None
等效于
raise
class()
而不是
raise
class(None
)
同样,语句
raise
class, value
其中 value 恰好是一个元组,等效于将元组的项作为单独的参数传递给类构造函数,而不是将 value 作为单个参数传递(空元组调用构造函数时不带参数)。这会产生差异,因为 f(a, b)
和 f((a, b))
之间存在差异。
这些都是妥协 - 它们很好地适用于标准异常通常采用的参数类型(如简单字符串)。为了在新代码中清晰起见,建议使用以下形式
raise
class(argument, ...)
(即显式调用构造函数)。
这有什么帮助?
引入兼容性形式的动机是为了允许与引发标准异常的旧代码向后兼容。例如,__getattr__ 钩子可能会调用语句
raise AttributeError
, attrname
当未定义所需的属性时。
使用新的类异常,要引发的正确异常将是 AttributeError
(attrname);兼容性形式确保旧代码不会中断。(事实上,想要与 -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
,是因为当异常类型是一个类时,它会打印为 modulename.classname。这由 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)
返回 true 当且仅当类 D 是类 C 的直接或间接派生类。issubclass(C, C)
始终返回 true。两个参数都必须是类对象。
isinstance(x, C)
返回 true 当且仅当 x 是 C 的实例或 C 的(直接或间接)子类的实例。第一个参数可以是任何类型;如果 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