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.
