{
 "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",
    "\n",
    "1. [Introducción](#Introducción)\n",
    "2. [Comando type](#Comando-type)\n",
    "3. [Metaclases personalizadas](#Metaclases-personalizadas)\n",
    "    1. [Asignación](#Asignación-de-metaclases)\n",
    "    2. [Creación](#Creación-de-metaclases)\n",
    "        1. [new](#Método-\\_\\_new__)\n",
    "        2. [init](#Método-\\_\\_init__)\n",
    "    3. [Creación de instancias](#Creación-de-instancias)\n",
    "4. [Ejemplos](#Ejemplos)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Introducción\n",
    "\n",
    "Como vimos en los contenidos de introducción a la programación, en Python podemos guardar diferentes tipos de datos dentro de variables. Por ejemplo, podemos guardar enteros, *strings* y listas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x = 10\n",
    "y = 20\n",
    "z = x + y  # 30\n",
    "\n",
    "string_ejemplo = 'Bienvenidos a IIC2233'\n",
    "\n",
    "lista_ejemplo = [10, 'variable', True, 5.5]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Adicionalmente, podemos usar variables para guardar resultados de funciones y métodos o instancias de clases."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def suma(a: int, b: int):\n",
    "    return a + b\n",
    "\n",
    "total = suma(x, y)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Persona:\n",
    "    def __init__(self, nombre: str, apellido: str):\n",
    "        self.nombre = nombre\n",
    "        self.apellido = apellido\n",
    "        self.edad = 60\n",
    "\n",
    "    def __str__(self):\n",
    "        return f'Mi nombre es {self.nombre} {self.apellido}'\n",
    "\n",
    "    def entregar_edad(self):\n",
    "        return self.edad\n",
    "\n",
    "\n",
    "maradona = Persona(\"Diego\", \"Maradona\")\n",
    "edad = maradona.entregar_edad()\n",
    "\n",
    "print(maradona)\n",
    "print(edad)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un aspecto que hasta ahora no hemos visto, es que también podemos guardar clases en variables."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Perro: \n",
    "    def __init__(self, nombre: str):\n",
    "        self.nombre = nombre\n",
    "\n",
    "clase_perro = Perro\n",
    "\n",
    "instancia_perro = clase_perro('Coraje')\n",
    "print(instancia_perro.nombre)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Siguiendo la misma línea, todos los elementos que se pueden guardar en variables, pueden ser entregados como argumento a una función. Esto incluye los tipos que utilizamos tradicionalmente, pero según lo que acabamos de ver, deberíamos poder entregar no sólo instancias de clases, si no que las clases mismas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Any\n",
    "\n",
    "def imprimir_dato(dato: Any):\n",
    "    print(dato)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Enteros y listas\n",
    "print('Enteros y Listas:')\n",
    "imprimir_dato(112)\n",
    "imprimir_dato(lista_ejemplo)\n",
    "\n",
    "# Instancias\n",
    "print('\\nInstancias:')\n",
    "imprimir_dato(maradona)\n",
    "imprimir_dato(instancia_perro)\n",
    "\n",
    "# Clases\n",
    "print('\\nClases:')\n",
    "imprimir_dato(Persona)\n",
    "imprimir_dato(Perro)\n",
    "imprimir_dato(clase_perro)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La razón por la que todos estos elementos se pueden guardar en variables y entregar como argumentos es porque tienen un tipo de dato, es decir, son instancias de una clase.\n",
    "\n",
    "Por ejemplo, los enteros son instancias de la clase `int`, las listas son instancias de la clase `list`, Maradona es una instancia de la clase `Persona` y Coraje es una instancia de la clase `Perro`.\n",
    "\n",
    "Entonces, ¿de qué objeto son instancias las clases `Persona` y `Perro`?.\n",
    "A las clases que definen el tipo de otras clases, en Python, las llamamos **Metaclases**. En otras palabras, una metaclase es una *clase* cuyas instancias son otras *clases*."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Comando type\n",
    "\n",
    "El comando `type()` recibe una argumento y retorna su tipo de dato, es decir, la clase a la que pertenece la instancia."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Any\n",
    "\n",
    "def imprimir_tipo_de_dato(dato: Any):\n",
    "    print(type(dato))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Enteros y listas\n",
    "print('Enteros y Listas:')\n",
    "imprimir_tipo_de_dato(112)\n",
    "imprimir_tipo_de_dato(lista_ejemplo)\n",
    "\n",
    "# Instancias\n",
    "print('\\nInstancias:')\n",
    "imprimir_tipo_de_dato(maradona)\n",
    "imprimir_tipo_de_dato(instancia_perro)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Luego, si el objeto que entregamos es una clase, `type()` retornará cuál es la metaclase a la que pertenece esa clase."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Clases\n",
    "print('\\nClases:')\n",
    "imprimir_tipo_de_dato(Persona)\n",
    "imprimir_tipo_de_dato(Perro)\n",
    "imprimir_tipo_de_dato(clase_perro)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este ejemplo, podemos ver que el tipo de dato, tanto para `Perro` como para `Persona`, es `type`. En otras palabras `type` es la Metaclase de estas dos clases. \n",
    "\n",
    "Lo anterior se debe a que la metaclase por defecto en Python es `type` y, dado que en ningún momento especificamos la metaclase que queríamos usar, esta fue asignada."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Metaclases personalizadas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Asignación de metaclases\n",
    "Para declarar que una clase tiene un cierto tipo, debemos agregar el _keyworded argument_ `metaclass={tipo_de_clases}`. \n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Gato(metaclass=type):\n",
    "    def __init__(self, nombre):\n",
    "        self.nombre = nombre\n",
    "        \n",
    "instancia_gato = Gato('Cachirulo')\n",
    "instancia_gato"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Creación de metaclases\n",
    "Ahora que sabemos asignar metaclases, solo necesitamos poder crear nuestras propias metaclases para poder utilizarlas.\n",
    "\n",
    "En Python, las metaclases se declaran como clases que heredan de `type` y su principal propósito es **controlar la creación e instanciación de una clase**. Para realizar esto, necesitamos implementar, en la metaclase, los métodos `__new__` e `__init__`, respectivamente. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Método \\_\\_new__\n",
    "El método `__new__` debe ser implementado cuando queremos controlar la creación de una nueva clase. Este método tendrá los siguientes parámetros:\n",
    "\n",
    "* `cls`: De manera similar a como `self` representa la instancia a actual, este argumento **representa la Meta Clase actual**.\n",
    "* nombre: Corresponde a un `str` con el nombre de la clase.\n",
    "* clases_base: Es una tupla que contiene las clases de las que hereda la clase instanciada.\n",
    "* diccionario: Como dice su nombre, es un diccionario que contiene la información de la clase instanciada.\n",
    "\n",
    "Utilizando este método podemos, por ejemplo, imprimir el nombre de la clase creada."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MetaClaseImpresora(type):\n",
    "    def __new__(cls, nombre, clases_base, diccionario):\n",
    "        # Imprimimos los mensajes que queremos\n",
    "        print(f'Llamando al método __new__ de {cls.__name__}')\n",
    "        print(f'Creando la clase {nombre}')\n",
    "        \n",
    "        # Creamos la clase usando el __new__ de type\n",
    "        clase = super().__new__(cls, nombre, clases_base, diccionario)\n",
    "        \n",
    "        # Retornamos la clase creada, sin ninguna modificación\n",
    "        return clase"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Es importante señalar que el `__new__` de la Metaclase **se llamará al momento de crear la clase**, **NO** al momento de crear instancias de la clase. Esto lo podemos ver claramente en el siguiente ejemplo: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('Aquí creamos la clase, por lo que se llama a new\\n')\n",
    "\n",
    "class Gato(metaclass=MetaClaseImpresora):\n",
    "    def __init__(self, nombre):\n",
    "        self.nombre = nombre\n",
    "    \n",
    "    def __call__(self):\n",
    "        print(self.nombre)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('Aquí instanciamos la clase, por lo que NO se llama a new')\n",
    "\n",
    "instancia_gato = Gato('Luna')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Utilizando el método `__new__`, podemos además agregar comportamientos mucho más interesantes y complejos. \n",
    "\n",
    "A continuación, nuestra metaclase cambia el nombre de la clase creada, agrega un atributo nuevo, y fuerza una herencia sobre la clase."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Guerrero:\n",
    "    def __init__(self, nombre):\n",
    "        self.nombre = nombre\n",
    "\n",
    "class MetaTransformaPaladin(type):\n",
    "    def __new__(cls, nombre, clases_base, diccionario):\n",
    "        # Imprimimos un mensaje\n",
    "        print(f'Llamando al método __new__ de {cls.__name__}')\n",
    "        \n",
    "        # Cambiamos el nombre de la clase\n",
    "        print(f'Intentaron crear la clase {nombre}, pero en su lugar crearemos Paladin')\n",
    "        nuevo_nombre = 'Paladin'\n",
    "             \n",
    "        # Cambiamos las clases base\n",
    "        print(f'Intentaron heredar de {clases_base}, pero Paladin hereda de Guerrero')\n",
    "        nuevas_clases_base = (Guerrero,)\n",
    "        \n",
    "        # Actualizamos el diccionario con el nuevo atributo\n",
    "        print('Los paladines deben tener el atributo \"defensa\" como atributo de clase')\n",
    "        diccionario['defensa'] = 100\n",
    "        \n",
    "        # Llamamos al __new__ de type con los nuevos argumentos\n",
    "        clase = super().__new__(cls, nuevo_nombre, nuevas_clases_base, diccionario)\n",
    "        print(clase)\n",
    "        return clase"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Mago(metaclass=MetaTransformaPaladin):\n",
    "    def __init__(self, nombre):\n",
    "        self.nombre = nombre"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "instancia_mago = Mago('Morgana')\n",
    "\n",
    "print(instancia_mago.__class__.__name__)\n",
    "print(isinstance(instancia_mago, Guerrero))\n",
    "print(instancia_mago.defensa)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Método \\_\\_init__\n",
    "El método `__init__` debe ser implementado cuando queremos controlar **la instanciación** de una nueva clase.\n",
    "\n",
    "Tiene casi los mismos parámetros que `__new__`, pero en lugar de recibir el `cls` que representa la metaclase, volveremos al `self`, que representará la instancia de la metaclase, es decir, la clase actual. \n",
    "\n",
    "La principal diferencia es que este método es llamado **después de que la clase haya sido creada**, por esto, si bien podemos agregar funciones y atributos a la clase dentro del `__init__` de la metaclase, no podremos modificar cosas propias de la clase como su nombre o sus clases base.\n",
    "\n",
    "Utilizando este método podemos recrear la metaclase impresora, pero no podremos hacer lo mismo que la metaclase usada para el Paladín."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MetaClaseImpresoraInit(type):\n",
    "    def __init__(self, nombre, clases_base, diccionario):\n",
    "        # Imprimimos los mensajes que queremos\n",
    "        print(f'Llamando al método __init__ de MetaClaseImpresoraInit')\n",
    "        print(f'Creando la clase {nombre}')\n",
    "        \n",
    "        # Creamos la clase usando el __init__ de type\n",
    "        clase = super().__init__(nombre, clases_base, diccionario)\n",
    "        \n",
    "        # Retornamos la clase creada, sin ninguna modificación\n",
    "        return clase"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Gato(metaclass=MetaClaseImpresoraInit):\n",
    "    def __init__(self, nombre):\n",
    "        self.nombre = nombre\n",
    "    \n",
    "    def __call__(self):\n",
    "        print(self.nombre)\n",
    "        \n",
    "print('\\nCreando instancia de gato:')\n",
    "instancia_gato = Gato('Grumo')\n",
    "instancia_gato()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MetaAgregaDefensa(type):\n",
    "    def __init__(self, nombre, clases_base, diccionario):\n",
    "        # Imprimimos un mensaje\n",
    "        print(f'Llamando al método __init__ de MetaAgregaDefensa')\n",
    "               \n",
    "        # Actualizamos el diccionario agregando el atributo defensa\n",
    "        print('Los gatos también merecen defenderse')\n",
    "        self.defensa = 100\n",
    "\n",
    "        clase = super().__init__(nombre, clases_base, diccionario)\n",
    "        return clase"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class GatoDefensivo(metaclass=MetaAgregaDefensa):\n",
    "    def __init__(self, nombre):\n",
    "        self.nombre = nombre\n",
    "    \n",
    "    def __call__(self):\n",
    "        print(f'Gato de nombre {self.nombre} con atributo de clase defensa = {self.defensa}')\n",
    "        \n",
    "print('\\nCreando instancia de gato:')\n",
    "instancia_gato = GatoDefensivo('Pepe')\n",
    "instancia_gato()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Creación de instancias\n",
    "\n",
    "Como podrán haber notado en algunos de los ejemplos anteriores, estábamos tratando las instancias de la clase gato como funciones, haciendo `instancia_gato()`. Al realizar esto, se llamará al método `__call__` definido en la clase y llamamos *callable* a los objetos cuyo método `__call__` se encuentra implementado.\n",
    "\n",
    "Si aplicamos esta misma idea a las clases, cuando queremos crear una nueva instancia de una clase, comúnmente lo hacemos a través de `NombreClase()`. Si bien esta es la manera esperada, ¿por qué no utilizamos `NombreClase.__init__()`?. La razón es que al utilizar directamente el nombre de la clase, lo que estamos haciendo es llamar al `__call__` de la metaclase, que luego se encargará de llamar al `__init__` de la clase y así crear la instancia correctamente.\n",
    "\n",
    "Luego, de lo anterior podemos concluir que si queremos afectar la creación de instancias de una clase, podemos hacerlo modificando el `__call__` de su metaclase. Este método recibe como argumentos el `cls` y además todos los argumentos que se entreguen a la llamada; dado que esto puede ser una cantidad arbitraria que dependerá de cada clase, podemos acomodarnos a todas utilizando `*args` y `**kwargs`.\n",
    "\n",
    "A continuación tenemos una metaclase que imprimirá un mensaje cada vez que se cree una instancia de sus clases correspondientes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MetaImprimeInstancias(type):\n",
    "    def __call__(cls, *args, **kwargs):\n",
    "        print('LLamando a __call__ de MetaImprimeInstancias')\n",
    "        print(f'Instanciando la clase {cls.__name__} con args: {args} y kwargs: {kwargs}')\n",
    "        instancia = super().__call__(*args, **kwargs)\n",
    "        return instancia"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class GatoImpresor(metaclass=MetaImprimeInstancias):\n",
    "    def __init__(self, nombre, edad, personalidad):\n",
    "        self.nombre = nombre\n",
    "        self.edad = edad\n",
    "        self.personalidad = personalidad\n",
    "    \n",
    "    def __call__(self):\n",
    "        print(f'Gato de nombre {self.nombre} con personalidad {self.personalidad}')\n",
    "\n",
    "\n",
    "instancia_gato = GatoImpresor('Mia', 6, 'Amigable')\n",
    "instancia_gato()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Ejemplos\n",
    "\n",
    "A continuación se muestran un par de ejemplos más realistas para el uso de metaclases."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Clase que no puede ser subclaseada\n",
    "\n",
    "Definamos una metaclase que asegure que ninguna clase va a poder heredar de la metaclase. Se podrán crear clases que tengan la metaclase `MetaFinal` pero no será posible que estas clases sean la clase base de otras."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MetaFinal(type):\n",
    "    def __new__(cls, nombre, clases_base, diccionario):\n",
    "        # Revisamos las clases base de la clase que se está creando\n",
    "        for clase in clases_base:\n",
    "            # Si la clase es instancia de la metaclse, no puede ser creada\n",
    "            if isinstance(clase, cls):\n",
    "                print(f'No puede crearse la clase {nombre}')\n",
    "                return None\n",
    "        print(f'Clase {nombre} creada correctamente')\n",
    "        return super().__new__(cls, nombre, clases_base, diccionario)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class NoHeredable(metaclass=MetaFinal):\n",
    "    def __init__(self, *args, **kwargs):\n",
    "        super().__init__(*args, **kwargs)\n",
    "    \n",
    "instancia_no_heredable = NoHeredable()\n",
    "print(instancia_no_heredable)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ClaseImposible(NoHeredable):\n",
    "    def __init__(self, *args, **kwargs):\n",
    "        super().__init__(*args, **kwargs)\n",
    "\n",
    "instancia_imposible = ClaseImposible()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Singletón\n",
    "\n",
    "En este ejemplo veremos cómo definir una metaclase que asegure que cada vez que se llama a la clase para crear una nueva instancia, ésta retorne la nueva instancia, si y sólo si, ninguna instancia de la clase se ha creado antes. En otro caso, se debería retornar la misma instancia creada la primera vez:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MetaSoloUna(type):\n",
    "    def __call__(cls, *args, **kwargs):\n",
    "        if not hasattr(cls, 'instance'):\n",
    "             cls.instance = super().__call__(*args, **kwargs)\n",
    "        return cls.instance"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class SoloUna(metaclass=MetaSoloUna):\n",
    "    pass\n",
    "\n",
    "\n",
    "instancia_1 = SoloUna()\n",
    "print(\"instancia_1.instance == None: \", instancia_1.instance == None)\n",
    "\n",
    "instancia_2 = SoloUna()\n",
    "print(\"instancia_1 is instancia_2 \", instancia_1 is instancia_2)\n",
    "print(\"id instancia_1: \", id(instancia_1))\n",
    "print(\"id instancia_2: \", id(instancia_2))\n"
   ]
  }
 ],
 "metadata": {
  "anaconda-cloud": {},
  "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
}
