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 支持 的示例中,每个异常都包含一个最小失败示例的备注
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__
或包含非字符串元素的列表时的预期行为。实现可以选择发出警告、丢弃或忽略错误值、将其转换为字符串、引发异常或执行其他任何操作。
向后兼容性
系统定义或“双下划线”名称(遵循 __*__
模式)是语言规范的一部分,其中 未分配的名称保留供将来使用,并且可能会在未经警告的情况下被破坏。我们也不知道有任何代码会被添加 __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
代码,但不适用于调用用户代码的库。
其次,异常链报告了额外几行详细信息,这会分散有经验用户的注意力,并且对初学者来说可能会非常令人困惑。例如,在这个简单示例中报告的 11 行中有 6 行与异常链相关,并且使用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],但我们更倾向于在 upstream 实现一次。
自定义异常类型可以实现其__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
上次修改时间:2023-10-10 15:15:34 GMT