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.
Ouvir o resumo desta aula
Um recap de cerca de 2 minutos na voz do Valim, para ouvir no trânsito ou na academia.
Ler a transcrição do resumo
Resumo da aula: Memoização com lru_cache e geradores para memória.
Os objetivos desta aula. 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.
Veja o essencial, parte por parte.
Não repita trabalho: lru_cache. lru_cache guarda o resultado de uma função e reusa na mesma entrada.
Geradores: processar muito sem gastar memória. Performance não é só tempo; memória também conta.
Armadilhas comuns de performance. Alguns padrões custam caro sem parecer.
Esse foi o resumo do essencial. Para se aprofundar, leia a aula completa e responda os exercícios.
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 cacheSem 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 totalA 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.
| Armadilha | Por que é lenta | O que fazer |
|---|---|---|
| s += texto num laço | Cada + copia a string toda (O(n ao quadrado)) | Acumular numa lista e usar str.join |
| item in lista num laço | Cada busca varre a lista (O(n)) | Converter para set uma vez e testar in |
| Recalcular fora do laço | Refaz o mesmo trabalho a cada volta | Calcular uma vez antes do laço |
| Carregar arquivo inteiro | Ocupa memória proporcional ao tamanho | Iterar 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.