Estas tres funciones vienen del paradigma funcional y Python las tiene desde siempre, pero en Python 3 su rol cambió sutilmente. No son malas —son herramientas con casos de uso precisos— pero usarlas por defecto en lugar de comprehensions es, en la mayoría de los casos, un error de criterio.
La mecánica primero
map(func, iterable) aplica func a cada elemento y devuelve un iterador perezoso. No ejecuta nada hasta que algo consuma el resultado. Lo mismo con filter(func, iterable): no filtra en el momento de la llamada, construye un objeto que filtrará bajo demanda. Esto es distinto a Python 2, donde ambas retornaban listas y materializaban todo en memoria de inmediato.
functools.reduce(func, iterable, initial) es diferente en naturaleza: no produce una secuencia, produce un único valor colapsando el iterable de izquierda a derecha. func recibe el acumulador y el elemento actual, y retorna el nuevo acumulador. Si das initial, ese es el valor de arranque; si no lo das y el iterable tiene un solo elemento, ese elemento es el resultado directo.
El motivo por el que reduce vive en functools en lugar de ser builtin es explícito en el PEP correspondiente: Guido consideró que casi siempre hay una alternativa más legible (sum, any, all, max), y exilar reduce a functools es una señal de que debería ser tu última opción, no la primera.
¿Cuándo usar cada uno entonces? La regla práctica es esta: si tienes una función ya nombrada, map y filter son limpios y directos. Si necesitas una lambda para que funcionen, casi seguro una comprehension es más legible. Y reduce brilla cuando la acumulación tiene semántica propia que ningún builtin captura.
Lo que rompes si los usas mal: map y filter son iteradores de un solo paso —si intentas iterar sobre el mismo objeto dos veces, la segunda pasada está vacía. Y reduce sin initial sobre un iterable vacío lanza TypeError; con un iterable de un elemento devuelve ese elemento sin llamar a func ni una vez, lo que puede sorprender si func tiene efectos secundarios.
from functools import reduce
from operator import add, mul
from typing import Iterable
# --- map: función ya nombrada vs lambda trivial ---
def normalize(s: str) -> str:
return s.strip().lower()
words = [" Hello ", "WORLD ", " Python"]
# Cuando ya existe una función nombrada, map es idiomático y claro
normalized = list(map(normalize, words))
# Con lambda, la comprehension gana en legibilidad
doubled_bad = list(map(lambda x: x * 2, range(10))) # ruido innecesario
doubled_good = [x * 2 for x in range(10)] # intención obvia
# --- filter: mismo principio ---
numbers = range(-5, 6)
# Función nombrada → map/filter aceptables
positives = list(filter(None, numbers)) # None como pred filtra falsy
# Lambda → la comprehension es más directa
evens_bad = list(filter(lambda x: x % 2 == 0, numbers))
evens_good = [x for x in numbers if x % 2 == 0]
# --- Combinando map+filter: aquí las comprehensions dominan ---
data = ["42", "x", "7", "", "13", "abc"]
# Con map+filter anidados la lectura va de adentro hacia afuera: incómodo
def is_digit_str(s: str) -> bool:
return s.isdigit()
parsed_functional = list(map(int, filter(is_digit_str, data)))
# La comprehension es lineal, se lee de izquierda a derecha
parsed_comprehension = [int(s) for s in data if s.isdigit()]
# --- reduce: dónde realmente aporta ---
# Suma simple → usa sum(), no reduce
total = sum(range(1, 11))
# Producto → math.prod en 3.8+, pero reduce lo ilustra bien
# operator.mul evita la lambda trivial: mul(a, b) == a * b
factorial_10 = reduce(mul, range(1, 11))
# Caso donde reduce es genuinamente expresivo:
# aplanar una lista de listas en una sola pasada
nested = [[1, 2], [3, 4], [5, 6]]
flat = reduce(add, nested, []) # initial=[] cubre el caso de nested vacío
# Construir un índice invertido: {valor: [índices donde aparece]}
# La acumulación es compleja; un for lo haría más verboso sin aportar claridad
entries = [("a", 1), ("b", 2), ("a", 3), ("c", 4), ("b", 5)]
def index_reducer(acc: dict, item: tuple) -> dict:
key, val = item
acc.setdefault(key, []).append(val)
return acc # reduce espera que devuelvas el acumulador
inverted = reduce(index_reducer, entries, {})
# {"a": [1, 3], "b": [2, 5], "c": [4]}
# --- operator como alternativa idiomática a lambdas aritméticas ---
from operator import attrgetter, itemgetter
records = [{"name": "Alice", "score": 92}, {"name": "Bob", "score": 78}]
# itemgetter devuelve un callable: más rápido que lambda y sin overhead de frame
names = list(map(itemgetter("name"), records))
scores = list(map(itemgetter("score"), records))
Lo que realmente importa en cada decisión
El patrón list(map(normalize, words)) es idiomático porque normalize ya existe y tiene nombre: leer map(normalize, words) te dice exactamente “aplica normalización a cada palabra” sin ruido sintáctico. La comprehension equivalente —[normalize(w) for w in words]— tampoco está mal, pero en este caso la ventaja es un empate o ligera victoria para map.
En cuanto aparece una lambda, la balanza se inclina. map(lambda x: x * 2, range(10)) te obliga a decodificar la lambda antes de entender el propósito; [x * 2 for x in range(10)] es lineal y no requiere ese salto mental. Python tiene comprehensions precisamente para esto: son sintaxis de primera clase, no un añadido.
La combinación map(int, filter(is_digit_str, data)) ilustra el problema de anidar estas funciones: la lectura va de adentro hacia afuera, que es la dirección opuesta a como pensamos. La comprehension [int(s) for s in data if s.isdigit()] se lee en el orden natural —producción, fuente, condición— y no necesita la función auxiliar is_digit_str.
Con reduce, el criterio es distinto. Pregúntate primero si hay un builtin que lo haga: sum, max, min, any, all, math.prod (3.8+). Si ninguno aplica, reduce con una función reductora bien nombrada puede ser más expresivo que un for explícito, especialmente cuando la acumulación construye una estructura compleja como el índice invertido del ejemplo. La clave está en que el index_reducer tiene nombre y propósito claro; si lo reemplazaras por una lambda de tres líneas, habrías ganado en concisión y perdido en comprensión.
El uso de operator.mul, operator.add, operator.itemgetter y compañía elimina las lambdas aritméticas o de acceso que no aportan nada propio. reduce(mul, range(1, 11)) se lee directamente; reduce(lambda a, b: a * b, range(1, 11)) es exactamente lo mismo con más ruido. El módulo operator existe para esto: expone las operaciones del intérprete como callables nombrados, que además son más rápidos que las lambdas equivalentes porque no tienen el overhead de un frame de función Python completo.
Errores que debes conocer
Error: consumir un map o filter dos veces esperando obtener los mismos resultados, porque son iteradores de un solo paso y la segunda iteración devuelve nada.
# ❌ Wrong result = map(str.upper, ["a", "b", "c"]) first_pass = list(result) # ['A', 'B', 'C'] second_pass = list(result) # [] — el iterador está agotado # ✅ Right result = list(map(str.upper, ["a", "b", "c"])) # materializa una vez first_pass = result # ['A', 'B', 'C'] second_pass = result # ['A', 'B', 'C']
Materializa con list() en el punto de creación si necesitas reutilizar el resultado; si solo lo recorres una vez, déjalo perezoso.
Error: llamar reduce sobre un iterable posiblemente vacío sin initial, lo que lanza TypeError en tiempo de ejecución.
from functools import reduce
from operator import mul
# ❌ Wrong
def product(nums):
return reduce(mul, nums) # TypeError si nums está vacío
# ✅ Right
def product(nums):
return reduce(mul, nums, 1) # 1 es el elemento neutro de la multiplicación
El valor initial actúa como identidad matemática de la operación: 1 para multiplicación, 0 para suma, [] para concatenación de listas; elegirlo mal produce resultados incorrectos, no solo errores.
N° 112