Python

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

15 min de leitura

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

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

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

<p>Os &quot;dunder methods&quot; (abreviação de &quot;double underscore methods&quot;) são métodos especiais em Python que começam e terminam com dois underscores, como <code>__init__</code>, <code>__str__</code> e <code>__eq__</code>. 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.</p>

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

<h2>__str__ vs __repr__: Entendendo a Representação de Objetos</h2>

<h3>O propósito de __str__ e __repr__</h3>

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

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

<h3>Exemplo prático com uma classe Pessoa</h3>

<pre><code class="language-python">class Pessoa:

def __init__(self, nome, idade):

self.nome = nome

self.idade = idade

def __str__(self):

return f&quot;{self.nome} tem {self.idade} anos&quot;

def __repr__(self):

return f&quot;Pessoa(nome=&#039;{self.nome}&#039;, idade={self.idade})&quot;

Testando

pessoa = Pessoa(&quot;Alice&quot;, 28)

print(pessoa) # Chama __str__: Alice tem 28 anos

print(repr(pessoa)) # Chama __repr__: Pessoa(nome=&#039;Alice&#039;, idade=28)</code></pre>

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

<h3>Quando usar cada um</h3>

<p>Use <code>__str__</code> quando quiser uma mensagem clara e contextualizada para o usuário final. Use <code>__repr__</code> 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 <code>__repr__</code>, pois ele será usado em listas e dicionários durante a depuração.</p>

<h2>__eq__ e Comparação: Definindo Igualdade</h2>

<h3>O conceito de igualdade em objetos</h3>

<p>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 <em>conteúdo</em>, não pela sua localização em memória. O método <code>__eq__</code> permite que você defina quando dois objetos devem ser considerados iguais. Ao implementá-lo, você também habilita automaticamente <code>!=</code> (que usa <code>__ne__</code>, mas Python fornece uma implementação padrão baseada em <code>__eq__</code>).</p>

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

<h3>Exemplo: Implementando __eq__ para uma classe Produto</h3>

<pre><code class="language-python">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&quot;Produto(codigo={self.codigo}, nome=&#039;{self.nome}&#039;, preco={self.preco})&quot;

Testando

p1 = Produto(101, &quot;Notebook&quot;, 3500.00)

p2 = Produto(101, &quot;Notebook&quot;, 3500.00)

p3 = Produto(102, &quot;Mouse&quot;, 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__)</code></pre>

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

<h3>Usando __eq__ em contextos práticos</h3>

<p>Uma vez que você implemente <code>__eq__</code>, 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:</p>

<pre><code class="language-python">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</code></pre>

<h2>Explorando Outros Dunder Methods Essenciais</h2>

<h3>__init__ e __del__: Construtor e Destrutor</h3>

<p>O método <code>__init__</code> é chamado quando você cria uma instância da classe (depois que <code>__new__</code> a aloca em memória). É onde você inicializa os atributos. O método <code>__del__</code> é 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 <code>__del__</code> para cleanup crítico; use context managers (with) quando possível.</p>

<pre><code class="language-python">class Arquivo:

def __init__(self, nome):

self.nome = nome

self.arquivo = open(nome, &#039;r&#039;)

print(f&quot;Arquivo {nome} aberto&quot;)

def __del__(self):

self.arquivo.close()

print(f&quot;Arquivo {self.nome} fechado&quot;)

Com context manager (melhor prática):

class ArquivoSeguro:

def __init__(self, nome):

self.nome = nome

self.arquivo = open(nome, &#039;r&#039;)

def __enter__(self):

return self.arquivo

def __exit__(self, exc_type, exc_val, exc_tb):

self.arquivo.close()

return False</code></pre>

<h3>__len__, __getitem__ e __setitem__: Comportamento de Sequências</h3>

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

<pre><code class="language-python">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&quot;Playlist(&#039;{self.nome}&#039;, {len(self)} músicas)&quot;

Usando como uma sequência

playlist = Playlist(&quot;Rock Clássico&quot;)

playlist.adicionar(&quot;Bohemian Rhapsody&quot;)

playlist.adicionar(&quot;Stairway to Heaven&quot;)

print(len(playlist)) # 2

print(playlist[0]) # Bohemian Rhapsody

playlist[1] = &quot;Hotel California&quot;</code></pre>

<h3>__call__: Tornando objetos invocáveis</h3>

<p>O método <code>__call__</code> 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.</p>

<pre><code class="language-python">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</code></pre>

<h3>__add__, __sub__ e Operadores Aritméticos</h3>

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

<pre><code class="language-python">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&quot;Vetor2D({self.x}, {self.y})&quot;

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)</code></pre>

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

<h2>Boas Práticas e Padrões Profissionais</h2>

<h3>Consistência entre __eq__ e __hash__</h3>

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

<pre><code class="language-python">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, &quot;Alice&quot;), Usuario(2, &quot;Bob&quot;), Usuario(1, &quot;Alice_2&quot;)}

print(len(usuarios)) # 2, não 3, porque dois têm id=1</code></pre>

<h3>Retornar NotImplemented, não None ou False</h3>

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

<pre><code class="language-python">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 + &quot;string&quot; # Lançará TypeError apropriadamente</code></pre>

<h3>Type hints e documentação clara</h3>

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

<pre><code class="language-python">from typing import Any

class Ponto:

def __init__(self, x: float, y: float) -&gt; None:

self.x = x

self.y = y

def __eq__(self, outro: Any) -&gt; bool:

if not isinstance(outro, Ponto):

return False

return self.x == outro.x and self.y == outro.y

def __str__(self) -&gt; str:

return f&quot;Ponto({self.x}, {self.y})&quot;

def __repr__(self) -&gt; str:

return f&quot;Ponto(x={self.x}, y={self.y})&quot;</code></pre>

<h2>Conclusão</h2>

<p>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 &quot;objetos estranhos&quot; em cidadãos de primeira classe no ecossistema Python. Lembre-se: <code>__str__</code> é para humanos, <code>__repr__</code> é para desenvolvedores. Sempre implemente <code>__repr__</code> pelo menos. E quando define <code>__eq__</code>, pense em <code>__hash__</code> também — eles devem ser consistentes para evitar surpresas desagradáveis com sets e dicionários.</p>

<p>A terceira lição é que o Python oferece uma riqueza de dunder methods além dos básicos. Explorar <code>__len__</code>, <code>__getitem__</code>, <code>__add__</code> e <code>__call__</code> 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.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://docs.python.org/3/reference/datamodel.html" target="_blank" rel="noopener noreferrer">Python Data Model - Official Documentation</a></li>

<li><a href="https://realpython.com/operator-function-overloading/" target="_blank" rel="noopener noreferrer">Real Python: Dunder Methods in Python</a></li>

<li><a href="https://www.python-course.eu/python3_magic_methods.php" target="_blank" rel="noopener noreferrer">Python&#039;s Magic Methods by Rafe Kettler</a></li>

<li><a href="https://www.oreilly.com/library/view/fluent-python/9781491946237/" target="_blank" rel="noopener noreferrer">Fluent Python by Luciano Ramalho - Chapter on Data Model</a></li>

<li><a href="https://www.python.org/dev/peps/pep-0207/" target="_blank" rel="noopener noreferrer">PEP 207 – Rich Comparisons</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Python

pip, virtualenv e venv em Python: Isolamento de Dependências: Do Básico ao Avançado
pip, virtualenv e venv em Python: Isolamento de Dependências: Do Básico ao Avançado

O Problema do Caos de Dependências Quando começamos a trabalhar com Python, e...

Mocks em Python: unittest.mock, patch e pytest-mock na Prática: Do Básico ao Avançado
Mocks em Python: unittest.mock, patch e pytest-mock na Prática: Do Básico ao Avançado

Introdução: Por que Mocks são Essenciais Quando desenvolvemos software profis...

Dominando asyncio Avançado em Python: Semáforos, Locks e Padrões de Concorrência em Projetos Reais
Dominando asyncio Avançado em Python: Semáforos, Locks e Padrões de Concorrência em Projetos Reais

Introdução: O Problema da Concorrência Controlada Quando trabalhamos com em P...