Chapter 9
Manejo de errores
NAESER’S LAW: You can make it foolproof, but you can’t make it damnfoolproof.
9.1 Introducción al manejo de errores
Raramente un programa funciona como uno espera.
Tradicionalmente se seguia una de estas dos estrategias a la hora de lidiar con los posibles errores en la ejecución de un programa. Basícamente o se ignoraba el problema o se verificaba cada condición donde pudiera haber un error y luego se escribia código en consecuencia. La primera aproximacion,
muy popular por cierto, no es aconsejable si queremos que el programa sea usado por alguien mas que nosotros. La segunda opción, también conocida como LBYL (Look Before You Leap), consume mucho tiempo y puede dejar el código ilegible. Veamos un ejemplo de cada estrategia:
El siguiente programa lee un archivo (miarchiv.csv ) separado por tabs y busca un número que se encuentra en la primera columna de la primera línea.
Ese valor es multiplicado por 0.2 y ese resultado es escrito a otro archivo (otro.csv ).
Nuestra primera versión no verifica ningún tipo de errores y se limita a hacer su cometido:
Listing 9.1:Abre un archivo, busca un valor, lo multiplica por otro y lo escribe en un archivo
1 iname=raw_input("Enter input filename: ")
2 oname=raw_input("Enter output filename: ")
3 fh=open(iname)
4 line=fh.readline()
5 fh.close()
6 MyValue=line[:line.index("\t")]
7 fw=open(oname,"w")
8 fw.write(str(int(MyValue)*.2))
9 fw.close()
Este programa puede cumplir con su cometido, siempre y cuando no ocurra ningún imprevisto. ¿Que quiere decir ”un imprevisto” en este contexto? Por ejemplo la primera línea es proponsa a errores, ya que puede esta intentando abrir un archivo que no existe. En ese caso, al ejecutar el programa este
se parar ́ inmediatamente luego de ejecutarse la primera línea y nos encontraremos con algo asi:
Traceback (most recent call last):
File "/home/sbassi/bioinfo/error2.py", line 1, in <module>
fh=open("noexistis.csv")
IOError: [Errno 2] No such file or directory: ’noexistis.csv’
Esto es un problema porque el programa se detiene y ademas porque es poco profesional mostrarle al usuario un error de sistema de manera directa.
Este programa no puede fallar solo en la primera línea, sino que hay varios puntos posibles de error. Podria no haber ningún tab dentro del archivo, podría haber letras en lugar de números y podriamos no contar con permisos de escritura en el directorio donde pretendemos escribir el archivo de salida.
Veamos que ocurriria si el archivo existiese pero no hubiese un ’tab’ adentro:
Traceback (most recent call last):
File "/home/sbassi/bioinfo/error2.py", line 4, in <module>
valor=line[:line.index("\t")]
ValueError: substring not found
El resultado es similar al anterior. Se produce la detención del programa y el interprete nos muestra otro mensaje de error. Asi podriamos seguir con todas las porciones del código propensas a fallar.
Veamos la estrategia de verificar cada condición probable de generar un error para evitar que este se produzca (LBYL):
Listing 9.2:Idem anterior, edición LBYL
1 import os
2 while True:
3 iname=raw_input("Enter input filename: ")
4 oname=raw_input("Enter output filename: ")
5 if os.path.exists(iname):
6 fh=open(iname)
7 line=fh.readline()
8 fh.close()
9 if "\t" in line:
10 MyValue=line[:line.index("\t")]
11 if os.access("/home/sb/"+oname,os.W_OK)==0:
12 fw=open("/home/sb/"+oname,"w")
13 if MyValue.isdigit():
14 fw.write(str(int(valor)*.2))
15 fw.close()
16 break
17 else:
18 print "It can’t be converted to int"
19 else:
20 print "Output file is not writable"
21 else:
23 print "There is no TAB. Check the input file"
24 else:
25 print "The file doesn't exist"
Este programa contempla casi todas las posibles fuentes de error. Ahora si el archivo que ingresa el usuario no existe, el programa no tendrá una salida anormal, sino que habrá un mensaje de error, diseñado por el programador, y se le permitirá al usuario que vuelva a ingresar el nombre del archivo de
entrada.
La desventaja de esta opción es que el código es mas dificil de leer y de mantener, ya que se mezcla la verificación de los errores, con su tratamiento y con el objetivo principal del programa. Es por eso que los nuevos lenguajes de programación han incluido un sistema especifico para el manejo de condiciones
exepcionales. A diferencia de LBYL, esta estrategia se la conoce como EAFP (It’s Easier to Ask Forgiveness than Permission). En el caso de Python se usan los statements try, except, else y finally.
9.1.1 Try y except
try delimita el bloque de código que queremos ejecutar, mientras que except delimita el código que deberá ejecutarse en caso que el código bajo try retorne una excepción. Veamos un esquema general:
try:
code block 1
# ...some error prone code...
except:
code block 2
# ...do something with the error...
else:
code block 3
# ...to do when there is no error...
finally:
code block 4
#...some clean up code...
Este código primero intentará ejecutar el código del block 1. Si este código es ejecutado sin problemas, el flujo de ejecución sigue por el bloque de código 3 y finalmente por el block 4. En el caso de que el código del block 1 produzca un error (o levante una excepción según la jerga), se ejecutará el bloque de
código 2 y luego el code block 4. La idea detras de este mecanismo es poner el bloque de código que sospechamos que puede generar un error, dentro de la clausula try. El código que tiene que actuar en caso de error, irá bajo el bloque except. Opcionalmente se puede agregar el statement else, que se ejecutará solo si el código bajo try (code block 1) se ejecutó con éxito. Es interesante notar que el código bajo else podría estar bajo try ya que tendria el mismo efecto (se ejecutará solo en caso de no haber errores). La diferencia en realidad es conceptual, el bloque bajo try debería contener solo el código que uno sospecha que puede fallar, mientras que tendriamos que dejar para el bloque bajo else a las instrucciones que deben ejecutarse cuando las instrucciones bajo try se ejecutaron correctamente. Note que el código bajo finally se ejecutará de todos modos, haya o no haya algún error.
Veamos un ejemplo sobresimplificado:
try:
print 0/0
except:
print "Houston, we have a problem..."
El resultado, indefectiblemente, es:
Houston, we have a problem...
Lo primero que notamos es que no se incluyó else ni finally, ya que son statements optativos. En este caso la sentencia print 0/0 ”lanza” una excepción. Esta excepción es ”atajada”1 por el código que le sigue a except. De esta manera podemos probar un bloque de código de manera segura, ya que cualquier error que ocurra redireccionará el flujo del programa de manera previsible.
Veamos como se aplica el manejo de excepciones al código 9.2. No se preocupe si no entiende todo en una primera leida. La idea de mostrar este programa es ver las diferencias que tiene con las versiones en donde no se aplica el manejo de excepciones (listado 9.2 y listado 9.1).
Listing 9.3: Idem al código 9.2 pero con manejo de excepciones
1 import os
2 while True:
3 try:
4 iname=raw_input("Enter input filename: ")
5 oname=raw_input("Enter output filename: ")
6 fh=open(iname)
7 linea=fh.readline()
8 fh.close()
9 valor=linea[:linea.index("\t")]
10 fw=open("/home/sb/"+oname,"w")
11 fw.write(str(int(valor)*.2))
12 fw.close()
13 except IOError, (errno,errtext):
14 if errno==13:
15 print "Can’t write to outfile."
16 elif errno==2:
17 print "File not exist"
18 except ValueError, strerror:
19 if "substring not found" in strerror.message:
20 print "There is no tab"
21 elif "invalid literal for int" in strerror.message:
22 print "The value can’t be converted to int"
23 else:
24 print "Thank you!. Everything went OK."
25 break
1 Esta terminología proviene de Java y C++ donde se usa try and catch to handle exceptions
A simple vista se nota que este código es mas simple que la versión anterior (listado 9.2). Al menos la lógica del código está separada del manejo de los errores. Se puede observar que desde la línea 4 a la 13, se repite el mismo código que en el listado original (listado 9.1). Es a partir de la línea 14 donde
comienza el manejo de las excepciones. Según el tipo de excepción, es el código que se ejecuta a continuación. Mas adelante veremos como diferenciar los distintos tipos de excepciones.
Hay que tener en cuenta que el listado 9.3 es solo un ejemplo introductorio de como se aplica el manejo de excepciones al listado 9.1, y no la guía definitiva de como manejar excepciones.
Por ejemplo si falla la conversión a entero de la línea 12, se levantará una excepción del tipo ValueError, se imprimirá un mensaje de acuerdo a la línea 23 y luego el programa retomará la línea 3 (debido a que está bajo un while TRUE), sin haber liberado el recurso utilizado (el filehandle fw ). Una manera
de solucionar esto sería copiar el statement donde se cierra el recurso (línea 13) al bloque donde se maneja la excepción correspondiente (luego de la línea Asi nos aseguramos de liberar el recurso:
Listing 9.4: Idem al código 9.2 pero con manejo de excepciones
1 import os
2 while True:
3 try:
4 iname=raw_input("Enter input filename: ")
5 oname=raw_input("Enter output filename: ")
6 fh=open(iname)
7 line=fh.readline()
8 fh.close()
9 valor=line[:line.index("\t")]
10 fw=open("/home/sb/"+oname,"w")
11 fw.write(str(int(valor)*.2))
12 fw.close()
13 except IOError, (errno,errtext):
14 if errno==13:
15 print "Can’t write to outfile."
16 elif errno==2:
17 print "File not exist"
18 except ValueError, strerror:
19 if "substring not found" in strerror.message:
20 print "There is no tab"
21 elif "invalid literal for int" in strerror.message:
22 print "The value can’t be converted to int"
23 fw.close()
24 else:
25 print "Thank you!. Everything went OK."
26 break
Si bien este código cumple con su cometido, no es buena idea repetir en dos lugares el mismo statement (fw.close()). Si tenemos un código que debe ejecutase si o si, podemos incluirlo en finally, que es donde va el código que se ejecuta de manera indendiente a lo que ocurra con el código en try. El problema de incluir fw.close() bajo finally es que puede haber una excepción o antes de abrir fh (por ejemplo en la conversión a entero, línea 10 del listado 9.4) y vamos a tratar de cerrar un filehandle que nunca fue abierto, lo que causará por si mismo otro error. Este error a su vez puede ser predicho, por lo que podemos recurrir al mecanismo de excepciones, e incluir una clausula try/except dentro de finally:
Listing 9.5: Idem al código 9.4 pero con manejo de excepciones
1 import os
2 while True:
3 try:
4 iname=raw_input("Enter input filename: ")
5 oname=raw_input("Enter output filename: ")
6 fh=open(iname)
7 line=fh.readline()
8 fh.close()
9 MyValue=line[:line.index("\t")]
10 fw=open("/home/sb/"+oname,"w")
11 fw.write(str(int(MyValue)*.2))
12 except IOError, (errno,errtext):
13 if errno==13:
14 print "Can’t write to outfile."
15 elif errno==2:
16 print "File not exist"
17 except ValueError, strerror:
18 if "substring not found" in strerror.message:
19 print "There is no tab"
20 elif "invalid literal for int" in strerror.message:
21 print "The value can’t be converted to int"
22 else:
23 print "Thank you!. Everything went OK."
24 break
25 finally:
26 try:
27 fw.close()
28 expect:
29 pass
30 else:
31 print "All resources freed"
El código del listado 9.5 fue hecho para ilustrar un ejemplo de uso de try anidado, no necesariamente porque sea la mejor forma de resolver el problema. Podriamos ahorrarnos de tener el problema de causar la excepción mientras tenemos el filehandle abierto con solo hacer la conversion a entero antes que
abrir el archivo. Otra reforma a considerar es remover statements del try, ya que es conveniente incluir solo los statements que suponemos que van a arrojar excepciones.
Listing 9.6: Idem al código 9.4 pero con manejo de excepciones
1 import os
2 while True:
3 iname=raw_input("Enter input filename: ")
4 oname=raw_input("Enter output filename: ")
5 try:
6 fh=open(iname)
7 line=fh.readline()
8 MyValue=str(int(line[:line.index("\t")])*.2)
9 fw=open("/home/sbassi/"+oname,"w")
10 fw.write(MyValue)
98 Python for Bioinformatics
11 except IOError, (errno,errtext):
12 if errno==13:
13 print "Can’t write to outfile."
14 elif errno==2:
15 print "File not exist"
16 except ValueError, strerror:
17 if "substring not found" in strerror.message:
18 print "There is no tab"
19 elif "invalid literal for int" in strerror.message:
20 print "The value can’t be converted to int"
21 else:
22 fw.close()
23 fh.close()
24 break
Ya vimos a grandes razgos el funcionamiento de try/except, ahora podemos profundizar un poco sobre los diversos tipos de excepciones:
9.1.2 Tipos de excepciones
No todas las excepciones son creadas iguales. No es lo mismo referirse a una variable inexistente que mezclar tipos de datos incompatibles. La primera excepción es del tipo NameError mientras que la segunda es del tipo Type- Error. Una lista completa de excepciones la podría encontrar en el apendice
XXXXXXXX. A continuación resumimos las que aparecen con mas frencuencia:
TABLE 9.1: Tipos de errores.
Nombre del error | Descripción |
KeyError | Se referenció una clave que no existe en un diccionario |
IndexError | Se referenció a un indice que no existe |
ValueError | Se buscó un valor inexistente en una lista |
NameError | Se referenció una variable que no existe |
TypeError | Se mezclaron datos incompatibles |
AttributeError | Se llamó un método que no existe |
IOError | Error asociado a la lectura y escritura en el disco |
ImportError | Se llamó a un módulo que no está disponible |
KeyboardInterrupt | Se recibio la orden de interrumpir desde el teclado (generalmente la secuencia control-C) |
Como responder ante distintas excepciones:
No es obligatorio manejar las excepciones en función del tipo de error. Se puede manejar genericamente un error usando except sin nigún parametro:
d={"A":"Adenine","C":"Cisteine","T":"Timine","G":"Guanine"}
try:
print d[raw_input("Enter letter: ")]
except:
print "No such nucleotide"
Que podamos responder de manera generica a todos los errores, no significa que sea deseable. El problema de esta estrategia es que dificulta debugear nuestro código, ya que un error no previsto puede pasar desapercivido. Este código devolverá ”No such nucleotide” ante cualquier tipo error. Por ejemplo
si introducimos una señal de EOF (end of file, CONTROL-D), obtendremos ”No such nucleotide”. Poder clasificar las excepciones nos permite poder diferenciar distintos tipos de eventos anormales, y reaccionar a consecuencia.
Por ejemplo para diferenciar el ingreso de un EOF de una clave de diccionario inexistente, podemos hacer:
d={"A":"Adenine","C":"Cisteine","T":"Timine","G":"Guanine"}
try:
print d[raw_input("Enter letter: ")]
except EOFError:
print "Good bye!"
except KeyError:
print "No such nucleotide"
De esta manera obtendremos un ”No such nucleotide” cuando el usuario ingresa clave que no existe en nuestro diccionario y un ”Good bye!” cuando ingresamos un EOF.
Ahora si estamos en condiciones de ver con mas detalle el código del listado 9.3. Empezamos con la línea 13, donde se caza una excepcion del tipo IOError: except IOError, (errno,errtext):. Ademas de cazar la excepcion, también guardamos dos valores: errno y errtext. El primer valor corresponde a un código relacionado con que tipo de IOError fue el que se produjo. A diferencia de otros errores, el error IO engloba distintos varios errores relacionados.
Por eso es que existe un código de error, el que es usado (en las lineas 14 y 16) para poder discriminar exactamente que error se trata. El error tipo ValueError también incuye mas de un tipo de error. Por un lado es llamado cuando se busca en una lista un valor que no existe (puede ocurrir en la línea 9), y
por otro, cuando se quiere hacer conversiones de tipos de datos incompatibles (línea 10). La manera de distinguir entre estas condiciones es por el mensaje de error, como se muestra en las líneas 20 y 22.
9.1.3 Provocando excepciones
Las excepciones pueden activarse de manera manual, sin esperar que ocurran naturalmente. El statement usado para levantar una excepción es raise. A esta altura se estarán preguntando para que uno va a querer provocar una excepción. Una excepción levantada de manera apropiada puede ser de mas
ayuda al programador (o al usuario si no es el mismo programador) que una excepción que se dispara de manera no controlada.
Este tipo de concepto se entiende mejor con un ejemplo. A continuación creamos una función que calcula el promedio de una secuencia de números:
def avg(numbers):
return sum(numbers)/len(numbers)
Una función de este tipo tendría problemas con una lista vacia:
>>> avg([])
Traceback (most recent call last):
File "<pyshell#4>", line 1, in <module>
avg([])
File "<pyshell#3>", line 2, in avg
return sum(numbers)/len(numbers)
ZeroDivisionError: integer division or modulo by zero
El problema de este mensaje de error, es que no nos dice directamente que fue ocasionado por una lista vacia, sino que dice que fue provocado por haber intentado dividir por cero. Conociendo como actua la función, uno podría deducir que una lista vacia desencadena este error. Pero seria mas interesante
que el error nos aclare justamente eso, sin tener que conocer la estructura interna de la función. Para eso es que podemos provocar un error a voluntad:
def avg(numbers):
if not numbers:
raise ValueError("Please enter at least one element")
return sum(numbers)/len(numbers)
En este caso, el tipo de error está mas acorde al problema real:
>>> avg([])
Traceback (most recent call last):
File "<pyshell#8>", line 1, in <module>
avg2([])
File "<pyshell#6>", line 3, in avg2
raise ValueError("Please enter at least one element")
ValueError: Please enter at least one element
Podriamos haber evitado el error si imprimiamos un string sin levantar el error, pero esto iria contra de los principios pythonicos. Especificamente contra ”Los errores no deben pasar inavertidos”. En la práctica esto puede traer problemas, ya que si una función devuelve un valor no esperado, los
efectos pueden ser impredecibles. Al levantar la excepción, nos aseguramos que el error no pase inadvertido.
En algunos textos o código antiguo encontrará una sintaxis del estilo raise ”This is an error”. Este tipo de excepciones (llamadas excepciones de cadena), dejará de funcionar desde Python 2.6 y posteriores. Tampoco es recomendable usar raise ValueError, ’A message’, en lugar de raise ValueError(’A message’). Esta última forma es preferible porque en cadenas largas el parentesis indica que continua en la siguiente línea.
9.2 Creando nuestras propias excepciones
Una ventaja del sistema de excepciones es que no tenemos que limitarnos a las que vienen con Python. Podemos definir nuevas excepciones en función de nuestras propias necesidades. Para poder crear una excepción, hay que manejar la OOP, tema que aun no ha sido tratado. Por esa razón, si está leyendo este libro desde el principio, mi recomendación es que saltie el resto de este capítulo y prosiga directamente a OOP. Luego de leer OOP, retome está sección si aun le interesa saber como crear sus propias excepciones.
Todas las excepciones derivan de la clase Exception
9.3 Autoevaluación
¿ Que significa LBYL y EAFP?
¿ Cuando se usa finally y cuando se usa else?
Reescriba el listado 9.3 usando la clausula else, sin modificar el resultado final.
Autor: Sebastián Bassi
Si encontrás un error en este tutorial, por favor reportalo a sbassi ARROBA clubdelarazon.org