{
 "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",
    "<font size='1'> Modificado en 2018-1 al 2026-1 por Equipo Docente IIC2233</font>\n",
    "</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Tabla de contenidos\n",
    "\n",
    "1. [Funciones _built-in_ en Python](#Funciones-built-in-en-Python)\n",
    "    1. [`reversed`](#reversed)\n",
    "    2. [Otras funciones *built-in*](#Otras-funciones-built-in)\n",
    "2. [Librerías *built-in*](#Librerías-built-in)\n",
    "    1. [`itertools`](#itertools)\n",
    "    2. [`collections`](#collections)\n",
    "3. [Casos aplicados](#Casos-aplicados)\n",
    "    1. [Consultas anidadas](#Consultas-anidadas)\n",
    "    2. [Unir generadores](#Unir-generadores)\n",
    "    3. [Eficiencia (Generadores vs Estructuras que usan memoria)](#Eficiencia-(Generadores-vs-Estructuras-que-usan-memoria))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Funciones *built-in* en Python\n",
    "\n",
    "Existen muchas funciones que vienen implementadas en Python, principalmente con el propósito de simplificar y \n",
    "abstraer cálculos que pueden aplicar a objetos de clases distintas (*duck typing*). Pueden revisar todas ellas en la [documentación de funciones](https://docs.python.org/3/library/functions.html) de Python. Veamos algunos ejemplos:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### `reversed`\n",
    "\n",
    "La función `reversed()` toma una **secuencia** cualquiera como input y retorna **una copia de la secuencia** en orden inverso. También podemos personalizar la función haciendo *overriding* de `__reversed__` en cada clase. \n",
    "\n",
    "Si no personalizamos el método `__reversed__`, se usará el *built-in* que iterará usando `__getitem__` y `__len__`. En ese caso, se itera `__len__` veces sobre el objeto usando `__getitem__` hacia atrás.\n",
    "\n",
    "Por ejemplo, podemos definir un tipo especial de lista que hace *override* de `__reversed__`. En este caso, intercambia la primera mitad con la segunda, en vez de invertir el orden de los elementos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Clase list: 6, 5, 4, 3, 2, 1\n",
      "Clase ListaReversaMitad: 4, 5, 6, 1, 2, 3\n"
     ]
    }
   ],
   "source": [
    "from functools import reduce\n",
    "\n",
    "\n",
    "class ListaReversaMitad(list):\n",
    "    def __init__(self, *args) -> None:\n",
    "        super().__init__(args)\n",
    "        \n",
    "    def __reversed__(self) -> list:\n",
    "        mitad = len(self) // 2\n",
    "        return self[mitad:] + self[:mitad]\n",
    "    \n",
    "\n",
    "lista = [1, 2, 3, 4, 5, 6]\n",
    "\n",
    "for secuencia in lista, ListaReversaMitad(*lista):\n",
    "    print(f\"Clase {type(secuencia).__name__}: \", end=\"\")\n",
    "    print(reduce(lambda x, y: f'{x}, {y}', reversed(secuencia)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Otras funciones *built-in*\n",
    "\n",
    "Además de las funciones antes nombradas, Python cuenta con otras [funciones built-in](https://docs.python.org/3/library/functions.html) que también puedes ser aplicadas sobre iterables. \n",
    "\n",
    "Hay funciones que tienen un enfoque más matemático:\n",
    "* `sum()`\n",
    "* `min()`\n",
    "* `max()`\n",
    "\n",
    "Y otras con un enfoque booleano:\n",
    "* `all()`\n",
    "* `any()`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un caso donde es útil utilizar estas funciones cuando:\n",
    "\n",
    "1. Queremos obtener la cantidad de elementos de un generador, sin tener que transformarlo en otra estructura de datos.  \n",
    "     \n",
    "   Por defecto, los generadores no presentan el método `__len__()`, por lo que no es posible obtener su largo utilizando dicho método:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "TypeError: object of type 'generator' has no len()\n"
     ]
    }
   ],
   "source": [
    "generador = (i for i in range(10))\n",
    "try:\n",
    "    largo = len(generador)\n",
    "except TypeError as error:\n",
    "    print('TypeError:', error)\n",
    "else:\n",
    "    print(f'Largo generador: {largo}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "   Pero haciendo uso de la función built-in `sum`, es posible calcular el porte del generador:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Largo generador: 10\n"
     ]
    }
   ],
   "source": [
    "generador = (i for i in range(10))\n",
    "\n",
    "largo = sum(1 for _ in generador)\n",
    "print(f'Largo generador: {largo}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2. Cuando queremos verificar si todos o alguno de los elementos de un iterble cumplen con alguna condición en específico."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Todos los elementos del iterable son impares: False\n",
      "El iterable contiene al menos un elemento par: True\n"
     ]
    }
   ],
   "source": [
    "iterable = {1, 3, 5, 11, 8, 9}\n",
    "\n",
    "todos_son_impar = all(map(lambda x: x % 2 == 0, iterable))\n",
    "contiene_un_par = any(map(lambda x: x % 2 == 1, iterable))\n",
    "\n",
    "print(f'Todos los elementos del iterable son impares: {todos_son_impar}')\n",
    "print(f'El iterable contiene al menos un elemento par: {contiene_un_par}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Librerías *built-in*\n",
    "\n",
    "Dentro del contexto de Python, también tenemos librerías *built-in* que nos permitirán trabajar con mayor facilidad las consultas relacionadas a la programación funcional."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### `itertools`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La [librería *built-in* `itertools`](https://docs.python.org/3/library/itertools.html), la cual cuenta con funciones especializadas en trabajar con iteradores. Dentro de este módulo podemos encontrar:\n",
    "* Funciones que entregan iteradores infinitos:\n",
    "    * `count()`\n",
    "    * `cycle()`\n",
    "    * `repeat()`\n",
    "* Funciones orientadas en la combinatoria:\n",
    "    * `product()`\n",
    "    * `permutations()`\n",
    "    * `combinations()`\n",
    "* y muchos más...\n",
    "\n",
    "A continuación veremos algunos ejemplos, pero te invitamos a investigar más al respecto y evaluar casos donde estas funciones puedas ser de utilidad."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. La función `count()` entrega un generador infinito que nos permite contar desde `0` aumentando los valores en `1`. Esta función nos permite cambiar el valor desde el que queremos contar (`start`) o el valor que será utilizado para aumentar el contador (`step`)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "22\n",
      "22.5\n",
      "23.0\n",
      "23.5\n",
      "24.0\n",
      "24.5\n"
     ]
    }
   ],
   "source": [
    "from itertools import count\n",
    "\n",
    "contador = count(22, 0.5)\n",
    "\n",
    "for _ in range(6):\n",
    "    print(next(contador))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2. La función `combinations()` recibe un iterable y número entero `r`, entrega un generador con la combinatoria de los distintos elementos del iterable, donde cada combinación entregada es de porte `r`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[('A', 'B'), ('A', 'C'), ('A', 'D'), ('B', 'C'), ('B', 'D'), ('C', 'D')]\n",
      "[('A', 'B', 'C', 'D')]\n"
     ]
    }
   ],
   "source": [
    "from itertools import combinations\n",
    "\n",
    "combinaciones_grupos_2 = combinations('ABCD', 2)\n",
    "combinaciones_grupos_4 = combinations('ABCD', 4)\n",
    "\n",
    "print(list(combinaciones_grupos_2))\n",
    "print(list(combinaciones_grupos_4))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### `collections`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Por otro lado, [la librería *built-in* `collections`](https://docs.python.org/3/library/collections.html) nos entrega alternativas a *dicts*, *lists*, *sets* y *tuples*.\n",
    "\n",
    "Entre los métodos y objetos que ya hemos visto podemos encontrar:\n",
    "* `namedtuple` \n",
    "* `deque` \n",
    "* `defaultdict`\n",
    "\n",
    "Pero también cuenta con otros objetos como:\n",
    "* `Counter`\n",
    "\n",
    "Veamos un ejemplos:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. El objeto `Counter` es una subclase de diccionario que permite contar los elementos de un iterable. Cada elementos será almacenado como llave del diccionario y sus recuentos, como valores del mismo.  \n",
    "     \n",
    "   Es importante tener en consideración que para que `Counter` funcione correctamente, los elementos del iterable deben ser hasheables."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Counter({2: 11, 3: 6, 1: 3})"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from collections import Counter\n",
    "from random import randint\n",
    "\n",
    "iterable = [randint(1, 3) for _ in range(20)]\n",
    "contador = Counter(iterable)\n",
    "contador"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Además, el objeto `Counter` no solo nos permite obtener la cantidad de veces que se repite cada elemento del iterable, también lo podemos utilizar para encontrar los elementos más repetidos dentro del mismo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[('i', 6), ('s', 4), ('a', 3)]"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "texto = 'supercalifragilisticoespialidoso'\n",
    "contador = Counter(texto)  # Recuerda que los strings también son iterables\n",
    "contador.most_common(3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Casos aplicados\n",
    "\n",
    "Finalmente, para ir cerrando este contenido, veamos algunos casos aplicados donde se haga uso de los contenidos vistos hasta el momento."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Consultas anidadas\n",
    "\n",
    "Una de las grandes características que presenta la programación funcional, es que podemos anidar las consultas que se realizan, siempre y cuando estas retornen un generador o algún tipo de estructura iterable."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Por ejemplo, asumiendo que tenemos un generador con información de distintas lenguas extintas o en peligro de extinción, donde cada grupo de datos contiene:\n",
    "* ID\n",
    "* Nombre de la lengua\n",
    "* Países donde se habla (separador por ;)\n",
    "* El código de los países donde se habla (separador por ;)\n",
    "* El grado de peligro\n",
    "* La cantidad de hablantes"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "from collections import namedtuple\n",
    "from typing import Generator\n",
    "\n",
    "\n",
    "LenguaExtinta = namedtuple('LenguaExtinta', 'id, name, country, country_code, endangerment, speakers')\n",
    "\n",
    "def generador_lenguas_extintas() -> Generator:\n",
    "    yield LenguaExtinta(349, 'Juang', 'India', 'IND', 'Definitely endangered', 230000)\n",
    "    yield LenguaExtinta(350, 'Tiwa', 'India', 'IND', 'Definitely endangered', 230000)\n",
    "    yield LenguaExtinta(351, 'Embera (Panama)','Panama', 'PAN', 'Vulnerable', 224850)\n",
    "    yield LenguaExtinta(352, 'Ediamat', 'Guinea-Bissau', 'GNB', 'Vulnerable', 220250)\n",
    "    yield LenguaExtinta(353, 'Mara', 'India', 'IND', 'Definitely endangered', 220000)\n",
    "    yield LenguaExtinta(354, 'Maring', 'India', 'IND', 'Vulnerable', 220000)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A partir del generador podríamos anidar consultas para encontrar la cantidad total de hablantes de la India, que saben lenguas que se encuentran en un estado vulnerable."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Los hablantes totales son: 220000\n"
     ]
    }
   ],
   "source": [
    "lenguas_india = filter(lambda lengua: lengua.country == 'India', generador_lenguas_extintas())\n",
    "lenguas_vulnerables = filter(lambda lengua: lengua.endangerment == 'Vulnerable', lenguas_india)\n",
    "hablantes_totales = sum(map(lambda lengua: lengua.speakers, lenguas_vulnerables))\n",
    "\n",
    "print('Los hablantes totales son:', hablantes_totales)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Otra forma de haber logrado lo anterior, es haber unido consultas: ya sea por medio de las funciones lambda o de los inputs de cada una:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Los hablantes totales son: 220000\n"
     ]
    }
   ],
   "source": [
    "hablantes_totales = sum(\n",
    "    map(lambda lengua: lengua.speakers, \n",
    "        filter(lambda lengua: (lengua.country == 'India') and (lengua.endangerment == 'Vulnerable'),\n",
    "               generador_lenguas_extintas())))\n",
    "\n",
    "print('Los hablantes totales son:', hablantes_totales)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Unir generadores\n",
    "\n",
    "En múltiples ocasiones deberemos unir dos o generadores en base a distintas condiciones.\n",
    "\n",
    "En este caso, trabajaremos con dos generadores de números y busquemos todos los pares que al mezclarlos sumen un múltiplo de 5. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[(17, 38), (6, 29), (6, 39), (6, 29), (10, 40), (10, 30), (20, 40), (20, 30), (4, 41), (4, 21), (4, 31), (8, 32), (11, 29), (11, 39), (11, 29), (5, 40), (5, 30), (2, 38), (16, 29), (16, 39), (16, 29)]\n"
     ]
    }
   ],
   "source": [
    "from itertools import product\n",
    "from random import randint\n",
    "\n",
    "\n",
    "generador_1 = (randint(0, 20) for _ in range(10))\n",
    "generador_2 = (randint(20, 50) for _ in range(10))\n",
    "\n",
    "combinaciones = product(generador_1, generador_2)\n",
    "\n",
    "# Es importante notar que 'product' nos entrega un generador que contiene tuplas\n",
    "# con los pares de elementos de ambos generadores, por lo que al momento de querer\n",
    "# aplicar una función sobre estos elementos, deberemos hacer indexación sobre ellos\n",
    "# o utilizar funciones que operen sobre iteradores.\n",
    "pares_multiplos_de_5 = filter(lambda x: (x[0] + x[1]) % 5 == 0, combinaciones)\n",
    "\n",
    "print(list(pares_multiplos_de_5))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Agrupar información a través de diccionarios\n",
    "\n",
    "A veces será necesario agrupar la información de un generador por medio de diccionarios. A continuación, evaluaremos diversas formas de lograr esto mismo.\n",
    "\n",
    "Aprovechemos el listado de lenguas extintas y en peligro de extinción que usamos antes, y agrupemos la cantidad de lenguas que hay por país. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "from collections import namedtuple\n",
    "from typing import Generator\n",
    "\n",
    "\n",
    "LenguaExtinta = namedtuple('LenguaExtinta', 'id, name, country, country_code, endangerment, speakers')\n",
    "\n",
    "def generador_lenguas_extintas() -> Generator:\n",
    "    yield LenguaExtinta(349, 'Juang', 'India', 'IND', 'Definitely endangered', 230000)\n",
    "    yield LenguaExtinta(350, 'Tiwa', 'India', 'IND', 'Definitely endangered', 230000)\n",
    "    yield LenguaExtinta(351, 'Embera (Panama)','Panama', 'PAN', 'Vulnerable', 224850)\n",
    "    yield LenguaExtinta(352, 'Ediamat', 'Guinea-Bissau', 'GNB', 'Vulnerable', 220250)\n",
    "    yield LenguaExtinta(353, 'Mara', 'India', 'IND', 'Definitely endangered', 220000)\n",
    "    yield LenguaExtinta(354, 'Maring', 'India', 'IND', 'Vulnerable', 220000)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. Utilizando estructuras por comprensión:  \n",
    "     \n",
    "   **Nota:** Dado que en este ejercicio recorreremos el generador en más de una ocasión, lo transformaremos en una lista para facilitar la lectura; Para hacerlo en programación funcional se puede usar `tee` de `itertools`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'Panama': 1, 'India': 4, 'Guinea-Bissau': 1}\n"
     ]
    }
   ],
   "source": [
    "países = map(lambda lengua: lengua.country, generador_lenguas_extintas())\n",
    "lista_países = list(países)\n",
    "\n",
    "contador = {país: lista_países.count(país) for país in set(lista_países)}\n",
    "print(contador)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2. Utilizando el objeto `Counter` de `collections`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Counter({'India': 4, 'Panama': 1, 'Guinea-Bissau': 1})\n"
     ]
    }
   ],
   "source": [
    "from collections import Counter\n",
    "\n",
    "países = map(lambda lengua: lengua.country, generador_lenguas_extintas())\n",
    "contador = Counter(países)\n",
    "print(contador)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "3. Utilizando `reduce`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'India': 4, 'Panama': 1, 'Guinea-Bissau': 1}\n"
     ]
    }
   ],
   "source": [
    "from functools import reduce\n",
    "\n",
    "\n",
    "def función_auxiliar(diccionario, lengua) -> dict:\n",
    "    país = lengua.country\n",
    "    \n",
    "    if país not in diccionario:\n",
    "        diccionario[país] = 0\n",
    "\n",
    "    diccionario[lengua.country] += 1\n",
    "    return diccionario\n",
    "\n",
    "contador = reduce(función_auxiliar, generador_lenguas_extintas(), {})\n",
    "print(contador)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Eficiencia (Generadores vs Estructuras que usan memoria)\n",
    "\n",
    "Finalmente, realicemos algunos ejemplos aplicados donde comparemos la realizar consultas utilizando unicamente generadores vs estructuras que utilizan memoria (*lists*, *dicts*, *tuples*, *sets*, ...).\n",
    "\n",
    "\n",
    "**Consideraciones**\n",
    "> Debido a que los códigos de esta sección están limitados para ser ejecutados en un tiempo límite, en este _notebook_ encontrarás un extracto de su implementación como texto y los resultados de la ejecución de los mismos. Estos fueron ejecutados usando `python -B -m unittest -v` dentro de la carpeta `código_eficiencia/`; el `-B` para que no se use el caché y así tener valores mas comparables.\n",
    "> \n",
    "> Para estos tests, seguiremos utilizando el archivo de lenguas extintas."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. **Leer archivos línea por línea vs Leer el archivo completo.**  \n",
    "     \n",
    "   Al momento de leer el contenido de un archivo existe tanto la posibilidad de revisar el contenido del archivo de apoco (`for line in file`) o cargar todo el contenido del archivo (`file.readlines()`). Veamos cómo se comporta el código cuando accedemos al contenido de distintas maneras.  \n",
    "      \n",
    "    ```python\n",
    "    def cargar_archivo_generador() -> Generator:\n",
    "        with open(ruta_archivo, encoding='utf-8') as file:\n",
    "            for line in file:\n",
    "                    yield line.strip().split(',')\n",
    "    \n",
    "    def cargar_archivo_memoria() -> Generator:\n",
    "        with open(ruta_transacciones, encoding='utf-8') as file:\n",
    "            for line in file.readlines():\n",
    "                yield line.strip().split(',')\n",
    "    ```  \n",
    "      \n",
    "    Al ejecutar los códigos, podemos observar los siguientes tiempos totales de ejecución son muy similares. Sin embargo, podemos observar los siguientes usos de memoria:\n",
    "    ```txt\n",
    "        Uso memoria cargar_archivo_generador        50.32 KB\n",
    "        Uso memoria cargar_archivo_memoria         271.92 KB\n",
    "    ```\n",
    "\n",
    "    Ojo que pese a que el tiempo es similar, para archivos mas grandes le tomará mas tiempo a python el asignar más memoria, por lo que el tiempo de `cargar_archivo_memoria` será considerablemente mayor al de `cargar_archivo_generador`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2. **Cargar los datos como `namedtuple` vs Cargar los datos como clases.**  \n",
    "      \n",
    "    Otro punto importante a tener en consideración el momento de trabajar con grandes cantidades de datos, es ver si se cargará la información como una `namedtuple` o si se utilizarán clases personalizadas.\n",
    "\n",
    "    ```python\n",
    "    def cargar_namedtuples() -> Generator:\n",
    "        with open(ruta_archivo, encoding='utf-8') as file:\n",
    "            file.readline()\n",
    "            for line in file:\n",
    "                info = (int(x) for x in line.strip().split(',')[::5])\n",
    "                yield NamedTupleLengua(*info)\n",
    "\n",
    "    def cargar_clases() -> Generator:\n",
    "        with open(ruta_archivo, encoding='utf-8') as file:\n",
    "            file.readline()\n",
    "            for line in file:\n",
    "                info = (int(x) for x in line.strip().split(',')[::5])\n",
    "                yield ClaseLengua(*info)\n",
    "    ```  \n",
    "      \n",
    "    Al ejecutar los códigos, podemos observar los siguientes tiempos totales de ejecución son muy similares. Sin embargo, podemos observar los siguientes usos de memoria gracias al uso de `namedtuple`:\n",
    "    ```txt\n",
    "        Uso memoria cargar_namedtuples             309.35 KB\n",
    "        Uso memoria cargar_clases                  452.13 KB\n",
    "    ```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En resumen, al momento de trabajar con estructuras Generadores es importante pensar en el código en términos de:\n",
    "- Eficiencia\n",
    "    - Cómo se carga la información\n",
    "    - Cómo se maneja la información\n",
    "- Comportamiento\n",
    "    - Qué hacen las distintas funciones\n",
    "    - Qué retornan las distintas funciones"
   ]
  }
 ],
 "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
}
