{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<p>\n",
    "<font size='5' face='Georgia, Arial'>IIC2233 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 desde 2017-2 al 2026-1 por Equipo Docente IIC2233</font>\n",
    "</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Tabla de contenidos\n",
    "1. [Clases Abstractas](#Clases-Abstractas)\n",
    "2. [En Python: *Abstract Base Class*](#en-python-abstract-base-class)\n",
    "3. [Ejemplo - modelando clase `Personaje`](#Ejemplo---modelando-clase-Personaje)\n",
    "4. [Ejemplo - modelando figuras geométricas](#ejemplo---modelando-figuras-geométricas)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Clases Abstractas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las clases abstractas, si pensamos en lenguajes de programación en general, nos permiten representar mejor lo que son las clases realmente abstractas desde el punto de vista del modelamiento. Por abstracta, nos referimos a que son clases cuya intención no es ser instanciada (o crear un objeto de esa clase directamente), si no que solo usarse como parte de modelamiento de otras clases. Estas a su vez pueden contener métodos abstractos, y de tener subclases, estas sobreescriben todos sus métodos abstractos.\n",
    "\n",
    "Por ejemplo, la clase `Mamifero` representa algo abstracto, no tiene forma específica, pero sí describe características de otras clases concretas, como `Perro`, `Humano` y `Ballena`. En términos de clases, se podría suponer que los ejemplos anteriores heredan de `Mamifero`. Luego `Mamifero` no puede tomar vida por sí sola a menos de que sea a través de una subclase. Una instancia de la clase `Mamifero` no tendría mucho sentido, si no que necesitamos saber a qué (subclase) tipo de mamífero corresponde (`Perro`, `Humano`, `Ballena`, etc.) para que sepamos cómo se comporta, su tamaño, etc. Son las subclases de la clase abstracta las que deben ser instanciadas.\n",
    "\n",
    "Por otro lado, un **método abstracto** en una clase abstracta representa comportamientos que deben tener todas las clases que hereden de ella, y generalmente difieren en implementación entre subclases. Luego, los métodos abstractos **deben** ser implementados por las subclases. Esto también permite crear una especie de *contrato* con las subclases de una clase abstracta, al establecer el comportamiento mínimo que debe implementar.\n",
    "\n",
    "Las clases abstractas también pueden tener métodos *normales*, que están implementados ya y que no es necesario re-implementar en las subclases. Luego también son capaces de permitir herencia de comportamiento a sus subclases, y provee la ventaja de ahorrarse repetir atributos y métodos que compartirían todas sus subclases.\n",
    "\n",
    "Entonces, una clase es **abstracta** dentro de una definición general en computación si:\n",
    "- Es una clase que no se debería instanciar directamente\n",
    "- Contiene uno o más métodos abstractos\n",
    "- Sus subclases implementan todos sus métodos abstractos"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## En Python: *Abstract Base Class*"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Específicamente en Python, una clase abstracta puede instanciarse bajo ciertas condiciones, pero en el curso seguiremos la definición general del patrón de clase abstracta, por lo que no se deben instanciar en ningún caso.\n",
    "\n",
    "La sintaxis base de Python no tiene una forma de definir clases abstractas, pero sí existe el módulo `abc` (\"Abstract Base Class\") que nos provee herramientas para hacerlo. Mediante la clase `ABC` y el [decorador](https://book.pythontips.com/en/latest/decorators.html) `abstractmethod` es posible definir una clase abstracta:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from abc import ABC, abstractmethod\n",
    "\n",
    "\n",
    "class Base(ABC):          ## al heredar de ABC, la clase Base se define como abstract\n",
    "\n",
    "    @abstractmethod       ## Esto hace que metodo_1 sea abstracto. Una subclase de Base está obligada a implementarlo.\n",
    "    def metodo_1(self):   \n",
    "        pass\n",
    "\n",
    "    @abstractmethod\n",
    "    def metodo_2(self):\n",
    "        pass"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Se definió la clase `Base` y se marca como abstracta al hacerle heredar de `ABC` en su definición. Además, los métodos `metodo_1` y `metodo_2` se marcan como abstractos con el decorador `abstractmethod`. Si intentamos instanciar la clase recién definida, obtenemos un error:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "instancia = Base()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "¡Porque es abstracta **y** contiene métodos abstractos! Pero si creamos una clase que herede de `Base` e implemente sus métodos no hay problemas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class SubClase1(Base):\n",
    "\n",
    "    def metodo_1(self):\n",
    "        pass\n",
    "\n",
    "    def metodo_2(self):\n",
    "        pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "instancia = SubClase1()\n",
    "print(type(instancia))\n",
    "print(f'Es subclase: {issubclass(SubClase1, Base)}')\n",
    "print(f'Es instancia: {isinstance(instancia, Base)}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Así, obtenemos una instancia de `SubClase1`. Esta clase hereda de `Base`, y sus instancias también son instancias de `Base`. En cambio, si se define una clase `SubClase2` que no implementa algún método abstracto, se produce un error al intentar instanciarla:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class SubClase2(Base):\n",
    "    \n",
    "    def metodo_1(self):\n",
    "        pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "instancia = SubClase2()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las clases abstractas **no deben únicamente contener métodos abstractos**. También se pueden aprovechar las ventajas de herencia con métodos no abstractos, ¡e incluso con los abstractos! El siguiente ejemplo es una clase abstracta con métodos no abstractos y abstractos:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from abc import ABC, abstractmethod\n",
    "\n",
    "\n",
    "class Base(ABC):\n",
    "\n",
    "    def __init__(self, nombre):\n",
    "        self.nombre = nombre\n",
    "        self.contador = 0\n",
    "\n",
    "    def metodo_1(self):\n",
    "        self.contador += 1\n",
    "\n",
    "    @abstractmethod\n",
    "    def metodo_2(self):\n",
    "        self.contador += 2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La subclase `SubClase1` re-implementa el método abstracto `metodo_2`, y es capaz de usar los métodos no abstractos de `Base`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class SubClase1(Base):\n",
    "\n",
    "    def metodo_2(self):\n",
    "        self.contador += 3"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "instancia_1 = SubClase1(\"1\")\n",
    "print(instancia_1.contador)\n",
    "instancia_1.metodo_1()\n",
    "print(instancia_1.contador)\n",
    "instancia_1.metodo_2()\n",
    "print(instancia_1.contador)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La subclase `SubClase2` también es capaz de usar los métodos no abstractos de `Base`, pero re-implementa el método abstracto `metodo_2` accediendo a la definición original de `Base` mediante `super`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class SubClase2(Base):\n",
    "\n",
    "    def metodo_2(self):\n",
    "        super().metodo_2()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "instancia_2 = SubClase2(\"2\")\n",
    "print(instancia_2.contador)\n",
    "instancia_2.metodo_1()\n",
    "print(instancia_2.contador)\n",
    "instancia_2.metodo_2()\n",
    "print(instancia_2.contador)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Es decir, las clases abstractas no son moldes de clases vacías necesariamente, sino que pueden contener mucha lógica que heredar a sus subclases."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejemplo - modelando clase `Personaje`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el siguiente ejemplo se usa una clase abstracta para modelar comportamiento común, pero delegando implementaciones específicas a subclases. \n",
    "\n",
    "Se desea modelar entidades de personajes de un juego, donde existe el personaje del jugador y el personaje del enemigo. Ambas entidades son similares, ya que poseen atributos y comportamiento similar, pero tienen sutiles diferencias. Por ejemplo, estos personajes al moverse pierden energía. Pero los enemigos pierden energía a una tasa constante, mientras que el jugador pierde una cantidad aleatoria e incluso tiene probabilidad de ganar un poco de energía. El uso de clases abstractas es perfecto en este escenario, ya que ambas entidades en lo general se comportan igual, pero tienen sutiles diferencias que pueden delegarse a implementación."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La clase abstracta `Personaje` modela el comportamiento de un personaje de un juego, que posee `nombre`, ubicación en un mapa (`x` e `y`), y `energia` numérica. Posee un método `simular` que realiza las acciones de cualquier personaje, que son `saludar`, `moverse` y gastar energía cada un segundo, mientras aún posea energía. Como los detalles de cómo saludar, cómo moverse y cómo se gasta energía dependen de cada implementación, se marcan como *abstract methods*. Incluso para el caso de saludar se provee una implementación base, pero al marcarse como *abstract method* se obliga a que la subclase haga explícito el comportamiento."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from random import randint\n",
    "from time import sleep\n",
    "\n",
    "\n",
    "class Personaje(ABC):\n",
    "\n",
    "    def __init__(self, nombre, x, y, energia):\n",
    "        self.nombre = nombre\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "        self.energia = energia\n",
    "\n",
    "    def simular(self):\n",
    "        while self.energia > 0:\n",
    "            sleep(1)\n",
    "            self.saludar()\n",
    "            self.moverse()\n",
    "            self.gastar_energia()\n",
    "        print(\"Perdí toda mi energía :(\")\n",
    "\n",
    "    @abstractmethod\n",
    "    def moverse(self):\n",
    "        pass\n",
    "\n",
    "    @abstractmethod\n",
    "    def gastar_energia(self):\n",
    "        pass\n",
    "\n",
    "    @abstractmethod\n",
    "    def saludar(self):\n",
    "        print(f\"Soy {self.nombre}. Estoy en {(self.x, self.y)}.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con la base creada, podemos definir un caso más específico. Primero definimos el personaje de jugador con la clase `Jugador`. Mediante herencia se comparten todas las definiciones de métodos regulares, como el constructor y *properties*. Solo queda implementar los métodos abstractos:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Jugador(Personaje):\n",
    "\n",
    "    def moverse(self):\n",
    "        # El jugador se mueve en la misma dirección de forma constante\n",
    "        self.x += 1\n",
    "        self.y += 1\n",
    "\n",
    "    def gastar_energia(self):\n",
    "        # Pierde una cantidad aleatoria de energía\n",
    "        cambio = randint(-1, 3)\n",
    "        self.energia -= cambio\n",
    "        self.energia = max(0, self.energia)            ## Energia no puede ser negativa\n",
    "        if cambio < 0: # Puede que gane energía de vez en cuando\n",
    "            print(\"¡Gané energía!\")\n",
    "\n",
    "    def saludar(self):\n",
    "        # Utiliza la definición de Personaje para saludar\n",
    "        super().saludar()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Luego, al instanciarse, podemos ver que funciona y utiliza el molde definido en `Personaje`, pero aterrizado a las implementaciones de `Jugador`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "jugador = Jugador(\"Javiera\", 0, 2, 10)\n",
    "jugador.simular()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora definimos otra subclase, que tiene otros casos específicos de implementación, para los enemigos del juego."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Enemigo(Personaje):\n",
    "\n",
    "    def moverse(self):\n",
    "        # Se mueven aleatoriamente por el mapa\n",
    "        self.x += randint(-1, 1)\n",
    "        self.y += randint(-1, 1)\n",
    "\n",
    "    def gastar_energia(self):\n",
    "        # Gastan energía a tasa constante\n",
    "        self.energia -= 1\n",
    "        self.energia = max(0, self.energia)\n",
    "\n",
    "    def saludar(self):\n",
    "        # Agrega un grito por sobre la implementación original\n",
    "        print(\"¡Te atraparé!\")\n",
    "        super().saludar()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "enemigo = Enemigo(\"Nicolás\", 0, 0, 15)\n",
    "enemigo.simular()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejemplo - modelando figuras geométricas\n",
    "\n",
    "Vamos a modelar un ejemplo de figuras geométricas. En este caso, aplicando clase abstractas para forzar la definición de ciertos métodos. En este caso, que toda figura geométrica debe tener una forma de obtener el área y el perímetro."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import math\n",
    "from abc import ABC, abstractmethod\n",
    "\n",
    "class Figura(ABC):\n",
    "    \n",
    "    @abstractmethod\n",
    "    def __init__(self):\n",
    "        pass\n",
    "\n",
    "    @abstractmethod\n",
    "    def area(self):\n",
    "        pass\n",
    "    \n",
    "    @abstractmethod\n",
    "    def perimetro(self):\n",
    "        pass"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora si creamos un `Cuadrado` que hereda de `Figura` e instanciamos un objeto de dicha clase...."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Cuadrado(Figura):\n",
    "    def __init__(self, lado: float) -> None:\n",
    "        super().__init__()\n",
    "        self._lado = lado"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ejemplo = Cuadrado(10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Podemos notar que nos genera un error porque justamente no redefinimos los método `area` y `perímetro`. Vamos a generar estas clases con los 2 métodos solicitados."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Cuadrado(Figura):\n",
    "    def __init__(self, lado: float) -> None:\n",
    "        super().__init__()\n",
    "        self.lado = lado\n",
    "\n",
    "    def area(self):\n",
    "        return math.pow(self.lado, 2)\n",
    "\n",
    "    def perimetro(self):\n",
    "        return 4 * self.lado"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ejemplo_1 = Cuadrado(5)\n",
    "print(f\"Lado: {ejemplo_1.lado}\")\n",
    "print(f\"Area: {ejemplo_1.area()}\")\n",
    "print(f\"Perimetro: {ejemplo_1.perimetro()}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Circulo(Figura):\n",
    "    def __init__(self, radio: float) -> None:\n",
    "        super().__init__()\n",
    "        self.radio = radio\n",
    "\n",
    "    def area(self):\n",
    "        return math.pi * math.pow(self.radio, 2)\n",
    "\n",
    "    def perimetro(self):\n",
    "        return 2 * math.pi * self.radio"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ejemplo_2 = Circulo(4)\n",
    "print(f\"Radio: {ejemplo_2.radio}\")\n",
    "print(f\"Area: {ejemplo_2.area()}\")\n",
    "print(f\"Perimetro: {ejemplo_2.perimetro()}\")"
   ]
  }
 ],
 "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.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
