PEP 678 – 通过备注丰富异常
- 作者:
- Zac Hatfield-Dodds <zac at zhd.dev>
- 发起人:
- Irit Katriel
- 讨论至:
- Discourse 帖子
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 要求:
- 654
- 创建日期:
- 2021年12月20日
- Python 版本:
- 3.11
- 发布历史:
- 2022年1月27日
- 决议:
- Discourse 消息
摘要
异常对象通常用描述所发生错误的消息进行初始化。由于当捕获并重新引发异常时,或者包含在 ExceptionGroup 中时,可能会有更多信息可用,本 PEP 提议添加 BaseException.add_note(note),一个 .__notes__ 属性,用于存储所添加备注的列表,并更新内置的追溯格式化代码,以便在格式化的追溯中将备注包含在异常字符串之后。
这对于与 PEP 654 ExceptionGroup 有关的情况特别有用,它使以前的变通方法无效或令人困惑。在标准库、Hypothesis 和 cattrs 包以及带有重试的常见代码模式中已经确定了用例。
动机
当创建异常以便引发时,它通常用描述所发生错误的信息进行初始化。在某些情况下,在捕获异常后添加信息会很有用。例如,
- 测试库可能希望显示失败断言中涉及的值,或重现失败的步骤(例如
pytest和hypothesis;下面的示例)。 - 当操作出错时重试的代码可能希望将迭代、时间戳或其他解释与多个错误中的每一个关联起来——尤其是在
ExceptionGroup中重新引发它们时。 - 为初学者设计的编程环境可以提供各种错误的更详细描述,以及解决它们的提示。
现有方法必须在传递这些额外信息的同时,使其与已引发的以及可能已捕获或已链接的异常状态保持同步。这已经容易出错,并且由于 PEP 654 ExceptionGroup 而变得更加困难,因此是时候采用内置解决方案了。因此,我们提议添加
- 一个新方法
BaseException.add_note(note: str), BaseException.__notes__,一个使用.add_note()添加的备注字符串列表,以及- 内置追溯格式化代码中的支持,以便在格式化的追溯中将备注显示在异常字符串之后。
使用示例
>>> try:
... raise TypeError('bad type')
... except Exception as e:
... e.add_note('Add some information')
... raise
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
TypeError: bad type
Add some information
>>>
当将异常收集到异常组中时,我们可能希望为单个错误添加上下文信息。在下面的 Hypothesis 提出的 ExceptionGroup 支持的示例中(Hypothesis 的 ExceptionGroup 支持提案),每个异常都包含一个最小失败示例的备注
from hypothesis import given, strategies as st, target
@given(st.integers())
def test(x):
assert x < 0
assert x > 0
+ Exception Group Traceback (most recent call last):
| File "test.py", line 4, in test
| def test(x):
|
| File "hypothesis/core.py", line 1202, in wrapped_test
| raise the_error_hypothesis_found
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| ExceptionGroup: Hypothesis found 2 distinct failures.
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "test.py", line 6, in test
| assert x > 0
| ^^^^^^^^^^^^
| AssertionError: assert -1 > 0
|
| Falsifying example: test(
| x=-1,
| )
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "test.py", line 5, in test
| assert x < 0
| ^^^^^^^^^^^^
| AssertionError: assert 0 < 0
|
| Falsifying example: test(
| x=0,
| )
+------------------------------------
非目标
将多个备注作为列表而不是在添加备注时连接字符串进行跟踪,旨在保持各个备注之间的区别。这在特殊用例中可能是必需的,例如像 friendly-traceback 这样的包对备注进行翻译。
然而,__notes__ 不旨在携带结构化数据。如果您的备注是供程序使用而不是显示给人类,我们建议(或另外)选择一个属性约定,例如在错误或 ExceptionGroup 上设置 err._parse_errors = ...。
通常来说,我们建议当错误将被重新引发或作为单个错误处理时,您应该优先使用异常链;而当您想避免更改异常类型或正在收集多个异常对象以便一起处理时,应优先使用 .add_note()。[1]
规范
BaseException 获得一个新方法 .add_note(note: str)。如果 note 是一个字符串,.add_note(note) 会将其附加到 __notes__ 列表,如果该属性不存在则创建它。如果 note 不是字符串,.add_note() 会引发 TypeError。
如果 __notes__ 列表已创建,库可以通过修改或删除该列表来清除现有备注,包括使用 del err.__notes__ 清除所有备注。这允许完全控制附加的备注,而不会过度复杂化 API 或向 BaseException.__dict__ 添加多个名称。
当解释器的内置回溯渲染代码显示异常时,它的备注(如果有的话)将紧跟在异常消息之后,按照添加的顺序显示,每个备注从新的一行开始。
如果 __notes__ 已被创建,BaseExceptionGroup.subgroup 和 BaseExceptionGroup.split 会为每个新实例创建一个新列表,其中包含与原始异常组的 __notes__ 相同的内容。
我们**不**指定当用户将非列表值分配给 __notes__,或包含非字符串元素的列表时,预期的行为。实现可以选择发出警告、丢弃或忽略错误值、将其转换为字符串、引发异常或完全做其他事情。
向后兼容性
系统定义的或“dunder”名称(遵循 __*__ 模式)是语言规范的一部分,未分配的名称保留供将来使用,并且可能会在不事先通知的情况下中断。我们也没有发现任何会因添加 __notes__ 而导致中断的代码。
我们也没有发现任何会因添加 BaseException.add_note() 而中断的代码:虽然在 Google 和 GitHub 上找到了几个 .add_note() 方法的定义,但它们都不是 BaseException 的子类。
如何教授此内容
add_note() 方法和 __notes__ 属性将作为语言标准的一部分进行文档化,并在“错误和异常”教程中进行解释。
参考实现
在围绕PEP 654[2]进行的讨论之后,此提案的早期版本在 CPython 3.11.0a3 中实现并发布,带有一个可变的字符串或 None 类型的 __note__ 属性。
CPython PR #31317 实现了 .add_note() 和 __notes__。
被拒绝的想法
使用 print() (或 logging 等)
通过打印或日志记录来报告有关错误的解释性或上下文信息,历来是一种可接受的变通方法。然而,我们不喜欢这种方法将内容与其所指的异常对象分离的方式——这可能导致如果错误后来被捕获和处理,或者只是很难弄清哪个解释对应哪个错误,则可能导致“孤立”报告。新的 ExceptionGroup 类型加剧了这些现有的挑战。
将 __notes__ 附加到异常对象上,就像 __traceback__ 属性一样,消除了这些问题。
raise Wrapper(explanation) from err
另一种模式是使用异常链:通过从当前异常from引发一个包含上下文或解释的“包装器”异常,我们避免了 print() 带来的分离问题。然而,这有两个主要问题。
首先,它改变了异常的类型,这通常会对下游代码造成破坏性更改。我们认为 总是 引发 Wrapper 异常是不可接受的笨拙;但是由于自定义异常类型可能需要任意数量的参数,我们不能总是使用我们的解释来创建 相同 类型的一个实例。在确切的异常类型已知的情况下,这可以奏效,例如标准库 http.client 代码,但对于调用用户代码的库则不行。
其次,异常链报告了多行额外细节,这对于经验丰富的用户来说是分散注意力的,对于初学者来说可能非常令人困惑。例如,这个简单示例中报告的十一行中有六行与异常链相关,而使用 BaseException.add_note() 则不需要它们
class Explanation(Exception):
def __str__(self):
return "\n" + str(self.args[0])
try:
raise AssertionError("Failed!")
except Exception as e:
raise Explanation("You can reproduce this error by ...") from e
$ python example.py
Traceback (most recent call last):
File "example.py", line 6, in <module>
raise AssertionError(why)
AssertionError: Failed!
# These lines are
The above exception was the direct cause of ... # confusing for new
# users, and they
Traceback (most recent call last): # only exist due
File "example.py", line 8, in <module> # to implementation
raise Explanation(msg) from e # constraints :-(
Explanation: # Hence this PEP!
You can reproduce this error by ...
**在不适用这两个问题的情况下,我们鼓励使用异常链而不是** __notes__。
一个可赋值的 __note__ 属性
本 PEP 的初稿和实现定义了一个单一属性 __note__,它默认为 None 但可以分配一个字符串。这在最多只有一个备注的情况下会大大简化。
为了促进互操作性并支持像 friendly-traceback 这样的库翻译错误消息,而无需诉诸可疑的解析启发式方法,我们因此决定采用 .add_note() 和 __notes__ API。
子类化 Exception 并向下游添加备注支持
追溯打印内置于 C 代码中,并在 traceback.py 中用纯 Python 重新实现。要从下游实现打印 err.__notes__,还需要编写自定义追溯打印代码;虽然这可以在项目之间共享并重用 traceback.py 的一些部分[3],但我们更倾向于在上游实现一次。
自定义异常类型可以实现其 __str__ 方法以包含我们提议的 __notes__ 语义,但这很少且不一致适用。
不向 Exception 添加备注,只将它们存储在 ExceptionGroup 中
本 PEP 最初的动机是为 ExceptionGroup 中的每个错误关联一个备注。以一个非常笨拙的 API 和上面讨论的交叉引用问题为代价,可以通过将备注存储在 ExceptionGroup 实例而不是其中包含的每个异常上,来支持此用例。
我们相信更清晰的接口以及上述其他用例足以证明本 PEP 提出的更通用功能的合理性。
添加一个辅助函数 contextlib.add_exc_note()
有人建议我们将下面这样的工具添加到标准库中。我们不认为这个想法是本 PEP 提案的核心,因此将其留待以后或下游实现——也许基于这个示例代码
@contextlib.contextmanager
def add_exc_note(note: str):
try:
yield
except Exception as err:
err.add_note(note)
raise
with add_exc_note(f"While attempting to frobnicate {item=}"):
frobnicate_or_raise(item)
增强 raise 语句
一项讨论提议 raise Exception() with "note contents",但这并没有解决与 ExceptionGroup 兼容的原始动机。
此外,我们不认为我们正在解决的问题需要或证明新的语言语法的合理性。
致谢
我们要感谢许多通过对话、代码审查、设计建议和实现帮助过我们的人:Adam Turner、Alex Grönholm、André Roberge、Barry Warsaw、Brett Cannon、CAM Gerlach、Carol Willing、Damian、Erlend Aasland、Etienne Pot、Gregory Smith、Guido van Rossum、Irit Katriel、Jelle Zijlstra、Ken Jin、Kumar Aditya、Mark Shannon、Matti Picus、Petr Viktorin、Will McGugan,以及 Discord 和 Reddit 上的匿名评论者。
参考资料
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0678.rst