Lambda, map, filter y functools en Python: programación funcional práctica
- Funciones como ciudadanos de primera clase
- lambda: funciones anónimas de una expresión
- map(): transformar todos los elementos
- filter(): filtrar con una condición
- reduce(): acumular en un único valor
- sorted() con key: ordenar por criterio propio
- zip() y enumerate(): iterar en paralelo
- any() y all(): consultas sobre colecciones
- Closures: funciones que recuerdan su entorno
- functools: partial, lru_cache, wraps
- Programa completo: pipeline de transformación de datos
- Errores clásicos
- Preguntas frecuentes
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)
🗺️ 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() 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
📊 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)
🔒 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) #
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.
💬 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.
Todavía no hay mensajes. ¡Sé el primero en participar!