PEP 3134 – Exception Chaining und eingebettete Tracebacks
- Autor:
- Ka-Ping Yee
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 12. Mai 2005
- Python-Version:
- 3.0
- 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ückung 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
- Implementierung
- Danksagungen
- Referenzen
- Urheberrecht
Nummerierungs-Hinweis
Diese PEP begann ihr Leben als PEP 344. Da sie nun für Python 3000 vorgesehen ist, wurde sie in den 3xxx-Bereich verschoben.
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 es vorkommen, dass eine andere Exception (Exception B) auftritt. 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.
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 sys-Modul stellt die aktuelle Exception in drei parallelen Variablen bereit: 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 Drei-Argument-Form, die diese drei Teile akzeptiert. Die Manipulation von Exceptions erfordert oft das parallele Übergeben dieser drei Dinge, was mühsam und fehleranfällig sein kann. Außerdem kann die except-Anweisung nur Zugriff auf den Wert, aber nicht auf den Traceback gewähren. Das Hinzufügen des Attributs __traceback__ zu Exception-Instanzen macht alle Exception-Informationen von einem einzigen Ort aus zugänglich.
Historie
Raymond Hettinger [1] warf im Januar 2003 auf Python-Dev die Frage der maskierten Exceptions auf und schlug eine PyErr_FormatAppend()-Funktion 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 ins Spiel, was zu einer langen Diskussion führte.
Greg Ewing [3] identifizierte den Fall einer Exception, die während des Ausrollens eines finally-Blocks auftritt, ausgelöst durch eine ursprüngliche Exception, als unterschieden von dem Fall einer Exception, die in einem except-Block auftritt, der die ursprüngliche Exception behandelt.
Greg Ewing [4] und Guido van Rossum [5], und wahrscheinlich auch andere, erwähnten bereits zuvor das Hinzufügen eines Traceback-Attributs zu Exception-Instanzen. Dies wird in PEP 3000 erwähnt.
Diese PEP wurde durch eine weitere kürzliche Wiederholung derselben Ideen auf Python-Dev motiviert [6] [7].
Begründung
Die Python-Dev-Diskussionen zeigten Interesse an Exception Chaining für zwei sehr unterschiedliche Zwecke. Um das unerwartete Auslösen 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 behandelt beides.
Mehrere Attributnamen für verkettete Exceptions wurden auf Python-Dev vorgeschlagen [2], darunter cause, antecedent, reason, original, chain, chainedexc, exc_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 als zeitliche Vorrangigkeit, aber weniger spezifisch als Kausalität ist: 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 besonderen 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 die gleiche Weise. Das Lesen des Tracebacks macht deutlich, wo die Exceptions aufgetreten sind, sodass zusätzliche Mechanismen zur Unterscheidung der beiden Fälle nur unnötige Komplexität hinzufügen würden.
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 mit dem aktuellen Verhalten kompatibel zu sein.
Diese PEP schlägt vor, dass Tracebacks die äußerste Exception zuletzt anzeigen, da dies der chronologischen Reihenfolge von Tracebacks (vom ältesten zum neuesten Frame) entspricht und die tatsächlich geworfene Exception leichter in der letzten Zeile zu finden ist.
Um die Dinge einfacher zu halten, setzen die C-API-Aufrufe zum Setzen einer Exception nicht automatisch den __context__ der Exception. 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 im catch/rescue- oder finally/ensure-Klausel eine andere Exception auftritt. Perl 5 verfügt über keine integrierte strukturierte Exception-Behandlung. Für Perl 6 schlägt die RFC-Nummer 88 [9] einen Exception-Mechanismus vor, der implizit verkettete Exceptions in einem Array namens @@ beibehält. In dieser 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 Ausnahmeübereinstimmung ausgewertet werden.
Exceptions in C# enthalten eine schreibgeschützte InnerException-Eigenschaft, 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 InnerException-Eigenschaft von X einen Verweis auf Y enthalten." Diese Eigenschaft wird nicht automatisch von der VM gesetzt; vielmehr nehmen alle Exception-Konstruktoren ein optionales innerException-Argument entgegen, um sie explizit zu setzen. Das Attribut __cause__ erfüllt denselben Zweck wie InnerException, aber diese PEP schlägt eine neue Form von raise vor, anstatt die Konstruktoren aller Exceptions zu erweitern. C# bietet außerdem 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 bietet.
Implizite Exception Chaining
Hier ist ein Beispiel zur Veranschaulichung des __context__-Attributs
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 aufwendigere 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 existierenden 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, weil die Datei schreibgeschützt ist, deren __context__ auf eine ZeroDivisionError verweist, deren __context__-Attribut None ist.
Die vorgeschlagenen Semantiken sind wie folgt:
- Jeder Thread hat einen Exception-Kontext, der initial auf
Nonegesetzt ist. - Immer wenn eine Exception ausgelöst wird, und wenn die Exception-Instanz noch kein
__context__-Attribut hat, setzt der Interpreter dieses auf den 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 mit 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 Arten von Speichern bereit, wobei die Dateispeicherung eine Art ist. Der Datenbankdesigner möchte, dass Fehler als DatabaseError-Objekte weitergegeben werden, damit der Client nicht auf die speicherspezifischen Details aufmerksam gemacht werden muss, möchte aber die zugrunde liegenden Fehlerinformationen nicht verlieren.
class DatabaseError(Exception):
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 erhält die Exception über das Attribut __traceback__.
Die vorgeschlagenen Semantiken sind wie folgt:
- Immer wenn eine Exception abgefangen wird, und wenn die Exception-Instanz noch kein
__traceback__-Attribut hat, setzt der Interpreter dieses auf den neu abgefangenen Traceback.
Erweiterte Berichterstattung
Der Standard-Exception-Handler wird modifiziert, um verkettete Exceptions zu melden. Die Kette von Exceptions wird durch Folgen der Attribute __cause__ und __context__ durchlaufen, wobei __cause__ Vorrang hat. Im Einklang mit der chronologischen Reihenfolge der Tracebacks wird die zuletzt ausgelöste Exception zuletzt angezeigt; das heißt, 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 liegt, abhängig davon, 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 chain-Argument zu akzeptieren, standardmäßig True. Wenn dieses Argument True ist, formatieren oder zeigen diese Funktionen die gesamte Kette von Exceptions wie gerade beschrieben. Wenn es False ist, formatieren oder zeigen diese Funktionen nur die äußerste Exception.
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__ bei 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 legen den Typ der letzten ausgelösten Exception offen, sodass sie weiterhin mit denselben except-Klauseln übereinstimmen, wie sie es jetzt tun.
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. Zum Stand 12.05.2005 enthält die Python-Standardbibliothek keine Erwähnung solcher Attribute.
Offene Frage: Zusätzliche Informationen
Walter Dörwald [11] äußerte den Wunsch, während der aufwärts gerichteten Weitergabe einer Exception zusätzliche Informationen an sie anzuhängen, ohne ihren Typ zu ändern. Dies könnte eine nützliche Funktion sein, wird aber von dieser PEP nicht behandelt. Sie könnte konzeptionell durch eine separate PEP behandelt werden, die Konventionen für andere Informationsattribute bei Exceptions festlegt.
Offene Frage: Unterdrückung des Kontexts
In der vorliegenden Form 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
Um die Kapselung zu verbessern, möchten Bibliotheksentwickler möglicherweise alle implementierungsbezogenen Exceptions mit einer anwendungsbezogenen Exception umhüllen. Man könnte versuchen, Exceptions einzuwickeln, indem man Folgendes 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 beides ist einigermaßen fehlerhaft. Es wäre schön, 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 Wiederaufnahme des Frames nach dem yield stellt den Kontext nicht wieder her. Die Behandlung dieses Problems liegt außerhalb des Geltungsbereichs dieser PEP; es handelt sich nicht um ein 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 war, dass er Zyklen zwischen Exceptions und Stack-Frames erzeugt [12]. Die Sammlung zyklischen Mülls (und damit die Freigabe von Ressourcen) kann stark verzögert werden.
>>> try:
>>> 1/0
>>> except Exception, err:
>>> pass
führt zu einem Zyklus von err -> traceback -> stack frame -> err, wodurch alle locals im selben Gültigkeitsbereich bis zum nächsten GC am Leben gehalten werden.
Heute würden diese locals den Gültigkeitsbereich verlassen. Es gibt viel Code, der davon ausgeht, dass "lokale" Ressourcen – insbesondere offene Dateien – schnell geschlossen werden. Wenn die Schließung auf den nächsten GC warten muss, kann ein Programm (das heute einwandfrei läuft) keine Dateihandles mehr haben.
Das Attribut __traceback__ zu einer schwachen Referenz zu machen, würde die Probleme mit zyklischem Müll vermeiden. Leider würde dies das Speichern der Exception für später (wie unittest es tut) umständlicher machen und nicht so viel Bereinigung des sys-Moduls ermöglichen.
Eine mögliche alternative Lösung, die von Adam Olsen vorgeschlagen wurde, wäre stattdessen, die Referenz vom Stack-Frame zur Variablen err zu einer schwachen Referenz zu machen, wenn die Variable ihren Gültigkeitsbereich verlässt [13].
Mögliche zukünftige kompatible Änderungen
Diese Änderungen sind konsistent mit dem Erscheinungsbild von Exceptions als einzelnes Objekt anstelle eines Tripels 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 Ausnahmeargument. - Stellen Sie
sys.exc_type,sys.exc_value,sys.exc_tracebackundsys.exc_info()als veraltet dar und bevorzugen Sie stattdessen ein einzelnes Mitglied,sys.exception. - Stellen Sie
sys.last_type,sys.last_valueundsys.last_tracebackals veraltet dar und bevorzugen Sie stattdessen ein einzelnes Mitglied,sys.last_exception. - Stellen Sie die Drei-Argument-Form der
raise-Anweisung als veraltet dar und bevorzugen Sie die Ein-Argument-Form. - Aktualisieren Sie
cgitb.html(), um als erstes Argument einen einzelnen Wert anstelle eines(type, value, traceback)-Tupels zu akzeptieren.
Mögliche zukünftige inkompatible Änderungen
Diese Änderungen könnten für Python 3000 in Betracht gezogen werden.
- Entfernen Sie
sys.exc_type,sys.exc_value,sys.exc_tracebackundsys.exc_info(). - Entfernen Sie
sys.last_type,sys.last_valueundsys.last_traceback. - Ersetzen Sie
sys.excepthookin der Drei-Argument-Form durch eine Ein-Argument-API und ändern Sie das Modulcgitbentsprechend. - Entfernen Sie die Drei-Argument-Form der
raise-Anweisung. - Aktualisieren Sie
traceback.print_exception, um einexception-Argument anstelle der Argumentetype,valueundtracebackzu akzeptieren.
Implementierung
Die Attribute __traceback__ und __cause__ sowie die neue raise-Syntax wurden in Revision 57783 [14] implementiert.
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-3134.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT