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

Exercícios

  1. Crie uma classe Livro com atributos titulo e autor. Implemente __str__ e __repr__ para que repr(livro) retorne algo como Livro('Título', 'Autor') e str(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))
    
  2. Implemente a classe Pessoa com atributos cpf e nome. 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))  # True
    
  3. Crie uma classe Cesta que 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))  # 2
    
  4. Sobrecarregue o operador + para a classe Ponto (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)
    
  5. Implemente a classe Fracao que representa uma fração (numerador e denominador). Sobrecarregue os operadores + e * para somar e multiplicar frações. Dica: use o módulo fractions ou 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)