Lambda, map/filter/reduce y recursividad
Tratar funciones como datos, transformar colecciones sin bucles y resolver problemas que se llaman a sí mismos
En Python una función es un objeto: puedes pasarla, devolverla y guardarla. Con lambda y las funciones map, filter y reduce transformas colecciones sin escribir bucles; con recursión resuelves problemas definidos en términos de sí mismos, siempre con un caso base y cuidado con la pila.
- Explicar por qué una función puede pasarse como argumento
- Escribir lambdas y usarlas como clave en sorted
- Transformar y filtrar colecciones con map, filter y reduce
- Identificar el caso base y el paso recursivo de un problema
- Seguir la pila de llamadas de una función recursiva
- Evitar el coste exponencial con memoización
Antes necesitas: Argumentos flexibles y alcance de variables
En esta lección
- ¿Qué es una función de orden superior?
- ¿Para qué sirve?
- ¿Cómo funciona por dentro?
- Sintaxis: lambda, key y desempaquetado
- Ejemplo básico: pasar funciones
- Ejemplo intermedio: filtrar, transformar y sumar
- Caso práctico: aplanar un inventario anidado
- Visualización: la pila de factorial
- Errores comunes
- Buenas prácticas
- Ejercicios
- Soluciones explicadas
- Resumen
- Evaluación
Hasta ahora las funciones eran acciones con nombre: las defines y las llamas. Pero en Python son algo más: son objetos como un número o una lista. Puedes guardarlas en una variable, pasarlas a otra función y devolverlas. Esa idea es la base de sorted(key=...), de map y de los decoradores.
La misma idea permite que una función se llame a sí misma; la recursión resuelve problemas definidos en partes del mismo tipo: una carpeta contiene carpetas, un árbol tiene ramas. Aquí aprenderás a escribirla con un caso base claro y a leer su pila.
¿Qué es una función de orden superior?
Una función de orden superior es la que recibe otra función como argumento o devuelve una función como resultado. sorted lo es cuando le pasas key; map lo es siempre. La función que se pasa se llama callback o simplemente función aplicada, y puede ser una función normal o una lambda.
Una lambda es una función anónima escrita en una sola expresión: lambda x: x * 2 equivale a def doble(x): return x * 2, pero sin nombre y sin cuerpo multilínea. No es una versión «mejor» de def: es una forma cómoda de definir una operación pequeña justo donde se usa.
lambda: función de una sola expresión, útil como clave o callback desechable.sorted(iterable, key=f): ordena según el valor que devuelvefpara cada elemento.map(f, iterable): aplicafa cada elemento y devuelve un iterador perezoso.filter(f, iterable): conserva los elementos para los quefdevuelve verdadero.reduce(f, iterable, inicial): acumula de izquierda a derecha; vive enfunctools.- Recursión: una función que se llama a sí misma con un problema más pequeño.
¿Para qué sirve?
- Ordenar por un criterio: productos por precio, alumnos por nota, diccionarios por una clave concreta.
- Limpiar datos: aplicar una conversión a cada celda de una columna o descartar registros inválidos.
- Pipelines de datos: filtrar, transformar y agregar en cadena, igual que hace una consulta SQL.
- Recorrer estructuras: sumar los nodos de un árbol, contar archivos dentro de carpetas o aplanar listas anidadas.
- Cálculo matemático: factorial, potencia, búsqueda binaria y definiciones que naturalmente se expresan en términos de sí mismas.
- Machine Learning: aplicar una métrica a cada pliegue de validación o una función de activación a un lote.
¿Cómo funciona por dentro?
Cuando escribes lambda x: x * 2, Python crea un objeto función sin nombre y lo deja en la expresión. Si lo pasas a map, map guarda la referencia y, cada vez que le pides un elemento, llama a esa función con el siguiente valor.
En Python 3, map y filter devuelven iteradores perezosos, no listas. Esto significa que no calculan nada hasta que los recorres. Por eso print(map(f, datos)) muestra algo como <map object at 0x...> y necesitas list(...) para ver los valores. La ventaja es que no reservan memoria para un millón de elementos si solo vas a consumir los primeros.
Una llamada recursiva crea un marco nuevo encima del anterior, igual que cualquier llamada. La diferencia es que el mismo código se repite con un valor menor. Cada marco espera el resultado del siguiente hasta llegar al caso base, que devuelve un valor sin volver a llamar. Al volver, los resultados se combinan y los marcos se cierran en orden inverso.
| Herramienta | Qué hace | Devuelve en Python 3 |
|---|---|---|
map(f, xs) | Aplica f a cada elemento | Un iterador perezoso |
filter(f, xs) | Deja los que cumplen f | Un iterador perezoso |
reduce(f, xs, init) | Acumula con f de izquierda a derecha | Un único valor |
sorted(xs, key=f) | Ordena según f | Una lista nueva |
[f(x) for x in xs] | Equivale a map | Una lista nueva |
Sintaxis: lambda, key y desempaquetado
La forma general es lambda parámetros: expresión. La expresión se evalúa y su valor se devuelve sin escribir return. Una lambda puede recibir varios parámetros e incluso valores por defecto, pero no puede contener asignaciones ni bloques if/for; sí admite una expresión condicional.
doble = lambda x: x * 2
sumar = lambda a, b=10: a + b
print(doble(5))
print(sumar(3))
print(sumar(3, 4))
personas = [("Ana", 28), ("Luis", 22), ("Eva", 35)]
por_edad = sorted(personas, key=lambda p: p[1])
print(por_edad)
precios = [120, 45, 300]
print(sorted(precios, key=lambda p: -p))- Los parámetros van antes de los dos puntos y la expresión después; no hay
return. sortedno modifica la lista original: devuelve una nueva.- La función
keyrecibe un elemento y devuelve el valor por el que se ordena. - Para ordenar de mayor a menor puedes usar
reverse=Trueo negar la clave cuando es numérica. mapacepta varios iterables a la vez;filtersolo uno. Lo habitual es pasar uno.
Ejemplo básico: pasar funciones
Este ejemplo muestra lo esencial de una función de orden superior: aplicar no sabe qué operación va a ejecutar. Recibe una función y un valor, y la llama. Puedes pasarle abs, una lambda o cualquier función que acepte un argumento.
def aplicar(funcion, valor):
"""Ejecuta funcion sobre valor y devuelve el resultado."""
return funcion(valor)
print(aplicar(abs, -7))
print(aplicar(lambda x: x ** 2, 6))
print(aplicar(str.upper, "hola"))La línea 1 declara dos parámetros: funcion y valor. En la línea 3, funcion(valor) usa el nombre del parámetro como si fuera el nombre de una función; Python busca el objeto al que apunta y lo llama. Con abs devuelve 7; con la lambda, 36; con str.upper, "HOLA".
Fíjate en str.upper: es un método sin ejecutar, pasado como objeto. No escribes str.upper() porque eso lo llamaría sin argumento y fallaría.
Ejemplo intermedio: filtrar, transformar y sumar
Las tres operaciones clásicas encajan en un pipeline. Supón una lista de importes con valores negativos que representan devoluciones: quieres quedarte con las ventas positivas, aplicarles un descuento y obtener el total.
from functools import reduce
importes = [120, -30, 80, 45, 200]
ventas = filter(lambda x: x > 0, importes)
con_descuento = map(lambda x: x * 0.9, ventas)
total = reduce(lambda acum, x: acum + x, con_descuento, 0)
print(total)filter descarta -30; map multiplica cada valor restante por 0.9; reduce empieza en 0 y va sumando. El resultado es (120 + 80 + 45 + 200) * 0.9 = 400.5. Observa que no hay ninguna lista intermedia: los iteradores se pasan el testigo y cada uno consume al anterior.
Ese encadenamiento ahorra memoria, pero es difícil de depurar. En código cotidiano, una comprensión suele leerse mejor: total = sum(x * 0.9 for x in importes if x > 0). reduce brilla cuando la operación no es una suma simple, como construir un diccionario o combinar objetos.
Caso práctico: aplanar un inventario anidado
Los datos reales llegan con estructura irregular: una categoría contiene productos, pero también subcategorías. Para obtener una lista plana de todo lo que hay dentro, una función recursiva recorre cada nivel. El caso base es un elemento que no es lista; el paso recursivo, una lista que se vuelve a procesar.
def aplanar(lista):
"""Devuelve una lista plana con todos los elementos anidados."""
resultado = []
for elemento in lista:
if isinstance(elemento, list):
resultado.extend(aplanar(elemento))
else:
resultado.append(elemento)
return resultado
datos = [1, [2, 3], [4, [5, 6]]]
print(aplanar(datos))Para [1, [2, 3], [4, [5, 6]]], el bucle encuentra primero un número y lo añade; después una lista, y en lugar de añadirla llama a aplanar con ella. Esa llamada devuelve [2, 3] y extend los incorpora uno a uno. La sublista [4, [5, 6]] provoca otra llamada que, a su vez, se llama con [5, 6].
El caso base está implícito: una lista vacía no entra en el bucle y devuelve []. Cada llamada trabaja con una lista más pequeña, así que la recursión termina. El resultado final es [1, 2, 3, 4, 5, 6]. extend es clave: append(aplanar(...)) habría añadido listas dentro de listas.
Visualización: la pila de factorial
El factorial es el ejemplo canónico: factorial(3) es 3 * factorial(2), que es 2 * factorial(1), y factorial(1) devuelve 1 sin llamar a nadie. La traza muestra los marcos apilados y su resolución.
def factorial(n): if n == 1: return 1 return n * factorial(n - 1)print(factorial(3))factorial. El cuerpo no se ejecuta todavía y no hay marcos de la función.—
Avanza paso a paso: observa la línea resaltada, las variables y la salida.
Cada marco ocupa memoria y Python limita la profundidad a unas mil llamadas por defecto. Si olvidas el caso base, la recursión intenta bajar para siempre hasta lanzar RecursionError: maximum recursion depth exceeded. Ese límite existe para proteger la memoria, no para castigar la recursión.
Errores comunes
| Mensaje o síntoma | Por qué ocurre | Cómo se arregla |
|---|---|---|
RecursionError: maximum recursion depth exceeded | Falta el caso base o el argumento no se reduce | Asegura una condición de corte y un paso hacia ella |
NameError: name 'reduce' is not defined | No importaste la función | Escribe from functools import reduce |
<map object at 0x...> en la salida | map devuelve un iterador, no una lista | Envuelve con list(...) o recórrelo con for |
TypeError: <lambda>() missing 1 required positional argument | La lambda espera un parámetro y la llamaste sin él | Revisa cómo la pasas: key=f no es key=f() |
TypeError: unsupported operand type(s) for +: 'int' and 'NoneType' | Una rama de la función no devuelve nada | Cubre todas las ramas con return |
def cuenta(n):
return cuenta(n - 1)
print(cuenta(3))def cuenta(n):
if n == 0:
return 0
return 1 + cuenta(n - 1)
print(cuenta(3))La primera versión nunca deja de llamarse: no hay condición que devuelva un valor sin recursión. La segunda corta en `n == 0` y reduce el argumento en cada paso, así que la pila crece hasta cuatro marcos y luego se cierra. Sin caso base no hay recursión válida, solo un bucle disfrazado.
Buenas prácticas
- Escribe el caso base primero y compruébalo con el valor más pequeño posible.
- Garantiza que cada llamada se acerque al caso base: resta 1, divide entre 2, avanza al siguiente nodo.
- Prefiere una comprensión a
mapyfiltercuando la operación cabe en una línea legible. - Usa
key=ensorteden lugar de comparadores manuales: es más claro y más rápido. - No asignes lambdas a un nombre; si necesita nombre y docstring, usa
def. - Para recursión con muchos subproblemas repetidos, añade memoización con
functools.lru_cache.
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
"""Devuelve el n-ésimo número de Fibonacci."""
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(30))productos = [("teclado", 120), ("mouse", 45)]
por_precio = sorted(productos, key=lambda p: p[1])
print(por_precio)#include <algorithm>
#include <iostream>
#include <string>
#include <tuple>
#include <vector>
int main() {
std::vector<std::pair<std::string, int>> productos{{"teclado", 120}, {"mouse", 45}};
std::sort(productos.begin(), productos.end(), [](const auto& a, const auto& b) {
return a.second < b.second;
});
std::cout << productos.front().first << std::endl;
return 0;
}En Python `key` recibe un elemento y devuelve el criterio; el orden lo decide `sorted`. En C++ el comparador recibe dos elementos y debe responder si el primero va antes que el segundo. La lambda de C++ usa `auto` para no repetir el tipo del par, pero sigue siendo una función con parámetros y cuerpo.
Ejercicios
Catálogo ordenado
nombre y precio. Ordénala de mayor a menor precio usando sorted y una lambda como key, y muestra solo los nombres en ese orden. Después obtén una lista con los precios con IGV incluido usando map.Pista
La clave puede ser lambda p: p["precio"] con reverse=True. Para el segundo paso, map devuelve un iterador: conviértelo con list.
doble = x: x * 2 print(doble(4)) print(list( (doble, [1, 2, 3])))
La primera palabra crea una función anónima; la segunda aplica una función a cada elemento de un iterable.
Suma de dígitos recursiva
suma_digitos(n) que devuelva la suma de los dígitos de un entero positivo sin convertirlo a texto. Debe ser recursiva: el caso base es un número de una cifra y el paso recursivo usa n // 10 y n % 10. Comprueba con 4729 y con 8.Pista
n % 10 da el último dígito y n // 10 elimina ese dígito. Con n < 10 ya no queda nada que sumar.
Soluciones explicadas
En el catálogo, sorted y map no modifican la lista original: trabajan sobre ella y producen otra. Esa ausencia de efectos secundarios permite encadenar operaciones sin miedo. La lambda de key solo devuelve el dato que ordena; de comparar se encarga sorted.
En la suma de dígitos, la división entera es lo que garantiza el avance hacia el caso base: cada llamada elimina un dígito, así que un número de k cifras produce k marcos. Si usarás números enormes, la versión iterativa con un while evita la pila. La recursión es valiosa cuando la estructura del problema es jerárquica, no cuando solo es un bucle.
Resumen
- Las funciones son objetos: se asignan, se pasan como argumento y se devuelven.
lambda parámetros: expresióncrea una función anónima de una sola expresión.sorted(key=f)ordena según el valor quefcalcula para cada elemento.mapyfilterdevuelven iteradores perezosos; usalist(...)para verlos.reduceestá enfunctoolsy acumula un solo valor de izquierda a derecha.- Toda recursión necesita un caso base y un paso que reduzca el problema.
- Cada llamada recursiva añade un marco; sin corte se produce
RecursionError. - La memoización con
lru_cacheevita recalcular subproblemas repetidos.
Evaluación
¿Qué devuelve sorted(["bb", "a", "ccc"], key=len)?
¿Qué muestra print(map(str.upper, ["a", "b"])) en Python 3?
¿Cuál es la condición indispensable para que una función recursiva termine?
¿Ya lo entendiste?
Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.