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  # AttributeError

O 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

Exercícios

  1. Crie uma classe Ponto2D sem __slots__ e outra com __slots__ para os atributos x e y. Instancie 100.000 objetos de cada e compare o uso de memória total usando sys.getsizeof e 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')
  2. 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:

    class Base:
        __slots__ = ['a']
    
    class Sub(Base):
        pass
    
    s = Sub()
    print(s.__dict__)  # {}
    # Isso mostra que existe um dict, mesmo vazio.
    Para preservar a otimização, a subclasse deve declarar __slots__ = [] ou listar os novos atributos.
  3. 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__:

    class Flexivel:
        __slots__ = ['x', '__dict__']
        def __init__(self, x):
            self.x = x
    
    f = Flexivel(10)
    f.y = 20  # permitido
    print(f.__dict__)  # {'y': 20}
    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.
  4. O que acontece se você tentar usar weakref em uma classe com __slots__ sem incluir __weakref__? Como resolver?

    ✓ Resposta: Sem __weakref__ na lista de __slots__, a classe não suporta referências fracas, e weakref.ref(obj) lança TypeError. 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)  # funciona
  5. Usando dataclasses, crie uma classe Produto com campos nome e preco que utilize slots=True e 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'