__slots__ e otimização de memória
Esta aula explora o uso de __slots__ em Python para otimizar o uso de memória em classes, explicando seu funcionamento, quando aplicá-lo, os trade-offs envolvidos e exemplos práticos. O aluno aprenderá a reduzir o consumo de RAM em aplicações com muitas instâncias, compreendendo as implicações de desempenho e flexibilidade.
Em Python, cada objeto criado a partir de uma classe padrão possui um dicionário (__dict__) que armazena seus atributos. Esse dicionário oferece grande flexibilidade, permitindo adicionar ou remover atributos dinamicamente, mas consome uma quantidade significativa de memória, especialmente quando você tem milhares ou milhões de instâncias. A aula de hoje apresenta o __slots__, um mecanismo que substitui o dicionário por uma estrutura fixa e mais compacta, resultando em economia de memória e, em alguns casos, ganho de velocidade.
Vamos mergulhar no funcionamento interno, nas situações em que o uso é vantajoso, nos custos dessa otimização e em exemplos práticos que ilustram o impacto real. Ao final, você terá um guia claro para decidir quando e como aplicar __slots__ em seus projetos.
O que faz
__slots__ é um atributo de classe que define uma lista fixa de nomes de atributos permitidos para as instâncias. Quando declarado, o Python não cria o dicionário __dict__ para cada instância; em vez disso, aloca espaço apenas para os atributos listados, usando um mecanismo interno de descritores. Isso reduz drasticamente o overhead de memória por objeto, pois elimina o custo do dicionário e suas tabelas de hash.
Além da economia de memória, o acesso a atributos pode se tornar mais rápido, pois não há necessidade de realizar uma busca no dicionário; o acesso é direto via índice. No entanto, a perda de flexibilidade é imediata: você não pode adicionar novos atributos a uma instância que não estejam em __slots__, a menos que inclua __dict__ explicitamente na lista.
Exemplo básico:
class Pessoa:
__slots__ = ['nome', 'idade']
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
p = Pessoa('Ana', 30)
print(p.nome) # Ana
# p.email = 'ana@example.com' # AttributeError: 'Pessoa' object has no attribute 'email'Note que tentar atribuir um atributo fora de __slots__ gera um AttributeError. Esse comportamento é intencional e é a base dos trade-offs que discutiremos.
Quando usar
O uso de __slots__ é recomendado em cenários onde o número de instâncias é muito grande e a economia de memória é crítica. Exemplos típicos incluem:
- Aplicações que processam grandes volumes de dados, como leitura de arquivos CSV ou bancos de dados, onde cada linha é representada por um objeto.
- Simulações numéricas ou científicas que criam milhões de partículas ou nós.
- Jogos ou gráficos em tempo real, onde cada entidade (inimigo, projétil) é uma instância.
- Cache em memória de objetos imutáveis ou semi-imutáveis.
Além disso, se você tem uma classe bem definida, com atributos conhecidos em tempo de escrita, e não precisa de atributos dinâmicos, o uso de __slots__ é uma boa prática. Muitas bibliotecas de alto desempenho, como numpy internamente ou dataclasses (com slots=True a partir do Python 3.10), utilizam esse mecanismo.
Para medir a economia, você pode comparar o uso de memória com e sem __slots__ usando sys.getsizeof ou ferramentas como tracemalloc. Em um exemplo com um milhão de instâncias, a diferença pode ser de dezenas de megabytes.
Trade-offs
A principal desvantagem do __slots__ é a perda de flexibilidade. Não é possível adicionar novos atributos a instâncias, o que pode ser problemático em código que depende de monkey-patching ou de atributos dinâmicos. Além disso, herança pode se tornar mais complexa: se uma classe base define __slots__ e a subclasse não, a subclasse ainda terá um __dict__, anulando parte da economia. Para preservar a otimização, a subclasse também deve definir __slots__ (mesmo que vazio).
Outro ponto é que classes com __slots__ não suportam weakref por padrão, a menos que você inclua __weakref__ na lista. Isso pode impactar usos com weakref ou cache baseado em referências fracas. Além disso, a implementação de __slots__ pode causar problemas com alguns protocolos que esperam __dict__, como serialização com pickle (embora seja possível implementar métodos especiais para contornar).
Também é importante notar que o ganho de velocidade no acesso a atributos é geralmente pequeno e pode ser irrelevante na maioria das aplicações; o benefício dominante é a memória. Por fim, o código se torna menos flexível para evolução: adicionar um novo atributo exige alterar a definição da classe e pode quebrar código externo que dependia da ausência desse atributo.
Para mitigar alguns problemas, você pode incluir '__dict__' em __slots__ para manter a flexibilidade, mas isso reduz a economia de memória. A decisão deve ser baseada na análise do seu caso de uso específico.
Exemplos
Vamos explorar alguns exemplos práticos que demonstram o uso e os efeitos de __slots__.
Exemplo 1: Medindo economia de memória
import sys
class SemSlots:
def __init__(self, x, y):
self.x = x
self.y = y
class ComSlots:
__slots__ = ['x', 'y']
def __init__(self, x, y):
self.x = x
self.y = y
# Criar uma lista de instâncias
sem = [SemSlots(1, 2) for _ in range(100000)]
com = [ComSlots(1, 2) for _ in range(100000)]
# Tamanho médio por instância
print('Sem slots:', sys.getsizeof(sem[0])) # geralmente 56 bytes (objeto) + 56 bytes (dict)
print('Com slots:', sys.getsizeof(com[0])) # geralmente 48 bytes (objeto) + 0 (dict)
# Memória total aproximada (somente objetos)
print('Total sem slots:', sum(sys.getsizeof(obj) for obj in sem))
print('Total com slots:', sum(sys.getsizeof(obj) for obj in com))No Python 3.11, o tamanho de um objeto sem slots é tipicamente 56 bytes (incluindo o dict), enquanto com slots é 48 bytes. Para 100 mil instâncias, a economia é de aproximadamente 800 KB apenas nos objetos, mas o dict adicional consome muito mais. Na prática, a economia é maior.
Exemplo 2: Herança e __slots__
class Base:
__slots__ = ['a']
class Filha(Base):
__slots__ = ['b']
def __init__(self, a, b):
self.a = a
self.b = b
f = Filha(1, 2)
print(f.a, f.b)
# f.c = 3 # AttributeError
# Se a subclasse não definir __slots__, ela ganha um __dict__
class SemSlotsFilha(Base):
pass
s = SemSlotsFilha()
s.a = 1
s.c = 3 # Ok, porque tem __dict__
print(s.__dict__) # {'c': 3}Perceba que a classe SemSlotsFilha não herda a otimização, pois a ausência de __slots__ faz com que instâncias tenham __dict__. Para manter a economia, a subclasse deve declarar __slots__ (mesmo vazio).
Exemplo 3: Incluindo __dict__ para manter flexibilidade
class Flexivel:
__slots__ = ['x', '__dict__']
def __init__(self, x):
self.x = x
f = Flexivel(10)
f.y = 20 # Ok, pois __dict__ está em slots
print(f.y, f.x)
print(f.__dict__) # {'y': 20}Isso permite atributos extras, mas o objeto ainda economiza memória em relação a uma classe sem slots, pois o dicionário só é criado quando necessário.
Exemplo 4: weakref e __slots__
import weakref
class ComWeakref:
__slots__ = ['x', '__weakref__']
def __init__(self, x):
self.x = x
obj = ComWeakref(5)
ref = weakref.ref(obj)
print(ref) #
# Sem __weakref__, isso falharia
class SemWeakref:
__slots__ = ['x']
def __init__(self, x):
self.x = x
# obj2 = SemWeakref(5)
# ref2 = weakref.ref(obj2) # TypeError: cannot create weak reference to 'SemWeakref' object Se você precisar de referências fracas, inclua __weakref__ na lista.
Exemplo 5: Dataclasses com slots
from dataclasses import dataclass
@dataclass(slots=True)
class Ponto:
x: int
y: int
p = Ponto(1, 2)
print(p)
# p.z = 3 # AttributeErrorO Python 3.10+ permite criar dataclasses com slots diretamente, combinando a concisão das dataclasses com a eficiência de memória.
Boas práticas e observações finais
Use __slots__ quando você tiver certeza de que a classe não precisa de atributos dinâmicos e quando o número de instâncias for alto. Documente a decisão no código para que outros desenvolvedores entendam a restrição.
Lembre-se de que a economia de memória é o principal benefício; não espere grandes ganhos de velocidade. Em Python 3.3+, a implementação de __dict__ já é otimizada (os dicionários de instância são compartilhados em alguns casos), mas __slots__ ainda oferece vantagens em cenários extremos.
Se você precisar de flexibilidade, considere usar __dict__ em slots ou usar uma abordagem diferente, como namedtuple ou dataclasses sem slots. Teste com sys.getsizeof e tracemalloc para verificar se a otimização vale a pena no seu contexto.
Referências
- Python Data Model: __slots__
- sys.getsizeof na documentação oficial
- Documentação de dataclasses (parâmetro slots)
- Módulo weakref
- PEP 412 – Key-Sharing Dictionary
- Discussão no StackOverflow sobre __slots__
Exercícios
Crie uma classe
Ponto2Dsem__slots__e outra com__slots__para os atributosxey. Instancie 100.000 objetos de cada e compare o uso de memória total usandosys.getsizeofe somando tamanhos. Qual a diferença aproximada?✓ Resposta: O código abaixo mostra a diferença. Em Python 3.11, a classe sem slots ocupa cerca de 56 bytes por objeto (mais o dict que é 64 bytes), enquanto com slots ocupa 48 bytes. Para 100.000 objetos, a economia é de aproximadamente 7,2 MB (considerando dict). O resultado exato varia conforme a versão.import sys class SemSlots: def __init__(self, x, y): self.x = x self.y = y class ComSlots: __slots__ = ['x', 'y'] def __init__(self, x, y): self.x = x self.y = y sem = [SemSlots(1, 2) for _ in range(100000)] com = [ComSlots(1, 2) for _ in range(100000)] sem_size = sum(sys.getsizeof(obj) for obj in sem) com_size = sum(sys.getsizeof(obj) for obj in com) print(f'Sem slots: {sem_size} bytes') print(f'Com slots: {com_size} bytes') print(f'Diferença: {sem_size - com_size} bytes')Explique por que uma classe que herda de uma classe com
__slots__pode perder a otimização se não declarar__slots__também. Dê um exemplo.✓ Resposta: Quando uma subclasse não define__slots__, o Python automaticamente cria um__dict__para as instâncias da subclasse, anulando a economia de memória. Exemplo:
Para preservar a otimização, a subclasse deve declararclass Base: __slots__ = ['a'] class Sub(Base): pass s = Sub() print(s.__dict__) # {} # Isso mostra que existe um dict, mesmo vazio.__slots__ = []ou listar os novos atributos.Como você faria para permitir atributos dinâmicos em uma classe com
__slots__sem perder toda a economia? Escreva um exemplo.✓ Resposta: Inclua'__dict__'na lista de__slots__:
Isso mantém a flexibilidade, mas o objeto só cria o dict quando necessário, economizando memória para instâncias que não usam atributos extras.class Flexivel: __slots__ = ['x', '__dict__'] def __init__(self, x): self.x = x f = Flexivel(10) f.y = 20 # permitido print(f.__dict__) # {'y': 20}O que acontece se você tentar usar
weakrefem uma classe com__slots__sem incluir__weakref__? Como resolver?✓ Resposta: Sem__weakref__na lista de__slots__, a classe não suporta referências fracas, eweakref.ref(obj)lançaTypeError. Para resolver, adicione'__weakref__'aos slots:class ComWeakref: __slots__ = ['x', '__weakref__'] def __init__(self, x): self.x = x import weakref obj = ComWeakref(5) ref = weakref.ref(obj) # funcionaUsando
dataclasses, crie uma classeProdutocom camposnomeeprecoque utilizeslots=Truee demonstre que não é possível adicionar um atributo novo. Inclua o código.✓ Resposta:from dataclasses import dataclass @dataclass(slots=True) class Produto: nome: str preco: float p = Produto('Caneta', 1.50) print(p) # p.desconto = 0.1 # AttributeError: 'Produto' object has no attribute 'desconto'