{}const=>[]async()letfn</>var
DesarrolloPython

Generadores e iteradores en Python: ahorramos memoria y aumentamos el rendimiento

En este artículo explicamos cómo utilizar generadores e iteradores en Python para trabajar eficazmente con grandes volúmenes de datos. Ejemplos prácticos de procesamiento de archivos, API y pipelines de datos. Aprende a escribir código que se adapte a cualquier cantidad de información sin desbordar la memoria.

К

Kodik

Autor

6 min de lectura

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

🔥 100.000+ estudiantes ya están con nosotros

¿Cansado de leer teoría?
¡Hora de programar!

Kodik — una app donde aprendes a programar con práctica. Mentor IA, lecciones interactivas, proyectos reales.

🤖 IA 24/7
🎓 Certificados
💰 Gratis
🚀 Empezar
Se unieron hoy

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, 1

La 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 uno

Secuencias 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.0

Medimos 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!

🎯Deja de postergar

¿Te gustó el artículo?
¡Hora de practicar!

En Kodik no solo lees — escribes código de inmediato. Teoría + práctica = habilidades reales.

Práctica instantánea
🧠IA explica código
🏆Certificado

Sin registro • Sin tarjeta