Lambda, map, filter y functools en Python: programación funcional práctica

📅 Actualizado en marzo 2026 📊 Nivel: Intermedio ⏱️ 20 min de lectura
λ
λx.M
«Una función que toma x y devuelve M»
Alonzo Church · Lógico y matemático · 1903 – 1995
En 1936, Alonzo Church formalizó el cálculo lambda: un sistema en el que todo —datos, funciones, lógica— se expresa como funciones que reciben y devuelven funciones. Alan Turing demostró ese mismo año que el cálculo lambda y las máquinas de Turing son equivalentes: dos formas de describir la misma idea de computación. Python no es un lenguaje funcional puro, pero adoptó sus herramientas más útiles: lambda, map(), filter(), reduce() y los closures. Church habría reconocido estas herramientas como instancias directas de su teoría.

En Python, las funciones son ciudadanos de primera clase: se pueden pasar como argumentos, devolver como resultados, almacenar en variables y en listas. Esta propiedad habilita un conjunto de herramientas —lambda, map(), filter(), sorted() con key, closures y functools— que permiten escribir código más expresivo, componible y conciso que con bucles explícitos.

⚡ lambda: funciones anónimas de una expresión

# Sintaxis: lambda parámetros: expresión
doble = lambda x: x * 2
suma  = lambda x, y: x + y
cuadrado = lambda x: x ** 2

doble(5)       # 10
suma(3, 4)     # 7
cuadrado(9)    # 81

# ⚠️ PEP 8 desaconseja asignar lambda a variable — mejor usar def:
def doble(x): return x * 2   # más legible, mejor en trazas de error

# El uso natural de lambda es como argumento inline
numeros = [3, -1, 7, -4, 2, -9]
sorted(numeros, key=lambda x: abs(x))   # [-1, 2, 3, -4, 7, -9]

# Condición en lambda (operador ternario)
clasificar = lambda x: "par" if x % 2 == 0 else "impar"
clasificar(4)   # 'par'
clasificar(7)   # 'impar'

# Lambda con varios parámetros
dentro_rango = lambda x, a, b: a <= x <= b
dentro_rango(5, 1, 10)    # True
dentro_rango(15, 1, 10)   # False

# En una lista de funciones (fábrica)
operaciones = [
    lambda x: x + 1,
    lambda x: x * 2,
    lambda x: x ** 2,
]
for op in operaciones:
    print(op(5))   # 6, 10, 25

# Lambda vs def — cuándo usar cada una:
# ✅ lambda: argumento inline simple y corto
lista.sort(key=lambda p: p["precio"])

# ✅ def: lógica reutilizable, documentable o compleja
def clave_producto(p):
    """Ordena por precio asc, stock desc como desempate."""
    return (p["precio"], -p["stock"])
lista.sort(key=clave_producto)
Código Python con funciones lambda en un editor de código oscuro con resaltado de sintaxis
Una función lambda define una función anónima en una sola línea. Su poder está en poder pasarla directamente como argumento a map(), filter() o sorted() sin necesidad de definirla antes con def. Fuente: Pexels (licencia libre).

🗺️ map(): transformar todos los elementos

# map(función, iterable) → aplica función a cada elemento
# Devuelve un ITERADOR (lazy) — convertir a lista si necesitas todos los valores

numeros = [1, 2, 3, 4, 5]

# Con lambda
list(map(lambda x: x ** 2, numeros))        # [1, 4, 9, 16, 25]
list(map(lambda x: x * 2, numeros))         # [2, 4, 6, 8, 10]

# Con función nombrada (más legible cuando la función ya existe)
list(map(str, numeros))                      # ['1', '2', '3', '4', '5']
list(map(abs, [-3, -1, 2, -4, 5]))          # [3, 1, 2, 4, 5]

# map() con múltiples iterables — función debe aceptar N parámetros
a = [1, 2, 3]
b = [10, 20, 30]
list(map(lambda x, y: x + y, a, b))         # [11, 22, 33]
list(map(pow, [2, 3, 4], [3, 2, 2]))        # [8, 9, 16]  ← 2³, 3², 4²

# Casos de uso reales
nombres = ["  ana  ", " LUIS  ", "mARTA"]
list(map(str.strip, nombres))               # ['ana', 'LUIS', 'mARTA']
list(map(str.lower, map(str.strip, nombres)))   # ['ana', 'luis', 'marta']

precios_str = ["12.50", "8.99", "34.00", "5.75"]
precios     = list(map(float, precios_str))  # [12.5, 8.99, 34.0, 5.75]
con_iva     = list(map(lambda p: round(p * 1.21, 2), precios))
# [15.13, 10.88, 41.14, 6.96]

# map() vs comprensión de lista — equivalentes en resultado
# Comprensión: generalmente más Pythónica y legible
cuadrados_map  = list(map(lambda x: x**2, numeros))
cuadrados_comp = [x**2 for x in numeros]   # ← preferida en Python

# map() brilla cuando la función ya existe (sin necesidad de lambda)
list(map(int, ["1", "2", "3"]))             # más corto que [int(x) for x in ...]

🔍 filter(): filtrar con una condición

# filter(función, iterable) → mantiene los elementos donde función(x) es True
# Devuelve un ITERADOR lazy

numeros = [1, -3, 7, -2, 0, 8, -5, 4]

list(filter(lambda x: x > 0, numeros))       # [1, 7, 8, 4]  — solo positivos
list(filter(lambda x: x % 2 == 0, numeros))  # [-2, 0, 8, 4] — solo pares

# filter(None, iterable) — elimina valores falsy (0, "", None, [], False…)
mezclado = [1, 0, "hola", "", None, [], [1, 2], False, True]
list(filter(None, mezclado))                  # [1, 'hola', [1, 2], True]

# Casos de uso reales
emails = ["ana@example.com", "invalido", "luis@test.org", "sinAt", "marta@co.es"]
validos = list(filter(lambda e: "@" in e and "." in e.split("@")[-1], emails))
# ['ana@example.com', 'luis@test.org', 'marta@co.es']

productos = [
    {"nombre": "Laptop", "precio": 899, "stock": 5},
    {"nombre": "Ratón",  "precio": 25,  "stock": 0},
    {"nombre": "Teclado","precio": 79,  "stock": 12},
    {"nombre": "Monitor","precio": 349, "stock": 0},
]
disponibles = list(filter(lambda p: p["stock"] > 0, productos))
# [Laptop, Teclado]

baratos_disponibles = list(filter(
    lambda p: p["stock"] > 0 and p["precio"] < 100,
    productos
))
# [Teclado]

# Combinando map() + filter()
# Precio con IVA de los productos disponibles con stock > 0
precios_iva = list(map(
    lambda p: round(p["precio"] * 1.21, 2),
    filter(lambda p: p["stock"] > 0, productos)
))
# [1087.79, 95.59]

# Equivalente con comprensión (más legible para casos simples)
precios_iva = [round(p["precio"] * 1.21, 2) for p in productos if p["stock"] > 0]
💡 map/filter vs comprensiones: Para transformaciones y filtros simples, las comprensiones de lista son más Pythónicas. Usa map() y filter() cuando la función ya existe (evitas crear una lambda), cuando compones varias operaciones en un pipeline, o cuando trabajas con iteradores grandes donde la evaluación lazy importa.

➕ reduce(): acumular en un único valor

from functools import reduce

# reduce(función, iterable, [inicial])
# Aplica función(acumulador, elemento) de izquierda a derecha
# hasta reducir toda la secuencia a un único valor

numeros = [1, 2, 3, 4, 5]

# Suma acumulada
reduce(lambda acc, x: acc + x, numeros)         # 15   (1+2+3+4+5)

# Producto
reduce(lambda acc, x: acc * x, numeros)         # 120  (1×2×3×4×5)

# Máximo sin usar max() — ilustrativo
reduce(lambda a, b: a if a > b else b, numeros) # 5

# Con valor inicial
reduce(lambda acc, x: acc + x, numeros, 100)    # 115  (100+1+2+3+4+5)

# Usar operator para operaciones estándar (más eficiente que lambda)
import operator
reduce(operator.add, numeros)          # 15
reduce(operator.mul, numeros)          # 120
reduce(operator.or_, [{1,2},{2,3},{3,4}])  # {1, 2, 3, 4} — unión de sets

# Casos de uso reales
# Aplanar lista de listas
listas = [[1, 2], [3, 4], [5, 6]]
reduce(lambda acc, x: acc + x, listas)   # [1, 2, 3, 4, 5, 6]
# Nota: [elem for sub in listas for elem in sub] es más eficiente

# Máximo de una lista de dicts por campo
empleados = [
    {"nombre": "Ana",   "salario": 42000},
    {"nombre": "Luis",  "salario": 55000},
    {"nombre": "Marta", "salario": 38000},
]
mejor_pagado = reduce(
    lambda a, b: a if a["salario"] > b["salario"] else b,
    empleados
)
# {"nombre": "Luis", "salario": 55000}

# Composición de funciones con reduce
def componer(*funciones):
    """Crea una función que aplica todas las funciones en orden."""
    return reduce(lambda f, g: lambda x: g(f(x)), funciones)

pipeline = componer(
    lambda x: x * 2,
    lambda x: x + 10,
    lambda x: x ** 2,
)
pipeline(3)   # ((3*2)+10)² = 16² = 256
Infografía de lambda, map, filter, reduce y sorted con key en Python: diagrama de flujo de cada operación funcional
Las herramientas funcionales de Python: lambda define la operación, map la aplica a cada elemento, filter selecciona los que cumplen la condición, reduce acumula en un valor único y sorted ordena por criterio propio. Infografía: Ciberaula.

📊 sorted() con key: ordenar por criterio propio

# sorted(iterable, key=función, reverse=False) → nueva lista ordenada
# key recibe cada elemento y devuelve el valor por el que ordenar

numeros = [3, -1, 7, -4, 2, -9]

sorted(numeros)                          # [-9, -4, -1, 2, 3, 7]  — orden natural
sorted(numeros, reverse=True)           # [7, 3, 2, -1, -4, -9]
sorted(numeros, key=abs)                # [-1, 2, 3, -4, 7, -9]  — por valor absoluto
sorted(numeros, key=abs, reverse=True)  # [-9, 7, -4, 3, 2, -1]

# Ordenar strings
palabras = ["plátano", "manzana", "kiwi", "cereza", "uva"]
sorted(palabras)                         # orden Unicode (mayús antes que minús)
sorted(palabras, key=len)               # ['uva', 'kiwi', 'cereza', 'manzana', 'plátano']
sorted(palabras, key=str.lower)         # orden alfabético insensible a mayús

# Ordenar por múltiples criterios con tupla
# Las tuplas se comparan elemento a elemento
estudiantes = [
    {"nombre": "Ana",   "nota": 9.2, "edad": 22},
    {"nombre": "Luis",  "nota": 8.5, "edad": 20},
    {"nombre": "Marta", "nota": 9.2, "edad": 21},
    {"nombre": "Pedro", "nota": 7.8, "edad": 22},
]

# Por nota desc, luego por nombre asc
sorted(estudiantes, key=lambda e: (-e["nota"], e["nombre"]))
# Ana (9.2), Marta (9.2), Luis (8.5), Pedro (7.8)

# Ordenar objetos con operator.attrgetter (más eficiente que lambda)
from operator import attrgetter, itemgetter

# Con diccionarios: itemgetter
sorted(estudiantes, key=itemgetter("nota"))           # nota asc
sorted(estudiantes, key=itemgetter("nota", "nombre")) # nota asc, nombre asc

# Ordenar strings con acentos en español
import locale
locale.setlocale(locale.LC_ALL, "es_ES.UTF-8")
sorted(["árbol", "casa", "álgebra", "barco"], key=locale.strxfrm)
# ['álgebra', 'árbol', 'barco', 'casa']  — orden español correcto

# sort() in-place vs sorted() — mismos parámetros, diferente resultado
lista = [3, 1, 4, 1, 5]
nueva = sorted(lista)   # nueva lista, lista sin cambios
lista.sort()            # modifica lista in-place, devuelve None

# Truco: Schwartzian transform — precalcular la clave (para claves costosas)
def clave_costosa(x):
    return x ** 3 - x ** 2 + x   # imaginemos que es muy cara

datos = [5, 2, 8, 1, 9, 3]
# ❌ clave calculada N log N veces
sorted(datos, key=clave_costosa)

# ✅ precalcular: (clave, valor) → ordenar → extraer
decorado = [(clave_costosa(x), x) for x in datos]
decorado.sort()
resultado = [x for _, x in decorado]   # [1, 2, 3, 5, 8, 9]

🔗 zip() y enumerate(): iterar en paralelo

# ── ZIP ─────────────────────────────────────────────────────────────────────
# zip(*iterables) → iterador de tuplas, se detiene al agotarse el más corto

nombres = ["Ana", "Luis", "Marta"]
edades  = [22, 20, 21]
ciudades = ["Madrid", "Barcelona", "Sevilla"]

# Emparejar dos listas
for nombre, edad in zip(nombres, edades):
    print(f"{nombre}: {edad}")

# Tres o más iterables
for nombre, edad, ciudad in zip(nombres, edades, ciudades):
    print(f"{nombre}, {edad} años, {ciudad}")

# Crear diccionario desde dos listas
dict(zip(nombres, edades))      # {'Ana': 22, 'Luis': 20, 'Marta': 21}

# Desempaquetar (unzip): transponer estructura
pares = [(1, "a"), (2, "b"), (3, "c")]
nums, letras = zip(*pares)      # (1,2,3)  y  ('a','b','c')

# zip_longest — usar cuando los iterables tienen distinta longitud
from itertools import zip_longest
a = [1, 2, 3]
b = ["a", "b"]
list(zip_longest(a, b, fillvalue=None))   # [(1,'a'), (2,'b'), (3,None)]

# Caso real: comparar dos versiones de una lista
v1 = [100, 200, 300, 400]
v2 = [110, 195, 300, 420]
cambios = [(i+1, old, new, round((new-old)/old*100, 1))
           for i, (old, new) in enumerate(zip(v1, v2))
           if old != new]
# [(1, 100, 110, 10.0), (2, 200, 195, -2.5), (4, 400, 420, 5.0)]


# ── ENUMERATE ──────────────────────────────────────────────────────────────
# enumerate(iterable, start=0) → iterador de (índice, elemento)

frutas = ["manzana", "pera", "naranja"]

# ❌ forma antigua — error frecuente
for i in range(len(frutas)):
    print(i, frutas[i])

# ✅ forma Pythónica
for i, fruta in enumerate(frutas):
    print(f"{i}: {fruta}")

# Empezar desde 1 (o cualquier número)
for i, fruta in enumerate(frutas, start=1):
    print(f"Opción {i}: {fruta}")

# Crear diccionario índice → valor
indice = {i: v for i, v in enumerate(frutas)}
# {0: 'manzana', 1: 'pera', 2: 'naranja'}

# Buscar el índice de todos los elementos que cumplen una condición
numeros = [4, 7, 2, 9, 4, 1, 7]
indices_pares = [i for i, n in enumerate(numeros) if n % 2 == 0]
# [0, 2, 4]

# Combinar zip + enumerate
for i, (nombre, edad) in enumerate(zip(nombres, edades), 1):
    print(f"{i}. {nombre} ({edad} años)")

✅ any() y all(): consultas sobre colecciones

# any(iterable) → True si AL MENOS UN elemento es verdadero
# all(iterable) → True si TODOS los elementos son verdaderos
# Ambas son lazy: paran en cuanto tienen la respuesta

numeros = [2, 4, 6, 7, 8]

any(x % 2 != 0 for x in numeros)    # True  — hay algún impar (el 7)
all(x % 2 == 0 for x in numeros)    # False — no todos son pares (el 7)
all(x > 0      for x in numeros)    # True  — todos son positivos

# Casos de uso típicos
usuarios = [
    {"nombre": "Ana",   "activo": True,  "edad": 22},
    {"nombre": "Luis",  "activo": False, "edad": 17},
    {"nombre": "Marta", "activo": True,  "edad": 30},
]

# ¿Hay algún usuario activo?
any(u["activo"] for u in usuarios)              # True

# ¿Todos son mayores de edad?
all(u["edad"] >= 18 for u in usuarios)          # False (Luis tiene 17)

# ¿Algún usuario es menor y activo a la vez?
any(u["edad"] < 18 and u["activo"] for u in usuarios)   # False

# Validar formulario: todos los campos requeridos tienen valor
campos = {"nombre": "Ana", "email": "ana@test.com", "ciudad": ""}
campos_requeridos = ["nombre", "email", "ciudad"]
formulario_completo = all(campos.get(c) for c in campos_requeridos)
# False — ciudad está vacía

# Buscar si algún elemento de una lista coincide con otro (más eficiente con set)
permitidos = {"admin", "editor", "moderador"}
roles_usuario = ["editor", "viewer"]
tiene_acceso = any(r in permitidos for r in roles_usuario)   # True

# any() / all() con listas vacías
any([])     # False — sin elementos que sean verdaderos
all([])     # True  — vacío trivialmente cumple la condición (vacuous truth)

# Usar en lugar de bucles de validación verbosos
# ❌ verboso
tiene_errores = False
for valor in datos:
    if not validar(valor):
        tiene_errores = True
        break

# ✅ conciso
tiene_errores = not all(validar(v) for v in datos)
Ficha de referencia de programación funcional en Python: lambda, map, filter, reduce, sorted key, zip, enumerate, any, all y functools
Ficha completa de herramientas funcionales de Python. Incluye sintaxis de lambda, comportamiento lazy de map/filter/zip, operaciones de reduce con operator, sorted con múltiples claves y functools. Infografía: Ciberaula.

🔒 Closures: funciones que recuerdan su entorno

# Un closure es una función interna que captura variables
# del ámbito de la función externa que la creó

def hacer_multiplicador(factor):
    """Fábrica de funciones: devuelve una función que multiplica por factor."""
    def multiplicar(x):
        return x * factor   # ← captura 'factor' del ámbito exterior
    return multiplicar

doble  = hacer_multiplicador(2)
triple = hacer_multiplicador(3)

doble(5)    # 10
triple(5)   # 15
doble(7)    # 14  — 'factor' vive en la closure, no en hacer_multiplicador

# Inspeccionar la closure
doble.__closure__                          # (,)
doble.__closure__[0].cell_contents        # 2

# Fábrica de saludos
def hacer_saludo(idioma):
    saludos = {"es": "Hola", "en": "Hello", "fr": "Bonjour"}
    mensaje = saludos.get(idioma, "Hi")
    def saludar(nombre):
        return f"{mensaje}, {nombre}!"
    return saludar

saludar_es = hacer_saludo("es")
saludar_en = hacer_saludo("en")
saludar_es("Ana")    # 'Hola, Ana!'
saludar_en("Luis")   # 'Hello, Luis!'

# Closure con estado mutable usando lista (truco clásico antes de nonlocal)
def hacer_contador(inicio=0):
    contador = [inicio]   # lista mutable — el closure puede modificarla
    def incrementar(paso=1):
        contador[0] += paso
        return contador[0]
    def resetear():
        contador[0] = inicio
    incrementar.resetear = resetear   # adjuntar método al callable
    return incrementar

c = hacer_contador(10)
c()       # 11
c()       # 12
c(5)      # 17
c.resetear()
c()       # 11

# Con nonlocal (Python 3) — forma explícita y clara
def hacer_contador_v2(inicio=0):
    n = inicio
    def incrementar(paso=1):
        nonlocal n
        n += paso
        return n
    return incrementar

c2 = hacer_contador_v2()
c2()    # 1
c2(3)   # 4

# Closure para memoización manual (sin functools)
def memoizar(func):
    cache = {}
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memoizar
def fibonacci(n):
    if n <= 1: return n
    return fibonacci(n - 1) + fibonacci(n - 2)

fibonacci(35)   # rápido — los subproblemas se cachean

🧰 functools: partial, lru_cache, wraps

import functools

# ── PARTIAL ─────────────────────────────────────────────────────────────────
# partial(función, *args, **kwargs) → nueva función con algunos argumentos ya fijados

from functools import partial

def potencia(base, exponente):
    return base ** exponente

cuadrado = partial(potencia, exponente=2)
cubo     = partial(potencia, exponente=3)

cuadrado(5)    # 25
cubo(3)        # 27

# Caso real: configurar funciones de la biblioteca estándar
import os
unir_con_base = partial(os.path.join, "/var/www/ciberaula")
unir_con_base("cursos", "python")      # '/var/www/ciberaula/cursos/python'

# Crear variantes de print
print_error = partial(print, "[ERROR]", sep=" ", end="\n")
print_error("Archivo no encontrado")   # [ERROR] Archivo no encontrado

# Usar partial en map y sorted
from functools import partial
en_rango = partial(lambda a, b, x: a <= x <= b, 0, 100)
list(filter(en_rango, [-5, 0, 50, 101, 75, -1, 100]))  # [0, 50, 75, 100]


# ── LRU_CACHE ────────────────────────────────────────────────────────────────
# Memoización automática — cachea hasta maxsize llamadas más recientes

from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    if n <= 1: return n
    return fibonacci(n - 1) + fibonacci(n - 2)

fibonacci(40)    # instantáneo — sin caché tardaría segundos
fibonacci.cache_info()   # CacheInfo(hits=38, misses=41, maxsize=128, currsize=41)
fibonacci.cache_clear()  # limpiar caché

# cache (Python 3.9+) — como lru_cache(maxsize=None), ilimitado
from functools import cache

@cache
def factorial(n):
    return 1 if n == 0 else n * factorial(n - 1)

# lru_cache solo funciona con argumentos hashables
@lru_cache
def procesar(datos):              # TypeError si datos es lista (no hashable)
    ...
@lru_cache
def procesar(datos: tuple):       # ✅ tupla es hashable


# ── WRAPS ────────────────────────────────────────────────────────────────────
# Preserva el nombre y docstring de la función original en un decorador

from functools import wraps

def mi_decorador(func):
    @wraps(func)               # ← sin esto, func.__name__ sería 'wrapper'
    def wrapper(*args, **kwargs):
        print(f"Llamando a {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@mi_decorador
def saludar(nombre):
    """Saluda a alguien."""
    return f"Hola, {nombre}!"

saludar("Ana")           # Llamando a saludar → 'Hola, Ana!'
saludar.__name__         # 'saludar'      ← correcto gracias a @wraps
saludar.__doc__          # 'Saluda a alguien.'


# ── REDUCE con operator ───────────────────────────────────────────────────────
from functools import reduce
import operator

reduce(operator.add, range(1, 11))    # 55   — suma 1..10
reduce(operator.mul, range(1, 6))     # 120  — factorial de 5
reduce(operator.or_, [{1,2},{2,3},{4}])  # {1,2,3,4}

# Tabla de operadores en functools/operator equivalentes a lambda
# operator.add      ↔  lambda a, b: a + b
# operator.mul      ↔  lambda a, b: a * b
# operator.lt       ↔  lambda a, b: a < b
# operator.itemgetter(k)  ↔  lambda x: x[k]
# operator.attrgetter(a)  ↔  lambda x: getattr(x, a)

🛠️ Programa completo: pipeline de transformación de datos

"""
Pipeline funcional para procesar un catálogo de productos:
- Limpiar datos de entrada
- Filtrar por criterios de negocio
- Transformar (calcular precios con IVA, descuentos)
- Ordenar por múltiples criterios
- Generar informe formateado
Todo usando lambda, map, filter, sorted, zip, any/all y functools.
"""

from functools import reduce, partial
import operator

# ── DATOS ─────────────────────────────────────────────────────────────────────
catalogo_raw = [
    {"id": 1,  "nombre": " Laptop Pro ",  "precio": 899.0, "stock": 5,  "categoria": "electronica", "descuento": 0.10},
    {"id": 2,  "nombre": "ratón inalámbrico", "precio": 25.5, "stock": 0,  "categoria": "accesorios", "descuento": 0.0},
    {"id": 3,  "nombre": "TECLADO MECÁNICO", "precio": 79.9, "stock": 12, "categoria": "accesorios", "descuento": 0.15},
    {"id": 4,  "nombre": "Monitor 4K  ",   "precio": 349.0, "stock": 3,  "categoria": "electronica", "descuento": 0.0},
    {"id": 5,  "nombre": "webcam HD",       "precio": 59.9, "stock": 8,  "categoria": "accesorios", "descuento": 0.05},
    {"id": 6,  "nombre": "SSD 1TB",         "precio": 119.0,"stock": 0,  "categoria": "almacenamiento","descuento": 0.20},
    {"id": 7,  "nombre": "auriculares BT",  "precio": 149.9,"stock": 15, "categoria": "electronica", "descuento": 0.0},
    {"id": 8,  "nombre": "hub usb-c",       "precio": 39.9, "stock": 6,  "categoria": "accesorios", "descuento": 0.10},
]

# ── PASO 1: LIMPIAR ───────────────────────────────────────────────────────────
def limpiar_producto(p):
    return {**p, "nombre": p["nombre"].strip().title()}

catalogo = list(map(limpiar_producto, catalogo_raw))

# ── PASO 2: FILTRAR — solo productos disponibles ──────────────────────────────
en_stock = list(filter(lambda p: p["stock"] > 0, catalogo))
print(f"En stock: {len(en_stock)} de {len(catalogo)} productos")

# ── PASO 3: TRANSFORMAR — precio final (descuento + IVA) ─────────────────────
IVA = 0.21

def calcular_precio_final(p):
    precio_dto = p["precio"] * (1 - p["descuento"])
    precio_iva = precio_dto * (1 + IVA)
    return {
        **p,
        "precio_dto":   round(precio_dto, 2),
        "precio_final": round(precio_iva, 2),
        "ahorro":       round(p["precio"] * p["descuento"], 2),
    }

enriquecidos = list(map(calcular_precio_final, en_stock))

# ── PASO 4: ORDENAR — por categoría asc, precio final asc ────────────────────
ordenados = sorted(
    enriquecidos,
    key=lambda p: (p["categoria"], p["precio_final"])
)

# ── PASO 5: ESTADÍSTICAS con reduce ──────────────────────────────────────────
precios = [p["precio_final"] for p in enriquecidos]
total_inventario = reduce(
    lambda acc, p: acc + p["precio"] * p["stock"],
    enriquecidos, 0
)
precio_medio = reduce(operator.add, precios) / len(precios)

hay_descuento  = any(p["descuento"] > 0 for p in enriquecidos)
todos_con_stock = all(p["stock"] >= 3 for p in enriquecidos)

# ── PASO 6: INFORME ──────────────────────────────────────────────────────────
print(f"\n{'═'*62}")
print(f"{'CATÁLOGO DE PRODUCTOS DISPONIBLES':^62}")
print(f"{'═'*62}")
print(f"{'#':<3} {'Producto':<22} {'Cat.':<14} {'P.Base':>8} {'P.Final':>8} {'Stock':>5}")
print(f"{'-'*62}")

for i, (num, p) in enumerate(zip(range(1, len(ordenados)+1), ordenados), 1):
    descuento_str = f"(-{p['descuento']*100:.0f}%)" if p["descuento"] else ""
    print(f"{i:<3} {p['nombre']:<22} {p['categoria']:<14} "
          f"{p['precio']:>7.2f}€ {p['precio_final']:>7.2f}€ "
          f"{p['stock']:>4} {descuento_str}")

print(f"{'─'*62}")
print(f"  Precio medio (con IVA):   {precio_medio:>8.2f}€")
print(f"  Valor total inventario:   {total_inventario:>8.2f}€")
print(f"  ¿Hay productos con dto?   {'Sí' if hay_descuento else 'No'}")
print(f"  ¿Todos con stock >= 3?    {'Sí' if todos_con_stock else 'No'}")
print(f"{'═'*62}\n")

# ── BONUS: fábrica de filtros con partial ─────────────────────────────────────
def filtrar_por_campo(campo, valor, productos):
    return [p for p in productos if p[campo] == valor]

filtrar_electronica  = partial(filtrar_por_campo, "categoria", "electronica")
filtrar_accesorios   = partial(filtrar_por_campo, "categoria", "accesorios")

electronica = filtrar_electronica(enriquecidos)
accesorios  = filtrar_accesorios(enriquecidos)
print(f"Electrónica en stock: {[p['nombre'] for p in electronica]}")
print(f"Accesorios en stock:  {[p['nombre'] for p in accesorios]}")

🐛 Errores clásicos

1. Olvidar convertir map() y filter() a lista

resultado = map(lambda x: x*2, [1,2,3])
print(resultado)    #  ← no es una lista
resultado[0]        # TypeError: 'map' object is not subscriptable

# ✅ Convertir cuando necesites acceso aleatorio o múltiples iteraciones
lista = list(map(lambda x: x*2, [1,2,3]))   # [2, 4, 6]

2. Captura tardía de variables en closures dentro de bucles

# ❌ Bug clásico: todas las funciones capturan la misma 'i'
funciones = [lambda x: x + i for i in range(5)]
funciones[0](10)   # 14, no 10  ← 'i' vale 4 (último valor del bucle)
funciones[2](10)   # 14, no 12

# ✅ Fijar el valor en el momento de creación con parámetro por defecto
funciones = [lambda x, i=i: x + i for i in range(5)]
funciones[0](10)   # 10 ✅
funciones[2](10)   # 12 ✅

# ✅ O con partial
from functools import partial
sumar = lambda x, n: x + n
funciones = [partial(sumar, n=i) for i in range(5)]

3. Usar reduce() cuando existe una función built-in equivalente

# ❌ Reinventar la rueda
reduce(lambda a, b: a + b, numeros)     # usa sum() directamente
reduce(lambda a, b: a if a>b else b, numeros)  # usa max()
reduce(lambda a, b: a and b, booleanos)         # usa all()

# ✅ Built-ins son más rápidos y más legibles
sum(numeros)
max(numeros)
all(booleanos)

4. lru_cache con argumentos no hashables

@lru_cache
def procesar(datos):
    ...

procesar([1, 2, 3])     # TypeError: unhashable type: 'list'

# ✅ Convertir a tupla antes de llamar, o rediseñar la interfaz
procesar(tuple([1, 2, 3]))   # ✅

✅ Resumen y próximos pasos

Las herramientas funcionales de Python —lambda, map(), filter(), reduce(), sorted() con key, zip(), enumerate(), any(), all(), closures y functools— no son alternativas al código imperativo sino complementos: permiten expresar transformaciones de datos de forma más declarativa, componible y a menudo más concisa.

La siguiente lección del Módulo 4 cubre módulos y paquetes: cómo organizar el código en ficheros, crear tu propio módulo, usar import y entender el sistema de paquetes de Python.

❓ Preguntas frecuentes

❓ Preguntas frecuentes sobre Lambda, map, filter y functools en Python: programación funcional práctica

Las dudas más comunes respondidas de forma clara y directa.

Lambda es adecuada cuando la lógica cabe en una sola expresión y se usa como argumento inline (sorted(lista, key=lambda x: x[1])). Si la función tiene más de una expresión, necesita un nombre descriptivo, tiene lógica condicional compleja, o la vas a reutilizar en varios sitios, define una función con def. El PEP 8 desaconseja explícitamente asignar una lambda a una variable (doble = lambda x: x*2) porque pierdes todas las ventajas: mejor usar def doble(x): return x*2, que tiene nombre, es más legible y aparece correctamente en los mensajes de error y trazas de pila.
En Python 3, map(), filter() y zip() devuelven iteradores lazy: no calculan nada hasta que alguien consume el resultado. Esto ahorra memoria en colecciones grandes porque los elementos se generan uno a uno, en lugar de construir toda la lista en memoria. Si necesitas una lista, conviertes explícitamente: list(map(...)). Esta decisión de diseño es consistente con la filosofía de Python 3 de favorecer la evaluación perezosa (también aplicada a range(), dict.keys(), dict.values(), etc.).
Funcionalmente son equivalentes: reduce(f, lista, inicial) es la versión funcional de un bucle acumulador que aplica f(acumulador, elemento) en cada paso. La diferencia es expresividad y composabilidad: reduce declara la intención (acumular con esta operación) sin describir los pasos. En Python, reduce se movió a functools en Python 3 como señal de que el bucle explícito es más legible en muchos casos. Úsalo cuando la operación tiene un nombre claro (operator.add, operator.mul) o cuando compones varias operaciones. Para lógica compleja, el bucle es más claro.
Un closure es una función que captura variables de su ámbito exterior (el ámbito de la función que la definió) y las mantiene vivas aunque esa función exterior ya haya terminado de ejecutarse. Es una forma ligera de encapsular estado sin crear una clase. Un closure y una clase con __call__ son técnicamente equivalentes, pero el closure es más conciso cuando solo necesitas un método y el estado es simple. Si el objeto necesita varios métodos o el estado es complejo, una clase es más clara. La regla práctica: closure para fábricas de funciones simples, clase para entidades con comportamiento rico.
lru_cache es útil cuando: (1) la función es pura (mismo input → mismo output, sin efectos secundarios), (2) se llama repetidamente con los mismos argumentos, y (3) el cálculo es costoso. Los casos clásicos son recursión con subproblemas repetidos (Fibonacci, rutas en grafos), cálculos matemáticos caros (factorial, números primos), llamadas a APIs externas en procesamiento por lotes, y parsing o compilación de expresiones regulares. No uses lru_cache cuando la función depende de estado externo, hora actual, aleatoriedad o tiene efectos secundarios, porque el caché devolverá el resultado guardado ignorando esos cambios.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Lambda, map, filter y functools en Python: programación funcional práctica? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!