Tratamiento de errores. Sentencia try-except

Hemos visto que cuando ocurre algún error en el código, Python detiene la ejecución y devuelve una excepción, que no es más que una señal que ha occurrido un funcionamiento no esperado o error en el programa, indicándonos aproximadamente qué fue lo que ocurrió.

Supongamos que tenemos un pequeño programa que por algún motivo realiza una división por cero.

a, b = 20, 0

resultado = a/b

print(resultado)

Al ejecutarlo tendremos el siguiente mensaje:

japp@vega:~$ python3 error.py
Traceback (most recent call last):
  File "test/errors.py", line 11, in <module>
    resultado = a/b
ZeroDivisionError: integer division or modulo by zero

En este ejemplo, el origen del error es fácil de identificar y el intérprete nos indica la línea en que ocurre. Si el programa es más complejo es posible que el error se propague por varias partes del programa y no sea tan evidente encontrar el origen, pero la traza inversa del error (Traceback) que da el intérprete muestra el camino que siguió el código que finalmente produjo el error. Supongamos un programa como el anterior pero en el que el cálculo se hace en varios pasos:

a, b = 20, 0

def division(x, y):
    return x/y

def imprime_resultado(x, y):
    resultado = division(x, y)

    print("La división de {} entre {} es {}".format(resultado))

imprime_resultado(a, b)
japp@vega:~$ python3 error.py
Traceback (most recent call last):
  File "test/errors.py", line 22, in <module>
    imprime_resultado()
  File "test/errors.py", line 17, in imprime_resultado
    resultado = division(a, b)
  File "test/errors.py", line 13, in division
    return x/y
ZeroDivisionError: division by zero

Aunque el error es el mismo, el mensaje que devuelve el intérprete es mucho más largo porque sigue la traza del error hasta llegar al origen, que se muestra al final de la lista (linea 13), permitiéndonos ver cómo ha pasado por partes de código hasta llegar al origen del problema.

Con este mensaje, nos avisa del error indicando el tipo en la última línea, ZeroDivisionError, terminando la ejecución. Que Python nos dé tanta información al ocurrir una excepción es muy útil pero muy a menudo sabemos que estos errores pueden ocurrir y lo ideal es estar preparado capturando la excepción y actuar en consecuencia en lugar de interrumpir el programa o al menos, antes de interrumpirlo realizar procesos alternativos o preparar un cierre seguro y limpio del programa.

Para hacer esto podemos usar la sentencia try-except, que nos permite probar (Try) una sentencia y capturar un eventual error y hacer algo al respecto (except) en lugar de detener el programa directamente. En el ejemplo anterior podríamos hacer lo siguiente:

a, b = 23, 0

try:
    resultado = a/b
except:
    print("Hay un error en los valores de entrada")

Ahora el código intenta ejecutar a/b y de haber algún tipo de error imprime el mensaje indicado y sigue adelante en lugar abortar la ejecución del programa. Al hacer esto hemos «capturado» la excepción evitando que el programa se detenga, suponiendo que éste puede continuar a pesar del error. Nótese que de esta manera no sabemos qué tipo de error ha ocurrido, que antes se indicaba con la clave ZeroDivisionError, que es uno de los muchos tipos de errores que Python reconoce. Esto no es una buena práctica porque perdemos información sobre qué fue exactamente lo que causó el error, simplemente sabremos que «algo fue mal».

Podemos usar esta técnica para pedir al usuario un tipo de dato determinado que se compruebe constantemente:

while True:
    try:
        x = int(input("Dame un numero: "))
        break  # Si no da error, corto el while con break
    except ValueError:
        print("Eso no es un número, prueba otra vez...")

Si quisiéramos distinguir el tipo de error ocurrido, para tomar distintas acciones o mensajes, debemos especificarlo en except, por ejemplo:

a, b, c = 23, 0, "A"

try:
    resultado = a/b
except ZeroDivisionError:
    print("Error, division por cero.")
except TypeError:
    print("Error en el tipo de dato.")

# Resultado:
# Error, division por cero.

try:
    resultado = a/c
except ZeroDivisionError:
    print("Error, division por cero.")
except TypeError:
    print("Error en el tipo de dato.")

# Resultado:
# Error en el tipo de dato.

De esta manera, sabemos exactamente qué tipo de error se cometió en cada caso, una división por cero o un error en el tipo de dato (que es lo que indica TypeError).

import sys
a, b, c = 23, 0, "A"

try:
    resultado = a/b
except (ZeroDivisionError, ValueError):
    print("Error, division por cero o tipo de dato incorrecto.")
except:
    # El metodo exc_info() nos da informacion sobre la ejecucion
    # del programa y los errores si los hay
    print("Error inesperado:", sys.exc_info()[0])
    raise

# Devuelve:
# Error, division por cero o tipo de dato incorrecto.

En este ejemplo ocurre una división por cero y el error es capturado con el primer except sin indicar cual de las dos posibles excepciones indicadas en la tupla ha ocurrido (ZeroDivisionError, ValueError). Sin embargo si en el try hacemos la operación resultado = a/c, el error que ocurre no es del tipo ZeroDivisionError o ValueError (este último salta cuando en una operación o función el tipo de dato es correcto, pero no su valor, por ejemplo logaritmo de un número negativo) y el error será capturado por el segundo except indicando que ocurrió un error no esperado, pero al usar la sentencia raise hacemos haga saltar el error ocurrido. De hecho, con raise es posible hacer saltar cualquier error si nuestro programa lo requiere.

Hay que recordar que la captura de excepciones no son para evitar que un programa se detenga, si no poder saber qué error ha ocurrido y se es posible tomar medidas alternativas. Si ocurre una excepción y el programa ya no puede seguir ejecutarse, o quiere seguir haciéndolo a pesar de haber una excepción, es posible que tengamos que hacer operaciones de notificación o de limpieza (enviar un mensaje, borrar ficheros temporales, cerrar ficheros que abrimos para leer, guardar un registro, etc.) antes de detener el programa o continuar con bloque de código siguiente. Para hacer esto podemos emplear la sentencia finally si no se cumple el try.

# fichero de texto para guardar los resultados
file_input = open("resultados.txt", "a")

try:
    a, b = eval(input("Dame dos numeros para dividir: "))
    resultado = a / b
    file_input.write("{} entre {} es {}".format(a, b, resultado))
except TypeError:
    print("Debes dar valores numericos")
except ZeroDivisionError:
    print("División por cero")
finally:
    print("Gracias por usar el programa.")
    # Cierra el fichero antes de abortar
    file_input.close()

La sentencias finally se ejecuta siempre en cualquier caso.

Conviene consultar la documentación oficial de Python para tener más información sobre la captura de excepciones y los tipos de errores reconocidos.

Encontrando errores con el depurador

Cuando los programas son más complejos no es sencillo encontrar el origen de errores como los ejemplos que acabamos de ver. Cuando las cosas se complican podemos usar un depurador, una potente herramienta para trazar y encontrar errores. Aunque existen varios depuradores para Python, incluso con interfaz gráfica, el depurador por defecto de Python, pdb hace un gran trabajo ayudándonos a encontrar errores ocultos.

Lo primero que debemos hacer es importar el depurador, que se hace como un módulo cualquiera y luego añadir un punto de traza en nuestro código donde queremos empezar a analizar. Consideremos en el ejemplo que hicimos antes y añadámosle el depurador:

import pdb

a, b = 20, 0

pdb.set_trace()

def division(x, y):
    return x/y

def imprime_resultado(x, y):

    resultado = division(x, y)

    print("La división de {} entre {} es {}".format(x, y, resultado))

imprime_resultado(a, b)

Hemos puesto un punto de control justo después de definir las variables. Al encontrarse esta línea hará lo siguiente:

  1. Detener la ejecución del programa.

  2. Mostrar línea actual (el siguiente comando que va a ejecutar).

  3. Esperar por el usuario para de alguna entrada.

Es decir, el programa se detiene en la línea donde está pdb.set\_trace() y aparece el prompt del depurador ((Pdb)) e indica el siguiente línea a ejecutar:

japp@vega:~$ python3 errors.py
> errors.py(15)<module>()
-> def division(x, y):
(Pdb)

Ahora podemos ejecutar algunos comandos del depurador según lo que queramos hacer:

s(tep)

Ejecuta la línea actual incluso dentro de una función, deteniéndose en la siguiente línea.

n(ext)

Continúa con la ejecución hasta la siguiente línea de la función actual o hasta que encuentre un return. La diferencia con step es que no entra dentro de funciones, siendo la ejecución más rápida.

c(ont(inue))

Continúa con la ejecución y solo se detiene si encuentra un punto de control.

r(eturn)

Continúa con la ejecución de la función actual hasta que encuentra un return.

l(ist) [primero[, ultimo]]

Muestra 11 líneas de código en torno a la línea actual. Si añaden los argumentos [primero[, ultimo]] se muestran las líneas indicadas.

p expresión

Evalúa una expresión en contexto actual imprimiendo su valor.

q(uit)

Sale del depurador abortando la ejecución.

Nuestro programa está ahora detenido en la definición de la función division(); si usamos el comando n <Enter> el depurador continuará con la siguiente línea en el programa principal sin entrar en el bloque de division() y se detendrá otra vez en la definición de imprime_resultado() y si repetimos lo mismo llegará a la ejecución de imprime_resultado() sin haber entrado en la definición anterior. Estando en la línea imprime_resultado() podemos ahora usar s <Enter> para hacer lo mismos que s pero esta vez entrar en el bloque que define imprime_resultado() deteniéndose en resultado = division(x, y); ahí podemos hacer lo mismo, usar s para entrar en la definición de division() y terminar encontrando el origen del problema, la división x/y en el return.

Si ya sospechamos que error está en función division(), podemos poner ahí el punto de control, o poner varios si lo necesitamos y saltar con c (continue) hasta el siguiente punto de control.

Para evaluar los valores que tienen las variables en un punto concreto de la ejecución, podemos imprimir sus valores con el comando p para comprobar si es lo que esperamos:

japp@vega:~$ python3 errors.py
-> def division(x, y):
(Pdb) p a, b
(20, 0)
(Pdb)

Incluso es posible cambiar los valores de las constantes en plena ejecución y continuar hasta el final. Si la variable que queremos cambiar coincide con algún comando del depurador (n, c, b, etc.), debemos añadir ! para indicar que es una variable y no el parámetro:

japp@vega:~$ python3 errors.py
-> def division(x, y):
(Pdb) !b = 2.5
(Pdb) c
La división de 20 entre 2.5 es 8.0
japp@vega:~$

Así, hemos cambiado el valor de b a 2.5 y continuado la ejecución hasta el final con c y el programa finaliza sin error.