"Наконец" всегда выполняется в Python?

Для любого возможного блока 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?

Ответы

Ответ 1

"Гарантированный" - это гораздо более сильное слово, чем любая реализация, которую, finally заслуживает. Гарантируется, что, если выполнение вытекает из всей try - finally построит, оно пройдет через finally чтобы сделать это. То, что не гарантируется, заключается в том, что выполнение будет вытекать из try - finally.

  • 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 (Coroutines через Enhanced Generators).

    Другие способы генератор или сопрограммный не может Выполнит на заключение включать, если объект не только никогда не GC'ed (да, возможно, даже в CPython), или если async with await в __aexit__, или если объект await или yield в finally блокировать. Этот список не является исчерпывающим.

  • 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 -safe код очень сложно записать).
  • Если компьютер теряет питание, или если он находится в спящем режиме и не просыпается, finally блоки не будут работать.

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

Ответ 2

Да. Наконец, всегда побеждает.

Единственный способ победить - остановить выполнение до 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')

Тем не менее, я все еще жду результатов, поэтому зайдите сюда позже.

Ответ 3

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

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

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

Ответ 4

Ну, да и нет.

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

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

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