Czy „w końcu” zawsze jest wykonywane w Pythonie?

126

Czy dla każdego możliwego bloku try-Final w Pythonie jest zagwarantowane, że finallyblok będzie zawsze wykonywany?

Na przykład, powiedzmy, że wracam będąc w exceptbloku:

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

A może przebijam ponownie Exception:

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

Testy pokazują, że finallytak się dzieje w powyższych przykładach, ale wyobrażam sobie, że istnieją inne scenariusze, o których nie pomyślałem.

Czy istnieją scenariusze, w których finallyblok może nie zostać wykonany w Pythonie?

Stevoisiak
źródło
18
Jedyny przypadek, w którym mogę sobie wyobrazić finallyniepowodzenie w wykonaniu lub „pokonaniu jego celu”, to nieskończona pętla sys.exitlub wymuszone przerwanie. W dokumentacji stwierdza, że finallyzawsze jest tak wykonana, że pójdę z tym.
Xay
1
Trochę myślenia bocznego i na pewno nie to, o co prosiłeś, ale jestem prawie pewien, że jeśli otworzysz Menedżera zadań i zabijesz proces, finallynie zadziała. Albo tak samo, jeśli komputer się wcześniej zawiesił: D
Alejandro
145
finallynie zadziała, jeśli przewód zasilający zostanie wyrwany ze ściany.
user253751
3
Możesz być zainteresowany tą odpowiedź na to samo pytanie o C #: stackoverflow.com/a/10260233/88656
Eric Lippert
1
Zablokuj go na pustym semaforze. Nigdy tego nie sygnalizuj. Gotowe.
Martin James,

Odpowiedzi:

207

„Gwarantowane” to o wiele mocniejsze słowo, niż jakakolwiek realizacja na to finallyzasługuje. Gwarantowane jest to, że jeśli wykonanie wypłynie z całości try- finallyconstruct, przejdzie przez to, finallyaby to zrobić. Nie można zagwarantować, że wykonanie wypłynie z try- finally.

  • A finallyw generatorze lub programie asynchronicznym może nigdy nie działać , jeśli obiekt nigdy nie zostanie zakończony. Może się to zdarzyć na wiele sposobów; Tu jest jeden:

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

    Zauważ, że ten przykład jest trochę skomplikowany: kiedy generator jest zbierany jako śmieci, Python próbuje uruchomić finallyblok, rzucając GeneratorExitwyjątek, ale tutaj łapiemy ten wyjątek i yieldznowu, w którym to momencie Python wyświetla ostrzeżenie ("generator zignorowany GeneratorExit ”) i poddaje się. Aby uzyskać szczegółowe informacje, patrz PEP 342 (Coroutines via Enhanced Generators) .

    Inne sposoby, w jakie generator lub procedura mogą nie wykonać, aby wyciągnąć wnioski, obejmują to, czy obiekt nigdy nie został poddany GC (tak, jest to możliwe, nawet w CPythonie), lub jeśli async with awaitjest w __aexit__lub jeśli obiekt awaitjest lub yieldjest w finallybloku. Ta lista nie jest wyczerpująca.

  • A finallyw wątku demona może nigdy nie zostać wykonany, jeśli wszystkie wątki inne niż demonowe zakończą się jako pierwsze.

  • os._exitnatychmiast zatrzyma proces bez wykonywania finallybloków.

  • os.forkmoże spowodować dwukrotne wykonaniefinally bloków . Oprócz zwykłych problemów, których można się spodziewać po zdarzeniach podwójnych, może to powodować współbieżne konflikty dostępu (awarie, blokady, ...), jeśli dostęp do współdzielonych zasobów nie jest poprawnie zsynchronizowany .

    Ponieważ multiprocessingużywa fork-without-exec do tworzenia procesów roboczych podczas korzystania z metody fork start (domyślna w systemie Unix), a następnie wywołuje os._exitproces roboczy po zakończeniu pracy pracownika, finallya multiprocessinginterakcja może być problematyczna ( przykład ).

  • Błąd segmentacji na poziomie C uniemożliwi finallyuruchomienie bloków.
  • kill -SIGKILLzapobiegnie finallyuruchamianiu bloków. SIGTERMa SIGHUPtakże uniemożliwi finallyuruchamianie bloków, chyba że zainstalujesz program obsługi, który sam kontroluje zamknięcie; domyślnie Python nie obsługuje SIGTERMani SIGHUP.
  • Wyjątek w programie finallymoże uniemożliwić ukończenie czyszczenia. Szczególnie godny uwagi jest przypadek, gdy użytkownik kliknie Ctrl-C tylko jako zaczynamy wykonać finallyblok. Python podniesie a KeyboardInterrupti pominie każdy wiersz finallyzawartości bloku. ( KeyboardInterrupt-bezpieczny kod jest bardzo trudny do napisania).
  • Jeśli komputer traci moc lub przechodzi w stan hibernacji i nie budzi się, finallybloki nie będą działać.

finallyBlok nie jest system transakcyjny; nie zapewnia gwarancji atomowości ani niczego w tym rodzaju. Niektóre z tych przykładów mogą wydawać się oczywiste, ale łatwo jest zapomnieć, że takie rzeczy mogą się zdarzyć i polegać na finallyzbyt dużym stopniu.

user2357112 obsługuje Monikę
źródło
14
Uważam, że tylko pierwszy punkt twojej listy jest naprawdę istotny i jest łatwy sposób na uniknięcie tego: 1) nigdy nie używaj gołego excepti nigdy nie łapaj GeneratorExitw generatorze. Spodziewane są punkty dotyczące wątków / zabijania procesu / segfaultingu / wyłączania zasilania, Python nie może robić magii. Ponadto: wyjątki w programie finallysą oczywiście problemem, ale nie zmienia to faktu, że przepływ sterowania został przeniesiony do finallybloku. Jeśli chodzi o to Ctrl+C, możesz dodać procedurę obsługi sygnału, która go ignoruje lub po prostu „planuje” czyste zamknięcie po zakończeniu bieżącej operacji.
Giacomo Alzetta,
8
Wzmianka o kill -9 jest technicznie poprawna, ale trochę niesprawiedliwa. Żaden program napisany w jakimkolwiek języku nie uruchamia żadnego kodu po otrzymaniu komunikatu kill -9. W rzeczywistości żaden program w ogóle nie otrzymuje kill -9, więc nawet gdyby chciał, nie mógłby niczego wykonać. O to chodzi w zabijaniu -9.
Tom
10
@Tom: Punkt dotyczący kill -9nie określa języka. I szczerze mówiąc, wymaga powtórzenia, ponieważ znajduje się w martwym punkcie. Zbyt wiele osób zapomina lub nie zdaje sobie sprawy, że ich program może zostać zatrzymany bez pozwolenia nawet na wyczyszczenie.
cHao
5
@GiacomoAlzetta: Są ludzie, którzy polegają na finallyblokach tak, jakby udzielali gwarancji transakcyjnych. Może się wydawać oczywiste, że tak nie jest, ale nie wszyscy zdają sobie z tego sprawę. Jeśli chodzi o obudowę generatora, istnieje wiele sposobów, w jakie generator może w ogóle nie zostać poddany GC, i wiele sposobów, w jakie generator lub program może przypadkowo ustąpić, GeneratorExitnawet jeśli nie łapie GeneratorExit, na przykład jeśli async withzawiesza się program w __exit__.
user2357112 obsługuje Monikę
2
@ user2357112 yeah - Od dziesięcioleci próbuję skłonić programistów do czyszczenia plików tymczasowych itp. podczas uruchamiania aplikacji, a nie zamykania. Poleganie na tzw. „Sprzątaniu i wdzięcznym zakończeniu”, prosi o rozczarowanie i łzy :)
Martin James
68

Tak. Wreszcie zawsze wygrywa.

Jedynym sposobem na pokonanie tego jest zatrzymanie wykonywania, zanim finally:będzie można je wykonać (np. Zawieszenie interpretera, wyłączenie komputera, zawieszenie generatora na zawsze).

Wyobrażam sobie, że są inne scenariusze, o których nie pomyślałem.

Oto kilka innych, o których być może nie pomyślałeś:

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'

W zależności od tego, jak zrezygnowałeś z tłumaczenia, czasami możesz ostatecznie „anulować”, ale nie w ten sposób:

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

Używanie precarious os._exit(moim zdaniem zalicza się to do „crash the interpreter”):

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

Obecnie uruchamiam ten kod, aby sprawdzić, czy w końcu nadal będzie działać po śmierci wszechświata na gorąco:

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

Jednak wciąż czekam na wynik, więc wróć tutaj później.

wim
źródło
5
lub mając skończoną pętlę w try catch
sapy
8
finallyw generatorze lub programie może z łatwością zakończyć się niepowodzeniem , bez zbliżania się do stanu „zawieszenie interpretera”.
user2357112 obsługuje Monikę
27
Po termicznej śmierci wszechświata czas przestaje istnieć, więc sleep(1)z pewnością skutkowałoby nieokreślonym zachowaniem. :-D
David Foerster
Możesz zechcieć wspomnieć o _os.exit bezpośrednio po „jedynym sposobem na pokonanie tego jest awaria kompilatora”. W tej chwili jest to zmieszane z kilkunastoma przykładami, w których ostatecznie wygrywa.
Stevoisiak
2
@StevenVascellaro Nie sądzę, że jest to konieczne - os._exitze wszystkich praktycznych względów jest tym samym, co spowodowanie awarii (nieczyste wyjście). Prawidłowy sposób wyjścia to sys.exit.
wim
9

Zgodnie z dokumentacją Pythona :

Bez względu na to, co stało się wcześniej, ostatni blok jest wykonywany po zakończeniu bloku kodu i obsłużeniu wszelkich podniesionych wyjątków. Nawet jeśli wystąpi błąd w programie obsługi wyjątków lub bloku else i zostanie zgłoszony nowy wyjątek, kod w ostatnim bloku jest nadal wykonywany.

Należy również zauważyć, że jeśli istnieje wiele instrukcji powrotu, w tym jedna w bloku final, to jedyny, który zostanie wykonany, będzie zwracany ostatni blok.

jayce
źródło
8

Cóż, tak i nie.

Gwarantowane jest to, że Python zawsze będzie próbował wykonać ostatni blok. W przypadku powrotu z bloku lub zgłoszenia nieprzechwyconego wyjątku, ostatni blok jest wykonywany tuż przed faktycznym zwróceniem lub podniesieniem wyjątku.

(co mogłeś kontrolować, uruchamiając kod w swoim pytaniu)

Jedyny przypadek, jaki mogę sobie wyobrazić, w którym ostatni blok nie zostanie wykonany, to sytuacja, w której sam interpreter Pythona ulega awarii, na przykład w kodzie C lub z powodu przerwy w zasilaniu.

Serge Ballesta
źródło
ha ha .. lub jest nieskończona pętla w try catch
sapy
Myślę, że „Cóż, tak i nie” jest najbardziej poprawne. Wreszcie: zawsze wygrywa, gdzie „zawsze” oznacza, że ​​interpreter jest w stanie uruchomić, a kod funkcji „final:” jest nadal dostępny, a „wygrywa” jest definiowany jako interpreter będzie próbował uruchomić blok final: i zakończy się powodzeniem. To jest „tak” i jest to bardzo warunkowe. „Nie” to wszystkie sposoby, w jakie interpreter może się zatrzymać, zanim „w końcu”: - awaria zasilania, awaria sprzętu, kill -9 skierowane do interpretera, błędy interpretera lub kodu, od których zależy, inne sposoby zawieszenia interpretera. I sposoby na zawieszenie „na końcu”.
Bill IV
1

Znalazłem ten bez użycia funkcji generatora:

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)

Uśpienie może być dowolnym kodem, który może działać przez niespójne okresy czasu.

Wydaje się, że dzieje się tutaj to, że pierwszy równoległy proces kończący pomyślnie opuszcza blok try, a następnie próbuje zwrócić z funkcji wartość (foo), która nie została nigdzie zdefiniowana, co powoduje wyjątek. Ten wyjątek zabija mapę, nie pozwalając innym procesom dotrzeć do ich ostatecznych bloków.

Ponadto, jeśli dodasz linię bar = bazztuż po wywołaniu funkcji sleep () w bloku try. Następnie pierwszy proces osiągający tę linię zgłasza wyjątek (ponieważ bazz nie jest zdefiniowany), który powoduje uruchomienie własnego bloku końcowego, ale następnie zabija mapę, powodując, że inne bloki próbne znikają bez dotarcia do ich ostatecznych bloków, i również pierwszy proces, aby nie dotrzeć do instrukcji return.

W przypadku wieloprocesorowości Pythona oznacza to, że nie można ufać mechanizmowi obsługi wyjątków w zakresie czyszczenia zasobów we wszystkich procesach, jeśli nawet jeden z procesów może mieć wyjątek. Konieczna byłaby dodatkowa obsługa sygnału lub zarządzanie zasobami poza wywołaniem mapy wieloprocesowej.

Blair Houghton
źródło
-2

Dodatek do zaakceptowanej odpowiedzi, aby pomóc zobaczyć, jak to działa, z kilkoma przykładami:

  • To:

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

    wyjdzie

    Wreszcie

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

    wyjdzie

    z wyjątkiem w
    końcu

Basj
źródło
2
To wcale nie odpowiada na pytanie. Ma tylko przykłady, kiedy to działa .
zixuan