¿Hay excepciones para la mejor práctica de control de flujo en Python?

15

Estoy leyendo "Learning Python" y he encontrado lo siguiente:

Las excepciones definidas por el usuario también pueden indicar condiciones que no son de error. Por ejemplo, una rutina de búsqueda puede codificarse para generar una excepción cuando se encuentra una coincidencia en lugar de devolver un indicador de estado para que el llamante lo interprete. A continuación, el controlador de excepciones try / except / else hace el trabajo de un probador de valor de retorno if / else:

class Found(Exception): pass

def searcher():
    if ...success...:
        raise Found()            # Raise exceptions instead of returning flags
    else:
        return

Debido a que Python se escribe dinámicamente y es polimórfico con respecto al núcleo, las excepciones, en lugar de los valores de retorno centinela, son la forma generalmente preferida de señalar tales condiciones.

He visto este tipo de cosas discutidas varias veces en varios foros, y referencias a Python usando StopIteration para finalizar los bucles, pero no puedo encontrar mucho en las guías de estilo oficiales (PEP 8 tiene una referencia a las excepciones para el control de flujo) o declaraciones de desarrolladores. ¿Hay algo oficial que indique que esta es la mejor práctica para Python?

Esto ( ¿Las excepciones como flujo de control se consideran un antipatrón serio? Si es así, ¿por qué? ) También tiene varios comentaristas que afirman que este estilo es Pythonic. ¿En qué se basa esto?

TIA

JB
fuente

Respuestas:

25

El consenso general "¡no use excepciones!" Proviene principalmente de otros idiomas e incluso a veces está desactualizado.

  • En C ++, lanzar una excepción es muy costoso debido al "desbobinado de la pila". Cada declaración de variable local es como una withdeclaración en Python, y el objeto en esa variable puede ejecutar destructores. Estos destructores se ejecutan cuando se produce una excepción, pero también cuando se regresa de una función. Este "modismo RAII" es una característica integral del lenguaje y es muy importante para escribir código robusto y correcto, por lo que RAII versus excepciones baratas fue una compensación que C ++ decidió hacia RAII.

  • A principios de C ++, una gran cantidad de código no se escribía de manera segura: a menos que realmente use RAII, es fácil perder memoria y otros recursos. Por lo tanto, lanzar excepciones haría que ese código sea incorrecto. Esto ya no es razonable, ya que incluso la biblioteca estándar de C ++ usa excepciones: no puede pretender que no existen excepciones. Sin embargo, las excepciones siguen siendo un problema al combinar código C con C ++.

  • En Java, cada excepción tiene un seguimiento de pila asociado. El seguimiento de la pila es muy valioso cuando se depuran errores, pero es un esfuerzo inútil cuando la excepción nunca se imprime, por ejemplo, porque solo se usó para el flujo de control.

Entonces, en esos idiomas, las excepciones son "demasiado caras" para ser utilizadas como flujo de control. En Python esto no es un problema y las excepciones son mucho más baratas. Además, el lenguaje Python ya sufre una sobrecarga que hace que el costo de las excepciones sea imperceptible en comparación con otras construcciones de flujo de control: por ejemplo, verificar si existe una entrada dict con una prueba de membresía explícita if key in the_dict: ...es generalmente tan rápido como simplemente acceder a la entrada the_dict[key]; ...y verificar si obtener un KeyError. Algunas características integrales del lenguaje (por ejemplo, generadores) están diseñadas en términos de excepciones.

Entonces, si bien no hay una razón técnica para evitar específicamente las excepciones en Python, todavía queda la pregunta de si debe usarlas en lugar de valores de retorno. Los problemas a nivel de diseño con excepciones son:

  • no son del todo obvios. No puede ver fácilmente una función y ver qué excepciones puede generar, por lo que no siempre sabe qué atrapar. El valor de retorno tiende a estar más bien definido.

  • Las excepciones son el flujo de control no local que complica su código. Cuando lanza una excepción, no sabe dónde se reanudará el flujo de control. Para los errores que no se pueden manejar de inmediato, esta es probablemente una buena idea, cuando notificar a la persona que llama sobre una condición es completamente innecesario.

La cultura de Python generalmente está inclinada a favor de las excepciones, pero es fácil exagerar. Imagine una list_contains(the_list, item)función que verifica si la lista contiene un elemento igual a ese elemento. Si el resultado se comunica a través de excepciones que es absolutamente molesto, porque tenemos que llamarlo así:

try:
  list_contains(invited_guests, person_at_door)
except Found:
  print("Oh, hello {}!".format(person_at_door))
except NotFound:
  print("Who are you?")

Devolver un bool sería mucho más claro:

if list_contains(invited_guests, person_at_door):
  print("Oh, hello {}!".format(person_at_door))
else:
  print("Who are you?")

Si ya se supone que la función devuelve un valor, entonces devolver un valor especial para condiciones especiales es bastante propenso a errores, porque las personas se olvidarán de verificar este valor (esa es probablemente la causa de 1/3 de los problemas en C). Una excepción suele ser más correcta.

Un buen ejemplo es una pos = find_string(haystack, needle)función que busca la primera aparición de la needlecadena en la `cadena de pajar y devuelve la posición de inicio. Pero, ¿qué pasa si el cordel de pajar no contiene el cordel de agujas?

La solución de C e imitada por Python es devolver un valor especial. En C esto es un puntero nulo, en Python esto es -1. Esto conducirá a resultados sorprendentes cuando la posición se use como un índice de cadena sin verificar, especialmente porque -1es un índice válido en Python. En C, su puntero NULL al menos le dará un segfault.

En PHP, se devuelve un valor especial de un tipo diferente: el booleano en FALSElugar de un entero. Resulta que esto no es realmente mejor debido a las reglas de conversión implícitas del lenguaje (¡pero tenga en cuenta que en Python también se pueden usar booleanos como ints!). Las funciones que no devuelven un tipo consistente generalmente se consideran muy confusas.

Una variante más robusta hubiera sido lanzar una excepción cuando no se puede encontrar la cadena, lo que asegura que durante el flujo de control normal es imposible usar accidentalmente el valor especial en lugar de un valor ordinario:

 try:
   pos = find_string(haystack, needle)
   do_something_with(pos)
 except NotFound:
   ...

Alternativamente, siempre se puede devolver un tipo que no se puede usar directamente pero que primero se debe desenvolver, por ejemplo, una tupla de resultado-bool donde el booleano indica si ocurrió una excepción o si el resultado es utilizable. Luego:

pos, ok = find_string(haystack, needle)
if not ok:
  ...
do_something_with(pos)

Esto lo obliga a manejar los problemas de inmediato, pero se vuelve molesto muy rápidamente. También evita que encadena la función fácilmente. Cada llamada a función ahora necesita tres líneas de código. Golang es un lenguaje que cree que esta molestia vale la pena.

Para resumir, las excepciones no carecen por completo de problemas y definitivamente se pueden usar en exceso, especialmente cuando reemplazan un valor de retorno "normal". Pero cuando se usa para indicar condiciones especiales (no necesariamente solo errores), las excepciones pueden ayudarlo a desarrollar API que sean limpias, intuitivas, fáciles de usar y difíciles de usar mal.

amon
fuente
1
Gracias por la respuesta en profundidad. Vengo de un entorno de otros lenguajes como Java, donde "las excepciones son solo para condiciones excepcionales", así que esto es nuevo para mí. ¿Hay desarrolladores centrales de Python que hayan dicho esto de la misma manera que con otras pautas de Python como EAFP, EIBTI, etc. (en una lista de correo, publicación de blog, etc.) Me gustaría poder citar algo oficial si otro desarrollador / jefe pregunta. ¡Gracias!
JB
Al sobrecargar el método fillInStackTrace de su clase de excepción personalizada para que solo regrese de inmediato, puede hacer que subir sea muy barato, ya que el apilamiento es costoso y aquí es donde se hace.
John Cowan
Tu primera bala en tu introducción fue confusa al principio. ¿Tal vez podría cambiarlo a algo como "El código de manejo de excepciones en C ++ moderno es de costo cero; pero lanzar una excepción es costoso"? (para acechadores: stackoverflow.com/questions/13835817/… ) [editar: editar propuesta]
phresnel
Tenga en cuenta que para las operaciones de búsqueda en el diccionario utilizando un código collections.defaultdicto my_dict.get(key, default)hace que el código sea mucho más claro quetry: my_dict[key] except: return default
Steve Barnes el
11

¡NO! - no en general - las excepciones no se consideran buenas prácticas de control de flujo con la excepción de una sola clase de código. El único lugar donde las excepciones se consideran una forma razonable, o incluso mejor, de señalar una condición son las operaciones de generador o iterador. Estas operaciones pueden devolver cualquier valor posible como resultado válido, por lo que se necesita un mecanismo para señalar un final.

Considere leer un archivo binario de transmisión de un byte a la vez; absolutamente cualquier valor es un resultado potencialmente válido, pero aún tenemos que señalar el final del archivo. Por lo tanto, tenemos una opción, devolver dos valores (el valor de byte y un indicador válido) cada vez o generar una excepción cuando no hay más que hacer. En los dos casos, el código consumidor puede verse así:

# Using validity flag
valid, val = readbyte(source)
while valid:
    processbyte(val)
    valid, val = readbyte(source)
tidy_up()

alternativamente:

# With exceptions
try:
   val = readbyte(source)
   processbyte(val) # Note if a problem occurs here it will also raise an exception
except Exception: # Use a specific exception here!
   tidy_up()

Pero esto, desde que PEP 343 fue implementado y portado de nuevo, todo ha sido cuidadosamente envuelto en la withdeclaración. Lo anterior se convierte en el muy pitónico:

with open(source) as input:
    for val in input.readbyte(): # This line will raise a StopIteration exception an call input.__exit__()
        processbyte(val) # Not called if there is nothing read

En python3 esto se convirtió en:

for val in open(source, 'rb').read():
     processbyte(val)

Le recomiendo encarecidamente que lea PEP 343, que ofrece antecedentes, justificación, ejemplos, etc.

También es habitual utilizar una excepción para señalar el final del procesamiento cuando se utilizan funciones de generador para indicar el final.

Me gustaría agregar que su ejemplo de buscador es casi seguro al revés, tales funciones deberían ser generadores, devolviendo la primera coincidencia en la primera llamada, luego llamadas sustituyentes que devuelven la próxima coincidencia y generando una NotFoundexcepción cuando no hay más coincidencias.

Steve Barnes
fuente
Usted dice: "¡NO! - no en general - las excepciones no se consideran buenas prácticas de control de flujo con la excepción de una sola clase de código". ¿Dónde está la justificación de esta declaración enfática? Esta respuesta parece centrarse estrechamente en el caso de iteración. Hay muchos otros casos en los que la gente podría pensar en usar excepciones. ¿Cuál es el problema de hacerlo en esos otros casos?
Daniel Waltrip el
@DanielWaltrip Veo demasiado código, en varios lenguajes de programación, donde se usan excepciones, a menudo demasiado, cuando un simple ifsería mejor. Cuando se trata de depurar y probar o incluso razonar sobre el comportamiento de sus códigos, es mejor no confiar en las excepciones, deberían ser la excepción. He visto, en el código de producción, un cálculo que regresó a través de un lanzamiento / captura en lugar de un simple retorno, esto significó que cuando el cálculo alcanza un error de división por cero, devuelve un valor aleatorio en lugar de fallar donde ocurrió el error, esto causó varias horas de depuración
Steve Barnes
Hola @SteveBarnes, el ejemplo de captura de error dividir por cero parece una mala programación (con una captura demasiado general que no vuelve a generar la excepción).
wTyeRogers
Su respuesta parece reducirse a: A) "¡NO!" B) hay ejemplos válidos de dónde el flujo de control a través de excepciones funciona mejor que las alternativas C) una corrección del código de la pregunta para usar generadores (que usan excepciones como flujo de control, y lo hacen bien). Si bien su respuesta es generalmente informativa, no parece existir ningún argumento para apoyar el elemento A, que, al estar en negrita y aparentemente la declaración principal, esperaría algún apoyo. Si fuera compatible, no rechazaría su respuesta. Por desgracia ...
wTyeRogers