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 NotImplemented em 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

Exercícios

  1. Implemente uma classe Livro com atributos titulo, autor e ano. Defina __str__ para exibir "titulo por autor (ano)" e __repr__ para retornar uma expressão que recrie o objeto. Teste com um exemplo.
  2. ✓ Resposta:
    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)
  3. Crie uma classe Data com atributos dia, mes e ano. Implemente __eq__ para comparar datas e __hash__ para que elas possam ser usadas em sets. Teste com duas datas iguais e diferentes.
  4. ✓ Resposta:
    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}))  # 2
  5. Defina uma classe Palavra que recebe uma string e implemente __len__ para retornar o número de caracteres. Teste com uma palavra.
  6. ✓ Resposta:
    class Palavra:
        def __init__(self, texto):
            self.texto = texto
    
        def __len__(self):
            return len(self.texto)
    
    p = Palavra("Python")
    print(len(p))  # 6
  7. Crie uma classe Dinheiro que armazena um valor em centavos (int). Implemente __add__ e __sub__ para operar com outro Dinheiro ou com um inteiro (representando centavos). Teste somando e subtraindo.
  8. ✓ Resposta:
    class 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)
  9. Implemente uma classe Fracao com numerador e denominador. Sobrecarregue os operadores __mul__ e __truediv__ para multiplicar e dividir frações. Simplifique o resultado (pode usar a função math.gcd). Teste com exemplos.
  10. ✓ Resposta:
    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)