¿Cómo limpiar try / except / else anidado?

8

Cuando escribo código, a menudo quiero hacer algo como esto:

try:
    foo()
except FooError:
    handle_foo()
else:
    try:
        bar()
    except BarError:
        handle_bar()
    else:
        try:
            baz()
        except BazError:
            handle_baz()
        else:
            qux()
finally:
    cleanup()

Obviamente, esto es completamente ilegible. Pero está expresando una idea relativamente simple: ejecutar una serie de funciones (o fragmentos de código cortos), con un controlador de excepción para cada una, y detenerse tan pronto como una función falle. Me imagino que Python podría proporcionar azúcar sintáctico para este código, quizás algo como esto:

# NB: This is *not* valid Python
try:
    foo()
except FooError:
    handle_foo()
    # GOTO finally block
else try:
    bar()
except BarError:
    handle_bar()
    # ditto
else try:
    baz()
except BazError:
    handle_baz()
    # ditto
else:
    qux()
finally:
    cleanup()

Si no se generan excepciones, esto es equivalente a foo();bar();baz();qux();cleanup(). Si se generan excepciones, las maneja el manejador de excepciones apropiado (si corresponde) y pasamos a cleanup(). En particular, si bar()aumenta un FooErroro BazError, la excepción no se detectará y se propagará a la persona que llama. Esto es deseable por lo que solo detectamos excepciones que realmente esperamos manejar.

Independientemente de la fealdad sintáctica, ¿es este tipo de código una mala idea en general? Si es así, ¿cómo lo refactorizaría? Me imagino que los gestores de contexto podrían usarse para absorber parte de la complejidad, pero realmente no entiendo cómo funcionaría eso en el caso general.

Kevin
fuente
¿Qué tipo de cosas haces en las handle_*funciones?
Winston Ewert

Respuestas:

8
try:
    foo()
except FooError:
    handle_foo()
else:
    ...
finally:
    cleanup()

¿Qué handle_foohacer? Hay algunas cosas que normalmente hacemos en los bloques de manejo de excepciones.

  1. Limpieza después del error: Pero en este caso, foo () debería limpiar después de sí mismo, no dejar que lo hagamos. Además, la mayoría de los trabajos de limpieza se manejan mejor conwith
  2. Recupere el camino feliz: pero no está haciendo esto ya que no continúa con el resto de las funciones.
  3. Traduce el tipo de excepción: pero no está lanzando otra excepción
  4. Registre el error: Pero no debería ser necesario tener bloques de excepción especiales para cada tipo.

Me parece que está haciendo algo extraño en su manejo de excepciones. Su pregunta aquí es un síntoma simple sobre el uso de excepciones de una manera inusual. No estás cayendo en el patrón típico, y es por eso que esto se ha vuelto incómodo.

Sin una mejor idea de lo que estás haciendo en esas handle_funciones, eso es todo lo que puedo decir.

Winston Ewert
fuente
Esto es solo un código de muestra. Las handle_funciones podrían ser fácilmente fragmentos cortos de código que (digamos) registran el error y devuelven valores de reserva, generan nuevas excepciones o hacen cualquier otra cantidad de cosas. Mientras tanto, el foo(), bar(), etc funciones podrían también ser pequeños fragmentos de código. Por ejemplo, podríamos tener spam[eggs], y necesitamos atrapar KeyError.
Kevin
1
@Kevin, cierto, pero eso no cambia mi respuesta. Si su manejador returned o raised algo, el problema que menciona no surgiría. El resto de la función se omitirá automáticamente. De hecho, una manera simple de resolver su problema sería regresar en todos los bloques de excepción. El código es incómodo porque no está haciendo una fianza o aumento habitual para indicar que se está rindiendo.
Winston Ewert
Una buena respuesta en general, pero tengo que estar en desacuerdo con su punto 1: ¿qué evidencia hay que foodebería estar haciendo su propia limpieza? fooindica que algo salió mal y no tiene conocimiento de cuál debería ser el procedimiento de limpieza correcto.
Ethan Furman
@ EthanFurman, creo que podemos estar pensando en diferentes artículos bajo el título de limpieza. Me imagino que si foo abre archivos, conexiones de bases de datos, asigna memoria, etc., entonces es responsabilidad de foo asegurarse de que estén cerrados / desasigados antes de regresar. Eso es lo que quise decir con limpieza.
Winston Ewert
Ah, en ese caso estoy completamente de acuerdo contigo. :)
Ethan Furman
3

Parece que tiene una secuencia de comandos que pueden generar una excepción que debe manejarse antes de regresar. Intente agrupar su código y el manejo de excepciones en ubicaciones separadas. Creo que esto hace lo que pretendes.

try:
    foo()
    bar()
    baz()
    qux()

except FooError:
    handle_foo()
except BarError:
    handle_bar()
except BazError:
    handle_baz()

finally:
    cleanup()
BillThor
fuente
2
Me enseñaron a tratar de evitar poner más código del absolutamente necesario en el try:bloque. En este caso, me preocuparía bar()generar un FooError, que se manejaría incorrectamente con este código.
Kevin
2
Esto puede ser razonable si todos los métodos generan excepciones muy específicas, pero eso generalmente no es cierto. Lo recomiendo en contra de este enfoque.
Winston Ewert
@Kevin Ajustar cada línea de código en un bloque Try Catch Else rápidamente se vuelve ilegible. Esto se hace aún más por las reglas de sangría de Python. Piense hasta qué punto esto terminaría sangrado si el código tuviera 10 líneas de largo. ¿Sería legible o mantenible? Siempre que el código siga la responsabilidad única, mantendré el ejemplo anterior. Si no, entonces el código debe reescribirse.
BillThor
@ WinstonEwert En un ejemplo de la vida real, esperaría cierta superposición en los Errores (excepciones). Sin embargo, también esperaría que el manejo sea el mismo. BazError podría ser una interrupción del teclado. Esperaría que se maneje adecuadamente para las cuatro líneas de código.
BillThor
1

Hay un par de formas diferentes, según lo que necesite.

Aquí hay una manera con bucles:

try:
    for func, error, err_handler in (
            (foo, FooError, handle_foo),
            (bar, BarError, handle_bar),
            (baz, BazError, handle_baz),
        ):
        try:
            func()
        except error:
            err_handler()
            break
finally:
    cleanup()

Aquí hay una manera con una salida después del error_handler:

def some_func():
    try:
        try:
            foo()
        except FooError:
            handle_foo()
            return
        try:
            bar()
        except BarError:
            handle_bar()
            return
        try:
            baz()
        except BazError:
            handle_baz()
            return
        else:
            qux()
    finally:
        cleanup()

Personalmente, creo que la versión en bucle es más fácil de leer.

Ethan Furman
fuente
En mi humilde opinión, esta es una mejor solución que la aceptada, ya que 1) intenta una respuesta, y 2) deja la secuencia muy clara.
bob
0

En primer lugar, el uso adecuado de a withmenudo puede reducir o incluso eliminar gran parte del código de manejo de excepciones, mejorando tanto la facilidad de mantenimiento como la legibilidad.

Ahora, puede reducir el anidamiento de muchas maneras; otros carteles ya han proporcionado algunos, así que aquí está mi propia variación:

for _ in range(1):
    try:
        foo()
    except FooError:
        handle_foo()
        break
    try:
        bar()
    except BarError:
        handle_bar()
        break
    try:
        baz()
    except BazError:
        handle_baz()
        break
    qux()
cleanup()
Viento rizado
fuente