Python itertools: Guía Completa de Bloques de Construcción de Iteradores
Updated on
Escribir bucles es algo natural para todo desarrollador Python. Pero al observar más de cerca la mayoría de las bases de código, encontrarás los mismos patrones apareciendo una y otra vez: aplanar listas anidadas, generar combinaciones, agrupar datos ordenados, dividir iteradores, acumular totales parciales. Cada uno se resuelve típicamente con un bucle for escrito a mano, variables temporales y malabarismo con índices. El código funciona, pero es verboso, propenso a errores y lento cuando los conjuntos de datos crecen.
Estos problemas se acumulan. Un bucle anidado sobre dos listas produce un producto cartesiano, pero escribirlo manualmente oscurece la intención. Generar todas las combinaciones de longitud 3 de una colección requiere una gestión cuidadosa de índices. Agrupar elementos consecutivos por una clave necesita una variable de estado que es fácil de equivocar. Cada uno de estos patrones ya ha sido resuelto, probado y optimizado en el módulo itertools de Python -- un conjunto de herramientas de la biblioteca estándar que convierte construcciones de bucles multilínea en llamadas a funciones individuales y legibles que producen iteradores eficientes en memoria.
Esta guía cubre cada función principal de itertools con ejemplos prácticos y ejecutables. Al final, tendrás una referencia para reemplazar patrones de bucles verbosos con pipelines de iteradores limpias y eficientes.
Importar itertools
El módulo itertools es parte de la biblioteca estándar de Python. No se necesita instalación.
import itertools
# O importar funciones específicas
from itertools import chain, combinations, groupby, isliceIteradores Infinitos
Estas funciones producen iteradores que nunca terminan por sí solos. Debes usar islice(), takewhile() u otro mecanismo de parada para evitar bucles infinitos.
count() -- Secuencias Aritméticas
count(start, step) genera valores uniformemente espaciados comenzando en start, incrementando en step (por defecto 1).
from itertools import count, islice
# Contar desde 10, paso 2
counter = count(10, 2)
print(list(islice(counter, 6)))
# [10, 12, 14, 16, 18, 20]
# Secuencias de punto flotante
floats = count(0.0, 0.25)
print(list(islice(floats, 5)))
# [0.0, 0.25, 0.5, 0.75, 1.0]
# Etiquetar filas con un índice
names = ["Alice", "Bob", "Charlie"]
for idx, name in zip(count(1), names):
print(f"{idx}. {name}")
# 1. Alice
# 2. Bob
# 3. Charliecycle() -- Repetir una Secuencia Indefinidamente
cycle(iterable) guarda una copia del iterable y devuelve elementos de él repetidamente.
from itertools import cycle, islice
colors = cycle(["red", "green", "blue"])
print(list(islice(colors, 7)))
# ['red', 'green', 'blue', 'red', 'green', 'blue', 'red']
# Asignación round-robin
tasks = ["task_a", "task_b", "task_c", "task_d", "task_e"]
workers = cycle(["Worker1", "Worker2", "Worker3"])
assignments = {task: worker for task, worker in zip(tasks, workers)}
print(assignments)
# {'task_a': 'Worker1', 'task_b': 'Worker2', 'task_c': 'Worker3',
# 'task_d': 'Worker1', 'task_e': 'Worker2'}repeat() -- Devolver el Mismo Valor
repeat(value, times) devuelve value ya sea infinitamente o un número fijo de veces.
from itertools import repeat
# Repeticiones fijas
print(list(repeat("hello", 3)))
# ['hello', 'hello', 'hello']
# Usar repeat como argumento constante en map()
import operator
bases = [2, 3, 4, 5]
squared = list(map(operator.pow, bases, repeat(2)))
print(squared)
# [4, 9, 16, 25]Iteradores Finitos: División y Filtrado
Estas funciones consumen uno o más iterables de entrada y producen una salida finita.
chain() -- Concatenar Múltiples Iterables
chain(*iterables) devuelve elementos del primer iterable hasta que se agota, luego del siguiente, y así sucesivamente. chain.from_iterable() acepta un único iterable de iterables.
from itertools import chain
# Encadenar múltiples listas
a = [1, 2, 3]
b = [4, 5, 6]
c = [7, 8, 9]
print(list(chain(a, b, c)))
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Aplanar un nivel de anidamiento
nested = [[1, 2], [3, 4], [5, 6]]
print(list(chain.from_iterable(nested)))
# [1, 2, 3, 4, 5, 6]
# Combinar un generador con una lista
def evens():
yield 2; yield 4; yield 6
print(list(chain(evens(), [8, 10])))
# [2, 4, 6, 8, 10]compress() -- Filtrar por Selectores
compress(data, selectors) devuelve elementos de data donde el elemento correspondiente en selectors es verdadero.
from itertools import compress
data = ["A", "B", "C", "D", "E"]
selectors = [1, 0, 1, 0, 1]
print(list(compress(data, selectors)))
# ['A', 'C', 'E']
# Usar con condiciones booleanas
values = [10, 25, 3, 42, 7, 18]
mask = [v > 15 for v in values]
print(list(compress(values, mask)))
# [25, 42, 18]islice() -- Dividir Cualquier Iterador
islice(iterable, stop) o islice(iterable, start, stop, step) funciona como la notación de slice pero en cualquier iterador, sin construir una lista primero.
from itertools import islice, count
# Primeros 5 elementos de un contador infinito
print(list(islice(count(100), 5)))
# [100, 101, 102, 103, 104]
# Elementos del 3 al 8
print(list(islice(range(20), 3, 9)))
# [3, 4, 5, 6, 7, 8]
# Cada tercer elemento de los primeros 15
print(list(islice(range(100), 0, 15, 3)))
# [0, 3, 6, 9, 12]
# Leer las primeras 5 líneas de un iterador de archivo
# lines = list(islice(open("data.txt"), 5))takewhile() y dropwhile() -- División Condicional
takewhile(predicate, iterable) devuelve elementos mientras el predicado sea verdadero, luego se detiene. dropwhile(predicate, iterable) omite elementos mientras el predicado sea verdadero, luego devuelve el resto.
from itertools import takewhile, dropwhile
data = [1, 3, 5, 7, 2, 4, 6, 8]
# Tomar elementos mientras sean menores que 6
print(list(takewhile(lambda x: x < 6, data)))
# [1, 3, 5]
# Descartar elementos mientras sean menores que 6
print(list(dropwhile(lambda x: x < 6, data)))
# [7, 2, 4, 6, 8]Ten en cuenta que takewhile se detiene en el primer False; no se reanuda si elementos posteriores satisfacen el predicado nuevamente.
starmap() -- Aplicar una Función a Argumentos Pre-agrupados
starmap(function, iterable) aplica una función usando tuplas de argumentos del iterable. Esto es equivalente a function(*args) para cada args en el iterable.
from itertools import starmap
import operator
pairs = [(2, 5), (3, 4), (10, 3)]
print(list(starmap(operator.mul, pairs)))
# [10, 12, 30]
# Calcular hipotenusa para múltiples triángulos
import math
triangles = [(3, 4), (5, 12), (8, 15)]
print(list(starmap(math.hypot, triangles)))
# [5.0, 13.0, 17.0]filterfalse() -- Filtro Inverso
filterfalse(predicate, iterable) devuelve elementos para los cuales el predicado retorna False. Es el complemento de la función integrada filter().
from itertools import filterfalse
numbers = range(10)
# Mantener números impares (donde "es_par" es falso)
odd = list(filterfalse(lambda x: x % 2 == 0, numbers))
print(odd)
# [1, 3, 5, 7, 9]Iteradores Combinatorios
Estas funciones producen todas las disposiciones posibles de los elementos de entrada. Son fundamentales para búsqueda por fuerza bruta, pruebas y cálculos matemáticos.
product() -- Producto Cartesiano
itertools.product(*iterables, repeat=1) reemplaza bucles for anidados sobre múltiples secuencias.
from itertools import product
# Dos listas
colors = ["red", "blue"]
sizes = ["S", "M", "L"]
print(list(product(colors, sizes)))
# [('red', 'S'), ('red', 'M'), ('red', 'L'),
# ('blue', 'S'), ('blue', 'M'), ('blue', 'L')]
# Equivalente a:
# [(c, s) for c in colors for s in sizes]
# Lanzamiento de dados: dos dados de seis caras
dice = range(1, 7)
all_rolls = list(product(dice, repeat=2))
print(f"Combinaciones totales: {len(all_rolls)}")
# Combinaciones totales: 36
print(all_rolls[:5])
# [(1, 1), (1, 2), (1, 3), (1, 4), (1, 5)]permutations() -- Todas las Ordenaciones
permutations(iterable, r) genera todas las ordenaciones posibles de longitud r a partir de la entrada. El orden importa, por lo que tanto (A, B) como (B, A) se incluyen.
from itertools import permutations
# Todas las disposiciones de 2 letras de 'ABC'
print(list(permutations("ABC", 2)))
# [('A', 'B'), ('A', 'C'), ('B', 'A'),
# ('B', 'C'), ('C', 'A'), ('C', 'B')]
# Permutaciones completas (todos los elementos)
print(list(permutations([1, 2, 3])))
# [(1, 2, 3), (1, 3, 2), (2, 1, 3),
# (2, 3, 1), (3, 1, 2), (3, 2, 1)]
# Número de permutaciones: n! / (n-r)!
import math
n, r = 5, 3
print(f"P({n},{r}) = {math.perm(n, r)}")
# P(5,3) = 60combinations() -- Subconjuntos Únicos
combinations(iterable, r) genera todos los subconjuntos únicos de longitud r. El orden no importa, por lo que (A, B) se incluye pero (B, A) no.
from itertools import combinations
# Todas las combinaciones de 2 elementos de [1, 2, 3, 4]
print(list(combinations([1, 2, 3, 4], 2)))
# [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
# Elegir 3 ingredientes de un menú
toppings = ["cheese", "pepperoni", "mushrooms", "onions", "peppers"]
combos = list(combinations(toppings, 3))
print(f"Número de combinaciones de 3 ingredientes: {len(combos)}")
# Número de combinaciones de 3 ingredientes: 10
for c in combos[:3]:
print(c)
# ('cheese', 'pepperoni', 'mushrooms')
# ('cheese', 'pepperoni', 'onions')
# ('cheese', 'pepperoni', 'peppers')combinations_with_replacement() -- Subconjuntos con Repetición
Los elementos pueden seleccionarse más de una vez.
from itertools import combinations_with_replacement
# Denominaciones de monedas: elegir 3 monedas
coins = [1, 5, 10, 25]
selections = list(combinations_with_replacement(coins, 3))
print(f"Selecciones totales: {len(selections)}")
# Selecciones totales: 35
print(selections[:5])
# [(1, 1, 1), (1, 1, 5), (1, 1, 10), (1, 1, 25), (1, 5, 5)]Agrupar Datos con groupby()
itertools.groupby(iterable, key=None) agrupa elementos consecutivos que comparten la misma clave. La entrada debe estar ordenada por la clave primero, de lo contrario claves idénticas en posiciones no adyacentes formarán grupos separados.
from itertools import groupby
# Agrupar elementos iguales consecutivos
data = "AAABBBCCAAB"
for key, group in groupby(data):
print(f"{key}: {list(group)}")
# A: ['A', 'A', 'A']
# B: ['B', 'B', 'B']
# C: ['C', 'C']
# A: ['A', 'A']
# B: ['B']Ejemplo Práctico: Agrupar Registros por Categoría
from itertools import groupby
from operator import itemgetter
sales = [
{"product": "Widget", "category": "Hardware", "revenue": 150},
{"product": "Gadget", "category": "Hardware", "revenue": 300},
{"product": "App Pro", "category": "Software", "revenue": 500},
{"product": "Cloud X", "category": "Software", "revenue": 200},
{"product": "Cable", "category": "Hardware", "revenue": 50},
]
# Ordenar por categoría primero -- groupby requiere entrada ordenada
sales.sort(key=itemgetter("category"))
for category, items in groupby(sales, key=itemgetter("category")):
item_list = list(items)
total = sum(item["revenue"] for item in item_list)
print(f"{category}: {len(item_list)} productos, ${total} ingresos")
# Hardware: 3 productos, $500 ingresos
# Software: 2 productos, $700 ingresosAgrupar Números por Propiedad
from itertools import groupby
numbers = sorted(range(1, 16), key=lambda x: x % 3)
for remainder, group in groupby(numbers, key=lambda x: x % 3):
print(f"Resto {remainder}: {list(group)}")
# Resto 0: [3, 6, 9, 12, 15]
# Resto 1: [1, 4, 7, 10, 13]
# Resto 2: [2, 5, 8, 11, 14]Totales Acumulados con accumulate()
itertools.accumulate(iterable, func=operator.add, initial=None) produce resultados acumulados (parciales). Por defecto calcula una suma acumulada, pero se puede suministrar cualquier función binaria.
from itertools import accumulate
import operator
# Suma acumulada
data = [1, 2, 3, 4, 5]
print(list(accumulate(data)))
# [1, 3, 6, 10, 15]
# Producto acumulado
print(list(accumulate(data, operator.mul)))
# [1, 2, 6, 24, 120]
# Máximo acumulado
temps = [72, 68, 75, 71, 78, 74, 80]
print(list(accumulate(temps, max)))
# [72, 72, 75, 75, 78, 78, 80]
# Con un valor inicial
print(list(accumulate(data, operator.add, initial=100)))
# [100, 101, 103, 106, 110, 115]Práctico: Saldo Acumulado
from itertools import accumulate
transactions = [1000, -200, -150, 500, -300, -100, 250]
balances = list(accumulate(transactions))
print("Transacciones:", transactions)
print("Saldos: ", balances)
# Transacciones: [1000, -200, -150, 500, -300, -100, 250]
# Saldos: [1000, 800, 650, 1150, 850, 750, 1000]Referencia Completa de Funciones
| Función | Categoría | Descripción | Salida de Ejemplo |
|---|---|---|---|
count(start, step) | Infinito | Secuencia aritmética | 10, 12, 14, 16, ... |
cycle(iterable) | Infinito | Repite iterable sin fin | A, B, C, A, B, C, ... |
repeat(val, n) | Infinito | Mismo valor n veces (o infinito) | 5, 5, 5, 5 |
chain(*iterables) | Finito | Concatenar iterables | [1,2] + [3,4] -> 1,2,3,4 |
compress(data, sel) | Finito | Filtrar por selectores booleanos | ABCDE, 10101 -> A,C,E |
islice(iter, start, stop, step) | Finito | Dividir cualquier iterador | Como list[start:stop:step] |
takewhile(pred, iter) | Finito | Devolver mientras predicado sea verdadero | Se detiene en primer falso |
dropwhile(pred, iter) | Finito | Omitir mientras predicado sea verdadero | Comienza en primer falso |
filterfalse(pred, iter) | Finito | Devolver donde predicado es falso | Inverso de filter() |
starmap(func, iter) | Finito | Aplicar función a tuplas de argumentos | func(*args) para cada uno |
accumulate(iter, func) | Finito | Totales acumulados | [1,3,6,10,15] |
groupby(iter, key) | Finito | Agrupar consecutivos por clave | Requiere entrada ordenada |
product(*iters) | Combinatorio | Producto cartesiano | Reemplaza bucles anidados |
permutations(iter, r) | Combinatorio | Todas las ordenaciones de longitud r | El orden importa |
combinations(iter, r) | Combinatorio | Todos los subconjuntos de longitud r | El orden no importa |
combinations_with_replacement(iter, r) | Combinatorio | Subconjuntos con repetición | Los elementos pueden repetirse |
Rendimiento: itertools vs Bucles Manuales
Las funciones de itertools están implementadas en C, lo que las hace significativamente más rápidas y eficientes en memoria que los bucles Python equivalentes. Producen iteradores (evaluación perezosa), lo que significa que devuelven un elemento a la vez en lugar de construir listas enteras en memoria.
import time
from itertools import chain
# Benchmark: aplanar una lista de 1000 sublistas, cada una con 1000 elementos
nested = [list(range(1000)) for _ in range(1000)]
# Enfoque manual
start = time.perf_counter()
result_manual = []
for sublist in nested:
result_manual.extend(sublist)
manual_time = time.perf_counter() - start
# Enfoque itertools
start = time.perf_counter()
result_itertools = list(chain.from_iterable(nested))
itertools_time = time.perf_counter() - start
print(f"Extend manual: {manual_time:.4f}s")
print(f"chain.from_iterable: {itertools_time:.4f}s")
print(f"Aceleración: {manual_time / itertools_time:.2f}x")
# Salida típica:
# Extend manual: 0.0180s
# chain.from_iterable: 0.0120s
# Aceleración: 1.50xEficiencia de Memoria
import sys
from itertools import islice, count
# Una lista de 1 millón de enteros
big_list = list(range(1_000_000))
print(f"Memoria de lista: {sys.getsizeof(big_list):,} bytes")
# Memoria de lista: 8,000,056 bytes
# Un iterador sobre el mismo rango (memoria despreciable)
big_iter = islice(count(), 1_000_000)
print(f"Memoria de iterador: {sys.getsizeof(big_iter)} bytes")
# Memoria de iterador: 72 bytesLos iteradores procesan un elemento a la vez. Al encadenar múltiples transformaciones, no se crean listas intermedias.
Recetas Comunes
La documentación de itertools incluye varias "recetas" -- patrones comunes construidos a partir de las primitivas del módulo. Aquí están las más útiles.
Aplanar Listas Anidadas
from itertools import chain
def flatten(nested_list):
"""Aplanar un nivel de anidamiento."""
return list(chain.from_iterable(nested_list))
data = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
print(flatten(data))
# [1, 2, 3, 4, 5, 6, 7, 8, 9]Ventana Deslizante
from itertools import islice
from collections import deque
def sliding_window(iterable, n):
"""Devolver una ventana deslizante de tamaño n sobre el iterable."""
iterator = iter(iterable)
window = deque(islice(iterator, n), maxlen=n)
if len(window) == n:
yield tuple(window)
for item in iterator:
window.append(item)
yield tuple(window)
data = [1, 2, 3, 4, 5, 6, 7]
for window in sliding_window(data, 3):
print(window)
# (1, 2, 3)
# (2, 3, 4)
# (3, 4, 5)
# (4, 5, 6)
# (5, 6, 7)Nota: Python 3.12+ incluye itertools.pairwise() para ventanas de tamaño 2, e itertools.batched() para bloques sin superposición.
Dividir un Iterable en Bloques
from itertools import islice
def chunked(iterable, size):
"""Dividir un iterable en bloques de tamaño fijo."""
iterator = iter(iterable)
while True:
chunk = list(islice(iterator, size))
if not chunk:
break
yield chunk
data = list(range(1, 12))
for chunk in chunked(data, 3):
print(chunk)
# [1, 2, 3]
# [4, 5, 6]
# [7, 8, 9]
# [10, 11]En Python 3.12+, usa itertools.batched(iterable, n) para el mismo resultado con una función integrada.
Iteración por Pares
from itertools import pairwise # Python 3.10+
data = [10, 20, 30, 40, 50]
for a, b in pairwise(data):
print(f"{a} -> {b}, diferencia = {b - a}")
# 10 -> 20, diferencia = 10
# 20 -> 30, diferencia = 10
# 30 -> 40, diferencia = 10
# 40 -> 50, diferencia = 10Round-Robin desde Múltiples Iterables
from itertools import cycle, islice
def roundrobin(*iterables):
"""Devolver elementos de cada iterable por turnos."""
iterators = [iter(it) for it in iterables]
active = len(iterators)
nexts = cycle(iter(it).__next__ for it in iterables)
# Enfoque más simple:
pending = len(iterables)
iters = cycle(iter(it) for it in iterables)
# Usar la receta de la documentación:
result = []
iterators = list(map(iter, iterables))
while iterators:
next_iterators = []
for it in iterators:
try:
result.append(next(it))
next_iterators.append(it)
except StopIteration:
pass
iterators = next_iterators
return result
print(roundrobin("ABC", "D", "EF"))
# ['A', 'D', 'E', 'B', 'F', 'C']Elementos Únicos Preservando el Orden
from itertools import filterfalse
def unique_everseen(iterable):
"""Devolver elementos únicos, preservando el orden de primera aparición."""
seen = set()
for element in filterfalse(seen.__contains__, iterable):
seen.add(element)
yield element
data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
print(list(unique_everseen(data)))
# [3, 1, 4, 5, 9, 2, 6]Encadenar Funciones itertools
El verdadero poder de itertools surge cuando compones múltiples funciones en un pipeline. Como cada función devuelve un iterador, no se desperdicia memoria entre etapas.
from itertools import chain, compress, accumulate, islice
# Pipeline: aplanar -> filtrar -> suma acumulada -> tomar los primeros 5
nested = [[10, 20, 30], [5, 15, 25], [40, 50]]
mask = [True, False, True, True, False, True, True, False]
flat = chain.from_iterable(nested) # 10, 20, 30, 5, 15, 25, 40, 50
filtered = compress(flat, mask) # 10, 30, 5, 25, 40
running = accumulate(filtered) # 10, 40, 45, 70, 110
first_four = list(islice(running, 4))
print(first_four)
# [10, 40, 45, 70]No se creó ninguna lista intermedia en ningún paso. Cada valor fluye a través del pipeline uno a la vez.
Experimentar con itertools en Jupyter
Los pipelines de iteradores pueden ser difíciles de depurar porque los iteradores se consumen en la primera pasada. Un entorno de notebook interactivo facilita inspeccionar resultados intermedios, probar casos límite y visualizar cómo fluyen los datos a través de cada etapa. RunCell (opens in a new tab) proporciona un entorno Jupyter con IA que es ideal para este tipo de exploración -- puedes recorrer paso a paso las salidas de iteradores, obtener explicaciones asistidas por IA cuando un pipeline se comporta de manera inesperada, y prototipar recetas rápidamente antes de llevarlas a código de producción.
FAQ
¿Qué es itertools en Python?
itertools es un módulo de la biblioteca estándar que proporciona una colección de funciones rápidas y eficientes en memoria para crear y trabajar con iteradores. Incluye herramientas para secuencias infinitas (count, cycle, repeat), patrones de iteración finitos (chain, islice, groupby) y combinatoria (product, permutations, combinations). Todas las funciones devuelven iteradores, lo que significa que generan valores de forma perezosa sin construir listas completas en memoria.
¿Cuál es la diferencia entre itertools.combinations e itertools.permutations?
combinations(iterable, r) genera todos los subconjuntos únicos de longitud r donde el orden no importa -- (A, B) y (B, A) se consideran iguales y solo se devuelve (A, B). permutations(iterable, r) genera todas las ordenaciones de longitud r donde el orden importa -- se devuelven tanto (A, B) como (B, A). Para n elementos eligiendo r, combinations produce n! / (r!(n-r)!) resultados mientras que permutations produce n! / (n-r)! resultados.
¿Cómo aplano una lista anidada con itertools?
Usa itertools.chain.from_iterable() para aplanar un nivel de anidamiento. Por ejemplo, list(chain.from_iterable([[1,2],[3,4],[5,6]])) devuelve [1, 2, 3, 4, 5, 6]. Para estructuras profundamente anidadas, necesitas un enfoque recursivo ya que chain.from_iterable solo elimina un nivel.
¿Por qué itertools.groupby requiere entrada ordenada?
groupby() agrupa elementos consecutivos que comparten la misma clave. No recorre todo el iterable para encontrar todos los elementos coincidentes. Si tus datos tienen [A, A, B, A], groupby produce tres grupos: A, B, A. Para obtener un solo grupo por cada clave, ordena los datos por la función clave antes de pasarlos a groupby.
¿Es itertools más rápido que los bucles normales de Python?
Sí. Las funciones de itertools están implementadas en C como parte de la biblioteca estándar de CPython, lo que las hace más rápidas que los bucles Python equivalentes escritos a mano. También usan evaluación perezosa (produciendo un elemento a la vez), lo que reduce el consumo de memoria. Para conjuntos de datos grandes, la combinación de velocidad C y cero listas intermedias puede proporcionar mejoras significativas de rendimiento.
Conclusión
El módulo itertools de Python reemplaza docenas de patrones de bucles comunes con llamadas a funciones individuales que son más rápidas, más legibles y más eficientes en memoria. Los iteradores infinitos (count, cycle, repeat) manejan secuencias que no tienen un final natural. Los iteradores finitos (chain, islice, groupby, accumulate, compress, takewhile, dropwhile) cubren filtrado, división y agregación. Las funciones combinatorias (product, permutations, combinations, combinations_with_replacement) eliminan bucles anidados para búsqueda exhaustiva.
El principio clave es la composición. Como cada función de itertools devuelve un iterador, puedes canalizar la salida de una directamente a otra sin almacenamiento intermedio. Esto permite procesar conjuntos de datos que no caben en memoria, un elemento a la vez, usando código limpio y declarativo.