Dicas para reduzir consumo de memória em Python
Python é uma linguagem poderosa, mas seu consumo de memória pode se tornar um gargalo em aplicações que processam grandes volumes de dados ou rodam em ambientes com recursos limitados. Este artigo apresenta técnicas práticas para otimizar o uso de memória, desde o gerenciamento básico de objetos até estratégias avançadas de perfilamento.
1. Gerenciamento de Objetos e Referências
Uso de __slots__ para classes com muitos objetos
Por padrão, cada instância de classe em Python possui um dicionário __dict__ para armazenar atributos, o que consome memória significativa. Ao definir __slots__, você substitui o dicionário por uma estrutura de dados mais compacta.
class PontoSemSlots:
def __init__(self, x, y):
self.x = x
self.y = y
class PontoComSlots:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
# Teste de consumo
import sys
p1 = PontoSemSlots(10, 20)
p2 = PontoComSlots(10, 20)
print(f"Sem slots: {sys.getsizeof(p1)} bytes") # ~56 bytes
print(f"Com slots: {sys.getsizeof(p2)} bytes") # ~40 bytes
Para classes com milhares de instâncias, a economia de memória pode chegar a 30-40%.
Eliminação de referências circulares com weakref
Referências circulares impedem que o garbage collector libere memória automaticamente. O módulo weakref permite criar referências que não impedem a coleta.
import weakref
class No:
def __init__(self, valor):
self.valor = valor
self.proximo = None
# Evitando referência circular
no1 = No(1)
no2 = No(2)
no1.proximo = weakref.ref(no2) # Referência fraca
no2.proximo = weakref.ref(no1)
Liberação explícita com del e gc.collect()
Em situações críticas, você pode forçar a liberação de objetos:
import gc
dados_grandes = [i for i in range(10**7)]
del dados_grandes # Remove a referência
gc.collect() # Força coleta imediata
2. Otimização de Estruturas de Dados
Substituição de listas por array.array
Para dados numéricos homogêneos, array.array é muito mais eficiente:
import array
# Lista tradicional
lista = [i for i in range(100000)]
print(f"Lista: {sys.getsizeof(lista)} bytes")
# Array tipado
arr = array.array('i', range(100000)) # 'i' = inteiro com sinal
print(f"Array: {sys.getsizeof(arr)} bytes")
Dicionários compactos com chaves reduzidas
Dicionários com chaves longas consomem mais memória. Use enumerações ou tuplas como chaves:
# Ruim: chaves longas
dados_ruim = {
"nome_completo_usuario": "João",
"endereco_email_usuario": "joao@email.com"
}
# Bom: chaves curtas ou enumerações
from enum import Enum
class Campos(Enum):
NOME = 1
EMAIL = 2
dados_bom = {
Campos.NOME: "João",
Campos.EMAIL: "joao@email.com"
}
namedtuple e dataclasses com slots=True
from collections import namedtuple
from dataclasses import dataclass
# namedtuple (imutável e leve)
Ponto = namedtuple('Ponto', ['x', 'y'])
p = Ponto(10, 20)
# dataclass com slots (Python 3.10+)
@dataclass(slots=True)
class Pessoa:
nome: str
idade: int
3. Manipulação Eficiente de Strings e Bytes
Preferência por bytes e bytearray
Strings Unicode consomem mais memória que bytes. Para dados binários, use bytes:
# String Unicode (2-4 bytes por caractere)
texto = "Olá mundo!" # ~72 bytes
# Bytes (1 byte por caractere)
binario = b"Ola mundo!" # ~50 bytes
Uso de io.StringIO e io.BytesIO
Evite criar strings intermediárias ao construir grandes blocos de texto:
from io import StringIO
# Ruim: concatenação cria novas strings
texto = ""
for i in range(10000):
texto += f"Linha {i}\n" # Cria 10000 objetos string
# Bom: buffer em memória
buffer = StringIO()
for i in range(10000):
buffer.write(f"Linha {i}\n")
texto = buffer.getvalue()
Interpolação com f-strings
f-strings são mais eficientes que concatenação com +:
nome = "Python"
versao = 3.12
# Ruim: concatenação
mensagem = "Versão " + str(versao) + " do " + nome
# Bom: f-string
mensagem = f"Versão {versao} do {nome}"
4. Processamento de Dados em Lote (Streaming)
Leitura com iteradores e yield
Processe grandes arquivos linha a linha sem carregar tudo na memória:
def ler_arquivo_grande(nome_arquivo):
with open(nome_arquivo, 'r') as f:
for linha in f:
yield linha.strip()
# Uso
for linha in ler_arquivo_grande("dados.csv"):
processar(linha)
Geradores em vez de listas completas
# Ruim: cria lista completa
quadrados = [x**2 for x in range(1000000)]
# Bom: gerador lazy
quadrados = (x**2 for x in range(1000000))
itertools para evitar alocações intermediárias
from itertools import chain, islice
# Evitando listas intermediárias
dados = chain(iter1, iter2, iter3) # Não cria lista
primeiros_100 = islice(dados, 100) # Apenas 100 itens
5. Gerenciamento de Memória com Bibliotecas Externas
Configuração de limites com resource.setrlimit (Unix)
import resource
# Limitar memória virtual a 500 MB
resource.setrlimit(resource.RLIMIT_AS, (500 * 1024 * 1024, -1))
Uso de memory_profiler
# Instalar: pip install memory-profiler
from memory_profiler import profile
@profile
def funcao_com_vazamento():
dados = [i for i in range(10**6)]
return sum(dados)
funcao_com_vazamento()
numpy vs listas nativas
Para arrays grandes, NumPy é drasticamente mais eficiente:
import numpy as np
# Lista nativa: ~8 MB para 1 milhão de floats
lista = [float(i) for i in range(10**6)]
# NumPy: ~8 MB para 1 milhão de floats (mas com muito menos overhead)
arr = np.arange(10**6, dtype=np.float64)
6. Técnicas Avançadas com Python Nativo
sys.intern para strings duplicadas
Quando muitas strings idênticas são criadas, intern as reutiliza:
import sys
# Sem intern: cada string é um objeto diferente
nomes1 = [sys.intern("João") for _ in range(1000)]
nomes2 = [sys.intern("Maria") for _ in range(1000)]
# Com intern: apenas 2 objetos string
nomes_internados = [sys.intern("João") for _ in range(1000)]
Compactação com struct.pack e struct.unpack
Para armazenar dados binários de forma compacta:
import struct
# Empacotar dados em bytes
dados = struct.pack('iif', 10, 20, 3.14) # 12 bytes
print(dados) # b'\n\x00\x00\x00\x14\x00\x00\x00\xc3\xf5H@'
# Desempacotar
x, y, z = struct.unpack('iif', dados)
__slots__ em heranças múltiplas
class Base:
__slots__ = ('x',)
class Derivada(Base):
__slots__ = ('y',) # Funciona se Base também tem __slots__
obj = Derivada()
obj.x = 10
obj.y = 20
7. Monitoramento e Perfilamento de Memória
Ferramentas essenciais
# tracemalloc - Rastreamento de alocações
import tracemalloc
tracemalloc.start()
# ... código sob análise ...
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for stat in top_stats[:10]:
print(stat)
# objgraph - Visualização de referências
# pip install objgraph
import objgraph
objgraph.show_most_common_types(limit=10)
# pympler - Análise detalhada
# pip install pympler
from pympler import asizeof
print(asizeof.asizeof([1, 2, 3])) # Tamanho real incluindo overhead
Identificação de objetos retidos em closures
def criar_closure():
dados_grandes = [i for i in range(10**6)]
def interna():
return sum(dados_grandes) # Retém dados_grandes na memória
return interna
# Solução: usar argumentos em vez de closure
def processar(dados):
return sum(dados)
Análise com sys.getsizeof e asizeof
import sys
from pympler import asizeof
# sys.getsizeof não mede objetos aninhados
lista = [[1, 2, 3] for _ in range(100)]
print(sys.getsizeof(lista)) # Apenas a lista externa
# asizeof mede recursivamente
print(asizeof.asizeof(lista)) # Tamanho total
Conclusão
A otimização de memória em Python requer uma combinação de boas práticas de codificação, escolha adequada de estruturas de dados e uso de ferramentas de monitoramento. Comece aplicando as técnicas de baixo custo, como __slots__ e geradores, e avance para soluções mais específicas conforme necessário. Lembre-se: a melhor otimização é aquela que resolve o problema real sem comprometer a legibilidade do código.
Referências
- Documentação oficial: slots — Guia completo sobre como usar slots para reduzir consumo de memória em classes Python.
- Python Memory Management (Real Python) — Tutorial abrangente sobre gerenciamento de memória, garbage collector e otimizações práticas.
- Documentação do módulo weakref — Referência oficial para criação de referências fracas e eliminação de referências circulares.
- memory-profiler no PyPI — Ferramenta para monitorar consumo de memória linha a linha em scripts Python.
- Documentação do módulo tracemalloc — Guia oficial para rastreamento de alocações de memória em Python.
- NumPy vs Listas: Comparação de Performance — Introdução ao NumPy com ênfase em eficiência de memória para arrays grandes.
- Pympler: Análise de Memória em Python — Biblioteca para medição precisa do consumo de memória, incluindo objetos aninhados.