Lazy Evaluation (avaliação preguiçosa) é uma estratégia de execução que adia a computação de uma expressão até o momento em que o resultado é de fato necessário, em vez de executar cada operação imediatamente conforme ela é declarada.

O problema que ela resolve

Na execução imediata (eager), cada operação roda na ordem em que aparece no código. O sistema não tem visão do que vem depois, então não pode eliminar trabalho redundante nem reordenar operações para ganhar eficiência.

Com lazy evaluation, o sistema acumula as operações em um plano e só executa quando forçado. Nesse momento, ele analisa o plano completo e pode otimizar antes de tocar nos dados.

Exemplo concreto: sem lazy evaluation, um filtro aplicado depois de um join processa todos os registros do join para depois descartar a maioria. Com lazy evaluation, o otimizador empurra o filtro para antes do join (predicate pushdown), reduzindo drasticamente os dados que participam da operação cara.

Como funciona no Spark

O Spark distingue dois tipos de operação:

Transformações constroem o plano sem executar nada. Cada transformação adiciona um nó ao DAG (Directed Acyclic Graph) de operações:

df = spark.read.parquet("gs://lake/pedidos/")   # nada executa
df2 = df.filter(df.status == "ativo")           # nada executa
df3 = df2.groupBy("cliente_id").count()         # nada executa
df4 = df3.filter(df3["count"] > 5)              # nada executa

Ações forçam a execução do plano acumulado:

df4.show()   # aqui o Spark analisa o DAG completo, otimiza e executa

Antes de executar, o Catalyst Optimizer analisa o plano e aplica otimizações como predicate pushdown, column pruning e reordenação de joins. Esse processo só é possível porque o Spark tem o plano inteiro disponível antes de começar.

Ver spark-arquitetura para como o DAGScheduler converte esse plano em Stages e Tasks.

Como funciona no Polars

O Polars tem dois modos de operação explícitos:

import polars as pl
 
# Eager (padrão com DataFrame): executa imediatamente
df = pl.read_parquet("pedidos.parquet")
resultado = df.filter(pl.col("status") == "ativo").select("cliente_id", "valor")
 
# Lazy (LazyFrame): acumula o plano
resultado = (
    pl.scan_parquet("pedidos.parquet")   # scan_* retorna LazyFrame
    .filter(pl.col("status") == "ativo")
    .select("cliente_id", "valor")
    .collect()                           # collect() força a execução
)

Com scan_parquet, o Polars sabe quais colunas e filtros serão aplicados antes de ler o arquivo, e lê apenas as colunas necessárias do Parquet (column pruning nativo).

Como funciona em Python puro

Python tem lazy evaluation via geradores. Um gerador não computa os valores antecipadamente: produz cada elemento sob demanda.

# Eager: cria toda a lista em memória imediatamente
quadrados = [x ** 2 for x in range(1_000_000)]
 
# Lazy: computa cada valor apenas quando iterado
quadrados = (x ** 2 for x in range(1_000_000))
 
# Útil para pipelines encadeadas: nenhuma lista intermediária é criada
resultado = sum(x ** 2 for x in range(1_000_000) if x % 2 == 0)

Trade-offs

A lazy evaluation não é sempre a melhor escolha:

SituaçãoPreferir
Pipeline longa com filtros e projeçõesLazy: o otimizador elimina trabalho desnecessário
Operação única e simplesEager: menos overhead, erro aparece imediatamente
Debug iterativoEager: o stack trace aponta para a linha exata do problema
Reutilização do resultado várias vezesCache explícito (persist(), collect()): lazy recomputaria do zero

O principal ponto de atenção com lazy evaluation é que erros de schema ou tipo só aparecem na execução, não na declaração. Um df.filter(col("coluna_inexistente") > 0) compila sem erro e só falha quando uma Action é chamada.

Conexões

  • spark-arquitetura: como o DAGScheduler e o Catalyst Optimizer exploram a lazy evaluation no Spark
  • spark-rdd: transformações e ações nos RDDs, o modelo original de lazy evaluation do Spark
  • spark-apis: lista completa de transformações (lazy) e ações (eager) no DataFrame e RDD
  • spark-sql: Catalyst Optimizer, que recebe o plano lazy e aplica as otimizações
  • python-polars: LazyFrame e scan_* como interface lazy do Polars

Referências