Las list comprehensions y las generator expressions permiten transformar o filtrar datos con una sintaxis compacta. A primera vista parecen casi iguales: una utiliza corchetes y la otra paréntesis. Sin embargo, su comportamiento en memoria, el momento en que calculan los valores y la forma de reutilizarlas son diferentes. Elegir correctamente puede mejorar la claridad y evitar consumir memoria innecesaria.
Para entender esta comparación conviene dominar las listas en Python, los bucles for, las funciones y el uso de list comprehensions. También es útil conocer que un iterable puede producir valores sin almacenarlos todos al mismo tiempo.
Qué es una list comprehension
Una list comprehension crea una lista completa inmediatamente:
cuadrados = [numero ** 2 for numero in range(10)]
print(cuadrados)El resultado contiene los diez valores en memoria. Esto es conveniente cuando necesitas acceder varias veces, conocer la longitud, usar índices, ordenar o modificar la colección.
Qué es una generator expression
Una generator expression utiliza paréntesis y produce un generador:
cuadrados = (numero ** 2 for numero in range(10))
print(cuadrados)
print(next(cuadrados))El objeto no calcula todos los cuadrados al crearse. Cada valor se genera cuando se solicita mediante next(), un bucle o una función consumidora como sum().
Diferencia principal: cálculo inmediato o perezoso
La lista realiza el trabajo de una vez. El generador aplica evaluación perezosa: calcula únicamente lo necesario.
lista = [n * 2 for n in range(1_000_000)]
generador = (n * 2 for n in range(1_000_000))La lista almacena un millón de referencias y valores. El generador conserva la lógica y su estado actual. Esto puede reducir mucho la memoria cuando procesas grandes rangos o archivos.
Medir el tamaño del objeto
import sys
lista = [n for n in range(100_000)]
generador = (n for n in range(100_000))
print(sys.getsizeof(lista))
print(sys.getsizeof(generador))getsizeof() no siempre cuenta todos los objetos internos, pero ilustra la diferencia. El tamaño del generador permanece aproximadamente constante, mientras la lista crece con los elementos.
Cuándo una lista es mejor
Utiliza una list comprehension cuando necesitas:
- recorrer los resultados más de una vez;
- acceder por índice;
- usar
len()inmediatamente; - ordenar o modificar la colección;
- trabajar con un volumen pequeño o moderado.
precios_con_impuesto = [precio * 1.21 for precio in precios]
print(len(precios_con_impuesto))
print(precios_con_impuesto[0])
precios_con_impuesto.sort()En este caso, la lista es clara y práctica.
Cuándo un generador es mejor
Utiliza una generator expression cuando:
- los datos son muy grandes;
- solo necesitas una pasada;
- la función consumidora acepta iterables;
- quieres iniciar el procesamiento sin esperar a calcular todo;
- la fuente puede ser infinita.
total = sum(numero ** 2 for numero in range(1_000_000))No es necesario crear una lista intermedia para sumar los valores.
Un generador se consume
generador = (n * 2 for n in range(5))
print(list(generador))
print(list(generador))La primera conversión produce los valores. La segunda devuelve una lista vacía porque el generador ya llegó al final. Si necesitas volver a recorrer, crea otro generador o utiliza una lista.
Filtrar datos
pares_lista = [n for n in range(20) if n % 2 == 0]
pares_generador = (n for n in range(20) if n % 2 == 0)La condición funciona igual. La diferencia sigue siendo el almacenamiento y el momento de cálculo.
Procesar un archivo grande
from pathlib import Path
ruta = Path("servidor.log")
with ruta.open(encoding="utf-8") as archivo:
errores = (
linea.strip()
for linea in archivo
if "ERROR" in linea
)
for error in errores:
procesar(error)El archivo se recorre línea por línea. No se carga todo en memoria. Este patrón es más adecuado que una lista cuando el registro ocupa varios gigabytes.
Encadenar transformaciones
numeros = range(1_000_000)
pares = (n for n in numeros if n % 2 == 0)
cuadrados = (n * n for n in pares)
limitados = (n for n in cuadrados if n < 10_000)
for valor in limitados:
print(valor)Cada etapa recibe valores de la anterior bajo demanda. Este pipeline evita colecciones intermedias, aunque demasiadas expresiones anidadas pueden dificultar la lectura.
El problema de los errores tardíos
Una lista ejecuta la transformación al crearla, por lo que un error aparece inmediatamente:
lista = [10 / n for n in [5, 2, 0, 1]]El generador no falla hasta alcanzar el cero:
generador = (10 / n for n in [5, 2, 0, 1])
print(next(generador))
print(next(generador))
print(next(generador))Esto puede ser útil, pero también hace que un problema aparezca lejos del lugar donde se creó la expresión.
Rendimiento: no siempre el generador gana
Los generadores ahorran memoria, pero una lista puede ser más rápida cuando los datos son pequeños y se reutilizan varias veces. Crear nuevamente el generador o recalcular una transformación costosa puede consumir más tiempo.
Mide el caso real con timeit y controla el consumo de memoria. No elijas únicamente por una regla general.
Pasar expresiones a funciones
Muchas funciones aceptan cualquier iterable:
maximo = max(n * 3 for n in datos)
minimo = min(n * 3 for n in datos)
total = sum(n * 3 for n in datos)
Cuando una generator expression es el único argumento, puedes omitir un par de paréntesis adicionales:
total = sum(n * 3 for n in datos)Convertir un generador en lista
generador = (n ** 2 for n in range(10))
resultado = list(generador)Esto conserva la posibilidad de construir el pipeline de forma perezosa y materializar únicamente al final. Sin embargo, al llamar a list() vuelves a almacenar todos los elementos.
Generadores infinitos
La biblioteca itertools permite fuentes infinitas. La documentación oficial de itertools describe funciones como count(), cycle() y repeat().
from itertools import count, islice
numeros = (n * 10 for n in count(1))
print(list(islice(numeros, 5)))Una lista infinita es imposible, pero un generador puede producir valores indefinidamente. Siempre limita su consumo.
Semántica de las expresiones generadoras
La referencia oficial de expresiones generadoras detalla cómo se evalúan y cómo capturan el contexto. La primera parte iterable se evalúa al crear el generador, mientras las operaciones internas avanzan durante la iteración.
Evitar expresiones demasiado complejas
Una expresión compacta no siempre es más legible:
resultado = [transformar(x) for grupo in datos for x in grupo if validar(x)]Si hay varias condiciones, manejo de errores o pasos con nombres útiles, utiliza un bucle o una función generadora:
def valores_validos(datos):
for grupo in datos:
for valor in grupo:
if validar(valor):
yield transformar(valor)yield mantiene la evaluación perezosa y mejora la claridad.
Comparación práctica
# Lista: necesitamos reutilizar e indexar
usuarios_activos = [u for u in usuarios if u.activo]
mostrar(usuarios_activos)
exportar(usuarios_activos)
# Generador: una sola exportación de muchos registros
usuarios_activos = (u for u in usuarios if u.activo)
exportar(usuarios_activos)La decisión depende de cómo se consumirá el resultado, no únicamente del tamaño de la fuente.
Errores frecuentes
Los problemas más comunes son intentar usar len() o índices en un generador, recorrerlo dos veces, crear una lista enorme solo para pasarla a sum(), usar generadores cuando se necesitan varias pasadas y escribir expresiones demasiado complejas. También debes recordar que un generador puede mantener referencias a recursos mientras no se consume completamente.
Conclusión
Una list comprehension calcula y almacena todos los resultados, mientras una generator expression produce valores bajo demanda. Las listas son mejores para reutilización, índices y colecciones pequeñas. Los generadores destacan en una sola pasada, archivos grandes, pipelines y fuentes potencialmente infinitas. La elección correcta combina claridad, memoria y patrón de uso. Cuando la expresión deja de ser legible, una función con yield suele ser la mejor alternativa.






