Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 344 – 异常链式和内嵌回溯

作者:
Ka-Ping Yee
状态:
已取代
类型:
标准跟踪
创建日期:
2005年5月12日
Python 版本:
2.5
发布历史:


目录

编号说明

此PEP已重编号为 PEP 3134。以下文本是在旧编号下提交的最后一个版本。

摘要

此PEP在异常实例上提出了三个标准属性:用于隐式链式异常的 `__context__` 属性,用于显式链式异常的 `__cause__` 属性,以及用于回溯的 `__traceback__` 属性。新的 `raise ... from` 语句用于设置 `__cause__` 属性。

动机

在处理一个异常(异常A)时,可能会发生另一个异常(异常B)。在今天的Python(2.4版本)中,如果发生这种情况,异常B会被传播出去,而异常A则会丢失。为了调试问题,了解这两个异常都很有用。`__context__` 属性会自动保留这些信息。

有时,异常处理程序有意地重新引发一个异常可能很有用,无论是为了提供额外信息还是为了将一个异常转换为另一种类型。`__cause__` 属性提供了一种明确记录异常直接原因的方法。

在今天的Python实现中,异常由三部分组成:类型、值和回溯。`sys` 模块通过三个并行变量 `exc_type`、`exc_value` 和 `exc_traceback` 来公开当前异常,`sys.exc_info()` 函数返回这三部分的元组,并且 `raise` 语句有一个接受这三部分的双参数形式。操作异常通常需要并行传递这三者,这可能很繁琐且容易出错。此外,`except` 语句只能提供对值的访问,而不能访问回溯。将 `__traceback__` 属性添加到异常值使得所有异常信息都可以从一个地方访问。

历史

Raymond Hettinger [1] 于2003年1月在Python-Dev上提出了被掩盖的异常问题,并提出了一个 `PyErr_FormatAppend()` 函数,C模块可以使用它来为当前活动的异常添加更多信息。Brett Cannon [2] 于2003年6月再次提出了链式异常,引发了长时间的讨论。

Greg Ewing [3] 指出了在原始异常触发的 `finally` 块展开过程中发生异常的情况,这与在处理原始异常的 `except` 块中发生异常的情况不同。

Greg Ewing [4] 和 Guido van Rossum [5],以及可能还有其他人,之前都提到过在 `Exception` 实例中添加回溯属性。这一点在 PEP 3000 中有所记录。

此PEP的动机是最近在Python-Dev上再次发布的相同想法 [6] [7]

基本原理

Python-Dev的讨论显示,人们对异常链式感兴趣,原因有两个截然不同的目的。为了处理意外地引发第二个异常,必须隐式保留该异常。为了支持有意地转换异常,必须有一种方法来显式地链式异常。此PEP同时解决了这两个问题。

在Python-Dev上 [2] 提出了几个链式异常的属性名称,包括 `cause`、`antecedent`、`reason`、`original`、`chain`、`chainedexc`、`xc_chain`、`excprev`、`previous` 和 `precursor`。对于显式链式异常,此PEP建议使用 `__cause__`,因为它具有特定的含义。对于隐式链式异常,此PEP建议使用 `__context__` 这个名称,因为其意图比时间顺序更具体,但比因果关系更不具体:异常发生在处理另一个异常的上下文中。

此PEP建议这些三个属性使用前后双下划线(double-underscores)作为名称,因为它们是由Python虚拟机设置的。只有在非常特殊的情况下,才应该通过普通赋值来设置它们。

此PEP以相同的方式处理在 `except` 块和 `finally` 块中发生的异常。阅读回溯可以清楚地显示异常发生的位置,因此区分这两种情况的额外机制只会增加不必要的复杂性。

此PEP建议,为了与当前行为兼容,最外层的异常对象(即通过 `except` 子句匹配的对象)应该是最近引发的异常。

此PEP建议回溯最后显示最外层异常,因为这与回溯的时间顺序(从最早到最近的帧)一致,并且实际抛出的异常更容易在最后一行找到。

为了保持简单,用于设置异常的C API调用不会自动设置异常的 `__context__`。Guido van Rossum 对进行此类更改表示担忧 [8]

至于其他语言,Java和Ruby在 `catch/rescue` 或 `finally/ensure` 子句中发生另一个异常时都会丢弃原始异常。Perl 5缺乏内置的结构化异常处理。对于Perl 6,RFC号88 [9] 提出了一个异常机制,该机制在名为 `@@` 的数组中隐式保留链式异常。在该RFC中,最近引发的异常会像本PEP一样对外公开以供匹配;此外,可以评估任意表达式(可能涉及 `@@`)来进行异常匹配。

C# 中的异常包含一个只读的 `InnerException` 属性,它可以指向另一个异常。其文档 [10] 称,“当异常X是由于先前的异常Y而直接抛出时,X的 `InnerException` 属性应包含对Y的引用。”此属性不是由虚拟机自动设置的;相反,所有异常构造函数都接受一个可选的 `innerException` 参数来显式设置它。`__cause__` 属性起到了与 `InnerException` 相同的作用,但此PEP提出了新的 `raise` 形式,而不是扩展所有异常的构造函数。C# 还提供了一个 `GetBaseException` 方法,可以直接跳转到 `InnerException` 链的末尾;此PEP没有提出类似的方法。

这三个属性被放在一个提议中的原因是 `__traceback__` 属性提供了对链式异常的回溯的便捷访问。

隐式异常链式

下面是一个说明 `__context__` 属性的例子

def compute(a, b):
    try:
        a/b
    except Exception, exc:
        log(exc)

def log(exc):
    file = open('logfile.txt')  # oops, forgot the 'w'
    print >>file, exc
    file.close()

调用 `compute(0, 0)` 会导致 `ZeroDivisionError`。`compute()` 函数捕获此异常并调用 `log(exc)`,但 `log()` 函数在尝试写入一个未打开写入模式的文件时也会引发异常。

在今天的Python中,`compute()` 的调用者会收到一个 `IOError`。`ZeroDivisionError` 会丢失。通过提议的更改,`IOError` 实例将有一个额外的 `__context__` 属性,其中保留了 `ZeroDivisionError`。

以下更复杂的示例演示了 `finally` 和 `except` 子句混合使用的处理方式

def main(filename):
    file = open(filename)       # oops, forgot the 'w'
    try:
        try:
            compute()
        except Exception, exc:
            log(file, exc)
        finally:
            file.clos()         # oops, misspelled 'close'

def compute():
    1/0

def log(file, exc):
    try:
        print >>file, exc       # oops, file is not writable
    except:
        display(exc)

def display(exc):
    print ex                    # oops, misspelled 'exc'

使用一个现有文件的名称调用 `main()` 将会触发四个异常。最终结果将是一个 `AttributeError`,因为 `clos` 拼写错误,其 `__context__` 指向一个 `NameError`,因为 `ex` 拼写错误,其 `__context__` 指向一个 `IOError`,因为文件是只读的,其 `__context__` 指向一个 `ZeroDivisionError`,其 `__context__` 属性为 `None`。

提议的语义如下:

  1. 每个线程都有一个异常上下文,最初设置为 `None`。
  2. 每当引发一个异常时,如果异常实例还没有 `__context__` 属性,解释器会将其设置为等于线程的异常上下文。
  3. 在异常引发后立即,线程的异常上下文被设置为该异常。
  4. 每当解释器通过到达末尾或执行 `return`、`yield`、`continue` 或 `break` 语句退出 `except` 块时,线程的异常上下文被设置为 `None`。

显式异常链式

异常对象的 `__cause__` 属性始终初始化为 `None`。它由一种新的 `raise` 语句形式设置

raise EXCEPTION from CAUSE

这等同于

exc = EXCEPTION
exc.__cause__ = CAUSE
raise exc

在以下示例中,一个数据库提供了几种不同类型存储的实现,文件存储是其中一种。数据库设计者希望错误作为 `DatabaseError` 对象传播,以便客户端不必了解存储特定细节,但又不想丢失底层错误信息

class DatabaseError(StandardError):
    pass

class FileDatabase(Database):
    def __init__(self, filename):
        try:
            self.file = open(filename)
        except IOError, exc:
            raise DatabaseError('failed to open') from exc

如果 `open()` 调用引发了异常,问题将报告为 `DatabaseError`,并带有 `__cause__` 属性,揭示 `IOError` 作为原始原因。

回溯属性

以下示例演示了 `__traceback__` 属性

def do_logged(file, work):
    try:
        work()
    except Exception, exc:
        write_exception(file, exc)
        raise exc

from traceback import format_tb

def write_exception(file, exc):
    ...
    type = exc.__class__
    message = str(exc)
    lines = format_tb(exc.__traceback__)
    file.write(... type ... message ... lines ...)
    ...

在今天的Python中,`do_logged()` 函数将不得不从 `sys.exc_traceback` 或 `sys.exc_info()` [2] 中提取回溯,并将值和回溯都传递给 `write_exception()`。通过提议的更改,`write_exception()` 只接受一个参数,并通过 `__traceback__` 属性获取异常。

提议的语义如下:

  1. 每当捕获到一个异常时,如果异常实例还没有 `__traceback__` 属性,解释器会将其设置为新捕获的回溯。

增强报告

默认异常处理器将被修改以报告链式异常。异常链通过跟随 `__cause__` 和 `__context__` 属性进行遍历,其中 `__cause__` 具有优先权。为了与回溯的时间顺序保持一致,最近引发的异常最后显示;也就是说,显示从最内层异常的描述开始,一直追溯到最外层异常。回溯按正常方式格式化,其中一条线

The above exception was the direct cause of the following exception:

During handling of the above exception, another exception occurred:

在回溯之间,取决于它们是分别由 `__cause__` 或 `__context__` 链接的。这是程序的草图:

def print_chain(exc):
    if exc.__cause__:
        print_chain(exc.__cause__)
        print '\nThe above exception was the direct cause...'
    elif exc.__context__:
        print_chain(exc.__context__)
        print '\nDuring handling of the above exception, ...'
    print_exc(exc)

在 `traceback` 模块中,`format_exception`、`print_exception`、`print_exc` 和 `print_last` 函数将被更新以接受一个可选的 `chain` 参数,默认值为 `True`。当此参数为 `True` 时,这些函数将按照前面所述的格式化或显示整个异常链。当为 `False` 时,这些函数将只格式化或显示最外层的异常。

cgitb` 模块也应该更新以显示整个异常链。

C API

用于设置异常的 `PyErr_Set*` 调用不会设置异常上的 `__context__` 属性。`PyErr_NormalizeException` 将始终将 `traceback` 属性设置为其 `tb` 参数,并将 `__context__` 和 `__cause__` 属性设置为 `None`。

一个新的API函数 `PyErr_SetContext(context)` 将帮助C程序员提供链式异常信息。该函数将首先标准化当前异常,使其成为一个实例,然后设置其 `__context__` 属性。一个类似的API函数 `PyErr_SetCause(cause)` 将设置 `__cause__` 属性。

兼容性

链式异常会暴露最内层异常的类型,因此它们仍然会匹配与现在相同的 `except` 子句。

提议的更改不应破坏任何代码,除非它在异常实例上设置或使用了名为 `__context__`、`__cause__` 或 `__traceback__` 的属性。截至2005年5月12日,Python标准库中没有提及此类属性。

待定问题:额外信息

Walter Dörwald [11] 表达了在异常的向上传播过程中附加额外信息而不改变其类型的愿望。这可能是一个有用的功能,但此PEP并未涉及。它可以通过单独的PEP来解决,该PEP为异常上的其他信息属性建立约定。

待定问题:抑制上下文

按照目前的写法,此PEP使得无法抑制 `__context__`,因为在 `except` 或 `finally` 子句中将 `exc.__context__` 设置为 `None` 只会导致在 `exc` 引发时再次将其设置。

待定问题:限制异常类型

为了改善封装,库实现者可能希望用应用程序级别的异常包装所有实现级别的异常。可以尝试通过编写以下内容来包装异常:

try:
    ... implementation may raise an exception ...
except:
    import sys
    raise ApplicationError from sys.exc_value

或此

try:
    ... implementation may raise an exception ...
except Exception, exc:
    raise ApplicationError from exc

但两者都有缺陷。能够在一个捕获所有异常的 `except` 子句中命名当前异常会很好,但这在这里没有涉及。这样的功能允许类似以下内容:

try:
    ... implementation may raise an exception ...
except *, exc:
    raise ApplicationError from exc

待定问题:yield

当执行 `yield` 语句时,异常上下文会丢失;恢复 `yield` 之后的帧不会恢复上下文。解决这个问题超出了此PEP的范围;这不是一个新问题,如下例所示:

>>> def gen():
...     try:
...         1/0
...     except:
...         yield 3
...         raise
...
>>> g = gen()
>>> g.next()
3
>>> g.next()
TypeError: exceptions must be classes, instances, or strings
(deprecated), not NoneType

待定问题:垃圾回收

对这个提案最强烈的反对意见是它在异常和堆栈帧之间创建了循环 [12]。循环垃圾的收集(因此资源释放)可能会被大大延迟。

>>> try:
>>>     1/0
>>> except Exception, err:
>>>     pass

将导致 `err` -> `traceback` -> `stack frame` -> `err` 的循环,使同一作用域内的所有局部变量在下次GC发生之前都保持活动状态。

今天,这些局部变量将超出范围。有大量代码假定“局部”资源——特别是打开的文件——会很快关闭。如果关闭必须等到下次GC,那么程序(今天运行正常)可能会耗尽文件句柄。

将 `__traceback__` 属性设为弱引用将避免循环垃圾的问题。不幸的是,这会使保存 `Exception` 以供以后使用(如 `unittest` 所做的那样)更加麻烦,并且不允许对 `sys` 模块进行如此多的清理。

一个可能的替代解决方案是,由Adam Olsen提出,将堆栈帧到 `err` 变量的引用在变量超出范围时改为弱引用 [13]

未来可能的兼容性更改

这些更改与异常作为单个对象而不是在解释器级别上的三元组的外观一致。

  • 如果 PEP 340PEP 343 被接受,将 `__exit__` 的三个(`type`、`value`、`traceback`)参数替换为单个异常参数。
  • 弃用 `sys.exc_type`、`sys.exc_value`、`sys.exc_traceback` 和 `sys.exc_info()`,转而使用单个成员 `sys.exception`。
  • 弃用 `sys.last_type`、`sys.last_value` 和 `sys.last_traceback`,转而使用单个成员 `sys.last_exception`。
  • 弃用 `raise` 语句的三参数形式,转而使用一参数形式。
  • 升级 `cgitb.html()` 以接受单个值作为其第一个参数,作为 `(type, value, traceback)` 元组的替代。

未来可能的不兼容更改

这些更改可能值得在Python 3000中考虑。

  • 移除 `sys.exc_type`、`sys.exc_value`、`sys.exc_traceback` 和 `sys.exc_info()`。
  • 移除 `sys.last_type`、`sys.last_value` 和 `sys.last_traceback`。
  • 将三参数的 `sys.excepthook` 替换为一参数API,并将 `cgitb` 模块与之匹配。
  • 移除 `raise` 语句的三参数形式。
  • 升级 `traceback.print_exception` 以接受 `exception` 参数,而不是 `type`、`value` 和 `traceback` 参数。

致谢

Brett Cannon、Greg Ewing、Guido van Rossum、Jeremy Hylton、Phillip J. Eby、Raymond Hettinger、Walter Dörwald 等人。

参考资料


Source: https://github.com/python/peps/blob/main/peps/pep-0344.rst

Last modified: 2025-02-01 08:59:27 GMT