Wird 'endlich' immer in Python ausgeführt?

126

Ist für jeden möglichen Try-finally-Block in Python garantiert, dass der finallyBlock immer ausgeführt wird?

Nehmen wir zum Beispiel an, ich kehre in einem exceptBlock zurück:

try:
    1/0
except ZeroDivisionError:
    return
finally:
    print("Does this code run?")

Oder vielleicht erhebe ich noch einmal Exception:

try:
    1/0
except ZeroDivisionError:
    raise
finally:
    print("What about this code?")

Tests zeigen, dass finallydies für die obigen Beispiele ausgeführt wird, aber ich stelle mir vor, dass es andere Szenarien gibt, an die ich nicht gedacht habe.

Gibt es Szenarien, in denen ein finallyBlock in Python nicht ausgeführt werden kann?

Stevoisiak
quelle
18
Der einzige Fall, den ich mir vorstellen kann, finallynicht auszuführen oder "seinen Zweck zu vereiteln", ist während einer Endlosschleife sys.exitoder eines erzwungenen Interrupts. Die Dokumentation besagt, dass finallyimmer ausgeführt wird, also würde ich damit weitermachen.
Xay
1
Ein bisschen Querdenken und sicher nicht das, was Sie gefragt haben, aber ich bin mir ziemlich sicher, dass finallyes nicht ausgeführt wird , wenn Sie den Task-Manager öffnen und den Prozess beenden. Oder das gleiche, wenn der Computer vorher abstürzt: D
Alejandro
144
finallywird nicht ausgeführt, wenn das Netzkabel von der Wand gerissen wird.
user253751
3
Diese Antwort auf dieselbe Frage zu C # könnte Sie interessieren: stackoverflow.com/a/10260233/88656
Eric Lippert
1
Blockiere es auf einem leeren Semaphor. Signalisiere es niemals. Getan.
Martin James

Antworten:

205

"Garantiert" ist ein viel stärkeres Wort als jede Umsetzung von finallyverdient. Was garantiert ist , dass , wenn die Ausführung fließt aus dem Ganzen try- finallyKonstrukt, wird es durch den Pass , finallydies zu tun. Was nicht garantiert ist, ist, dass die Ausführung aus dem try- heraus fließt finally.

  • A finallyin einem Generator oder einer asynchronen Coroutine wird möglicherweise nie ausgeführt , wenn das Objekt niemals zum Abschluss ausgeführt wird. Es gibt viele Möglichkeiten, die passieren können. hier ist eine:

    def gen(text):
        try:
            for line in text:
                try:
                    yield int(line)
                except:
                    # Ignore blank lines - but catch too much!
                    pass
        finally:
            print('Doing important cleanup')
    
    text = ['1', '', '2', '', '3']
    
    if any(n > 1 for n in gen(text)):
        print('Found a number')
    
    print('Oops, no cleanup.')
    

    Beachten Sie, dass dieses Beispiel etwas knifflig ist: Wenn der Generator durch Müll gesammelt wird, versucht Python, den finallyBlock auszuführen , indem eine GeneratorExitAusnahme ausgelöst wird. Hier wird jedoch diese Ausnahme abgefangen, und yieldan diesem Punkt gibt Python eine Warnung aus ("Generator ignoriert GeneratorExit" ") und gibt auf. Weitere Informationen finden Sie in PEP 342 (Coroutinen über erweiterte Generatoren) .

    Andere Möglichkeiten , wie ein Generator oder Koroutine möglicherweise nicht zu dem Schluss führen Sie schließen , wenn das Objekt wird einfach nie GC'ed (ja, das möglich ist, auch in CPython), oder wenn ein async with awaits in __aexit__, oder wenn das Objekt awaits oder yields in einem finallyBlock. Diese Liste erhebt keinen Anspruch auf Vollständigkeit.

  • Ein finallyThread in einem Daemon wird möglicherweise nie ausgeführt, wenn alle Nicht-Daemon-Threads zuerst beendet werden.

  • os._exitstoppt den Prozess sofort, ohne finallyBlöcke auszuführen .

  • os.forkkann zu finallyBlöcken auszuführen zweimal . Neben den normalen Problemen, die Sie von zweimaligen Ereignissen erwarten, kann dies zu gleichzeitigen Zugriffskonflikten (Abstürze, Blockierungen usw.) führen, wenn der Zugriff auf freigegebene Ressourcen nicht korrekt synchronisiert ist .

    Da multiprocessingAnwendungen gabel ohne-exec Arbeitsprozesse zu erstellen , wenn die Verwendung von Gabelstartmethode (der Standard unter Unix), und ruft dann in den Arbeitern einmal die Aufgabe der Arbeiter durchgeführt wird, und kann Interaktion problematisch sein ( Beispiel ).os._exitfinallymultiprocessing

  • Ein Segmentierungsfehler auf C-Ebene verhindert finally, dass Blöcke ausgeführt werden.
  • kill -SIGKILLverhindert finally, dass Blöcke ausgeführt werden. SIGTERMund SIGHUPverhindert auch finallyBlöcke von Lauf , wenn Sie einen Handler installieren Sie das Herunterfahren selbst zu steuern; Standardmäßig verarbeitet Python nicht SIGTERModer SIGHUP.
  • Eine Ausnahme in finallykann verhindern, dass die Bereinigung abgeschlossen wird. Ein besonders bemerkenswerter Fall ist, wenn der Benutzer Control-C drückt, gerade als wir mit der Ausführung des finallyBlocks beginnen. Python löst ein KeyboardInterruptund überspringt jede Zeile des finallyBlockinhalts. ( KeyboardInterrupt-sicherer Code ist sehr schwer zu schreiben).
  • Wenn der Computer die Stromversorgung verliert oder in den Ruhezustand wechselt und nicht aufwacht, finallywerden keine Blöcke ausgeführt.

Der finallyBlock ist kein Transaktionssystem. Es bietet keine Atomgarantien oder ähnliches. Einige dieser Beispiele mögen offensichtlich erscheinen, aber es ist leicht zu vergessen, dass solche Dinge passieren können und sich finallyzu sehr darauf verlassen können.

user2357112 unterstützt Monica
quelle
14
Ich glaube, nur der erste Punkt Ihrer Liste ist wirklich relevant, und es gibt einen einfachen Weg, dies zu vermeiden: 1) Verwenden Sie niemals einen nackten exceptund fangen Sie niemals GeneratorExitin einem Generator. Die Punkte über Threads / Beenden des Prozesses / Segfaulting / Ausschalten werden erwartet, Python kann keine Magie ausführen. Auch: Ausnahmen in finallysind offensichtlich ein Problem , aber das macht nichts an der Tatsache , dass der Steuerfluss wurde auf dem bewegten finallyBlock. In Bezug auf Ctrl+C, können Sie einen Signal - Handler , dass sie ignoriert oder einfach „ Zeitplan“ eine saubere Abschaltung nach dem aktuellen Vorgang abgeschlossen ist hinzuzufügen.
Giacomo Alzetta
8
Die Erwähnung von kill -9 ist technisch korrekt, aber etwas unfair. Kein Programm, das in einer Sprache geschrieben ist, führt nach Erhalt eines Kill -9 einen Code aus. Tatsächlich erhält kein Programm jemals einen Kill -9. Selbst wenn es dies wollte, konnte es nichts ausführen. Das ist der springende Punkt bei Kill -9.
Tom
10
@ Tom: Der Punkt über kill -9hat keine Sprache angegeben. Und ehrlich gesagt muss es wiederholt werden, weil es an einem blinden Fleck sitzt. Zu viele Leute vergessen oder merken nicht, dass ihr Programm auf der Strecke bleiben könnte, ohne dass sie überhaupt aufräumen dürfen.
CHao
5
@GiacomoAlzetta: Es gibt Leute da draußen, die sich auf finallyBlöcke verlassen, als ob sie Transaktionsgarantien geben würden. Es mag offensichtlich erscheinen, dass dies nicht der Fall ist, aber es ist nicht jedermanns Sache. Was den Generatorfall betrifft, gibt es viele Möglichkeiten, wie ein Generator überhaupt nicht GC-fähig sein kann, und viele Möglichkeiten, wie ein Generator oder eine Coroutine versehentlich nachgeben kann, GeneratorExitselbst wenn er die nicht erfasst GeneratorExit, z. B. wenn ein async withSuspendiert eine Coroutine in __exit__.
user2357112 unterstützt Monica
2
@ user2357112 yeah - Ich habe jahrzehntelang versucht, Entwickler dazu zu bringen, temporäre Dateien usw. beim Start der App zu bereinigen, nicht zu beenden. Sich auf die sogenannte "Aufräumen und anmutige Kündigung" zu verlassen, bittet um Enttäuschung und Tränen :)
Martin James
69

Ja. Endlich gewinnt immer.

Die einzige Möglichkeit, dies zu verhindern, besteht darin, die Ausführung anzuhalten, bevor finally:die Ausführung möglich ist (z. B. den Interpreter zum Absturz bringen, den Computer ausschalten, einen Generator für immer anhalten ).

Ich stelle mir vor, es gibt andere Szenarien, an die ich nicht gedacht habe.

Hier sind noch ein paar, an die Sie vielleicht nicht gedacht haben:

def foo():
    # finally always wins
    try:
        return 1
    finally:
        return 2

def bar():
    # even if he has to eat an unhandled exception, finally wins
    try:
        raise Exception('boom')
    finally:
        return 'no boom'

Abhängig davon, wie Sie den Interpreter verlassen, können Sie manchmal endgültig "abbrechen", aber nicht so:

>>> import sys
>>> try:
...     sys.exit()
... finally:
...     print('finally wins!')
... 
finally wins!
$

Verwendung der prekären os._exit(dies fällt meiner Meinung nach unter "Absturz des Dolmetschers"):

>>> import os
>>> try:
...     os._exit(1)
... finally:
...     print('finally!')
... 
$

Ich führe derzeit diesen Code aus, um zu testen, ob er nach dem Hitzetod des Universums endlich noch ausgeführt wird:

try:
    while True:
       sleep(1)
finally:
    print('done')

Ich warte jedoch immer noch auf das Ergebnis. Schauen Sie später noch einmal hier vorbei.

wim
quelle
5
oder mit einer endlichen Schleife in try catch
sapy
8
finallyIn einem Generator oder einer Coroutine kann die Ausführung leicht fehlschlagen , ohne dass die Bedingung "Absturz des Dolmetschers" erreicht wird.
user2357112 unterstützt Monica
27
Nach dem Hitzetod des Universums hört die Zeit auf zu existieren, sleep(1)was definitiv zu undefiniertem Verhalten führen würde. :-D
David Foerster
Möglicherweise möchten Sie _os.exit direkt nach "Der einzige Weg, es zu besiegen, ist das Abstürzen des Compilers" erwähnen. Im Moment sind es gemischte Beispiele, bei denen endlich gewonnen wird.
Stevoisiak
2
@StevenVascellaro Ich denke nicht, dass dies notwendig ist - os._exitist praktisch das Gleiche wie das Auslösen eines Absturzes (unreiner Exit). Der richtige Weg zum Verlassen ist sys.exit.
wim
9

Laut der Python-Dokumentation :

Unabhängig davon, was zuvor passiert ist, wird der letzte Block ausgeführt, sobald der Codeblock vollständig ist und alle ausgelösten Ausnahmen behandelt wurden. Selbst wenn ein Fehler in einem Ausnahmebehandler oder im else-Block auftritt und eine neue Ausnahme ausgelöst wird, wird der Code im letzten Block weiterhin ausgeführt.

Es sollte auch beachtet werden, dass, wenn mehrere return-Anweisungen vorhanden sind, einschließlich einer im finally-Block, die finally-Block-Rückgabe die einzige ist, die ausgeführt wird.

Jayce
quelle
8

Ja und nein.

Was garantiert ist, ist, dass Python immer versucht, den finally-Block auszuführen. Wenn Sie vom Block zurückkehren oder eine nicht erfasste Ausnahme auslösen, wird der finally-Block unmittelbar vor dem tatsächlichen Zurückgeben oder Auslösen der Ausnahme ausgeführt.

(Was Sie selbst hätten kontrollieren können, indem Sie einfach den Code in Ihrer Frage ausgeführt hätten)

Der einzige Fall, in dem ich mir vorstellen kann, wo der finally-Block nicht ausgeführt wird, ist, wenn der Python-Interpreter selbst abstürzt, beispielsweise innerhalb von C-Code oder aufgrund eines Stromausfalls.

Serge Ballesta
quelle
ha ha .. oder es gibt eine Endlosschleife in try catch
sapy
Ich denke "Nun ja und nein" ist am richtigsten. Schließlich: gewinnt immer, wobei "immer" bedeutet, dass der Interpreter ausgeführt werden kann und der Code für "finally:" noch verfügbar ist. "Wins" wird definiert, wenn der Interpreter versucht, den finally: -Block auszuführen, und dies ist erfolgreich. Das ist das "Ja" und es ist sehr bedingt. "Nein" ist alle Möglichkeiten, wie der Interpreter vor "endlich" anhalten kann: - Stromausfall, Hardwarefehler, Kill -9 für den Interpreter, Fehler im Interpreter oder Code, von dem er abhängt, andere Möglichkeiten, den Interpreter aufzuhängen. Und Möglichkeiten, im "endlich:" zu hängen.
Bill IV
1

Ich habe diese gefunden, ohne eine Generatorfunktion zu verwenden:

import multiprocessing
import time

def fun(arg):
  try:
    print("tried " + str(arg))
    time.sleep(arg)
  finally:
    print("finally cleaned up " + str(arg))
  return foo

list = [1, 2, 3]
multiprocessing.Pool().map(fun, list)

Der Ruhezustand kann ein beliebiger Code sein, der möglicherweise inkonsistent ausgeführt wird.

Was hier zu passieren scheint, ist, dass der erste parallele Prozess, der beendet wird, den try-Block erfolgreich verlässt, dann aber versucht, von der Funktion einen Wert (foo) zurückzugeben, der nirgendwo definiert wurde, was eine Ausnahme verursacht. Diese Ausnahme beendet die Karte, ohne dass die anderen Prozesse ihre endgültigen Blöcke erreichen können.

Wenn Sie die Zeile bar = bazzdirekt nach dem Aufruf von sleep () im try-Block hinzufügen . Dann löst der erste Prozess, der diese Linie erreicht, eine Ausnahme aus (da bazz nicht definiert ist), die bewirkt, dass sein eigener finally-Block ausgeführt wird, aber dann die Karte beendet, wodurch die anderen try-Blöcke verschwinden, ohne ihre finally-Blöcke zu erreichen, und der erste Prozess, der auch seine return-Anweisung nicht erreicht.

Für die Python-Mehrfachverarbeitung bedeutet dies, dass Sie dem Mechanismus zur Ausnahmebehandlung nicht vertrauen können, um Ressourcen in allen Prozessen zu bereinigen, wenn auch nur einer der Prozesse eine Ausnahme haben kann. Zusätzliche Signalverarbeitung oder Verwaltung der Ressourcen außerhalb des Multiprocessing-Map-Aufrufs wäre erforderlich.

Blair Houghton
quelle
-2

Nachtrag zur akzeptierten Antwort, nur um zu sehen, wie es funktioniert, mit ein paar Beispielen:

  • Dies:

     try:
         1
     except:
         print 'except'
     finally:
         print 'finally'

    wird ausgegeben

    endlich

  •    try:
           1/0
       except:
           print 'except'
       finally:
           print 'finally'

    wird ausgegeben

    außer
    endlich

Basj
quelle
Dies beantwortet die Frage überhaupt nicht. Es gibt nur Beispiele dafür, wann es funktioniert .
Zixuan