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

Python 增强提案

PEP 3134 – 异常链接和嵌入式追踪

作者:
Ka-Ping Yee
状态:
最终版
类型:
标准追踪
创建时间:
2005-05-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_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](以及可能还有其他人)以前曾提到过向异常实例添加追踪属性。这在 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) 会导致 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() 函数将引发四个异常。最终结果将是 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__ 优先。为了保持跟踪的顺序,最近引发的异常最后显示;也就是说,显示从最内层异常的描述开始,并沿链向上追溯到最外层异常。跟踪按通常方式格式化,其中一行

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-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 -> traceback -> 堆栈帧 -> 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
  • 用一个参数 API 替换三个参数的 sys.excepthook,并更改 cgitb 模块以匹配。
  • 删除 raise 语句的三参数形式。
  • traceback.print_exception 升级为接受 exception 参数,而不是 typevaluetraceback 参数。

实现

在修订版 57783 中实现了 __traceback____cause__ 属性以及新的 raise 语法 [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

最后修改时间:2023-09-09 17:39:29 GMT