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