{
 "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. [Iterables](#Iterables)\n",
    "    1. [Iterar sobre estructuras de datos](#Iterar-sobre-estructuras-de-datos)\n",
    "        1. [Ejemplo iterable 1](#Ejemplo-iterable-1)\n",
    "        1. [Ejemplo iterable 2](#Ejemplo-iterable-2)\n",
    "        1. [Iterador](#Iterador)\n",
    "    2. [Forma básica de hacer una estructura iterable](#Forma-básica-de-hacer-una-estructura-iterable)\n",
    "    3. [Ejemplos de iteradores personalizados](#Ejemplos-de-iteradores-personalizados)\n",
    "1. [Generadores](#Generadores)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Iterables\n",
    "\n",
    "Esta semana revisaremos técnicas que diversos lenguajes, entre ellos Python, proveen para iterar (recorrer) estructuras de datos de manera sencilla y genérica. Veremos cómo implementar estos elementos y aplicar a estructuras existentes, o a estructuras creadas por nosotros."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Iterar sobre estructuras de datos \n",
    "\n",
    "En muchas ocasiones implementaremos estructuras de datos en que resulta natural la noción de que pueden ser iteradas (recorridas). Este es el caso para estructuras que hemos visto como listas, tuplas, *sets* y diccionarios. Nos gustaría entender cómo funciona el iterar sobre estas estructuras, y aún más importante, que al momento de crear nuestras propias estructuras estas también puedan ser iteradas utilizando `for`. Para esto hay que entender dos conceptos claves: **iterable** e **iterador**.\n",
    "\n",
    "Un **iterable** es cualquier **objeto sobre el cual se puede iterar**, es decir, que se pueden ir obteniendo paulatinamente elementos del objeto. En Python, un iterable se puede recorrer mediante el uso de `for`. En concreto, un **iterable** podría aparecer al lado derecho del `in` de un `for` (`for i in iterable:`). Estructuras *built-ins* como *sets*, listas y diccionarios, son **iterables**.\n",
    "\n",
    "Un **iterable** se puede encontrar de las siguientes dos formas en Python:\n",
    "1. **Implementado el método** **`__iter__()`**. Se puede iterar todas las veces que uno quiera sobre un iterable, como es el caso de las listas. Para este caso no es necesario que el objeto iterable se pueda indexar (hacer uso de `[]` para acceder a un elemento: `iterable[posición]`). Por ejemplo, los *sets* no se indexan, pero sí podemos iterar sobre ellos.\n",
    "2. **Implementado el método** **`__getitem__()`**. El `__getitem__()` debe representar la posición de lo que se desea acceder. Cuando se pide una posición inválida se **debe** levantar una excepción de tipo `IndexError`; con eso se finalizará la iteración.\n",
    "\n",
    "> (Inicio paréntesis\n",
    "> \n",
    "> Una excepción es un objeto de Python, que nos indica la existencia de un error en el código. Esto se explica con mayor detalle en los contenidos de Excepciones, puedes revisar la ruta de aprendizaje para encontrar a qué semana corresponde este contenido.\n",
    "> \n",
    "> Fin paréntesis)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Ejemplo iterable 1\n",
    "Implementar `__iter__()`, pero no `__getitem__()`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1 3 4 6 "
     ]
    }
   ],
   "source": [
    "conjunto = {1, 3, 4, 6}\n",
    "\n",
    "for i in conjunto:\n",
    "    print(i, end=\" \")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Nota**: usamos `print(i, end=\" \")` donde `end=\" \"` para que el fin de un `print` sea solo un espacio y no un salto de línea (`\\n`). Así el print va hacia la derecha en vez de un número por línea."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Ejemplo iterable 2\n",
    "Implementar `__getitem__()`, pero no `__iter__()`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Obteniendo posición: 0\n",
      "↳ ..\n",
      "Obteniendo posición: 1\n",
      "↳ pp\n",
      "Obteniendo posición: 2\n",
      "↳ yy\n",
      "Obteniendo posición: 3\n"
     ]
    }
   ],
   "source": [
    "class DuplicarCaracteresPalabra:\n",
    "    def __init__(self, palabra_original: str) -> None:\n",
    "        self._len = len(palabra_original)\n",
    "        self._palabra = palabra_original\n",
    "\n",
    "    def __getitem__(self, index: int) -> str:\n",
    "        print('Obteniendo posición:', index)\n",
    "\n",
    "        # La palabra original levanta un IndexError\n",
    "        # cuando se intenta de acceder a un índice inválido\n",
    "        return self._palabra[index]*2\n",
    "   \n",
    "\n",
    "iterable = DuplicarCaracteresPalabra('.py')\n",
    "for elemento in iterable:\n",
    "    print('↳', elemento)\n",
    "# El for termina cuando se obtiene un IndexError,\n",
    "# por lo anterior se ve que la última posición que se intentó\n",
    "# de acceder no imprime"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Iterador\n",
    "Por otra parte, un **iterador** es un **objeto que itera sobre un iterable**, y es el objeto retornado por el método `__iter__()`. Este objeto iterador implementa el método `__next__()`, que nos retorna uno a uno los elementos de la estructura cada vez que se invoca a esta función. Cuando no quedan objetos por recorrer el iterador **debe** levantar una excepción de tipo `StopIteration`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'set_iterator'>\n",
      "1\n",
      "4\n"
     ]
    }
   ],
   "source": [
    "# iter(conjunto) nos entrega un objeto que itera sobre ese conjunto\n",
    "conjunto = {1, 4, 6}\n",
    "iterador = iter(conjunto)  # Esto es lo mismo que conjunto.__iter__()\n",
    "print(type(iterador))\n",
    "\n",
    "# Ahora vamos a invocar a next para que el iterador nos entregue\n",
    "# el siguiente valor del iterable\n",
    "print(next(iterador))      # Esto es lo mismo que iterador.__next__()\n",
    "print(iterador.__next__())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si al iterador le pedimos más elementos de los que tiene la estructura, levantará una excepción de tipo `StopIteration`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "6\n"
     ]
    },
    {
     "ename": "StopIteration",
     "evalue": "",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mStopIteration\u001b[39m                             Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[11]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;28mnext\u001b[39m(iterador))\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;28;43mnext\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43miterador\u001b[49m\u001b[43m)\u001b[49m)\n",
      "\u001b[31mStopIteration\u001b[39m: "
     ]
    }
   ],
   "source": [
    "print(next(iterador))\n",
    "print(next(iterador))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**En resumen** \n",
    "\n",
    "> DEFINICIONES SIMPLES: \n",
    "> \n",
    "> *Un **iterable** es un objeto que contiene una colección de elementos y que puede ser recorrido o **iterado** uno a uno. Un **iterador** es un objeto que proporciona un mecanismo para **iterar sobre un iterable**, avanzando de un elemento al siguiente hasta que se han recorrido todos los elementos.*\n",
    "\n",
    ">\n",
    "> Un **iterable** se puede utilizar/recorrer múltiples veces; en cambio un **iterador** solo puede utilizarse una única vez.  \n",
    "> \n",
    "> Esto se debe a que un **iterable** tiene el método `__iter__` implementado, y este **siempre** retorna un iterador.\n",
    "> \n",
    "> Por su parte, un **iterador** es un objeto que tiene el método `__next__` implementado, es decir puedo hacer `next(iterador)` y esto retornará un **valor**. Cuando el iterador se queda sin elementos que recorrer, levanta una excepción."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "tags": [
     "hacer_iterables"
    ]
   },
   "source": [
    "## Forma básica de hacer una estructura iterable\n",
    "\n",
    "Ahora veremos la forma más básica de crear un iterable e iterador que nos permita recorrer una lista de números. Para esto crearemos nuestras propias **clases iteradora e iterable**.\n",
    "\n",
    "**Nota:** Hay más formas de hacer que una estructura sea iterable sin crear una nueva clase, entre ellas una que veremos [más adelante](#Otra-forma-de-hacer-iterable-una-estructura-propia)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Empezamos creando una clase iterable (`IterableListaNumeros`), la cual implementará el método `__iter__`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "from __future__ import annotations \n",
    "\n",
    "# Declaramos la clase IteradorListaNumeros,\n",
    "# pero no la definimos. Esto lo haremos\n",
    "# mas adelante\n",
    "class IteradorListaNumeros:\n",
    "    pass\n",
    "\n",
    "class IterableListaNumeros:\n",
    "    def __init__(self, objeto: list) -> None:\n",
    "        self.objeto = objeto\n",
    "    \n",
    "    def __iter__(self) -> IteradorListaNumeros:\n",
    "        return IteradorListaNumeros(self.objeto)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora creamos una clase iterador (`IteradorListaNumeros`), la cual será la encargada de iterar sobre el iterable.\n",
    "\n",
    "Esta clase debe implementar:\n",
    "* El método `__next__` que retornará los valores hasta que el iterador se queda sin elementos.\n",
    "* El método `__iter__` que sólo debe retornar una referencia a la instancia de iterador (`self`).\n",
    "\n",
    "**Nota:** Es posible no implementar el método `__iter__` en un iterador, pero no será posible obtener el iterador de forma manual (`iterador = iter(iterable)`) y luego usar el iterador directamente en el `for`.\n",
    "\n",
    "**Importante:** En general, los iteradores no se pueden reiniciar."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Es necesario tener una versión de\n",
    "# Python >= 3.11.x para que esto funcione\n",
    "# Si no le funciona, actualice su versión\n",
    "from typing import Self\n",
    "\n",
    "\n",
    "class IteradorListaNumeros:\n",
    "    def __init__(self, iterable: list) -> None:\n",
    "        # Hacemos una copia del iterable para no afectar los valores originales\n",
    "        self.iterable = iterable.copy()\n",
    "    \n",
    "    def __iter__(self) -> Self: \n",
    "        return self\n",
    "    \n",
    "    def __next__(self) -> int:\n",
    "        if not self.iterable:\n",
    "            # Levantamos una excepción del tipo StopIteration\n",
    "            # con el mensaje \"Llegamos al final\".\n",
    "            raise StopIteration(\"Llegamos al final\")\n",
    "        else:\n",
    "            valor = self.iterable.pop(0)\n",
    "            return valor"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora vamos a generar nuestro iterable con los datos de una lista."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1 2 3 4 5 "
     ]
    }
   ],
   "source": [
    "datos = [1, 2, 3, 4, 5]\n",
    "iterable = IterableListaNumeros(datos)\n",
    "for i in iterable:\n",
    "    print(i, end=\" \")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Recordemos que un iterable se puede iterar las veces que uno quiera. Si volvermos a hacer el `for`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1 2 3 4 5 "
     ]
    }
   ],
   "source": [
    "for i in iterable:\n",
    "    print(i, end=\" \")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Por otro lado, si usamos un **iterador**, este solo funcionará una vez."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Primer intento:\n",
      "1 2 3 4 5 \n",
      "\n",
      "Segundo intento:\n"
     ]
    }
   ],
   "source": [
    "iterador = IteradorListaNumeros(datos)\n",
    "# Primer intento\n",
    "print(f'Primer intento:')\n",
    "for i in iterador:\n",
    "    print(i, end=\" \")\n",
    "print('\\n')\n",
    "    \n",
    "# Segundo intento con el mismo iterador\n",
    "print(f'Segundo intento:')\n",
    "for i in iterador:\n",
    "    print(i, end=\" \")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Por este motivo, cada vez que queramos tener una conjunto de datos iterable, tenemos que construir un `IterableListaNumeros` cuyo método `__iter__` retorne un nuevo `IteradorListaNumeros`. De este modo, cada vez que hagamos `for`, se retorna un nuevo `IteradorListaNumeros`.\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora, vemos un nuevo caso:  **si el iterador no implementa `__iter__` ni `__getitem__`**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "from __future__ import annotations\n",
    "\n",
    "\n",
    "class IterableListaNumerosMalo:\n",
    "    \n",
    "    def __init__(self, objeto: list) -> None:\n",
    "        self.objeto = objeto\n",
    "    \n",
    "    def __iter__(self) -> IteradorListaNumerosMalo:\n",
    "        return IteradorListaNumerosMalo(self.objeto)\n",
    "\n",
    "    \n",
    "class IteradorListaNumerosMalo:\n",
    "    \n",
    "    def __init__(self, iterable: list) -> None:\n",
    "        self.iterable = iterable.copy()\n",
    "    \n",
    "    def __next__(self) -> int:\n",
    "        if not self.iterable:\n",
    "            raise StopIteration(\"Llegamos al final\")\n",
    "        else:           \n",
    "            valor = self.iterable.pop(0)\n",
    "            return valor"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "ename": "TypeError",
     "evalue": "'IteradorListaNumerosMalo' object is not iterable",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
      "Cell \u001b[0;32mIn[17], line 3\u001b[0m\n\u001b[1;32m      1\u001b[0m iterable \u001b[38;5;241m=\u001b[39m IterableListaNumerosMalo(datos)\n\u001b[1;32m      2\u001b[0m iterador \u001b[38;5;241m=\u001b[39m \u001b[38;5;28miter\u001b[39m(iterable)\n\u001b[0;32m----> 3\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m i \u001b[38;5;129;01min\u001b[39;00m iterador:\n\u001b[1;32m      4\u001b[0m     \u001b[38;5;28mprint\u001b[39m(i, end\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m \u001b[39m\u001b[38;5;124m\"\u001b[39m)\n",
      "\u001b[0;31mTypeError\u001b[0m: 'IteradorListaNumerosMalo' object is not iterable"
     ]
    }
   ],
   "source": [
    "iterable = IterableListaNumerosMalo(datos)\n",
    "iterador = iter(iterable)\n",
    "for i in iterador:\n",
    "    print(i, end=\" \")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esto ocurre porque `IteradorListaNumerosMalo` no tiene `__iter__` ni `__getitem__` y por lo tanto, para el `for` esta clase no es **iterable**.\n",
    "\n",
    "Por lo tanto, con esto podemos notar que un **iterador** es un **iterable** en sí mismo; es decir, es un tipo de **iterable**\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cómo se mencionó anteriormente, un iterador solo se puede recorrer una vez. La ventaja de esto es que podemos iterrumpir el recorrido y luego continuar desde el punto en que lo dejamos:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1 2 3 "
     ]
    }
   ],
   "source": [
    "iterable = IterableListaNumeros(datos)\n",
    "\n",
    "iterador = iter(iterable)\n",
    "for i in iterador:\n",
    "    print(i, end=\" \")\n",
    "    if i >= 3:\n",
    "        break"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "4 5 "
     ]
    }
   ],
   "source": [
    "for i in iterador:\n",
    "    print(i, end=\" \")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para empezar nuevamente a iterar, debemos **obtener otro iterador**. El código de abajo funciona, debido a que cuando se invoca otra vez la función `__iter__` del iterable debido al `for`, se retorna un **nuevo iterador**."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1 2 3 4 5 "
     ]
    }
   ],
   "source": [
    "for i in iterable:\n",
    "    print(i, end=\" \")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cada iterador tiene su propia memoria sobre cuál es el siguiente valor a acceder, la cual **no depende del iterable**. Para verlo, creamos dos iteradores:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [],
   "source": [
    "iterador_1 = iter(iterable)\n",
    "iterador_2 = iter(iterable)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Utilizamos el primero sólo tres veces:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1 2 3 "
     ]
    }
   ],
   "source": [
    "for i in iterador_1:\n",
    "    print(i, end=\" \")\n",
    "    if i >= 3:\n",
    "        break"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y el segundo iterador recorre la colección otra vez, independiente de lo recorrido por el primero."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1 2 3 4 5 "
     ]
    }
   ],
   "source": [
    "for i in iterador_2:\n",
    "    print(i, end=\" \")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Pero si volvemos a recorrer la estructura con el primer iterador (que sólo usamos tres veces), continuaremos desde donde lo dejamos:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "4 5 "
     ]
    }
   ],
   "source": [
    "for i in iterador_1:\n",
    "    print(i, end=\" \")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Por último, una vez que el iterador agotó la estructura de datos no lo podemos utilizar de nuevo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [],
   "source": [
    "for i in iterador_1:\n",
    "    print(i, end=\" \")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejemplos de iteradores personalizados\n",
    "\n",
    "Un iterador no solo nos permite recorrer los elementos de un objeto iterable, sino que también permite definir la forma en que estos elementos serán recorridos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "datos = [5, 1, 3, 2, 4]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Actualmente podemos según el orden propio del iterable:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "5 1 3 2 4 "
     ]
    }
   ],
   "source": [
    "from typing import Self\n",
    "\n",
    "\n",
    "class IteradorListaNumerosOriginal:\n",
    "    def __init__(self, iterable: list) -> None:\n",
    "        self.iterable = iterable.copy()\n",
    "\n",
    "    def __iter__(self) -> Self: \n",
    "        return self\n",
    "\n",
    "    def __next__(self) -> int:\n",
    "        if not self.iterable:\n",
    "            raise StopIteration(\"Llegamos al final\")\n",
    "        else:\n",
    "            valor = self.iterable.pop(0)\n",
    "            return valor\n",
    "\n",
    "\n",
    "iterable = IteradorListaNumerosOriginal(datos)\n",
    "for i in iter(iterable):\n",
    "    print(i, end=\" \")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Pero nos podemos asegurar de recorrer los elementos de forma ordenada:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1 2 3 4 5 "
     ]
    }
   ],
   "source": [
    "from typing import Self\n",
    "\n",
    "\n",
    "# Aprovechamos la herencia para no declarar las funciones que se mantienen\n",
    "class IteradorListaNumerosOrdenada(IteradorListaNumerosOriginal):\n",
    "    def __iter__(self) -> Self:\n",
    "        # Ordenamos los elementos del iterable antes de empezar a recorrerlos\n",
    "        self.iterable.sort()\n",
    "        return self\n",
    "\n",
    "iterable = IteradorListaNumerosOrdenada(datos)\n",
    "for i in iter(iterable):\n",
    "    print(i, end=\" \")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "O de recorrer los elementos de forma aleatoria:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "3 1 5 2 4 "
     ]
    }
   ],
   "source": [
    "from random import shuffle\n",
    "from typing import Self\n",
    "\n",
    "\n",
    "class IteradorListaNumerosAleatoria(IteradorListaNumerosOriginal):    \n",
    "    def __iter__(self) -> Self:\n",
    "        # Mezclamos los elementos del iterable antes de empezar a recorrerlos\n",
    "        shuffle(self.iterable)\n",
    "        return self\n",
    "\n",
    "\n",
    "iterable = IteradorListaNumerosAleatoria(datos)\n",
    "for i in iter(iterable):\n",
    "    print(i, end=\" \")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Adicionalmente, los iterables e iteradores no aplican únicamente para listas y números, también los podemos utilizar sobre otras estructuras de datos (tuplas, _sets_, etc.) y cualquier dato que estas contengan (_strings_, clases, etc.).\n",
    "\n",
    "Por ejemplo, podemos recorrer un _set_ que contiene _strings_ y hacerlos de forma ordenada en base al largo de cada texto:  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "en un tres trigo tigres trigal tristes trastos tragaban "
     ]
    }
   ],
   "source": [
    "from typing import Self\n",
    "\n",
    "\n",
    "def ordenar_por_largo(valor: str) -> int:\n",
    "    return len(valor)\n",
    "\n",
    "class IteradorSetStringsOrdenado:\n",
    "    def __init__(self, iterable: str) -> None:\n",
    "        self.iterable = iterable.copy()\n",
    "\n",
    "    def __iter__(self) -> Self:\n",
    "        self.iterable = list(self.iterable)\n",
    "        self.iterable.sort(key=ordenar_por_largo)\n",
    "        return self\n",
    "    \n",
    "    def __next__(self) -> str:\n",
    "        if not self.iterable:\n",
    "            raise StopIteration(\"Llegamos al final\")\n",
    "        else:\n",
    "            valor = self.iterable.pop(0)\n",
    "            return valor\n",
    "\n",
    "trabalenguas = '''tres tristes tigres\n",
    "tragaban trigo en un trigal\n",
    "en tres tristes trastos\n",
    "tragaban trigo tres tristes tigres'''.replace('\\n', ' ').split(' ')\n",
    "trabalenguas = set(trabalenguas)\n",
    "\n",
    "iterable = IteradorSetStringsOrdenado(trabalenguas)\n",
    "for i in iter(iterable):\n",
    "    print(i, end=\" \")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Generadores\n",
    "\n",
    "Los **generadores** son un caso especial de los **iteradores**. Los generadores nos permiten iterar sobre secuencias de datos sin la necesidad de almacenarlos en alguna estructura especial, evitando el uso innecesario de memoria.\n",
    "\n",
    "Una vez que terminamos de iterar sobre un generador, el generador desaparece. Esto es muy útil cuando queremos realizar cálculos sobre secuencias de números que sólo nos sirven para ese cálculo en particular. La sintaxis para crear generadores es muy parecida a la comprensión de listas, sólo que en vez de paréntesis cuadrados `[]` usamos paréntesis normales `()`.\n",
    "\n",
    "Por ejemplo, crearemos un generador para los números pares del 0 al 18:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Por el sólo hecho de usar paréntesis estamos creando un generador.\n",
    "generador_pares = (2 * i for i in range(10))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'generator'>\n"
     ]
    }
   ],
   "source": [
    "print(type(generador_pares))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora, vamos a mostrar lo que nos entrega con un `for`. Esto es posible ya que **los generadores implementan `__iter__`** retornando `self`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Recorremos el generador:\n",
      "0 2 4 6 8 10 12 14 16 18 "
     ]
    }
   ],
   "source": [
    "print(f'Recorremos el generador:')\n",
    "for i in generador_pares:\n",
    "    print(i, end=\" \")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Nuevamente, como los generadores son un caso especial de iteradores, estos solo se pueden usar vez. Si queremos volver a acceder a los datos que el generador contenía, tendremos que crear otro generador con la misma información."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Recorremos el generador:\n"
     ]
    }
   ],
   "source": [
    "print(f'Recorremos el generador:')\n",
    "for i in generador_pares:\n",
    "    print(i, end=\" \")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Recordar también que se puede usar `next`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0\n",
      "2\n"
     ]
    }
   ],
   "source": [
    "generador_pares = (2 * i for i in range(10))\n",
    "print(next(generador_pares))\n",
    "print(next(generador_pares))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> ⚠️ **Nota importante**\n",
    ">\n",
    "> En los generadores podemos usar condiciones de dos formas:\n",
    ">\n",
    "> - Si usamos `if` al final, estamos **filtrando elementos**, es decir,  \n",
    "> solo se generan aquellos que cumplen la condición.\n",
    ">\n",
    "> - Si usamos `if-else` dentro del generador, estamos **transformando los valores**,  \n",
    "> es decir, todos los elementos se generan, pero algunos pueden modificarse.\n",
    ">\n",
    "> 🎯 **Idea clave:**  \n",
    "> El `if` al final **filtra**, mientras que el `if-else` inline **transforma**.\n",
    ">\n",
    "> A continuación, puedes revisar algunos ejemplos de código para ver la diferencia."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0 1 2 "
     ]
    }
   ],
   "source": [
    "# Filtra: solo deja pasar valores menores a 3\n",
    "generador = (i for i in range(10) if i < 3)\n",
    "\n",
    "for x in generador:\n",
    "    print(x, end=\" \")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0 1 2 3 3 3 3 3 3 3 "
     ]
    }
   ],
   "source": [
    "# Transforma: si i < 3 mantiene i, si no reemplaza por 3\n",
    "generador = (i if i < 3 else 3 for i in range(10))\n",
    "\n",
    "for x in generador:\n",
    "    print(x, end=\" \")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora, veamos cuánta memoria ocupa nuestro generador, *versus* una lista que contiene todos los resultados. Para ello ocuparemos la función [`getsizeof`](https://docs.python.org/3/library/sys.html#sys.getsizeof)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sys import getsizeof"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Bytes del generador: 104\n",
      "Bytes de la lista: 184\n"
     ]
    }
   ],
   "source": [
    "generador_pares = (2 * i for i in range(10))\n",
    "lista_pares = [2 * i for i in range(10)]\n",
    "\n",
    "print(\"Bytes del generador:\", getsizeof(generador_pares))\n",
    "print(\"Bytes de la lista:\", getsizeof(lista_pares))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esta diferencia es más grande si aumentamos la cantidad de resultados:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Bytes del generador: 104\n",
      "Bytes de la lista: 8448728\n"
     ]
    }
   ],
   "source": [
    "generador_pares = (2 * i for i in range(10 ** 6))\n",
    "lista_pares = [2 * i for i in range(10 ** 6)]\n",
    "\n",
    "print(\"Bytes del generador:\", getsizeof(generador_pares))\n",
    "print(\"Bytes de la lista:\", getsizeof(lista_pares))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Concluimos que una ventaja de los generadores es que consumen mucho menos memoria, ya que **generan** cada nuevo elemento de la secuencia cuando se le solicita, y no mantienen todos los elementos de la secuencia en memoria. Es particularmente útil cuando queremos leer archivos con muchos datos. En lugar de usar una instrucción `archivo.readlines()` para leer todos los datos de una sola vez en memoria (supongamos que queremos leer un archivo de 1GB), podemos abrir el archivo, y usar un generador para extraer una línea a la vez y así evitar llenar la memoria. En la semana de **Programación funcional** nos adentraremos más en el tema y aprenderemos a construir funciones generadoras que abra un archivo, y entregue una línea del archivo cada vez."
   ]
  }
 ],
 "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.11.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
