{
 "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. [Multiherencia en Python](#multiherencia-en-python)\n",
    "    1. [Solucionando el problema del diamante](#solucionando-el-problema-del-diamante)\n",
    "        1. [Solución](#solución)\n",
    "        2. [Obteniendo el orden de herencia: el método `__mro__` ](#obteniendo-el-orden-de-herencia-el-método-__mro__)\n",
    "    2. [Ejemplo multiherencia: clase `Academico`](#ejemplo-multiherencia-clase-academico)\n",
    "        1. [Solución: uso de `*args` y `**kwargs`](#solución-uso-de-args-y-kwargs)\n",
    "        2. [Ejemplo: solución para clase `Académico`](#ejemplo-solución-para-clase-académico)\n",
    "    3. [Ejemplo multiherencia: clase `Avatar`](#ejemplo-multiherencia-clase-avatar)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Multiherencia en Python\n",
    "\n",
    "Como vimos en el Notebook \"3-multiherencia.ipynb\" , 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",
    "\n",
    "Volvamos a ver la clase `Academico`. Un académico posee dos roles: investigador y docente, por lo tanto en nuestro modelo reflejaremos este hecho haciendo que `Academico` herede de las clases ya existentes `Investigador` y `Docente`. Una manera **correcta** de implementar esto en Python sería:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Investigador:\n",
    "\n",
    "    def __init__(self, area='', **kwargs):\n",
    "        # Utilizamos super() para heredar correctamente\n",
    "        super().__init__(**kwargs)\n",
    "        self.area = area\n",
    "        self.num_publicaciones = 0\n",
    "\n",
    "\n",
    "class Docente:\n",
    "\n",
    "    def __init__(self, departamento='', **kwargs):\n",
    "        # Utilizamos super() para heredar correctamente\n",
    "        super().__init__(**kwargs)\n",
    "        self.departamento = departamento\n",
    "        self.num_cursos = 3\n",
    "\n",
    "# Aquí decimos que Academico hereda tanto de Docente como de Investigador\n",
    "class Academico(Docente, Investigador):\n",
    "    \n",
    "    def __init__(self, nombre, oficina, **kwargs):\n",
    "        # Utilizamos super() para heredar correctamente\n",
    "        super().__init__(**kwargs)\n",
    "        self.nombre = nombre\n",
    "        self.oficina = oficina\n",
    "\n",
    "\n",
    "p1 = Academico(\n",
    "    \"Emilia Donoso\",\n",
    "    oficina=\"O5\",\n",
    "    area=\"Inteligencia de Máquina\",\n",
    "    departamento=\"Ciencia De La Computación\"\n",
    ")\n",
    "print(p1.nombre)\n",
    "print(p1.area)\n",
    "print(p1.departamento)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Podemos ver que para esta la implementación estamos utilizando tanto `super()` como `**kwargs` de maneras que no hemos visto antes y que serán explicadas a lo largo de este notebook, pero para llegar a esto, comenzaremos con una **implementación incorrecta**, pero que usa solamente los contenidos vistos hasta el momento, y la repararemos paso a paso. Esta implementación es:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Investigador:\n",
    "\n",
    "    def __init__(self, area):\n",
    "        self.area = area\n",
    "        self.num_publicaciones = 0\n",
    "\n",
    "\n",
    "class Docente:\n",
    "\n",
    "    def __init__(self, departamento):\n",
    "        self.departamento = departamento\n",
    "        self.num_cursos = 3\n",
    "        \n",
    "\n",
    "class Academico(Docente, Investigador):\n",
    "    \n",
    "    def __init__(self, nombre, oficina, area_investigacion, departamento):\n",
    "        # Esta es la parte incorrecta, pero sigue los contenidos de herencia\n",
    "        Investigador.__init__(self, area_investigacion)\n",
    "        Docente.__init__(self, departamento)\n",
    "        self.nombre = nombre\n",
    "        self.oficina = oficina\n",
    "\n",
    "        \n",
    "p1 = Academico(\"Emilia Donoso\", \"O-5\", \"Inteligencia de Máquina\", \"Ciencia De La Computación\")\n",
    "print(p1.nombre)\n",
    "print(p1.area)\n",
    "print(p1.departamento)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este ejemplo, como la clase `Academico` hereda tanto de `Docente` como de `Investigador`, parece natural llamar a ambos métodos `__init()__` de cada una de sus clases superiores. Sin embargo, esto provoca problemas en modelos más complejos, particularmente el **problema del diamante**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Solucionando el problema del diamante\n",
    "\n",
    "Recordemos que tendremos 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.\n",
    "\n",
    "Veamos qué ocurre cuando llamamos al método `llamar()` en ambas superclases desde una clase `SubClaseA`, **sin utilizar `super()`**."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ClaseB:\n",
    "    \n",
    "    num_llamadas_B = 0\n",
    "    \n",
    "    def llamar(self):\n",
    "        print(\"Iniciando llamada al metodo en Clase B...\")\n",
    "        print(\"Finalizando llamada al metodo en Clase B...\")\n",
    "        self.num_llamadas_B += 1\n",
    "\n",
    "\n",
    "class SubClaseIzquierda(ClaseB):\n",
    "    \n",
    "    num_llamadas_izq = 0\n",
    "    \n",
    "    def llamar(self):\n",
    "        print(\"Iniciando llamada al metodo en Subclase Izquierda...\")\n",
    "        ClaseB.llamar(self)\n",
    "        print(\"Finalizando llamada al metodo en Subclase Izquierda...\")\n",
    "        self.num_llamadas_izq += 1\n",
    "\n",
    "\n",
    "class SubClaseDerecha(ClaseB):\n",
    "    \n",
    "    num_llamadas_der = 0\n",
    "    \n",
    "    def llamar(self):\n",
    "        print(\"Iniciando llamada al metodo en Subclase Derecha...\")\n",
    "        ClaseB.llamar(self)\n",
    "        print(\"Finalizando llamada al metodo en Subclase Derecha...\")\n",
    "        self.num_llamadas_der += 1\n",
    "\n",
    "\n",
    "class SubClaseA(SubClaseIzquierda, SubClaseDerecha):\n",
    "    \n",
    "    num_llamadas_subA = 0\n",
    "    \n",
    "    def llamar(self):\n",
    "        print(\"Iniciando llamada al metodo en Subclase A...\")        \n",
    "        SubClaseIzquierda.llamar(self)\n",
    "        SubClaseDerecha.llamar(self)\n",
    "        print(\"Finalizando llamada al metodo en Subclase A...\")\n",
    "        self.num_llamadas_subA += 1\n",
    "\n",
    "\n",
    "s = SubClaseA()\n",
    "s.llamar()\n",
    "print()\n",
    "print(f\"Llamadas en Subclase A: {s.num_llamadas_subA}\")\n",
    "print(f\"Llamadas en Subclase Izquierda: {s.num_llamadas_izq}\")\n",
    "print(f\"Llamadas en Subclase Derecha: {s.num_llamadas_der}\")\n",
    "print(f\"Llamadas en Clase B: {s.num_llamadas_B}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Podemos apreciar que el método `llamar` de la clase de más arriba en la jerarquía (`ClaseB`) fue llamada dos veces. Luego de cada ejecución de `llamar`, la secuencia de invocaciones sube por la jerarquía hasta el método correspondiente en `ClaseB`.\n",
    "\n",
    "La estructura de jerarquía en forma de diamante ocurre **siempre** que tengamos una clase que hereda de dos clases, aun cuando no tengamos una tercera superclase explícita. ¿Por qué? Porque en Python (y en varios lenguajes OOP), existe una clase superior [`object`](https://docs.python.org/3.6/library/functions.html#object) de la cual heredan **todas** las clases que creamos. \n",
    "\n",
    "En particular, cuando se utiliza multiherencia, podemos crear un diagrama, para visualizar estos casos, que se vería de la siguiente forma:\n",
    "\n",
    "![Diamante2](img/diamante_2_small.png)\n",
    "\n",
    "De esta manera, si, estando en un objeto de `SubClase`, llamamos al método `__init__` tanto de `ClaseA` como de `ClaseB`, estaríamos inicializando dos veces la clase `object`. Eso es precisamente lo que ocurre en el ejemplo de clase `Academico`, la cual llama explícitamente al inicializador de `Investigador` y de `Docente`, lo que tiene como consecuencia que la clase `object` se inicializa dos veces.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Solución\n",
    "\n",
    "La solución es que cada clase debe preocuparse de llamar a inicializar a la clase que la \"precede\" en el orden del esquema de la multiherencia, lo que puede hacerse llamando a `super()`. En Python, cada jerarquía posee un orden predefinido por la construcción de la jerarquía. El orden de las clases va **de izquierda a derecha** dentro de la lista de superclases desde donde hereda la subclase. \n",
    "\n",
    "En el siguiente ejemplo, basado en la primera jerarquía de diamante que presentamos, sólo debemos preocuparnos de hacer una llamada a `super()`. Python se encargará de que la llamada corresponda a la clase que respeta el orden en la multiherencia."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ClaseB:\n",
    "    \n",
    "    num_llamadas_B = 0\n",
    "    \n",
    "    def llamar(self):\n",
    "        print(\"Iniciando llamada al metodo en Clase B...\")\n",
    "        print(\"Finalizando llamada al metodo en Clase B...\")\n",
    "        self.num_llamadas_B += 1\n",
    "\n",
    "\n",
    "class SubClaseIzquierda(ClaseB):\n",
    "    \n",
    "    num_llamadas_izq = 0\n",
    "    \n",
    "    def llamar(self):\n",
    "        print(\"Iniciando llamada al metodo en Subclase Izquierda...\")\n",
    "        super().llamar()\n",
    "        print(\"Finalizando llamada al metodo en Subclase Izquierda...\")\n",
    "        self.num_llamadas_izq += 1\n",
    "\n",
    "\n",
    "class SubClaseDerecha(ClaseB):\n",
    "    \n",
    "    num_llamadas_der = 0\n",
    "    \n",
    "    def llamar(self):\n",
    "        print(\"Iniciando llamada al metodo en Subclase Derecha...\")\n",
    "        super().llamar()\n",
    "        print(\"Finalizando llamada al metodo en Subclase Derecha...\")\n",
    "        self.num_llamadas_der += 1\n",
    "\n",
    "\n",
    "class SubClaseA(SubClaseIzquierda, SubClaseDerecha):\n",
    "    \n",
    "    num_llamadas_subA = 0\n",
    "    \n",
    "    def llamar(self):\n",
    "        print(\"Iniciando llamada al metodo en Subclase A...\")\n",
    "        super().llamar()\n",
    "        print(\"Finalizando llamada al metodo en Subclase A...\")\n",
    "        self.num_llamadas_subA += 1\n",
    "\n",
    "\n",
    "s = SubClaseA()\n",
    "s.llamar()\n",
    "print()\n",
    "print(f\"Llamadas en Subclase A: {s.num_llamadas_subA}\")\n",
    "print(f\"Llamadas en Subclase Izquierda: {s.num_llamadas_izq}\")\n",
    "print(f\"Llamadas en Subclase Derecha: {s.num_llamadas_der}\")\n",
    "print(f\"Llamadas en Clase B: {s.num_llamadas_B}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Podemos ver que esta vez estamos llamando solamente una vez al método `llamar` de `ClaseB`. Es más, si bien el diamante sigue existiendo, esta vez bastó ejecutar solo una vez el método `llamar` de `SubclaseA`, y conseguimos ejecutar el método `llamar` de todas las clases, y solamente una vez cada uno. \n",
    "\n",
    "Si ponemos atención a la secuencia de llamados, pareciera que hay un _orden_ impuesto entre las clases que componen el diamante. Este orden es: primero `SubClaseA`, luego `SubClaseIzquierda`, a continuación `SubClaseDerecha`, y finalmente `ClaseB`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Obteniendo el orden de herencia: el método `__mro__` \n",
    "\n",
    "La solución para determinar en qué orden se ejecutan los métodos en un esquema de multiherencia, se estableció mediante un algoritmo llamado [**C3**](https://www.python.org/download/releases/2.3/mro/) que permite calcular un orden lineal entre las clases que participan del esquema. Este algoritmo puede ser ejecutado por todas las clases de Python usando el método predefinido **`__mro__`**, cuyo nombre viene de _method resolution order_. Este método nos muestra el orden en la jerarquía de clases a partir de la clase actual. Es útil para casos de multiherencia complejos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "SubClaseA.__mro__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notemos que el resultado de `__mro__` depende de la clase a la cual se aplica. Si lo aplicamos a `SubClaseIzquierda`, entonces su MRO no incluye a `SubClaseDerecha` pues ésta no es parte de su jerarquía."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "SubClaseIzquierda.__mro__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora bien, **no toda estructura de multiherencia está permitida**. No es tan difícil armar una jerarquía en que no sea posible armar un MRO consistente para todas las clases. Por ejemplo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class X():\n",
    "    def call_me(self):\n",
    "        print(\"soy X\")\n",
    "    \n",
    "class Y():\n",
    "    def call_me(self):\n",
    "        print(\"soy Y\")\n",
    "    \n",
    "class A(X, Y):\n",
    "    def call_me(self):\n",
    "        print(\"soy A\")\n",
    "    \n",
    "class B(Y, X):\n",
    "     def call_me(self):\n",
    "        print(\"soy B\")\n",
    "\n",
    "class F(A, B):\n",
    "    def call_me(self):\n",
    "        print(\"soy F\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este ejemplo, tanto `A` como `B` heredan de `X` e `Y` pero en distinto orden. Esto no es un problema hasta que se define la clase `F` que hereda de `A` y de `B`. En este momento Python prohíbe la creación de la clase pues no puede determinar un MRO consistente para llegar a `X` e `Y`.\n",
    "\n",
    "Para este caso, basta modificar `A` y `B` para que hereden de la misma manera, y ahora sí se puede determinar un MRO."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class X():\n",
    "    def call_me(self):\n",
    "        print(\"soy X\")\n",
    "    \n",
    "class Y():\n",
    "    def call_me(self):\n",
    "        print(\"soy Y\")\n",
    "    \n",
    "class A(X, Y):\n",
    "    def call_me(self):\n",
    "        print(\"soy A\")\n",
    "    \n",
    "class B(X, Y):\n",
    "     def call_me(self):\n",
    "        print(\"soy B\")\n",
    "\n",
    "class F(A, B):\n",
    "    def call_me(self):\n",
    "        print(\"soy F\")\n",
    "        \n",
    "print(F.__mro__)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejemplo multiherencia: clase `Academico`\n",
    "\n",
    "Volviendo al ejemplo del académico que presentamos al inicio del tema de multiherencia, si bien el segundo código parece funcionar bien, ahora sabemos que si llamamos por separado a cada inicializador de las superclases, **estamos llamando dos veces al inicializador** de `object`, y no queremos que esto ocurra.\n",
    "\n",
    "El código anterior era así:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Investigador:\n",
    "\n",
    "    def __init__(self, area):\n",
    "        self.area = area\n",
    "        # self.num_publicaciones = 0\n",
    "        self.num_cursos = 3\n",
    "\n",
    "\n",
    "class Docente:\n",
    "\n",
    "    def __init__(self, departamento):\n",
    "        self.departamento = departamento\n",
    "        self.num_cursos = 0\n",
    "\n",
    "\n",
    "class Academico(Docente, Investigador):\n",
    "\n",
    "    def __init__(self, nombre, oficina, area_investigacion, departamento):\n",
    "        # Queremos reemplazar esto por un super().__init__(...)\n",
    "        Investigador.__init__(self, area_investigacion)\n",
    "        Docente.__init__(self, departamento)\n",
    "        self.nombre = nombre\n",
    "        self.oficina = oficina\n",
    "\n",
    "\n",
    "p1 = Academico(\"Emilia Donoso\", \"O-5\", \"Inteligencia de Máquina\", \"Ciencia De La Computación\")\n",
    "print(p1.nombre)\n",
    "print(p1.area)\n",
    "print(p1.departamento)\n",
    "print(p1.num_cursos)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La solución, tal como lo hicimos con el método `llamar()` de la sección anterior, debería ser reemplazar las inicializaciones explícitas de `Investigador` y `Docente`, por un único llamado a `super().__init__()`, y así dejamos que MRO haga su trabajo.\n",
    "\n",
    "Sin embargo, tenemos una situación levemente distinta. ¿Qué argumentos le entregamos a `super().__init__()`? Si le entregamos solamente los de `Investigador` o solamente los de `Docente`, uno de los inicializadores se quedaría sin argumentos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Investigador:\n",
    "\n",
    "    def __init__(self, area):\n",
    "        print(\"Inicializando investigador\")\n",
    "        self.area = area\n",
    "        self.num_publicaciones = 0\n",
    "\n",
    "\n",
    "class Docente:\n",
    "\n",
    "    def __init__(self, departamento):\n",
    "        print(\"Inicializando docente\")\n",
    "        self.departamento = departamento\n",
    "        self.num_cursos = 3\n",
    "\n",
    "\n",
    "class Academico(Docente, Investigador):\n",
    "\n",
    "    def __init__(self, nombre, oficina, area_investigacion, departamento):\n",
    "        # Solo un llamado, pero solo con un argumento\n",
    "        super().__init__(departamento)\n",
    "        self.nombre = nombre\n",
    "        self.oficina = oficina\n",
    "\n",
    "\n",
    "print(Academico.__mro__)\n",
    "p1 = Academico(\"Emilia Donoso\", \"O-5\", \"Inteligencia de Máquina\", \"Ciencia De La Computación\")\n",
    "print(p1.nombre)\n",
    "print(p1.area)\n",
    "print(p1.departamento)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si bien, hay un MRO definido para `Academico`, cuando solo entregamos los argumentos de un inicializador, solo se ejecuta el `__init()__` de `Docente`, y no el de `Investigador`, por lo tanto nuestro `Academico` se queda sin su atributo `area`. \n",
    "\n",
    "Entonces una mejor solución sería entregarle a `super().__init()__` _todos_ los argumentos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Investigador:\n",
    "\n",
    "    def __init__(self, area):\n",
    "        print(\"Inicializando investigador\")\n",
    "        self.area = area\n",
    "        self.num_publicaciones = 0\n",
    "\n",
    "\n",
    "class Docente:\n",
    "\n",
    "    def __init__(self, departamento):\n",
    "        print(\"Inicializando docente\")\n",
    "        self.departamento = departamento\n",
    "        self.num_cursos = 3\n",
    "\n",
    "\n",
    "class Academico(Docente, Investigador):\n",
    "\n",
    "    def __init__(self, nombre, oficina, area_investigacion, departamento):\n",
    "        # Solo un llamado, con todos los argumentos que tenemos\n",
    "        super().__init__(departamento, area_investigacion)\n",
    "        self.nombre = nombre\n",
    "        self.oficina = oficina\n",
    "\n",
    "\n",
    "print(Academico.__mro__)\n",
    "p1 = Academico(\"Emilia Donoso\", \"O-5\", \"Inteligencia de Máquina\", \"Ciencia De La Computación\")\n",
    "print(p1.nombre)\n",
    "print(p1.area)\n",
    "print(p1.departamento)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Pero esto tampoco sirve, porque cada `__init__()` recibe solamente dos argumentos y le estamos entregando tres (no olvidemos al `self`). El ejemplo inicial con el método `llamar()` era muy ~~tramposo~~ sencillo porque `llamar()` no tenía argumentos. Estamos en un dilema."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Solución: uso de `*args` y `**kwargs`\n",
    "\n",
    "El dilema que tenemos se produce porque, aunque entreguemos todos los argumentos a `super().__init__()`, ninguno de los inicializadores sabe cuáles argumentos son para él, y cuáles para otro inicializador. Pero Python provee una solución a través de `*args` y `**kwargs`. Recordando:\n",
    "\n",
    "* `**kwargs` es una *secuencia de argumentos de largo variable*, donde cada elemento de la lista tiene asociado un ***keyword***. El `**` mapea los elementos contenidos en el diccionario `kwargs` y los pasa a la función como _argumentos no posicionales_. Esto significa que los argumentos no se asignan a la función por su posición en el orden en que se entregan (como es lo habitual) sino por su _keyword_ asociado.\n",
    "* `*args`, es una lista de argumentos de largo variable, pero sin *keywords* asociados. El operador `*` desempaqueta el contenido de args y los pasa a la función como argumentos posicionales. La función asigna valores a sus argumentos a partir del orden que trae esta lista."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Ejemplo: solución para clase `Académico`\n",
    "\n",
    "Podemos aplicar el uso de `*args` y `**kwargs` para, finalmente, implementar correctamente la inicialización en el ejemplo del académico. \n",
    "\n",
    "Recordemos el código que **queremos mejorar**:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Investigador:\n",
    "\n",
    "    def __init__(self, area):\n",
    "        self.area = area\n",
    "        self.num_publicaciones = 0\n",
    "\n",
    "\n",
    "class Docente:\n",
    "\n",
    "    def __init__(self, departamento):\n",
    "        self.departamento = departamento\n",
    "        self.num_cursos = 3\n",
    "\n",
    "\n",
    "class Academico(Docente, Investigador):\n",
    "\n",
    "    def __init__(self, nombre, oficina, area_investigacion, departamento):\n",
    "        # Queremos reemplazar esto por un super().__init__(...), pero no sabemos qué argumentos usar\n",
    "        Investigador.__init__(self, area_investigacion)\n",
    "        Docente.__init__(self, departamento)\n",
    "        self.nombre = nombre\n",
    "        self.oficina = oficina\n",
    "\n",
    "\n",
    "p1 = Academico(\"Emilia Donoso\", \"O-5\", \"Inteligencia de Máquina\", \"Ciencia De La Computación\")\n",
    "print(p1.nombre)\n",
    "print(p1.area)\n",
    "print(p1.departamento)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Deseamos reemplazar los llamados a ambos inicializadores, por una única invocación `super().__init__()`, pero no sabemos qué argumentos entregar.\n",
    "\n",
    "Aprovecharemos el hecho que `**kwargs` nos permite entregar un diccionario de argumentos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Investigador:\n",
    "\n",
    "    def __init__(self, area, **kwargs):\n",
    "        print(f\"init Investigador con area '{area}' y kwargs:{kwargs}\")\n",
    "        super().__init__(**kwargs)\n",
    "        self.area = area\n",
    "        self.num_publicaciones = 0\n",
    "\n",
    "\n",
    "class Docente:\n",
    "\n",
    "    def __init__(self, departamento, **kwargs):\n",
    "        print(f\"init Docente con depto '{departamento}' y kwargs:{kwargs}\")\n",
    "        super().__init__(**kwargs)\n",
    "        self.departamento = departamento\n",
    "        self.num_cursos = 3\n",
    "\n",
    "\n",
    "class Academico(Docente, Investigador):\n",
    "\n",
    "    def __init__(self, nombre, oficina, **kwargs):\n",
    "        print(f\"init Academico con nombre '{nombre}', oficina '{oficina}', kwargs:{kwargs}\")\n",
    "        super().__init__(**kwargs)\n",
    "        self.nombre = nombre\n",
    "        self.oficina = oficina\n",
    "\n",
    "\n",
    "print(Academico.__mro__)\n",
    "print(\"--------\")\n",
    "\n",
    "p1 = Academico(\n",
    "    \"Emilia Donoso\",\n",
    "    oficina=\"O5\",\n",
    "    area=\"I.A.\",\n",
    "    departamento=\"Computación\"\n",
    ")\n",
    "print(\"--------\")\n",
    "print(p1.nombre)\n",
    "print(p1.area)\n",
    "print(p1.departamento)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Este ejemplo permite, finalmente, llamar **una sola vez** y de manera correcta a todos los inicializadores de las clases de una jerarquía con multiherencia. El diccionario `**kwargs` contiene los argumentos identificados por su nombre, de manera que en cada llamado a un inicializador, éste extrae los _keywords_ que correspondan a algún nombre de los argumentos que espera, y el resto permanecen en el `**kwargs` y son pasados como argumento a la siguiente clase en la jerarquía. De esta manera, cada inicializador _consume_ del `**kwargs` lo que necesita. Notemos que en la última clase de la jerarquía, antes de llamar a `object`, todos los _keywords_ en `**kwargs` han sido consumidos, lo que está bien porque el inicializador de `object` no recibe argumentos.\n",
    "\n",
    "Es muy **importante** incluir los operadores `*` y `**` cuando se ocupa multiherencia y cada clase padre recibe argumentos distintos en el `__init__`.\n",
    "\n",
    "Una adecuada comprensión de los llamados a métodos/funciones usando lista variables de argumentos es muy práctica y flexible. Lo estaremos ocupando en numerosas ocasiones."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejemplo multiherencia: clase `Avatar`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A continuación vamos a modelar al `Avatar`, un maestro de los 4 elementos. Para esto, vamos a crear 4 clases `MaestroXXXX` con su método `saludar()` que permitirá identificar al maestro y un método para hacer uso de su elemento."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MaestroFuego:\n",
    "    def __init__(self, nombre: str):\n",
    "        self.nombre = nombre\n",
    "        \n",
    "    def saludar(self):\n",
    "        print(f\"Hola!! mi nombre es {self.nombre} y soy un maestro fuego\")\n",
    "        \n",
    "    def fuego_control(self):\n",
    "        print(\"¡Fuego control!\")\n",
    "\n",
    "        \n",
    "class MaestroAgua:\n",
    "    def __init__(self, nombre: str):\n",
    "        self.nombre = nombre\n",
    "        \n",
    "    def saludar(self):\n",
    "        print(f\"Hola!! mi nombre es {self.nombre} y soy un maestro agua\")\n",
    "    \n",
    "    def agua_control(self):\n",
    "        print(\"¡Agua control!\")\n",
    "\n",
    "            \n",
    "class MaestroTierra:\n",
    "    def __init__(self, nombre: str):\n",
    "        self.nombre = nombre\n",
    "        \n",
    "    def saludar(self):\n",
    "        print(f\"Hola!! mi nombre es {self.nombre} y soy un maestro tierra\")\n",
    "\n",
    "    def tierra_control(self):\n",
    "        print(\"¡Tierra control!\")\n",
    "        \n",
    "        \n",
    "class MaestroAire:\n",
    "    def __init__(self, nombre: str):\n",
    "        self.nombre = nombre\n",
    "        \n",
    "    def saludar(self):\n",
    "        print(f\"Hola!! mi nombre es {self.nombre} y soy un maestro aire\")\n",
    "        \n",
    "    def aire_control(self):\n",
    "        print(\"¡Aire control!\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora creamos a la clase `Avatar` que es un maestro en los 4 elementos. Así que hereda de los 4 maestros."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Avatar(MaestroFuego, MaestroAgua, MaestroTierra, MaestroAire):\n",
    "    def __init__(self, nombre: str):\n",
    "        super().__init__(nombre)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "el_ultimo_maestro_aire = Avatar(\"Aang\")\n",
    "el_ultimo_maestro_aire.saludar()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**🤔 ¿Qué pasó? ¿Por qué no se imprimió los 4 saludos si es el avatar?**\n",
    "\n",
    "Esto pasa porque el `Avatar` hereda de las 4 clases, y cada clase tiene el mismo método que es `saludar`. En este caso, se quedará el método **de la primera clase que heredamos**.\n",
    "\n",
    "Intentemos utilizar `super()` para que se gatille el método de sus padres."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Avatar(MaestroFuego, MaestroAgua, MaestroTierra, MaestroAire):\n",
    "    def __init__(self, nombre: str):\n",
    "        super().__init__(nombre)\n",
    "        \n",
    "    def saludar(self):\n",
    "        super().saludar()\n",
    "        \n",
    "el_ultimo_maestro_aire = Avatar(\"Aang\")\n",
    "el_ultimo_maestro_aire.saludar()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**🤔 ¿Qué pasó? ¿Por qué `super()` no solucionó esto?**\n",
    "\n",
    "Es importante destacar que `super()` llama a la clase madre según el MRO, y se asegura que esta clase madre solo sea llamada 1 vez en todas las herencias, con el fín de solucionar el problema del diamante. Por lo tanto, si heredamos de múltiples clases, y cada una tiene el mismo método, cuando invoquemos el `super()`, este tambien llamará al método de la primera clase que heredamos.\n",
    "\n",
    "\n",
    "Si exploramos el MRO de la clase `Avatar`, podremos ver el orden en el que serán llamados las clases cuando recurramos a `super()`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Avatar.__mro__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**🤔 Y ¿hay formas de que se ejecuten el saludo de los 4 maestros?**\n",
    "\n",
    "Si, una forma **NO RECOMENDADA** es agregar `super().saludar()` dentro de los 3 primeras clases `MaestroXXXXX` de las que estamos heredando, es decir, agregar `super().saludar()` en las clases `MaestroFuego`, `MaestroAgua` y `MaestroTierra`. Con esto, vamos a provocar que cada método de `saludar` llame al siguiente método `saludar()` dentro del MRO.\n",
    "\n",
    "No agregaremos `super().saludar()` dentro de la clase `MaestroAire` porque si usamos `super()`, estaríamos accediendo a la clase `object` (recordar que toda clase hereda de `object`). Y esta no tiene el método `saludar`.\n",
    "\n",
    "Esta forma no es recomendada porque estamos modificando 3 clases pequeñas (`MaestroFuego`, `MaestroAgua` y `MaestroTierra`) para que `Avatar` funcione, pero esto nos va a generar 2 problemas que explicaremos después de ver el código."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Versión 2 - Usando super() dentro de los 3 primeros maestros\n",
    "\n",
    "class MaestroFuego:\n",
    "    def __init__(self, nombre: str):\n",
    "        self.nombre = nombre\n",
    "        \n",
    "    def saludar(self):\n",
    "        super().saludar() # Agregar super() para forzar que se llame al siguiente saludar dentro del MRO\n",
    "        print(f\"Hola!! mi nombre es {self.nombre} y soy un maestro fuego\")\n",
    "        \n",
    "    def fuego_control(self):\n",
    "        print(\"¡Fuego control!\")\n",
    "\n",
    "        \n",
    "class MaestroAgua:\n",
    "    def __init__(self, nombre: str):\n",
    "        self.nombre = nombre\n",
    "        \n",
    "    def saludar(self):\n",
    "        super().saludar() # Agregar super() para forzar que se llame al siguiente saludar dentro del MRO\n",
    "        print(f\"Hola!! mi nombre es {self.nombre} y soy un maestro agua\")\n",
    "    \n",
    "    def agua_control(self):\n",
    "        print(\"¡Agua control!\")\n",
    "\n",
    "            \n",
    "class MaestroTierra:\n",
    "    def __init__(self, nombre: str):\n",
    "        self.nombre = nombre\n",
    "        \n",
    "    def saludar(self):\n",
    "        super().saludar() # Agregar super() para forzar que se llame al siguiente saludar dentro del MRO\n",
    "        print(f\"Hola!! mi nombre es {self.nombre} y soy un maestro tierra\")\n",
    "\n",
    "    def tierra_control(self):\n",
    "        print(\"¡Tierra control!\")\n",
    "        \n",
    "        \n",
    "class MaestroAire:\n",
    "    def __init__(self, nombre: str):\n",
    "        self.nombre = nombre\n",
    "        \n",
    "    def saludar(self):\n",
    "        # No agregamos super porque esto llamaría a object y esa clase no tiene saludar()\n",
    "        print(f\"Hola!! mi nombre es {self.nombre} y soy un maestro aire\")\n",
    "        \n",
    "    def aire_control(self):\n",
    "        print(\"¡Aire control!\")     \n",
    "        \n",
    "        \n",
    "class Avatar(MaestroFuego, MaestroAgua, MaestroTierra, MaestroAire):\n",
    "    def __init__(self, nombre: str):\n",
    "        super().__init__(nombre)\n",
    "        \n",
    "    def saludar(self):\n",
    "        super().saludar()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "la_sucesora_de_aang = Avatar(\"Korra\")\n",
    "la_sucesora_de_aang.saludar()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si bien logramos imprimir los 4 saludos. Surgen 2 problemas:\n",
    "\n",
    "**Problema 1**: esta solución solo funciona cuando `MaestroAire` es la última clase que heredamos. Vamos a cambiar el orden de las herencias para a ver qué imprime."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Avatar(MaestroFuego, MaestroAire, MaestroAgua, MaestroTierra):\n",
    "    def __init__(self, nombre: str):\n",
    "        super().__init__(nombre)\n",
    "        \n",
    "    def saludar(self):\n",
    "        super().saludar()\n",
    "        \n",
    "la_sucesora_de_aang = Avatar(\"Korra\")\n",
    "la_sucesora_de_aang.saludar()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esto ocurre porque primero se llama a `saludar()` del `MaestroFuego`, esta clase hace `super().saludar()` para ejecutar el método `saludar()` de la clase `MaestroAire`. Luego, este método no hace `super()` así que no se sigue llamando a los siguientes `saludar()` del MRO. Esto implica que la solución de usar `super()` solo funcionó bajo cierto orden.\n",
    "\n",
    "\n",
    "**Problema 2**: esta solución no permite que funcionen correctamente las clases `MaestroFuego`, `MaestroAgua` y `MaestroTierra`. Vamos a probar este caso instanciando un objeto de clase `MaestroFuego`. Primero veamos su MRO y luego instanciemos el objeto."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "MaestroFuego.__mro__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "maestro_fuego = MaestroFuego(\"Zuko\")\n",
    "maestro_fuego.saludar()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lo que ocurrió en este caso, es que el `super()` del `MaestroFuego` va a llamar a la siguiente clase, que es `object`, y esta clase no tiene el método saludar. Por lo tanto, agregar el `super()` para que `Avatar` funcione provocó que las demás clases con `super()` ya no funcionen correctamente.\n",
    "\n",
    "Una solución podría ser que todos los Maestros heredaran de una clase padre, por ejemplo, `Humano` y que esta tenga un método `saludar(): pass`. Con esto, los `super()` no provocarían problemas. No obstante, al final estamos creando código adicional y modificando otras clases solo para que una nueva clase, `Avatar`, funcione correctamente. \n",
    "\n",
    "Otra solución, que veremos a continuación, es prescindir del uso de `super()` para esta versión. En su reemplazo, vamos a llamar directamente a los métodos de las clases padres."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Versión 3 - No usar super en cada Maestro, pero que Avatar llame al método de cada maestro.\n",
    "\n",
    "class MaestroFuego:\n",
    "    def __init__(self, nombre: str):\n",
    "        self.nombre = nombre\n",
    "        \n",
    "    def saludar(self):\n",
    "        print(f\"Hola!! mi nombre es {self.nombre} y soy un maestro fuego\")\n",
    "        \n",
    "    def fuego_control(self):\n",
    "        print(\"¡Fuego control!\")\n",
    "\n",
    "        \n",
    "class MaestroAgua:\n",
    "    def __init__(self, nombre: str):\n",
    "        self.nombre = nombre\n",
    "        \n",
    "    def saludar(self):\n",
    "        print(f\"Hola!! mi nombre es {self.nombre} y soy un maestro agua\")\n",
    "    \n",
    "    def agua_control(self):\n",
    "        print(\"¡Agua control!\")\n",
    "\n",
    "            \n",
    "class MaestroTierra:\n",
    "    def __init__(self, nombre: str):\n",
    "        self.nombre = nombre\n",
    "        \n",
    "    def saludar(self):\n",
    "        print(f\"Hola!! mi nombre es {self.nombre} y soy un maestro tierra\")\n",
    "\n",
    "    def tierra_control(self):\n",
    "        print(\"¡Tierra control!\")\n",
    "        \n",
    "        \n",
    "class MaestroAire:\n",
    "    def __init__(self, nombre: str):\n",
    "        self.nombre = nombre\n",
    "        \n",
    "    def saludar(self):\n",
    "        print(f\"Hola!! mi nombre es {self.nombre} y soy un maestro aire\")\n",
    "        \n",
    "    def aire_control(self):\n",
    "        print(\"¡Aire control!\")\n",
    "\n",
    "        \n",
    "class Avatar(MaestroFuego, MaestroAgua, MaestroTierra, MaestroAire):\n",
    "    def __init__(self, nombre: str):\n",
    "        super().__init__(nombre)\n",
    "        \n",
    "    def saludar(self):\n",
    "        MaestroFuego.saludar(self)\n",
    "        MaestroAgua.saludar(self)\n",
    "        MaestroTierra.saludar(self)\n",
    "        MaestroAire.saludar(self)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "el_antecesor_de_aang = Avatar(\"Roku\")\n",
    "el_antecesor_de_aang.saludar()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En esta forma, fue la clase `Avatar` la encargada de llamar al método saludar de cada clase padre. De este modo, no es necesario usar `super()` y evaluar el MRO de la clase para entender el orden de ejecución, y redujimos la cantidad de código a editar para hacer funcionar esta nueva clase.\n",
    "\n",
    "\n",
    "----"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Reflexión\n",
    "\n",
    "\n",
    "Con este ejemplo, estamos viendo una de las dificultades que tiene la multiherencia, que es la escalabilidad de código y que el uso de `super()` a veces puede ser la solución al problema del diamante, pero otras veces puede generar ciertas dificultades o necesidades de modificar código ya existente. Por lo tanto, siempre es recomendable reflexionar sobre cuándo usar multiherencia y el `super()`, y cómo modelar la posible solución.\n",
    "\n",
    "De todas formas, si bien este ejemplo mostró una dificultad de la multiherencia, tambien tiene sus ventajas. Por ejemplo, que `Avatar` ganó acceso a 4 métodos que no tenía inicialmente, sino que eran de sus clases padres."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "el_antecesor_de_aang.fuego_control()\n",
    "el_antecesor_de_aang.agua_control()\n",
    "el_antecesor_de_aang.tierra_control()\n",
    "el_antecesor_de_aang.aire_control()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para finalizar esta reflexión, el último código presentado mostraba 4 clases con sus métodos de saludar y `xxxx_control()`, y una clase Avatar que puede saludar y llamar a cualquier tipo de elemento control. No obstante, siempre es bueno recordar que la modelación puede ser distinta para cada programador. \n",
    "\n",
    "A continuación se muestra otra posible modelación a este problema. Donde cada `MaestroXXXX` tendrá el método `saludar` y un nuevo método llamado `elemento_control` en reemplazo de `xxxx_control` (por ejemplo `fuego_control`). Ahora, será el `Avatar` quien puede diferenciar estos 4 métodos. De esta forma, las 4 clases de `Maestro` son muy similares en los métodos que tienen."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MaestroFuego:\n",
    "    def __init__(self, nombre: str):\n",
    "        self.nombre = nombre\n",
    "        \n",
    "    def saludar(self):\n",
    "        print(f\"Hola!! mi nombre es {self.nombre} y soy un maestro fuego\")\n",
    "        \n",
    "    def elemento_control(self):\n",
    "        print(\"¡Fuego control!\")\n",
    "\n",
    "        \n",
    "class MaestroAgua:\n",
    "    def __init__(self, nombre: str):\n",
    "        self.nombre = nombre\n",
    "        \n",
    "    def saludar(self):\n",
    "        print(f\"Hola!! mi nombre es {self.nombre} y soy un maestro agua\")\n",
    "    \n",
    "    def elemento_control(self):\n",
    "        print(\"¡Agua control!\")\n",
    "\n",
    "            \n",
    "class MaestroTierra:\n",
    "    def __init__(self, nombre: str):\n",
    "        self.nombre = nombre\n",
    "        \n",
    "    def saludar(self):\n",
    "        print(f\"Hola!! mi nombre es {self.nombre} y soy un maestro tierra\")\n",
    "\n",
    "    def elemento_control(self):\n",
    "        print(\"¡Tierra control!\")\n",
    "        \n",
    "        \n",
    "class MaestroAire:\n",
    "    def __init__(self, nombre: str):\n",
    "        self.nombre = nombre\n",
    "        \n",
    "    def saludar(self):\n",
    "        print(f\"Hola!! mi nombre es {self.nombre} y soy un maestro aire\")\n",
    "        \n",
    "    def elemento_control(self):\n",
    "        print(\"¡Aire control!\")\n",
    "\n",
    "        \n",
    "class Avatar(MaestroFuego, MaestroAgua, MaestroTierra, MaestroAire):\n",
    "    def __init__(self, nombre: str):\n",
    "        super().__init__(nombre)\n",
    "        \n",
    "    def saludar(self):\n",
    "        MaestroFuego.saludar(self)\n",
    "        MaestroAgua.saludar(self)\n",
    "        MaestroTierra.saludar(self)\n",
    "        MaestroAire.saludar(self)\n",
    "        \n",
    "    def fuego_control(self):\n",
    "        MaestroFuego.elemento_control(self)\n",
    "        \n",
    "    def agua_control(self):\n",
    "        MaestroAgua.elemento_control(self)\n",
    "        \n",
    "    def tierra_control(self):\n",
    "        MaestroTierra.elemento_control(self)\n",
    "        \n",
    "    def aire_control(self):\n",
    "        MaestroAire.elemento_control(self)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "kyoshi = Avatar(\"kyoshi\")\n",
    "kyoshi.saludar()\n",
    "print()\n",
    "kyoshi.fuego_control()\n",
    "kyoshi.agua_control()\n",
    "kyoshi.tierra_control()\n",
    "kyoshi.aire_control()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Te invitamos a pensar en otras formas de modelar esta situacion, y reflexionar sobre las ventajas y desventajas de cada forma. Algunas preguntas que pueden servir para reflexionar son\n",
    "- ¿Cuál permite agregar nuevas clases sin editar las anteriores?\n",
    "- ¿Cuál no presentaría problema del diamante?\n",
    "- ¿Estoy generando código nuevo desde cero o estoy modificando código existente?\n",
    "- Si tengo subclases, ¿estas deben funcionar de forma independiente o solo son un medio para que las clases que heredan de estas funcionen correctamente?"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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
}
