'Finalmente' sempre é executado em Python?

126

Para qualquer bloco try-finally possível no Python, é garantido que o finallybloco será sempre executado?

Por exemplo, digamos que eu retorne enquanto estiver em um exceptbloco:

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

Ou talvez eu re-levante um Exception:

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

O teste mostra que finallyé executado para os exemplos acima, mas imagino que há outros cenários em que não pensei.

Existem cenários nos quais um finallybloco pode falhar na execução em Python?

Stevoisiak
fonte
18
O único caso que consigo imaginar que finallyfalha ao executar ou "anular seu objetivo" é durante um loop infinito sys.exitou uma interrupção forçada. A documentação afirma que finallysempre é executada, então eu iria com isso.
Xay
1
Um pouco de raciocínio lateral e com certeza não o que você pediu, mas tenho certeza de que, se você abrir o Gerenciador de Tarefas e encerrar o processo, finallynão será executado. Ou o mesmo se o computador travar antes: D
Alejandro
144
finallynão será executado se o cabo de alimentação for arrancado da parede.
user253751
3
Você pode estar interessado neste resposta para a mesma pergunta sobre C #: stackoverflow.com/a/10260233/88656
Eric Lippert
1
Bloqueie-o em um semáforo vazio. Nunca sinalize. Feito.
Martin James

Respostas:

206

"Garantido" é uma palavra muito mais forte do que qualquer implementação de finallymerece. O que é garantido é que, se a execução fluir para fora do todo try- finallyconstrua, ela passará pelo processo finally. O que não é garantido é que a execução sairá do try- finally.

  • A finallyem um gerador ou em uma rotina assíncrona pode nunca ser executada , se o objeto nunca for executado até a conclusão. Existem muitas maneiras de isso acontecer; aqui está um:

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

    Observe que este exemplo é um pouco complicado: quando o gerador é coletado de lixo, o Python tenta executar o finallybloco lançando uma GeneratorExitexceção, mas aqui capturamos essa exceção e, yieldnovamente, quando o Python imprime um aviso ("gerador ignorado GeneratorExit ") e desiste. Consulte PEP 342 (Coroutines através de geradores aprimorados) para obter detalhes.

    Outras maneiras um gerador ou co-rotina pode não executar para conclusão incluem se o objeto nunca é GC'ed (sim, isso é possível, mesmo em CPython), ou se um async with awaits em __aexit__, ou se o objeto awaits ou yields em um finallybloco. Esta lista não pretende ser exaustiva.

  • A finallyem um encadeamento daemon pode nunca ser executado se todos os encadeamentos que não sejam daemon sairem primeiro.

  • os._exitinterromperá o processo imediatamente sem executar finallyblocos.

  • os.forkpode causar finallya execução de blocos duas vezes . Além dos problemas normais que você esperaria que acontecesse duas vezes, isso poderia causar conflitos de acesso simultâneos (travamentos, paralisações, ...) se o acesso a recursos compartilhados não estiver sincronizado corretamente .

    Como multiprocessingusa fork-sem-exec para criar processos de trabalho ao usar o método de início de forquilha (o padrão no Unix), e depois chama os._exito trabalhador assim que o trabalho do trabalhador é concluído, finallye a multiprocessinginteração pode ser problemática ( exemplo ).

  • Uma falha de segmentação no nível C impedirá a finallyexecução de blocos.
  • kill -SIGKILLimpedirá a finallyexecução de blocos. SIGTERMe SIGHUPtambém impedirá a finallyexecução de blocos, a menos que você instale um manipulador para controlar o desligamento; Por padrão, o Python não manipula SIGTERMou SIGHUP.
  • Uma exceção em finallypode impedir que a limpeza seja concluída. Um caso particularmente digno de nota é se o usuário pressionar o controle-C, assim que começarmos a executar o finallybloco. Python irá gerar KeyboardInterrupte pular todas as linhas finallydo conteúdo do bloco. (o KeyboardInterruptcódigo -safe é muito difícil de escrever).
  • Se o computador perder energia, ou hibernar e não acordar, os finallyblocos não serão executados.

O finallybloco não é um sistema de transações; não fornece garantias de atomicidade nem nada do tipo. Alguns desses exemplos podem parecer óbvios, mas é fácil esquecer que essas coisas podem acontecer e confiar finallydemais.

user2357112 suporta Monica
fonte
14
Acredito que apenas o primeiro ponto da sua lista é realmente relevante e existe uma maneira fácil de evitá-lo: 1) nunca use nada excepte nunca pegue GeneratorExitdentro de um gerador. Os pontos sobre threads / eliminação do processo / segfaulting / power off são esperados, o python não pode fazer mágica. Além disso: as exceções em finallysão obviamente um problema, mas isso não altera o fato de que o fluxo de controle foi movido para o finallybloco. Em relação a Ctrl+C, você pode adicionar um manipulador de sinal que o ignore ou simplesmente "agendar" um desligamento limpo após a conclusão da operação atual.
Giacomo Alzetta 14/03
8
A menção de kill -9 é tecnicamente correta, mas um pouco injusta. Nenhum programa escrito em qualquer idioma executa qualquer código ao receber um kill -9. De fato, nenhum programa recebe um kill -9, portanto, mesmo que quisesse, não poderia executar nada. Esse é o ponto principal de matar -9.
Tom
10
@ Tom: O ponto sobre kill -9não especificou um idioma. E, francamente, ele precisa ser repetido, porque fica em um ponto cego. Muitas pessoas esquecem, ou não percebem, que seu programa poderia ser interrompido sem sucesso, mesmo sem permissão para limpar.
Chao
5
@GiacomoAlzetta: Existem pessoas por aí confiando em finallyblocos como se tivessem fornecido garantias transacionais. Pode parecer óbvio que não, mas não é algo que todos percebem. Quanto ao gabinete do gerador, existem várias maneiras de um gerador não ter o GC ativado, e muitas maneiras que um gerador ou uma rotina pode acidentalmente ceder após, GeneratorExitmesmo que não capte o GeneratorExit, por exemplo, se um async withsuspender uma corotina em __exit__.
User2357112 suporta Monica
2
@ user2357112 yeah - Eu venho tentando há décadas conseguir que os desenvolvedores limpem os arquivos temporários etc. na inicialização do aplicativo, e não na saída. Baseando-se no chamado '-se limpo e rescisão graciosa', está pedindo desapontamento e lágrimas :)
Martin James
69

Sim. Finalmente sempre vence.

A única maneira de derrotá-lo é interromper a execução antes de finally:ter a chance de executar (por exemplo, travar o intérprete, desligar o computador, suspender um gerador para sempre).

Eu imagino que existem outros cenários em que não pensei.

Aqui estão mais algumas que você pode não ter pensado:

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'

Dependendo de como você saiu do intérprete, às vezes você pode "cancelar" finalmente, mas não assim:

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

Usando o precário os._exit(isso se enquadra em "travar o intérprete" na minha opinião):

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

Atualmente, estou executando este código, para testar se finalmente ainda será executado após a morte por calor do universo:

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

No entanto, ainda estou esperando o resultado, então volte aqui mais tarde.

wim
fonte
5
ou possuindo um loop i finita na captura tentativa
sapy
8
finallyem um gerador ou corotina pode facilmente falhar na execução , sem chegar nem perto de uma condição "travar o intérprete".
user2357112 suporta Monica
27
Depois que a morte por calor do universo deixa de existir, isso sleep(1)definitivamente resultaria em um comportamento indefinido. :-D
David Foerster
Você pode mencionar _os.exit diretamente após "a única maneira de derrotá-lo é travar o compilador". No momento, é um exemplo misto entre os dois, onde finalmente vence.
Stevoisiak 13/03/19
2
@StevenVascellaro Eu não acho que seja necessário - os._exité, para todos os efeitos práticos, o mesmo que induzir um acidente (saída impura). A maneira correta de sair é sys.exit.
wim
9

De acordo com a documentação do Python :

Não importa o que aconteceu anteriormente, o bloco final é executado assim que o bloco de código é concluído e as exceções levantadas são tratadas. Mesmo se houver um erro em um manipulador de exceções ou no bloco else e uma nova exceção for gerada, o código no bloco final ainda será executado.

Também deve ser observado que, se houver várias instruções de retorno, incluindo uma no bloco final, o retorno do bloco final será o único que será executado.

Jayce
fonte
8

Bem, sim e não.

O que é garantido é que o Python sempre tentará executar o bloco final. No caso em que você retorna do bloco ou gera uma exceção não capturada, o bloco final é executado imediatamente antes de realmente retornar ou gerar a exceção.

(o que você poderia ter se controlado simplesmente executando o código em sua pergunta)

O único caso em que posso imaginar onde o bloco final não será executado é quando o próprio interpretador Python trava, por exemplo, no código C ou por falta de energia.

Serge Ballesta
fonte
ha ha .. ou existe um loop infinito no try catch
sapy 13/03/18
Eu acho que "Bem, sim e não" é o mais correto. Finalmente: always wins onde "always" significa que o intérprete é capaz de executar e o código para o "finally:" ainda está disponível, e "wins" é definido como o intérprete tentará executar o finalmente: bloco e será bem-sucedido. Esse é o "Sim" e é muito condicional. "Não" é todas as maneiras pelas quais o intérprete pode parar antes de "finalmente:" - falha de energia, falha de hardware, kill -9 voltada para o intérprete, erros no intérprete ou código de que depende, outras maneiras de travar o intérprete. E maneiras de pendurar dentro do "finalmente:".
Bill IV
1

Eu encontrei este sem usar uma função de gerador:

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)

O sono pode ser qualquer código que possa ser executado por quantidades inconsistentes de tempo.

O que parece estar acontecendo aqui é que o primeiro processo paralelo a ser concluído deixa o bloco try com êxito, mas tenta retornar da função um valor (foo) que não foi definido em nenhum lugar, o que causa uma exceção. Essa exceção mata o mapa sem permitir que os outros processos alcancem seus blocos finalmente.

Além disso, se você adicionar a linha bar = bazzlogo após a chamada sleep () no bloco try. Em seguida, o primeiro processo a alcançar essa linha lança uma exceção (porque bazz não está definido), o que faz com que seu próprio bloco finalmente seja executado, mas mata o mapa, fazendo com que os outros blocos de tentativa desapareçam sem alcançar seus blocos finalmente, e o primeiro processo a não alcançar sua declaração de retorno também.

O que isso significa para o multiprocessamento Python é que você não pode confiar no mecanismo de tratamento de exceções para limpar recursos em todos os processos, se mesmo um dos processos puder ter uma exceção. Seria necessário manipular sinais adicionais ou gerenciar recursos fora da chamada do mapa de multiprocessamento.

Blair Houghton
fonte
-2

Adendo à resposta aceita, apenas para ajudar a ver como funciona, com alguns exemplos:

  • Este:

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

    irá produzir

    finalmente

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

    irá produzir

    exceto
    finalmente

Basj
fonte
Isso não responde à pergunta. Apenas tem exemplos de quando funciona .
zixuan