{
 "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; 2025-1, 2026-1 Equipo Docente IIC2233. Todos los derechos reservados.</font>\n",
    "</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Tabla de contenidos\n",
    "1. [Multiherencia](#multiherencia)\n",
    "    1. [Diferencias](#diferencias-)\n",
    "    2. [Ventajas](#ventajas-)\n",
    "    2. [Desafíos](#desafíos-)\n",
    "    2. [Uso adecuado](#uso-adecuado-)\n",
    "2. [Conflictos de la multiherencia](#conflictos-de-la-multiherencia)\n",
    "    1. [Resolución del conflicto](#resolución-del-conflicto)\n",
    "    2. [El problema del diamante](#el-problema-del-diamante)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Multiherencia\n",
    "\n",
    "Como vimos anteriormente, la herencia es un mecanismo en la programación orientada a objetos (OOP) que permite definir una clase “base” (o superclase) que contiene atributos y comportamientos generales, y a partir de ella crear clases “derivadas” (o subclase) que extienden ese comportamiento. Esto implica que la modelación de la subclase es una forma especializada de la superclase. Por ejemplo, un “Perro” es un tipo de “Animal”. Sin embargo, esto no puede decirse si lo planteamos al revés, ya que \"Animal\" no es un tipo de \"Perro\". \n",
    "\n",
    "Las herencias van sucedidas hacia abajo en una cadena, y pueden existir varias clases que sean subclases de una misma superclase. Por ejemplo:\n",
    "\n",
    "```python\n",
    "class Animal:\n",
    "    pass\n",
    "\n",
    "class Perro(Animal):\n",
    "    pass\n",
    "\n",
    "class Terrier(Perro):\n",
    "    pass\n",
    "\n",
    "class ScottishTerrier(Terrier):\n",
    "    pass\n",
    "\n",
    "class BullTerrier(Terrier):\n",
    "    pass\n",
    "\n",
    "class YorkshireTerrier(Terrier):\n",
    "    pass\n",
    "```\n",
    "\n",
    "En este caso, tenemos que `Terrier` es un extenso grupo de razas de `Perro`, que además contiene dentro de ella muchas subrazas cada una de ellas con diferentes características. \n",
    "\n",
    "![Herencia](img/diagrama_herencia.jpeg)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "¿Qué pasaría si es que quisiéramos modelar un problema más complejo, en donde necesitara heredar de más de una superclase? Tal como es posible que una subclase herede datos y comportamiento de una superclase, también es posible heredar de más de una clase a la vez. Esto se conoce en OOP como **multiherencia**.\n",
    "```python\n",
    "class Docente:\n",
    "    # Métodos y atributos propios de la docencia\n",
    "    pass\n",
    "\n",
    "class Investigador:\n",
    "    # Métodos y atributos propios de la investigación\n",
    "    pass\n",
    "\n",
    "class Academico(Docente, Investigador):\n",
    "    # Combinación de comportamientos de Docente e Investigador\n",
    "    pass\n",
    "```\n",
    "\n",
    "En este caso, una clase es capaz de heredar de más de una superclase. La clase `Academico` hereda las características tanto de `Docente` como de `Investigador`, lo que le permite desempeñar ambos roles. La **multiherencia** permite mezclar funcionalidades de distintas jerarquías, pero introduce complejidades adicionales en cuanto a la resolución de métodos y la gestión de estados compartidos.\n",
    "\n",
    "![Multiherencia](img/diagrama_multiherencia.jpeg)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Diferencias ❓\n",
    "- **Herencia simple**: Se sigue una única línea de herencia (como en el ejemplo anterior de las clases Animal, Perro, Terrier, etc.).\n",
    "- **Multiherencia**: Introduce una estructura de árbol más compleja, donde una clase derivada tiene varias \"ramas\" de comportamiento a considerar."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Ventajas 🚀\n",
    "- **Flexibilidad y reutilización:** Permite construir clases que integren funcionalidades diversas sin necesidad de reescribir código.\n",
    "- **Representación de múltiples roles:** Es útil cuando un objeto debe cumplir con varios papeles o responsabilidades (como el académico que es a la vez docente e investigador)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Desafíos 😵‍💫\n",
    "- **Ambigüedades y conflictos:** Cuando las clases base tienen métodos o atributos con el mismo nombre, puede surgir el problema de ambigüedad (por ejemplo, ¿qué método se debe ejecutar?).\n",
    "- **Orden de Resolución de Métodos (MRO):** En Python se usa el MRO para decidir el orden en que se buscan los métodos en la jerarquía, lo cual es fundamental para resolver conflictos. Veremos esto con mayor profundidad en la siguiente sección.\n",
    "- **Complejidad:** El uso excesivo o inadecuado de la multiherencia puede complicar el mantenimiento y la comprensión del diseño del código."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Uso Adecuado: 🤗\n",
    "- Se recomienda la multiherencia para escenarios donde un objeto necesita cumplir roles múltiples de forma natural (por ejemplo, un académico que enseña e investiga).\n",
    "- Se recomienda utilizar la multiherencia solo cuando realmente aporte claridad y simplifique el diseño, y no como una solución para forzar la reutilización de código.\n",
    "- En casos de conflictos o ambigüedades, se debe tener claro el orden de resolución (MRO) o incluso considerar alternativas como la composición para mantener la claridad del diseño."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Conflictos de la multiherencia\n",
    "\n",
    "Imagina que tienes dos clases:\n",
    "\n",
    "- `Docente`: Representa a un profesor y define un método `publicar()` que indica, por ejemplo, la publicación de un artículo relacionado con la docencia.\n",
    "- `Investigador`: Representa a un investigador y define su propia versión de `publicar()` para publicaciones científicas.\n",
    "\n",
    "Luego, creamos una clase `Academico` que hereda de ambas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Docente:\n",
    "\n",
    "    def publicar(self):\n",
    "        print(\"Publicación orientada a la docencia\")\n",
    "\n",
    "class Investigador:\n",
    "\n",
    "    def publicar(self):\n",
    "        print(\"Publicación orientada a la investigación\")\n",
    "\n",
    "class Academico(Docente, Investigador):\n",
    "    \n",
    "    pass\n",
    "\n",
    "a = Academico()\n",
    "a.publicar()  # ¿Cuál se ejecuta?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Resolución del conflicto\n",
    "Cuando se llama a `a.publicar()`, el intérprete de Python debe determinar cuál de los dos métodos `publicar()` utilizar. Para ello, se sigue el [Orden de Resolución de Métodos](https://docs.python.org/3/howto/mro.html) (MRO, por sus siglas en inglés). \n",
    "\n",
    "Conceptualmente, el proceso es el siguiente:\n",
    "\n",
    "1. **Búsqueda en la clase derivada:**\n",
    "Primero se busca el método en la propia clase `Academico`. Como no se ha definido una versión de `publicar()` en `Academico`, se pasa al siguiente paso.\n",
    "\n",
    "2. **Búsqueda en la primera clase base:**\n",
    "Se recorre la lista de superclases en el orden en que se declararon. En la definición `class Academico(Docente, Investigador):`, `Docente` es la primera clase listada. Dado esto, Python revisa si `Docente` tiene el método `publicar()`.\n",
    "En este caso, `Docente.publicar()` existe, por lo que se utiliza este método.\n",
    "\n",
    "3. **Búsqueda en las clases restantes (si fuera necesario):**\n",
    "Si `Docente` no hubiera definido `publicar()`, Python habría continuado la búsqueda en `Investigador`. Esto muestra que el orden en la definición de las clases base influye directamente en qué método se selecciona.\n",
    "\n",
    "📚 *La estrategia de resolución se basa en el algoritmo de **[C3 Linearization](https://en.wikipedia.org/wiki/C3_linearization)**, que asegura que la jerarquía de clases se mantenga consistente y predecible. Esto significa que la primera clase base tiene prioridad sobre las siguientes en caso de conflictos de métodos.*"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Usando este mismo ejemplo, invirtamos el orden en que se entregan las clases base a la nueva subclase."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Academico(Investigador, Docente):\n",
    "    \n",
    "    pass\n",
    "\n",
    "a = Academico()\n",
    "a.publicar()  # Ahora se ejecuta Investigador.publicar()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con esta modificación, la búsqueda iniciaría en `Investigador` y se utilizaría su método `publicar()`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Este ejemplo muestra de forma clara que en la multiherencia:\n",
    "\n",
    "- El orden de las clases base en la definición es crucial para la resolución de métodos.\n",
    "- El intérprete de Python busca primero en la clase derivada, luego en la primera clase base, y así sucesivamente.\n",
    "- Entender el MRO ayuda a evitar comportamientos inesperados y a diseñar sistemas con herencia múltiple de manera segura."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### El Problema del diamante\n",
    "\n",
    "Imagina un sistema de clases que modela vehículos, cuya característica común es que todos son capaces de acelerar:\n",
    "\n",
    "- `Vehiculo` es la clase base.\n",
    "- `Terrestre` hereda de `Vehiculo`.\n",
    "- `Acuatico` hereda de `Vehiculo`.\n",
    "- `Anfibio` hereda de `Terrestre` y `Acuatico`.\n",
    "\n",
    "```python\n",
    "class Vehiculo:\n",
    "\n",
    "    def acelerar(self):\n",
    "        pass\n",
    "\n",
    "class Terrestre(Vehiculo):\n",
    "\n",
    "    def acelerar(self):\n",
    "        pass\n",
    "\n",
    "class Acuatico(Vehiculo):\n",
    "\n",
    "    def acelerar(self):\n",
    "        pass\n",
    "\n",
    "class Anfibio(Terrestre, Acuatico):\n",
    "    \n",
    "    def acelerar(self):\n",
    "        pass\n",
    "```\n",
    "\n",
    "![Diamante](img/diagrama_diamante.jpeg)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tenemos una jerarquía de diamante cada vez que tenemos más de un \"camino\" en la jerarquía desde la clase inferior a una clase superior. En muchos casos esto genera comportamientos duplicados o inesperados. Por ejemplo, si esa funcionalidad en la clase base incrementa un contador, se estaría incrementando dos veces. O si guarda algo en un registro, se duplicarían los registros.\n",
    "\n",
    "Veamos qué ocurre cuando llamamos al método `acelerar()` en la última subclase `Anfibio`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Vehiculo:\n",
    "\n",
    "    num_llamadas_vehiculo = 0\n",
    "\n",
    "    def acelerar(self):\n",
    "        print(\"Acelerando desde Vehiculo...\")\n",
    "        self.num_llamadas_vehiculo += 1\n",
    "\n",
    "class Terrestre(Vehiculo):\n",
    "\n",
    "    num_llamadas_terrestre = 0\n",
    "    \n",
    "    def acelerar(self):\n",
    "        print(\"Iniciando aceleración como Terrestre...\")\n",
    "        Vehiculo.acelerar(self)\n",
    "        print(\"Finalizando aceleración como Terrestre...\")\n",
    "        self.num_llamadas_terrestre += 1\n",
    "\n",
    "class Acuatico(Vehiculo):\n",
    "\n",
    "    num_llamadas_acuatico = 0\n",
    "\n",
    "    def acelerar(self):\n",
    "        print(\"Iniciando aceleración como Acuático...\")\n",
    "        Vehiculo.acelerar(self)\n",
    "        print(\"Finalizando aceleración como Acuático...\")\n",
    "        self.num_llamadas_acuatico += 1\n",
    "\n",
    "class Anfibio(Terrestre, Acuatico):\n",
    "    \n",
    "    num_llamadas_anfibio = 0\n",
    "\n",
    "    def acelerar(self):\n",
    "        print(\"Iniciando aceleración como Anfibio...\")\n",
    "        Terrestre.acelerar(self)\n",
    "        Acuatico.acelerar(self)\n",
    "        print(\"Finalizando aceleración como Anfibio...\")\n",
    "        self.num_llamadas_anfibio += 1\n",
    "\n",
    "a = Anfibio()\n",
    "a.acelerar()\n",
    "\n",
    "print()\n",
    "print(f\"Llamadas en Anfibio: {a.num_llamadas_anfibio}\")\n",
    "print(f\"Llamadas en Terrestre: {a.num_llamadas_terrestre}\")\n",
    "print(f\"Llamadas en Acuático: {a.num_llamadas_acuatico}\")\n",
    "print(f\"Llamadas en Vehiculo (base): {a.num_llamadas_vehiculo}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si seguimos la lógica de llamadas a los métodos, podemos ver cómo `Vehiculo.acelerar()` se llama dos veces (una por cada rama del diamante). Eso puede ser indeseado y, en programas reales, ocasionar efectos secundarios peligrosos (por ejemplo, inicializar dos veces el mismo objeto).\n",
    "\n",
    "Veamos paso a paso cómo ocurre y qué está sucediendo:\n",
    "\n",
    "- 1. `Anfibio` tiene su propio método `acelerar`, el cual:\n",
    "    - 1.1 Llama primero a `Terrestre.acelerar(self)`.\n",
    "    - 1.2 Después llama a `Acuatico.acelerar(self)`.\n",
    "- 2. Dentro de `Terrestre` y `Acuatico`, cada uno llama directamente al método de la clase base (`Vehiculo`).\n",
    "- 3. Al llamar a `Terrestre.acelerar(self)`, el contador de la clase `Vehiculo` se incrementa una vez. Posteriormente, al llamar también a `Acuatico.acelerar()`, el contador vuelve a incrementarse, quedando dos invocaciones a `Vehiculo.acelerar()`.\n",
    "\n",
    "A nivel conceptual, se han realizado dos llamadas a la misma funcionalidad base cuando quizá solo queríamos una. Eso puede ser problemático si, por ejemplo, el método de `Vehiculo` hiciese algo como “gastar combustible”, “registrar eventos” o “inicializar conexiones”; terminaríamos duplicando operaciones que esperábamos sucedieran una sola vez. Este es el conocido “problema del diamante”, que en resumen consiste en que, en una herencia múltiple, una misma clase base puede ser invocada más de una vez al llegar a la clase final, si no se maneja correctamente la cadena de llamadas.\n",
    "\n",
    "**Podrás encontrar una solución a este problema en el Notebook de bonus \"bonus-multiherencia-en-python.ipynb\", así como varios códigos complementarios.**"
   ]
  }
 ],
 "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
}
