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, in 
IndexError: 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.

La sentencia try ejecuta y monitorea las sentencias del primer bloque.

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.

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.

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.

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.

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.

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.

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.

19.5) Glosario

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)