Чи "нарешті" завжди виконується в Python?


126

Чи гарантується будь-який можливий блок спробу в остаточному підсумку в Python, що finallyблок завжди буде виконуватися?

Наприклад, скажімо, я повертаюсь у exceptблоці:

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

А може, я знову піднімаю Exception:

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

Тестування показує, що finallyвиконується для вищезазначених прикладів, але я думаю, що є й інші сценарії, про які я не думав.

Чи є сценарії, в яких finallyблок не може виконатись у Python?


18
Єдиний випадок, який я можу собі уявити, finallyне виконати або "перемогти свою мету" - це під час нескінченного циклу sys.exitабо вимушеного переривання. У документації зазначено, що finallyзавжди виконується, тому я б пішов з цим.
Xay

1
Трохи бічного роздуму і впевненості, не те, про що ви просили, але я впевнений, що якщо ви відкриєте диспетчер завдань і вб'єте процес, finallyвін не запуститься. Або те саме, якщо комп'ютер вийшов з ладу раніше: D
Алехандро

144
finallyне виконається, якщо шнур живлення відірваний від стіни.
користувач253751

3
Можливо, вас зацікавить ця відповідь на той самий питання щодо C #: stackoverflow.com/a/10260233/88656
Ерік Ліпперт

1
Блокуйте його на порожньому семафорі. Ніколи не сигналізуйте про це. Зроблено.
Мартін Джеймс

Відповіді:


206

"Гарантоване" - це набагато сильніше слово, ніж будь-яка реалізація finallyзаслуговує. Гарантоване, що якщо виконання витікає з цілого try- finallyконструювати, воно пройде через те, finallyщоб це зробити. Що не гарантується, що виконання буде витікати з try- finally.

  • A finallyв генераторі або асинхронній програмі ніколи не може працювати , якщо об'єкт ніколи не виконує висновок. Є багато способів, які могли б статися; ось один:

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

    Зауважте, що цей приклад трохи хитромудрий: коли генератор збирає сміття, Python намагається запустити finallyблок, кидаючи GeneratorExitвиняток, але тут ми вловлюємо це виняток і потім yieldзнову, після чого Python друкує попередження ("генератор проігнорував GeneratorExit ") і здається. Докладні відомості див. У розділі PEP 342 (Коротифікації через покращені генератори) .

    Інші способи, які генератор або супровід може не виконати для висновку, включають, якщо об'єкт просто ніколи не є GC'ed (так, це можливо навіть у CPython), або якщо async with awaits в __aexit__, або якщо об'єкт awaits або yields в finallyблоці. Цей список не є вичерпним.

  • A finallyв демоновій нитці ніколи не може виконуватися, якщо всі недемонові потоки вийдуть першими.

  • os._exitзупинить процес негайно без виконання finallyблоків.

  • os.forkможе викликати finallyблоки виконати двічі . Окрім звичайних проблем, яких ви очікуєте від подій, що трапляються двічі, це може спричинити одночасні конфлікти доступу (збої, стійла, ...), якщо доступ до спільних ресурсів не синхронізований правильно .

    Оскільки multiprocessingвикористовується fork-without-exec для створення робочих процесів при використанні методу запуску fork (за замовчуванням у Unix), а потім викликає os._exitпрацівника, коли робота робітника виконана, finallyі multiprocessingвзаємодія може бути проблематичною ( приклад ).

  • Помилка сегментації на рівні C запобіжить finallyзапуску блоків.
  • kill -SIGKILLзапобіжить finallyзапуску блоків. SIGTERMа SIGHUPтакож запобіжить finallyзапуску блоків, якщо ви не встановите обробник для управління відключенням самостійно; за замовчуванням Python не обробляє SIGTERMабо SIGHUP.
  • Виняток у finallyможе запобігти завершенню очищення. Особливо примітний випадок, якщо користувач натискає на Control-C так само, як ми починаємо виконувати finallyблок. Python підніме a KeyboardInterruptі пропустить кожен рядок вмісту finallyблоку. ( KeyboardInterrupt-забезпечити код дуже важко).
  • Якщо комп'ютер втрачає живлення або перебуває в сплячку і не прокидається, finallyблоки не запускаються.

finallyБлок не є система транзакцій; він не дає гарантій атомності або нічого подібного. Деякі з цих прикладів можуть здатися очевидними, але легко забути такі речі можуть трапитися і покладатися на них finallyзанадто багато.


14
Я вважаю, що лише перша точка вашого списку справді актуальна, і є простий спосіб цього уникнути: 1) ніколи не використовуйте голий exceptі ніколи не ловіть GeneratorExitвсередині генератора. Очікуються моменти щодо потоків / вбивства процесу / відключення / відключення живлення, пітон не може зробити магію. Крім того : виключення в finallyявно проблема , але це не змінює той факт , що потік управління був переміщений в finallyблок. Щодо того Ctrl+C, ви можете додати обробник сигналу, який його ігнорує, або просто "планує" чисте відключення після завершення поточної операції.
Джакомо Альзетта

8
Згадка про kill -9 технічно коректна, але трохи несправедлива. Жодна програма, написана будь-якою мовою, не виконує жодного коду після отримання команди kill -9. Насправді жодна програма ніколи не отримує вбивства -9, тому навіть якщо б цього хотіла, вона нічого не могла виконати. У цьому вся суть вбивства -9.
Том

10
@Tom: пункт про kill -9те, що мова не вказана. І, чесно кажучи, це потребує повторення, бо сидить на сліпому місці. Занадто багато людей забувають або не усвідомлюють, що їхню програму можна було зупинити мертвою в її слідах, навіть не дозволяючи прибирати.
cHao

5
@GiacomoAlzetta: Є люди, які покладаються на finallyблоки, ніби вони забезпечують транзакційні гарантії. Може здатися очевидним, що вони цього не роблять, але це не те, що всі розуміють. Що стосується випадку генератора, то існує багато способів, коли генератор може взагалі не бути GC'ed, і багато способів, коли генератор або посібник можуть випадково вийти, GeneratorExitнавіть якщо він не вловить GeneratorExit, наприклад, якщо async withпризупинення співпрограми в __exit__.
user2357112 підтримує Моніку

2
@ user2357112 так - я десятиліттями намагався отримати розробники для очищення тимчасових файлів тощо при запуску програми, а не виходу. Спираючись на так зване "очищення та витончене припинення", просить розчарування та сліз :)
Мартін Джеймс

68

Так. Нарешті завжди виграє.

Єдиний спосіб перемогти це - зупинити виконання, перш ніж finally:отримати шанс виконання (наприклад, збій перекладача, вимкнення комп'ютера, призупинення генератора назавжди).

Я думаю, є й інші сценарії, про які я не думав.

Ось ще пара, про яку ви, можливо, не думали:

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'

Залежно від способу виходу з перекладача, іноді ви можете "скасувати" нарешті, але не так:

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

Використовуючи нестабільний os._exit(на мою думку, це під «крах перекладача»):

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

Я зараз запускаю цей код, щоб перевірити, чи остаточно він виконається після теплової смерті Всесвіту:

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

Однак я все ще чекаю результату, тому перевірте тут пізніше.


5
або у вас є кінцевий цикл у спробі лову
сапі


27
Після теплової загибелі Всесвіту час припиняє своє існування, sleep(1)що, безумовно, призведе до невизначеної поведінки. :-D
Девід Фоерстер

Ви можете згадати _os.exit безпосередньо після "єдиний спосіб перемогти його - це збій компілятора". Зараз це змішано між прикладами, коли нарешті виграє.
Стевойсяк

2
@StevenVascellaro Я не думаю, що це потрібно - os._exitце в усіх практичних цілях те саме, що викликати аварію (нечистий вихід). Правильний спосіб виходу - це sys.exit.
Вім

9

Відповідно до документації Python :

Незалежно від того, що відбувалося раніше, остаточний блок виконується після завершення блоку коду та оброблення будь-яких підвищених винятків. Навіть якщо є помилка в обробці винятків або в блоці else і порушено новий виняток, код у фінальному блоці все ще виконується.

Слід також зазначити, що якщо є кілька операторів повернення, включаючи одне в остаточному блоці, то повернення остаточно блоку є єдиним, яке буде виконуватися.


8

Ну так і ні.

Гарантоване, що Python завжди намагатиметься виконати остаточний блок. У випадку, коли ви повертаєтеся з блоку або піднімаєте невиконаний виняток, нарешті блок виконується безпосередньо перед поверненням або підвищенням винятку.

(що ви могли контролювати, просто запустивши код у своєму запитанні)

Єдиний випадок, який я можу собі уявити, коли остаточний блок не буде виконаний - це коли сам інтерпретатор Python виходить з ладу, наприклад, всередині коду С або через відключення електроенергії.


ха-ха .. або є нескінченна петля у спробі лову
сапі

Я думаю, що «ну так і ні» є найбільш правильним. Нарешті: завжди виграє там, де "завжди" означає, що інтерпретатор може працювати, а код для "нарешті:" все ще доступний, а "перемагає" визначається так, як інтерпретатор буде намагатися запустити остаточно: блок і це буде успішно. Це "так", і це дуже умовно. "Ні" - це всі способи, через які перекладач може зупинитися перед "нарешті:" - відмова живлення, збій апаратури, вбивство -9, спрямоване на перекладача, помилки в перекладачі або код, від якого залежить, інші способи повісити перекладача. І способи повісити всередину "нарешті:".
Білл IV

1

Я знайшов цю без використання функції генератора:

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)

Сном може бути будь-яким кодом, який може працювати за непослідовну кількість часу.

Тут, мабуть, відбувається те, що перший паралельний процес, який завершується, успішно залишає блок спробу, але потім намагається повернути з функції значення (foo), яке ніде не було визначено, що спричиняє виняток. Цей виняток вбиває карту, не дозволяючи іншим процесам досягти їх остаточних блоків.

Крім того, якщо ви додасте рядок bar = bazzвідразу після сну (), дзвоніть у спробу блоку. Тоді перший процес для досягнення цього рядка викидає виняток (оскільки баз не визначений), який призводить до запуску власного блоку, але потім вбиває карту, внаслідок чого інші блоки спробу зникають, не дійшовши до їх остаточних блоків, і перший процес також не досяг свого повернення.

Що означає для багатопроцесорної обробки Python, це те, що ви не можете довіряти механізму обробки винятків для очищення ресурсів у всіх процесах, якщо навіть один із процесів може мати виняток. Буде необхідним додаткове оброблення сигналу або управління ресурсами поза багатовикористовувальним викликом карти.


-2

Додайте до прийнятої відповіді, щоб допомогти побачити, як вона працює, з кількома прикладами:

  • Це:

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

    виведе

    нарешті

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

    виведе

    крім
    нарешті


1
Це зовсім не відповідає на питання. Він просто має приклади, коли це працює .
zixuan
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.