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 por la que existe el 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 es así?

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. control.

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
respuesta

"I do not know if it is out of ignorance, but I do not like that kind of programming, as it is using exceptions to perform flow control."

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

Incluso los desarrolladores centrales 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 "look-before-you-leap" . Por ejemplo, probar os.path.exists da como resultado información que puede estar desactualizada en el momento en que la use. Asimismo, Queue.full devuelve información que puede estar obsoleta. El estilo try-except-else producirá un código más confiable en estos casos.

"It my understanding that exceptions are not errors, they should only be used for exceptional conditions"

En algunos otros idiomas, esa regla refleja sus normas culturales como se refleja 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 controlar el flujo. 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 algunos otros lenguajes, pero no para Python.

"However, if it is included in the language itself, there must be a good reason for it, isn't it?"

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

Además, las excepciones pueden simplificar un poco el código en situaciones comunes en las que la capacidad de manejar un problema está muy alejada de donde surgió el problema. Por ejemplo, es común tener un código de interfaz de usuario de nivel superior que llame a un código para la lógica empresarial que, a su vez, llame 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 pueden manejarse en código de nivel superior (como pedirle 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 por completo el problema y se desvinculen agradablemente de ese aspecto del control de flujo.

Hay una buena entrada de blog sobre la indispensabilidad de las excepciones aquí .

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

"What is the reason for the try-except-else to exist?"

La cláusula else en sí misma es interesante. Se ejecuta cuando no hay excepción pero antes de la cláusula final. 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 torpe práctica 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 bloque de prueba.

El caso de uso de ejecutar código desprotegido adicional antes de la finalización no surge muy a menudo. 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 ocurre ninguna 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 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 bloque try es un poco de embellecimiento (alinear 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.

What is the reason for the try-except-else to exist?

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

Una elsecláusula se ejecutará si no hubo errores, y al no ejecutar ese código en el trybloque, evita detectar un error inesperado. Una vez más, 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 "intentar, excepto" tiene dos cláusulas opcionales, elsey finally. Así que en realidad es try-except-else-finally.

elseevaluará solo si no hay una 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

por lo tanto, si comparamos an elsecon la alternativa (que podría generar 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

finallyse ejecutará pase lo que pase, incluso si se evalúa otra línea con una declaración de retorno.

Desglosado con pseudocódigo

Podría ayudar desglosar esto, en la forma más pequeña posible que demuestre todas las características, con comentarios. Suponga que este pseudocódigo sintácticamente correcto (pero no ejecutable a menos que se definan los nombres) está 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, donde se ejecutaría si no hubiera excepciones, pero ¿qué pasa si ese código genera una excepción del tipo que estamos detectando? Dejarlo en el trybloque ocultaría ese error.

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

It is my understanding that exceptions are not errors

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 Pitón 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 terminar 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ó:

Say you have a method which pings an external API and you want to handle the exception at a class outside the API wrapper, do you simply return e from the method under the except clause where e is the exception object?

No, no devuelves la excepción, solo vuelves a subirla con un desnudo raisepara preservar el seguimiento de la pila.

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

O, en Python 3, puede generar una nueva excepción y conservar el seguimiento con el encadenamiento de excepciones:

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

Elaboro en mi respuesta aquí .

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

En general, esto es algo bueno, ya que trabajar de esta manera ayuda a evitar algunos problemas (como un ejemplo obvio, las condiciones de carrera a menudo se evitan) 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 debe procesarse, 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 con 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 realizan una vez. El código también sigue una progresión más lógica, la ruta del código principal es primero, seguida de 'si no funciona, haz esto'.

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

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

Implementarlo y venir 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.

Is it a good practice to use try-except-else in 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 mucho más detallada y visualmente desordenada de escribir lo que se puede ejecutar de manera eficiente en una sola línea con un método atómico. Hay otros casos en los que esto es cierto.

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

Lea el Zen de Python , comprenda que hay principios que están en tensión y tenga cuidado con los dogmas que se basan demasiado en cualquiera de las declaraciones que contiene.

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, usar el bloque else hace que su código sea más legible y solo se ejecuta cuando no se lanza una excepción.

In [14]: try:
             dict_["b"]
         except KeyError:
             pass
         else:
             print "something"
   ....:

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

avoid else clauses in try/excepts because they're unfamiliar to most people

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 lean su código quieran verificar los documentos para asegurarse de que entienden lo que está sucediendo.

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

Entonces, donde sea que vea un código como el ejemplo 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 está rebajado en un nivel.

Siempre que veas 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

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

Estoy tratando de responder a esta pregunta en un ángulo ligeramente diferente.

Había 2 partes de la pregunta del OP, y también agrego la tercera.

  1. ¿Cuál es la razón por la que existe el try-except-else?
  2. ¿El patrón try-except-else, o Python en general, fomenta el uso de excepciones para el control de flujo?
  3. ¿Cuándo usar excepciones, de todos modos?

Pregunta 1: ¿Cuál es la razón para que exista el intento excepto si no?

Se puede responder desde un punto de vista táctico. Por supuesto, hay una razón para try...except...existir. La única adición nueva aquí es la else...cláusula, cuya utilidad se reduce a su singularidad:

  • Ejecuta un bloque de código adicional SOLO CUANDO no hubo ninguna excepción en el try...bloque.

  • Ejecuta ese bloque de código adicional, FUERA del try...bloque (lo que significa que cualquier posible excepción que ocurra dentro del else...bloque NO se detectará).

  • Ejecuta ese bloque de código adicional ANTES de la final...finalización.

      db = open(...)
      try:
          db.insert(something)
      except Exception:
          db.rollback()
          logging.exception('Failing: %s, db is ROLLED BACK', something)
      else:
          db.commit()
          logging.info(
              'Successful: %d',  # <-- For the sake of demonstration,
                                 # there is a typo %d here to trigger an exception.
                                 # If you move this section into the try... block,
                                 # the flow would unnecessarily go to the rollback path.
              something)
      finally:
          db.close()
    

    En el ejemplo anterior, no puede mover esa línea de registro exitosa detrás del finally...bloque. Tampoco puede moverlo dentro del try...bloque, debido a la posible excepción dentro del else...bloque.

Pregunta 2: ¿Python fomenta el uso de excepciones para el control de flujo?

No encontré documentación escrita oficial para respaldar esa afirmación. (Para los lectores que no estén de acuerdo: dejen comentarios con enlaces a las evidencias que encontraron). El único párrafo vagamente relevante que encontré es este término EAFP :

EAFP

Easier to ask for forgiveness than permission. This common Python coding style assumes the existence of valid keys or attributes and catches exceptions if the assumption proves false. This clean and fast style is characterized by the presence of many try and except statements. The technique contrasts with the LBYL style common to many other languages such as C.

Dicho párrafo simplemente describía que, en lugar de hacer esto:

def make_some_noise(speaker):
    if hasattr(speaker, "quack"):
        speaker.quack()

Preferiríamos esto:

def make_some_noise(speaker):
    try:
        speaker.quack()
    except AttributeError:
        logger.warning("This speaker is not a duck")

make_some_noise(DonaldDuck())  # This would work
make_some_noise(DonaldTrump())  # This would trigger exception

o potencialmente incluso omitiendo el intento... excepto:

def make_some_noise(duck):
    duck.quack()

Por eso, la EAFP fomenta el duck-typing. Pero no fomenta el uso de excepciones para el control de flujo .

Pregunta 3: ¿En qué situación debe diseñar su programa para emitir excepciones?

Es una conversación discutible si es antipatrón usar la excepción como flujo de control. Porque, una vez que se toma una decisión de diseño para una función determinada, también se determinaría su patrón de uso, y luego la persona que llama no tendría más remedio que usarla de esa manera.

Entonces, volvamos a los fundamentos para ver cuándo una función produciría mejor su resultado devolviendo un valor o emitiendo excepciones.

¿Cuál es la diferencia entre el valor de retorno y la excepción?

  1. Su "radio de explosión" es diferente. El valor de retorno solo está disponible para la persona que llama inmediatamente; La excepción se puede transmitir automáticamente a una distancia ilimitada hasta que se detecte.

  2. Sus patrones de distribución son diferentes. El valor devuelto es, por definición, un dato (aunque podría devolver un tipo de datos compuesto, como un diccionario o un objeto contenedor, técnicamente sigue siendo un valor). El mecanismo de excepción, por el contrario, permite que se devuelvan múltiples valores (uno a la vez) a través de su respectivo canal dedicado. Aquí, cada bloque except FooError: ...y except BarError: ...se considera como su propio canal dedicado.

Por lo tanto, depende de cada escenario diferente usar un mecanismo que se ajuste bien.

  • Es mejor que todos los casos normales se devuelvan a través del valor de retorno, porque lo más probable es que las personas que llaman necesiten usar ese valor de retorno de inmediato. El enfoque de valor de retorno también permite anidar capas de llamadores en un estilo de programación funcional. El largo radio de explosión del mecanismo de excepción y los múltiples canales no ayudan aquí. Por ejemplo, sería poco intuitivo si cualquier función nombrada get_something(...)produce su resultado de camino feliz como una excepción. (Este no es realmente un ejemplo inventado. Hay una práctica a implementar BinaryTree.Search(value)para usar la excepción para devolver el valor en medio de una recursión profunda).

  • Si es probable que la persona que llama se olvide de manejar el centinela de error del valor de retorno, probablemente sea una buena idea usar la característica de excepción #2 para salvar a la persona que llama de su error oculto. Un no ejemplo típico sería el position = find_string(haystack, needle), desafortunadamente su valor de retorno de -1o nulltendería a causar un error en la persona que llama.

  • Si el centinela de error chocara con un valor normal en el espacio de nombres de resultados, es casi seguro que usará una excepción, porque tendría que usar un canal diferente para transmitir ese error.

  • Si el canal normal, es decir, el valor de retorno, ya se usa en el camino feliz, Y el camino feliz NO tiene un control de flujo sofisticado, no tiene más remedio que usar la excepción para el control de flujo. La gente sigue hablando de cómo Python usa StopIterationla excepción para la terminación de la iteración y la usa para justificar "usar la excepción para el control de flujo". Pero en mi humilde opinión, esta es solo una opción práctica en una situación particular, no generaliza ni glorifica "usar la excepción para el control de flujo".

En este punto, si ya tomó una decisión acertada sobre si su función get_stock_price()produciría solo un valor de retorno o también generaría excepciones, o si esa función es proporcionada por una biblioteca existente para que su comportamiento haya sido decidido por mucho tiempo, no tiene mucho elección en la escritura de su llamante calculate_market_trend(). Usar get_stock_price()la excepción de para controlar el flujo en su calculate_market_trend()es simplemente una cuestión de si su lógica comercial requiere que lo haga. Si es así, hazlo; de lo contrario, deje que la excepción suba a un nivel superior (esto utiliza la característica n. ° 1 "radio de explosión largo" de excepción).

En particular, si está implementando una biblioteca de capa intermedia Fooy está creando una dependencia en la biblioteca de nivel inferior Bar, probablemente querrá ocultar los detalles de su implementación capturando todos los Bar.ThisError, Bar.ThatError, ... y mapeándolos en Foo.GenericError. En este caso, el radio de explosión largo en realidad está trabajando en nuestra contra, por lo que puede esperar "solo si la barra de la biblioteca devolviera sus errores a través de valores de retorno". Pero, de nuevo, esa decisión se tomó hace mucho tiempo Bar, por lo que puede vivir con ella.

Considerándolo todo, creo que si usar la excepción como flujo de control es un punto discutible.

OP, TIENES RAZON. El else después de try/except 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 else después de try/except no se escribe con frecuencia, por lo que lleva un momento darse cuenta de 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 cuantas más características, menos claras y obvias son las cosas porque la gente no suele usar esas campanas y silbatos.

Solo mis 5 centavos aquí. Tengo que ir detrás y limpiar una gran cantidad de código escrito por desarrolladores de primer año de la universidad que piensan que son inteligentes y quieren escribir código de una manera súper estricta y eficiente cuando eso solo lo convierte en un desastre. para probar y leer/modificar más tarde. Voto por la legibilidad todos los días y dos veces los domingos.