itertools en Python: chain, product, combinations, groupby e islice para iteraciones eficientes

El módulo itertools proporciona iteradores que generan sus valores de forma lazy: no cargan todos los datos en memoria, sino que producen un elemento cada vez que se les pide. Esto los hace indispensables cuando trabajas con ficheros grandes, streams de datos o combinaciones cuyo tamaño sería inmanejable si se materializasen en una lista.

chain() — concatenar múltiples iterables

from itertools import chain

# Concatenar varias listas sin copiarlas
logs_enero = ["01: inicio", "01: warning"]
logs_febrero = ["02: error", "02: ok"]
logs_marzo = ["03: inicio"]

for linea in chain(logs_enero, logs_febrero, logs_marzo):
    print(linea)

# chain.from_iterable: cuando tienes una lista de listas
listas = [range(3), range(3, 6), range(6, 9)]
for n in chain.from_iterable(listas):
    print(n, end=" ")  # 0 1 2 3 4 5 6 7 8

islice() — paginar un iterable

islice(iterable, start, stop, step) funciona como el slicing de listas pero sobre cualquier iterable, sin crear copias. Útil para paginar resultados de un generador o tomar muestras de un stream.

from itertools import islice

def leer_lineas_csv(ruta: str):
    """Generador que lee un CSV línea a línea."""
    with open(ruta) as f:
        for linea in f:
            yield linea.strip()

# Solo las líneas 10 a 19 (sin cargar el fichero entero)
generador = leer_lineas_csv("ventas.csv")
pagina = list(islice(generador, 10, 20))

# Paginación en bloques
def en_bloques(iterable, tamano: int):
    it = iter(iterable)
    while bloque := list(islice(it, tamano)):
        yield bloque

for bloque in en_bloques(range(10), 3):
    print(bloque)
# [0, 1, 2]  [3, 4, 5]  [6, 7, 8]  [9]

product() — producto cartesiano

from itertools import product

# Generar todas las combinaciones de tallas y colores
tallas = ["S", "M", "L", "XL"]
colores = ["negro", "blanco", "rojo"]

for talla, color in product(tallas, colores):
    print(f"{talla}-{color}")
# S-negro, S-blanco, S-rojo, M-negro...

# Equivalente a dos bucles for anidados:
# for t in tallas:
#     for c in colores:
#         print(f"{t}-{c}")

# product con repeat: generar todas las contraseñas de 3 dígitos
for combo in product("0123456789", repeat=3):
    clave = "".join(combo)  # "000", "001", ..., "999"

permutations() y combinations()

from itertools import permutations, combinations, combinations_with_replacement

elementos = ["A", "B", "C"]

# Permutaciones: todos los órdenes posibles
print(list(permutations(elementos)))
# [('A','B','C'), ('A','C','B'), ('B','A','C'), ('B','C','A'), ('C','A','B'), ('C','B','A')]

# Permutaciones de longitud 2
print(list(permutations(elementos, 2)))
# [('A','B'), ('A','C'), ('B','A'), ('B','C'), ('C','A'), ('C','B')]

# Combinaciones: sin importar el orden, sin repetición
print(list(combinations(elementos, 2)))
# [('A','B'), ('A','C'), ('B','C')]

# Combinaciones con repetición
print(list(combinations_with_replacement(["a", "b"], 2)))
# [('a','a'), ('a','b'), ('b','b')]

groupby() — agrupar registros consecutivos

groupby() agrupa elementos consecutivos iguales. Importante: el iterable debe estar ordenado por la clave de agrupación antes de pasarlo a groupby().

from itertools import groupby

ventas = [
    {"mes": "enero", "producto": "manzana", "unidades": 100},
    {"mes": "enero", "producto": "pera", "unidades": 50},
    {"mes": "febrero", "producto": "manzana", "unidades": 80},
    {"mes": "febrero", "producto": "uva", "unidades": 30},
    {"mes": "marzo", "producto": "pera", "unidades": 90},
]

# Ordenar por mes antes de groupby
ventas_ordenadas = sorted(ventas, key=lambda v: v["mes"])
for mes, grupo in groupby(ventas_ordenadas, key=lambda v: v["mes"]):
    items = list(grupo)
    total = sum(v["unidades"] for v in items)
    print(f"{mes}: {total} unidades")
# enero: 150  febrero: 110  marzo: 90

takewhile() y dropwhile()

from itertools import takewhile, dropwhile

temperaturas = [18, 19, 21, 25, 27, 22, 18, 15]

# Tomar mientras la temperatura sube (hasta el primer descenso)
subiendo = list(takewhile(lambda t: t <= 25, temperaturas))
print(subiendo)  # [18, 19, 21, 25]

# Descartar mientras hay descenso inicial (log que empieza con INFO)
logs = ["INFO: arranque", "INFO: carga", "ERROR: fallo", "INFO: reintento"]
errores_en_adelante = list(dropwhile(lambda l: l.startswith("INFO"), logs))
print(errores_en_adelante)  # ['ERROR: fallo', 'INFO: reintento']

count(), cycle() y repeat()

from itertools import count, cycle, repeat, islice

# count: contador infinito
ids = islice(count(start=1000, step=5), 5)
print(list(ids))  # [1000, 1005, 1010, 1015, 1020]

# cycle: repite una secuencia infinitamente
colores = islice(cycle(["rojo", "verde", "azul"]), 7)
print(list(colores))  # ['rojo', 'verde', 'azul', 'rojo', 'verde', 'azul', 'rojo']

# repeat: repite un valor N veces
print(list(repeat("python", 3)))  # ['python', 'python', 'python']
# Útil con map:
print(list(map(pow, range(5), repeat(2))))  # [0, 1, 4, 9, 16]

La ventaja real de itertools no es la comodidad sino la eficiencia: todo es lazy. Un product() de cuatro listas de 100 elementos genera 100 millones de combinaciones una a una sin ocupar apenas memoria. Si solo necesitas procesar las primeras 1000, un islice(product(...), 1000) es infinitamente más eficiente que materializar la lista entera.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP