¿Controlar primero frente a manejo de excepciones?

83

Estoy trabajando en el libro "Head First Python" (es mi idioma para aprender este año) y Llegué a una sección donde discuten sobre dos técnicas de código:
Comprobando el manejo de la primera vs la excepción.

Aquí hay una muestra del código de Python:

# Checking First
for eachLine in open("../../data/sketch.txt"):
    if eachLine.find(":") != -1:
        (role, lineSpoken) = eachLine.split(":",1)
        print("role=%(role)s lineSpoken=%(lineSpoken)s" % locals())

# Exception handling        
for eachLine in open("../../data/sketch.txt"):
    try:
        (role, lineSpoken) = eachLine.split(":",1)
        print("role=%(role)s lineSpoken=%(lineSpoken)s" % locals())
    except:
        pass

El primer ejemplo trata directamente con un problema en la función .split . El segundo simplemente permite que el controlador de excepciones lo resuelva (e ignora el problema).

Argumentan en el libro para usar el manejo de excepciones en lugar de verificar primero. El argumento es que el código de excepción detectará todos los errores, donde la primera comprobación solo detectará las cosas que usted piensa (y se pierde los casos de la esquina). Primero me enseñaron a verificar, así que mi instinto inicial fue hacer eso, pero su idea es interesante. Nunca había pensado en utilizar el manejo de excepciones para tratar casos.

¿Cuál de los dos es generalmente considerado la mejor práctica?

    
pregunta jmq 11.03.2012 - 03:53
fuente

9 respuestas

63

En .NET, es una práctica común evitar el uso excesivo de Excepciones. Un argumento es el rendimiento: en .NET, lanzar una excepción es computacionalmente costoso.

Otra razón para evitar su uso excesivo es que puede ser muy difícil leer el código que se basa demasiado en ellos. La entrada de blog de Joel Spolsky hace un buen trabajo al describir el problema.

En el corazón del argumento se encuentra la siguiente cita:

  

El razonamiento es que considero que las excepciones no son mejores que las de "goto", consideradas dañinas desde la década de 1960, ya que crean un salto abrupto de un punto de código a otro. De hecho, son significativamente peores que los de Goto:

     

1. Son invisibles en el código fuente . Al observar un bloque de código, incluidas las funciones que pueden o no generar excepciones, no hay forma de ver qué excepciones se pueden lanzar y desde dónde. Esto significa que incluso una inspección cuidadosa del código no revela posibles errores.

     

2. Crean demasiados puntos de salida posibles para una función. Para escribir el código correcto, realmente tiene que pensar en cada ruta de código posible a través de su función. Cada vez que llama a una función que puede generar una excepción y no la captura en el momento, crea oportunidades para errores inesperados causados por funciones que terminaron abruptamente, dejando los datos en un estado incoherente u otras rutas de código que usted no realizó. pensar en.

Personalmente, lanzo excepciones cuando mi código no puede hacer lo que se le contrata. Tiendo a usar try / catch cuando estoy a punto de lidiar con algo fuera de los límites de mi proceso, por ejemplo, una llamada SOAP, una llamada a la base de datos, un archivo IO o una llamada al sistema. De lo contrario, intento codificar defensivamente. No es una regla difícil y rápida, pero es una práctica general.

Scott Hanselman también escribe sobre excepciones en .NET aquí . En este artículo él describe varias reglas de oro con respecto a las excepciones. ¿Mi favorito?

  

No debes lanzar excepciones para las cosas que suceden todo el tiempo. Entonces serían "ordinarios".

    
respondido por el Kyle Hodgson 11.03.2012 - 04:42
fuente
75

En Python en particular, generalmente se considera una mejor práctica para detectar la excepción. Tiende a llamarse Más fácil de pedir perdón que el permiso (EAFP), en comparación con Look Before You Leap (LBYL). Hay casos en los que LBYL te dará errores sutiles en algunos casos.

Sin embargo, tenga cuidado con bare except: enunciados , así como en general, excepto en los extractos. , ya que ambos también pueden enmascarar errores, algo como esto sería mejor:

for eachLine in open("../../data/sketch.txt"):
    try:
        role, lineSpoken = eachLine.split(":",1)
    except ValueError:
        pass
    else:
        print("role=%(role)s lineSpoken=%(lineSpoken)s" % locals())
    
respondido por el lvc 11.03.2012 - 07:06
fuente
25

Un enfoque pragmático

Debes estar a la defensiva pero hasta cierto punto. Deberías escribir manejo de excepciones pero a un punto. Voy a usar la programación web como ejemplo porque aquí es donde vivo.

  1. Suponga que todas las entradas del usuario son malas y escriba defensivamente solo en el punto de verificación del tipo de datos, verificaciones de patrones e inyección maliciosa. La programación defensiva debe ser algo que puede suceder muy a menudo y que no puedes controlar.
  2. Escriba el manejo de excepciones para los servicios en red que pueden fallar en algunas ocasiones y manejar adecuadamente para la retroalimentación del usuario. La programación de excepciones debe usarse para cosas en red que pueden fallar de vez en cuando, pero generalmente son sólidas Y usted necesita mantener su programa en funcionamiento.
  3. No se moleste en escribir defensivamente dentro de su aplicación después de que se hayan validado los datos de entrada. Es una pérdida de tiempo e infla tu aplicación. Deje que explote porque es algo muy raro que no vale la pena manipular o significa que debe observar los pasos 1 y 2 más detenidamente.
  4. Nunca escriba manejo de excepciones dentro de su código de núcleo que no depende de un dispositivo en red. Hacerlo es una mala programación y es costoso para el rendimiento. Por ejemplo, escribir un try-catch en el caso de una matriz fuera de límites en un bucle significa que no programó el bucle correctamente en primer lugar.
  5. Deje que todo sea manejado por el registro central de errores que detecta las excepciones en un lugar después de seguir los procedimientos anteriores. No puede detectar todos los casos de borde, ya que puede ser infinito, solo necesita escribir código que maneje la operación esperada. Es por eso que utiliza el manejo central de errores como último recurso.
  6. TDD es bueno porque de alguna manera es un intento de atraparte sin hincharse, lo que significa brindarte cierta seguridad de funcionamiento normal.
  7. Los puntos de bonificación es utilizar una herramienta de cobertura de código , por ejemplo, Estambul es una buena opción para el nodo, ya que muestra dónde no está realizando pruebas.
  8. La advertencia de todo esto es excepciones fáciles de usar . Por ejemplo, si se usa la sintaxis incorrectamente y se explica por qué, se generaría un lenguaje. Entonces, ¿deberían las utilidades bibliotecas de las que depende la mayor parte de su código?

Esto es por experiencia trabajando en grandes escenarios de equipo.

Una analogía

Imagínese si usara un traje espacial dentro de la ISS TODO el tiempo. Sería difícil ir al baño o comer, en absoluto. Sería súper voluminoso dentro del módulo espacial para moverse. Apestaría Escribir un montón de try-catches dentro de tu código es algo así. Tienes que tener un punto en el que dices, oye, aseguré que la ISS y mis astronautas están bien, así que no es práctico usar un traje espacial para cada escenario que pueda ocurrir.

    
respondido por el Jason Sebring 11.03.2012 - 06:22
fuente
14

El argumento principal del libro es que la versión de excepción del código es mejor porque detectará cualquier cosa que pueda haber pasado por alto si intentara escribir su propia comprobación de errores.

Creo que esta afirmación es verdadera solo en circunstancias muy específicas, donde no te importa si la salida es correcta.

No hay duda de que aumentar las excepciones es una práctica segura y segura. Debe hacerlo siempre que sienta que hay algo en el estado actual del programa con el que usted (como desarrollador) no puede o no quiere hacer frente.

Su ejemplo, sin embargo, se trata de atrapar excepciones. Si detecta una excepción, no se está protegiendo de los escenarios que podría haber pasado por alto. Está haciendo exactamente lo contrario: asume que no ha pasado por alto ningún escenario que podría haber causado este tipo de excepción y, por lo tanto, está seguro de que está bien atraparlo (y, por lo tanto, evitar que el programa se cierre). como cualquier excepción no capturada lo haría).

Usando el enfoque de excepción, si ves la excepción ValueError , saltas una línea. Usando el enfoque tradicional de no excepción, cuenta el número de valores devueltos de split , y si es menos de 2, salta una línea. ¿Debería sentirse más seguro con el enfoque de excepción, ya que puede haber olvidado otras situaciones de "error" en su verificación de error tradicional, y except ValueError las detectaría por usted?

Esto depende de la naturaleza de su programa.

Si está escribiendo, por ejemplo, un navegador web o un reproductor de video, un problema con las entradas no debería causar que se bloquee con una excepción no detectada. Es mucho mejor emitir algo de forma remota (incluso si, hablando estrictamente, incorrecto) que dejarlo.

Si está escribiendo una aplicación en la que la corrección es importante (como un software comercial o de ingeniería), este sería un enfoque terrible. Si olvidó algún escenario que genera ValueError , lo peor que puede hacer es ignorar silenciosamente este escenario desconocido y simplemente omitir la línea. Así es como los errores muy sutiles y costosos terminan en el software.

Podría pensar que la única forma en que puede ver ValueError en este código es si split devolvió solo un valor (en lugar de dos). Pero, ¿qué pasa si tu declaración print comienza más tarde con una expresión que genera ValueError bajo ciertas condiciones? Esto hará que omita algunas líneas no porque pierdan : , sino porque print falla en ellas. Este es un ejemplo de un error sutil al que me refería anteriormente: no notaría nada, simplemente perdería algunas líneas.

Mi recomendación es evitar la captura (¡pero no el aumento!) de excepciones en el código donde producir una salida incorrecta es peor que salir. La única vez que detecto una excepción en dicho código es cuando tengo una expresión realmente trivial, por lo que puedo razonar fácilmente qué puede causar cada uno de los posibles tipos de excepción.

En cuanto al impacto en el rendimiento del uso de excepciones, es trivial (en Python) a menos que se encuentren excepciones con frecuencia.

Si utiliza excepciones para manejar condiciones que ocurren de manera rutinaria, en algunos casos puede pagar un enorme costo de rendimiento. Por ejemplo, supongamos que ejecuta de forma remota algún comando. Puede verificar que el texto de su comando pase al menos la validación mínima (por ejemplo, sintaxis). O puede esperar a que se genere una excepción (lo que ocurre solo después de que el servidor remoto analice su comando y encuentre un problema). Obviamente, el primero es órdenes de magnitud más rápido. Otro ejemplo simple: puede verificar si un número es cero ~ 10 veces más rápido que intentar ejecutar la división y luego capturar la excepción ZeroDivisionError.

Estas consideraciones solo importan si con frecuencia envías cadenas de comando mal formadas a servidores remotos o si recibes argumentos de valor cero que usas para la división.

Nota: asumo que usarías except ValueError en lugar de solo except ; como otros lo señalaron, y como dice el libro en unas pocas páginas, nunca debes usar bare except .

Otra nota: el enfoque adecuado sin excepción es contar el número de valores devueltos por split , en lugar de buscar : . Este último es demasiado lento, ya que repite el trabajo realizado por split y puede casi duplicar el tiempo de ejecución.

    
respondido por el max 23.04.2013 - 02:54
fuente
6

Como regla general, si sabe que una declaración podría generar un resultado no válido, haga una prueba para eso y trátela. Use excepciones para cosas que no espera; Cosas que son "excepcionales". Aclara el código en un sentido contractual ("no debería ser nulo" como ejemplo).

    
respondido por el Ian 11.03.2012 - 08:58
fuente
2

Usa lo que siempre funciona bien en ...

  • su lenguaje de programación elegido en términos de legibilidad y eficiencia del código
  • su equipo y el conjunto de convenciones de códigos acordadas

Tanto el manejo de excepciones como la programación defensiva son formas diferentes de expresar el mismo propósito.

    
respondido por el Sri 11.03.2012 - 16:39
fuente
0

TBH, no importa si utiliza el try/except mechanic o el if . Por lo general, se ven tanto EAFP como LBYL en la mayoría de las líneas de base de Python, siendo EAFP un poco más común. A veces, EAFP es mucho más legible / idiomático, pero en este caso particular, creo que está bien de cualquier manera.

Sin embargo ...

Tendré cuidado al usar tu referencia actual. Un par de problemas evidentes con su código:

  1. El descriptor del archivo está filtrado. Las versiones modernas de CPython (un intérprete de Python específico ) lo cerrarán, ya que es un objeto anónimo que solo está dentro del alcance durante el ciclo (gc will Nuke después del bucle). Sin embargo, otros intérpretes no tienen esta garantía. Pueden filtrar el descriptor directamente. Casi siempre desea utilizar el idioma with al leer archivos en Python: hay muy pocas excepciones. Este no es uno de ellos.
  2. El manejo de excepciones de Pokémon está mal visto , ya que enmascara los errores (es decir, la simple declaración except que no detecta una excepción específica)
  3. Nit: No necesitas paréntesis para desempaquetar tuple. Solo se puede hacer role, lineSpoken = eachLine.split(":",1)

Ivc tiene una buena respuesta sobre esto y EAFP, pero también está filtrando el descriptor.

La versión LBYL no es necesariamente tan eficaz como la versión EAFP, por lo que decir que lanzar excepciones es "caro en términos de rendimiento" es categóricamente falso. Realmente depende del tipo de cadenas que estés procesando:

In [33]: def lbyl(lines):
    ...:     for line in lines:
    ...:         if line.find(":") != -1:
    ...:             # Nuke the parens, do tuple unpacking like an idiomatic Python dev.
    ...:             role, lineSpoken = line.split(":",1)
    ...:             # no print, since output is obnoxiously long with %timeit
    ...:

In [34]: def eafp(lines):
    ...:     for line in lines:
    ...:         try:
    ...:             # Nuke the parens, do tuple unpacking like an idiomatic Python dev.
    ...:             role, lineSpoken = eachLine.split(":",1)
    ...:             # no print, since output is obnoxiously long with %timeit
    ...:         except:
    ...:             pass
    ...:

In [35]: lines = ["abc:def", "onetwothree", "xyz:hij"]

In [36]: %timeit lbyl(lines)
100000 loops, best of 3: 1.96 µs per loop

In [37]: %timeit eafp(lines)
100000 loops, best of 3: 4.02 µs per loop

In [38]: lines = ["a"*100000 + ":" + "b", "onetwothree", "abconetwothree"*100]

In [39]: %timeit lbyl(lines)
10000 loops, best of 3: 119 µs per loop

In [40]: %timeit eafp(lines)
100000 loops, best of 3: 4.2 µs per loop
    
respondido por el Matt Messersmith 29.10.2018 - 22:48
fuente
-4

Básicamente, manejo de excepciones se supone que es más apropiado para los idiomas OOP.

El segundo punto es el rendimiento, porque no tienes que ejecutar eachLine.find para cada línea.

    
respondido por el Elalfer 11.03.2012 - 04:29
fuente
-6

Creo que la programación defensiva perjudica el rendimiento. También debe capturar solo las excepciones que va a manejar, deje que el tiempo de ejecución trate con la excepción que no sabe cómo manejar.

    
respondido por el Manoj 11.03.2012 - 05:37
fuente

Lea otras preguntas en las etiquetas