Introdução aos Dunder Methods: A Magia por Trás dos Nomes Especiais
Os "dunder methods" (abreviação de "double underscore methods") são métodos especiais em Python que começam e terminam com dois underscores, como __init__, __str__ e __eq__. Eles permitem que seus objetos se comportem como tipos nativos de Python, respondendo a operações comuns como impressão, comparação, adição e indexação. Não são métodos que você chama diretamente na maioria dos casos; em vez disso, Python os invoca automaticamente em resposta a certos eventos ou operadores. Entender esses métodos é fundamental para criar classes robustas, intuitivas e profissionais.
A filosofia por trás dos dunder methods está no conceito de "sobrecarga de operadores" ou "operator overloading". Quando você escreve print(objeto), Python automaticamente chama objeto.__str__(). Quando compara dois objetos com ==, ele chama __eq__(). Isso cria uma experiência coerente: seus objetos não parecem "estranhos" em relação aos tipos built-in como strings, listas e dicionários.
str vs repr: Entendendo a Representação de Objetos
O propósito de str e repr
Embora muitos iniciantes confundam __str__ e __repr__, eles têm propósitos distintos. O método __str__ deve retornar uma representação legível e "amigável" do objeto, destinada ao usuário final. Já __repr__ deve retornar uma representação técnica e sem ambiguidades, idealmente algo que o desenvolvedor possa usar para entender ou até reproduzir o objeto.
Uma boa regra prática: __str__ é para o usuário, __repr__ é para o desenvolvedor. Se você não implementar __str__, Python usará __repr__ como fallback. Se não implementar nenhum dos dois, você verá aquela saída padrão pouco útil: <__main__.Pessoa object at 0x7f8b8c0f9a30>.
Exemplo prático com uma classe Pessoa
class Pessoa:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
def __str__(self):
return f"{self.nome} tem {self.idade} anos"
def __repr__(self):
return f"Pessoa(nome='{self.nome}', idade={self.idade})"
# Testando
pessoa = Pessoa("Alice", 28)
print(pessoa) # Chama __str__: Alice tem 28 anos
print(repr(pessoa)) # Chama __repr__: Pessoa(nome='Alice', idade=28)
Note que __repr__ é mais técnico e inclui o construtor. Se você copiar e colar o resultado de repr(), pode recriar o objeto (em muitos casos). Isso é uma boa prática ao implementar __repr__.
Quando usar cada um
Use __str__ quando quiser uma mensagem clara e contextualizada para o usuário final. Use __repr__ quando estiver debugando, registrando logs técnicos ou quando precisa de uma forma não ambígua de representar o objeto. Uma dica profissional: sempre implemente __repr__, pois ele será usado em listas e dicionários durante a depuração.
eq e Comparação: Definindo Igualdade
O conceito de igualdade em objetos
Por padrão, dois objetos só são iguais se forem exatamente a mesma instância em memória (identidade). Mas frequentemente queremos comparar objetos pelo seu conteúdo, não pela sua localização em memória. O método __eq__ permite que você defina quando dois objetos devem ser considerados iguais. Ao implementá-lo, você também habilita automaticamente != (que usa __ne__, mas Python fornece uma implementação padrão baseada em __eq__).
Relacionados a __eq__, existem outros dunder methods de comparação: __lt__ (menor que), __le__ (menor ou igual), __gt__ (maior que), __ge__ (maior ou igual). Juntos, formam a interface de comparação do Python.
Exemplo: Implementando eq para uma classe Produto
class Produto:
def __init__(self, codigo, nome, preco):
self.codigo = codigo
self.nome = nome
self.preco = preco
def __eq__(self, outro):
# Verifica se é da mesma classe
if not isinstance(outro, Produto):
return False
# Compara pelo código (pode ser outro critério)
return self.codigo == outro.codigo
def __repr__(self):
return f"Produto(codigo={self.codigo}, nome='{self.nome}', preco={self.preco})"
# Testando
p1 = Produto(101, "Notebook", 3500.00)
p2 = Produto(101, "Notebook", 3500.00)
p3 = Produto(102, "Mouse", 50.00)
print(p1 == p2) # True (mesmo código)
print(p1 == p3) # False (códigos diferentes)
print(p1 != p3) # True (usa __ne__, que inverte __eq__)
Note que é boa prática começar __eq__ verificando o tipo com isinstance(). Isso evita comparações erradas e erros silenciosos. Se o outro objeto não for do tipo esperado, retorne False em vez de lançar uma exceção.
Usando eq em contextos práticos
Uma vez que você implemente __eq__, pode usar seus objetos em estruturas de dados que dependem de comparação. Por exemplo, verificar se um objeto está em uma lista ou encontrar índices:
lista_produtos = [p1, p2, p3]
print(p1 in lista_produtos) # True
print(lista_produtos.index(p1)) # 0
# Remover um produto pela igualdade
lista_produtos.remove(p2) # Remove p2 porque p2 == p1
print(len(lista_produtos)) # 2
Explorando Outros Dunder Methods Essenciais
init e del: Construtor e Destrutor
O método __init__ é chamado quando você cria uma instância da classe (depois que __new__ a aloca em memória). É onde você inicializa os atributos. O método __del__ é chamado quando o objeto é destruído (garbage collection), útil para limpar recursos como arquivos abertos ou conexões de banco de dados. Porém, não confie demais em __del__ para cleanup crítico; use context managers (with) quando possível.
class Arquivo:
def __init__(self, nome):
self.nome = nome
self.arquivo = open(nome, 'r')
print(f"Arquivo {nome} aberto")
def __del__(self):
self.arquivo.close()
print(f"Arquivo {self.nome} fechado")
# Com context manager (melhor prática):
class ArquivoSeguro:
def __init__(self, nome):
self.nome = nome
self.arquivo = open(nome, 'r')
def __enter__(self):
return self.arquivo
def __exit__(self, exc_type, exc_val, exc_tb):
self.arquivo.close()
return False
len, getitem e setitem: Comportamento de Sequências
Esses métodos permitem que seus objetos se comportem como listas ou dicionários. __len__ retorna o tamanho, __getitem__ permite acesso por índice (ou chave), e __setitem__ permite atribuição por índice.
class Playlist:
def __init__(self, nome):
self.nome = nome
self.musicas = []
def adicionar(self, musica):
self.musicas.append(musica)
def __len__(self):
return len(self.musicas)
def __getitem__(self, indice):
return self.musicas[indice]
def __setitem__(self, indice, musica):
self.musicas[indice] = musica
def __repr__(self):
return f"Playlist('{self.nome}', {len(self)} músicas)"
# Usando como uma sequência
playlist = Playlist("Rock Clássico")
playlist.adicionar("Bohemian Rhapsody")
playlist.adicionar("Stairway to Heaven")
print(len(playlist)) # 2
print(playlist[0]) # Bohemian Rhapsody
playlist[1] = "Hotel California"
call: Tornando objetos invocáveis
O método __call__ permite que você chame um objeto como se fosse uma função. Isso é útil para criar decoradores, callables e classes que agem como funções parametrizadas.
class Multiplicador:
def __init__(self, fator):
self.fator = fator
def __call__(self, valor):
return valor * self.fator
vezes_3 = Multiplicador(3)
print(vezes_3(5)) # 15
print(vezes_3(10)) # 30
# Usando em um contexto mais real: callback parametrizado
callbacks = [Multiplicador(2), Multiplicador(5), Multiplicador(10)]
for callback in callbacks:
print(callback(7)) # 14, 35, 70
add, sub e Operadores Aritméticos
Esses métodos permitem usar operadores como +, -, *, / com seus objetos. Útil para domínios como vetores, frações, moedas e quantidades.
class Vetor2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, outro):
if isinstance(outro, Vetor2D):
return Vetor2D(self.x + outro.x, self.y + outro.y)
return NotImplemented
def __sub__(self, outro):
if isinstance(outro, Vetor2D):
return Vetor2D(self.x - outro.x, self.y - outro.y)
return NotImplemented
def __mul__(self, escalar):
if isinstance(escalar, (int, float)):
return Vetor2D(self.x * escalar, self.y * escalar)
return NotImplemented
def __repr__(self):
return f"Vetor2D({self.x}, {self.y})"
v1 = Vetor2D(1, 2)
v2 = Vetor2D(3, 4)
print(v1 + v2) # Vetor2D(4, 6)
print(v1 - v2) # Vetor2D(-2, -2)
print(v1 * 2) # Vetor2D(2, 4)
Note que retornar NotImplemented (não exceção!) permite que Python tente a operação reversa. Por exemplo, se você implementar __add__ e o primeiro operando não souber como somar com o segundo, Python tentará __radd__ no segundo operando.
Boas Práticas e Padrões Profissionais
Consistência entre eq e hash
Se você sobrescrever __eq__, deve considerar também __hash__. Objetos que são iguais devem ter o mesmo hash. Caso contrário, comportamentos inesperados ocorrem em sets e dicionários. A regra é: se a == b, então hash(a) == hash(b).
class Usuario:
def __init__(self, id, nome):
self.id = id
self.nome = nome
def __eq__(self, outro):
if not isinstance(outro, Usuario):
return False
return self.id == outro.id
def __hash__(self):
return hash(self.id)
# Agora usuários podem ser usados em sets e como chaves de dicionário
usuarios = {Usuario(1, "Alice"), Usuario(2, "Bob"), Usuario(1, "Alice_2")}
print(len(usuarios)) # 2, não 3, porque dois têm id=1
Retornar NotImplemented, não None ou False
Quando um dunder method não pode lidar com os tipos fornecidos, retorne NotImplemented em vez de False ou None. Isso permite que Python tente a operação reversa (como __radd__) ou lance um erro apropriado.
class Valor:
def __init__(self, quantia):
self.quantia = quantia
def __add__(self, outro):
if isinstance(outro, Valor):
return Valor(self.quantia + outro.quantia)
if isinstance(outro, (int, float)):
return Valor(self.quantia + outro)
return NotImplemented # Correto
v = Valor(10)
# v + "string" # Lançará TypeError apropriadamente
Type hints e documentação clara
Use type hints nos seus dunder methods. Isso melhora a legibilidade e permite que IDEs forneçam autocompletar correto.
from typing import Any
class Ponto:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def __eq__(self, outro: Any) -> bool:
if not isinstance(outro, Ponto):
return False
return self.x == outro.x and self.y == outro.y
def __str__(self) -> str:
return f"Ponto({self.x}, {self.y})"
def __repr__(self) -> str:
return f"Ponto(x={self.x}, y={self.y})"
Conclusão
Você aprendeu que os dunder methods são hooks que Python chama automaticamente em resposta a operadores e funções built-in. Implementá-los transforma suas classes de "objetos estranhos" em cidadãos de primeira classe no ecossistema Python. Lembre-se: __str__ é para humanos, __repr__ é para desenvolvedores. Sempre implemente __repr__ pelo menos. E quando define __eq__, pense em __hash__ também — eles devem ser consistentes para evitar surpresas desagradáveis com sets e dicionários.
A terceira lição é que o Python oferece uma riqueza de dunder methods além dos básicos. Explorar __len__, __getitem__, __add__ e __call__ abre possibilidades de design expressivo e pythônico. Estude-os conforme a necessidade da sua aplicação surgir, mas comece dominando os fundamentais aqui apresentados.