Wie bereinige versuche versuchen / außer / sonst?

8

Beim Schreiben von Code möchte ich oft Folgendes tun:

try:
    foo()
except FooError:
    handle_foo()
else:
    try:
        bar()
    except BarError:
        handle_bar()
    else:
        try:
            baz()
        except BazError:
            handle_baz()
        else:
            qux()
finally:
    cleanup()

Dies ist offensichtlich völlig unlesbar. Es drückt jedoch eine relativ einfache Idee aus: Führen Sie eine Reihe von Funktionen (oder Kurzcode-Schnipsel) mit jeweils einem Ausnahmebehandler aus und stoppen Sie, sobald eine Funktion ausfällt. Ich stelle mir vor, Python könnte syntaktischen Zucker für diesen Code bereitstellen, vielleicht so etwas:

# NB: This is *not* valid Python
try:
    foo()
except FooError:
    handle_foo()
    # GOTO finally block
else try:
    bar()
except BarError:
    handle_bar()
    # ditto
else try:
    baz()
except BazError:
    handle_baz()
    # ditto
else:
    qux()
finally:
    cleanup()

Wenn keine Ausnahmen gemacht werden, entspricht dies foo();bar();baz();qux();cleanup(). Wenn Ausnahmen ausgelöst werden, werden sie vom entsprechenden Ausnahmebehandler (falls vorhanden) behandelt, und wir fahren mit fort cleanup(). Insbesondere wenn bar()ein FooErroroder BazErrorausgelöst wird, wird die Ausnahme nicht abgefangen und an den Anrufer weitergegeben. Dies ist wünschenswert, damit wir nur Ausnahmen abfangen, die wir wirklich erwarten.

Ist diese Art von Code unabhängig von der syntaktischen Hässlichkeit im Allgemeinen nur eine schlechte Idee? Wenn ja, wie würden Sie es umgestalten? Ich stelle mir vor, dass Kontextmanager verwendet werden könnten, um einen Teil der Komplexität zu absorbieren, aber ich verstehe nicht wirklich, wie das im allgemeinen Fall funktionieren würde.

Kevin
quelle
Was machst du in den handle_*Funktionen?
Winston Ewert

Antworten:

8
try:
    foo()
except FooError:
    handle_foo()
else:
    ...
finally:
    cleanup()

Was macht handle_foodas Es gibt einige Dinge, die wir normalerweise in Ausnahmebehandlungsblöcken tun.

  1. Bereinigung nach dem Fehler: In diesem Fall sollte foo () nach sich selbst bereinigen und uns dies nicht überlassen. Darüber hinaus werden die meisten Bereinigungsjobs am besten erledigtwith
  2. Stellen Sie den glücklichen Weg wieder her: Aber Sie tun dies nicht, da Sie nicht mit den restlichen Funktionen fortfahren.
  3. Übersetzt den Ausnahmetyp: Sie lösen jedoch keine weitere Ausnahme aus
  4. Protokollieren Sie den Fehler: Es sollten jedoch keine speziellen Ausnahmeblöcke für jeden Typ erforderlich sein.

Es scheint mir, dass Sie bei der Ausnahmebehandlung etwas Seltsames tun. Ihre Frage hier ist einfach ein Symptom für die ungewöhnliche Verwendung von Ausnahmen. Sie fallen nicht in das typische Muster, und deshalb ist dies unangenehm geworden.

Ohne eine bessere Vorstellung davon, was Sie in diesen handle_Funktionen tun , ist das alles, was ich sagen kann.

Winston Ewert
quelle
Dies ist nur ein Beispielcode. Die handle_Funktionen können genauso gut kurze Codeausschnitte sein, die (sagen wir) den Fehler protokollieren und Fallback-Werte zurückgeben, neue Ausnahmen auslösen oder eine Reihe anderer Dinge tun. Inzwischen hat die foo(), bar()könnte etc. Funktionen auch kurze Code - Schnipsel sein. Zum Beispiel könnten spam[eggs]und müssen wir fangen KeyError.
Kevin
1
@ Kevin, richtig, aber das ändert nichts an meiner Antwort. Wenn Ihr Handler etwas tut returnoder raisetut, würde das von Ihnen erwähnte Problem nicht auftreten. Der Rest der Funktion wird automatisch übersprungen. Eine einfache Möglichkeit, Ihr Problem zu beheben, besteht darin, in allen Ausnahmeblöcken zurückzukehren. Der Code ist umständlich, weil Sie keine übliche Kaution hinterlegen oder erhöhen, um anzuzeigen, dass Sie aufgeben.
Winston Ewert
Insgesamt eine gute Antwort, aber ich muss Ihrem Punkt 1 nicht zustimmen - welche Beweise gibt es, foodie eine eigene Bereinigung durchführen sollten? foosignalisiert, dass ein Fehler aufgetreten ist, und weiß nicht, wie das richtige Bereinigungsverfahren aussehen soll.
Ethan Furman
@EthanFurman, ich denke, wir denken möglicherweise an verschiedene Elemente unter der Überschrift "Aufräumen". Ich denke, wenn foo Dateien öffnet, Datenbankverbindungen herstellt, Speicher zuweist usw., liegt es in der Verantwortung von foo, sicherzustellen, dass diese geschlossen / freigegeben werden, bevor sie zurückkehren. Das habe ich mit Aufräumen gemeint.
Winston Ewert
Ah, in diesem Fall stimme ich Ihnen vollkommen zu. :)
Ethan Furman
3

Es scheint, als hätten Sie eine Folge von Befehlen, die möglicherweise eine Ausnahme auslösen, die vor der Rückkehr behandelt werden muss. Versuchen Sie, Ihren Code und die Ausnahmebehandlung an verschiedenen Orten zu gruppieren. Ich glaube, das macht, was Sie beabsichtigen.

try:
    foo()
    bar()
    baz()
    qux()

except FooError:
    handle_foo()
except BarError:
    handle_bar()
except BazError:
    handle_baz()

finally:
    cleanup()
BillThor
quelle
2
Mir wurde beigebracht, zu vermeiden, dass mehr Code als unbedingt erforderlich in den try:Block eingefügt wird. In diesem Fall wäre ich besorgt über das bar()Auslösen von a FooError, was mit diesem Code falsch behandelt würde.
Kevin
2
Dies kann sinnvoll sein, wenn alle Methoden sehr spezifische Ausnahmen auslösen, dies ist jedoch im Allgemeinen nicht der Fall. Ich würde diesen Ansatz wärmstens empfehlen.
Winston Ewert
@ Kevin Das Umschließen jeder Codezeile in einen Try Catch Else-Block wird schnell unlesbar. Dies wird durch Pythons Einrückungsregeln noch verstärkt. Überlegen Sie, wie weit dies eingerückt wäre, wenn der Code 10 Zeilen lang wäre. Wäre es lesbar oder wartbar? Solange der Code der Einzelverantwortung folgt, würde ich zu dem obigen Beispiel stehen. Wenn nicht, sollte der Code neu geschrieben werden.
BillThor
@WinstonEwert In einem realen Beispiel würde ich eine gewisse Überlappung der Fehler (Ausnahmen) erwarten. Ich würde jedoch auch erwarten, dass die Handhabung dieselbe ist. BazError könnte ein Tastaturinterrupt sein. Ich würde erwarten, dass es für alle vier Codezeilen angemessen behandelt wird.
BillThor
1

Es gibt verschiedene Möglichkeiten, je nachdem, was Sie benötigen.

Hier ist ein Weg mit Schleifen:

try:
    for func, error, err_handler in (
            (foo, FooError, handle_foo),
            (bar, BarError, handle_bar),
            (baz, BazError, handle_baz),
        ):
        try:
            func()
        except error:
            err_handler()
            break
finally:
    cleanup()

Hier ist ein Weg mit einem Exit nach dem error_handler:

def some_func():
    try:
        try:
            foo()
        except FooError:
            handle_foo()
            return
        try:
            bar()
        except BarError:
            handle_bar()
            return
        try:
            baz()
        except BazError:
            handle_baz()
            return
        else:
            qux()
    finally:
        cleanup()

Persönlich denke ich, dass die Loop-Version leichter zu lesen ist.

Ethan Furman
quelle
IMHO ist dies eine bessere Lösung als die akzeptierte, da es 1) eine Antwort versucht und 2) die Reihenfolge sehr klar macht.
Bob
0

Erstens kann eine angemessene Verwendung von withhäufig einen Großteil des Codes für die Ausnahmebehandlung reduzieren oder sogar eliminieren, wodurch sowohl die Wartbarkeit als auch die Lesbarkeit verbessert werden.

Jetzt können Sie die Verschachtelung auf viele Arten reduzieren. Andere Poster haben bereits einige bereitgestellt. Hier ist meine eigene Variante:

for _ in range(1):
    try:
        foo()
    except FooError:
        handle_foo()
        break
    try:
        bar()
    except BarError:
        handle_bar()
        break
    try:
        baz()
    except BazError:
        handle_baz()
        break
    qux()
cleanup()
Rüschenwind
quelle