¿'Finalmente' siempre se ejecuta en Python?

126

Para cualquier posible bloque try-finally en Python, ¿se garantiza que el finallybloque siempre se ejecutará?

Por ejemplo, digamos que regreso mientras estoy en un exceptbloque:

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

O tal vez vuelvo a subir un Exception:

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

Las pruebas muestran que finallyse ejecuta para los ejemplos anteriores, pero imagino que hay otros escenarios en los que no he pensado.

¿Hay algún escenario en el que un finallybloque no pueda ejecutarse en Python?

Stevoisiak
fuente
18
El único caso que puedo imaginar que finallyno se ejecuta o "vence su propósito" es durante un bucle infinito sys.exito una interrupción forzada. La documentación indica que finallysiempre se ejecuta, así que iría con eso.
Xay
1
Un poco de pensamiento lateral y seguro no es lo que pediste, pero estoy bastante seguro de que si abres el Administrador de tareas y eliminas el proceso, finallyno se ejecutará. O lo mismo si la computadora falla antes: D
Alejandro
144
finallyno se ejecutará si el cable de alimentación se arranca de la pared.
user253751
3
Quizás le interese esta respuesta a la misma pregunta sobre C #: stackoverflow.com/a/10260233/88656
Eric Lippert el
1
Bloquéelo en un semáforo vacío. Nunca lo señales. Hecho.
Martin James

Respuestas:

206

"Garantizado" es una palabra mucho más fuerte que cualquier implementación de finallymerece. Lo que está garantizado es que si la ejecución fluye fuera del conjunto try- finallyconstrucción, pasará por el finallypara hacerlo. Lo que no está garantizado es que la ejecución fluirá fuera de try- finally.

  • Es finallyposible que nunca se ejecute A en un generador o en una rutina asíncrona , si el objeto nunca se ejecuta hasta la conclusión. Hay muchas maneras en que podría suceder; Aquí hay uno:

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

    Tenga en cuenta que este ejemplo es un poco complicado: cuando el generador se recolecta basura, Python intenta ejecutar el finallybloque lanzando una GeneratorExitexcepción, pero aquí capturamos esa excepción y luego yieldnuevamente, en ese momento Python imprime una advertencia ("generador ignorado Generator Salir ") y se da por vencido. Ver PEP 342 (Corutinas a través de generadores mejorados) para más detalles.

    Otras formas en que un generador o una rutina no pueden ejecutarse hasta la conclusión incluyen si el objeto nunca se GC'ed (sí, eso es posible, incluso en CPython), o si una async with awaits __aexit__, o si el objeto awaits o yields en un finallybloque. Esta lista no pretende ser exhaustiva.

  • A finallyen un hilo de daemon podría nunca ejecutarse si todos los hilos no daemon salen primero.

  • os._exitdetendrá el proceso inmediatamente sin ejecutar finallybloques.

  • os.forkpuede hacer que los finallybloques se ejecuten dos veces . Además de los problemas normales que esperaría de las cosas que suceden dos veces, esto podría causar conflictos de acceso concurrentes (bloqueos, paradas, ...) si el acceso a los recursos compartidos no se sincroniza correctamente .

    Dado que multiprocessingutiliza fork-without-exec para crear procesos de trabajo cuando se usa el método de inicio de fork (el valor predeterminado en Unix), y luego llama os._exital trabajador una vez que se realiza el trabajo del trabajador, finallyy la multiprocessinginteracción puede ser problemática ( ejemplo ).

  • Una falla de segmentación de nivel C evitará que los finallybloques se ejecuten.
  • kill -SIGKILLevitará que los finallybloques se ejecuten. SIGTERMy SIGHUPtambién evitará que los finallybloques se ejecuten a menos que instale un controlador para controlar el apagado usted mismo; por defecto, Python no maneja SIGTERMo SIGHUP.
  • Una excepción en finallypuede evitar que se complete la limpieza. Un caso particularmente notable es si el usuario toca control-C justo cuando estamos comenzando a ejecutar el finallybloque. Python elevará ay KeyboardInterruptomitirá cada línea del finallycontenido del bloque. ( KeyboardInterruptEl código seguro es muy difícil de escribir).
  • Si la computadora pierde energía, o si hiberna y no se despierta, los finallybloques no se ejecutarán.

El finallybloque no es un sistema de transacción; no proporciona garantías de atomicidad ni nada por el estilo. Algunos de estos ejemplos pueden parecer obvios, pero es fácil olvidar que tales cosas pueden suceder y depender finallydemasiado.

user2357112 es compatible con Monica
fuente
14
Creo que solo el primer punto de su lista es realmente relevante, y hay una manera fácil de evitarlo: 1) nunca use un pelado excepty nunca se meta GeneratorExitdentro de un generador. Se esperan los puntos sobre hilos / matar el proceso / segfaulting / apagado, python no puede hacer magia. Además: las excepciones finallyson obviamente un problema, pero esto no cambia el hecho de que el flujo de control se movió al finallybloque. Con respecto a esto Ctrl+C, puede agregar un controlador de señal que lo ignore, o simplemente "programar" un apagado limpio después de que se complete la operación actual.
Giacomo Alzetta
8
La mención de kill -9 es técnicamente correcta, pero un poco injusta. Ningún programa escrito en ningún idioma ejecuta ningún código al recibir un kill -9. De hecho, ningún programa recibe un kill -9 en absoluto, por lo que incluso si quisiera, no podría ejecutar nada. Ese es el punto de matar -9.
Tom
10
@Tom: El punto sobre kill -9no especificó un idioma. Y, francamente, necesita repetirse, porque se encuentra en un punto ciego. Demasiadas personas olvidan, o no se dan cuenta, de que su programa podría detenerse en seco sin siquiera permitirse la limpieza.
cHao
55
@GiacomoAlzetta: Hay personas que confían en los finallybloques como si proporcionaran garantías transaccionales. Puede parecer obvio que no lo hacen, pero no es algo que todos se den cuenta. En cuanto al caso del generador, hay muchas formas en que un generador no puede ser GC'ed en absoluto, y muchas formas en que un generador o una rutina pueden ceder accidentalmente GeneratorExitincluso si no atrapa el GeneratorExit, por ejemplo, si se async withsuspende una co-rutina en __exit__.
user2357112 es compatible con Monica
2
@ user2357112 sí, he estado intentando durante décadas conseguir que los desarrolladores limpien archivos temporales, etc. al iniciar la aplicación, no salir. Confiar en la llamada 'limpieza y terminación elegante', es pedir desilusión y lágrimas :)
Martin James
69

Si. Finalmente siempre gana.

La única forma de derrotarlo es detener la ejecución antes de tener la finally:oportunidad de ejecutarla (por ejemplo, bloquear el intérprete, apagar la computadora, suspender un generador para siempre).

Me imagino que hay otros escenarios en los que no he pensado.

Aquí hay un par más que quizás no haya 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'

Dependiendo de cómo salga del intérprete, a veces puede "cancelar" finalmente, pero no así:

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

Usando lo precario os._exit(en mi opinión, esto se clasifica como "accidente del intérprete"):

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

Actualmente estoy ejecutando este código, para probar si finalmente aún se ejecutará después de la muerte por calor del universo:

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

Sin embargo, todavía estoy esperando el resultado, así que vuelve aquí más tarde.

wim
fuente
55
o tener un bucle finito en try catch
sapy
8
finallyen un generador o en una rutina puede fallar fácilmente en la ejecución , sin acercarse a una condición de "bloqueo del intérprete".
user2357112 es compatible con Monica
27
Después de la muerte por calor del universo, el tiempo deja de existir, por sleep(1)lo que definitivamente daría lugar a un comportamiento indefinido. :-D
David Foerster
Es posible que desee mencionar _os.exit directamente después de "la única forma de vencerlo es bloquear el compilador". En este momento está mezclado entre ejemplos donde finalmente gana.
Stevoisiak
2
@StevenVascellaro No creo que sea necesario, os._exites, a todos los efectos prácticos, lo mismo que inducir un bloqueo (salida impura). La forma correcta de salir es sys.exit.
wim
9

De acuerdo con la documentación de Python :

No importa lo que sucedió anteriormente, el bloque final se ejecuta una vez que se completa el bloque de código y se manejan las excepciones planteadas. Incluso si hay un error en un controlador de excepciones o en el bloque else y se genera una nueva excepción, el código en el bloque final todavía se ejecuta.

También debe tenerse en cuenta que si hay varias declaraciones de retorno, incluida una en el bloque de finalmente, entonces el retorno de último bloque es el único que se ejecutará.

jayce
fuente
8

Pues sí y no.

Lo que está garantizado es que Python siempre intentará ejecutar el último bloque. En el caso de que regrese del bloque o genere una excepción no capturada, el bloque finalmente se ejecuta justo antes de regresar o aumentar la excepción.

(lo que podría haber controlado usted mismo simplemente ejecutando el código en su pregunta)

El único caso que puedo imaginar donde el bloque finalmente no se ejecutará es cuando el propio intérprete de Python se bloquea, por ejemplo, dentro del código C o debido a un corte de energía.

Serge Ballesta
fuente
ja ja .. o hay un bucle infinito en try catch
sapy
Creo que "Bueno, sí y no" es lo más correcto. Finalmente: siempre gana donde "siempre" significa que el intérprete puede ejecutarse y el código para "finalmente:" todavía está disponible, y "gana" se define ya que el intérprete intentará ejecutar el bloque finalmente: y tendrá éxito. Ese es el "Sí" y es muy condicional. "No" son todas las formas en que el intérprete puede detenerse antes de "finalmente": falla de energía, falla de hardware, kill -9 dirigido al intérprete, errores en el intérprete o código del que depende, otras formas de colgar el intérprete. Y formas de colgar dentro del "finalmente:".
Proyecto de ley IV
1

Encontré este sin usar una función de generador:

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)

La suspensión puede ser cualquier código que pueda ejecutarse durante períodos de tiempo inconsistentes.

Lo que parece estar sucediendo aquí es que el primer proceso paralelo que termina deja el bloque try con éxito, pero luego intenta devolver de la función un valor (foo) que no se ha definido en ninguna parte, lo que causa una excepción. Esa excepción mata el mapa sin permitir que los otros procesos alcancen finalmente sus bloques.

Además, si agrega la línea bar = bazzjusto después de la llamada sleep () en el bloque try. Luego, el primer proceso para llegar a esa línea arroja una excepción (porque el bazz no está definido), lo que hace que se ejecute su propio bloque finalmente, pero luego mata el mapa, haciendo que los otros bloques de prueba desaparezcan sin llegar a sus bloques finalmente, y el primer proceso tampoco llega a su declaración de devolución.

Lo que esto significa para el multiprocesamiento de Python es que no puede confiar en el mecanismo de manejo de excepciones para limpiar recursos en todos los procesos si incluso uno de los procesos puede tener una excepción. Sería necesario un manejo adicional de la señal o la administración de los recursos fuera de la llamada del mapa de multiprocesamiento.

Blair Houghton
fuente
-2

Anexo a la respuesta aceptada, solo para ayudar a ver cómo funciona, con algunos ejemplos:

  • Esta:

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

    saldrá

    finalmente

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

    saldrá

    excepto
    finalmente

Basj
fuente
Esto no responde a la pregunta en absoluto. Solo tiene ejemplos de cuándo funciona .
zixuan