Métodos mágicos (dunder)
Esta aula explora os métodos mágicos (dunder) em Python, que permitem personalizar o comportamento de objetos para operações como representação, comparação, tamanho e sobrecarga de operadores. Serão abordados __str__, __repr__, __eq__, __hash__, __len__ e operadores aritméticos, com exemplos práticos e exercícios.
Os métodos mágicos, também conhecidos como métodos dunder (dupla sublinhado), são métodos especiais em Python que permitem que objetos definam seu comportamento para operações padrão da linguagem. Eles são chamados automaticamente em contextos como impressão, comparação, iteração e operadores aritméticos. Compreender esses métodos é essencial para criar classes que se integram perfeitamente ao ecossistema Python.
Nesta aula, vamos explorar os principais métodos mágicos, começando pela representação de objetos (__str__ e __repr__), passando por comparação e hash (__eq__ e __hash__), tamanho (__len__) e, finalmente, a sobrecarga de operadores. Cada seção inclui exemplos práticos para consolidar o aprendizado.
__str__ e __repr__
Os métodos __str__ e __repr__ controlam como um objeto é representado como string. __repr__ deve retornar uma string que, idealmente, possa recriar o objeto (ou seja, uma representação 'oficial'). __str__ é usado pelo print() e str(), fornecendo uma representação 'informal' e legível para o usuário.
Se __str__ não for definido, Python usa __repr__ como fallback. Na prática, é uma boa prática definir pelo menos __repr__ para depuração.
class Pessoa:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
def __repr__(self):
return f"Pessoa('{self.nome}', {self.idade})"
def __str__(self):
return f"{self.nome} ({self.idade} anos)"
p = Pessoa("Ana", 30)
print(repr(p)) # Pessoa('Ana', 30)
print(str(p)) # Ana (30 anos)
print(p) # Ana (30 anos) - usa __str__
Note que __repr__ retorna uma string que poderia ser usada para recriar o objeto (embora não seja obrigatório). Já __str__ é mais amigável.
__eq__ e __hash__
__eq__ define o comportamento do operador ==. Por padrão, compara identidade (mesmo objeto). __hash__ é usado em estruturas como dicionários e conjuntos; objetos que são iguais devem ter o mesmo hash. Se você implementar __eq__, o Python automaticamente define __hash__ como None (tornando o objeto não hasheável), a menos que você também implemente __hash__.
class Ponto:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Ponto):
return NotImplemented
return self.x == other.x and self.y == other.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) == hash(p2)) # True
# Agora Ponto pode ser usado em conjuntos
s = {p1, p2}
print(len(s)) # 1 (pois são iguais)
Se você quiser objetos mutáveis, evite definir __hash__ ou torne-os imutáveis. A regra é: objetos iguais devem ter o mesmo hash, e o hash não deve mudar durante a vida do objeto.
__len__
O método __len__ deve retornar um inteiro não negativo representando o 'tamanho' do objeto. É chamado pela função len().
class Fila:
def __init__(self):
self.itens = []
def enfileirar(self, item):
self.itens.append(item)
def desenfileirar(self):
return self.itens.pop(0)
def __len__(self):
return len(self.itens)
f = Fila()
f.enfileirar(1)
f.enfileirar(2)
print(len(f)) # 2
Além de len(), __len__ também é usado em contextos booleanos: se __len__ retornar 0, o objeto é considerado False; caso contrário, True.
Sobrecarga de operadores
Sobrecarga de operadores permite que objetos definam comportamento para operadores aritméticos, de comparação, etc. Cada operador tem seu método correspondente: __add__ para +, __sub__ para -, __mul__ para *, __truediv__ para /, __floordiv__ para //, __mod__ para %, __pow__ para **, __neg__ para unário -, entre outros. Também há métodos para operadores de comparação: __lt__, __le__, __gt__, __ge__, __eq__, __ne__.
class Vetor:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vetor({self.x}, {self.y})"
def __add__(self, other):
if not isinstance(other, Vetor):
return NotImplemented
return Vetor(self.x + other.x, self.y + other.y)
def __sub__(self, other):
if not isinstance(other, Vetor):
return NotImplemented
return Vetor(self.x - other.x, self.y - other.y)
def __mul__(self, escalar):
if not isinstance(escalar, (int, float)):
return NotImplemented
return Vetor(self.x * escalar, self.y * escalar)
def __rmul__(self, escalar):
# Para multiplicação escalar à esquerda (ex: 3 * v)
return self.__mul__(escalar)
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)
print(3 * v1) # Vetor(3, 6) (graças a __rmul__)
É importante retornar NotImplemented quando a operação não é suportada para o tipo do operando, permitindo que Python tente o método reverso ou levante TypeError adequadamente.
Boas práticas e observações finais
Ao implementar métodos mágicos, mantenha a consistência: se __eq__ é definido, considere também __hash__ (a menos que o objeto seja mutável). Para sobrecarga de operadores, sempre retorne NotImplemented em vez de levantar TypeError diretamente, para permitir que o outro operando tenha chance de implementar a operação. Documente o comportamento esperado. Lembre-se de que métodos como __len__ devem ser rápidos e sem efeitos colaterais.
Referências
- Documentação oficial: Métodos especiais (Python)
- repr() - Documentação Python
- hash() - Documentação Python
- Real Python: Python Magic Methods
- GeeksforGeeks: Dunder/Magic Methods in Python
Exercícios
Crie uma classe
Livrocom atributostituloeautor. Implemente__str__e__repr__para querepr(livro)retorne algo comoLivro('Título', 'Autor')estr(livro)retorne"Título por Autor".✓ Resposta:class Livro: def __init__(self, titulo, autor): self.titulo = titulo self.autor = autor def __repr__(self): return f"Livro('{self.titulo}', '{self.autor}')" def __str__(self): return f"{self.titulo} por {self.autor}" livro = Livro("1984", "George Orwell") print(repr(livro)) print(str(livro))Implemente a classe
Pessoacom atributoscpfenome. Faça com que duas pessoas sejam consideradas iguais se tiverem o mesmo CPF. Implemente__hash__adequadamente.✓ Resposta:class Pessoa: def __init__(self, cpf, nome): self.cpf = cpf self.nome = nome def __eq__(self, other): if not isinstance(other, Pessoa): return NotImplemented return self.cpf == other.cpf def __hash__(self): return hash(self.cpf) p1 = Pessoa("123", "Ana") p2 = Pessoa("123", "Maria") # mesmo CPF print(p1 == p2) # True print(hash(p1) == hash(p2)) # TrueCrie uma classe
Cestaque armazena itens em uma lista. Implemente__len__para retornar a quantidade de itens.✓ Resposta:class Cesta: def __init__(self): self.itens = [] def adicionar(self, item): self.itens.append(item) def __len__(self): return len(self.itens) c = Cesta() c.adicionar("maçã") c.adicionar("banana") print(len(c)) # 2Sobrecarregue o operador
+para a classePonto(com atributos x e y) de forma que some dois pontos, retornando um novo Ponto.✓ Resposta:class Ponto: def __init__(self, x, y): self.x = x self.y = y def __add__(self, other): if not isinstance(other, Ponto): return NotImplemented return Ponto(self.x + other.x, self.y + other.y) def __repr__(self): return f"Ponto({self.x}, {self.y})" p1 = Ponto(1, 2) p2 = Ponto(3, 4) print(p1 + p2) # Ponto(4, 6)Implemente a classe
Fracaoque representa uma fração (numerador e denominador). Sobrecarregue os operadores+e*para somar e multiplicar frações. Dica: use o módulofractionsou implemente manualmente.✓ Resposta:class Fracao: def __init__(self, num, den): self.num = num self.den = den def __add__(self, other): if not isinstance(other, Fracao): return NotImplemented novo_num = self.num * other.den + other.num * self.den novo_den = self.den * other.den return Fracao(novo_num, novo_den) def __mul__(self, other): if not isinstance(other, Fracao): return NotImplemented return Fracao(self.num * other.num, self.den * other.den) def __repr__(self): return f"{self.num}/{self.den}" f1 = Fracao(1, 2) f2 = Fracao(2, 3) print(f1 + f2) # 7/6 print(f1 * f2) # 2/6 (ou 1/3 simplificado, mas sem simplificação)