Excepciones

Las excepciones son la herramienta que implementa Python para manejar los errores potenciales de un programa. Cuando una porción de código quiere indicar que ocurrió un error, se dice que debe lanzar una excepción, mientras que, cuando otra quiere saber si surgió un error y actuar en consecuencia, se dice que debe capturarla o manejarla. Si una excepción es lanzada y ningún código la captura, el programa finaliza.

Muchos de los operadores y funciones que hemos estado utilizando lanzan excepciones cuando ocurre algún error. Por ejemplo, habíamos visto cómo convertir una cadena a un número vía int().

>>> int("30")
30

Esta función lanza una excepción cuando el argumento pasado no puede ser representado como un número entero.

>>> int("¡Hola, mundo!")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: '¡Hola, mundo!'

Como se observa, las excepciones están generalmente acompañadas de un mensaje que nos indica qué salió mal y en dónde (nombre del archivo y la línea). En el caso de la consola interactiva, las excepciones no manejadas se imprimen automáticamente en la pantalla, pero, como se dijo anteriormente, para programas reales un hecho tal implica la finalización del mismo.

Las excepciones son, en definitiva, clases. Por ello llevan la nomenclatura denominada CamelCase. ValueError es una excepción incorporada (definida por Python) que es lanzada cuando una función espera un valor determinado pero recibe otro.

Consideremos un ejemplo más real.

edad = int(input("Escribe tu edad: "))

if edad >= 18:
    print("Eres un adulto.")
else:
    print("Aún no eres un adulto.")

Aquí pronto vemos cuál es el problema. Si el usuario escribe cualquier cosa que no sea un número en la consola, el programa arrojará una excepción y terminará. Lo ideal sería que cuando eso ocurre, en lugar de terminar, el mismo programa vuelva a solicitar que el usuario ingrese su edad (pues bien podría haberse equivocado). Para eso necesitamos capturar la excepción, lo cual haremos con las palabras reservadas try y except.

while True:
    try:
        edad = int(input("Escribe tu edad: "))
        break
    except ValueError:
        print("¡Debes ingresar un número!")

if edad >= 18:
    print("Eres un adulto.")
else:
    print("Aún no eres un adulto.")

Si esto te ha desconcertado un poco, analicémoslo paso a paso. Primero hemos incluido un bucle while, que se ejecuta infinitamente por cuanto la condición es siempre verdadera (True). Esto nos permitirá que el programa siga preguntando la edad hasta que el usuario ingrese un número. Para ello "envolvimos" las llamadas a int() e input() dentro de un bloque de código try/except. La lógica es la siguiente: cuando una excepción es capturada, la ejecución del código salta desde donde haya ocurrido (la llamada a int()) al bloque de código dentro de la cláusula except. Así, cuando int() lanza ValueError, el código salta a la llamada a print() sin llegar a ejecutar la palabra reservada break, la cual da término al bucle. Por esta razón el proceso se repite siempre que ocurra una excepción del tipo ValueError. Ahora bien, si la llamada a int() es exitosa, break llega a ejecutarse y el código continúa en el condicional.

El dilema también puede ser resuelto usando funciones y recursión.

def solicitar_edad():
    try:
        return int(input("Escribe tu edad: "))
    except ValueError:
        return solicitar_edad()

edad = solicitar_edad()

Otras excepciones incoroporadas incluyen TypeError, KeyError, IndexError, NameError, RuntimeError, ZeroDivisionError. Las irás conociendo a medida que avances con el lenguaje. Por ejemplo, IndexError es lanzada cuando intentamos acceder a un elemento de una lista o tupla que está por fuera de sus límites.

>>> lenguajes = ["Python", "C", "C++", "Java"]
>>> lenguajes[5]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

O bien, KeyError, cuando indicamos una clave que no está en un diccionario.

>>> d = {"Python": 1991, "C": 1972}
>>> d["Java"]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'Java'

Un mismo código puede lanzar múltiples excepciones. Por ejemplo, int(), como hemos visto, lanza ValueError cuando el argumento no puede ser convertido a un número entero, pero también TypeError cuando el argumento no es una cadena.

>>> int([1, 2])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: int() argument must be a string, a bytes-like object or a number, not 'list'

En estos casos, podemos definir múltiples bloques de códigos para las distintas excepciones.

try:
    int(...)  # Pseudocódigo.
except ValueError:
    print("No puede convertirse a un entero.")
except TypeError:
    print("No es una cadena.")

O bien definir el mismo bloque de código para distintas excepciones.

try:
    int(...)
except (ValueError, TypeError):
    print("No puede convertirse a un entero o no es una cadena.")

Por último, para capturar cualquier excepción:

try:
    int(...)
except Exception:
    print("Ocurrió un error.")

Lanzando una excepción

Ya sabemos cómo capturar una excepción. Pero cuando escribimos nuestras propias funciones, a menudo querremos lanzar nuestras propias excepciones. Traigamos nuevamente nuestra función sumar().

def sumar(a, b):
    return a + b

Queremos que únicamente sea capaz de sumar números enteros. Para ello podemos chequear el tipo de dato de los argumentos vía la función incorporada isinstance() y, en caso negativo, lanzar la excepción TypeError, que es la que más se adecúa al error que queremos expresar (esperabamos un entero pero obtuvimos otro tipo).

def sumar(a, b):
    if not isinstance(a, int) or not isinstance(b, int):
        raise TypeError("a y b tienen que ser números enteros.")
    return a + b

Como se observa, se emplea la palabra reservada raise seguida del nombre de la excepción y, opcionalmente, un mensaje que detalle el error ocurrido.

Definiendo una excepción

Eventualmente necesitaremos definir nuevas excepciones si ninguna de las incorporadas se adecúa a nuestros intereses. Simplemente debemos crear una clase que herede de Exception.

class NuevaExcepcion(Exception):
    pass

Bien, hasta aquí hemos abordado las cuestiones fundamentales del lenguaje. Sigamos con un apéndice para conocer más sobre las colecciones, de las que ya hemos hablado.