{
 "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 desde 2018-1 al 2026-1 por Equipo Docente IIC2233</font>\n",
    "</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Tabla de contenidos\n",
    "\n",
    "1. [Funciones que trabajan con iterables](#Funciones-que-trabajan-con-iterables)\n",
    "    1. [Funciones que necesitan de una función](#Funciones-que-necesitan-de-una-función)\n",
    "        1. [`map`](#map)\n",
    "        2. [`filter`](#filter)\n",
    "        3. [`reduce`](#reduce)\n",
    "        4. [Funciones *lambda* o funciones anónimas](#Funciones-lambda-o-funciones-anónimas-✨)\n",
    "    1. [Funciones que no necesitan de una función](#Funciones-que-no-necesitan-de-una-función)\n",
    "        1. [`enumerate`](#enumerate)\n",
    "        2. [`zip`](#zip)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Funciones que trabajan con iterables\n",
    "\n",
    "Además de las funciones generadoras, también podemos encontrar funciones que no utilizan necesariamente generadores, pero aceptan el uso de generadores. En este sentido, agruparemos estas funciones en dos grupos:\n",
    "1. Aquellas que necesitan de una función para ser ejecutadas.\n",
    "2. Aquellas que no necesitan de una función. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Funciones que necesitan de una función"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### `map`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "`map` recibe como parámetros una función y **al menos** un iterable. Retorna un iterador que resulta de aplicar la función sobre cada elemento del iterable por separado. Es así como `map(f, iterable)` es similar a `(f(x) for x in iterable)`.\n",
    "\n",
    "La cantidad de iterables entregada a `map` debe corresponder con la cantidad de parámetros que recibe la función `f`. Por ejemplo, si tenemos `map(f, iterable1, iterable2)` entonces `f` debe recibir dos parámetros. Es así como  `map(f, iterable1, iterable2)` es similar a `(f(x, y) for x, y in zip(iterable1, iterable2))`.\n",
    "\n",
    "> (Inicio paréntesis\n",
    "> \n",
    "> La función `zip` nos permite recorrer de forma conjunta 2 o más iterables en base a la posición de sus elementos. Entraremos en más detalle sobre esta función en la sección [`zip`](#zip).\n",
    "> \n",
    "> Fin paréntesis)\n",
    "\n",
    "\n",
    "En resumen, la función `map` presenta la siguiente sintaxis `map(función, iterable1, ...)`, donde:\n",
    "\n",
    "- `función`: denota una función de Python o, en general, cualquier invocable de Python. Esto incluye funciones integradas y definidas por el usuario, clases, métodos de instancia y clase, y más.\n",
    "\n",
    "- `iterable`: es cualquier iterable de Python válido, como una lista, una tupla y un set.\n",
    "\n",
    "La función `map()` aplicará el función a cada elemento en el iterable.\n",
    "\n",
    "\n",
    "![](img/map.png)\n",
    "\n",
    "Imagen obtenida de [Swift unboxed](https://swiftunboxed.com/open-source/map/ \"Swift unboxed\")."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Veamos algunos ejemplos:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1\\. Tenemos una lista de *strings*, donde queremos colocar cada uno en minúsculas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "señores pasajeros, disculpen, mi, intención, no, es, molestar\n"
     ]
    }
   ],
   "source": [
    "def lower(string: str) -> str:\n",
    "    return string.lower()\n",
    "\n",
    "\n",
    "strings = ['Señores pasajeros', 'Disculpen', 'mi', 'IntencIÓN', 'no', 'Es', 'MolEstar']\n",
    "mapeo = map(lower, strings)\n",
    "\n",
    "print(', '.join(mapeo))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2\\. Tenemos dos o más listas de números y queremos, a partir de esos números, calcular otro:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[290, 148, 130, 116]\n",
      "[289, 82, 249, 833]\n"
     ]
    }
   ],
   "source": [
    "def polinomio_1(x: int, y: int) -> int:\n",
    "    return x ** 2 + y ** 2\n",
    "\n",
    "def polinomio_2(x: int, y: int, z: int) -> int:\n",
    "    return x + y ** 2 + z ** 3\n",
    "\n",
    "\n",
    "a = [1, 2, 3, 4]\n",
    "b = [17, 12, 11, 10]\n",
    "c = (-1, -4, 5, 9)\n",
    "\n",
    "mapeo_1 = map(polinomio_1, a, b)\n",
    "mapeo_2 = map(polinomio_2, a, b, c)\n",
    "\n",
    "print(list(mapeo_1))\n",
    "print(list(mapeo_2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notar que la cantidad de elementos que procesa la función en un `map` corresponde a la cantidad que tiene el iterable más pequeño:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[101, 103, 105]"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def suma(x: int, y: int) -> int:\n",
    "    return x + y\n",
    "\n",
    "\n",
    "a = range(1, 2233)\n",
    "b = [100, 101, 102]\n",
    "\n",
    "mapeo = map(suma, a, b)\n",
    "list(mapeo)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "#### `filter`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`filter(f, iterable)` recibe como parámetros una función que retorna `True` o `False` (o función *booleana*), y un iterable. Retorna un generador que entrega aquellos elementos del iterable donde la función `f` retorna `True`.\n",
    "\n",
    "Se puede ver que `filter(f, iterable)` es equivalente a `(x for x in iterable if f(x))`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[1, 1, 3, 5, 13, 21]\n",
      "[0, 2, 8, 34]\n"
     ]
    }
   ],
   "source": [
    "from typing import Generator\n",
    "\n",
    "\n",
    "def fibonacci(límite: int) -> Generator:\n",
    "    a, b = 0, 1\n",
    "    for _ in range(límite):\n",
    "        yield a\n",
    "        a, b = b, a + b\n",
    "\n",
    "def es_impar(número: int) -> bool:\n",
    "    return número % 2 != 0\n",
    "\n",
    "def es_par(número: int) -> bool:\n",
    "    return número % 2 == 0\n",
    "\n",
    "\n",
    "filtrado_impares = filter(es_impar, fibonacci(10))\n",
    "print(list(filtrado_impares))\n",
    "\n",
    "filtrado_pares = filter(es_par, fibonacci(10))\n",
    "print(list(filtrado_pares))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Otro ejemplo, en el que se entrega un *set* a `filter`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[1, 5, 9]\n"
     ]
    }
   ],
   "source": [
    "def menor_que_10(número: int) -> bool:\n",
    "    return número < 10\n",
    "\n",
    "\n",
    "set_filtrado = filter(menor_que_10, {100, 1, 5, 9, 91, 1})\n",
    "print(list(set_filtrado))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "#### `reduce`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Vamos a explicar la idea del `reduce` con un ejemplo de cálculo manual. Imaginemos que tenemos una secuencia con números, y que queremos obtener la suma de ellos. También supongamos que nos complica sumar más de dos números a la vez."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "lista = [1, 2, 3, 4, 5, 6]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si hicieramos esta suma en forma procedural, lo que probablemente haríamos es sumar los dos primeros elementos, guardar el resultado, y ese resultado sumarlo con el siguiente elemento. Y así sucesivamente:\n",
    "\n",
    "- $1 + 2 = 3$\n",
    "- $3 + 3 = 6$\n",
    "- $6 + 4 = 10$\n",
    "- $10 + 5 = 15$\n",
    "- $15 + 6 = 21$\n",
    "\n",
    "El resultado final es **21**. Ahora supongamos que no necesariamente queremos sumar los números de a pares, sino que aplicar una función cualquiera `f`:\n",
    "- $f(1, 2) = a$\n",
    "- $f(a, 3) = b$\n",
    "- $f(b, 4) = c$\n",
    "- $f(c, 5) = d$\n",
    "- $f(d, 6) = e$\n",
    "\n",
    "En este caso, el resultado final es **e**. Reemplazando las variables, nuestro cómputo fue:\n",
    "\n",
    "$f(f(f(f(f(1, 2), 3), 4), 5), 6)$\n",
    "\n",
    "Esa es exactamente la idea detrás del `reduce`. Esta operación consiste en aplicar sucesivamente una función `f(x, y)`, donde `x` es el resultado acumulado e `y` es un elemento de la secuencia. Esto *reducirá* el iterable a un sólo resultado.\n",
    "\n",
    "![](img/reduce.png)\n",
    "\n",
    "Entonces, `reduce(f, iterable)` recibe una función que toma dos valores y un iterable. Retorna lo que resulta de aplicar la función `f` al iterable `[s1, s2, s3, ..., sn]` de la siguiente forma: `f(f(f(f(s1, s2), s3), s4), s5), ...`.\n",
    "\n",
    "Podemos ver que funciona muy bien para la suma que habíamos propuesto al principio:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "21"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from functools import reduce\n",
    "\n",
    "\n",
    "def suma(x: int, y: int) -> int:\n",
    "    return x + y\n",
    "\n",
    "\n",
    "reduce(suma, lista)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Finalmente, es posible agregar un inicializador al `reduce`. Este inicializador será el primer elemento que la función procese.\n",
    "\n",
    "A continuación, un ejemplo donde genera la representación como *string* de una lista de números y se utiliza el inicializador para agregar una etiqueta a esta representación:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Sin inicializador: 1->2->3->4->5\n",
      "Con inicializador: [Inicio]->1->2->3->4->5\n"
     ]
    }
   ],
   "source": [
    "from typing import Any\n",
    "\n",
    "\n",
    "def concatenar_textos(x: Any, y: Any) -> str:\n",
    "    return f'{x}->{y}'\n",
    "\n",
    "\n",
    "lista = [1, 2, 3, 4, 5]\n",
    "\n",
    "reduce_sin_inicializador = reduce(concatenar_textos, lista)\n",
    "reduce_con_inicializador = reduce(concatenar_textos, lista, '[Inicio]')\n",
    "\n",
    "print(f'Sin inicializador: {reduce_sin_inicializador}')\n",
    "print(f'Con inicializador: {reduce_con_inicializador}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora, veamos algunos ejemplos aplicados:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. Aplanamiento de listas\n",
    "       \n",
    "   Consideremos que tenemos una lista con más listas dentro, y queremos juntar todos los elementos en orden en una gran lista. Podemos hacer eso con `reduce`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[1, 2, 3, 4, 5, 6, 7, 8, 9]"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def concatenar_listas(x: list, y: list) -> list:\n",
    "    return x + y\n",
    "\n",
    "\n",
    "lista_con_listas = [[1, 2], [3, 4], [5, 6], [7, 8, 9]]\n",
    "lista_aplanada = reduce(concatenar_listas, lista_con_listas)\n",
    "lista_aplanada"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2. Intersección o unión de varios _sets_  \n",
    "     \n",
    "   Consideremos que tenemos varios _sets_ en una lista, de los cuales queremos obtener su intersección o unión:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Unión: {1, 2, 3, 4, 5, 9}\n",
      "Intersección: {1}\n"
     ]
    }
   ],
   "source": [
    "def operación_unión(x: set, y: set) -> set:\n",
    "    return x | y\n",
    "\n",
    "def operación_intersección(x: set, y: set) -> set:\n",
    "    return x & y\n",
    "\n",
    "\n",
    "conjuntos = [{3, 5, 1}, {4, 3, 1}, {1, 2, 5}, {9, 5, 4, 1}]\n",
    "\n",
    "resultado_unión = reduce(operación_unión, conjuntos)\n",
    "resultado_intersección = reduce(operación_intersección, conjuntos)\n",
    "\n",
    "print(\"Unión:\", resultado_unión)\n",
    "print(\"Intersección:\", resultado_intersección)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "3. Duplicación de elementos\n",
    "  \n",
    "\t Supongamos que queremos crear una cantidad de elementos dependiendo del valor que posean:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5)"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def duplicar_elementos(x: int, y: int) -> int:\n",
    "    # Recuerda que el * se usa aquí para desempaquetar valores\n",
    "    return *x, *(y for _ in range(y))\n",
    "\n",
    "reduce(duplicar_elementos, lista, [])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "4. Cálculo de mínimos o máximos  \n",
    "      \n",
    "   Se puede obtener el máximo en una colección usando `reduce`. Hagámoslo sobre la unión de conjuntos que acabamos de obtener:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "9"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Hecho así sólo como ejemplo pedagógico\n",
    "def máximo(x: int, y:int) -> int:\n",
    "    if x >= y:\n",
    "        return x\n",
    "    return y\n",
    "\n",
    "\n",
    "reduce(máximo, resultado_unión) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "No obstante, es más limpio usar simplemente la función `max` o `min` que nos provee Python:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "9"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "max(resultado_unión)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##### Precauciones ⚠️\n",
    "\n",
    "1. Cantidad de elementos de la secuencia a reducir  \n",
    "     \n",
    "   Cuando la secuencia que se entrega a `reduce` tiene sólo un elemento, la operación retornará sólo ese elemento sin aplicar la función. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def suma(x: int, y: int) -> int:\n",
    "    return x + y\n",
    "\n",
    "\n",
    "reduce(suma, [1])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Como se comentaba anteriormente:\n",
    "> Es posible agregar un inicializador al `reduce`. Este inicializador será el primer elemento que la función procese.\n",
    "\n",
    "Normalmente se define como el objeto que no altera el resultado de nuestra función, que en el caso de la suma es el número 0."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "reduce(suma, [1], 0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si la secuencia entregada es vacía y no entregamos un valor de inicialización, se lanza una excepción:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "TypeError: reduce() of empty iterable with no initial value\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    reduce(suma, [])\n",
    "except TypeError as error:\n",
    "    print('TypeError:', error)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mientras que añadiendo un inicializador, el proceso termina exitosamente:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "reduce(suma, [], 0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2. Operaciones no conmutativas  \n",
    "     \n",
    "   Es necesario que tomes en cuenta que en ciertas operaciones el resultado va a depender del orden en que se encuentren los elementos en la colección. Esto es así por ejemplo en la división, donde $\\frac{x}{y} \\neq \\frac{y}{x}$, mientras que en operaciones como la suma el orden no es relevante pues $x + y = y + x$.  \n",
    "   \n",
    "   Veamos con un ejemplo qué pasa cuando el orden de los elementos cambia en una operación sensible al orden:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.0008658008658008659"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def división(x: int, y: int) -> float:\n",
    "    return x / y\n",
    "\n",
    "\n",
    "números = [3, 5, 7, 9, 11]\n",
    "reduce(división, números)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Acá invertiremos el orden de los números:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.01164021164021164"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "reduce(división, números[::-1])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "3. Reducir _sets_ u otros iterables no ordenados  \n",
    "     \n",
    "   En este caso hay que tener cuidado cuando la operación que se haga dependa del orden de los elementos, pues el resultado podría no ser el esperado.  \n",
    "     \n",
    "   En el ejemplo, se tiene un iterador con varias palabras que queremos concatenar con un `reduce`. Vemos que el orden final dista del orden que se declaró al inicio, pues esta estructura no es ordenada."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1. casa mar piso roca ventana\n",
      "2. piso casa roca mar ventana\n",
      "3. piso ventana roca casa mar\n",
      "4. piso roca ventana mar casa\n",
      "5. casa mar roca ventana piso\n"
     ]
    }
   ],
   "source": [
    "from random import shuffle\n",
    "from typing import Self\n",
    "\n",
    "\n",
    "class IteradorListaPalabrasDesordenadas:\n",
    "    def __init__(self, iterable: list) -> None:\n",
    "        self.iterable = iterable.copy()\n",
    "    \n",
    "    def __iter__(self) -> Self:\n",
    "        shuffle(self.iterable)\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",
    "\n",
    "def concatenar_textos(x: str, y: str) -> str:\n",
    "    return f\"{x} {y}\"\n",
    "\n",
    "\n",
    "# Ejecutemos este código múltiples veces para apreciar cómo cambia el orden en cada ejecución\n",
    "for i in range(1, 6):\n",
    "    palabras = IteradorListaPalabrasDesordenadas(['casa', 'mar', 'ventana', 'roca', 'piso'])\n",
    "    texto = reduce(concatenar_textos, palabras)\n",
    "    print(f'{i}. {texto}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "#### Funciones *lambda* o funciones anónimas ✨\n",
    "\n",
    "Hasta el momento, para cada función `map`, `filter` o `reduce` que hemos implementado, hemos tenido que escribir una función que pueda ser utilizada en dicho caso, pese a la complejidad de las funciones en sí.\n",
    "\n",
    "En este contexto, las funciones *lambda* o funciones anónimas nos serán de especial utilidad, ya que nos permitirá hacer uso de `map`, `filter` y `reduce` sin la necesidad de tener que definir funciones usando el `def`.\n",
    "\n",
    "Pero antes de explicar qué son las funciones *lambda* y cómo funcionan, necesitamos hablar sobre **cómo se tratan las funciones en Python**. En el caso de Python, se dice que el lenguaje tiene **funciones de primera clase** (*first-class functions*), es decir, que las funciones son tratadas como cualquier otra variable (objeto). Esto no es así en otros lenguajes como Java.\n",
    "\n",
    "El hecho que las funciones sean de primera clase tiene algunas consecuencias, como:\n",
    "\n",
    "> **1\\. Las funciones pueden ser asignadas a una variable, y luego usar esa variable igual que la función.**\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<function suma at 0x000001A6F136A020>\n",
      "<function suma at 0x000001A6F136A020>\n",
      "8\n",
      "8\n"
     ]
    }
   ],
   "source": [
    "def suma(x: int, y: int) -> int:\n",
    "    return x + y\n",
    "\n",
    "\n",
    "adición = suma\n",
    "\n",
    "# Ambas son la misma función\n",
    "print(adición)\n",
    "print(suma)\n",
    "\n",
    "# Y por lo tanto entregan el mismo resultado\n",
    "print(suma(3, 5))\n",
    "print(adición(3, 5))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **2\\. Las funciones pueden ser pasadas como parámetro a otras funciones.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Buenas tardes Señora Valeria\n",
      "Buenas tardes Joven Bon\n"
     ]
    }
   ],
   "source": [
    "from typing import Callable\n",
    "\n",
    "\n",
    "def saludar_señora(nombre: str) -> str:\n",
    "    return \"Señora \" + nombre\n",
    "\n",
    "def saludar_joven(nombre: str) -> str:\n",
    "    return \"Joven \" + nombre\n",
    "\n",
    "def saludar_tarde(función_saludo: Callable, nombre: str) -> str:\n",
    "    return \"Buenas tardes \" + función_saludo(nombre)\n",
    "\n",
    "\n",
    "print(saludar_tarde(saludar_señora, \"Valeria\"))\n",
    "print(saludar_tarde(saludar_joven, \"Bon\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este contexto, **las funciones *lambda*** son una forma alternativa de definir funciones en Python. Además de su nombre griego, no hay nada intimidante en ellas. Veamos un ejemplo de cómo definirlas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2 + 1 = 3\n",
      "2 + 1 = 3\n"
     ]
    }
   ],
   "source": [
    "# Definimos una función lambda a retornar\n",
    "def retornar_sucesor():\n",
    "    return lambda x: x + 1\n",
    "\n",
    "# guardamos una función lambda para luego imprimir su nombre.\n",
    "sucesor = retornar_sucesor()\n",
    "\n",
    "# Lo anterior es equivalente a escribir esta función:\n",
    "def sumar_uno():\n",
    "    def sucesor(x):\n",
    "        return x + 1\n",
    "    return sucesor\n",
    "\n",
    "sucesor_funcion = sumar_uno()\n",
    "\n",
    "print(\"2 + 1 =\", sucesor(2))\n",
    "print(\"2 + 1 =\", sucesor_funcion(2))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "5 - 3 =  2\n",
      "5 - 3 =  2\n"
     ]
    }
   ],
   "source": [
    "# Aquí tenemos otro ejemplo\n",
    "def restar_por(n: int):\n",
    "    return lambda x: x - n\n",
    "\n",
    "restar_3_a = restar_por(3)\n",
    "\n",
    "# Lo anterior es equivalente a escribir esta función:\n",
    "def disminuir_por(n: int):\n",
    "    def disminuir(x):\n",
    "        return x - n\n",
    "    return disminuir\n",
    "\n",
    "disminuir_3_a = disminuir_por(3)\n",
    "\n",
    "\n",
    "print(\"5 - 3 = \", restar_3_a(5))\n",
    "print(\"5 - 3 = \", disminuir_3_a(5))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Como se puede observar, la sintaxis consiste en `lambda <parámetros>: <valor a retornar>`. En estas funciones no se necesita la sentencia `return` en su interior, puesto que la operación que se coloca a la derecha de los dos puntos (`:`) es el valor que se devolverá.\n",
    "\n",
    "Una característica que distingue a las funciones *lambda* es que **pueden ser definidas en forma anónima**, es decir, funciones que no reciben un nombre específico."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'<lambda>'"
      ]
     },
     "execution_count": 25,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "sucesor.__name__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'<lambda>'"
      ]
     },
     "execution_count": 26,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "restar_3_a.__name__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'disminuir'"
      ]
     },
     "execution_count": 27,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "disminuir_3_a.__name__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Estas funciones *lambda* pueden ser vistas como una forma alternativa para definir funciones que no requiere declararlas como variable (no es necesario usar el `def`), requiere de menos código para escribirlas, y son anónimas al no poseer un nombre asignado.\n",
    "\n",
    "Estas características que presentan las *lambda functions*, combinan bien con las funciones que vimos anteriormente: `map`, `filter`, `reduce`.\n",
    "\n",
    "Veamos cómo se verían algunos de los ejemplos anteriores, pero utilizando *lambda functions*:  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "señores pasajeros, disculpen, mi, intención, no, es, molestar\n"
     ]
    }
   ],
   "source": [
    "strings = ['Señores pasajeros', 'Disculpen', 'mi', 'IntencIÓN', 'no', 'Es', 'MolEstar']\n",
    "mapeo = map(lambda x: x.lower(), strings)\n",
    "\n",
    "print(', '.join(mapeo))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[1, 5, 9]\n"
     ]
    }
   ],
   "source": [
    "set_filtrado = filter(lambda x: x < 10, {100, 1, 5, 9, 91, 1})\n",
    "print(list(set_filtrado))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Sin inicializador: 1->2->3->4->5\n",
      "Con inicializador: [Inicio]->1->2->3->4->5\n"
     ]
    }
   ],
   "source": [
    "lista = [1, 2, 3, 4, 5]\n",
    "\n",
    "reduce_sin_inicializador = reduce(lambda x, y: f'{x}->{y}', lista)\n",
    "reduce_con_inicializador = reduce(lambda x, y: f'{x}->{y}', lista, '[Inicio]')\n",
    "\n",
    "print(f'Sin inicializador: {reduce_sin_inicializador}')\n",
    "print(f'Con inicializador: {reduce_con_inicializador}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Funciones que no necesitan de una función"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### `enumerate`\n",
    "\n",
    "`enumerate()` entrega una especie de generador que retorna tuplas, donde el primer objeto en cada tupla es el indice y el segundo es el ítem original. Por ejemplo, si queremos iterar sobre una lista, y necesitamos obtener tanto el índice como su valor, una forma poco *pythonic* de hacer esto sería la siguiente:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0: a\n",
      "1: b\n",
      "2: c\n",
      "3: d\n"
     ]
    }
   ],
   "source": [
    "lista = [\"a\",\"b\",\"c\",\"d\"]\n",
    "\n",
    "for indice in range(len(lista)):\n",
    "    elemento = lista[indice]\n",
    "    print(f\"{indice}: {elemento}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La función `enumerate` nos permite hacer exactamente mismo, pero de una forma más elegante y *pythonic*:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0: a\n",
      "1: b\n",
      "2: c\n",
      "3: d\n"
     ]
    }
   ],
   "source": [
    "for indice, elemento in enumerate(lista):\n",
    "    print(f\"{indice}: {elemento}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notar que la función `enumerate` retorna un objeto de tipo `enumerate`, que se comporta de manera similar a un generador, por lo que puedes usar `next` para acceder a sus elementos, y eso es lo que permite utilizarlo en un `for`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### `zip`\n",
    "\n",
    "Toma dos o más secuencias o iterables y retorna un iterador que entrega tuplas, donde cada tupla está formada por los elementos i-ésimos de cada una de las secuencias o iterables. La cantidad de elementos que retorna este iterador es igual al menor de los largos de las secuencias o iterables.\n",
    "\n",
    "De manera similar a `enumerate`, `zip` retorna un objeto de tipo `zip` que se comporta de manera similar a un generador, por lo que también se puede usar `next` para acceder a sus elementos. Además de tener los mismos beneficios que un generador regular (ocupa poco espacio y \"genera\" los elementos a medida que son requeridos)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A modo de ejemplo, consideremos que tenemos una tupla con los *headers* (o nombres de columnas) de un `CSV` y una tupla con los datos de una persona en particular. Queremos obtener una lista con tuplas, donde en cada una aparezca el *header* con su valor:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[('nombre', 'Juan'), ('apellido', 'Perez'), ('email', 'jp1@hotmail.com')]"
      ]
     },
     "execution_count": 33,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "columnas = (\"nombre\", \"apellido\", \"email\")\n",
    "persona = (\"Juan\", \"Perez\", \"jp1@hotmail.com\")\n",
    "\n",
    "list(zip(columnas, persona))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si ampliamos nuestro ejemplo con una lista de tuplas de personas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[('nombre', 'Juan', 'Gonzalo', 'Alberto'),\n",
       " ('apellido', 'Perez', 'Aldunate', 'Gomez'),\n",
       " ('email', 'jp1@hotmail.com', 'gan@gmail.com', 'agomez@yahoo.com')]"
      ]
     },
     "execution_count": 34,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "columnas = (\"nombre\", \"apellido\", \"email\")\n",
    "personas = [\n",
    "            (\"Juan\", \"Perez\", \"jp1@hotmail.com\"), \n",
    "            (\"Gonzalo\", \"Aldunate\", \"gan@gmail.com\"),\n",
    "            (\"Alberto\", \"Gomez\", \"agomez@yahoo.com\")\n",
    "           ]\n",
    "\n",
    "# El asterisco simple es para pasar la lista de personas como argumentos separados:\n",
    "# Si personas = [p1, p2, p3], entonces lo siguiente es equivalente a zip(columnas, p1, p2, p3)\n",
    "list(zip(columnas, *personas))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "También recordemos que `zip` sólo tomará la cantidad de elementos del iterable más corto. Si quieres que tome en cuenta la lista más larga puedes ver la función [`zip_longest`](https://docs.python.org/3/library/itertools.html#itertools.zip_longest) del paquete `itertools`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[('nombre', 'Juan'), ('apellido', 'Perez'), ('email', 'jp1@hotmail.com')]"
      ]
     },
     "execution_count": 35,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "columnas = (\"nombre\", \"apellido\", \"email\")\n",
    "persona = (\"Juan\", \"Perez\", \"jp1@hotmail.com\", \"+56123123??\")\n",
    "\n",
    "list(zip(columnas, persona))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##### `zip` como inversa de sí misma\n",
    "\n",
    "`zip` en conjunto con el operador `*` (usado para desempacar listas o tuplas a argumentos de una función) puede ser usado como inversa de la operación `zip`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[(1, 'a'), (2, 'b'), (3, 'c'), (4, 'd')]\n"
     ]
    }
   ],
   "source": [
    "a = [1, 2, 3, 4]\n",
    "b = [\"a\", \"b\", \"c\", \"d\"]\n",
    "\n",
    "zipped = zip(a, b)\n",
    "zipped = list(zipped)\n",
    "print(zipped)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[(1, 2, 3, 4), ('a', 'b', 'c', 'd')]\n"
     ]
    }
   ],
   "source": [
    "unzipped = zip(*zipped)\n",
    "unzipped = list(unzipped)\n",
    "print(unzipped)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el ejemplo, al ejecutar `zip(*zipped)` el desempaquetamiento entrega los 4 iterables a `zip` **por separado**. Cada uno de estos iterables tiene largo 2. Luego, `zip` concatena todos los primeros elementos `(1, 2, 3, 4)` y luego todos los segundos elementos `('a', 'b', 'c', 'd')`, volviendo a como estaba en un principio."
   ]
  }
 ],
 "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
}
