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

Python Enhancement Proposals

PEP 678 – Exceptions mit Notizen anreichern

Autor:
Zac Hatfield-Dodds <zac at zhd.dev>
Sponsor:
Irit Katriel
Discussions-To:
Discourse thread
Status:
Final
Typ:
Standards Track
Benötigt:
654
Erstellt:
20. Dez. 2021
Python-Version:
3.11
Post-History:
27. Jan. 2022
Resolution:
Discourse-Nachricht

Inhaltsverzeichnis

Wichtig

Diese PEP ist ein historisches Dokument. Die aktuelle, kanonische Dokumentation finden Sie jetzt unter BaseException.add_note() und BaseException.__notes__.

×

Siehe Exceptions mit Notizen anreichern für ein benutzerorientiertes Tutorial.

Siehe PEP 1, um Änderungen vorzuschlagen.

Zusammenfassung

Ausnahmeobjekte werden typischerweise mit einer Nachricht initialisiert, die den aufgetretenen Fehler beschreibt. Da zusätzliche Informationen verfügbar sein können, wenn die Ausnahme abgefangen und erneut ausgelöst wird, oder in einer ExceptionGroup enthalten ist, schlägt diese PEP die Hinzufügung von BaseException.add_note(note), eines .__notes__-Attributs, das eine Liste von so hinzugefügten Notizen enthält, und die Aktualisierung des eingebauten Traceback-Formatierungscodes vor, um Notizen im formatierten Traceback nach der Fehlermeldung einzufügen.

Dies ist besonders nützlich im Zusammenhang mit PEP 654 ExceptionGroups, die frühere Workarounds unwirksam oder verwirrend machen. Anwendungsfälle wurden in der Standardbibliothek, den Paketen Hypothesis und cattrs sowie gängigen Code-Mustern mit Wiederholungsversuchen identifiziert.

Motivation

Wenn eine Ausnahme erstellt wird, um ausgelöst zu werden, wird sie normalerweise mit Informationen initialisiert, die den aufgetretenen Fehler beschreiben. Es gibt Fälle, in denen es nützlich ist, Informationen hinzuzufügen, nachdem die Ausnahme abgefangen wurde. Zum Beispiel:

  • Testbibliotheken möchten möglicherweise die Werte anzeigen, die bei einer fehlerhaften Assertion beteiligt sind, oder die Schritte zur Reproduktion eines Fehlers (z. B. pytest und hypothesis; Beispiel unten).
  • Code, der einen Vorgang bei einem Fehler wiederholt, möchte möglicherweise eine Iteration, einen Zeitstempel oder eine andere Erklärung mit jedem der mehreren Fehler verknüpfen – insbesondere, wenn sie in einer ExceptionGroup erneut ausgelöst werden.
  • Programmierumgebungen für Anfänger können detailliertere Beschreibungen verschiedener Fehler und Tipps zur Behebung dieser Fehler liefern.

Bestehende Ansätze müssen diese zusätzlichen Informationen weitergeben und sie mit dem Zustand von ausgelösten und möglicherweise abgefangenen oder verketteten Ausnahmen synchron halten. Dies ist bereits fehleranfällig und wird durch PEP 654 ExceptionGroups noch schwieriger, sodass nun der richtige Zeitpunkt für eine eingebaute Lösung ist. Wir schlagen daher vor, Folgendes hinzuzufügen:

  • eine neue Methode BaseException.add_note(note: str),
  • BaseException.__notes__, eine Liste von Notizzeichenfolgen, die mit .add_note() hinzugefügt wurden, und
  • Unterstützung im eingebauten Code zur Traceback-Formatierung, sodass Notizen im formatierten Traceback nach der Fehlermeldung angezeigt werden.

Beispielhafte Nutzung

>>> 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
>>>

Beim Sammeln von Ausnahmen in einer Ausnahme-Gruppe möchten wir möglicherweise Kontextinformationen für die einzelnen Fehler hinzufügen. Im folgenden Beispiel mit Hypothesis' vorgeschlagener Unterstützung für ExceptionGroup enthält jede Ausnahme eine Notiz des minimalen fehlerhaften Beispiels

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,
    | )
    +------------------------------------

Non-goals

Das Verfolgen mehrerer Notizen als Liste anstatt durch Aneinanderreihung von Zeichenketten beim Hinzufügen von Notizen soll die Unterscheidung zwischen den einzelnen Notizen beibehalten. Dies könnte in spezialisierten Anwendungsfällen erforderlich sein, z. B. bei der Übersetzung von Notizen durch Pakete wie friendly-traceback.

Es ist jedoch nicht beabsichtigt, dass __notes__ strukturierte Daten enthält. Wenn Ihre Notiz für die Verwendung durch ein Programm und nicht zur Anzeige für einen Menschen bestimmt ist, empfehlen wir stattdessen (oder zusätzlich) eine Konvention für ein Attribut zu wählen, z. B. err._parse_errors = ... für den Fehler oder die ExceptionGroup.

Als Faustregel schlagen wir vor, dass Sie die Ausnahmeverkettung bevorzugen sollten, wenn die Ausnahme erneut ausgelöst oder als einzelne Ausnahme behandelt wird, und .add_note() bevorzugen sollten, wenn Sie den Ausnahmetyp nicht ändern möchten oder mehrere Ausnahmeobjekte zum gemeinsamen Verarbeiten sammeln. [1]

Spezifikation

BaseException erhält eine neue Methode .add_note(note: str). Wenn note eine Zeichenkette ist, fügt .add_note(note) diese der __notes__-Liste hinzu und erstellt das Attribut, falls es noch nicht existiert. Wenn note keine Zeichenkette ist, löst .add_note() einen TypeError aus.

Bibliotheken können bestehende Notizen löschen, indem sie die __notes__-Liste ändern oder löschen, falls sie erstellt wurde, einschließlich des Löschens aller Notizen mit del err.__notes__. Dies ermöglicht die vollständige Kontrolle über die angehängten Notizen, ohne die API übermäßig zu verkomplizieren oder mehrere Namen zu BaseException.__dict__ hinzuzufügen.

Wenn eine Ausnahme vom eingebauten Traceback-Rendering-Code des Interpreters angezeigt wird, erscheinen ihre Notizen (falls vorhanden) unmittelbar nach der Fehlermeldung in der Reihenfolge, in der sie hinzugefügt wurden, wobei jede Notiz mit einer neuen Zeile beginnt.

Wenn __notes__ erstellt wurde, erstellen BaseExceptionGroup.subgroup und BaseExceptionGroup.split eine neue Liste für jede neue Instanz, die dieselben Inhalte wie die __notes__ der ursprünglichen Ausnahme-Gruppe enthält.

Wir legen das erwartete Verhalten *nicht* fest, wenn Benutzer __notes__ einen Nicht-Listen-Wert oder eine Liste mit Nicht-String-Elementen zugewiesen haben. Implementierungen könnten Warnungen ausgeben, ungültige Werte verwerfen oder ignorieren, sie in Zeichenketten umwandeln, eine Ausnahme auslösen oder etwas ganz anderes tun.

Abwärtskompatibilität

Systemdefinierte oder "Dunder"-Namen (nach dem Muster __*__) sind Teil der Sprachspezifikation, wobei nicht zugewiesene Namen für die zukünftige Verwendung reserviert sind und ohne Vorwarnung zu Brüchen führen können. Wir sind uns auch keinerlei Code bewusst, der durch die Hinzufügung von __notes__ *gebrochen* würde.

Wir konnten auch keinen Code finden, der durch die Hinzufügung von BaseException.add_note() gebrochen würde: Während eine Suche auf Google und GitHub mehrere Definitionen einer .add_note()-Methode findet, ist keine davon auf einer Unterklasse von BaseException.

Wie man das lehrt

Die Methode add_note() und das Attribut __notes__ werden als Teil des Sprachstandards dokumentiert und im Tutorial „Errors and Exceptions“ erklärt.

Referenzimplementierung

Nach Diskussionen im Zusammenhang mit PEP 654 [2] wurde eine frühe Version dieses Vorschlags in CPython 3.11.0a3 implementiert und veröffentlicht, mit einem veränderbaren String-oder-None __note__-Attribut.

CPython PR #31317 implementiert .add_note() und __notes__.

Abgelehnte Ideen

Verwenden Sie print() (oder logging, etc.)

Das Melden von erklärenden oder kontextbezogenen Informationen über einen Fehler durch Drucken oder Protokollieren war historisch gesehen eine akzeptable Lösung. Wir mögen jedoch nicht, wie dies den Inhalt von dem Ausnahmeobjekt trennt, auf das es sich bezieht – dies kann zu „verwaisten“ Berichten führen, wenn der Fehler später abgefangen und behandelt wurde, oder lediglich zu erheblichen Schwierigkeiten bei der Zuordnung, welche Erklärung zu welchem Fehler gehört. Der neue ExceptionGroup-Typ verschärft diese bestehenden Herausforderungen.

Das Beibehalten der __notes__ am Ausnahmeobjekt, auf die gleiche Weise wie das __traceback__-Attribut, eliminiert diese Probleme.

raise Wrapper(explanation) from err

Ein alternatives Muster ist die Verwendung von Ausnahmeverkettung: Indem eine „Wrapper“-Ausnahme ausgelöst wird, die den Kontext oder die Erklärung from der aktuellen Ausnahme enthält, vermeiden wir die Trennungsprobleme von print(). Dies hat jedoch zwei Hauptprobleme.

Erstens ändert es den Typ der Ausnahme, was für nachgelagerten Code oft eine brechende Änderung darstellt. Wir halten das *ständige* Auslösen einer Wrapper-Ausnahme für unannehmbar unintelligent; aber da benutzerdefinierte Ausnahmetypen beliebige erforderliche Argumente haben können, können wir nicht immer eine Instanz des *gleichen* Typs mit unserer Erklärung erstellen. In Fällen, in denen der genaue Ausnahmetyp bekannt ist, kann dies funktionieren, wie z. B. im Code der Standardbibliothek http.client Code, aber nicht für Bibliotheken, die Benutzercode aufrufen.

Zweitens zeigt die Ausnahmeverkettung mehrere zusätzliche Zeilen an Details an, die für erfahrene Benutzer ablenkend und für Anfänger sehr verwirrend sein können. Zum Beispiel beziehen sich sechs der elf Zeilen, die für dieses einfache Beispiel gemeldet werden, auf Ausnahmeverkettung und sind mit BaseException.add_note() unnötig.

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

**In Fällen, in denen diese beiden Probleme nicht zutreffen, empfehlen wir die Verwendung von Ausnahmeverkettung anstelle von** __notes__.

Ein zuweisbares Attribut __note__

Der erste Entwurf und die Implementierung dieser PEP definierten ein einzelnes Attribut __note__, das standardmäßig None war, dem aber eine Zeichenkette zugewiesen werden konnte. Dies ist nur dann wesentlich einfacher, wenn es höchstens eine Notiz gibt.

Um die Interoperabilität zu fördern und die Übersetzung von Fehlermeldungen durch Bibliotheken wie friendly-traceback ohne zweifelhafte Parsing-Heuristiken zu unterstützen, haben wir uns daher für die API .add_note() und __notes__ entschieden.

Leiten Sie von Exception ab und fügen Sie nachgelagert Notizunterstützung hinzu

Die Traceback-Ausgabe ist in C-Code eingebaut und wird in reinem Python in traceback.py neu implementiert. Um err.__notes__ aus einer nachgelagerten Implementierung drucken zu lassen, müsste man *auch* benutzerdefinierten Code zur Traceback-Ausgabe schreiben; obwohl dieser zwischen Projekten geteilt und einige Teile von traceback.py wiederverwenden könnte [3], ziehen wir es vor, dies einmal, upstream, zu implementieren.

Benutzerdefinierte Ausnahmetypen könnten ihre __str__-Methode implementieren, um unsere vorgeschlagene __notes__-Semantik einzubeziehen, aber dies wäre selten und inkonsistent anwendbar.

Hängen Sie keine Notizen an Exceptions, sondern speichern Sie sie nur in ExceptionGroups

Die ursprüngliche Motivation für diese PEP war, jeder Ausnahme in einer ExceptionGroup eine Notiz zuzuordnen. Auf Kosten einer bemerkenswert umständlichen API und des oben diskutierten Cross-Referencing-Problems könnte dieser Anwendungsfall unterstützt werden, indem Notizen auf der ExceptionGroup-Instanz anstatt auf jeder einzelnen Ausnahme, die sie enthält, gespeichert werden.

Wir glauben, dass die sauberere Schnittstelle und andere oben beschriebene Anwendungsfälle ausreichen, um das allgemeinere Feature, das diese PEP vorschlägt, zu rechtfertigen.

Fügen Sie eine Hilfsfunktion contextlib.add_exc_note() hinzu

Es wurde vorgeschlagen, dass wir ein Dienstprogramm wie das untenstehende zur Standardbibliothek hinzufügen. Wir sehen diese Idee nicht als Kernstück des Vorschlags dieser PEP an und überlassen sie daher einer späteren oder nachgelagerten Implementierung – möglicherweise basierend auf diesem Beispielcode.

@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)

Erweitern Sie die raise-Anweisung

Eine Diskussion schlug raise Exception() with "note contents" vor, aber dies löst nicht die ursprüngliche Motivation der Kompatibilität mit ExceptionGroup.

Darüber hinaus glauben wir nicht, dass das Problem, das wir lösen, eine neue Syntax erfordert oder rechtfertigt.

Danksagungen

Wir möchten uns bei den vielen Personen bedanken, die uns durch Gespräche, Code-Reviews, Design-Ratschläge und Implementierung geholfen haben: 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 und pseudonyme Kommentatoren auf Discord und Reddit.

Referenzen


Quelle: https://github.com/python/peps/blob/main/peps/pep-0678.rst

Zuletzt geändert: 2025-02-01 08:55:40 GMT