PEP 344 – Exception Chaining und eingebettete Tracebacks
- Autor:
- Ka-Ping Yee
- Status:
- Abgelöst
- Typ:
- Standards Track
- Erstellt:
- 12. Mai 2005
- Python-Version:
- 2.5
- Post-History:
Inhaltsverzeichnis
- Nummerierungs-Hinweis
- Zusammenfassung
- Motivation
- Historie
- Begründung
- Implizite Exception Chaining
- Explizite Exception Chaining
- Traceback-Attribut
- Erweiterte Berichterstattung
- C API
- Kompatibilität
- Offene Frage: Zusätzliche Informationen
- Offene Frage: Unterdrücken des Kontexts
- Offene Frage: Begrenzung von Exception-Typen
- Offene Frage: yield
- Offene Frage: Garbage Collection
- Mögliche zukünftige kompatible Änderungen
- Mögliche zukünftige inkompatible Änderungen
- Danksagungen
- Referenzen
- Urheberrecht
Nummerierungs-Hinweis
Diese PEP wurde in PEP 3134 umbenannt. Der folgende Text ist die letzte unter der alten Nummer eingereichte Version.
Zusammenfassung
Diese PEP schlägt drei Standardattribute für Exception-Instanzen vor: das Attribut __context__ für implizit verkettete Exceptions, das Attribut __cause__ für explizit verkettete Exceptions und das Attribut __traceback__ für den Traceback. Eine neue raise ... from-Anweisung setzt das Attribut __cause__.
Motivation
Während der Behandlung einer Exception (Exception A) kann eine andere Exception (Exception B) auftreten. Im heutigen Python (Version 2.4) wird, wenn dies geschieht, Exception B nach außen weitergegeben und Exception A geht verloren. Um das Problem zu debuggen, ist es nützlich, beide Exceptions zu kennen. Das Attribut __context__ behält diese Informationen automatisch bei.
Manchmal kann es für einen Exception-Handler nützlich sein, absichtlich eine Exception erneut auszulösen, entweder um zusätzliche Informationen bereitzustellen oder um eine Exception in einen anderen Typ zu übersetzen. Das Attribut __cause__ bietet eine explizite Möglichkeit, die direkte Ursache einer Exception aufzuzeichnen.
In der heutigen Python-Implementierung bestehen Exceptions aus drei Teilen: dem Typ, dem Wert und dem Traceback. Das Modul sys stellt die aktuelle Exception in drei parallelen Variablen dar: exc_type, exc_value und exc_traceback. Die Funktion sys.exc_info() gibt ein Tupel dieser drei Teile zurück, und die raise-Anweisung hat eine dreiteilige Form, die diese drei Teile akzeptiert. Die Manipulation von Exceptions erfordert oft die parallele Übergabe dieser drei Elemente, was mühsam und fehleranfällig sein kann. Darüber hinaus kann die except-Anweisung nur auf den Wert zugreifen, nicht auf den Traceback. Durch das Hinzufügen des Attributs __traceback__ zu Exception-Werten werden alle Exception-Informationen von einem einzigen Ort aus zugänglich.
Historie
Raymond Hettinger [1] brachte das Problem der maskierten Exceptions im Januar 2003 auf Python-Dev zur Sprache und schlug eine Funktion PyErr_FormatAppend() vor, die C-Module verwenden könnten, um die aktuell aktive Exception mit weiteren Informationen zu ergänzen. Brett Cannon [2] brachte im Juni 2003 erneut verkettete Exceptions zur Sprache, was zu einer langen Diskussion führte.
Greg Ewing [3] identifizierte den Fall, dass während des Ausrollens eines ursprünglichen Exceptions eine Exception in einem finally-Block auftritt, was sich von dem Fall unterscheidet, dass eine Exception in einem except-Block auftritt, der die ursprüngliche Exception behandelt.
Greg Ewing [4] und Guido van Rossum [5] und wahrscheinlich auch andere haben zuvor das Hinzufügen eines Traceback-Attributs zu Exception-Instanzen erwähnt. Dies wird in PEP 3000 erwähnt.
Diese PEP wurde durch eine weitere kürzliche Wiederholung derselben Ideen auf Python-Dev [6] [7] motiviert.
Begründung
Die Python-Dev-Diskussionen zeigten Interesse an Exception Chaining für zwei ganz unterschiedliche Zwecke. Um die unerwartete Auslösung einer sekundären Exception zu behandeln, muss die Exception implizit beibehalten werden. Um die absichtliche Übersetzung einer Exception zu unterstützen, muss es eine Möglichkeit geben, Exceptions explizit zu verketten. Diese PEP befasst sich mit beidem.
Mehrere Attributnamen für verkettete Exceptions wurden auf Python-Dev [2] vorgeschlagen, darunter cause, antecedent, reason, original, chain, chainedexc, xc_chain, excprev, previous und precursor. Für eine explizit verkettete Exception schlägt diese PEP __cause__ wegen seiner spezifischen Bedeutung vor. Für eine implizit verkettete Exception schlägt diese PEP den Namen __context__ vor, da die beabsichtigte Bedeutung spezifischer ist als zeitliche Vorrangigkeit, aber weniger spezifisch als Kausalität: Eine Exception tritt im Kontext der Behandlung einer anderen Exception auf.
Diese PEP schlägt Namen mit führenden und nachfolgenden doppelten Unterstrichen für diese drei Attribute vor, da sie von der Python VM gesetzt werden. Nur in sehr speziellen Fällen sollten sie durch normale Zuweisung gesetzt werden.
Diese PEP behandelt Exceptions, die während except-Blöcken und finally-Blöcken auftreten, auf dieselbe Weise. Das Lesen des Tracebacks macht deutlich, wo die Exceptions aufgetreten sind, daher würden zusätzliche Mechanismen zur Unterscheidung der beiden Fälle nur unnötige Komplexität hinzufügen.
Diese PEP schlägt vor, dass das äußerste Exception-Objekt (dasjenige, das für den Abgleich durch except-Klauseln verfügbar gemacht wird) die zuletzt ausgelöste Exception ist, um die Kompatibilität mit dem aktuellen Verhalten zu gewährleisten.
Diese PEP schlägt vor, dass Tracebacks die äußerste Exception zuletzt anzeigen, da dies mit der chronologischen Reihenfolge von Tracebacks (vom ältesten zum neuesten Frame) übereinstimmt und die tatsächlich ausgelöste Exception leichter in der letzten Zeile zu finden ist.
Um die Dinge einfacher zu halten, werden die C API-Aufrufe zum Setzen einer Exception nicht automatisch den __context__ der Exception setzen. Guido van Rossum hat Bedenken hinsichtlich solcher Änderungen geäußert [8].
Was andere Sprachen betrifft, so verwerfen sowohl Java als auch Ruby die ursprüngliche Exception, wenn eine andere Exception in einer catch/rescue- oder finally/ensure-Klausel auftritt. Perl 5 verfügt nicht über eine integrierte strukturierte Exception-Behandlung. Für Perl 6 schlägt RFC Nummer 88 [9] einen Exception-Mechanismus vor, der verkettete Exceptions implizit in einem Array namens @@ beibehält. In diesem RFC wird die zuletzt ausgelöste Exception für den Abgleich verfügbar gemacht, wie in dieser PEP; außerdem können beliebige Ausdrücke (möglicherweise unter Einbeziehung von @@) zur Exception-Abgleichung ausgewertet werden.
Exceptions in C# enthalten eine schreibgeschützte Eigenschaft InnerException, die auf eine andere Exception verweisen kann. Ihre Dokumentation [10] besagt, dass "Wenn eine Exception X als direkte Folge einer vorherigen Exception Y ausgelöst wird, sollte die Eigenschaft InnerException von X einen Verweis auf Y enthalten." Diese Eigenschaft wird nicht automatisch von der VM gesetzt; stattdessen akzeptieren alle Exception-Konstruktoren ein optionales Argument innerException, um sie explizit zu setzen. Das Attribut __cause__ erfüllt den gleichen Zweck wie InnerException, aber diese PEP schlägt eine neue Form von raise vor, anstatt die Konstruktoren aller Exceptions zu erweitern. C# bietet auch eine Methode GetBaseException, die direkt zum Ende der InnerException-Kette springt; diese PEP schlägt kein Analogon vor.
Der Grund, warum alle drei dieser Attribute in einem Vorschlag zusammen präsentiert werden, ist, dass das Attribut __traceback__ einen bequemen Zugriff auf den Traceback bei verketteten Exceptions ermöglicht.
Implizite Exception Chaining
Hier ist ein Beispiel zur Veranschaulichung des Attributs __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()
Der Aufruf von compute(0, 0) verursacht eine ZeroDivisionError. Die Funktion compute() fängt diese Exception ab und ruft log(exc) auf, aber die Funktion log() löst ebenfalls eine Exception aus, wenn sie versucht, in eine Datei zu schreiben, die nicht zum Schreiben geöffnet wurde.
Im heutigen Python erhält der Aufrufer von compute() eine IOError. Die ZeroDivisionError geht verloren. Mit der vorgeschlagenen Änderung hat die Instanz von IOError ein zusätzliches Attribut __context__, das die ZeroDivisionError beibehält.
Das folgende, ausführlichere Beispiel demonstriert die Behandlung einer Mischung aus finally- und except-Klauseln
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'
Der Aufruf von main() mit dem Namen einer vorhandenen Datei löst vier Exceptions aus. Das Endergebnis ist eine AttributeError aufgrund des Tippfehlers in clos, deren __context__ auf eine NameError aufgrund des Tippfehlers in ex verweist, deren __context__ auf eine IOError verweist, da die Datei schreibgeschützt ist, deren __context__ auf eine ZeroDivisionError verweist, deren Attribut __context__ None ist.
Die vorgeschlagene Semantik lautet wie folgt
- Jeder Thread hat einen Exception-Kontext, der anfangs auf
Nonegesetzt ist. - Wenn eine Exception ausgelöst wird, und die Exception-Instanz noch kein
__context__-Attribut hat, setzt der Interpreter es gleich dem Exception-Kontext des Threads. - Unmittelbar nach dem Auslösen einer Exception wird der Exception-Kontext des Threads auf die Exception gesetzt.
- Wenn der Interpreter einen
except-Block verlässt, indem er das Ende erreicht oder einereturn-,yield-,continue- oderbreak-Anweisung ausführt, wird der Exception-Kontext des Threads aufNonegesetzt.
Explizite Exception Chaining
Das Attribut __cause__ auf Exception-Objekten wird immer auf None initialisiert. Es wird durch eine neue Form der raise-Anweisung gesetzt.
raise EXCEPTION from CAUSE
was äquivalent ist zu
exc = EXCEPTION
exc.__cause__ = CAUSE
raise exc
Im folgenden Beispiel stellt eine Datenbank Implementierungen für verschiedene Speicherarten bereit, darunter Dateispeicher. Der Datenbankentwickler möchte, dass Fehler als DatabaseError-Objekte weitergegeben werden, damit der Client die speicherspezifischen Details nicht kennen muss, aber die zugrunde liegenden Fehlerinformationen nicht verlieren.
class DatabaseError(StandardError):
pass
class FileDatabase(Database):
def __init__(self, filename):
try:
self.file = open(filename)
except IOError, exc:
raise DatabaseError('failed to open') from exc
Wenn der Aufruf von open() eine Exception auslöst, wird das Problem als DatabaseError mit einem __cause__-Attribut gemeldet, das die IOError als ursprüngliche Ursache offenbart.
Traceback-Attribut
Das folgende Beispiel illustriert das Attribut __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 ...)
...
Im heutigen Python müsste die Funktion do_logged() den Traceback aus sys.exc_traceback oder sys.exc_info() [2] extrahieren und sowohl den Wert als auch den Traceback an write_exception() übergeben. Mit der vorgeschlagenen Änderung erhält write_exception() einfach ein Argument und ruft die Exception über das Attribut __traceback__ ab.
Die vorgeschlagene Semantik lautet wie folgt
- Wenn eine Exception abgefangen wird und die Exception-Instanz noch kein
__traceback__-Attribut hat, setzt der Interpreter es auf den neu abgefangenen Traceback.
Erweiterte Berichterstattung
Der Standard-Exception-Handler wird so modifiziert, dass er verkettete Exceptions meldet. Die Kette von Exceptions wird durch Befolgen der Attribute __cause__ und __context__ durchlaufen, wobei __cause__ Vorrang hat. Im Einklang mit der chronologischen Reihenfolge von Tracebacks wird die zuletzt ausgelöste Exception zuletzt angezeigt; d.h., die Anzeige beginnt mit der Beschreibung der innersten Exception und arbeitet sich die Kette bis zur äußersten Exception zurück. Die Tracebacks werden wie üblich formatiert, wobei eine der Zeilen
The above exception was the direct cause of the following exception:
oder
During handling of the above exception, another exception occurred:
zwischen den Tracebacks, je nachdem, ob sie durch __cause__ oder __context__ verbunden sind. Hier ist ein Entwurf des Verfahrens
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)
Im Modul traceback werden die Funktionen format_exception, print_exception, print_exc und print_last aktualisiert, um ein optionales Argument chain zu akzeptieren, das standardmäßig True ist. Wenn dieses Argument True ist, formatieren oder zeigen diese Funktionen die gesamte Kette von Exceptions wie beschrieben an. Wenn es False ist, formatieren oder zeigen diese Funktionen nur die äußerste Exception an.
Das Modul cgitb sollte ebenfalls aktualisiert werden, um die gesamte Kette von Exceptions anzuzeigen.
C API
Die PyErr_Set*-Aufrufe zum Setzen von Exceptions setzen das Attribut __context__ auf Exceptions nicht. PyErr_NormalizeException setzt immer das Attribut traceback auf sein tb-Argument und die Attribute __context__ und __cause__ auf None.
Eine neue API-Funktion, PyErr_SetContext(context), wird C-Programmierern helfen, Informationen über verkettete Exceptions bereitzustellen. Diese Funktion normalisiert zuerst die aktuelle Exception, so dass sie eine Instanz ist, und setzt dann ihr Attribut __context__. Eine ähnliche API-Funktion, PyErr_SetCause(cause), setzt das Attribut __cause__.
Kompatibilität
Verkettete Exceptions zeigen den Typ der letzten Exception an, sodass sie immer noch mit denselben except-Klauseln übereinstimmen wie bisher.
Die vorgeschlagenen Änderungen sollten keinen Code brechen, es sei denn, er setzt oder verwendet Attribute mit den Namen __context__, __cause__ oder __traceback__ auf Exception-Instanzen. Stand 12.05.2005 gibt es in der Python-Standardbibliothek keine Erwähnung solcher Attribute.
Offene Frage: Zusätzliche Informationen
Walter Dörwald [11] äußerte den Wunsch, zusätzliche Informationen an eine Exception während ihrer aufsteigenden Weitergabe anzuhängen, ohne ihren Typ zu ändern. Dies könnte eine nützliche Funktion sein, wird aber in dieser PEP nicht behandelt. Sie könnte denkbar durch eine separate PEP behandelt werden, die Konventionen für andere Informationsattribute auf Exceptions festlegt.
Offene Frage: Unterdrücken des Kontexts
Wie geschrieben, macht diese PEP es unmöglich, __context__ zu unterdrücken, da das Setzen von exc.__context__ auf None in einer except- oder finally-Klausel nur dazu führt, dass es erneut gesetzt wird, wenn exc ausgelöst wird.
Offene Frage: Begrenzung von Exception-Typen
Zur Verbesserung der Kapselung möchten Bibliotheksentwickler möglicherweise alle Implementierungs-Level-Exceptions mit einer Anwendungs-Level-Exception wrappen. Man könnte versuchen, Exceptions zu wrappen, indem man dies schreibt
try:
... implementation may raise an exception ...
except:
import sys
raise ApplicationError from sys.exc_value
oder dies
try:
... implementation may raise an exception ...
except Exception, exc:
raise ApplicationError from exc
aber beide sind teilweise fehlerhaft. Es wäre wünschenswert, die aktuelle Exception in einer Catch-all except-Klausel benennen zu können, aber das wird hier nicht behandelt. Eine solche Funktion würde so etwas wie das Folgende ermöglichen
try:
... implementation may raise an exception ...
except *, exc:
raise ApplicationError from exc
Offene Frage: yield
Der Exception-Kontext geht verloren, wenn eine yield-Anweisung ausgeführt wird; die Fortsetzung des Frames nach dem yield stellt den Kontext nicht wieder her. Die Behandlung dieses Problems liegt außerhalb des Rahmens dieser PEP; es ist kein neues Problem, wie das folgende Beispiel zeigt
>>> 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
Offene Frage: Garbage Collection
Der stärkste Einwand gegen diesen Vorschlag ist, dass er Zyklen zwischen Exceptions und Stack-Frames erzeugt [12]. Die Sammlung von zyklischem Müll (und damit die Freigabe von Ressourcen) kann stark verzögert werden
>>> try:
>>> 1/0
>>> except Exception, err:
>>> pass
erzeugt einen Zyklus von err -> traceback -> stack frame -> err, wodurch alle lokalen Variablen im selben Gültigkeitsbereich am Leben gehalten werden, bis der nächste GC stattfindet.
Heute würden diese lokalen Variablen den Gültigkeitsbereich verlassen. Es gibt viel Code, der davon ausgeht, dass "lokale" Ressourcen – insbesondere offene Dateien – schnell geschlossen werden. Wenn das Schließen auf den nächsten GC warten muss, kann ein Programm (das heute einwandfrei läuft) keine Dateihandles mehr haben.
Die Umwandlung des Attributs __traceback__ in eine schwache Referenz würde die Probleme mit zyklischem Müll vermeiden. Unglücklicherweise würde das Speichern der Exception für später (wie unittest tut) umständlicher machen und nicht so viel Bereinigung des sys-Moduls erlauben.
Eine mögliche alternative Lösung, die von Adam Olsen vorgeschlagen wurde, wäre, stattdessen die Referenz vom Stack-Frame zur Variablen err in eine schwache Referenz umzuwandeln, wenn die Variable den Gültigkeitsbereich verlässt [13].
Mögliche zukünftige kompatible Änderungen
Diese Änderungen sind konsistent mit dem Erscheinen von Exceptions als einem einzelnen Objekt anstelle eines Tupels auf Interpreter-Ebene.
- Wenn PEP 340 oder PEP 343 akzeptiert wird, ersetzen Sie die drei Argumente (
type,value,traceback) für__exit__durch ein einzelnes Exception-Argument. - Veralten von
sys.exc_type,sys.exc_value,sys.exc_tracebackundsys.exc_info()zugunsten eines einzelnen Elements,sys.exception. - Veralten von
sys.last_type,sys.last_valueundsys.last_tracebackzugunsten eines einzelnen Elements,sys.last_exception. - Veralten der dreiteiligen Form der
raise-Anweisung zugunsten der einteiligen Form. - Upgrade von
cgitb.html(), um ein einzelnes Wertargument als erstes Argument anstelle eines Tupels(type, value, traceback)zu akzeptieren.
Mögliche zukünftige inkompatible Änderungen
Diese Änderungen könnten für Python 3000 in Betracht gezogen werden.
- Entfernen von
sys.exc_type,sys.exc_value,sys.exc_tracebackundsys.exc_info(). - Entfernen von
sys.last_type,sys.last_valueundsys.last_traceback. - Ersetzen von
sys.excepthookmit drei Argumenten durch eine einteilige API und Anpassung des Modulscgitbzur Angleichung. - Entfernen der dreiteiligen Form der
raise-Anweisung. - Upgrade von
traceback.print_exception, um einexception-Argument anstelle der Argumentetype,valueundtracebackzu akzeptieren.
Danksagungen
Brett Cannon, Greg Ewing, Guido van Rossum, Jeremy Hylton, Phillip J. Eby, Raymond Hettinger, Walter Dörwald und andere.
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0344.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT