Cap. 19 - Excepciones
19.1) Captura de excepciones
Cada vez que ocurre un error en tiempo de ejecución, crea un objeto de tipo excepción. El programa detiene su ejecución en ese momento y Python imprime el traceback, el cual termina con un mensaje que describe qué excepción ha ocurrido.
Por ejemplo, una división por cero crea esta excepción:
>>> print(55/0) Traceback (most recent call last): File "<interactive input>", line 1, in <module> ZeroDivisionError: integer division or modulo by zero
El intento de acceder a un elemento inexistente en una lista imprime esto:
>>> a = [] >>> print(a[5]) Traceback (most recent call last): File "<interactive input>", line 1, inIndexError: list index out of range
O tratar de hacer hacer una asignación de item en una tupla:
>>> tup = ("a", "b", "d", "d") >>> tup[2] = "c" Traceback (most recent call last): File "", line 1, in TypeError: 'tuple' object does not support item assignment
En cada caso, el mensaje de error en la última línea tiene dos partes: el tipo de error antes de los dos puntos, y detalles descriptivos del error después de los dos puntos.
A veces queremos ejecutar una operación que podría causar una excepción, pero no queremos que detenga al programa. Podemos manejar excepciones usando la sentencia try para "envolver" una región de código.
Por ejemplo, podemos pedirle al usuario el nombre del archivo y entonces intentar abrirlo. Si el archivo no existe, no queremos que el programa se cuelgue, queremos manejar la excepción:
nombre_archivo = input("Ingresa un nombre de archivo: ") try: f = open(nombre_archivo, "r") except: print("No hay un archivo con el nombre", nombre_archivo)
La sentencia try tiene tres cláusulas separadas, o partes, introducidas por las palabras claves try ... except ... finally.
- Tanto la parte except como la parte finally pueren omitirse, así que el ejemplo anterior considera la forma más habitual de uso de la sentencia try.
La sentencia try ejecuta y monitorea las sentencias del primer bloque.
- Si no ocurre ninguna excepción, saltea el bloque que está bajo la cláusula except.
- Si ocurre alguna excepción, ejecuta las sentencias que están bajo except y luego continúa.
Podríamos encapsular esta capacidad en una función existe_archivo que toma un nombre de archivo y devuelve True si el archivo existe, y False en caso contrario:
def existe_archivo(nombre_archivo): try: f = open(nombre_archivo) f.close() return True except: return False
Un template para chequear si un archivo existe, sin usar excepciones
La función que acabamos de implementar no es la que recomendamos usar. Abre y cierra el archivo, lo cual es semánticamente distinto a sólo preguntar "existe el archivo"?
- ¿Por qué? Primero, porque va a actualizar algunos datos de fecha y hora del archivo (del tipo: última vez leído).
- Segundo, puede respondernos que no existe el archivo si otro programa en ese mismo momento tiene el archivo abierto, o si no tenemos permisos para abrir el archivo.
Python provee un módulo llamado os.path dentro del módulo os, el cual viene con una buena cantidad de funciones útiles para trabajar con caminos (paths), archivos y directorios, así que deberías consultar su ayuda.
import os # Esta es la forma recomendada de chequear si un archivo existe. if os.path.isfile("unit_tester.py"):
Observación final: El método os.path.isfile puede recibir un nombre de archivo (como en el ejemplo) o bien un path completo hasta el archivo.
- Es decir que la línea if anterior también se podría haber escrito así:
if os.path.isfile("c:/temp/testdata.txt"):
Podemos usar múltiples cláusulas except para manejar distintos tipos de excepciones.
- Ver la lección Errores y excepciones de Guido van Rossum, el creador de Python, que es parte de su Tutorial de Python, allí se encontrará una discusión completa sobre excepciones.
- Los links anteriores llevan a páginas que son las traducciones al español del original de Guido van Rossum, que está en inglés. El original es Errors and Exceptions, que es parte de su Python Tutorial.
- Así que el programa puede hacer una cosa si el archivo no existe, y otra cosa si el archivo existe pero está en uso por otro programa (por lo cual también habrá una excepción, pero de otra clase).
19.2) Lanzamiento (raising) de nuestras propias excepciones
¿Puede nuestro programa causar deliberadamente sus propias excepciones? Si nuestro programa detecta una condición de error, pueden lanzar su propia excepción. Aquí hay un ejemplo que recibe input del usuario y chequea si el número recibido es no-negativo:
def get_edad(): edad = int(input("Por favor ingresa tu edad: ")) if edad < 0: # Crear una nueva instancia de una excepción mi_error = ValueError("{0} no es una edad válida".format(edad)) raise mi_error return edad
La línea 5 crea un objeto excepción, en este caso, un objeto ValueError, el cual encapsula información específica sobre el error. Supongamos que en este caso la función A llamó a B, la cual llamó a C, que llamó a D que llamó a get_edad. La sentencia raise eleva este objeto como una especie de "valor de retorno" e inmediatamente sale de get_edad a su llamador D. Luego D sale de inmediato y vuelve a C, el cual sale de inmediato y vuelve a B, y así sucesivamente, cada uno elevando la excepción al llamador anterior, hasta que se encuentra un try ... except que pueda manejar la excepción. A este proceso se lo conoce como "desenrollando la pila" (en inglés: "unwinding the stack").
El tipo ValueError es uno de los tipos de excepción built-in y era el que más se ajustaba al tipo de error que queríamos lanzar en este ejemplo. La lista completa de excepciones built-in se puede encontrar en la sección Excepciones incorporadas de La Biblioteca Estándar de Python, también escrita por Guido van Rossum, creador de Python.
- En la versión original en inglés, es la sección Built-in Exceptions de The Python Standard Library
Si la función que creó get_edad (o su llamador, o alguno de los llamadores de su llamador...) maneja el error, entonces el programa podrá seguir corriendo. De lo contrario, Python imprime el traceback y corta la ejecución del programa.
>>> get_edad() Por favor ingresa tu edad: -23 Traceback (most recent call last): File "", line 1, in File "C:\Users\javid\Desktop\cambios\stp\clases\programación\python\tests curso\L19_excepciones.py", line 30, in get_edad raise mi_error ValueError: -23 no es una edad válida
El mensaje de error incluye el tipo de excepción y la información adicional que fue provista en el momento en que se creó el objeto excepción.
Es común que las líneas 5 y 6 del código de get_edad (creación del objeto excepción, y lanzamiento de la excepción) se combinen en una sentencia única, pero son realmente dos cosas distintas e independientes las que están ocurriendo aquí, así que podría tener sentido mantener los dos pasos por separado cuando recién estamos aprendiendo a trabajar con excepciones.
- Aquí lo mostramos como una sentencia de una línea:
raise ValueError("{0} no es una edad válida".format(edad))
19.3) Revisitando un ejemplo anterior
Aplicando manejo de excepciones, podemos ahora modificar la función recursion_profundidad que implementamos en el capítulo anterior para que se detenga cuando alcance el máximo nivel de profundidad de recursión permitido:
def recursion_profundidad(num): print("Número de recursión profunda", num) try: recursion_profundidad(num + 1) except: print("No puedo ir más hondo en este agujero de gusano.") recursion_profundidad(0)
Ejecuta esta versión y observa los resultados.
19.4) La cláusula FINALLY de la sentencia TRY
Un patrón de programación muy común es tomar un recurso de cierto tipo.
- Por ejemplo creamos una ventana para tortugas para dibujar en ella, o establecemos una conexión con nuestro proveedor de servicios internet, o podemos abrir un archivo para escribir.
Luego realizamos ciertos cómputos que pueden provocar una excepción, o podemos trabajar sin problemas.
Pase lo que pase, queremos "limpiar" los recursos que hemos tomado.
- En los ejemplos anteriores, cerrar la ventana, desconectarnos del proveedor de servicios de internet, cerrar el archivo.
La cláusula finally
de la sentencia try
es la forma de hacer esto. Considerar el siguiente ejemplo, que es un poco forzado pero sirve para explicar el punto:
def mostrar_poligono(): try: win = turtle.Screen() # Tomar/crear un recurso, en este caso una ventana tess = turtle.Turtle() # Este diálogo pordría ser cancelado, # o la conversión a int podría fallar, o n podría ser cero. n = int(input("Cuántos lados quieres que tenga el polígono?")) angulo = 360 / n for i in range(n): # Dibujar el polígono tess.forward(10) tess.left(angulo) time.sleep(3) # Haz que el programa espere unos segundos finally: win.bye() # Cerrar la ventana de la tortuga mostrar_poligono() mostrar_poligono() mostrar_poligono()
Nota: Este código (adaptado del del curso original) se cuelga, probablemente porque cambiaron los métodos turtle.Screen() y turtle.Screen().bye() - revisarlo bien.
En las 3 líneas finales, mostrar_poligono se llama 3 veces. Cada una crea una nueva ventana para su tortuga, y dibuja un polígono con la cantidad de lados solicitados por el usuario.
- Pero ¿qué pasa si el usuario ingresa un string que no puede convertirse a entero? ¿Qué si cierra el diálogo?
- Tendremos una excepción, pero aun así, queremos cerrar la ventana de la tortuga al final.
La línea de la cláusula finally lo hace por nosotros. Sin importar si las sentencias de la cláusula try terminan o no de ejecutarse, el bloque finally se ejecutará siempre al final.
- Observar que seguimos sin manejar la excepción - sólo una cláusula except maneja excepciones, por lo cual nuestro programa se colgará de todas formas.
- Pero al menos nuestra ventana de la tortuga estará cerrada antes del crash!
19.5) Glosario
- excepción, manejo de excepciones, lanzar excepciones
19.6) Ejercicios
1) Escribir una función leer_entero_positivo que use el diálogo input para pedir al usuario un número positivo y luego chequee el input para confirmar que cumple con los requerimientos. Debería ser capaz de manejar inputs que no se pueden convertir a int, así como ints negativos y casos de borde (es decir casos en que el usuario cierra el diálogo o simplemente no escribe nada). (hacerlo)