¿Comprobar primero frente a manejo de excepciones?

88

Estoy trabajando en el libro "Head First Python" (es mi idioma para aprender este año) y llegué a una sección en la que discuten sobre dos técnicas de código:
Verificar primero frente a manejo de excepciones.

Aquí hay una muestra del código 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 .splitfunción. El segundo simplemente deja que el manejador de excepciones lo solucione (e ignora el problema).

Argumentan en el libro que usan 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 comprobación primero solo detectará las cosas en las que piensa (y omite los casos de 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 usar el manejo de excepciones para tratar casos.

¿Cuál de las dos es generalmente considerada la mejor práctica?

jmq
fuente
12
Esa sección del libro no es inteligente. Si está en un bucle y está lanzando excepciones una y otra vez, es muy costoso. Traté de esbozar algunos buenos puntos de cuándo hacer esto.
Jason Sebring
99
Simplemente no caigas en la trampa de "verificación de archivo existe". ¡El archivo existe! = Tiene acceso al archivo, o que existirá en los 10 ms necesarios para acceder a mi llamada abierta de archivo, etc. blogs.msdn.com/b/jaredpar/archive/2009/04/27/…
Billy ONeal
11
Se piensa que las excepciones son diferentes en Python que en otros lenguajes. Por ejemplo, la forma de iterar a través de una colección es llamar a .next () hasta que arroje una excepción.
WuHoUnited
44
@ emeraldcode.com Eso no es del todo cierto sobre Python. No conozco los detalles, pero el lenguaje se ha construido alrededor de ese paradigma, por lo que lanzar excepciones no es tan costoso como en otros idiomas.
Izkata
Dicho esto, para este ejemplo, usaría una declaración de guardia: if -1 == eachLine.find(":"): continueentonces el resto del ciclo tampoco estaría sangrado.
Izkata

Respuestas:

68

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 depende demasiado de ellos. La entrada de blog de Joel Spolsky hace un buen trabajo al describir el problema.

En el corazón del argumento está la siguiente cita:

El razonamiento es que considero que las excepciones no son mejores que "goto's", consideradas dañinas desde la década de 1960, ya que crean un salto brusco 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 arrojar 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 detecta en el acto, crea oportunidades para errores sorpresa causados ​​por funciones que terminaron abruptamente, dejando datos en un estado inconsistente u otras rutas de código que usted no pensar en.

Personalmente, lanzo excepciones cuando mi código no puede hacer lo que está contratado. Tiendo a usar try / catch cuando estoy a punto de lidiar con algo fuera del límite 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 describe varias reglas generales con respecto a las excepciones. ¿Mi favorito?

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

Kyle Hodgson
fuente
55
Aquí hay otro punto: si el registro de excepciones está habilitado en toda la aplicación, es mejor usar la excepción solo para condiciones excepcionales, no para las ordinarias. De lo contrario, el registro se desordenará y se ocultarán los motivos reales que causan errores.
rwong
2
Buena respuesta. Tenga en cuenta que las excepciones tienen un alto rendimiento en la mayoría de las plataformas. Sin embargo, como habrás notado con mis comentarios sobre otras respuestas, el rendimiento no es una consideración en el caso de decidir una regla general sobre cómo codificar algo.
mattnz
1
La cita de Scott Hanselman describe mejor la actitud .Net hacia las excepciones que el "uso excesivo". El rendimiento se menciona con frecuencia, pero el argumento real es el inverso de por qué DEBE usar excepciones: hace que el código sea más difícil de entender y tratar cuando una condición ordinaria resulta en una excepción. En cuanto a Joel, el punto 1 es realmente positivo (invisible significa que el código muestra lo que hace, no lo que no hace), y el punto 2 es irrelevante (ya estás en un estado inconsistente, o no debería haber una excepción) . Aún así, +1 para "no puede hacer lo que se le ha pedido que haga".
jmoreno
55
Si bien esta respuesta está bien para .Net, no es muy pitónica , así que dado que esta es una pregunta de Python, no veo por qué la respuesta de Ivc no se ha votado más.
Mark Booth
2
@IanGoldby: no. El manejo de excepciones se describe mejor como recuperación de excepciones. Si no puede recuperarse de una excepción, entonces probablemente no debería tener ningún código de manejo de excepciones. Si el método A llama al método B que llama a C y C arroja, lo más probable es que CUALQUIERA O B se recuperen, no ambos. La decisión "si no puedo hacer X haré Y" debe evitarse si Y requiere que alguien más termine la tarea. Si no puede finalizar la tarea, todo lo que queda es la limpieza y el registro. La limpieza en .net debe ser automática, el registro debe estar centralizado.
jmoreno
78

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

Sin embargo, tenga cuidado con las except:declaraciones simples , así como las declaraciones generales, excepto las declaraciones, ya que ambas 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())
lvc
fuente
8
Como programador de .NET, me avergüenzo de esto. Pero, de nuevo, ustedes hacen todo raro. :)
Phil
Esto es excepcionalmente frustrante (juego de palabras no intencionado) cuando las API no son consistentes acerca de qué excepciones se lanzan bajo qué circunstancias, o cuando se lanzan múltiples tipos diferentes de fallas bajo el mismo tipo de excepción.
Jack
Entonces terminas usando el mismo mecanismo para errores inesperados y valores esperados de tipo de retorno. Eso es casi tan bueno como usar 0 como un número, un falso bool Y un puntero inválido que cerrará su proceso con un código de salida de 128 + SIGSEGV, porque qué conveniente, no necesita cosas diferentes ahora. Como el spork! O zapatos con dedos de los pies ...
yeoman
2
@yeoman cuándo lanzar una excepción es una pregunta diferente, esta se trata de usar try/ en exceptlugar de configurar un condicional para "es probable que la siguiente arroje una excepción", y la práctica de Python es definitivamente preferir la primera. No hace daño que ese enfoque sea (probablemente) más eficiente aquí, ya que, en el caso de que la división tenga éxito, solo camina la cadena una vez. En cuanto a si splitdebería lanzar una excepción aquí, diría que definitivamente debería hacerlo: una regla común es que debe lanzar cuando no puede hacer lo que dice su nombre y no puede dividirse en un delimitador faltante.
lvc
No lo encuentro malo, lento o terrible, especialmente porque solo se detecta una excepción específica. Resp. En realidad me gusta Python. Es curioso cómo a veces no muestra ningún gusto, ya que dicho C usa el número cero, el Spork y los zapatos favoritos de todos los tiempos de Randall Munroe con los dedos de los pies :) Además, cuando estoy en Python y un API dice que esto es la forma de hacerlo, lo haré :) Comprobar las condiciones de antemano, por supuesto, nunca es una buena idea debido a la concurrencia, las rutinas o uno de los que se agregan más adelante ...
yeoman
27

Un enfoque pragmático

Deberías estar a la defensiva pero hasta cierto punto. Debería escribir manejo de excepciones pero hasta cierto 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 a la defensiva solo hasta el punto de verificación del tipo de datos, verificación 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 servicios en red que pueden fallar a veces y maneje con gracia los comentarios de los usuarios. La programación de excepción debe usarse para cosas en red que pueden fallar de vez en cuando, pero generalmente son sólidas Y usted necesita mantener su programa funcionando.
  3. No se moleste en escribir a la defensiva dentro de su aplicación después de validar los datos de entrada. Es una pérdida de tiempo e hincha tu aplicación. Deje que explote porque es algo muy raro que no vale la pena manejar o significa que debe mirar los pasos 1 y 2 con más cuidado.
  4. Nunca escriba manejo de excepciones dentro de su código central que no dependa de un dispositivo en red. Hacerlo es una mala programación y es costoso para el rendimiento. Por ejemplo, escribir un try-catch en caso de una matriz fuera de los límites en un bucle significa que no programó el bucle correctamente en primer lugar.
  5. Deje que todo se maneje mediante un registro central de errores que capture excepciones en un solo lugar después de seguir los procedimientos anteriores. No puede atrapar todos los casos extremos 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, en cierto modo, es una forma de atraparlo sin hinchazón, lo que le brinda cierta seguridad de funcionamiento normal.
  7. Los puntos de bonificación son usar una herramienta de cobertura de código, por ejemplo, Estambul es una buena opción para el nodo, ya que esto le muestra dónde no está probando.
  8. La advertencia de todo esto son las excepciones amigables para los desarrolladores . Por ejemplo, un lenguaje se generaría si usara la sintaxis de forma incorrecta y explicara por qué. Entonces, ¿deberían sus bibliotecas de utilidad de las que depende la mayor parte de su código?

Esto se debe a la experiencia de trabajar en escenarios de equipos grandes.

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 moverse. Sería una mierda. Escribir un montón de intentos de captura dentro de su código es algo así. Debes tener un punto en el que digas, hey, aseguré la EEI y mis astronautas están bien, así que no es práctico usar un traje espacial para cada escenario que pueda suceder.

Jason Sebring
fuente
44
El problema con el Punto 3 es que asume que el programa y los programadores que trabajan en él son perfectos. No lo son, por lo que es el mejor programa defensivo teniendo esto en cuenta. Cantidades apropiadas en la coyuntura clave pueden hacer que el software sea mucho más confiable que la mentalidad "Si se comprueban las entradas, todo perfecto".
mattnz
para eso están las pruebas.
Jason Sebring
3
La prueba no es una trampa para todos. Todavía tengo que ver un conjunto de pruebas que tenga un 100% de código y cobertura "ambiental".
Marjan Venema
1
@emeraldcode: ¿Quieres un trabajo conmigo? Me encantaría tener a alguien en el equipo que siempre, con excepción, pruebe cada permutación de cada caso marginal que el software ejecutará. Debe ser agradable saber con certeza absoluta que su código está perfectamente probado.
mattnz
1
De acuerdo. Hay escenarios en los que tanto la programación defensiva como el manejo de excepciones funcionan bien y mal, y como programadores debemos aprender a reconocerlos y elegir la técnica que mejor se adapte. Me gusta el punto 3 porque creo que debemos suponer, en cierto nivel del código, que se deben cumplir algunas condiciones contextuales. Estas condiciones se satisfacen mediante la codificación defensiva en la capa externa de código, y creo que el manejo de excepciones es adecuado cuando estos supuestos se rompen en la capa interna.
yaobin
15

El argumento principal del libro es que la versión de excepción del código es mejor porque detectará todo lo que podría haber pasado por alto si intenta escribir su propia comprobación de errores.

Creo que esta afirmación es cierta solo en circunstancias muy específicas, donde no le importa si el resultado es correcto.

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

Su ejemplo, sin embargo, se trata de detectar excepciones. Si detecta una excepción, no se protege de escenarios que podría haber pasado por alto. Estás haciendo exactamente lo contrario: asumes que no has pasado por alto ningún escenario que podría haber causado este tipo de excepción y, por lo tanto, estás seguro de que está bien atraparlo (y así evitar que haga que el programa salga, como lo haría cualquier excepción no descubierta).

Usando el enfoque de excepción, si ve una ValueErrorexcepción, omite una línea. Usando el enfoque tradicional sin excepción, cuenta el número de valores devueltos splity, si es menor que 2, omite una línea. ¿Debería sentirse más seguro con el enfoque de excepción, ya que puede haber olvidado algunas otras situaciones de "error" en su verificación de error tradicional y las except ValueErrordetectarí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 generar algo remotamente sensible (incluso si, estrictamente hablando, incorrecto) que renunciar.

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

Puede pensar que la única forma en que puede ver ValueErroren este código es si splitdevuelve un solo valor (en lugar de dos). Pero, ¿qué printpasa si su declaración luego comienza a usar una expresión que surge ValueErrorbajo ciertas condiciones? Esto hará que omita algunas líneas no porque se pierdan :, sino porque printfallan en ellas. Este es un ejemplo de un error sutil al que me refería anteriormente: no notarías nada, solo perderías algunas líneas.

Mi recomendación es evitar capturar (¡pero no aumentar!) Excepciones en el código donde producir resultados incorrectos es peor que salir. La única vez que detectaría una excepción en dicho código es cuando tengo una expresión verdaderamente 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 usa excepciones para manejar condiciones que ocurren de manera rutinaria, en algunos casos puede pagar un costo de rendimiento enorme. Por ejemplo, suponga que ejecuta remotamente 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 analiza su comando y encuentra un problema con él). 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 detectar la excepción ZeroDivisionError.

Estas consideraciones solo importan si con frecuencia envía cadenas de comandos con formato incorrecto a servidores remotos o recibe argumentos de valor cero que utiliza para la división.

Nota: Asumo que usaría except ValueErroren lugar de los justos except; como otros señalaron, y como el libro mismo dice en unas pocas páginas, nunca debes usar desnudo 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 splity puede casi duplicar el tiempo de ejecución.

max
fuente
6

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

Ian
fuente
2

Usa lo que funciona bien en ...

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

Tanto el manejo de excepciones como la programación defensiva son formas diferentes de expresar la misma intención.

Sri
fuente
0

TBH, no importa si usa la try/exceptmecánica o una ifverificación de estado de cuenta. Comúnmente 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ía cuidado al usar su referencia actual. Un par de problemas evidentes con su código:

  1. El descriptor de archivo se filtró. Las versiones modernas de CPython (un intérprete de Python específico ) en realidad lo cerrarán, ya que es un objeto anónimo que solo está dentro del alcance durante el ciclo (gc lo bombardeará después del ciclo). Sin embargo, otros intérpretes no tienen esta garantía. Pueden filtrar el descriptor directamente. Casi siempre quieres usar el withidioma 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 oculta errores (es decir, una exceptdeclaración simple que no detecta una excepción específica)
  3. Nit: No necesitas parens para desempacar tuplas. Solo puede hacerrole, 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 "costoso 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
Matt Messersmith
fuente
-4

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

El segundo punto es el rendimiento, porque no tiene que ejecutar eachLine.findpara cada línea.

Elalfer
fuente
77
-1: El rendimiento es una razón extremadamente pobre para las reglas generales.
mattnz
3
No, las excepciones no tienen ninguna relación con OOP.
Pubby
-6

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

Manoj
fuente
77
Sin embargo, anotehr -1 por preocuparse por el rendimiento sobre la legibilidad, mantenimiento bla bla bla. El rendimiento no es una razón.
mattnz
¿Puedo saber por qué estás distribuyendo -1s sin explicar? La programación defensiva significa más líneas de código, lo que significa un peor rendimiento. ¿Alguien quiere explicar antes de bajar el puntaje?
Manoj
3
@Manoj: a menos que haya medido con un generador de perfiles y haya encontrado que un bloque de código es inaceptablemente lento, codifique la legibilidad y el mantenimiento mucho antes del rendimiento.
Daenyth
Lo que @Manoj dijo con la adición de que menos código universalmente significa menos para trabajar al depurar y mantener. El éxito en el tiempo del desarrollador por algo menos que el código perfecto es extremadamente alto. Supongo (como yo) que no escribes el código perfecto, perdóname si me equivoco.
mattnz
2
Gracias por el enlace. Es interesante leer que tengo que estar de acuerdo con él, hasta cierto punto ... Trabajando en un sistema crítico para la vida, como hago "El sistema imprimió el rastro de la pila, por lo que sabemos exactamente por qué esas 300 personas murieron innecesariamente. .... "realmente no va a caer demasiado bien en el estrado de los testigos. Supongo que es una de esas cosas donde cada situación tiene una respuesta apropiada diferente.
mattnz