Explicación de los generadores y el rendimiento de Python: Ahorre memoria con ejemplos reales
Los generadores producen valores uno a la vez en lugar de crear una lista completa en la memoria. Observe exactamente cómo el rendimiento detiene una función, vea la diferencia de memoria que medimos y cree una pequeña canalización de datos.
- Una función que contiene
yielddevuelve un generador y su cuerpo no se ejecuta hasta que solicitas un valor. yieldpausa la función y mantiene su estado local hasta la siguiente solicitud.- Los generadores utilizan una cantidad pequeña y fija de memoria, independientemente de la cantidad de valores que produzcan.
- Un generador sólo se puede consumir una vez, que es la trampa más común.
Imagine cargar un archivo de registro de 10 GB en una lista solo para encontrar los errores. Su programa puede quedarse sin memoria antes de encontrarla. Los generadores de Python resuelven esto al producir valores a pedido, uno a la vez, para que puedas trabajar con datos enormes o incluso infinitos casi sin memoria.
Esta guía muestra lo que realmente sucede cuando un generador funciona, paso a paso. Ejecutamos cada ejemplo en Python 3.13 y el resultado proviene de esa ejecución. Es útil sentirse cómodo con las funciones y los bucles, que cubre nuestro tutorial de Python para principiantes.
¿Qué es un generador?
Un generador es un tipo especial de iterador que se crea escribiendo una función usando yield. Cualquier función con yield se convierte automáticamente en una función generadora y llamarla le proporciona un objeto generador en lugar de ejecutar el código.
def count_up(limit):
print("starting")
n = 1
while n <= limit:
yield n
n += 1
print("finished")
gen = count_up(2)
print(gen)
# <generator object count_up at 0x...>
Observe que “inicio” no se imprimió. Llamar a la función solo creó el generador. Nada dentro ha funcionado todavía.
Cómo funciona el rendimiento, paso a paso
Cada llamada a next() ejecuta la función hasta que llega a yield, devuelve ese valor y se congela en su lugar. La siguiente llamada se reanuda justo después de esa línea, con todas las variables locales intactas.
print(next(gen))
# starting
# 1
print(next(gen))
# 2
next(gen)
# finished
# StopIteration
Siga la salida con atención. “iniciando” aparece sólo en el primero next(). En el segundo, la ejecución continúa desde donde se detuvo, por lo que n todavía está disponible y se convierte en 2. En el tercero, el ciclo termina, se imprime “terminado”, la función cae al final y Python eleva StopIteration para indicar que no queda nada.
Rara vez te llamas next() a ti mismo. Un bucle for lo hace por usted y maneja StopIteration silenciosamente, al igual que list(), sum() y funciones similares:
print(list(count_up(3)))
# starting
# finished
# [1, 2, 3]
rendimiento versus retorno
| Aspecto | volver |
rendimiento |
|---|---|---|
| que hace | Finaliza la función | Pausa la función |
| Valores producidos | un resultado | Una serie, una a la vez. |
| estado local | Descartado | Preservado entre valores |
| Cuando se ejecuta el código | Inmediatamente cuando lo llaman | Sólo cuando se solicita un valor |
| Memoria para muchos valores. | Debe contenerlos a todos | Sostiene uno a la vez |
Este retraso se conoce como evaluación diferida: los valores se calculan solo cuando es necesario. Un simple return dentro de un generador simplemente lo termina, lo que activa StopIteration.
La diferencia de memoria, medida
Comparamos una lista de un millón de cuadrados con un generador equivalente, usando sys.getsizeof:
import sys
squares_list = [n * n for n in range(1_000_000)]
squares_gen = (n * n for n in range(1_000_000))
print(sys.getsizeof(squares_list), sys.getsizeof(squares_gen))
# 8448728 200
La propia estructura de la lista ocupaba unos 8,4 MB, mientras que el generador ocupaba sólo 200 bytes en nuestro sistema. La brecha real es aún mayor, ya que getsizeof no cuenta los objetos enteros dentro de la lista. El tamaño del generador también permanece igual para mil millones de artículos, ya que sólo almacena su estado actual y no sus resultados.
Sin embargo, existe una compensación. Una lista se puede reutilizar, indexar y medir con len(). Un generador no ofrece nada de esto. Si necesita acceso aleatorio o recorrerá varias veces una pequeña cantidad de datos, una lista es más sencilla. Consulte nuestra guía para Lista de Python, tupla y conjunto para obtener más información sobre esos tipos.
Expresiones generadoras
Para casos simples, no necesita una función. Una expresión generadora se parece a una lista de comprensión con corchetes en lugar de cuadrados:
squares = [n * n for n in range(5)] # builds a list now
squares = (n * n for n in range(5)) # produces values on demand
print(sum(n * n for n in range(5)))
# 30
Cuando una expresión generadora es el único argumento de una función, como en sum(), puede omitir los corchetes adicionales. Este es un hábito ordenado para totales de datos grandes, porque no se crea ninguna lista temporal.
Construya una canalización de datos
Aquí es donde realmente brillan los generadores. Se pueden encadenar pequeños generadores para que cada elemento fluya por cada paso, uno a la vez:
def read_lines(lines):
for line in lines:
yield line.strip()
def only_errors(lines):
for line in lines:
if line.startswith("ERROR"):
yield line
def messages(lines):
for line in lines:
yield line.split(" ", 1)[1]
log = ["INFO started\n", "ERROR db down\n",
"ERROR disk full\n", "INFO stopped\n"]
print(list(messages(only_errors(read_lines(log)))))
# ['db down', 'disk full']
Cada paso realiza un pequeño trabajo y puede probarse por sí solo. En uso real, cambie la lista de muestra por un archivo abierto, porque los objetos del archivo se pueden leer línea por línea, como muestra nuestra guía para el manejo de archivos Python. Incluso un registro de varios gigabytes pasaría con muy poca memoria.
Consejo: Una canalización no funciona hasta que algo la consume. Construir la cadena es instantáneo. El procesamiento ocurre solo cuando recorre el generador final o lo envuelve en una llamada como list().
secuencias infinitas
Una lista no puede ser infinita, pero un generador sí, porque solo calcula el siguiente valor. Utilice itertools.islice para tomar tantos como necesite:
import itertools
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
print(list(itertools.islice(fibonacci(), 8)))
# [0, 1, 1, 2, 3, 5, 8, 13]
Sin embargo, tenga cuidado: llamar a list(fibonacci()) sin límite se ejecutará hasta que se acabe la memoria. El mismo patrón se adapta a los generadores de ID, los bucles de sondeo y los resultados de API paginados, donde se recupera la página siguiente sólo cuando se agota la actual.
rendimiento de y lotes
yield from pasa a otro iterable, ahorrándote un bucle anidado:
def chain_all(*groups):
for group in groups:
yield from group
print(list(chain_all([1, 2], (3, 4))))
# [1, 2, 3, 4]
Un patrón muy práctico es dividir los datos en lotes de tamaño fijo, por ejemplo para insertar mil filas en una base de datos a la vez:
def batches(items, size):
batch = []
for item in items:
batch.append(item)
if len(batch) == size:
yield batch
batch = []
if batch:
yield batch
print(list(batches(range(7), 3)))
# [[0, 1, 2], [3, 4, 5], [6]]
El if batch final asegura que no se pierda un grupo sobrante más pequeño. Python 3.12 y posteriores también incluyen itertools.batched, que hace un trabajo similar.
enviar, cerrar y limpiar
Los generadores también pueden recibir valores. send() reanuda el generador y el valor se convierte en el resultado de la expresión yield. Primero debe llamar a next() una vez para comunicarse con el primer yield:
def running_total():
total = 0
while True:
value = yield total
total += value
t = running_total()
next(t)
print(t.send(10), t.send(5))
# 10 15
La mayoría de la gente rara vez necesita esto. Una preocupación más común es la limpieza. Si se detiene antes, todavía se ejecuta un bloque finally cuando el generador está cerrado:
def tracked():
try:
yield 1
yield 2
finally:
print("cleanup ran")
tr = tracked()
next(tr)
tr.close()
# cleanup ran
Pytest usa esta misma idea: un dispositivo con yield ejecuta su limpieza después, como lo demuestra nuestro tutorial de pytest. Del mismo modo, el decorador contextmanager de la biblioteca estándar convierte un generador en un bloque with.
Generadores en clases.
Escribir un método __iter__ como generador es la forma más sencilla de hacer que su clase sea iterable. Una ventaja es que cada bucle obtiene un generador nuevo, por lo que el objeto se puede repetir una y otra vez:
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
n = self.start
while n > 0:
yield n
n -= 1
c = Countdown(3)
print(list(c), list(c))
# [3, 2, 1] [3, 2, 1]
Cuando usar un generador
| Utilice un generador cuando | Utilice una lista cuando |
|---|---|
| Los datos son grandes o se desconoce su tamaño. | Los datos son pequeños y caben en la memoria. |
| Procesas los artículos una vez, en orden | Haces un bucle varias veces |
| Puedes parar temprano | Necesitas len(), indexar o cortar |
| Estás leyendo una transmisión o archivos. | Necesitas ordenar o revertir los datos. |
| Estás construyendo un oleoducto | La simplicidad importa más que la memoria |
Un generador no siempre es más rápido en general. Si termina usando todos los valores, el trabajo total es similar y la comprensión de una lista puede ser incluso más rápida para datos pequeños porque tiene menos gastos generales por elemento. Los principales beneficios son el uso de la memoria y la obtención de los primeros resultados antes.
Errores comunes
Reutilizar un generador agotado
Esto atrapa a casi todo el mundo en algún momento. Un generador recuerda que está terminado:
g = count_up(2)
print(list(g), list(g))
# [1, 2] []
La segunda llamada obtiene una lista vacía sin ningún error. Llame a la función nuevamente para obtener un nuevo generador o use el enfoque de clases que se muestra arriba.
Esperando que el código se ejecute inmediatamente
def noisy():
print("body runs")
yield 1
n = noisy() # nothing is printed
print(next(n)) # body runs, then 1
Las comprobaciones de argumentos en la parte superior de un generador no se ejecutan hasta el primer next(), por lo que los errores pueden aparecer lejos de la llamada que los causó.
Otras trampas
- Llamar a
len()o indexar. Los generadores no admiten ninguno de los dos y generan unTypeError. - Cerrar un archivo demasiado pronto. Devolver un generador desde dentro de un bloque
withpuede significar que el archivo se cierra antes de que se lea cualquier línea. Coloque elyielddentro del bloquewith. - Envolviendo todo en
list(). Eso desperdicia el beneficio de la memoria. - Usar uno para datos pequeños. Una lista sencilla suele ser más clara.
Conclusión
Un generador es una función que puede pausarse. Esa única idea le brinda un bajo uso de memoria, secuencias interminables y canalizaciones limpias. Para ponerlo en práctica, busque un lugar en su código que cree una lista grande solo para recorrerla una vez y conviértalo en un generador. Luego intente encadenar dos pequeños generadores en un archivo de registro real.
Preguntas frecuentes
¿Qué hace el rendimiento en Python?
rendimiento produce un valor y pausa la función, manteniendo sus variables locales. La función se reanuda desde el mismo punto cuando se solicita el siguiente valor.
¿Cuál es la diferencia entre un generador y una lista?
Una lista almacena todos sus valores en la memoria a la vez. Un generador calcula cada valor según demanda, utiliza poca memoria y normalmente sólo se puede consumir una vez.
¿Cuál es la diferencia entre un iterador y un generador?
Cada generador es un iterador. Un generador es simplemente la forma más fácil de escribir uno, ya que Python crea los métodos __iter__ y __next__ por usted.
¿Puedes hacer un bucle sobre un generador dos veces?
No. Una vez agotado no arroja nada más, sin generar error. Cree un nuevo generador llamando a la función nuevamente o almacene los resultados en una lista.
¿Son los generadores más rápidos que las listas?
No siempre. Utilizan mucha menos memoria y entregan el primer resultado antes, pero la comprensión de una lista puede ser un poco más rápida cuando se consumen pequeñas cantidades de datos por completo.
sigue aprendiendo
¿Necesitas ayuda con pruebas, WordPress o SEO?
Ayudamos a los equipos a enviar mejor software y hacer crecer sus sitios. Cuéntenos en qué está trabajando y le sugeriremos un siguiente paso claro.
Más guías sobre testing, IA y desarrollo moderno
¿Te gustó este artículo? Elige un tema y salta directo a nuestros tutoriales más recientes.
Explicación de las alucinaciones de la IA: por qué los modelos de lenguaje inventan cosas y cómo reducirlas
Se explicaron las alucinaciones de la IA: qué son, por qué los modelos de lenguaje las producen, tipos comunes, cómo reducirlas con conexión a tierr…
Leer artículo →DevOpsExplicación de las variables de entorno: archivos .env, secretos y configuración bien hechos
Variables de entorno explicadas con ejemplos de Python que ejecutamos: leerlas y configurarlas, por qué cada valor es una cadena, archivos .env, her…
Leer artículo →Desarrollo de softwareExplicación de la limitación de velocidad de API: algoritmos, encabezados y manejo de errores 429
La limitación de la tasa de API se explica con una simulación que ejecutamos: depósito de tokens, ventana fija, ventana deslizante, 429 respuestas, …
Leer artículo →Automatización de pruebasBurlarse en Python: unittest.mock, patch y los errores que hacen que las pruebas mientan
Burlarse en Python con unittest.mock, probado: Mock y return_value, side_effect, parches donde se usa un nombre, especificación y autospec, fechas d…
Leer artículo →Pruebas de softwareExplicación de la cobertura del código: cobertura de línea versus sucursal y lo que realmente significa 100%
Cobertura del código explicada con una ejecución real de cobertura.py: cobertura de línea, rama y condición, lectura de un informe, por qué una cobe…
Leer artículo →Bases de datos y SQLDELETE vs TRUNCATE vs DROP en SQL: diferencias, reversión y cuándo usar cada uno
DELETE vs TRUNCATE vs DROP explicado con SQL probado: qué elimina cada uno, reversión, activadores, restablecimiento de identidad, velocidad y en qu…
Leer artículo →Bases de datos y SQLVistas SQL explicadas: cree, use y evite los errores, con ejemplos
Vistas SQL explicadas con consultas que ejecutamos: creación de una vista, resultados en vivo, ocultación de columnas, rendimiento, vistas actualiza…
Leer artículo →Desarrollo de softwareInterfaz TypeScript versus tipo: diferencias reales, probadas con el compilador
Interfaz TypeScript versus tipo, verificado con el compilador real: qué pueden hacer ambos, qué puede hacer solo uno, fusión de declaraciones, exten…
Leer artículo →Desarrollo de softwarePython *args y **kwargs explicados: funciones flexibles con ejemplos claros
Python *args y **kwargs explicados con el código que ejecutamos: empaquetado y desempaquetado, orden de parámetros, argumentos solo de palabras clav…
Leer artículo →Desarrollo de softwareFunciones Lambda de Python: sintaxis, usos reales y cuándo no utilizarlas
Funciones lambda de Python explicadas con el código que ejecutamos: sintaxis, clasificación con clave, mapa y filtro, cierres, trampa de enlace tard…
Leer artículo →Inteligencia artificial¿Qué son los agentes de IA? Cómo funcionan, con un bucle de agente simple que puedes ejecutar
¿Qué son los agentes de IA? Una guía sencilla sobre el bucle del agente, las herramientas, la memoria y las barreras de seguridad, con un pequeño bu…
Leer artículo →DevOpsMonitoreo versus observabilidad: la verdadera diferencia, con registros, métricas y seguimientos
Explicación del monitoreo versus observabilidad: fallas conocidas y desconocidas, registros, métricas y seguimientos, un recorrido por incidentes de…
Leer artículo →DevOpsDocker Volumes versus Bind Mounts: persistir los datos del contenedor de la manera correcta
Se explican los volúmenes de Docker frente a los montajes de enlace: por qué desaparecen los datos del contenedor, volúmenes con nombre, montajes de…
Leer artículo →Automatización de pruebasTDD vs BDD: diferencias clave, una función creada en ambos sentidos y cuándo usar cada una
TDD vs BDD explicado con una característica de descuento construida en ambos sentidos: refactor rojo-verde con el código que ejecutamos, escenarios …
Leer artículo →Pruebas de softwarePruebas de Aceptación de Usuario (UAT): Proceso, Lista de Verificación y Ejemplos Reales
Explicación de las pruebas de aceptación del usuario: quién las ejecuta, en qué se diferencia del control de calidad, un proceso de siete pasos, una…
Leer artículo →Bases de datos y SQLValores NULL de SQL explicados: IS NULL, COALESCE, NULLIF y las trampas que se deben evitar
Los valores NULL de SQL se explican con las consultas que ejecutamos: lógica de tres valores, IS NULL, COALESCE, NULLIF, cómo los agregados, las uni…
Leer artículo →Bases de datos y SQLDeclaración SQL CASE WHEN: sintaxis, ejemplos reales y errores comunes
CASO SQL CUANDO se explicó con las consultas que ejecutamos: formularios simples y buscados, orden de condiciones, pivotes con SUMA, trucos COUNT, c…
Leer artículo →Desarrollo de software¿Qué es JWT? Tokens web JSON explicados con un ejemplo práctico
¿Qué es JWT? JSON Web Tokens explicó: las tres partes, un token creado y manipulado en el código que ejecutamos, JWT vs sesiones, almacenamiento, to…
Leer artículo →Desarrollo de softwareExplicación del bucle de eventos de JavaScript: pila de llamadas, microtareas y temporizadores con ejemplos
El bucle de eventos de JavaScript explicado con el código que ejecutamos: pila de llamadas, colas de tareas y microtareas, por qué setTimeout 0 no e…
Leer artículo →Desarrollo de softwareExplicación de los generadores y el rendimiento de Python: ahorre memoria con ejemplos reales
Los generadores de Python y el rendimiento se explican con el código que ejecutamos: cómo se pausan y reanudan, midieron el ahorro de memoria, canal…
Leer artículo →Inteligencia artificial¿Qué son las incrustaciones? Los vectores y la similitud del coseno se explican de forma sencilla
¿Qué son las incrustaciones? Una guía en lenguaje sencillo sobre vectores, similitud de cosenos y búsqueda semántica, con un pequeño ejemplo que eje…
Leer artículo →DevOpsImplementación azul-verde versus implementación canaria: diferencias, compensaciones y cómo elegir
Explicación de la implementación azul-verde versus canaria: cómo funciona cada uno, costo, riesgo y reversión, agrupación estable de usuarios que pr…
Leer artículo →Automatización de pruebasPruebas inestables: qué las causa y cómo solucionarlas para siempre
Pruebas inestables explicadas con demostraciones que realizamos: las causas principales, cómo confirmar y diagnosticar una, correcciones adecuadas p…
Leer artículo →Pruebas de softwarePruebas de carga frente a pruebas de estrés: diferencias, métricas y ejemplos de k6
Explicación de las pruebas de carga frente a las pruebas de estrés: qué prueba cada una, pruebas de picos y de absorción, las métricas que importan,…
Leer artículo →Pruebas de softwareCaso de prueba frente a escenario de prueba: diferencias clave, ejemplos y cuándo utilizar cada uno
Caso de prueba versus escenario de prueba explicado: definiciones claras, una tabla en paralelo, una función de restablecimiento de contraseña tomad…
Leer artículo →Bases de datos y SQLOptimización de consultas SQL: lea planes de ejecución y corrija consultas lentas, con tiempos
Optimización de consultas SQL probada en un millón de filas: lea planes de ejecución, agregue los índices correctos, corrija filtros no sargables, u…
Leer artículo →Bases de datos y SQLSQL UNION vs UNION ALL: diferencias, ejemplos y rendimiento probado
SQL UNION vs UNION ALL explicado con consultas que ejecutamos: duplicados, las reglas que siguen ambos, un error de datos real, rendimiento medido, …
Leer artículo →Desarrollo de softwareTutorial de JavaScript Fetch API: GET, POST, manejo de errores y tiempos de espera
Un tutorial de JavaScript Fetch API probado en un servidor local: GET y POST, por qué 404 no arroja, un asistente reutilizable, cadenas de consulta,…
Leer artículo →Desarrollo de softwareManejo de archivos Python: lectura, escritura, CSV, JSON y pathlib con ejemplos
El manejo de archivos Python se explica con el código que ejecutamos: modos abiertos, lectura de archivos grandes, CSV, JSON, pathlib, archivos bina…
Leer artículo →Desarrollo de softwareClases y objetos de Python: tutorial de programación orientada a objetos con ejemplos reales
Clases y objetos de Python explicados con el código que ejecutamos: __init__, métodos, atributos de clase versus instancia, herencia, propiedades, m…
Leer artículo →Inteligencia artificial¿Qué es un modelo de lenguaje grande? Cómo funcionan los LLM, explicados sin exageraciones
¿Qué es un modelo de lenguaje grande? Los LLM explicados en lenguaje sencillo: tokens, incrustaciones, atención, entrenamiento, ventanas de temperat…
Leer artículo →DevOpsComandos de Linux para principiantes: la guía esencial de terminal con ejemplos
Comandos de Linux para principiantes, agrupados por tarea: navegación, archivos, visualización de registros, grep, canalizaciones, permisos, proceso…
Leer artículo →Automatización de pruebasSelenium vs Playwright: diferencias clave, comparación de códigos y cuál elegir
Comparación de Selenium vs Playwright: arquitectura, espera, soporte de navegador e idioma, depuración, la misma prueba en ambas herramientas, conse…
Leer artículo →Pruebas de softwarePartición de equivalencia: técnica de diseño de pruebas explicada con ejemplos resueltos
Explicación de la partición de equivalencia: cómo encontrar particiones válidas e inválidas, ejemplos prácticos para edades, contraseñas y cupones, …
Leer artículo →Pruebas de softwareSeveridad vs Prioridad en Pruebas de Software: Diferencias, Matriz y Ejemplos Reales
Explicación de gravedad versus prioridad en las pruebas de software: definiciones claras, una matriz de cuatro vías con ejemplos, escalas de niveles…
Leer artículo →Bases de datos y SQLTutorial de subconsultas SQL: tipos, subconsultas correlacionadas y EXISTENCIAS con ejemplos
Un tutorial de subconsultas SQL con consultas que ejecutamos: subconsultas escalares, IN, FROM y SELECT, subconsultas correlacionadas, EXISTS, la tr…
Leer artículo →Bases de datos y SQLTransacciones SQL y ACID explicados: COMMIT, ROLLBACK y niveles de aislamiento
Transacciones SQL y ACID explicadas con una transferencia bancaria que ejecutamos: COMMIT, ROLLBACK, puntos de guardado, bloqueos, niveles de aislam…
Leer artículo →Desarrollo de softwareGit merge vs rebase: diferencias, ejemplos y cuándo usar cada uno
Git merge vs rebase explicado con salida real del log, una tabla comparativa, la regla de oro del rebase y un flujo de trabajo seguro para equipos.
Leer artículo →Desarrollo de softwareList comprehension en Python: sintaxis, ejemplos y cuándo evitarla
List comprehension en Python explicada: sintaxis, filtros, if-else, bucles anidados, tiempos medidos y cuándo es mejor un bucle for normal.
Leer artículo →