Métodos mágicos (dunder)
Nesta aula, exploramos os métodos mágicos (dunder) em Python, que permitem personalizar o comportamento das classes. Aprendemos a implementar representações de objetos com __str__ e __repr__, comparar e hashear com __eq__ e __hash__, definir o tamanho com __len__ e sobrecarregar operadores aritméticos e de comparação.
Os métodos mágicos, também conhecidos como métodos dunder (double underscore), são métodos especiais em Python que começam e terminam com dois sublinhados. Eles permitem que objetos definidos pelo usuário se comportem de maneira semelhante aos tipos embutidos, como inteiros, strings ou listas. Ao implementar esses métodos, podemos personalizar como nossos objetos são representados, comparados, somados, iterados, entre outras operações.
Nesta aula, vamos mergulhar nos principais métodos mágicos que você usará no dia a dia. Entender esses métodos é fundamental para escrever código Python idiomático e elegante, além de facilitar a integração com funcionalidades da linguagem, como funções embutidas e operadores.
__str__ e __repr__
Os métodos __str__ e __repr__ definem como um objeto é convertido em string. A diferença principal é que __str__ é usado para representações amigáveis ao usuário, enquanto __repr__ é usado para representações que idealmente podem recriar o objeto ou que são mais técnicas, voltadas ao desenvolvedor.
Quando você usa print(objeto) ou str(objeto), o Python procura pelo método __str__. Se ele não estiver definido, usa __repr__ como fallback. Já repr(objeto) e o console interativo usam sempre __repr__.
Vamos ver um exemplo com uma classe Pessoa:
class Pessoa:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
def __str__(self):
return f"Pessoa: {self.nome} ({self.idade} anos)"
def __repr__(self):
return f"Pessoa('{self.nome}', {self.idade})"
p = Pessoa("Ana", 30)
print(p) # Pessoa: Ana (30 anos)
print(str(p)) # Pessoa: Ana (30 anos)
print(repr(p)) # Pessoa('Ana', 30)
É uma boa prática definir __repr__ sempre, pois é útil para depuração. Se possível, faça com que __repr__ retorne uma string que recrie o objeto, como mostrado acima.
__eq__ e __hash__
O método __eq__ define o comportamento do operador de igualdade (==). Por padrão, dois objetos são iguais se forem o mesmo objeto (mesma identidade). Ao implementar __eq__, podemos comparar atributos.
O método __hash__ retorna um inteiro que representa o objeto, usado em conjuntos (sets) e como chave em dicionários. Se você definir __eq__ sem definir __hash__, o Python define __hash__ como None, tornando o objeto não-hashável. Isso é feito para manter a consistência: objetos iguais devem ter o mesmo hash.
Exemplo:
class Ponto:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, outro):
if not isinstance(outro, Ponto):
return NotImplemented
return self.x == outro.x and self.y == outro.y
def __hash__(self):
return hash((self.x, self.y))
p1 = Ponto(1, 2)
p2 = Ponto(1, 2)
print(p1 == p2) # True
print(hash(p1)) # 3713081631934410656 (exemplo)
Se você não precisa que os objetos sejam usados em sets ou como chaves, pode definir __eq__ e deixar __hash__ como None (atribua __hash__ = None) para torná-los não-hasháveis, o que evita erros sutis.
__len__
O método __len__ permite que seu objeto seja usado com a função embutida len(). Ele deve retornar um inteiro não negativo.
Exemplo com uma classe Fila:
class Fila:
def __init__(self, itens):
self.itens = list(itens)
def __len__(self):
return len(self.itens)
fila = Fila([1, 2, 3])
print(len(fila)) # 3
Implementar __len__ também permite que o objeto seja usado em contextos booleanos: se len() retornar 0, o objeto é considerado False em condições.
Sobrecarga de operadores
A sobrecarga de operadores permite que os operadores aritméticos, de comparação e outros funcionem com objetos definidos pelo usuário. Os métodos mágicos correspondentes são:
__add__para+__sub__para-__mul__para*__truediv__para/__lt__,__le__,__gt__,__ge__para comparações
Exemplo com uma classe Vetor:
class Vetor:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, outro):
if not isinstance(outro, Vetor):
return NotImplemented
return Vetor(self.x + outro.x, self.y + outro.y)
def __sub__(self, outro):
if not isinstance(outro, Vetor):
return NotImplemented
return Vetor(self.x - outro.x, self.y - outro.y)
def __mul__(self, escalar):
return Vetor(self.x * escalar, self.y * escalar)
def __repr__(self):
return f"Vetor({self.x}, {self.y})"
v1 = Vetor(1, 2)
v2 = Vetor(3, 4)
print(v1 + v2) # Vetor(4, 6)
print(v1 - v2) # Vetor(-2, -2)
print(v1 * 3) # Vetor(3, 6)
Note que retornamos NotImplemented quando o tipo não é suportado, permitindo que o Python tente a operação reversa ou levante um erro adequado.
Boas práticas
Ao implementar métodos mágicos, siga estas recomendações:
- Sempre retorne
NotImplementedem operadores binários quando o tipo não for suportado, em vez de levantar exceção diretamente. - Mantenha
__repr__informativo e, se possível, que recrie o objeto. - Se implementar
__eq__e quiser que o objeto seja hashável, implemente também__hash__. - Evite implementar muitos métodos mágicos desnecessariamente; apenas os que fizerem sentido para a sua classe.
Referências
- Python Data Model (documentação oficial)
- Python Magic Methods (Real Python)
- repr() na documentação oficial
- len() na documentação oficial
- Diferença entre __str__ e __repr__ (Stack Overflow)
- Dunder (glossário Python)
Exercícios
- Implemente uma classe
Livrocom atributostitulo,autoreano. Defina__str__para exibir "titulo por autor (ano)" e__repr__para retornar uma expressão que recrie o objeto. Teste com um exemplo. - Crie uma classe
Datacom atributosdia,meseano. Implemente__eq__para comparar datas e__hash__para que elas possam ser usadas em sets. Teste com duas datas iguais e diferentes. - Defina uma classe
Palavraque recebe uma string e implemente__len__para retornar o número de caracteres. Teste com uma palavra. - Crie uma classe
Dinheiroque armazena um valor em centavos (int). Implemente__add__e__sub__para operar com outroDinheiroou com um inteiro (representando centavos). Teste somando e subtraindo. - Implemente uma classe
Fracaocom numerador e denominador. Sobrecarregue os operadores__mul__e__truediv__para multiplicar e dividir frações. Simplifique o resultado (pode usar a funçãomath.gcd). Teste com exemplos.
class Livro:
def __init__(self, titulo, autor, ano):
self.titulo = titulo
self.autor = autor
self.ano = ano
def __str__(self):
return f"{self.titulo} por {self.autor} ({self.ano})"
def __repr__(self):
return f"Livro('{self.titulo}', '{self.autor}', {self.ano})"
livro = Livro("O Senhor dos Anéis", "J.R.R. Tolkien", 1954)
print(livro) # O Senhor dos Anéis por J.R.R. Tolkien (1954)
print(repr(livro)) # Livro('O Senhor dos Anéis', 'J.R.R. Tolkien', 1954)class Data:
def __init__(self, dia, mes, ano):
self.dia = dia
self.mes = mes
self.ano = ano
def __eq__(self, outro):
if not isinstance(outro, Data):
return NotImplemented
return (self.dia, self.mes, self.ano) == (outro.dia, outro.mes, outro.ano)
def __hash__(self):
return hash((self.dia, self.mes, self.ano))
d1 = Data(1, 1, 2024)
d2 = Data(1, 1, 2024)
d3 = Data(2, 1, 2024)
print(d1 == d2) # True
print(d1 == d3) # False
print(len({d1, d2, d3})) # 2class Palavra:
def __init__(self, texto):
self.texto = texto
def __len__(self):
return len(self.texto)
p = Palavra("Python")
print(len(p)) # 6class Dinheiro:
def __init__(self, centavos):
self.centavos = centavos
def __add__(self, outro):
if isinstance(outro, Dinheiro):
return Dinheiro(self.centavos + outro.centavos)
elif isinstance(outro, int):
return Dinheiro(self.centavos + outro)
return NotImplemented
def __sub__(self, outro):
if isinstance(outro, Dinheiro):
return Dinheiro(self.centavos - outro.centavos)
elif isinstance(outro, int):
return Dinheiro(self.centavos - outro)
return NotImplemented
def __repr__(self):
return f"Dinheiro({self.centavos})"
d1 = Dinheiro(150)
d2 = Dinheiro(100)
print(d1 + d2) # Dinheiro(250)
print(d1 - d2) # Dinheiro(50)
print(d1 + 50) # Dinheiro(200)import math
class Fracao:
def __init__(self, numerador, denominador):
if denominador == 0:
raise ValueError("Denominador não pode ser zero")
self.numerador = numerador
self.denominador = denominador
self._simplificar()
def _simplificar(self):
g = math.gcd(self.numerador, self.denominador)
self.numerador //= g
self.denominador //= g
def __mul__(self, outro):
if not isinstance(outro, Fracao):
return NotImplemented
return Fracao(self.numerador * outro.numerador, self.denominador * outro.denominador)
def __truediv__(self, outro):
if not isinstance(outro, Fracao):
return NotImplemented
return Fracao(self.numerador * outro.denominador, self.denominador * outro.numerador)
def __repr__(self):
return f"Fracao({self.numerador}, {self.denominador})"
f1 = Fracao(1, 2)
f2 = Fracao(3, 4)
print(f1 * f2) # Fracao(3, 8)
print(f1 / f2) # Fracao(2, 3)