Historia de la pregunta
La evaluación perezosa (lazy evaluation) es una técnica de programación en la que los cálculos se posponen hasta que el resultado es necesario para el código. En el lenguaje Python, esta paradigma se ha vuelto popular gracias a los generadores y funciones especiales de la biblioteca estándar, como itertools. Originalmente, estas técnicas provienen de lenguajes funcionales, pero Python ofrece herramientas nativas y de terceros para el procesamiento perezoso.
Problema
El procesamiento tradicional eager (hambriento) requiere cargar y calcular todos los datos de inmediato (por ejemplo, al usar expresiones de lista), lo que puede llevar a un alto consumo de memoria y rendimiento reducido al trabajar con secuencias grandes o infinitas. El procesamiento perezoso permite "cargar" y procesar elementos solo según sea necesario, evitando gastos de recursos innecesarios.
Solución
En Python, el procesamiento perezoso se implementa no solo a través de generadores (yield), sino también mediante funciones especiales perezosas en los módulos itertools, funciones estándar como map, filter, así como objetos de expresiones generadoras (generator expressions). Por ejemplo, la función map() devuelve un iterador perezoso que calcula valores solo cuando se les solicita:
# Ejemplo de procesamiento perezoso: elevar cada número al cuadrado squares = map(lambda x: x ** 2, range(10**10)) # no se gasta memoria en una lista print(next(squares)) # 0 print(next(squares)) # 1
Características clave:
¿Siempre la función map() en Python implementa el procesamiento perezoso? ¿Cómo saber qué funciones estándar son perezosas?
No, a partir de Python 3, las funciones map(), filter(), zip() devuelven iteradores, es decir, implementan procesamiento perezoso. En Python 2, estas funciones devolvían listas. Para saber si un objeto es perezoso, debes consultar su tipo o revisar la documentación:
result = map(lambda x: x+1, range(5)) print(type(result)) # 'map' — es un iterador
¿Funcionará el procesamiento perezoso al aplicar una expresión generadora dentro de la función sum()?
La función sum() necesita iterar a través de todo el iterador hasta el final. La expresión generadora es perezosa por sí misma, pero sum() finalmente consume toda la secuencia:
s = sum(x**2 for x in range(1000000)) # el generador se consume por completo
¿Se puede aplicar procesamiento perezoso a listas y tuplas comunes, por ejemplo, a través de map/lambda?
Sí, se puede, pero las listas y tuplas aún están cargadas en la memoria. map devolverá un iterador perezoso sobre ellas, pero los datos originales todavía están completamente en memoria. Para una cadena perezosa completa, es deseable trabajar con generadores en cada etapa:
def gen(): for i in range(1, 100): yield i squares = map(lambda x: x**2, gen()) # todo perezoso
Un desarrollador escribe un procesamiento para un gran archivo de logs, utilizando una expresión generadora para filtrar líneas, pero accidentalmente lo convierte de inmediato en una lista:
with open('biglog.txt') as f: important_lines = [line for line in f if 'ERROR' in line] # carga todo el archivo
Ventajas:
Desventajas:
Otro equipo utiliza un enfoque perezoso con una expresión generadora y procesa las líneas a medida que las recibe:
with open('biglog.txt') as f: for line in (l for l in f if 'ERROR' in l): process(line)
Ventajas:
Desventajas: