Programación funcional en Python: map, filter, reduce y más

📅 Actualizado en marzo 2026 📊 Nivel: Intermedio ⏱️ 21 min de lectura

Llegamos al Módulo 8 — Python avanzado. Hasta aquí hemos aprendido a escribir código que funciona. En este módulo aprendemos a escribir código que razona: estructurado según principios que hacen que la corrección sea demostrable, el comportamiento predecible y la composición natural. La programación funcional es uno de esos principios. No es una moda ni un paradigma exótico: es una forma de pensar que lleva décadas produciendo el software más fiable que existe.

En esta lección aprendemos los fundamentos funcionales que Python soporta de primera clase: funciones puras, inmutabilidad, lambdas, map, filter, reduce, closures y los módulos functools e itertools. No vas a reescribir todo tu código en estilo funcional — Python no es Haskell — pero vas a tener una herramienta más en el cinturón que te permitirá resolver ciertos problemas con menos código, más claridad y cero estado oculto.

Medallón de Demócrito de Abdera, filósofo griego presocrático y padre del atomismo
Ἐν δὲ τῷ παντὶ οὐδὲν ἐστιν ἀλλ' ἢ ἄτομοι καὶ κενόν
«En el todo no existe nada salvo átomos y espacio vacío»
Demócrito de Abdera · Filósofo griego · 460 a.C. – 370 a.C.
Demócrito construyó su cosmología sobre una idea radical para el siglo V a.C.: la realidad entera se compone de partículas indivisibles —átomos— que se combinan en el vacío para formar todo lo que existe. Cada átomo es puro, inmutable, sin propiedades que dependan de su contexto. La programación funcional lleva esa intuición al software: una función pura es un átomo computacional. No tiene estado interior, no modifica nada fuera de sí misma, y siempre produce el mismo resultado para los mismos argumentos. Como los átomos de Demócrito, es inmutable y composable. La complejidad real —un sistema entero, una aplicación— surge de combinar esos átomos en el vacío de datos inmutables que fluyen de una función a la siguiente. El software más fiable de la historia —compiladores, sistemas operativos de telecomunicaciones, reactores nucleares— está escrito en lenguajes que fuerzan este modelo. Python no fuerza nada, pero te da todas las herramientas para adoptarlo donde tiene sentido.

¿Qué es la programación funcional?

La programación funcional (FP) es un paradigma que trata la computación como evaluación de funciones matemáticas. Tiene tres pilares fundamentales: funciones puras (sin efectos secundarios), inmutabilidad (los datos no se modifican, se transforman en copias nuevas) y composición (los programas se construyen combinando funciones simples). En contraposición al paradigma imperativo, donde el programa es una secuencia de instrucciones que modifican el estado, FP describe qué se quiere calcular, no cómo hacerlo paso a paso.

# ── Estilo imperativo: el programa modifica estado ────────────
numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
resultado = []
for n in numeros:
    if n % 2 == 0:
        resultado.append(n * n)

# ── Estilo funcional: transformaciones sin estado mutable ─────
from functools import reduce

pares_al_cuadrado = list(map(lambda n: n * n,
                              filter(lambda n: n % 2 == 0, numeros)))

# ── Estilo Pythónico (comprensión): el equilibrio pragmático ──
pares_al_cuadrado = [n * n for n in numeros if n % 2 == 0]

# Los tres producen el mismo resultado: [4, 16, 36, 64, 100]
# La comprensión es la preferida en Python por legibilidad,
# pero map/filter brillan en pipelines con funciones ya definidas.


# ── Las funciones son ciudadanos de primera clase en Python ───
def aplicar(funcion, valor):
    return funcion(valor)

def doble(x): return x * 2
def cuadrado(x): return x ** 2

print(aplicar(doble, 5))      # 10
print(aplicar(cuadrado, 5))   # 25

transformaciones = [doble, cuadrado, abs, str]
for fn in transformaciones:
    print(fn(-3))   # -6, 9, 3, '-3'
📋 FP en Python: pragmatismo, no purismo. Python no es un lenguaje funcional puro y no intenta serlo. No optimiza la recursión de cola, las listas son mutables y el bucle for es perfectamente aceptable. La idea es conocer las herramientas funcionales y usarlas cuando simplifican el código, no sustituir todos los bucles por map() para parecer más sofisticado.

Funciones puras e inmutabilidad

Una función pura cumple dos condiciones: dado el mismo input siempre produce el mismo output, y no tiene efectos secundarios (no modifica variables externas, no hace I/O, no muta sus argumentos). Las funciones puras son la base de la testabilidad: si no tienen efectos secundarios, cada test es aislado por definición, sin necesidad de mocks ni fixtures de setup.

# ── Función IMPURA: depende de estado externo ─────────────────
descuento_global = 0.1

def calcular_precio_impuro(precio: float) -> float:
    return precio * (1 - descuento_global)
    # si alguien cambia descuento_global, la función da resultados distintos

# ── Función PURA: solo depende de sus argumentos ─────────────
def calcular_precio(precio: float, descuento: float) -> float:
    return precio * (1 - descuento)

# Siempre: calcular_precio(100, 0.1) == 90.0
# Testeable directamente, sin setup, sin mocks


# ── Mutabilidad: el error más silencioso de Python ────────────
# MAL: mutar el argumento de entrada
def agregar_impuesto_malo(precios: list[float], tasa: float) -> list[float]:
    for i in range(len(precios)):
        precios[i] *= (1 + tasa)   # ← MUTA la lista original
    return precios

mis_precios = [10.0, 20.0, 30.0]
resultado   = agregar_impuesto_malo(mis_precios, 0.21)
print(mis_precios)   # [12.1, 24.2, 36.3] ← la lista original cambió sin aviso

# BIEN: devolver una transformación, no mutar
def agregar_impuesto(precios: list[float], tasa: float) -> list[float]:
    return [p * (1 + tasa) for p in precios]

mis_precios = [10.0, 20.0, 30.0]
con_iva     = agregar_impuesto(mis_precios, 0.21)
print(mis_precios)  # [10.0, 20.0, 30.0]  ← intacta
print(con_iva)      # [12.1, 24.2, 36.3]


# ── Estructuras inmutables: tuple, frozenset, namedtuple ──────
from collections import namedtuple
from typing import NamedTuple

Punto = namedtuple('Punto', ['x', 'y'])
p1 = Punto(3, 4)
p2 = p1._replace(x=10)   # nuevo Punto, p1 intacto
print(p1)   # Punto(x=3, y=4)
print(p2)   # Punto(x=10, y=4)

class Punto3D(NamedTuple):
    x: float
    y: float
    z: float = 0.0

p3d = Punto3D(1.0, 2.0, 3.0)
print(p3d.x, p3d.z)   # 1.0  3.0

# frozenset: set inmutable — útil como clave de diccionario
permisos_admin  = frozenset({'leer', 'escribir', 'borrar'})
permisos_lector = frozenset({'leer'})
print(permisos_lector.issubset(permisos_admin))   # True
Cristal geométrico transparente con facetas perfectas reflejando luz sobre superficie oscura
Un cristal es el modelo perfecto de la inmutabilidad: su estructura atómica está fijada en el momento de su formación y no cambia con el tiempo ni el uso. Puedes iluminarlo, girarlo, usarlo como lente — pero sus facetas permanecen exactamente igual. Una función pura es así: puedes llamarla mil veces con los mismos argumentos y siempre obtendrás el mismo resultado, sin importar cuándo, desde dónde o en qué orden la llames. Esa propiedad — la referencialidad transparente — es lo que hace el código funcional tan fácil de razonar y testear. Fuente: Pexels (licencia libre).

Funciones lambda: funciones anónimas

Una expresión lambda crea una función anónima en una sola línea: lambda parámetros: expresión. Su poder no está en hacer cosas que def no pueda hacer — puede hacer exactamente lo mismo — sino en la concisión cuando la función es simple y se usa en un contexto donde una función completa sería verbosa.

# ── Sintaxis básica ───────────────────────────────────────────
cuadrado = lambda x: x ** 2
suma      = lambda a, b: a + b

print(cuadrado(5))    # 25
print(suma(3, 4))     # 7


# ── Caso de uso principal: key= en sorted(), min(), max() ────
empleados = [
    {"nombre": "Ana",    "salario": 55000, "departamento": "Ingeniería"},
    {"nombre": "Carlos", "salario": 42000, "departamento": "Marketing"},
    {"nombre": "Beatriz","salario": 68000, "departamento": "Ingeniería"},
    {"nombre": "David",  "salario": 48000, "departamento": "Marketing"},
]

por_salario  = sorted(empleados, key=lambda e: e["salario"], reverse=True)
mejor_pagado = max(empleados, key=lambda e: e["salario"])
print(f"Mejor pagado: {mejor_pagado['nombre']}")

# Ordenar por varios criterios
ordenado = sorted(empleados,
                  key=lambda e: (e["departamento"], -e["salario"]))


# ── Lambda en pipelines ───────────────────────────────────────
transformaciones = [
    lambda x: x * 2,
    lambda x: x + 10,
    lambda x: x ** 2,
]

valor = 5
for fn in transformaciones:
    valor = fn(valor)
print(valor)   # ((5*2)+10)^2 = 400


# ── Limitaciones: solo una expresión ─────────────────────────
clasificar = lambda n: "par" if n % 2 == 0 else "impar"
print(clasificar(7))    # 'impar'

# PEP 8: desaconseja asignar lambdas a variables
# doble = lambda x: x * 2  ← en traceback aparece como ""
# Usa def en ese caso

map() y filter(): transformar y filtrar

map(funcion, iterable) aplica la función a cada elemento y devuelve un iterador lazy. filter(funcion, iterable) devuelve solo los elementos para los que la función devuelve True. Ambos son lazy: no calculan nada hasta que se itera sobre ellos, lo que los hace eficientes con grandes datasets.

# ── map(): transformar cada elemento ─────────────────────────
numeros = [1, 2, 3, 4, 5]

cuadrados  = list(map(lambda x: x ** 2, numeros))   # [1, 4, 9, 16, 25]
como_texto = list(map(str, numeros))                 # ['1', '2', '3', '4', '5']

palabras = ["  hola  ", "mundo  ", "  python"]
limpias  = list(map(str.strip, palabras))   # ['hola', 'mundo', 'python']

# map con múltiples iterables
a = [1, 2, 3]; b = [10, 20, 30]
sumas = list(map(lambda x, y: x + y, a, b))   # [11, 22, 33]


# ── filter(): seleccionar elementos ───────────────────────────
numeros  = range(1, 21)
pares    = list(filter(lambda n: n % 2 == 0, numeros))
no_nulos = list(filter(None, [0, 1, "", "hola", None, [1]]))  # [1, 'hola', [1]]
# filter(None, ...) elimina valores falsy: 0, "", None, [], {}

def es_primo(n):
    if n < 2: return False
    return all(n % i != 0 for i in range(2, int(n**0.5) + 1))

primos = list(filter(es_primo, range(2, 50)))
print(primos)   # [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]


# ── Pipeline map + filter ─────────────────────────────────────
ventas = [
    {"producto": "A", "cantidad": 5,  "precio": 12.0, "activo": True},
    {"producto": "B", "cantidad": 0,  "precio": 8.0,  "activo": True},
    {"producto": "C", "cantidad": 3,  "precio": 25.0, "activo": False},
    {"producto": "D", "cantidad": 12, "precio": 5.0,  "activo": True},
]

con_ventas = filter(lambda v: v["activo"] and v["cantidad"] > 0, ventas)
ingresos   = map(lambda v: {"producto": v["producto"],
                              "ingreso": v["cantidad"] * v["precio"]}, con_ventas)
resultado  = sorted(ingresos, key=lambda x: x["ingreso"], reverse=True)

for r in resultado:
    print(f"{r['producto']}: {r['ingreso']:.2f} €")
# D: 60.00 €   A: 60.00 €
Diagrama de programación funcional en Python: pipeline map-filter-reduce, estructura de una closure, composición de funciones y módulos functools e itertools
El mapa completo de la programación funcional en Python: cómo fluyen los datos a través de un pipeline map-filter-reduce, cómo una closure captura su entorno léxico, cómo se componen funciones simples en transformaciones complejas, y qué ofrece cada módulo de la librería estándar. Infografía: Ciberaula.

reduce() y acumuladores

reduce(funcion, iterable) aplica una función acumulativa de dos argumentos de izquierda a derecha, reduciéndola a un único valor. Es la operación de fold de la programación funcional: toma una secuencia y la colapsa en un escalar (suma, producto, máximo, concatenación, cualquier acumulación).

from functools import reduce

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

suma     = reduce(lambda acc, x: acc + x, numeros)   # 15
producto = reduce(lambda acc, x: acc * x, numeros)   # 120
maximo   = reduce(lambda a, b: a if a > b else b, numeros)   # 5

# Con valor inicial
suma_con_base = reduce(lambda acc, x: acc + x, numeros, 100)   # 115

# ── Cómo funciona reduce paso a paso ─────────────────────────
# reduce(f, [a, b, c, d]) = f(f(f(a, b), c), d)
# Para suma [1,2,3,4,5]: f(1,2)=3 → f(3,3)=6 → f(6,4)=10 → f(10,5)=15


# ── Casos de uso reales ───────────────────────────────────────
# Concatenar listas (flatten un nivel)
listas = [[1, 2], [3, 4], [5, 6]]
plana  = reduce(lambda acc, lst: acc + lst, listas, [])
print(plana)   # [1, 2, 3, 4, 5, 6]

# Contar frecuencias
palabras = ["hola", "mundo", "hola", "python", "hola", "mundo"]
frecuencias = reduce(
    lambda acc, p: {**acc, p: acc.get(p, 0) + 1},
    palabras, {}
)
print(frecuencias)   # {'hola': 3, 'mundo': 2, 'python': 1}

# Estadísticas en una sola pasada
datos = [4.2, 7.1, 2.8, 9.3, 5.5, 3.7]
stats = reduce(
    lambda acc, x: {
        "suma":   acc["suma"] + x,
        "minimo": min(acc["minimo"], x),
        "maximo": max(acc["maximo"], x),
        "cuenta": acc["cuenta"] + 1,
    },
    datos,
    {"suma": 0, "minimo": float("inf"), "maximo": float("-inf"), "cuenta": 0}
)
stats["media"] = stats["suma"] / stats["cuenta"]
print(stats)   # {'suma': 32.6, 'minimo': 2.8, 'maximo': 9.3, 'cuenta': 6, 'media': 5.43}


# ── Cuándo usar reduce vs alternativas ───────────────────────
# sum(numeros)    → más claro que reduce para suma
# max(numeros)    → más claro que reduce para máximo
# "".join(lista)  → más claro que reduce para concatenar
# reduce brilla cuando no hay built-in equivalente y el patrón es no trivial
💡 reduce() fue built-in en Python 2. En Python 3 se movió a functools deliberadamente — Guido van Rossum considera que la mayoría de los casos de uso de reduce() son más claros con un bucle for explícito o funciones built-in como sum(), max() o any(). Úsalo cuando la acumulación es genuinamente no trivial y la versión funcional es más concisa que el bucle equivalente.

Closures: funciones que recuerdan su contexto

Una closure es una función que captura y recuerda las variables de su entorno léxico (el scope donde fue definida), incluso después de que ese entorno ya no esté activo en el stack de llamadas. Es el mecanismo que hace posibles los decoradores, las fábricas de funciones y el estado encapsulado sin clases.

# ── Closure básico: fábrica de funciones ──────────────────────
def hacer_multiplicador(factor: int):
    """Devuelve una función que multiplica su argumento por factor."""
    def multiplicar(x):
        return x * factor   # 'factor' capturado del scope exterior
    return multiplicar

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

print(doble(5))    # 10
print(triple(5))   # 15

print(doble.__closure__[0].cell_contents)    # 2
print(triple.__closure__[0].cell_contents)   # 3


# ── Estado mutable encapsulado ────────────────────────────────
def hacer_contador(inicio: int = 0):
    count = [inicio]   # lista para permitir mutación

    def incrementar(paso: int = 1):
        count[0] += paso
        return count[0]

    def resetear():
        count[0] = inicio

    def valor():
        return count[0]

    return incrementar, resetear, valor

inc, reset, val = hacer_contador(10)
print(inc())    # 11
print(inc(5))   # 16
reset()
print(val())    # 10


# ── Closure en la práctica: formateador configurable ─────────
def hacer_formateador(moneda: str, decimales: int = 2):
    simbolos = {"EUR": "€", "USD": "$", "GBP": "£"}
    simbolo  = simbolos.get(moneda, moneda)

    def formatear(valor: float) -> str:
        return f"{valor:,.{decimales}f} {simbolo}"

    return formatear

fmt_eur = hacer_formateador("EUR")
fmt_usd = hacer_formateador("USD", decimales=0)

print(fmt_eur(1234.5))   # '1,234.50 €'
print(fmt_usd(99.99))    # '100 $'


# ── Trampa clásica: closures en bucles ────────────────────────
# PROBLEMA: todas capturan la MISMA variable i (su valor final: 4)
funciones_malas = [lambda x: x * i for i in range(5)]
print([f(2) for f in funciones_malas])   # [8, 8, 8, 8, 8] ← incorrecto

# SOLUCIÓN: capturar el valor en el momento de creación
funciones_ok = [lambda x, factor=i: x * factor for i in range(5)]
print([f(2) for f in funciones_ok])   # [0, 2, 4, 6, 8] ← correcto

functools: partial, lru_cache y compose

El módulo functools contiene las herramientas de programación funcional de la librería estándar. Las más útiles en el día a día son partial para crear versiones especializadas de funciones con argumentos prefijados, y lru_cache para memoización automática.

from functools import partial, lru_cache, reduce, wraps, cache
import math, operator

# ── partial: fijar argumentos ─────────────────────────────────
log2   = partial(math.log, base=2)
log10  = partial(math.log, base=10)

print(log2(8))    # 3.0
print(log10(100)) # 2.0

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

cuadrado = partial(potencia, exponente=2)
cubo     = partial(potencia, exponente=3)
print(cuadrado(5))   # 25
print(cubo(3))       # 27

multiplicar_por_3 = partial(operator.mul, 3)
print(list(map(multiplicar_por_3, [1, 2, 3, 4, 5])))   # [3, 6, 9, 12, 15]


# ── lru_cache: memoización automática ────────────────────────
@lru_cache(maxsize=None)
def fib(n: int) -> int:
    if n <= 1: return n
    return fib(n-1) + fib(n-2)

import time
t0 = time.perf_counter()
print(fib(40))   # 102334155
print(f"{time.perf_counter()-t0:.4f}s")   # < 0.001s

print(fib.cache_info())
# CacheInfo(hits=38, misses=41, maxsize=None, currsize=41)

# @cache (Python 3.9+): equivalente más rápido
@cache
def factorial(n: int) -> int:
    return 1 if n == 0 else n * factorial(n-1)


# ── Composición de funciones ──────────────────────────────────
def pipe(*funciones):
    """Compone funciones de izquierda a derecha: pipe(f,g,h)(x) = h(g(f(x)))"""
    return reduce(lambda f, g: lambda *args: g(f(*args)), funciones)

limpiar       = str.strip
minusculas    = str.lower
quitar_puntos = lambda s: s.replace(".", "").replace(",", "")

normalizar = pipe(limpiar, minusculas, quitar_puntos)
print(normalizar("  Hola, Mundo.  "))   # 'hola mundo'

procesar = pipe(
    lambda nums: filter(lambda x: x > 0, nums),
    lambda nums: map(lambda x: x ** 2, nums),
    list
)
print(procesar([-3, 1, -1, 4, 2]))   # [1, 16, 4]


# ── wraps: preservar metadatos en decoradores ─────────────────
def mi_decorador(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Llamando a {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@mi_decorador
def saludar(nombre: str) -> str:
    """Genera un saludo personalizado."""
    return f"Hola, {nombre}!"

print(saludar.__name__)   # 'saludar'  (sin @wraps sería 'wrapper')
print(saludar.__doc__)    # 'Genera un saludo personalizado.'

itertools: combinatoria y pipelines lazy

El módulo itertools es una colección de herramientas para trabajar con iteradores de forma eficiente y lazy. Sus funciones nunca materializan la secuencia completa en memoria: generan elementos uno a uno bajo demanda. Indispensable para procesar grandes volúmenes de datos y para combinatoria.

import itertools

# ── Infinitos (usar con islice) ───────────────────────────────
numeros  = itertools.count(start=1, step=2)
primeros = list(itertools.islice(numeros, 5))   # [1, 3, 5, 7, 9]

ciclo = itertools.cycle(['A', 'B', 'C'])
turno = [next(ciclo) for _ in range(7)]   # ['A','B','C','A','B','C','A']


# ── Combinatoria ──────────────────────────────────────────────
letras = ['A', 'B', 'C']

permutaciones = list(itertools.permutations(letras))
print(len(permutaciones))   # 6 (3!)

combinaciones = list(itertools.combinations(letras, 2))
print(combinaciones)   # [('A','B'), ('A','C'), ('B','C')]

producto = list(itertools.product([0, 1], repeat=3))   # 2^3 = 8 combis
print(producto[:4])   # [(0,0,0), (0,0,1), (0,1,0), (0,1,1)]


# ── Encadenamiento y agrupación ───────────────────────────────
todas = list(itertools.chain([1, 2], [3, 4], [5, 6]))   # [1,2,3,4,5,6]
plano = list(itertools.chain.from_iterable([[1,2],[3,4],[5,6]]))

# groupby: agrupar elementos CONSECUTIVOS con la misma clave
# IMPORTANTE: ordenar antes de groupby
ventas = [
    {"region": "Norte", "importe": 1200},
    {"region": "Norte", "importe": 800},
    {"region": "Sur",   "importe": 950},
    {"region": "Sur",   "importe": 1300},
]

ventas_sorted = sorted(ventas, key=lambda v: v["region"])
for region, grupo in itertools.groupby(ventas_sorted, key=lambda v: v["region"]):
    total = sum(v["importe"] for v in grupo)
    print(f"{region}: {total:,} €")
# Norte: 2,000 €  /  Sur: 2,250 €


# ── takewhile / dropwhile / filterfalse ──────────────────────
nums = [2, 4, 6, 7, 8, 10]

pares_iniciales = list(itertools.takewhile(lambda n: n % 2 == 0, nums))
print(pares_iniciales)   # [2, 4, 6] — para en el primer impar

desde_impar = list(itertools.dropwhile(lambda n: n % 2 == 0, nums))
print(desde_impar)   # [7, 8, 10] — salta hasta el primer impar


# ── Pipeline lazy de gran escala ──────────────────────────────
def procesar_csv_lazy(ruta: str, limite: int | None = None):
    """Genera dicts de un CSV enorme sin cargarlo entero en RAM."""
    with open(ruta, 'r', encoding='utf-8') as f:
        cabeceras = next(f).strip().split(',')
        lineas = itertools.islice(f, limite) if limite else f
        for linea in lineas:
            yield dict(zip(cabeceras, linea.strip().split(',')))
Tuberías industriales de acero inoxidable conectadas en serie con válvulas y codos, instalación de procesamiento industrial
Una tubería industrial es la metáfora más precisa de un pipeline funcional: el fluido entra por un extremo y atraviesa etapas sucesivas de transformación — filtrado, calentamiento, presurización — antes de salir convertido en algo distinto. Cada etapa es independiente, intercambiable y hace una sola cosa bien. itertools construye pipelines así: cadenas de iteradores lazy que transforman datos elemento a elemento sin cargarlos en memoria, donde cada función es una válvula que pasa o bloquea, acumula o transforma. Fuente: Pexels (licencia libre).
Ficha de referencia rápida de programación funcional en Python: lambda, map, filter, reduce, functools y itertools
La referencia completa de programación funcional en Python en una página: sintaxis lambda, map/filter/reduce con ejemplos, las funciones más útiles de functools (partial, lru_cache, wraps) e itertools (chain, groupby, product, combinations). Ficha de referencia: Ciberaula.

❓ Preguntas frecuentes

❓ Preguntas frecuentes sobre Programación funcional en Python: map, filter, reduce y más

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

Python es un lenguaje multiparadigma: soporta programación imperativa, orientada a objetos y funcional, pero sin ser puramente funcional. A diferencia de Haskell o Erlang, Python no fuerza la inmutabilidad (las listas son mutables), no optimiza la recursión de cola (hay un límite de recursión por defecto de 1000), y no tiene un sistema de tipos algebraicos nativo. Lo que Python sí ofrece es un conjunto sólido de herramientas funcionales: funciones como objetos de primera clase, lambdas, map/filter/reduce, closures, decoradores, functools e itertools. La filosofía Pythónica es usar el paradigma correcto para cada problema: comprensiones de lista en lugar de map/filter cuando es más legible, y programación funcional cuando produce código más limpio y testeable.
Lambda es apropiado cuando la función es simple (una expresión), se usa una sola vez, y su propósito queda claro en contexto. Los casos típicos: como argumento de sorted(), map(), filter(), o max() — sorted(lista, key=lambda x: x["precio"]) — y como callback en frameworks que esperan funciones pequeñas. Usa def cuando: la función tiene más de una expresión, necesita un nombre descriptivo para que el código sea legible, lanza excepciones, o vas a reutilizarla en más de un lugar. La guía de estilo PEP 8 desaconseja asignar lambdas a variables (lambda x: x*2 asignado a una variable) porque en ese caso una función def es siempre más clara y tiene nombre en los tracebacks.
En la comunidad Python, las comprensiones de lista se consideran más "Pythónicas" que map() y filter() por tres razones: legibilidad (el flujo es más natural para alguien que lee de izquierda a derecha), expresividad (puedes combinar transformación y filtrado en una sola expresión: [x*2 for x in lista if x > 0]), y rendimiento (en la mayoría de benchmarks, las comprensiones son igual o más rápidas). map() y filter() tienen ventajas cuando: trabajas con funciones ya definidas (map(str.upper, palabras) es más conciso que [p.upper() for p in palabras]), o cuando produces iterables lazy en pipelines de procesamiento con grandes volúmenes de datos donde no quieres materializar la lista completa en memoria.
Una closure es una función que "captura" variables de su scope exterior y las recuerda aunque ese scope ya no exista en el stack de llamadas. En la práctica, sirve para tres cosas principales: (1) crear fábricas de funciones — hacer_multiplicador(3) devuelve una función que siempre multiplica por 3, sin repetir código; (2) mantener estado sin usar clases — un contador implementado como closure es más ligero que una clase con un solo método; (3) implementar decoradores — la función wrapper que devuelve un decorador es una closure que "recuerda" la función original. La diferencia clave con una clase es que una closure es una función, no un objeto, por lo que es más ligera y composable. Para estado complejo con múltiples métodos, una clase es más apropiada.
lru_cache merece la pena cuando la función: (1) es pura — mismo input siempre produce mismo output, sin efectos secundarios; (2) se llama repetidamente con los mismos argumentos; (3) el cálculo es costoso (recursión, operaciones matemáticas pesadas, consultas a BD). Los casos canónicos son Fibonacci recursivo, cálculos combinatorios, y consultas a APIs externas que no cambian frecuentemente. No uses lru_cache cuando: la función tiene efectos secundarios, los argumentos son siempre distintos (el caché no ayuda), los argumentos no son hashables (listas, dicts — da TypeError), o la función consume mucha memoria por resultado (el caché retiene las N últimas llamadas). Para APIs HTTP con TTL, considera cachetools con expiración en lugar de lru_cache.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Programación funcional en Python: map, filter, reduce y más? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

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