AulaML Python · C++
Python intermedio · 42 min · Funciones

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.

Lección 3 de 3 del módulo
Al terminar esta lección podrás
  • 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
  1. ¿Qué es una función de orden superior?
  2. ¿Para qué sirve?
  3. ¿Cómo funciona por dentro?
  4. Sintaxis: lambda, key y desempaquetado
  5. Ejemplo básico: pasar funciones
  6. Ejemplo intermedio: filtrar, transformar y sumar
  7. Caso práctico: aplanar un inventario anidado
  8. Visualización: la pila de factorial
  9. Errores comunes
  10. Buenas prácticas
  11. Ejercicios
  12. Soluciones explicadas
  13. Resumen
  14. 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 devuelve f para cada elemento.
  • map(f, iterable): aplica f a cada elemento y devuelve un iterador perezoso.
  • filter(f, iterable): conserva los elementos para los que f devuelve verdadero.
  • reduce(f, iterable, inicial): acumula de izquierda a derecha; vive en functools.
  • 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.

HerramientaQué haceDevuelve en Python 3
map(f, xs)Aplica f a cada elementoUn iterador perezoso
filter(f, xs)Deja los que cumplen fUn iterador perezoso
reduce(f, xs, init)Acumula con f de izquierda a derechaUn único valor
sorted(xs, key=f)Ordena según fUna lista nueva
[f(x) for x in xs]Equivale a mapUna 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.

PYTHON lambda.py
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.
  • sorted no modifica la lista original: devuelve una nueva.
  • La función key recibe un elemento y devuelve el valor por el que se ordena.
  • Para ordenar de mayor a menor puedes usar reverse=True o negar la clave cuando es numérica.
  • map acepta varios iterables a la vez; filter solo 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.

PYTHON aplicar.py
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.

PYTHON pipeline.py
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.

PYTHON aplanar.py
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.

Ejecución visual ·Tres marcos antes de volver
1 / 8
1 def factorial(n):
2 if n == 1:
3 return 1
4 return n * factorial(n - 1)
5
6
7 print(factorial(3))
Se define factorial. El cuerpo no se ejecuta todavía y no hay marcos de la función.
Variables
factorial = <function factorial>
Salida
—

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íntomaPor qué ocurreCómo se arregla
RecursionError: maximum recursion depth exceededFalta el caso base o el argumento no se reduceAsegura una condición de corte y un paso hacia ella
NameError: name 'reduce' is not definedNo importaste la funciónEscribe from functools import reduce
<map object at 0x...> en la salidamap devuelve un iterador, no una listaEnvuelve con list(...) o recórrelo con for
TypeError: <lambda>() missing 1 required positional argumentLa lambda espera un parámetro y la llamaste sin élRevisa 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 nadaCubre todas las ramas con return
Recursión sin freno
Incorrecto
PYTHON RecursionError
def cuenta(n):
    return cuenta(n - 1)

print(cuenta(3))
Correcto
PYTHON Correcto
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 map y filter cuando la operación cabe en una línea legible.
  • Usa key= en sorted en 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.
PYTHON fibonacci.py
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))
Ordenar por un criterio en Python y C++
Python
PYTHON ordenar.py
productos = [("teclado", 120), ("mouse", 45)]
por_precio = sorted(productos, key=lambda p: p[1])
print(por_precio)
C++
CPP ordenar.cpp
#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

Ejercicio 1

Catálogo ordenado

Tienes una lista de diccionarios con 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.

Completar código Completa la transformación
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.

Ejercicio 2

Suma de dígitos recursiva

Escribe 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ón crea una función anónima de una sola expresión.
  • sorted(key=f) ordena según el valor que f calcula para cada elemento.
  • map y filter devuelven iteradores perezosos; usa list(...) para verlos.
  • reduce está en functools y 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_cache evita recalcular subproblemas repetidos.

Evaluación

Evaluación 1

¿Qué devuelve sorted(["bb", "a", "ccc"], key=len)?

Evaluación 2

¿Qué muestra print(map(str.upper, ["a", "b"])) en Python 3?

Evaluación 3

¿Cuál es la condición indispensable para que una función recursiva termine?

Tus notas

¿Ya lo entendiste?

Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.