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 executaAções forçam a execução do plano acumulado:
df4.show() # aqui o Spark analisa o DAG completo, otimiza e executaAntes 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ção | Preferir |
|---|---|
| Pipeline longa com filtros e projeções | Lazy: o otimizador elimina trabalho desnecessário |
| Operação única e simples | Eager: menos overhead, erro aparece imediatamente |
| Debug iterativo | Eager: o stack trace aponta para a linha exata do problema |
| Reutilização do resultado várias vezes | Cache 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