{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<p>\n",
    "<font size='5' face='Georgia, Arial'>IIC-2233 Apunte Programación Avanzada</font><br>\n",
    "<font size='1'>&copy; 2015 Karim Pichara - Christian Pieringer. Todos los derechos reservados.</font>\n",
    "<br>\n",
    "<font size='1'>Modificado en 2017-1 al 2026-1 por Equipo Docente IIC2233</font>\n",
    "</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Tabla de contenidos\n",
    "\n",
    "1. [Clases de Excepciones](#Clases-de-Excepciones)\n",
    "2. [Excepciones personalizadas](#Excepciones-personalizadas)\n",
    "    1. [Un ejemplo: La lista del curso](#un-ejemplo-la-lista-del-curso)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Clases de Excepciones"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En Python, todas las excepciones heredan de `BaseException`. A partir de ella existen tres tipos de excepciones: **`SystemExit`**, **`KeyboardInterrupt`**, y **`Exception`**. Todas las excepciones generadas por errores durante la ejecución de un programa son subclases de `Exception`, tal como se muestra en el siguiente diagrama:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "![](img/jerarquia-excepciones.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esto quiere decir, que si se usa `Exception` para manejar errores (`except Exception`), esto capturará cualquier error que sea instancia de una subclase de `Exception`. De esta forma, el tratamiento es general y no específico a un tipo de error en especial. En general, es recomendable actuar de forma **selectiva** sobre un tipo determinado de excepciones (`IOError`, `AtributeError`, `ValueError`, etc.). Si bien es posible manejar cualquier tipo de excepción capturando `Exception`, esto se considera una **mala práctica**, pues capturar una excepción sin saber su naturaleza, puede causar comportamientos inesperados en el programa."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "def divide(num, den):\n",
    "    if not (isinstance(num, int) and isinstance(den, int)):\n",
    "        raise TypeError(\"Type error in numerator or denominator. :'(\")\n",
    "\n",
    "    if num < 0 or den < 0:\n",
    "        raise ValueError(\"There is a negative value in numerator or denominator >:(\")\n",
    "\n",
    "    return float(num) / float(den)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Error: Type error in numerator or denominator. :'(\n",
      "Check the input data. Something went wrong, but I don't know the specific type of Exception...\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    print(divide(1, \"1\"))\n",
    "except Exception as err:\n",
    "    # Este bloque opera para todos los tipos de excepciones que hereden de Exception.\n",
    "    print(f\"Error: {err}\")\n",
    "    print(\"Check the input data. Something went wrong, but I don't know the specific type of Exception...\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Error: There is a negative value in numerator or denominator >:(\n",
      "Check the input data. Something went wrong, but I don't know the specific type of Exception...\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    print(divide(1, -2))\n",
    "except Exception as err:\n",
    "    # Este bloque opera para todos los tipos de excepciones que hereden de Exception.\n",
    "    print(f\"Error: {err}\")\n",
    "    print(\"Check the input data. Something went wrong, but I don't know the specific type of Exception...\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Error: float division by zero\n",
      "Check the input data. Something went wrong, but I don't know the specific type of Exception...\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    print(divide(4, 0))\n",
    "except Exception as err:\n",
    "    # Este bloque opera para todos los tipos de excepciones que hereden de Exception.\n",
    "    print(f\"Error: {err}\")\n",
    "    print(\"Check the input data. Something went wrong, but I don't know the specific type of Exception...\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Excepciones personalizadas\n",
    "\n",
    "Podemos crear nuestros propios tipos de excepciones. Para ello debemos heredar desde la clase `Exception`. Podemos modificar el comportamiento heredado sobrescribiendo los métodos que tiene implementada esta clase."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Exception1(Exception):\n",
    "\n",
    "    # Al no sobrescribir nada, hereda todo sin modificaciones.\n",
    "    pass\n",
    "\n",
    "\n",
    "class Exception2(Exception):\n",
    "\n",
    "    def __init__(self, a, b):\n",
    "        # Sobrescribimos el __init__ para cambiar el ingreso de los parámetros.\n",
    "        super().__init__(f\"One of the values {a} or {b} is not an integer\")\n",
    "\n",
    "\n",
    "def divide(num, den):\n",
    "    # Por ejemplo, redefiniremos las excepciones que\n",
    "    # utilizamos en los ejemplos anteriores.\n",
    "    if not (isinstance(num, int) and isinstance(den, int)):\n",
    "        raise Exception2(num, den)\n",
    "\n",
    "    if num < 0 or den < 0:\n",
    "        raise Exception1(\"The values are negative\")\n",
    "\n",
    "    return float(num) / float(den)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Error: The values are negative\n"
     ]
    }
   ],
   "source": [
    "# Este ejemplo lanza una Excepcion1.\n",
    "try:\n",
    "    print(divide(4, -3))\n",
    "\n",
    "except Exception1 as err:\n",
    "    # Este bloque opera para la Excepcion1.\n",
    "    print(f\"Error: {err}\")\n",
    "\n",
    "except Exception2 as err:\n",
    "    # Este bloque opera para Excepcion2 cuando los datos no son enteros.\n",
    "    print(f\"Error: {err}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Error: One of the values 4.4 or -3 is not an integer\n"
     ]
    }
   ],
   "source": [
    "# Este ejemplo lanza una Excepcion2.\n",
    "try:\n",
    "    print(divide(4.4, -3))\n",
    "\n",
    "except Exception1 as err:\n",
    "    # Este bloque opera para la Excepcion1.\n",
    "    print(f\"Error: {err}\")\n",
    "\n",
    "except Exception2 as err:\n",
    "    # Este bloque opera para Excepcion2 cuando los datos no son enteros.\n",
    "    print(f\"Error: {err}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Podemos definir comportamientos personalizados para las excepciones que creamos. Por ejemplo, agregar métodos que nos permitan recuperar información de la excepción."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Error: The money in the wallet is not enough to pay $1500. There is an overspending of $500.\n"
     ]
    }
   ],
   "source": [
    "class TransactionError(Exception):\n",
    "\n",
    "    def __init__(self, funds, expense):\n",
    "        super().__init__(f\"The money in the wallet is not enough to pay ${expense}\")\n",
    "        self.funds = funds\n",
    "        self.expense = expense\n",
    "\n",
    "    def excess(self):\n",
    "        return self.expense - self.funds\n",
    "\n",
    "\n",
    "class Wallet:\n",
    "\n",
    "    def __init__(self, money):\n",
    "        self.funds = money\n",
    "\n",
    "    def pay(self, expense):\n",
    "        if self.funds - expense < 0:\n",
    "            raise TransactionError(self.funds, expense)\n",
    "        self.funds -= expense\n",
    "\n",
    "\n",
    "w = Wallet(1000)\n",
    "\n",
    "try:\n",
    "    w.pay(1500)\n",
    "\n",
    "except TransactionError as err:\n",
    "    print(f\"Error: {err}. There is an overspending of ${err.excess()}.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Un ejemplo: La lista del curso\n",
    "\n",
    "En este ejemplo desea modelar la siguiente situación: se desean inscribir estudiantes en distintos cursos universitarios, para lo cual se cuentan con listas de las peticiones de varios estudiantes por curso. El problema es que estas listas fueron afectadas por *hackers* que poblaron de información falsa, específicamente, hay estudiantes con número de identificación duplicados. Ya que no hay forma de identificar los falsos de los verdaderos, se inscribirán solamente quienes tengan un identificador nuevo, y los duplicados se almacenarán de alguna forma, para futura referencia.\n",
    "\n",
    "La siguiente solución utiliza una excepción personalizada para llevar el registro de los duplicados encontrados en la lista. Aprovecha el uso de atributos de clase para mantener un registro general de todos los duplicados, y atributos de instancia para obtener información."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **(Recordatorio...** \n",
    ">\n",
    ">Al usar clases, normalmente definimos atributos para las instancias mediante el uso de `self.attribute = valor`. Esto genera la creación de un **atributo de instancia**, cuyo valor es accesible desde cualquier punto de la instancia mediante `self`. Si definimos un atributo al nivel de los métodos, se le conoce como **atributo de clase** y es accesible por todas las instancias mediante el nombre de la clase: `Class.attribute`. También, es posible acceder mediante `self` en una instancia, siempre y cuando no tenga un atributo de instancia del mismo nombre definido. Cuando se busca un valor de atributo mediante `self.attribute`, el orden de búsqueda es:\n",
    ">1. Buscar en la instancia. (atributo de instancia)\n",
    ">2. Si no existe, buscar en la clase. (atributo de clase)\n",
    ">3. Si no existe, levantar un error (`AttributeError`).\n",
    ">\n",
    ">**...cierra recordatorio.)**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "from collections import namedtuple, defaultdict\n",
    "\n",
    "\n",
    "# Modelamos les estudiantes como entidades simples.\n",
    "Student = namedtuple(\"Student\", [\"id_number\", \"first_name\", \"last_name\"])\n",
    "\n",
    "\n",
    "class DuplicateStudentError(Exception):\n",
    "\n",
    "    # Se mantiene un diccionario como atributo de clase para almacenar todos los duplicados.\n",
    "    duplicate_students = defaultdict(list)\n",
    "\n",
    "    def __init__(self, student):\n",
    "         # Se almacena al estudiante que lanzó la excepción.\n",
    "        self.student = student\n",
    "        id_number = student.id_number\n",
    "        # Se almacena el duplicado.\n",
    "        self.duplicate_students[id_number].append(student)\n",
    "        super().__init__(f\"Duplicate student error with ID number: {student}\")\n",
    "\n",
    "    # Creamos property de instancia que accede a todos los duplicados del mismo número.\n",
    "    @property\n",
    "    def duplicates(self):\n",
    "        return self.duplicate_students[self.student.id_number]\n",
    "\n",
    "     # Creamos property de instancia que calcula la cantidad de duplicados del mismo número.\n",
    "    @property\n",
    "    def count(self):\n",
    "        return len(self.duplicate_students[self.student.id_number])\n",
    "\n",
    "# Modelamos el curso con una estructura simple para almacenar sus estudiantes\n",
    "class Course:\n",
    "\n",
    "    def __init__(self, name):\n",
    "        self.name = name\n",
    "        self.students = dict()\n",
    "\n",
    "    # Método encargado de inscribir nuevo estudiante, en caso de encontrar duplicado, levanta un error.\n",
    "    def enroll(self, student):\n",
    "        if student.id_number in self.students:\n",
    "            raise DuplicateStudentError(student)\n",
    "        else:\n",
    "            self.students[student.id_number] = student\n",
    "            print(f\"✅ Student with ID {student.id_number} enrolled successfully.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✅ Student with ID 15633459 enrolled successfully.\n",
      "✅ Student with ID 1663525J enrolled successfully.\n",
      "✅ Student with ID 17632451 enrolled successfully.\n",
      "❌ Duplicate student with ID 15633459 found.\n",
      "There are now 1 students with the duplicate ID 15633459:\n",
      "\tFernanda Errazuriz\n",
      "✅ Student with ID 1563001J enrolled successfully.\n",
      "❌ Duplicate student with ID 1663525J found.\n",
      "There are now 1 students with the duplicate ID 1663525J:\n",
      "\tBenjamín Valdivieso\n",
      "❌ Duplicate student with ID 15633459 found.\n",
      "There are now 2 students with the duplicate ID 15633459:\n",
      "\tFernanda Errazuriz, Cristian Soto\n"
     ]
    }
   ],
   "source": [
    "students_to_enroll = [\n",
    "    Student('15633459', 'Juan', 'Hernández'),\n",
    "    Student('1663525J', 'Belén', 'Pinto'),\n",
    "    Student('17632451', 'Ariel', 'Gonzalez'),\n",
    "    Student('15633459', 'Fernanda', 'Errazuriz'),\n",
    "    Student('1563001J', 'Javiera', 'Martínez'),\n",
    "    Student('1663525J', 'Benjamín', 'Valdivieso'),\n",
    "    Student('15633459', 'Cristian', 'Soto'),\n",
    "]\n",
    "\n",
    "course_iic2233 = Course('IIC2233')\n",
    "\n",
    "for student in students_to_enroll:\n",
    "    try:\n",
    "        course_iic2233.enroll(student)\n",
    "    except DuplicateStudentError as error:\n",
    "        print(f\"❌ Duplicate student with ID {student.id_number} found.\")\n",
    "        print(f\"There are now {error.count} students with the duplicate ID {student.id_number}:\")\n",
    "        print(\"\\t\" + \", \".join([f\"{s.first_name} {s.last_name}\" for s in error.duplicates]))"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
