25 de Agosto, 2011

try...except...else...finally

Es una de esas construcciones que a veces me hace dudar, por eso de que yo vengo de C, y aún se hace raro.

Por ejemplo, hoy me he encontrado con un trozo de código que, aún funcionando, me ha dejado intrigado un buen rato:

# try...except...else...finally y continue
for i in [1, 2, 3]:
    try:
        print "try", i
    except:
        # no va a pasar
        pass
    else:
        print "else", i
        continue
        print "no se ejecuta"
    finally:
        print "finally", i
        i += 1  # este incremento no afecta al bucle (es una lista)
    print "nunca se ejecuta"

print "fin con", i

Evidentemente no era ese el código, pero el comportamiento es aproximadamente el mismo. Sabemos que el bloque de código de finally se ejecutará siempre se dé o no la excepción (en este caso no se dará nunca), aunque se ejecute el continue:

try 1
else 1
finally 1
try 2
else 2
finally 2
try 3
else 3
finally 3
fin con 4

Así que se ejecuta el bloque del else y antes de que empiece una nueva interacción del bucle, se ejecuta el bloque de finally (notar qué partes no se ejecutan). Es el comportamiento esperado.

Ahora, ¿qué pasa con el siguiente código?

# try...except...else...finally y return
def test(i):
    try:
        print "try", i
    except:
        # no va a pasar
        pass
    else:
        print "else", i
        return i
        print "no se ejecuta"
    finally:
        print "finally", i
        i += 1
    print "nunca se ejecuta"

a = test(3)
print "fin con", a

Es fácil de seguir, ¿no?

Pues no tanto ;), cualquiera esperaría encontrarse que la ejecución acaba con fin con 4, pero no es así:

try 3
else 3
finally 3
fin con 3

Vemos ese finally 3, así que el bloque de finally se ejecuta... ¿o no? porque el incremento de la variable no parece llevarse a cabo :D. ¿Alguien se anima a explicar este comportamiento?

Nota: esto es Python 2.7, aunque no creo que cambie nada (no he probado con Python 3).

Actualización: el código real en el que he escrito un try...except...else...finally incluía un eventlet.timeout.Timeout, usando el bloque finally para cancelar el timeout, con la complicación de que el código a ejecutar controlado por Timeout podía generar sus propias excepciones (como un socket.timeout). Simplemente me ha parecido demasiado complicado para comentarlo por aquí ;).

Anotación por Juan J. Martínez, clasificada en: python.

Hay 3 comentarios

Gravatar

Tiene sentido, la instrucción “return i” establece el valor de retorno de la función (3) y posteriormente en el finally modificas el valor de la variable local i a 4, pero el valor de retorno ya ha sido establecido.
Otra cosa sería que devuelvas un puntero a un espacio de memoria y modifiques el contenido de ese espacio de memoria en el finally, en ese caso si que la devolución sería 4 a traves de un puntero.

En el primer ejemplo de código es el for el que deja la variable con un valor de 4 y es por eso por lo que se sale del bucle y finalmente imprime 4.

En el segundo caso yo no esperaría encontrarme un 4 como retorno. En este ejemplo de código en realidad se está haciendo un mal uso del bloque finally.

Saludos

por Pablo, en 2011-08-25 21:37:10

Gravatar

Es un ejemplo para ilustrar el comportamiento, no tiene especial sentido :P

finally se utiliza como mecanismo de limpieza. Según la documentación: finally

When a return, break or continue statement is executed in the try suite of a try…finally statement, the finally clause is also executed ‘on the way out.’

Creo entender que cuando se salta al bloque de finally el intérprete guarda el contexto, como si llamaras a una función, y eso incluye el valor de retorno i.

Creo, porque no estoy seguro. Igual no debería preguntar cosas sin saber la respuesta ;)

Gracias por el comentario!

(me parece que más o menos decimos lo mismo)

por Juanjo, en 2011-08-25 22:33:13

Gravatar

Exacto, finally se usa para limpiar recursos, el más típico suele ser el de cerrar archivos o conexiones después de hacer una lectura o escritura. Que en caso de fallo igualmente se deben cerrar. Aunque este bloque finally solo puede acceder al contexto local.

Por limpieza y por llevar un control más estricto de lo que se devuelve podrías dejar solo un return debajo del finally y en el bloque try … else … establecer el valor de la variable local. Aunque no se cual de los dos estilos quedará más claro.

por Pablo, en 2011-08-28 11:28:27

Los comentarios están cerrados: los comentarios se cierran automáticamente una vez pasados 30 días. Si quieres comentar algo acerca de la anotación, puedes hacerlo por e-mail.

Algunas anotaciones relacionadas: