{
 "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'>\n",
    "    &copy; 2015 Karim Pichara - Christian Pieringer. Todos los derechos reservados. \n",
    "    Modificado desde 2018-1 al 2026-1 por Equipo Docente IIC2233\n",
    "</font>\n",
    "</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Tabla de contenidos\n",
    "\n",
    "1. [Funciones Generadoras](#Funciones-Generadoras)\n",
    "    1. [Ejemplos](#Ejemplos)\n",
    "    2. [Otra forma de hacer iterable una estructura propia](#Otra-forma-de-hacer-iterable-una-estructura-propia)\n",
    "    3. [También podemos interactuar con la función generadora enviando mensajes](#También-podemos-interactuar-con-la-función-generadora-enviando-mensajes)\n",
    "    4. [¿Qué hace que una función sea generadora en Python?](#qué-hace-que-una-función-sea-generadora-en-Python)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Funciones Generadoras\n",
    "\n",
    "Como vimos anteriormente, los **generadores** nos permiten iterar sobre secuencias de datos sin la necesidad de almacenarlos en alguna estructura especial, evitando el uso innecesario de memoria. Una vez que terminamos de iterar sobre un generador, el generador desaparece.\n",
    "\n",
    "En este contexto, las funciones en Python también tienen la posibilidad de funcionar como generadores, con la sentencia `yield`. El *statement* `yield` es un análogo a `return`, con ciertas diferencias. Por un lado, `yield` se encarga de retornar el valor indicado, pero también se asegura que en la próxima llamada a la función, la ejecución parta desde donde se dejó antes. \n",
    "\n",
    "En otras palabras, trabajamos con una función que una vez que entrega un valor a través de `yield`, está cediendo el control sólo en forma temporal, asumiendo que pronto será utilizada nuevamente para generar más valores.\n",
    "\n",
    "Creemos nuestra primera función generadora de números decrecientes:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Generator\n",
    "\n",
    "\n",
    "def conteo_decreciente(n: int) -> Generator:\n",
    "    print(f\"Contando en forma decreciente desde {n}\")\n",
    "    while n > 0:\n",
    "        yield n\n",
    "        n -= 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Vemos que cuando se llama a la función generadora, esta no ejecuta nada:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "x = conteo_decreciente(4)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esto se debe a que cuando se invoca la función generadora, esta retorna un **generador**. Luego, `x` es un generador de números desde el 10 hasta el 1."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'generator'>\n"
     ]
    }
   ],
   "source": [
    "print(type(x))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Podemos usar el generador directamente en un `for`, ya que como vimos estos implementan `__iter__` devolviendo `self`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Contando en forma decreciente desde 4\n",
      "4\n",
      "3\n",
      "2\n",
      "1\n"
     ]
    }
   ],
   "source": [
    "for number in x:\n",
    "    print(number)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "También se puede usar `next`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Contando en forma decreciente desde 5\n",
      "5\n"
     ]
    }
   ],
   "source": [
    "x = conteo_decreciente(5)\n",
    "print(next(x))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejemplos\n",
    "\n",
    "Veamos un ejemplo de una función generadora de números de Fibonacci:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0\n",
      "1\n",
      "1\n",
      "2\n",
      "3\n"
     ]
    }
   ],
   "source": [
    "from typing import Generator\n",
    "\n",
    "\n",
    "def fibonacci() -> Generator:\n",
    "    a, b = 0, 1\n",
    "    while True:  # Notar que este generador nunca \"se agota\"\n",
    "        yield a\n",
    "        a, b = b, a + b\n",
    "\n",
    "\n",
    "generador_fibonacci = fibonacci()\n",
    "\n",
    "# Imprimimos los primeros 5 elementos\n",
    "for i in range(5):\n",
    "    print(next(generador_fibonacci))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Además, veamos cómo utilizar las funciones generadoras para leer cargar los datos de un archivo, sin cargar todos los datos en memoria."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "id,Name in English,Countries,Country codes,Degree of endangerment,Number of speakers\n",
      "\n",
      "0,South Italian,Italy,ITA,Vulnerable,75000000\n",
      "\n",
      "1,Sicilian,Italy,ITA,Vulnerable,50000000\n",
      "\n",
      "2,Low Saxon,Germany;Denmark;Netherlands;Poland;Russian Federation,DEU;DNK;NLD;POL;RUS,Vulnerable,48000000\n",
      "\n",
      "3,Belarusian,Belarus;Latvia;Lithuania;Poland;Russian Federation;Ukraine,BRB;LVA;LTU;POL;RUS;UKR,Vulnerable,40000000\n",
      "\n"
     ]
    }
   ],
   "source": [
    "from os import path\n",
    "from typing import Generator\n",
    "\n",
    "\n",
    "def cargar_archivo(path: str) -> Generator:\n",
    "    \"\"\"Lee el contenido de un archivo, sin cargar toda la información del mismo en memoria.\"\"\"\n",
    "\n",
    "    with open(path, encoding='utf-8') as file:\n",
    "        for line in file:\n",
    "            yield line\n",
    "\n",
    "\n",
    "lenguas_extintas = cargar_archivo(path.join('data', 'lenguas_extintas.csv'))\n",
    "\n",
    "for i in range(5):\n",
    "    print(next(lenguas_extintas))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Es importante notar, que al momento de leer el archivo estamos iterando sobre el mismo (`for line in file`), lo que en sí, nos entrega un generador que itera sobre las líneas del archivos. \n",
    "\n",
    "En cambio, al usar la función `file.readlines()`, esto nos retornar una lista con todos los datos del archivo; por lo que al hacer esto, estamos cargando toda la información del archivo en la memoria del computador.  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "También veamos un ejemplo de que las funciones generadoras pueden operar con otras colecciones, como listas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1\n",
      "10\n",
      "14\n",
      "14\n",
      "14\n",
      "14\n",
      "19\n",
      "33\n"
     ]
    }
   ],
   "source": [
    "from typing import Generator, List\n",
    "\n",
    "\n",
    "def maximo_acumulativo(valores: List[int]) -> Generator:\n",
    "    \"\"\"Retorna el máximo visto hasta ahora en una colección de valores.\"\"\"\n",
    "    max_ = float('-inf')\n",
    "    for valor in valores:\n",
    "        max_ = max(valor, max_)\n",
    "        yield max_\n",
    "\n",
    "\n",
    "lista = [1, 10, 14, 7, 9, 12, 19, 33]\n",
    "\n",
    "for i in maximo_acumulativo(lista):\n",
    "    print(i)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Otra forma de hacer iterable una estructura propia\n",
    "\n",
    "Si tenemos una estructura de datos propia, podemos usar una función generadora en `__iter__` en vez de crear nuestra propia clase iteradora. Para ilustrarlo usaremos el mismo ejemplo del principio:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Generator\n",
    "\n",
    "\n",
    "class IterableListaNumeros:\n",
    "    def __init__(self, objeto: list) -> None:\n",
    "        self.objeto = objeto.copy()\n",
    "    \n",
    "    def __iter__(self) -> Generator:\n",
    "        while self.objeto:\n",
    "            yield self.objeto.pop(0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "datos = [1, 2, 3, 4, 5]\n",
    "iterable = IterableListaNumeros(datos)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1 2 3 4 5 "
     ]
    }
   ],
   "source": [
    "for x in iterable:\n",
    "    print(x, end=\" \")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'generator'>\n"
     ]
    }
   ],
   "source": [
    "iterador = iter(iterable)\n",
    "print(type(iterador))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## También podemos interactuar con la función generadora enviando mensajes \n",
    "\n",
    "El método `send()` permite enviar un valor hacia el generador, lo que significa que la expresión `yield` lo recibirá. El valor enviado puede ser usado para asignarlo a otra variable, por ejemplo `v = yield value` guardará en la variable `v` el valor enviado con `send()`.\n",
    "\n",
    "Veamos un ejemplo de una función generadora que entrega números que se incrementan según lo que le es enviado mediante `send`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Generator\n",
    "\n",
    "\n",
    "def funcion_generadora_send() -> Generator:\n",
    "    contador = 0\n",
    "\n",
    "    while True:\n",
    "        valor_recibido = yield contador\n",
    "        print(\"Hemos recibido {}\".format(valor_recibido))\n",
    "\n",
    "        if valor_recibido is None:  # Consideraremos 0 si nos llega un None\n",
    "            valor_recibido = 0\n",
    "\n",
    "        print(\"Sumaremos {} a nuestro contador\".format(valor_recibido))\n",
    "        contador += valor_recibido  # Sumamos el valor recibido al contador que llevamos"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "generador_send = funcion_generadora_send()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lo primero en lo que nos tenemos que fijar es que debemos avanzar hasta `yield` antes de poder enviar valores. Es decir, la primera vez no podremos enviar nada, sólo usar `next`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "TypeError: can't send non-None value to a just-started generator\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    generador_send.send(5)\n",
    "except TypeError as error:\n",
    "    print(TypeError.__name__+':', error)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ya hecho el primer `yield`, podemos enviarle valores a la función generadora:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "next(generador_send)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Hemos recibido 5\n",
      "Sumaremos 5 a nuestro contador\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "5"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "generador_send.send(5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### (Pequeño paréntesis)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Acá podras preguntarte ¿Por qué se guarda el valor `5` si es que ya fue ejecutada la linea `valor_recibido = yield contador` al ejecutar `next(generador_send)`? Es una pregunta muy válida, y para esto es necesario entender cómo Python maneja estos comandos para obtener la respuesta.\n",
    "\n",
    "Esto se debe a que, aunque la línea `valor_recibido = yield contador` ya se ejecutó en cuanto al `yield`, la asignación de `valor_recibido` no ocurre hasta que el generador se reanuda con un `send()`. Por eso, cuando se ejecuta `generador_send.send(5)`, el valor `5` es recibido y asignado a `valor_recibido`\n",
    "\n",
    "En otras palabras, la clave está en que el `yield` actúa como una puerta de entrada para los valores enviados. Al pausar la ejecución, se \"espera\" a que se le envíe un valor, el cual se asignará a la variable cuando la ejecución se reanude. Esto permite una **comunicación bidireccional** donde, además de emitir valores, el generador puede recibir información dinámica a través de `send()`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Continuamos..."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Luego, hacer `next`, es equivalente a hacer `send` de `None`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Hemos recibido None\n",
      "Sumaremos 0 a nuestro contador\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "5"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "next(generador_send)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Hemos recibido 10\n",
      "Sumaremos 10 a nuestro contador\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "15"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "generador_send.send(10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Funciones Generadoras a partir de un Iterable\n",
    "\n",
    "Otra manera de crear una función generadora es utilizando el *statement* `yelid from`. Éste nos permite **crear un generador a partir de algún otro iterable** sin la necesidad de ocupar algún tipo de bucle o *loop* (*for* o *while*) para recorrer sus elementos. Debido a sus caractetísticas, este *statement* nos permite escribir código más limpio, modular y fácil de mantener cuando se trabaja con **generadores compuestos o anidados**.\n",
    "\n",
    "Veamos un ejemplo de una función generadora creada a partir de un iterable utilizando el *statement* `yield from`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0\n",
      "1\n",
      "2\n",
      "3\n",
      "4\n"
     ]
    }
   ],
   "source": [
    "def generador_simple():\n",
    "    yield from range(5)\n",
    "\n",
    "for valor in generador_simple():\n",
    "    print(valor)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Además como un generador es un iterable, podemos **crear un generador a partir de otro generador**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0\n",
      "1\n",
      "2\n",
      "3\n",
      "4\n"
     ]
    }
   ],
   "source": [
    "def generador_hijo():\n",
    "    for i in range(5):\n",
    "        yield i\n",
    "\n",
    "def generador_padre():\n",
    "    yield from generador_hijo()\n",
    "\n",
    "\n",
    "for valor in generador_padre():\n",
    "    print(valor)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ¿Qué hace que una función sea generadora en Python?\n",
    "\n",
    "Como comentábamos en un inicio, la funciones se vuelven funciones generadoras por medio de la sentencia `yield`, pero ¿toda función que contenga `yield` es una función generadora? ¿necesitamos que el `yield` se ejecute para que una función sea considerada generadora?\n",
    "\n",
    "A continuación, definamos distintas funciones y veamos si Python las considera funciones generadoras o no:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'generator'>\n"
     ]
    }
   ],
   "source": [
    "def funcion1():\n",
    "    '''Función con el yield dentro del scope principal.'''\n",
    "    yield 1\n",
    "\n",
    "\n",
    "print(type(funcion1()))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'generator'>\n"
     ]
    }
   ],
   "source": [
    "def funcion2():\n",
    "    '''\n",
    "    Función con el yield dentro de un if que es falso,\n",
    "    donde además la función presenta un return.\n",
    "    '''\n",
    "    if False:\n",
    "        yield 1\n",
    "    return 1\n",
    "\n",
    "\n",
    "print(type(funcion2()))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'generator'>\n"
     ]
    }
   ],
   "source": [
    "def funcion3():\n",
    "    '''\n",
    "    Función que contiene un yield,\n",
    "    pero que nunca alcanza a ser ejecutado. \n",
    "    '''\n",
    "    return 1\n",
    "    yield 1\n",
    "\n",
    "\n",
    "print(type(funcion3()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Como podemos observar, para que una función sea considerada generadora basta con que esta **contenga un `yield`**, independientemente de si este alcanza a ser ejecutado o no."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "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.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
