Historia del asunto
Python, a partir de la versión 2.4, complementó las comprensiones de listas (list comprehensions) con lo que se llaman "expresiones generadoras". Estas permiten crear una secuencia perezosa de valores, similar a los generadores, pero en una forma compacta y legible.
Problema
Las comprensiones de listas ([x for x in iterable]) crean una lista, cargando todos los elementos en memoria de inmediato. Esto no es eficiente o incluso puede ser peligroso si la cantidad de elementos es muy grande. Las funciones generadoras (usando yield) son más flexibles, pero requieren la definición de una función separada y más líneas de código.
Solución
Las expresiones generadoras ((x for x in iterable)) proporcionan una sintaxis concisa para producir secuencias perezosas (los elementos se calculan según sea necesario, y no se cargan todos de inmediato). Se ven similares a las comprensiones de listas, pero utilizan paréntesis:
# La comprensión de listas carga todo en memoria squares_list = [x**2 for x in range(10**6)] # La expresión generadora: los elementos llegan bajo demanda, la memoria casi no se utiliza squares_gen = (x**2 for x in range(10**6)) # Obtener los primeros cinco valores del generador for _ in range(5): print(next(squares_gen))
Características clave:
¿Se puede "recorrer" la misma expresión generadora varias veces?
No, después de iterar una vez, el generador se "agota". Para volver a recorrerlo, es necesario crear un nuevo generador o utilizar una comprensión de listas.
it = (x for x in range(3)) print(list(it)) # [0,1,2] print(list(it)) # [] — ya no se pueden obtener más valores
¿Los generadores mantienen el estado entre usos?
Sí, la expresión generadora mantiene la "posición" entre llamadas a next() (o durante la siguiente iteración), pero no se puede reiniciar al principio a menos que cree un nuevo objeto.
¿Se puede usar la expresión generadora múltiples veces en una sola línea?
¡No! Si "desempaquetas" un generador en varios lugares (por ejemplo, en varias funciones al mismo tiempo, sin devolverlo a una lista), parte de los datos se perderá — cada uso secundario avanza el puntero hacia adelante.
g = (x for x in range(3)) print(sum(g), list(g)) # sum(g) obtendrá todo, list(g) quedará vacío
En un proyecto para analizar archivos grandes se utilizó:
data = (parse_line(line) for line in file) process(list(data)) other_process(list(data))
Ventajas:
Desventajas:
Se utilizaron comprensiones de listas si se necesitaba reutilizar los datos, o se crea un generador para consumo único:
# Generador solo para análisis único (por ejemplo, contar la suma) total = sum(parse_line(line) for line in file)
Ventajas:
Desventajas: