Following system colour scheme Selected dark colour scheme Selected light colour scheme

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_typeexc_valueexc_tracebacksys.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] 上已经提出了几个用于链式异常的属性名称,包括 causeantecedentreasonoriginalchainchainedexcxc_chainexcprevpreviousprecursor。对于显式链式异常,此 PEP 建议使用 __cause__,因为它的含义很具体。对于隐式链式异常,此 PEP 提出使用名称 __context__,因为其预期含义比时间优先级更具体,但比因果关系不太具体:异常是在处理另一个异常的上下文中发生的。

此 PEP 建议对这三个属性使用以开头和结尾双下划线命名的属性,因为它们是由 Python 虚拟机设置的。只有在非常特殊的情况下,才能通过普通赋值来设置它们。

此 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) 会导致 ZeroDivisionErrorcompute() 函数捕获此异常并调用 log(exc),但当 log() 函数尝试写入未打开以进行写入的文件时,它也会引发异常。

在今天的 Python 中, compute() 的调用者会抛出 IOErrorZeroDivisionError 会丢失。通过提议的更改, 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() 将触发四个异常。最终结果将是由于 clos 拼写错误导致的 AttributeError,其 __context__ 指向由于 ex 拼写错误导致的 NameError,其 __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(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_tracebacksys.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_exceptionprint_exceptionprint_excprint_last functions将更新为接受一个可选的chain参数,默认为True。当此参数为True时,这些函数将像上面描述的那样格式化或显示整个异常链。当它为False时,这些函数将仅格式化或显示最外部的异常。

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

C API

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

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

兼容性

链式异常公开最近异常的类型,因此它们仍然与现在匹配相同的except子句。

除非代码在异常实例上设置或使用名为__context____cause____traceback__的属性,否则建议的更改不会破坏任何代码。截至2005-05-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 -> 回溯 -> 堆栈帧 -> err的循环,使同一作用域中的所有局部变量保持活动状态,直到下一次GC发生。

今天,这些局部变量将超出作用域。有很多代码假设“本地”资源(特别是打开的文件)将很快关闭。如果关闭必须等到下一个GC,则程序(在今天运行良好)可能会耗尽文件句柄。

使__traceback__属性成为弱引用将避免循环垃圾问题。不幸的是,这将使稍后保存Exception(如unittest所做的那样)变得更加笨拙,并且不允许对sys模块进行太多清理。

Adam Olsen提出的另一种可能的解决方案是在变量超出作用域时将从堆栈帧到err变量的引用改为弱引用[13]

可能的未来兼容性更改

这些更改与异常在解释器级别显示为单个对象而不是三元组一致。

  • 如果PEP 340PEP 343被接受,则用单个异常参数替换__exit__的三个(typevaluetraceback)参数。
  • 弃用sys.exc_typesys.exc_valuesys.exc_tracebacksys.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 参数。

致谢

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-0344.rst

上次修改:2023-09-09 17:39:29 GMT