Python Admin

Guia Completo de Métodos Especiais em Python: __str__, __repr__, __eq__ e Dunder Methods Já leu

Introdução aos Dunder Methods: A Magia por Trás dos Nomes Especiais Os "dunder methods" (abreviação de "double underscore methods") são métodos especiais em Python que começam e terminam com dois underscores, como , e . Eles permitem que seus objetos se comportem como tipos nativos de Python, respondendo a operações comuns como impressão, comparação, adição e indexação. Não são métodos que você chama diretamente na maioria dos casos; em vez disso, Python os invoca automaticamente em resposta a certos eventos ou operadores. Entender esses métodos é fundamental para criar classes robustas, intuitivas e profissionais. A filosofia por trás dos dunder methods está no conceito de "sobrecarga de operadores" ou "operator overloading". Quando você escreve , Python automaticamente chama . Quando compara dois objetos com , ele chama . Isso cria uma experiência coerente: seus objetos não parecem "estranhos" em relação aos tipos built-in como strings, listas e dicionários. str vs repr: Entendendo a Representação de Objetos O propósito de str

Introdução aos Dunder Methods: A Magia por Trás dos Nomes Especiais

Os "dunder methods" (abreviação de "double underscore methods") são métodos especiais em Python que começam e terminam com dois underscores, como __init__, __str__ e __eq__. Eles permitem que seus objetos se comportem como tipos nativos de Python, respondendo a operações comuns como impressão, comparação, adição e indexação. Não são métodos que você chama diretamente na maioria dos casos; em vez disso, Python os invoca automaticamente em resposta a certos eventos ou operadores. Entender esses métodos é fundamental para criar classes robustas, intuitivas e profissionais.

A filosofia por trás dos dunder methods está no conceito de "sobrecarga de operadores" ou "operator overloading". Quando você escreve print(objeto), Python automaticamente chama objeto.__str__(). Quando compara dois objetos com ==, ele chama __eq__(). Isso cria uma experiência coerente: seus objetos não parecem "estranhos" em relação aos tipos built-in como strings, listas e dicionários.

str vs repr: Entendendo a Representação de Objetos

O propósito de str e repr

Embora muitos iniciantes confundam __str__ e __repr__, eles têm propósitos distintos. O método __str__ deve retornar uma representação legível e "amigável" do objeto, destinada ao usuário final. Já __repr__ deve retornar uma representação técnica e sem ambiguidades, idealmente algo que o desenvolvedor possa usar para entender ou até reproduzir o objeto.

Uma boa regra prática: __str__ é para o usuário, __repr__ é para o desenvolvedor. Se você não implementar __str__, Python usará __repr__ como fallback. Se não implementar nenhum dos dois, você verá aquela saída padrão pouco útil: <__main__.Pessoa object at 0x7f8b8c0f9a30>.

Exemplo prático com uma classe Pessoa

class Pessoa:
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade

    def __str__(self):
        return f"{self.nome} tem {self.idade} anos"

    def __repr__(self):
        return f"Pessoa(nome='{self.nome}', idade={self.idade})"

# Testando
pessoa = Pessoa("Alice", 28)
print(pessoa)  # Chama __str__: Alice tem 28 anos
print(repr(pessoa))  # Chama __repr__: Pessoa(nome='Alice', idade=28)

Note que __repr__ é mais técnico e inclui o construtor. Se você copiar e colar o resultado de repr(), pode recriar o objeto (em muitos casos). Isso é uma boa prática ao implementar __repr__.

Quando usar cada um

Use __str__ quando quiser uma mensagem clara e contextualizada para o usuário final. Use __repr__ quando estiver debugando, registrando logs técnicos ou quando precisa de uma forma não ambígua de representar o objeto. Uma dica profissional: sempre implemente __repr__, pois ele será usado em listas e dicionários durante a depuração.

eq e Comparação: Definindo Igualdade

O conceito de igualdade em objetos

Por padrão, dois objetos só são iguais se forem exatamente a mesma instância em memória (identidade). Mas frequentemente queremos comparar objetos pelo seu conteúdo, não pela sua localização em memória. O método __eq__ permite que você defina quando dois objetos devem ser considerados iguais. Ao implementá-lo, você também habilita automaticamente != (que usa __ne__, mas Python fornece uma implementação padrão baseada em __eq__).

Relacionados a __eq__, existem outros dunder methods de comparação: __lt__ (menor que), __le__ (menor ou igual), __gt__ (maior que), __ge__ (maior ou igual). Juntos, formam a interface de comparação do Python.

Exemplo: Implementando eq para uma classe Produto

class Produto:
    def __init__(self, codigo, nome, preco):
        self.codigo = codigo
        self.nome = nome
        self.preco = preco

    def __eq__(self, outro):
        # Verifica se é da mesma classe
        if not isinstance(outro, Produto):
            return False
        # Compara pelo código (pode ser outro critério)
        return self.codigo == outro.codigo

    def __repr__(self):
        return f"Produto(codigo={self.codigo}, nome='{self.nome}', preco={self.preco})"

# Testando
p1 = Produto(101, "Notebook", 3500.00)
p2 = Produto(101, "Notebook", 3500.00)
p3 = Produto(102, "Mouse", 50.00)

print(p1 == p2)  # True (mesmo código)
print(p1 == p3)  # False (códigos diferentes)
print(p1 != p3)  # True (usa __ne__, que inverte __eq__)

Note que é boa prática começar __eq__ verificando o tipo com isinstance(). Isso evita comparações erradas e erros silenciosos. Se o outro objeto não for do tipo esperado, retorne False em vez de lançar uma exceção.

Usando eq em contextos práticos

Uma vez que você implemente __eq__, pode usar seus objetos em estruturas de dados que dependem de comparação. Por exemplo, verificar se um objeto está em uma lista ou encontrar índices:

lista_produtos = [p1, p2, p3]
print(p1 in lista_produtos)  # True
print(lista_produtos.index(p1))  # 0

# Remover um produto pela igualdade
lista_produtos.remove(p2)  # Remove p2 porque p2 == p1
print(len(lista_produtos))  # 2

Explorando Outros Dunder Methods Essenciais

init e del: Construtor e Destrutor

O método __init__ é chamado quando você cria uma instância da classe (depois que __new__ a aloca em memória). É onde você inicializa os atributos. O método __del__ é chamado quando o objeto é destruído (garbage collection), útil para limpar recursos como arquivos abertos ou conexões de banco de dados. Porém, não confie demais em __del__ para cleanup crítico; use context managers (with) quando possível.

class Arquivo:
    def __init__(self, nome):
        self.nome = nome
        self.arquivo = open(nome, 'r')
        print(f"Arquivo {nome} aberto")

    def __del__(self):
        self.arquivo.close()
        print(f"Arquivo {self.nome} fechado")

# Com context manager (melhor prática):
class ArquivoSeguro:
    def __init__(self, nome):
        self.nome = nome
        self.arquivo = open(nome, 'r')

    def __enter__(self):
        return self.arquivo

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.arquivo.close()
        return False

len, getitem e setitem: Comportamento de Sequências

Esses métodos permitem que seus objetos se comportem como listas ou dicionários. __len__ retorna o tamanho, __getitem__ permite acesso por índice (ou chave), e __setitem__ permite atribuição por índice.

class Playlist:
    def __init__(self, nome):
        self.nome = nome
        self.musicas = []

    def adicionar(self, musica):
        self.musicas.append(musica)

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

    def __getitem__(self, indice):
        return self.musicas[indice]

    def __setitem__(self, indice, musica):
        self.musicas[indice] = musica

    def __repr__(self):
        return f"Playlist('{self.nome}', {len(self)} músicas)"

# Usando como uma sequência
playlist = Playlist("Rock Clássico")
playlist.adicionar("Bohemian Rhapsody")
playlist.adicionar("Stairway to Heaven")

print(len(playlist))  # 2
print(playlist[0])  # Bohemian Rhapsody
playlist[1] = "Hotel California"

call: Tornando objetos invocáveis

O método __call__ permite que você chame um objeto como se fosse uma função. Isso é útil para criar decoradores, callables e classes que agem como funções parametrizadas.

class Multiplicador:
    def __init__(self, fator):
        self.fator = fator

    def __call__(self, valor):
        return valor * self.fator

vezes_3 = Multiplicador(3)
print(vezes_3(5))  # 15
print(vezes_3(10))  # 30

# Usando em um contexto mais real: callback parametrizado
callbacks = [Multiplicador(2), Multiplicador(5), Multiplicador(10)]
for callback in callbacks:
    print(callback(7))  # 14, 35, 70

add, sub e Operadores Aritméticos

Esses métodos permitem usar operadores como +, -, *, / com seus objetos. Útil para domínios como vetores, frações, moedas e quantidades.

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

    def __add__(self, outro):
        if isinstance(outro, Vetor2D):
            return Vetor2D(self.x + outro.x, self.y + outro.y)
        return NotImplemented

    def __sub__(self, outro):
        if isinstance(outro, Vetor2D):
            return Vetor2D(self.x - outro.x, self.y - outro.y)
        return NotImplemented

    def __mul__(self, escalar):
        if isinstance(escalar, (int, float)):
            return Vetor2D(self.x * escalar, self.y * escalar)
        return NotImplemented

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

v1 = Vetor2D(1, 2)
v2 = Vetor2D(3, 4)
print(v1 + v2)  # Vetor2D(4, 6)
print(v1 - v2)  # Vetor2D(-2, -2)
print(v1 * 2)   # Vetor2D(2, 4)

Note que retornar NotImplemented (não exceção!) permite que Python tente a operação reversa. Por exemplo, se você implementar __add__ e o primeiro operando não souber como somar com o segundo, Python tentará __radd__ no segundo operando.

Boas Práticas e Padrões Profissionais

Consistência entre eq e hash

Se você sobrescrever __eq__, deve considerar também __hash__. Objetos que são iguais devem ter o mesmo hash. Caso contrário, comportamentos inesperados ocorrem em sets e dicionários. A regra é: se a == b, então hash(a) == hash(b).

class Usuario:
    def __init__(self, id, nome):
        self.id = id
        self.nome = nome

    def __eq__(self, outro):
        if not isinstance(outro, Usuario):
            return False
        return self.id == outro.id

    def __hash__(self):
        return hash(self.id)

# Agora usuários podem ser usados em sets e como chaves de dicionário
usuarios = {Usuario(1, "Alice"), Usuario(2, "Bob"), Usuario(1, "Alice_2")}
print(len(usuarios))  # 2, não 3, porque dois têm id=1

Retornar NotImplemented, não None ou False

Quando um dunder method não pode lidar com os tipos fornecidos, retorne NotImplemented em vez de False ou None. Isso permite que Python tente a operação reversa (como __radd__) ou lance um erro apropriado.

class Valor:
    def __init__(self, quantia):
        self.quantia = quantia

    def __add__(self, outro):
        if isinstance(outro, Valor):
            return Valor(self.quantia + outro.quantia)
        if isinstance(outro, (int, float)):
            return Valor(self.quantia + outro)
        return NotImplemented  # Correto

v = Valor(10)
# v + "string"  # Lançará TypeError apropriadamente

Type hints e documentação clara

Use type hints nos seus dunder methods. Isso melhora a legibilidade e permite que IDEs forneçam autocompletar correto.

from typing import Any

class Ponto:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

    def __eq__(self, outro: Any) -> bool:
        if not isinstance(outro, Ponto):
            return False
        return self.x == outro.x and self.y == outro.y

    def __str__(self) -> str:
        return f"Ponto({self.x}, {self.y})"

    def __repr__(self) -> str:
        return f"Ponto(x={self.x}, y={self.y})"

Conclusão

Você aprendeu que os dunder methods são hooks que Python chama automaticamente em resposta a operadores e funções built-in. Implementá-los transforma suas classes de "objetos estranhos" em cidadãos de primeira classe no ecossistema Python. Lembre-se: __str__ é para humanos, __repr__ é para desenvolvedores. Sempre implemente __repr__ pelo menos. E quando define __eq__, pense em __hash__ também — eles devem ser consistentes para evitar surpresas desagradáveis com sets e dicionários.

A terceira lição é que o Python oferece uma riqueza de dunder methods além dos básicos. Explorar __len__, __getitem__, __add__ e __call__ abre possibilidades de design expressivo e pythônico. Estude-os conforme a necessidade da sua aplicação surgir, mas comece dominando os fundamentais aqui apresentados.

Referências


Artigos relacionados