{
 "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. [Decoradores](#decoradores)\n",
    "    1. [Objetos de primera clase](#objetos-de-primera-clase)\n",
    "    2. [Sintaxis de los decoradores](#sintaxis-de-los-decoradores)\n",
    "    3. [Funciones dentro de funciones](#funciones-dentro-de-funciones)\n",
    "2. [Ejemplos de decoradores más usados](#ejemplos-de-decoradores-mas-usados)\n",
    "    1. [Properties: @property](#properties-property)\n",
    "\n",
    "        1. [Ejemplo: Definiendo una clase `Email`](#ejemplo-definiendo-una-clase-email)\n",
    "    \n",
    "    2. [Métodos estáticos](#métodos-estáticos)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Decoradores"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Conceptualmente, un decorador es en sí una función ([callable](https://docs.python.org/3.11/c-api/call.html) o invocable) que toma otra función como *input* y retorna una versión modificada de esa función. Los decoradores proveen una forma sintética de agregar funcionalidades a una función, sin cambiar las líneas de código que la componen en su origen. Se puede modificar o aumentar el comportamiento de una función, o incluso una clase. \n",
    "\n",
    "## Objetos de primera clase\n",
    "\n",
    "Una de las razones de su uso ampliado, que veremos a lo largo del curso, es que las funciones en Python son **objetos de primera clase**. Esto implica que:\n",
    "\n",
    "- Puedes asignar una función a una variable como si fuera un *int* o un *string*.\n",
    "- Puedes pasar una función como argumento a otra función (o devolverla como resultado).\n",
    "- Puedes almacenar funciones en estructuras de datos (listas, diccionarios, etc.).\n",
    "- Tienen atributos (puedes agregar o acceder dinámicamente a los atributos de una función).\n",
    "\n",
    "Veamos un ejemplo de funciones como objetos de primera clase en Python."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# función regular que espera un nombre como string y retorna un string\n",
    "def decir_hola(nombre: str) -> str:\n",
    "    return f\"Hola {nombre}\"\n",
    "\n",
    "# función regular que espera un nombre como string y retorna un string\n",
    "def ser_bacan(nombre: str) -> str:\n",
    "    return f\"Oye {nombre}, ¡juntos somos demasiado bacanes!\"\n",
    "\n",
    "# función que espera una función como argumento y retorna un string\n",
    "def saludar_dani(funcion_saludadora) -> str:\n",
    "    return funcion_saludadora(\"Dani\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(saludar_dani(decir_hola))\n",
    "print(saludar_dani(ser_bacan))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tenga en cuenta que `saludar_dani(decir_hola)` hace referencia a dos funciones, `saludar_dani()` y `decir_hola`, pero de forma diferente. La función `decir_hola` se nombra sin paréntesis. Esto significa que solo se pasa una **referencia** a la función, y la función no se ejecuta. La función `saludar_dani()`, en cambio, se escribe con paréntesis, por lo que se llamará normalmente.\n",
    "\n",
    "Esta es una distinción importante, crucial para el funcionamiento de las funciones como objetos de primera clase. Un nombre de función sin paréntesis es una referencia a una función, mientras que un nombre de función con paréntesis finales la llama y hace referencia a su valor de retorno."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def decir_hola(nombre: str) -> None:\n",
    "    print(f\"Hola {nombre}\")\n",
    "\n",
    "saludar = decir_hola # asignamos la función a una variable\n",
    "saludar(\"Dani\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el caso del código de arriba, `decir_hola` es simplemente otra referencia (o puntero) al mismo objeto de función al que se refiere `saludar`. En el ejemplo a continuación, vemos una situación similar, pero ahora ejemplificando que una función puede perfectamente retornar otra función."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# función regular que espera un nombre como string y retorna un string\n",
    "def decir_hola(nombre: str) -> None:\n",
    "    return f\"Hola {nombre}\"\n",
    "\n",
    "# función que espera una función como argumento y retorna una función\n",
    "def saludar_dani(funcion_saludadora):\n",
    "    return funcion_saludadora\n",
    "\n",
    "saludar = saludar_dani(decir_hola)\n",
    "print(saludar(\"Dani\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Al ser objetos, los decoradores pueden recibir funciones como argumentos, inspeccionarlas o modificarlas, y devolverlas (o devolver nuevos objetos función). Esta es la idea central que hace posibles los decoradores. Resumamos lo aprendido hasta ahora:\n",
    "\n",
    "- Los decoradores son funciones (callables o invocables) que toman un objeto de función como entrada.\n",
    "- Pueden añadir atributos, encapsular comportamientos o cualquier otra función, ya que la entrada es simplemente otro objeto de Python.\n",
    "- Devuelven la función original o una versión modificada de esta.\n",
    "\n",
    "Por lo tanto, en Python, tratar las funciones como objetos es lo que da lugar a construcciones potentes como decoradores."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Sintaxis de los decoradores\n",
    "\n",
    "En Python, los decoradores se aplican comúnmente usando el símbolo @, continuado del nombre del decorador, de esta forma: `@nombre_decorador`. Esta se coloca en la línea directamente arriba de la definición de la función que será \"decorada\".\n",
    "\n",
    "Cuando vemos este código:\n",
    "\n",
    "```python\n",
    "@decorador\n",
    "def mi_funcion():\n",
    "    pass\n",
    "```\n",
    "\n",
    "Es realmente una abreviación para:\n",
    "\n",
    "```python\n",
    "def mi_funcion():\n",
    "    pass\n",
    "    \n",
    "mi_funcion = decorador(mi_funcion)\n",
    "```\n",
    "\n",
    "En otras palabras, la sintaxis @decorador significa que `decorador` recibe a `mi_funcion` como parámetro, y lo que sea que `decorador(mi_funcion)` returne se convertirá en el nuevo valor de `mi_funcion`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Veamos el siguiente ejemplo, que modifica un string agregando un signo de exclamación al final de este."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def agregar_exclamacion(func):\n",
    "    def decorada():\n",
    "        return func() + \"!\"\n",
    "    return decorada\n",
    "\n",
    "@agregar_exclamacion\n",
    "def decir_hola():\n",
    "    return \"Hola\"\n",
    "\n",
    "print(decir_hola())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Paso a paso:\n",
    "1. `agregar_exclamacion` toma una función (func) y devuelve una nueva función (`decorada`).\n",
    "2. La ​​nueva función (`decorada`) llama a la función original y añade `\"!\"` al resultado.\n",
    "3. El decorador `@agregar_exclamacion` se aplica a `decir_hola`, por lo que al llamar a `decir_hola()`, modifica la salida añadiendo `\"!\"`.\n",
    "\n",
    "Lo que logramos: Tomar una función y cambiar ligeramente su comportamiento sin cambiar su código original.\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Funciones dentro de funciones\n",
    "\n",
    "Notarás que en el ejemplo anterior, estamos definiendo una función dentro de otra. ¿Acaso no estaba prohibido en intro? 🤔\n",
    "\n",
    "**¿Por qué es válido escribir una función dentro de otra función en Python?** En Python, escribir una función dentro de otra función es válido por la misma razón anterior de que las funciones son objetos de primera clase. Esto significa que:\n",
    "\n",
    "- Las funciones pueden definirse en cualquier lugar, incluso dentro de otras funciones.\n",
    "- Las funciones internas (también llamadas funciones anidadas) solo existen dentro del ámbito de la función externa, lo que ayuda a mantener el código organizado y evita funciones globales innecesarias.\n",
    "\n",
    "**¿Por qué es importante esto para los decoradores?** La función interna modifica el comportamiento de la función original, manteniéndola separada del ámbito global. Esta capacidad de definir y devolver funciones dinámicamente es lo que hace que Python sea flexible y potente para aplicaciones como decoradores."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def funcion_externa():\n",
    "    def funcion_interna():\n",
    "        print(\"Hello darkness my old friend!\")\n",
    "    \n",
    "    # Llamamos a la funcion interna dentro de la externa\n",
    "    funcion_interna()  \n",
    "\n",
    "funcion_externa()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aquí, `funcion_interna()` solo existe dentro de `funcion_externa()` y no se puede llamar desde afuera. ¡Probemos transformando esta misma dinámica usando la sintaxis de decoradores!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def mi_decorador(func):\n",
    "    def funcion_interna():\n",
    "        print(\"Hello darkness my old friend!\")\n",
    "        func()\n",
    "    return funcion_interna  # Return the modified function\n",
    "\n",
    "@mi_decorador\n",
    "def saludar():\n",
    "    print(\"I've come to talk with you again\")\n",
    "\n",
    "saludar()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Ejemplos de decoradores mas usados"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una de las razones más importantes para enseñar decoradores en este curso, es porque es una muy buena forma de programar, y es parte del conjunto de buenas prácticas que actualmente se usan en la industria. Permite evitar la duplicación de código extrayendo la lógica repetida en decoradores reutilizables. Podrán ver aplicaciones inmediatas en áreas como desarrollo web, servicios web, seguridad, pruebas (*testing*) y registro (*logging*)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## *Properties*: `@property`\n",
    "\n",
    "En los lenguajes OOP, existen formas de definir atributos de clase _públicos_ o _privados_. Un atributo privado se considera como un atributo que no quisiéramos modificar fácilmente, es decir, que tenga cierta protección en su lectura o escritura. En Python, **todos** los atributos y métodos de una clase son **públicos**, y el hecho de iniciar el nombre de un atributo o método con _underscore_ (guión bajo) es una convención (y una buena práctica), pero no asegura un caracter privado de estos elementos.\n",
    "\n",
    "Una consecuencia de tener atributos privados (o casi privados) es que si queremos modificarlos tenemos que, forzosamente, utilizar un método. En el paradigma OOP, se definen métodos específicos para **obtener el valor de un atributo (privado)**, y para **actualizar el valor de un atributo (privado)**. A estos métodos se llama respectivamente **getters** y **setters**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`property(get, set, del)` es una función integrada (built-in) de Python que crea un atributo administrado en una clase. Permite definir un *getter*, un *setter* y un *deleter* para un atributo de forma controlada. Puedes encontrar más información en la [documentación de la función](https://docs.python.org/3/library/functions.html#property).\n",
    "\n",
    "La función se utiliza de la siguiente forma:\n",
    "```python\n",
    "property(fget=None, fset=None, fdel=None)\n",
    "```\n",
    "Para esta función especial de Python, vemos que lo que pide como argumento son funciones.\n",
    "- fget: Una función que actuará como getter (va a buscar el atributo).\n",
    "- fset: Una función que actuará como setter (setea el atributo).\n",
    "- fdel: Una función que actuará como deleter (borrará el atributo).\n",
    "\n",
    "Veamos el siguiente ejemplo donde trataremos de proteger la informacion de una instancia de la clase `Circulo`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Circulo:\n",
    "    \n",
    "    def __init__(self, radio):\n",
    "        self._radio = radio  # atributo a proteger, privado\n",
    "\n",
    "    # Getter para el radio\n",
    "    def get_radio(self):\n",
    "        print(\"Obteniendo el radio...\")\n",
    "        return self._radio\n",
    "\n",
    "    # Setter para el radio, que asegura valores positivos\n",
    "    def set_radio(self, value):\n",
    "        if value >= 0:\n",
    "            self._radio = value\n",
    "        else:\n",
    "            print(\"El radio no puede ser negativo\")\n",
    "    \n",
    "    # Metodo que retorna el area del circulo\n",
    "    def area(self):\n",
    "        print(\"Obteniendo el área...\")\n",
    "        print(\"Area:\",3.14 * self._radio ** 2)\n",
    "\n",
    "    # Definimos propiedades usando property()\n",
    "    radio = property(get_radio, set_radio)\n",
    "\n",
    "c = Circulo(5)\n",
    "print(\"Radio:\", c.radio)\n",
    "c.area()\n",
    "\n",
    "# Intentamos cambiar el radio a un valor negativo\n",
    "c.radio = -3\n",
    "c.area()\n",
    "\n",
    "# Cambiamos el radio a un valor válido\n",
    "c.radio = 10\n",
    "c.area()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Como vemos, la función *built-in* `property` de Python recibe a otras funciones como argumentos. Esto la hace ideal para utilizar los decoradores. El decorador @property en Python se usa para definir un método como un atributo de solo lectura. Esto permite que una clase exponga un método como si fuera un atributo normal, sin necesidad de paréntesis al acceder a él. Veamos el mismo ejemplo, ahora usando el decorador @property. Además, agregaremos una segunda propiedad, el área."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Circulo:\n",
    "\n",
    "    def __init__(self, radio):\n",
    "        self._radio = radio  # Atributo privado, queremos protegerlo\n",
    "\n",
    "    @property\n",
    "    def radio(self):\n",
    "        # Getter para el radio\n",
    "        print(\"Obteniendo el radio...\")\n",
    "        return self._radio\n",
    "\n",
    "    @radio.setter\n",
    "    def radio(self, valor):\n",
    "        # Setter para el radio, asegurando valores positivos\n",
    "        if valor >= 0:\n",
    "            self._radio = valor\n",
    "        else:\n",
    "            print(\"El radio no puede ser negativo\")\n",
    "\n",
    "    @property\n",
    "    def area(self):\n",
    "        # Método que retorna el área del círculo (atributo de solo lectura)\n",
    "        print(\"Obteniendo el área...\")\n",
    "        return 3.14 * self._radio ** 2\n",
    "\n",
    "c = Circulo(5)\n",
    "print(\"Radio:\", c.radio)\n",
    "print(\"Área:\", c.area) # ahora es un atributo\n",
    "\n",
    "# Intentamos cambiar el radio a un valor negativo\n",
    "c.radio = -3 \n",
    "print(\"Área:\", c.area)\n",
    "\n",
    "# Cambiamos el radio a un valor válido\n",
    "c.radio = 10\n",
    "print(\"Área:\", c.area)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Ejemplo: definiendo una clase `Email`\n",
    "\n",
    "Este ejemplo utiliza la segunda forma de usar *properties*, que es definiendo los métodos y luego asignarlos a una variable usando `property`. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Email:\n",
    "    \n",
    "    def __init__(self, address):\n",
    "        self._email = address\n",
    "        \n",
    "    def _get_email(self):\n",
    "        return self._email\n",
    "        \n",
    "    def _set_email(self, value):\n",
    "        if '@' not in value:\n",
    "            print(f\"El string {value} no parece una dirección de correo.\")\n",
    "        else:\n",
    "            self._email = value \n",
    "\n",
    "    email = property(_get_email, _set_email)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mail = Email(\"profe1@gmail.com\")\n",
    "print(mail.email)\n",
    "mail.email = \"profe2@gmail.com\"\n",
    "print(mail.email)\n",
    "mail.email = \"profe2.com\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La siguiente versión de la clase `Email` define la `property` utilizando la notación de decoradores. Es equivalente al ejemplo anterior, y su funcionamiento es igualmente equivalente."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Email2:\n",
    "    \n",
    "    def __init__(self, address):\n",
    "        self._email = address\n",
    "    \n",
    "    @property\n",
    "    def email(self):\n",
    "        return self._email\n",
    "    \n",
    "    @email.setter\n",
    "    def email(self, value):\n",
    "        if '@' not in value:\n",
    "            print(f\"El string {value} no parece una dirección de correo.\")\n",
    "        else:\n",
    "            self._email = value\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mail = Email2(\"profe1@gmail.com\")\n",
    "print(mail.email)\n",
    "mail.email = \"profe2@gmail.com\"\n",
    "print(mail.email)\n",
    "mail.email = \"profe2.com\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Métodos estáticos\n",
    "\n",
    "Imaginemos que nos solicitan crear una clase para poder sumar y restar números. Para esta solicitud, cramos una clase `MiMath` que permita realizar dichas operaciones."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MiMath:\n",
    "\n",
    "    def sumar(self, num1, num2):\n",
    "        return num1 + num2\n",
    "\n",
    "    def restar(self, num1, num2):\n",
    "        return num1 - num2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(MiMath().sumar(1, 2))\n",
    "print(MiMath().restar(5, 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si bien la clase anterior cumple con lo solicitado, para PEP8 no es correcto hacer clases que tengan `self` cuando no es necesario. Una solución a esto es utilizar funciones (`def sumar(num1, num2)`), pero en otros lenguajes que sean extrictamente OOP, como Java, no es posible crear funciones que no esten adscritas a una clase, sino que debemos crear una clase con dicho métodos. La solución para crear métodos que no necesitan al `self` son **métodos estáticos**.\n",
    "\n",
    "Los métodos estáticos (_static methods_) son métodos que pertenecen a una clase. No obstante, no dependen de información de la clase ni de atributos de la instancia. Por lo tanto, en este caso no se necesita ningún atributo principal como sucede con los métodos normales, es decir, no requiere del `self`.\n",
    "\n",
    "Para declarar un método estático, solo necesitamos usar el decorador `@staticmethod` y nuestro método sin el `self`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MiMath:\n",
    "\n",
    "    @staticmethod\n",
    "    def sumar(num1, num2):\n",
    "        return num1 + num2\n",
    "\n",
    "    @staticmethod\n",
    "    def restar(num1, num2):\n",
    "        return num1 - num2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(MiMath.sumar(5, 6))\n",
    "print(MiMath.restar(14, 1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Como puedes notar, en estos ejemplos no fue necesario generar una instancia de la clase `MiMath`, sino que solo se hace `clase.Metodo`. Esto es porque no necesitamos de usar `self` en los métodos usados. Esta forma nos permite generar métodos dentro de una clase, lo cual permite tener empaquetadas ciertas funcionalidades \n",
    "\n",
    "Tambien, resulta útil tener un indicador de que ciertos métodos no requieren del `self` y por lo tanto, no accederán ni modificarán el estado de la instancia. Es cierto que en Python se podría hacer lo mismo con un método de instancia como el ejemplo adicional o utilizar funciones, pero a veces el lenguaje no permite dichas opciones."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "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
}
