¿Es una buena práctica usar try-except-else en Python?

440

De vez en cuando en Python, veo el bloque:

try:
   try_this(whatever)
except SomeException as exception:
   #Handle exception
else:
   return something

¿Cuál es la razón para que exista try-except-else?

No me gusta ese tipo de programación, ya que utiliza excepciones para realizar el control de flujo. Sin embargo, si está incluido en el idioma, debe haber una buena razón para ello, ¿no?

Tengo entendido que las excepciones no son errores , y que solo deben usarse para condiciones excepcionales (por ejemplo, intento escribir un archivo en el disco y no hay más espacio, o tal vez no tengo permiso), y no para el flujo controlar.

Normalmente manejo excepciones como:

something = some_default_value
try:
    something = try_this(whatever)
except SomeException as exception:
    #Handle exception
finally:
    return something

O si realmente no quiero devolver nada si ocurre una excepción, entonces:

try:
    something = try_this(whatever)
    return something
except SomeException as exception:
    #Handle exception
Juan Antonio Gómez Moriano
fuente

Respuestas:

667

"No sé si es por ignorancia, pero no me gusta ese tipo de programación, ya que utiliza excepciones para realizar el control de flujo".

En el mundo de Python, el uso de excepciones para el control de flujo es común y normal.

Incluso los desarrolladores principales de Python usan excepciones para el control de flujo y ese estilo está fuertemente integrado en el lenguaje (es decir, el protocolo iterador usa StopIteration para señalar la terminación del bucle).

Además, el estilo try-except-se utiliza para evitar las condiciones de carrera inherentes a algunas de las construcciones de "mira antes de saltar" . Por ejemplo, probar os.path.exists da como resultado información que puede estar desactualizada para cuando la use. Del mismo modo, Queue.full devuelve información que puede estar obsoleta. El estilo try-except-else producirá un código más confiable en estos casos.

"Entiendo que las excepciones no son errores, solo deben usarse para condiciones excepcionales"

En algunos otros idiomas, esa regla refleja sus normas culturales tal como se reflejan en sus bibliotecas. La "regla" también se basa en parte en consideraciones de rendimiento para esos idiomas.

La norma cultural de Python es algo diferente. En muchos casos, debe usar excepciones para control-flow. Además, el uso de excepciones en Python no ralentiza el código circundante y el código de llamada como lo hace en algunos lenguajes compilados (es decir, CPython ya implementa código para la verificación de excepciones en cada paso, independientemente de si realmente usa excepciones o no).

En otras palabras, su comprensión de que "las excepciones son para lo excepcional" es una regla que tiene sentido en otros idiomas, pero no para Python.

"Sin embargo, si está incluido en el lenguaje en sí, debe haber una buena razón para ello, ¿no?"

Además de ayudar a evitar las condiciones de carrera, las excepciones también son muy útiles para extraer el manejo de errores fuera de los bucles. Esta es una optimización necesaria en lenguajes interpretados que no tienden a tener un movimiento de código invariante de bucle automático .

Además, las excepciones pueden simplificar bastante el código en situaciones comunes donde la capacidad de manejar un problema está muy lejos de donde surgió el problema. Por ejemplo, es común tener un código de llamada de código de interfaz de usuario de nivel superior para la lógica empresarial que a su vez llama a rutinas de bajo nivel. Las situaciones que surgen en las rutinas de bajo nivel (como registros duplicados para claves únicas en accesos a bases de datos) solo se pueden manejar en el código de nivel superior (como solicitar al usuario una nueva clave que no entre en conflicto con las claves existentes). El uso de excepciones para este tipo de flujo de control permite que las rutinas de nivel medio ignoren completamente el problema y se desacoplen de ese aspecto del control de flujo.

Aquí hay una buena publicación de blog sobre la indispensabilidad de las excepciones .

Además, vea esta respuesta de desbordamiento de pila: ¿Son realmente excepciones para errores excepcionales?

"¿Cuál es la razón para que exista el try-except-else?"

La cláusula else es interesante. Se ejecuta cuando no hay excepción, pero antes de la cláusula finalmente. Ese es su propósito principal.

Sin la cláusula else, la única opción para ejecutar código adicional antes de la finalización sería la práctica torpe de agregar el código a la cláusula try. Eso es torpe porque corre el riesgo de generar excepciones en el código que no estaba destinado a ser protegido por el try-block.

El caso de uso de ejecutar código desprotegido adicional antes de la finalización no surge con mucha frecuencia. Por lo tanto, no espere ver muchos ejemplos en el código publicado. Es algo raro

Otro caso de uso para la cláusula else es realizar acciones que deben ocurrir cuando no se produce una excepción y que no ocurren cuando se manejan las excepciones. Por ejemplo:

recip = float('Inf')
try:
    recip = 1 / f(x)
except ZeroDivisionError:
    logging.info('Infinite result')
else:
    logging.info('Finite result')

Otro ejemplo ocurre en los corredores unittest:

try:
    tests_run += 1
    run_testcase(case)
except Exception:
    tests_failed += 1
    logging.exception('Failing test case: %r', case)
    print('F', end='')
else:
    logging.info('Successful test case: %r', case)
    print('.', end='')

Por último, el uso más común de una cláusula else en un try-block es para un poco de embellecimiento (alineando los resultados excepcionales y los resultados no excepcionales en el mismo nivel de sangría). Este uso es siempre opcional y no es estrictamente necesario.

Raymond Hettinger
fuente
28
"Eso es torpe porque se corre el riesgo de generar excepciones en el código que no estaba destinado a ser protegido por el try-block". Este es el aprendizaje más importante aquí
Felix Dombek
2
Gracias por la respuesta. Para los lectores que buscan ejemplos de uso de probar excepto excepto, eche un vistazo al método de copia de
archivos de shutil
2
El punto es que la cláusula else solo se ejecuta cuando la cláusula try tiene éxito.
Jonathan
173

¿Cuál es la razón para que exista try-except-else?

Un trybloque le permite manejar un error esperado. El exceptbloque solo debe capturar excepciones que esté preparado para manejar. Si maneja un error inesperado, su código puede hacer lo incorrecto y ocultar errores.

Se elseejecutará una cláusula si no hubo errores, y al no ejecutar ese código en el trybloque, evitará detectar un error inesperado. Nuevamente, detectar un error inesperado puede ocultar errores.

Ejemplo

Por ejemplo:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
else:
    return something

La suite "try, except" tiene dos cláusulas opcionales, elsey finally. Entonces es en realidad try-except-else-finally.

elseevaluará solo si no hay excepción del trybloque. Nos permite simplificar el código más complicado a continuación:

no_error = None
try:
    try_this(whatever)
    no_error = True
except SomeException as the_exception:
    handle(the_exception)
if no_error:
    return something

así que si comparamos una elsecon la alternativa (que podría crear errores), vemos que reduce las líneas de código y podemos tener una base de código más legible, mantenible y con menos errores.

finally

finally se ejecutará sin importar qué, incluso si otra línea se está evaluando con una declaración de devolución.

Desglosado con pseudocódigo

Podría ser útil desglosar esto, en la forma más pequeña posible que muestre todas las características, con comentarios. Suponga que este pseudocódigo está sintácticamente correcto (pero no se puede ejecutar a menos que se definan los nombres) en una función.

Por ejemplo:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle_SomeException(the_exception)
    # Handle a instance of SomeException or a subclass of it.
except Exception as the_exception:
    generic_handle(the_exception)
    # Handle any other exception that inherits from Exception
    # - doesn't include GeneratorExit, KeyboardInterrupt, SystemExit
    # Avoid bare `except:`
else: # there was no exception whatsoever
    return something()
    # if no exception, the "something()" gets evaluated,
    # but the return will not be executed due to the return in the
    # finally block below.
finally:
    # this block will execute no matter what, even if no exception,
    # after "something" is eval'd but before that value is returned
    # but even if there is an exception.
    # a return here will hijack the return functionality. e.g.:
    return True # hijacks the return in the else clause above

Es cierto que podríamos incluir el código en el elsebloque en el trybloque en su lugar, donde se ejecutaría si no hubiera excepciones, pero ¿y si ese código genera una excepción del tipo que estamos atrapando? Dejarlo en el trybloque escondería ese error.

Queremos minimizar las líneas de código en el trybloque para evitar detectar excepciones que no esperábamos, bajo el principio de que si nuestro código falla, queremos que falle en voz alta. Esta es una mejor práctica .

Entiendo que las excepciones no son errores

En Python, la mayoría de las excepciones son errores.

Podemos ver la jerarquía de excepciones usando pydoc. Por ejemplo, en Python 2:

$ python -m pydoc exceptions

o Python 3:

$ python -m pydoc builtins

Nos dará la jerarquía. Podemos ver que la mayoría de los tipos Exceptionson errores, aunque Python usa algunos de ellos para cosas como finalizar forbucles ( StopIteration). Esta es la jerarquía de Python 3:

BaseException
    Exception
        ArithmeticError
            FloatingPointError
            OverflowError
            ZeroDivisionError
        AssertionError
        AttributeError
        BufferError
        EOFError
        ImportError
            ModuleNotFoundError
        LookupError
            IndexError
            KeyError
        MemoryError
        NameError
            UnboundLocalError
        OSError
            BlockingIOError
            ChildProcessError
            ConnectionError
                BrokenPipeError
                ConnectionAbortedError
                ConnectionRefusedError
                ConnectionResetError
            FileExistsError
            FileNotFoundError
            InterruptedError
            IsADirectoryError
            NotADirectoryError
            PermissionError
            ProcessLookupError
            TimeoutError
        ReferenceError
        RuntimeError
            NotImplementedError
            RecursionError
        StopAsyncIteration
        StopIteration
        SyntaxError
            IndentationError
                TabError
        SystemError
        TypeError
        ValueError
            UnicodeError
                UnicodeDecodeError
                UnicodeEncodeError
                UnicodeTranslateError
        Warning
            BytesWarning
            DeprecationWarning
            FutureWarning
            ImportWarning
            PendingDeprecationWarning
            ResourceWarning
            RuntimeWarning
            SyntaxWarning
            UnicodeWarning
            UserWarning
    GeneratorExit
    KeyboardInterrupt
    SystemExit

Un comentarista preguntó:

Digamos que tiene un método que hace ping a una API externa y desea manejar la excepción en una clase fuera del contenedor API, ¿simplemente devuelve e del método bajo la cláusula except donde e es el objeto de excepción?

No, no devuelve la excepción, simplemente vuelva a subirla con un desnudo raisepara preservar el stacktrace.

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
    raise

O, en Python 3, puede generar una nueva excepción y preservar la traza inversa con el encadenamiento de excepciones:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
    raise DifferentException from the_exception

Elaboro mi respuesta aquí .

Aaron Hall
fuente
¡Votado! ¿Qué sueles hacer dentro de la parte del mango? Digamos que tiene un método que hace ping a una API externa y desea manejar la excepción en una clase fuera del contenedor API, ¿simplemente devuelve e del método bajo la cláusula except donde e es el objeto de excepción?
PirateApp
1
@PirateApp gracias! no, no lo devuelva, probablemente debería volver a subir con un raiseencadenamiento simple o de excepción, pero eso es más sobre el tema y cubierto aquí: stackoverflow.com/q/2052390/541136 - Probablemente elimine estos comentarios después de haber visto que los has visto.
Aaron Hall
muchas gracias por los detalles! pasando por la publicación ahora
PirateApp
36

Python no se suscribe a la idea de que las excepciones solo deben usarse para casos excepcionales, de hecho, el idioma es "pedir perdón, no permiso" . Esto significa que usar excepciones como parte rutinaria de su control de flujo es perfectamente aceptable y, de hecho, se recomienda.

En general, esto es algo bueno, ya que trabajar de esta manera ayuda a evitar algunos problemas (como un ejemplo obvio, a menudo se evitan las condiciones de carrera), y tiende a hacer que el código sea un poco más legible.

Imagine que tiene una situación en la que toma alguna entrada del usuario que necesita ser procesada, pero tiene un valor predeterminado que ya está procesado. La try: ... except: ... else: ...estructura hace que el código sea muy legible:

try:
   raw_value = int(input())
except ValueError:
   value = some_processed_value
else: # no error occured
   value = process_value(raw_value)

Compare cómo podría funcionar en otros idiomas:

raw_value = input()
if valid_number(raw_value):
    value = process_value(int(raw_value))
else:
    value = some_processed_value

Tenga en cuenta las ventajas. No es necesario verificar que el valor sea válido y analizarlo por separado, se hacen una vez. El código también sigue una progresión más lógica, la ruta del código principal es la primera, seguida de 'si no funciona, haga esto'.

El ejemplo es, naturalmente, un poco artificial, pero muestra que hay casos para esta estructura.

Gareth Latty
fuente
15

¿Es una buena práctica usar try-except-else en python?

La respuesta a esto es que depende del contexto. Si haces esto:

d = dict()
try:
    item = d['item']
except KeyError:
    item = 'default'

Demuestra que no conoces muy bien Python. Esta funcionalidad está encapsulada en el dict.getmétodo:

item = d.get('item', 'default')

El bloque try/ exceptes una forma de escritura mucho más desordenada y visualmente desordenada que se puede ejecutar de manera eficiente en una sola línea con un método atómico. Hay otros casos donde esto es cierto.

Sin embargo, eso no significa que debamos evitar todo manejo de excepciones. En algunos casos se prefiere evitar las condiciones de carrera. No compruebe si existe un archivo, solo intente abrirlo y capture el IOError apropiado. En aras de la simplicidad y la legibilidad, intente encapsular esto o factorizarlo como sea apropiado.

Lea el Zen de Python , entendiendo que hay principios que están en tensión, y desconfíe del dogma que se basa demasiado en cualquiera de las declaraciones en él.

Aaron Hall
fuente
12

Vea el siguiente ejemplo que ilustra todo sobre try-except-else-finally:

for i in range(3):
    try:
        y = 1 / i
    except ZeroDivisionError:
        print(f"\ti = {i}")
        print("\tError report: ZeroDivisionError")
    else:
        print(f"\ti = {i}")
        print(f"\tNo error report and y equals {y}")
    finally:
        print("Try block is run.")

Impleméntalo y ven por:

    i = 0
    Error report: ZeroDivisionError
Try block is run.
    i = 1
    No error report and y equals 1.0
Try block is run.
    i = 2
    No error report and y equals 0.5
Try block is run.
Cálculo
fuente
3
Este es un gran ejemplo simple que demuestra rápidamente la cláusula de prueba completa sin requerir que alguien (que puede tener mucha prisa) lea una larga explicación abstracta. (Por supuesto, cuando ya no tengan prisa, deberían regresar y leer el resumen completo.)
GlobalSoftwareSociety
6

Debe tener cuidado al usar el bloque finalmente, ya que no es lo mismo que usar un bloque else en el intento, excepto. El último bloque se ejecutará independientemente del resultado del intento, excepto.

In [10]: dict_ = {"a": 1}

In [11]: try:
   ....:     dict_["b"]
   ....: except KeyError:
   ....:     pass
   ....: finally:
   ....:     print "something"
   ....:     
something

Como todos han notado, el uso del bloque else hace que su código sea más legible y solo se ejecuta cuando no se produce una excepción

In [14]: try:
             dict_["b"]
         except KeyError:
             pass
         else:
             print "something"
   ....:
Greg
fuente
Sé que finalmente siempre se ejecuta, y es por eso que se puede utilizar para nuestra ventaja siempre estableciendo un valor predeterminado, por lo que en caso de excepción se devuelve, si no queremos devolver dicho valor en caso de excepción, es suficiente para eliminar el bloque final. Por cierto, usar pase en una captura de excepción es algo que nunca haría :)
Juan Antonio Gomez Moriano
@Juan Antonio Gómez Moriano, mi bloque de codificación es solo para fines de ejemplo. Probablemente nunca usaría el pase tampoco
Greg
4

Cada vez que ves esto:

try:
    y = 1 / x
except ZeroDivisionError:
    pass
else:
    return y

O incluso esto:

try:
    return 1 / x
except ZeroDivisionError:
    return None

Considere esto en su lugar:

import contextlib
with contextlib.suppress(ZeroDivisionError):
    return 1 / x
Rajiv Bakulesh Shah
fuente
1
No responde a mi pregunta, ya que fue simplemente un ejemplo, mi amigo.
Juan Antonio Gómez Moriano
En Python, las excepciones no son errores. Ni siquiera son excepcionales. En Python, es normal y natural usar excepciones para el control de flujo. Esto se evidencia por la inclusión de contextlib.suppress () en la biblioteca estándar. Vea la respuesta de Raymond Hettinger aquí: stackoverflow.com/a/16138864/1197429 (¡Raymond es un contribuidor central de Python y una autoridad en todo lo relacionado con Pythonic!)
Rajiv Bakulesh Shah
4

Solo porque nadie más ha publicado esta opinión, diría

evite las elsecláusulas try/excepts porque no son familiares para la mayoría de las personas

A diferencia de las palabras clave try, excepty finally, el significado de la elsecláusula no es evidente; Es menos legible. Debido a que no se usa con mucha frecuencia, hará que las personas que leen su código deseen verificar los documentos para asegurarse de que comprenden lo que está sucediendo.

(Estoy escribiendo esta respuesta precisamente porque encontré un try/except/elseen mi base de código y causó un momento wtf y me obligó a buscar en Google).

Entonces, donde veo código como el ejemplo de OP:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
else:
    # do some more processing in non-exception case
    return something

Preferiría refactorizar a

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
    return  # <1>
# do some more processing in non-exception case  <2>
return something
  • <1> retorno explícito, muestra claramente que, en el caso de excepción, hemos terminado de trabajar

  • <2> como un agradable efecto secundario menor, el código que solía estar en el elsebloque se ve afectado por un nivel.

hwjp
fuente
1
Contraargumento del argumento del diablo: cuanto más lo use la gente, más se adoptará. Solo algo para pensar, aunque estoy de acuerdo en que la legibilidad es importante. Dicho esto, una vez que alguien entienda try-else, diría que en muchos casos es mucho más legible que la alternativa.
bob
2

Este es mi fragmento simple sobre cómo entender el bloque try-except-else-finally en Python:

def div(a, b):
    try:
        a/b
    except ZeroDivisionError:
        print("Zero Division Error detected")
    else:
        print("No Zero Division Error")
    finally:
        print("Finally the division of %d/%d is done" % (a, b))

Probemos div 1/1:

div(1, 1)
No Zero Division Error
Finally the division of 1/1 is done

Probemos div 1/0

div(1, 0)
Zero Division Error detected
Finally the division of 1/0 is done
zakiakhmad
fuente
1
Creo que esto no puede ejemplificar por qué no puedes simplemente poner el código else dentro del intento
Mojimi
-4

OP, ERES CORRECTO. Lo demás después de intentar / excepto en Python es feo . conduce a otro objeto de control de flujo donde no se necesita ninguno:

try:
    x = blah()
except:
    print "failed at blah()"
else:
    print "just succeeded with blah"

Un equivalente totalmente claro es:

try:
    x = blah()
    print "just succeeded with blah"
except:
    print "failed at blah()"

Esto es mucho más claro que una cláusula else. El resto después de try / except no se escribe con frecuencia, por lo que lleva un momento determinar cuáles son las implicaciones.

Solo porque PUEDES hacer algo, no significa que DEBES hacer algo.

Se han agregado muchas funciones a los idiomas porque alguien pensó que podría ser útil. El problema es que, entre más características, las cosas menos claras y obvias se deben a que la gente generalmente no usa esas campanas y silbatos.

Solo mis 5 centavos aquí. Tengo que ir detrás y limpiar un montón de código escrito por desarrolladores universitarios de primer año que piensan que son inteligentes y quieren escribir código de una manera súper ajustada y súper eficiente cuando eso solo lo hace un desastre para intentar leer / modificar más tarde. Yo voto por la legibilidad todos los días y dos veces los domingos.

Kevin J. Rice
fuente
15
Tienes razón. Eso es totalmente claro y equivalente ... a menos que sea su printdeclaración la que falle. ¿Qué sucede si x = blah()devuelve un str, pero su declaración de impresión es print 'just succeeded with blah. x == %d' % x? Ahora tienes un TypeErrorser generado donde no estás preparado para manejar uno; estás inspeccionando x = blah()para encontrar la fuente de la excepción, y ni siquiera está allí. He hecho esto (o el equivalente) más de una vez donde elseme habría evitado cometer este error. Ahora lo se mejor. :-D
Doug R.
2
... y sí, tienes razón. La elsecláusula no es una declaración bonita, y hasta que no estés acostumbrado, no es intuitiva. Pero entonces, tampoco lo fue finallycuando empecé a usarlo ...
Doug R.
2
Para hacerse eco de Doug R., no es equivalente porque las excepciones durante las declaraciones en la elsecláusula no son capturadas por el except.
alastair
si ... excepto ... más es más legible, de lo contrario, debe leer "oh, después del bloque try, y sin excepción, vaya a la declaración fuera del bloque try", por lo que usar else: tiende a conectar semánticamente un poco la sintaxis mejor imo. Además, es una buena práctica dejar sus declaraciones sin atrapar fuera del bloque de prueba inicial.
cowbert 05 de
1
@DougR. "estás inspeccionando x = blah()para encontrar la fuente de la excepción", traceback¿ teniendo en cuenta por qué inspeccionarías la fuente de la excepción desde un lugar equivocado?
nehem