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. .. code-block:: python a, b = 20, 0 resultado = a/b print(resultado) Al ejecutarlo tendremos el siguiente mensaje: .. code-block:: bash japp@vega:~$ python3 error.py Traceback (most recent call last): File "test/errors.py", line 11, in 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: .. code-block:: python 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) .. code-block:: bash japp@vega:~$ python3 error.py Traceback (most recent call last): File "test/errors.py", line 22, in 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: .. code-block:: python 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: .. code-block:: python 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: .. code-block:: python 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``). .. code-block:: python 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``. .. code-block:: python # 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: .. code-block:: python 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: #. Detener la ejecución del programa. #. Mostrar línea actual (el siguiente comando que va a ejecutar). #. 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: .. code-block:: bash japp@vega:~$ python3 errors.py > errors.py(15)() -> 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 `` 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 `` 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: .. code-block:: bash 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: .. code-block:: bash 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.