Всегда ли "finally" выполняется в Python?


126

Гарантируется ли, что для любого возможного блока try-finally в 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
Алехандро

145
finallyне будет работать, если шнур питания оторван от стены.
user253751

3
Возможно, вас заинтересует этот ответ на тот же вопрос о C #: stackoverflow.com/a/10260233/88656
Эрик Липперт,

1
Заблокируйте его на пустой семафор. Никогда не сигнализируйте об этом. Готово.
Мартин Джеймс

Ответы:


207

«Гарантированно» - это более сильное слово, чем любая реализация 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использование раскошелиться-без-Exec для создания рабочих процессов при использовании вилки способа запуска (по умолчанию на Unix), а затем вызывает os._exitв рабочий раз работа работника делается, finallyи multiprocessingвзаимодействие может быть проблематичным ( пример ).

  • Ошибка сегментации C-уровня предотвратит finallyзапуск блоков.
  • kill -SIGKILLпредотвратит finallyзапуск блоков. SIGTERMа SIGHUPтакже предотвратит finallyзапуск блоков, если вы не установите обработчик для управления выключением самостоятельно; по умолчанию Python не обрабатывает SIGTERMили SIGHUP.
  • Исключение finallyможет помешать завершению очистки. Один особо отметить случай , если пользователь нажимает Control-C только , как мы начинаем выполнять finallyблок. Python поднимет KeyboardInterruptи пропустит каждую строку finallyсодержимого блока. ( KeyboardInterrupt-безопасный код писать очень сложно).
  • Если компьютер теряет питание или переходит в спящий режим и не просыпается, finallyблоки не запускаются.

finallyБлок не является система транзакций; он не предоставляет гарантий атомарности или чего-либо в этом роде. Некоторые из этих примеров могут показаться очевидными, но легко забыть, что такие вещи могут случиться, и finallyслишком сильно полагаться на них .


14
Я считаю, что только первый пункт вашего списка действительно актуален, и есть простой способ избежать этого: 1) никогда не используйте голый exceptи никогда не ловите GeneratorExitвнутри генератора. Ожидаются точки про потоки / убийство процесса / segfaulting / отключение питания, python не может творить чудеса. Также: исключения в finallyявно являются проблемой, но это не меняет того факта, что поток управления был перемещен в finallyблок. Что касается Ctrl+C, вы можете добавить обработчик сигнала, который его игнорирует, или просто «планирует» полное завершение работы после завершения текущей операции.
Джакомо Альцетта,

8
Упоминание kill -9 технически правильно, но немного несправедливо. Ни одна программа, написанная на каком-либо языке, не запускает код после получения kill -9. Фактически, ни одна программа никогда не получает kill -9, поэтому, даже если бы она захотела, она не смогла бы ничего выполнить. В этом весь смысл kill -9.
Tom

10
@Tom: Дело в том, kill -9что не указан язык. И, честно говоря, его нужно повторить, потому что он находится в слепой зоне. Слишком много людей забывают или не осознают, что их программа может быть остановлена ​​как вкопанная, даже не получив разрешения на очистку.
cHao

5
@GiacomoAlzetta: Есть люди, которые полагаются на finallyблоки, как будто они предоставляют транзакционные гарантии. Может показаться очевидным, что это не так, но это не все понимают. Что касается случая генератора, есть много способов, которыми генератор может вообще не обрабатываться с помощью GC, и множество способов, которыми генератор или сопрограмма могут случайно сработать после, 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
или имеющий конечный цикл i в try catch
sapy


27
После тепловой смерти Вселенной время перестает существовать, поэтому sleep(1)определенно приведет к неопределенному поведению. :-D
Дэвид Ферстер

Вы можете упомянуть _os.exit сразу после того, как «единственный способ победить это - вывести компилятор из строя». Прямо сейчас это смешано между примерами, где наконец-то побеждает.
Stevoisiak

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

9

Согласно документации Python :

Независимо от того, что произошло ранее, final-block выполняется после завершения блока кода и обработки любых возникших исключений. Даже если в обработчике исключений или в блоке else возникает ошибка и возникает новое исключение, код в последнем блоке все равно выполняется.

Следует также отметить, что при наличии нескольких операторов возврата, включая один в блоке finally, будет выполняться только возврат блока finally.


8

Ну да и нет.

Гарантируется, что Python всегда будет пытаться выполнить блок finally. В случае, когда вы возвращаетесь из блока или вызываете неперехваченное исключение, блок finally выполняется непосредственно перед фактическим возвратом или вызовом исключения.

(что вы могли бы контролировать сами, просто запустив код в своем вопросе)

Единственный случай, который я могу представить, когда блок finally не будет выполнен, - это когда сам интерпретатор Python выйдет из строя, например, внутри кода C или из-за отключения электроэнергии.


ха-ха .. или есть бесконечный цикл в try catch
sapy

Я считаю, что «и да, и нет» наиболее правильное. Наконец: always wins, где «always» означает, что интерпретатор может работать, а код для «finally:» все еще доступен, а «wins» определяется, поскольку интерпретатор попытается запустить блок finally: и будет успешным. Это «Да», и это очень условно. «Нет» - это все способы, которыми интерпретатор может остановиться перед «finally:» - сбой питания, аппаратный сбой, kill -9, направленный на интерпретатор, ошибки в интерпретаторе или коде, от которого он зависит, другие способы зависания интерпретатора. И способы вешать внутрь "наконец:".
Bill 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)

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

Похоже, что здесь происходит то, что первый завершившийся параллельный процесс успешно покидает блок try, но затем пытается вернуть из функции значение (foo), которое нигде не было определено, что вызывает исключение. Это исключение уничтожает карту, не позволяя другим процессам достичь своих блоков finally.

Кроме того, если вы добавите строку bar = bazzсразу после вызова sleep () в блоке try. Затем первый процесс, достигший этой строки, выдает исключение (поскольку bazz не определен), которое вызывает запуск собственного блока finally, но затем убивает карту, в результате чего другие блоки try исчезают, не достигнув своих блоков finally, и первый процесс также не достигает своего оператора возврата.

Для многопроцессорной обработки Python это означает, что вы не можете доверять механизму обработки исключений для очистки ресурсов во всех процессах, если хотя бы один из процессов может иметь исключение. Потребуется дополнительная обработка сигналов или управление ресурсами вне вызова карты многопроцессорности.


-2

Дополнение к принятому ответу, чтобы увидеть, как он работает, с несколькими примерами:

  • Это:

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

    выведет

    Ну наконец то

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

    выведет

    кроме,
    наконец


2
Это вообще не отвечает на вопрос. Просто есть примеры того, когда это работает .
zixuan
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.