Métodos mágicos: str, repr, len, eq

1. Introdução aos Métodos Mágicos (Dunder Methods)

Métodos mágicos, também conhecidos como dunder methods (double underscore methods), são métodos especiais em Python que começam e terminam com dois underscores. Eles permitem que objetos definidos pelo usuário se comportem de forma semelhante aos tipos nativos do Python, respondendo a operações como print(), len(), ==, entre outras.

O termo "mágico" vem do fato de que esses métodos são chamados automaticamente pelo interpretador Python em contextos específicos, sem que o programador precise invocá-los explicitamente. Por exemplo, quando você usa print(objeto), o Python automaticamente procura pelo método __str__ do objeto.

Neste artigo, exploraremos quatro métodos mágicos fundamentais:
- __str__: representação amigável para usuários
- __repr__: representação técnica para desenvolvedores
- __len__: tamanho do objeto
- __eq__: comparação de igualdade

2. __str__ vs __repr__: Duas Faces da Representação

A principal diferença entre __str__ e __repr__ está no público-alvo:

  • __repr__: deve fornecer uma representação inequívoca e técnica do objeto, idealmente uma string que possa recriar o objeto. É usada para debugging e logs.
  • __str__: deve fornecer uma representação legível e amigável para usuários finais. É usada por print() e f-strings.

Na prática, quando você não define __str__, o Python usa __repr__ como fallback. Experimente no console interativo:

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(p)           # Saída: Ana, 30 anos
print(repr(p))     # Saída: Pessoa('Ana', 30)

3. Implementando __str__ na Prática

O método __str__ deve retornar uma string concisa e informativa. É ideal para classes que representam entidades do mundo real.

class Data:
    def __init__(self, dia, mes, ano):
        self.dia = dia
        self.mes = mes
        self.ano = ano

    def __str__(self):
        return f"{self.dia:02d}/{self.mes:02d}/{self.ano}"

data = Data(25, 12, 2024)
print(data)  # Saída: 25/12/2024

Boas práticas:
- Retorne strings curtas (máximo 1-2 linhas)
- Evite informações técnicas internas
- Se o objeto for complexo, foque nos atributos mais relevantes

4. Implementando __repr__ na Prática

A regra de ouro para __repr__ é: a string retornada deve, idealmente, ser capaz de recriar o objeto quando passada para eval().

class Ponto2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Ponto2D({self.x}, {self.y})"

p = Ponto2D(3, 5)
print(repr(p))  # Saída: Ponto2D(3, 5)

# Recriando o objeto a partir da representação
p2 = eval(repr(p))
print(p2.x, p2.y)  # Saída: 3 5

Cuidados com eval(): só use eval() com dados confiáveis. Em produção, prefira ast.literal_eval() ou use __repr__ apenas para debugging.

5. __len__: Tornando Objetos "Medíveis"

O método __len__ permite que objetos personalizados respondam à função len(). Deve retornar um inteiro não-negativo.

class Fila:
    def __init__(self):
        self._itens = []

    def enfileirar(self, item):
        self._itens.append(item)

    def desenfileirar(self):
        if self._itens:
            return self._itens.pop(0)
        raise IndexError("Fila vazia")

    def __len__(self):
        return len(self._itens)

fila = Fila()
fila.enfileirar("A")
fila.enfileirar("B")
fila.enfileirar("C")

print(len(fila))  # Saída: 3

# Integração com bool(): objetos vazios são False
if fila:
    print("Fila não está vazia")  # Esta linha executa

Importante: Python também usa __len__ indiretamente em bool(). Se __len__ retornar 0, o objeto é considerado False em contextos booleanos.

6. __eq__: Controlando a Igualdade entre Objetos

Por padrão, == compara identidade de objetos (mesmo que is). Para comparar por valor, implemente __eq__.

class Livro:
    def __init__(self, titulo, autor, isbn):
        self.titulo = titulo
        self.autor = autor
        self.isbn = isbn

    def __eq__(self, outro):
        if not isinstance(outro, Livro):
            return NotImplemented
        return self.isbn == outro.isbn

    def __repr__(self):
        return f"Livro('{self.titulo}', '{self.autor}', '{self.isbn}')"

livro1 = Livro("1984", "George Orwell", "978-8535902776")
livro2 = Livro("1984", "George Orwell", "978-8535902776")
livro3 = Livro("A Revolução dos Bichos", "George Orwell", "978-8535902950")

print(livro1 == livro2)  # True (mesmo ISBN)
print(livro1 == livro3)  # False (ISBNs diferentes)
print(livro1 is livro2)  # False (objetos diferentes)

Cuidados importantes:
- Retorne NotImplemented (não False) quando comparar com tipos incompatíveis
- Se implementar __eq__, o Python 3 automaticamente fornece __ne__ (!=)
- Objetos mutáveis com __eq__ não devem ser usados como chaves de dicionário sem implementar __hash__

7. Interação e Boas Práticas com os Métodos Mágicos

Vamos criar uma classe que utiliza todos os quatro métodos de forma consistente:

class ContaBancaria:
    def __init__(self, titular, saldo=0.0):
        self.titular = titular
        self._saldo = saldo
        self._transacoes = []

    def depositar(self, valor):
        self._saldo += valor
        self._transacoes.append(f"Depósito: +R${valor:.2f}")

    def sacar(self, valor):
        if valor > self._saldo:
            raise ValueError("Saldo insuficiente")
        self._saldo -= valor
        self._transacoes.append(f"Saque: -R${valor:.2f}")

    def __len__(self):
        return len(self._transacoes)

    def __eq__(self, outra):
        if not isinstance(outra, ContaBancaria):
            return NotImplemented
        return self.titular == outra.titular and self._saldo == outra._saldo

    def __repr__(self):
        return f"ContaBancaria('{self.titular}', {self._saldo:.2f})"

    def __str__(self):
        return f"Conta de {self.titular} - Saldo: R${self._saldo:.2f}"

# Uso prático
conta1 = ContaBancaria("Maria", 1000)
conta1.depositar(500)
conta1.sacar(200)

print(str(conta1))   # Saída: Conta de Maria - Saldo: R$1300.00
print(repr(conta1))  # Saída: ContaBancaria('Maria', 1300.00)
print(len(conta1))   # Saída: 2 (duas transações)

conta2 = ContaBancaria("Maria", 1300)
print(conta1 == conta2)  # True (mesmo titular e saldo)

Boas práticas:
- Mantenha consistência entre __str__, __repr__ e __eq__
- Use __len__ para objetos que representam coleções ou contêineres
- Sempre retorne NotImplemented em __eq__ para tipos incompatíveis

8. Conclusão e Próximos Passos

Os métodos mágicos __str__, __repr__, __len__ e __eq__ são ferramentas essenciais para criar classes Python que se integram naturalmente com a linguagem. Eles permitem que seus objetos personalizados:
- Sejam exibidos de forma amigável (__str__)
- Forneçam representações técnicas para debugging (__repr__)
- Respondam a len() (__len__)
- Sejam comparados por valor (__eq__)

Para aprofundar, explore tópicos como dataclasses, que geram automaticamente esses métodos, e outros métodos mágicos como __hash__, __lt__ e __getitem__. Dominar esses conceitos é fundamental para escrever código Python idiomático e elegante.

Referências