Módulo 14 - Performance e profiling

Memoização com lru_cache e geradores para memória

10 min de leitura · por Cesar Gargiulo, revisado pela equipe ValorFinal e GuardiaSec · Atualizado em 01/07/2026

O que você vai aprender

  • Usar functools.lru_cache para memoizar resultados de funções puras.
  • Entender quando o cache ajuda e quando ele atrapalha.
  • Usar geradores para processar dados grandes sem carregar tudo na memória.
  • Reconhecer e evitar armadilhas comuns de performance.

Não repita trabalho: lru_cache

Às vezes a otimização não é fazer o trabalho mais rápido, e sim não refazer o mesmo trabalho. Se uma função é chamada muitas vezes com as mesmas entradas e sempre devolve o mesmo resultado, guardar esse resultado e reutilizá-lo economiza tudo. Essa técnica é a memoização, e o functools.lru_cache a entrega pronta: você decora a função e o Python passa a lembrar os resultados. Na segunda chamada com a mesma entrada, ele devolve o valor guardado em vez de recalcular.

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(35))          # instantaneo com cache
print(fib.cache_info())  # mostra hits e misses do cache

Sem cache, fib(35) recalcula os mesmos valores milhões de vezes; com lru_cache, cada um é feito uma vez.

O ganho no exemplo da sequência de Fibonacci é dramático porque a versão sem cache recalcula os mesmos valores um número absurdo de vezes. Com o lru_cache, cada valor é calculado uma vez e reaproveitado, transformando um cálculo lentíssimo em instantâneo. O parâmetro maxsize controla quantos resultados o cache guarda; com None, ele guarda todos. E o método cache_info revela quantas vezes o cache foi aproveitado, um bom diagnóstico de se ele está valendo a pena.

Geradores: processar muito sem gastar memória

Performance não é só tempo; memória também conta. Quando você carrega um arquivo gigante inteiro numa lista, ou monta uma lista com milhões de itens só para percorrê-la uma vez, o programa pode engasgar ou estourar a memória. Os geradores resolvem isso com avaliação preguiçosa: eles produzem um valor por vez, sob demanda, sem nunca guardar a sequência inteira. Trocar uma list comprehension entre colchetes por uma expressão geradora entre parênteses já é o suficiente em muitos casos.

# Lista: cria um milhao de itens na memoria de uma vez
soma = sum([n * n for n in range(1_000_000)])

# Gerador: produz um item por vez, memoria quase constante
soma = sum(n * n for n in range(1_000_000))

# Ler um arquivo enorme linha a linha, sem carregar tudo
def contar_erros(caminho):
    total = 0
    with open(caminho, encoding="utf-8") as f:
        for linha in f:            # o arquivo e iterado preguicosamente
            if "ERRO" in linha:
                total += 1
    return total

A versão com gerador (parênteses) não materializa a lista; iterar o arquivo linha a linha faz o mesmo.

List comprehension (colchetes)

  • Cria a lista inteira na memória
  • Boa quando você reutiliza os itens várias vezes
  • Permite índice, len e fatiamento
  • Custosa em memória para sequências enormes

Expressão geradora (parênteses)

  • Produz um item por vez, sob demanda
  • Ideal para percorrer uma vez só
  • Memória quase constante, mesmo com milhões de itens
  • Não permite índice nem reuso direto

Armadilhas comuns de performance

Alguns padrões custam caro sem parecer. O mais famoso é concatenar strings com o operador mais dentro de um laço: como strings são imutáveis, cada mais cria uma string nova e copia todo o conteúdo anterior, o que vira O(n ao quadrado). A solução é acumular os pedaços numa lista e juntar uma vez só com str.join. Outra armadilha é verificar pertencimento numa lista dentro de um laço, quando um set resolveria em O(1). E há o clássico de recriar, dentro de um laço, algo que poderia ser calculado uma vez fora dele.

ArmadilhaPor que é lentaO que fazer
s += texto num laçoCada + copia a string toda (O(n ao quadrado))Acumular numa lista e usar str.join
item in lista num laçoCada busca varre a lista (O(n))Converter para set uma vez e testar in
Recalcular fora do laçoRefaz o mesmo trabalho a cada voltaCalcular uma vez antes do laço
Carregar arquivo inteiroOcupa memória proporcional ao tamanhoIterar linha a linha com for

Quatro armadilhas de performance frequentes e a correção idiomática de cada uma.

# Lenta: concatena string dentro do laco (O(n ao quadrado))
def juntar_ruim(itens):
    s = ""
    for x in itens:
        s += str(x) + ", "
    return s

# Rapida: acumula e junta uma vez (O(n))
def juntar_bom(itens):
    return ", ".join(str(x) for x in itens)

A concatenação em laço copia tudo a cada volta; str.join monta a string final de uma vez.

Perceba que nenhuma dessas correções é um truque obscuro: todas são também o jeito mais pythônico e legível de escrever. Esse é o resumo do módulo. Medir aponta onde agir; a estrutura de dados certa muda a ordem de grandeza; o cache evita repetir trabalho; os geradores poupam memória; e evitar as armadilhas clássicas já entrega código rápido e limpo ao mesmo tempo. Performance, no avançado, raramente é sobre esperteza; é sobre medir, escolher bem e conhecer as ferramentas que a linguagem já oferece.

Teste rápido

Por que concatenar strings com += dentro de um laço é lento?

Perguntas frequentes

Quando o lru_cache atrapalha em vez de ajudar?
Quando a função não é pura. Se ela depende do relógio, de arquivos, de rede ou de dados que mudam, o cache devolve um resultado velho na próxima chamada. Também não vale a pena se as entradas quase nunca se repetem, porque aí o cache só ocupa memória sem entregar reaproveitamento.
Qual a diferença prática entre lista e gerador para percorrer dados?
A lista guarda todos os itens na memória de uma vez; o gerador produz um item por vez, sob demanda. Se você vai percorrer a sequência uma única vez, o gerador economiza muita memória. Se precisa de índice, tamanho ou reutilizar os itens várias vezes, a lista é melhor.
O que significa o maxsize do lru_cache?
É quantos resultados o cache guarda. Com um número, ele mantém os mais recentes e descarta os antigos quando enche (o LRU do nome, menos usado recentemente). Com None, guarda todos sem limite. Para funções com muitas entradas diferentes, um limite evita que o cache cresça sem parar.
str.join é sempre melhor que concatenar com +?
Para juntar muitos pedaços, sim, porque evita as cópias repetidas do +. Para colar duas ou três strings soltas, o + é claro e o ganho seria irrelevante. A regra vale mesmo dentro de laços que constroem uma string grande: acumule os pedaços e junte uma vez com str.join.
Geradores servem só para economizar memória?
Economizar memória é o principal, mas eles também permitem trabalhar com sequências infinitas e montar pipelines de processamento que consomem dados aos poucos. Como só calculam o próximo valor quando pedido, um gerador pode representar um fluxo sem fim sem nunca estourar a memória.

Fontes

Seu progresso fica salvo neste aparelho. Assinantes sincronizam entre os aparelhos.