itertools es un módulo de la biblioteca estándar que expone bloques de iteración combinables. Su interés no está en sustituir un bucle for por una función de nombre críptico, sino en manipular flujos de datos sin cargarlos nunca por completo en memoria. Cada función devuelve un iterador perezoso: nada se calcula hasta que se consume el resultado. Eso es lo que permite encadenar transformaciones sobre millones de elementos con una huella de memoria constante.
Estas son las funciones que uso realmente, agrupadas por uso, con las trampas que hacen perder tiempo.
Iteradores infinitos: count, cycle, repeat
Estas tres funciones producen flujos sin fin. Solo son útiles asociadas a una condición de parada, de lo contrario el bucle nunca termina.
count(start, step) genera una sucesión aritmética infinita. Práctico para numerar sin gestionar un contador manual.
from itertools import count, islice
for i in count(10, 2):
if i > 20:
break
print(i) # 10, 12, 14, 16, 18, 20
cycle(iterable) repite un iterable indefinidamente. Típico para alternar entre recursos (round-robin sobre servidores, colores, workers).
from itertools import cycle
colores = cycle(['rojo', 'verde', 'azul'])
for _, color in zip(range(5), colores):
print(color) # rojo, verde, azul, rojo, verde
repeat(elem, times) repite un valor. Sin times, es infinito. Su utilidad principal es proporcionar un argumento constante a map o starmap.
from itertools import repeat
list(repeat(7, 3)) # [7, 7, 7]
list(map(pow, range(5), repeat(2))) # [0, 1, 4, 9, 16] — cada base al cuadrado
La trampa. count(), cycle() y repeat() sin times no se detienen nunca. Hay que acotarlos siempre con islice, takewhile, un zip sobre una secuencia finita, o un break. Un list(count()) bloquea el proceso.
Recortar y filtrar un flujo
islice(iterable, start, stop, step) aplica un recorte tipo slice sobre cualquier iterador, incluso infinito, sin materializarlo en una lista. Es la herramienta de referencia para acotar un flujo.
from itertools import islice, count
list(islice(count(), 5)) # [0, 1, 2, 3, 4]
list(islice(count(), 2, 8, 2)) # [2, 4, 6]
takewhile(pred, iterable) devuelve los elementos mientras el predicado se cumple, y luego se detiene en el primer fallo. dropwhile hace lo contrario: salta los elementos mientras el predicado se cumple, y luego devuelve todo el resto sin reevaluar.
from itertools import takewhile, dropwhile
data = [2, 3, 8, 1, 9, 4]
list(takewhile(lambda x: x < 5, data)) # [2, 3] — se detiene en el 8
list(dropwhile(lambda x: x < 5, data)) # [8, 1, 9, 4] — salta 2 y 3, conserva el resto
filterfalse(pred, iterable) es el complemento de filter: conserva los elementos para los que el predicado es falso. Más legible que filter(lambda x: not pred(x), ...).
from itertools import filterfalse
list(filterfalse(lambda x: x % 2, range(10))) # [0, 2, 4, 6, 8] — los pares
compress(data, selectors) filtra data según un segundo iterable de booleanos. Útil cuando la máscara de selección se calcula en otro sitio, por separado de los datos.
from itertools import compress
nombres = ['Alice', 'Bob', 'Carol', 'Dan']
activos = [True, False, True, False]
list(compress(nombres, activos)) # ['Alice', 'Carol']
Combinar y transformar
chain(*iterables) encadena varios iterables en una sola secuencia, sin crear una lista intermedia. Su variante chain.from_iterable aplana un iterable de iterables, ideal para flujos perezosos.
from itertools import chain
list(chain([1, 2], [3, 4], [5])) # [1, 2, 3, 4, 5]
list(chain.from_iterable([[1, 2], [3, 4]])) # [1, 2, 3, 4]
accumulate(iterable, func, initial) produce resultados acumulados. Por defecto es una suma acumulada, pero sirve cualquier función binaria (max, operator.mul, etc.). El parámetro initial (Python 3.8+) fija un valor de partida.
from itertools import accumulate
import operator
list(accumulate([1, 2, 3, 4])) # [1, 3, 6, 10] — suma acumulada
list(accumulate([1, 2, 3, 4], operator.mul)) # [1, 2, 6, 24] — producto acumulado
list(accumulate([3, 1, 4, 1, 5], max)) # [3, 3, 4, 4, 5] — máximo corriente
starmap(func, iterable) aplica una función a argumentos ya agrupados en tuplas. Es map cuando los argumentos vienen preempaquetados: starmap(f, [(a, b)]) llama a f(a, b).
from itertools import starmap
puntos = [(3, 4), (6, 8), (5, 12)]
list(starmap(lambda x, y: (x**2 + y**2)**0.5, puntos)) # [5.0, 10.0, 13.0]
pairwise(iterable) (Python 3.10+) devuelve los elementos por pares consecutivos solapados. Perfecto para calcular diferencias o comparar cada elemento con el siguiente.
from itertools import pairwise
list(pairwise([1, 2, 3, 4])) # [(1, 2), (2, 3), (3, 4)]
temps = [10, 13, 12, 18]
[b - a for a, b in pairwise(temps)] # [3, -1, 6] — variaciones sucesivas
zip_longest(*iterables, fillvalue) fusiona varios iterables como zip, pero se alinea con el más largo rellenando los huecos con fillvalue en lugar de detenerse en el más corto.
from itertools import zip_longest
list(zip_longest([1, 2, 3], ['a', 'b'], fillvalue='?'))
# [(1, 'a'), (2, 'b'), (3, '?')]
groupby: la trampa de ordenar antes
groupby(iterable, key) agrupa los elementos consecutivos que comparten la misma clave. La palabra importante es consecutivos: groupby solo agrupa rachas adyacentes, no ordena. Sobre una entrada no ordenada por la clave, se obtienen grupos fragmentados.
from itertools import groupby
data = [('FR', 'Paris'), ('US', 'NYC'), ('FR', 'Lyon')]
# Mal: no ordenado por país → 'FR' aparece en dos grupos separados
for pais, grupo in groupby(data, key=lambda x: x[0]):
print(pais, [v for _, v in grupo])
# FR ['Paris']
# US ['NYC']
# FR ['Lyon']
# Bien: ordenar primero por la misma clave
data.sort(key=lambda x: x[0])
for pais, grupo in groupby(data, key=lambda x: x[0]):
print(pais, [v for _, v in grupo])
# FR ['Paris', 'Lyon']
# US ['NYC']
Es el error más frecuente con esta función: olvidar el sort con la misma clave que el groupby. Otra sutileza, el objeto grupo es un iterador compartido: si se pasa al grupo siguiente sin haber consumido el anterior, su contenido se pierde. La función key se presta bien a operator.itemgetter, más rápida y legible que una lambda.
tee: duplicar un iterador, con prudencia
tee(iterable, n) devuelve n iteradores independientes a partir de uno solo. No copia los datos: los iteradores comparten un buffer interno que retiene todo lo que el más lento aún no ha consumido.
from itertools import tee
it = iter([1, 2, 3, 4])
a, b = tee(it, 2)
list(a) # [1, 2, 3, 4]
list(b) # [1, 2, 3, 4]
Dos trampas reales. Primero, no volver a tocar el iterable de origen tras tee: seguir consumiéndolo desincroniza las copias. Segundo, si uno de los iteradores se adelanta mucho al otro, el buffer interno crece para retener los elementos pendientes. Duplicar un flujo y luego consumir por completo la primera copia antes que la segunda equivale a guardarlo todo en memoria, lo que anula la ventaja de la pereza. tee es eficiente solo si las copias avanzan a un ritmo parecido.
Combinatoria: product, permutations, combinations
Estas funciones generan disposiciones. Siguen siendo perezosas, pero cuidado: el número de resultados explota rápido (factorial o exponencial).
product(*iterables, repeat) calcula el producto cartesiano. Sustituye los bucles for anidados.
from itertools import product
list(product([1, 2], ['a', 'b'])) # [(1,'a'), (1,'b'), (2,'a'), (2,'b')]
list(product([0, 1], repeat=3)) # todas las combinaciones binarias sobre 3 bits
permutations(iterable, r) genera todas las disposiciones ordenadas de longitud r (el orden importa). combinations(iterable, r) genera los subconjuntos no ordenados de longitud r (el orden no importa). combinations_with_replacement permite además repetir un mismo elemento.
from itertools import permutations, combinations, combinations_with_replacement
list(permutations('ABC', 2)) # AB AC BA BC CA CB — 6 disposiciones
list(combinations('ABC', 2)) # AB AC BC — 3 combinaciones
list(combinations_with_replacement('ABC', 2)) # AA AB AC BB BC CC — 6 con repetición
La trampa. permutations(range(10)) produce 3 628 800 tuplas. Nunca envolver estas funciones en un list() sin acotar el tamaño de la entrada, so pena de saturar la memoria. Iterar directamente con un break o un islice cuando solo se busca una muestra.
Resumen
| Función | Papel | A recordar |
|---|---|---|
count / cycle / repeat | flujos infinitos | acotar con islice o takewhile |
islice | slice perezoso | funciona sobre un iterador infinito |
takewhile / dropwhile | cortar según un predicado | se detiene / salta en el primer cambio |
filterfalse / compress | filtrado | complemento de filter / máscara externa |
chain | concatenar iterables | from_iterable para aplanar |
accumulate | resultados acumulados | func e initial personalizables |
starmap | map sobre tuplas de argumentos | f((a, b)) → f(a, b) |
pairwise | pares consecutivos | Python 3.10+, ideal para deltas |
zip_longest | zip sin truncar | rellena con fillvalue |
groupby | agrupar rachas | ordenar antes por la clave |
tee | duplicar un iterador | costoso en memoria si se desincroniza |
product / permutations / combinations | combinatoria | la salida explota, acotar la entrada |
itertools no aporta nada imposible de escribir a mano. Aporta primitivas en C, probadas, que se componen entre sí y preservan la pereza de principio a fin. Donde una comprensión de lista materializa todo, un pipeline itertools procesa un elemento cada vez. En grandes volúmenes, esa es la diferencia entre un script que cabe en memoria y uno que la satura. Para las estructuras de datos complementarias, ver el módulo collections.
