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

Python 增强提案

PEP 3134 – 异常链和嵌入式回溯

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


目录

编号说明

本PEP最初是 PEP 344。由于它现在针对Python 3000,因此已移至3xxx空间。

摘要

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

动机

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

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

在今天的Python实现中,异常由三部分组成:类型、值和回溯。sys 模块通过三个并行变量 exc_typeexc_valueexc_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] 上提出了几个用于链式异常的属性名称,包括 causeantecedentreasonoriginalchainchainedexcexc_chainexcprevpreviousprecursor。对于显式链式异常,本PEP建议使用 __cause__,因为它具有特定的含义。对于隐式链式异常,本PEP建议使用 __context__,因为其预期含义比时间上的优先级更具体,但不如因果关系那么具体:在处理另一个异常的上下文中发生异常。

本PEP建议这些属性的名称带有前导和尾随双下划线,因为它们是由 Python VM 设置的。只有在非常特殊的情况下,它们才应由正常的赋值设置。

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

本PEP提议,最外层异常对象(由 except 子句暴露用于匹配的对象)应是最近引发的异常,以与当前行为兼容。

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

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

至于其他语言,Java 和 Ruby 在 catch/rescuefinally/ensure 子句中发生另一个异常时都会丢弃原始异常。Perl 5 缺少内置的结构化异常处理。对于 Perl 6,RFC 88 [9] 提出了一个异常机制,它将链式异常隐式地保留在一个名为 @@ 的数组中。在该 RFC 中,最近引发的异常被暴露用于匹配,如本 PEP 中所示;此外,可以评估任意表达式(可能涉及 @@)以进行异常匹配。

C# 中的异常包含一个只读的 InnerException 属性,它可能指向另一个异常。其文档 [10] 说:“当异常 X 是由于先前的异常 Y 直接引发时,X 的 InnerException 属性应包含对 Y 的引用。”此属性不是由 VM 自动设置的;相反,所有异常构造函数都接受一个可选的 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

以下更详细的示例演示了 finallyexcept 子句的混合处理。

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. 每当解释器通过到达末尾或执行 returnyieldcontinuebreak 语句退出 except 块时,线程的异常上下文将设置为 None

显式异常链

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

raise EXCEPTION from CAUSE

这等同于

exc = EXCEPTION
exc.__cause__ = CAUSE
raise exc

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

class DatabaseError(Exception):
    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_tracebacksys.exc_info() [2] 中提取回溯,并将值和回溯都传递给 write_exception()。通过建议的更改,write_exception() 只接收一个参数,并使用 __traceback__ 属性获取异常。

提议的语义如下:

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

增强报告

默认异常处理程序将被修改以报告链式异常。通过遵循 __cause____context__ 属性遍历异常链,其中 __cause__ 具有优先级。为了与回溯的时间顺序保持一致,最近引发的异常最后显示;也就是说,显示从最内层异常的描述开始,然后沿着链向上追溯到最外层异常。回溯按 usual 格式化,其中一行

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_exceptionprint_exceptionprint_excprint_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__ 变得不可能,因为在 exceptfinally 子句中将 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__ 的三个参数(typevaluetraceback)替换为单个异常参数。
  • 弃用 sys.exc_type, sys.exc_value, 和 sys.exc_traceback,以及 sys.exc_info(),转而使用单个成员 sys.exception
  • 弃用 sys.last_typesys.last_valuesys.last_traceback,转而使用单个成员 sys.last_exception
  • 弃用 raise 语句的三参数形式,转而使用单参数形式。
  • cgitb.html() 升级为接受单个值作为其第一个参数,而不是 (type, value, traceback) 元组。

可能的未来不兼容性变更

这些变化可能值得考虑用于 Python 3000。

  • 删除 sys.exc_typesys.exc_valuesys.exc_tracebacksys.exc_info()
  • 删除 sys.last_typesys.last_valuesys.last_traceback
  • 将三参数的 sys.excepthook 替换为单参数 API,并修改 cgitb 模块以匹配。
  • 移除 raise 语句的三参数形式。
  • 升级 traceback.print_exception 以接受 exception 参数,而不是 typevaluetraceback 参数。

实施

__traceback____cause__ 属性以及新的 raise 语法已在 revision 57783 中实现 [14]

致谢

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

参考资料


来源: https://github.com/python/peps/blob/main/peps/pep-3134.rst

上次修改: 2025-02-01 08:59:27 GMT