Imagina que necesitas procesar un archivo de registro de varios gigabytes. La solución más sencilla es cargar todo el archivo en la memoria, dividirlo en líneas y comenzar a procesarlo. Pero, ¿qué pasa si no hay suficiente memoria RAM? ¿O si hay decenas de archivos de este tipo? Precisamente para estas situaciones, Python cuenta con generadores e iteradores, herramientas potentes que permiten trabajar con grandes cantidades de datos sin cargarlos completamente en la memoria.
¿Qué son los iteradores?
Un iterador es un objeto que permite recorrer los elementos de una colección de uno en uno, sin cargar toda la colección en la memoria a la vez. En Python, cualquier objeto que implemente el método __iter__() y __next__() es un iterador.
Cuando escribes for item in collection, Python llama implícitamente a iter(collection) para obtener un iterador y luego llama repetidamente a next() para obtener el siguiente elemento hasta que se lance la excepción StopIteration.
# Ejemplo de un iterador simple
class CountDown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
self.current -= 1
return self.current + 1
# Uso
counter = CountDown(5)
for num in counter:
print(num) # Mostrará: 5, 4, 3, 2, 1
Generadores: iteradores con esteroides
Los generadores son una forma simplificada de crear iteradores. En lugar de escribir una clase con los métodos __iter__() y __next__(), simplemente crea una función con la palabra clave yield. El generador guarda automáticamente su estado entre llamadas y reanuda la ejecución desde donde se detuvo.
def count_down(start):
while start > 0:
yield start
start -= 1
# Uso
for num in count_down(5):
print(num) # Mostrará: 5, 4, 3, 2, 1La ventaja es obvia: el código se ha vuelto más compacto y claro, y la funcionalidad sigue siendo la misma.
¿Por qué ahorra memoria?
La principal ventaja de los generadores es el cálculo perezoso. Los elementos se crean solo cuando se necesitan y pueden procesarse y olvidarse de inmediato. Veamos un ejemplo concreto:
# Mal: cargamos todo en la memoria
def read_large_file_bad(file_path):
with open(file_path, 'r') as file:
lines = file.readlines() # ¡Todo el archivo en la memoria!
return [line.strip().upper() for line in lines]
# Bien: procesamos línea por línea
def read_large_file_good(file_path):
with open(file_path, 'r') as file:
for line in file: # ¡file ya es un iterador!
yield line.strip().upper()
# Uso
for processed_line in read_large_file_good('huge_log.txt'):
# Procesamos una línea a la vez
process(processed_line)En el primer caso, si el archivo pesa 2 GB, su programa ocupará al menos 2 GB de RAM. En el segundo caso, solo unos pocos kilobytes, independientemente del tamaño del archivo.

Ejemplos prácticos de uso.
Procesamiento de datos de la API
A menudo, las API devuelven datos página por página. El generador puede ocultar esta complejidad:
def fetch_all_users(api_client, page_size=100):
page = 1
while True:
users = api_client.get_users(page=page, size=page_size)
if not users:
break
for user in users:
yield user
page += 1
# Uso
for user in fetch_all_users(api):
print(user['name']) # Procesamos a los usuarios de uno en unoSecuencias infinitas
Los generadores permiten crear secuencias infinitas sin riesgo de desbordamiento de memoria:
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# Puedes tomar tantos elementos como necesites
from itertools import islice
first_ten = list(islice(fibonacci(), 10))
print(first_ten) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]Cintas transportadoras de procesamiento de datos
Los generadores son ideales para crear cadenas de procesamiento:
def read_csv(file_path):
with open(file_path) as f:
for line in f:
yield line.strip().split(',')
def filter_by_status(rows, status):
for row in rows:
if row[2] == status: # estado en la tercera columna
yield row
def extract_emails(rows):
for row in rows:
yield row[1] # correo electrónico en la segunda columna
# Montaje de la cinta transportadora
rows = read_csv('users.csv')
active_users = filter_by_status(rows, 'active')
emails = extract_emails(active_users)
# Solo aquí comienza el procesamiento real
for email in emails:
send_notification(email)La belleza de este enfoque es que cada etapa del transportador es independiente y se puede probar, y la memoria se utiliza solo para un elemento.
Expresiones generadoras
Además de las funciones generadoras, Python admite expresiones generadoras, una sintaxis compacta para crear generadores:
# Comprensión de listas: crea una lista en la memoria
squares_list = [x**2 for x in range(1000000)] # Ocupa mucha memoria
# Generator expression — crea un generador
squares_gen = (x**2 for x in range(1000000)) # Casi no ocupa memoria
# Se puede utilizar en funciones
total = sum(x**2 for x in range(1000000)) # ¡Eficaz!Tenga en cuenta: se utilizan paréntesis en lugar de corchetes. Este es uno de los cambios más fáciles y efectivos que puedes hacer en tu código para ahorrar memoria.
¿Cuándo NO se deben usar generadores?
A pesar de todas las ventajas, los generadores no siempre son adecuados:
Cuando se necesita acceso múltiple a los datos: el generador solo se puede pasar una vez. Si necesita acceder a los datos varias veces, es mejor usar una lista.
Cuando la velocidad de acceso es importante: obtener un elemento por índice de la lista es instantáneo, y el generador tendrá que iterar desde el principio.
Cuando los datos se almacenan en la memoria: si tiene un conjunto de datos pequeño, los gastos generales de creación de un generador pueden ser injustificados.
Cuando necesitas saber la longitud: len() no funciona con generadores, y el cálculo de la longitud requiere una búsqueda exhaustiva.
Técnicas avanzadas.
Delegación de generadores
Con la ayuda de yield from se puede delegar la ejecución a otro generador:
def read_multiple_files(*file_paths):
for path in file_paths:
with open(path) as f:
yield from f # Delegar la lectura línea por línea
# Leemos varios archivos como un flujo
for line in read_multiple_files('log1.txt', 'log2.txt', 'log3.txt'):
process(line)Comunicación bidireccional con generadores
Los generadores no solo pueden dar valores a través de yield, sino también aceptarlos a través del método send():
def running_average():
total = 0
count = 0
average = None
while True:
value = yield average
total += value
count += 1
average = total / count
# Uso
avg = running_average()
next(avg) # Ponemos en marcha el generador
print(avg.send(10)) # 10.0
print(avg.send(20)) # 15.0
print(avg.send(30)) # 20.0Medimos la efectividad
Veamos la diferencia real en el uso de la memoria:
import sys
# Lista
numbers_list = [x for x in range(1000000)]
print(f"List: {sys.getsizeof(numbers_list) / 1024 / 1024:.2f} MB")
# Generador
numbers_gen = (x for x in range(1000000))
print(f"Generator: {sys.getsizeof(numbers_gen) / 1024:.2f} KB")En mi máquina, el resultado muestra una diferencia de miles de veces: la lista ocupa unos 8 MB y el generador menos de 0,1 KB.
Conclusión
Los generadores e iteradores no son solo azúcar sintáctico, sino una herramienta fundamental para trabajar con grandes cantidades de datos. Te permiten escribir un código más eficiente que se adapta a datos de cualquier tamaño. Comprender cuándo y cómo usar los generadores distingue a un desarrollador experimentado de un principiante.
Empiece por lo más sencillo: reemplace las comprensiones de lista por expresiones generadoras cuando no necesite acceso múltiple a los datos. Luego intenta escribir una función generadora para procesar archivos. Y gradualmente descubrirás que estás pensando en los flujos de datos, no en las colecciones en la memoria.
Anexo Kodik ofrece cursos estructurados para desarrolladores principiantes que cubren Python, JavaScript, HTML, CSS y muchas otras tecnologías.
Únete a nuestro Canal de Telegram, donde encontrarás apoyo de la comunidad, materiales adicionales y respuestas a preguntas de desarrolladores experimentados.
¡Aprende a programar de manera efectiva y con gusto!
