El módulo itertools de la biblioteca estándar ofrece herramientas para construir pipelines de iteradores eficientes en memoria. Sus funciones trabajan con iteradores y devuelven iteradores, por lo que combinan sin coste de copia de datos.
chain: concatenar iterables sin copiarlos
from itertools import chain lista1 = [1, 2, 3] lista2 = [4, 5, 6] tupla = (7, 8, 9) # Concatena sin crear una nueva lista combinado = chain(lista1, lista2, tupla) print(list(combinado)) # [1, 2, 3, 4, 5, 6, 7, 8, 9] # chain.from_iterable: aplanar un nivel de anidamiento anidado = [[1, 2], [3, 4], [5, 6]] plano = list(chain.from_iterable(anidado)) print(plano) # [1, 2, 3, 4, 5, 6]
islice: cortar generadores
from itertools import islice
def naturales():
n = 1
while True:
yield n
n += 1
# Tomar los primeros 5
print(list(islice(naturales(), 5))) # [1, 2, 3, 4, 5]
# Desde la posición 10, hasta 15
print(list(islice(naturales(), 10, 15))) # [11, 12, 13, 14, 15]
# Con paso 3
print(list(islice(naturales(), 0, 20, 3))) # [1, 4, 7, 10, 13, 16, 19]
groupby: agrupar elementos consecutivos
Importante: groupby agrupa solo los elementos consecutivos iguales. Si los datos no están ordenados por la clave, necesitas ordenarlos primero con sorted():
from itertools import groupby
registros = [
{"dept": "ventas", "nombre": "Ana"},
{"dept": "ventas", "nombre": "Luis"},
{"dept": "tech", "nombre": "María"},
{"dept": "tech", "nombre": "Pedro"},
{"dept": "ventas", "nombre": "Juan"}, # ventas no consecutivo
]
# NECESARIO: ordenar por la clave antes de agrupar
registros_ord = sorted(registros, key=lambda r: r["dept"])
for dept, grupo in groupby(registros_ord, key=lambda r: r["dept"]):
miembros = [r["nombre"] for r in grupo]
print(f"{dept}: {miembros}")
# tech: ['María', 'Pedro']
# ventas: ['Ana', 'Luis', 'Juan']
product: producto cartesiano
from itertools import product
# Equivalente a bucles for anidados
colores = ["rojo", "azul"]
tallas = ["S", "M", "L"]
for color, talla in product(colores, tallas):
print(f"{color}-{talla}", end=" ")
# rojo-S rojo-M rojo-L azul-S azul-M azul-L
# repeat: repetir el mismo iterable N veces
# Generar todas las combinaciones de 2 dígitos binarios
print(list(product([0, 1], repeat=2)))
# [(0, 0), (0, 1), (1, 0), (1, 1)]
combinations y permutations
from itertools import combinations, permutations, combinations_with_replacement
letras = ["A", "B", "C", "D"]
# combinations: subconjuntos sin repetición, sin orden
print(list(combinations(letras, 2)))
# [('A','B'), ('A','C'), ('A','D'), ('B','C'), ('B','D'), ('C','D')]
# permutations: todas las ordenaciones posibles
print(list(permutations(["X", "Y", "Z"])))
# [('X','Y','Z'), ('X','Z','Y'), ('Y','X','Z'), ...]
# combinations_with_replacement: permite repetir elementos
print(list(combinations_with_replacement([1, 2, 3], 2)))
# [(1,1), (1,2), (1,3), (2,2), (2,3), (3,3)]
Errores habituales con itertools
from itertools import groupby
# MAL: intentar consumir el iterador de grupo dos veces
for clave, grupo in groupby(datos, key=lambda x: x["dept"]):
lista = list(grupo) # OK: consume el grupo
# print(list(grupo)) # MAL: ya está consumido, devuelve []
# MAL: olvidar que los iteradores se agotan
import itertools
gen = itertools.chain([1, 2], [3, 4])
print(list(gen)) # [1, 2, 3, 4]
print(list(gen)) # [] ya estaba agotado
itertools es especialmente útil cuando trabajas con conjuntos de datos grandes que no caben en memoria: todas sus funciones producen elementos uno a uno sin cargar el conjunto completo.
