Excepciones

Qué es una excepción / Exception ?

Primero que nada vamos a separar dos ideas. Por un lado tenemos la definción conceptual de excepciones de un programa. Por otro lado a veces nos referimos a excepciones o exceptions a un mecanismo especifico que tienen ciertos lenguajes para modelar y tratar el concepto general.

Vamos a ver ambas cosas en este apunte.

Concepto de Excepción de Programa

Una excepción de programa, conceptualmente, se refiere a un cierto evento o condición que genera la situación en la cual el programa no puede continuar. Es decir que no hay un requerimiento que especifique cómo salvar esa situación.

Por ejemplo, si se intenta dividir por cero. No es matemáticamente posible.

Bajemos un poco a detalle. Lo que no puede continuar o lo que no se puede llevar a cabo es una tarea específica. En nuestro ejemplo, la división. Esto depende del paradigma, pero digamos que en general entonces esto va a suceder en una función.

Dada la función (trivial, claro, pero a modo de ejemplo) dividir:

def dividir(numerador, denominador):
    return numerador / denominador

Si, denominador es cero (0), entonces, se puede dividir ?? No !

Entonces cómo qué debería hacer mi método dividir ?

Y acá entra la idea de mecanismo para programar estos casos excepcionales.

Mecanismo del Lenguaje para Programar Excepciones

Dependiendo del lenguaje es cómo programaremos estos casos excepcionales.

En general todos los lenguajes modernos tienen algún tipo de mecanismo para excepciones. Pero un lenguaje como C, que es lo que venían utilizando ustedes hasta ahora, no tiene ningún mecanismo. Y eso lleva a una muy mala práctica.

Lenguajes sin Mecanismo - Valor de retorno

Como no existe ningún mecanismo, la función recibe parámetros y no tiene otra opción que devolver algo. Entonces, la única forma de que la función avise a quien la llamó de que no pudo cumplir con su cometido es devolviendo algún tipo de código de error.

def dividir(numerador, denominador):
    if denominador == 0:
        return ERROR_DIVISION_POR_CERO
    else:
        return numerador / denominador

Utilizamos ahí una constante. Ahora lo dificil es, qué valor le vamos a asignar a esa constante ??

Ahí ya aparece un problema:

    • se mezcla la idea de que el método retorna un valor, como parte de su lógica normal con valores especiales de errores.

Por otro lado, si la función devuelve un valor especial, el que lo llama deberá entonces tener código específico para checkearlo. Y acá está el segundo gran problema

  • en cada lugar en que se llame al método, deberemos tener un if (retorno == ERROR_DIVISION_POR_CERO).
  • Si además, durante la función varias cosas pueden salir mal, habrá múltiples IF's

Estaremos mezclando la lógica principal del método, con la de manejo de casos especiales.

El impacto es aún mayor si pensamos todo un sistema completo codificado de esta forma.

Otra impacto negativo se da en cuanto a legibilidad de mi código. Si antes tenía una función que no retornaba ningún parámetro, ahora deberá retornar un int solo para manejar los casos excepcionales. Es dificil entonces determinar a partir de la firma si la función devuelve int porque debe hacerlo a nivel funcionalidad, o si el int ahí declarado en la función es solo para manejo de errores.

Manejo de Exceptions en Python

Por todos los problemas antes mencionados es que la gran mayoría de los lenguajes modernos tienen soporte explícito para manejar excepciones. Vamos a ver cómo es esto en Python, ya que es el lenguaje que vamos a estar utilizando durante la cursada.

La idea del mecanismo de exceptions es que si una función se encuentra en una de estas situaciones donde no puede continuar, levante una especie de "alerta" cortando el flujo de ejecución. Es decir que la función no continúa en la siguiente linea. Si no que se sale de ella abruptamente. Volviendo a quién la llamó (por ahora decimos esto, aunque es más complejo)

A esto se le denomina comúnmente como lanzar una exception.

En python esto se hace con la instrucción raise.

def dividir(numerador, denominador):
    if denominador == 0:
        raise ZeroDivisionError, "No se puede dividir por cero"
    else:
        return numerador / denominador

Técnicamente el else no es necesario porque el raise corta la ejecución, así que podemos simplificar a:

def dividir(numerador, denominador):
    if denominador == 0:
        raise ZeroDivisionError, "No se puede dividir por cero"
    return numerador / denominador

//TODO: continuar, mostrando un ejempo con try, como wrappear, hacer propias, etc.