Métodos especiais (dunder methods) em POO Python: __repr__, __str__, __eq__ e coleções

Capítulo 9

Tempo estimado de leitura: 12 minutos

+ Exercício

O que são métodos especiais (dunder methods)

Métodos especiais (também chamados de dunder methods) são métodos com nomes no formato __nome__ que o Python chama automaticamente em situações comuns: imprimir um objeto, comparar dois objetos, iterar, medir tamanho, usar with, chamar como função etc. Ao implementá-los, sua classe passa a “se encaixar” naturalmente nas estruturas e funções nativas do Python, melhorando legibilidade, depuração e integração com bibliotecas.

Você não chama esses métodos diretamente na maior parte do tempo. Em vez disso, você usa operações normais (como print(obj), len(obj), for x in obj) e o Python delega para o dunder correspondente.

OperaçãoDunder chamadoExemplo
Representação para dev__repr__repr(obj)
Representação amigável__str__str(obj), print(obj)
Igualdade__eq__obj1 == obj2
Ordenação__lt__obj1 < obj2, sorted()
Tamanho__len__len(obj)
Iteração__iter__for x in obj
Chamada como função__call__obj()
Context manager__enter__/__exit__with obj as x:

Representação: __repr__ e __str__

Quando usar cada um

  • __repr__: voltado para depuração e logs técnicos. Idealmente, deve ser não ambíguo e ajudar a identificar o estado do objeto. Muitas vezes, tenta ser algo próximo de “como recriar o objeto”.
  • __str__: voltado para exibição ao usuário. Pode ser mais curto e amigável.

Regra prática: implemente __repr__ sempre que fizer sentido; implemente __str__ quando houver uma forma “humana” de mostrar o objeto. Se __str__ não existir, o Python pode cair no __repr__ em alguns contextos.

Passo a passo: criando uma classe com boas representações

Vamos criar uma classe Produto com __repr__ e __str__ úteis para depuração e para exibição.

class Produto:    def __init__(self, sku: str, nome: str, preco: float):        self.sku = sku        self.nome = nome        self.preco = float(preco)    def __repr__(self) -> str:        # Foco: depuração. Mostra classe e campos relevantes.        return f"Produto(sku={self.sku!r}, nome={self.nome!r}, preco={self.preco!r})"    def __str__(self) -> str:        # Foco: apresentação amigável.        return f"{self.nome} (SKU {self.sku}) - R$ {self.preco:.2f}"

Detalhes importantes:

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

  • Use {valor!r} no __repr__ para aplicar repr(valor) e deixar strings com aspas, facilitando ver espaços, caracteres especiais etc.
  • Evite print dentro de __repr__/__str__. Eles devem apenas retornar uma string.
  • Não faça operações caras (consultas, leitura de arquivo, rede) nesses métodos: eles podem ser chamados com frequência (por exemplo, em logs).

Como isso melhora depuração e logs

p = Produto("A1", "Café", 19.9)print(p)        # usa __str__print(repr(p))   # usa __repr__itens = [p]print(itens)    # listas usam repr() dos elementos

Quando você imprime uma lista de objetos, o Python usa repr() de cada elemento. Por isso, um __repr__ bem feito melhora muito a inspeção de coleções em logs e no console.

Comparação: __eq__ e __lt__ (e por que isso afeta coleções)

__eq__: igualdade sem surpresas

__eq__ define o comportamento de ==. Isso impacta diretamente:

  • Busca em listas: x in lista usa igualdade.
  • Remoção: lista.remove(x) usa igualdade.
  • Testes e validações: comparações em asserts.

Exemplo: dois produtos são “iguais” se tiverem o mesmo SKU.

class Produto:    def __init__(self, sku: str, nome: str, preco: float):        self.sku = sku        self.nome = nome        self.preco = float(preco)    def __repr__(self):        return f"Produto(sku={self.sku!r}, nome={self.nome!r}, preco={self.preco!r})"    def __str__(self):        return f"{self.nome} (SKU {self.sku}) - R$ {self.preco:.2f}"    def __eq__(self, other):        if not isinstance(other, Produto):            return NotImplemented        return self.sku == other.sku

Por que retornar NotImplemented? Porque isso permite que o Python tente a comparação reversa ou decida corretamente que os tipos não são comparáveis, em vez de “forçar” um False silencioso.

__lt__: ordenação e sorted()

__lt__ define o comportamento de <. Com ele, você pode ordenar objetos com sorted() e list.sort() sem precisar passar key=... toda vez.

Exemplo: ordenar produtos por preço e, em caso de empate, por SKU.

class Produto:    def __init__(self, sku: str, nome: str, preco: float):        self.sku = sku        self.nome = nome        self.preco = float(preco)    def __repr__(self):        return f"Produto(sku={self.sku!r}, nome={self.nome!r}, preco={self.preco!r})"    def __eq__(self, other):        if not isinstance(other, Produto):            return NotImplemented        return self.sku == other.sku    def __lt__(self, other):        if not isinstance(other, Produto):            return NotImplemented        return (self.preco, self.sku) < (other.preco, other.sku)

Teste rápido:

itens = [    Produto("B2", "Chá", 12.0),    Produto("A1", "Café", 19.9),    Produto("C3", "Açúcar", 12.0),]print(sorted(itens))

Observação: implementar apenas __lt__ não cria automaticamente <=, >, >=. Se você precisar do conjunto completo, pode implementar os demais ou usar functools.total_ordering (com cuidado para não mascarar comparações mal definidas).

Tamanho: __len__

__len__ permite usar len(obj). Isso é especialmente útil quando sua classe representa uma coleção (carrinho, fila, catálogo, conjunto de itens etc.).

Exemplo: um carrinho que guarda itens internamente em uma lista.

class Carrinho:    def __init__(self):        self._itens = []    def adicionar(self, produto):        self._itens.append(produto)    def __len__(self):        return len(self._itens)
c = Carrinho()c.adicionar(Produto("A1", "Café", 19.9))print(len(c))

Boas práticas:

  • __len__ deve retornar um inteiro >= 0.
  • Evite calcular tamanho de forma cara; prefira manter estrutura interna que já saiba seu tamanho.

Iteração: __iter__ (fazendo sua classe funcionar com for, list(), sum(), etc.)

Ao implementar __iter__, sua classe se torna iterável. Isso habilita:

  • for item in obj
  • list(obj), tuple(obj)
  • compreensões: [x for x in obj]
  • funções como sum, any, all (quando aplicável)

Passo a passo: tornando Carrinho iterável

class Carrinho:    def __init__(self):        self._itens = []    def adicionar(self, produto):        self._itens.append(produto)    def __len__(self):        return len(self._itens)    def __iter__(self):        # Delegando a iteração para a lista interna        return iter(self._itens)
c = Carrinho()c.adicionar(Produto("A1", "Café", 19.9))c.adicionar(Produto("B2", "Chá", 12.0))for p in c:    print(p)print([p.sku for p in c])

Esse padrão (delegar para uma coleção interna) é simples, eficiente e muito comum.

Chamada: __call__ (objetos que se comportam como funções)

__call__ permite que uma instância seja chamada como se fosse uma função. Isso é útil para encapsular comportamento configurável (por exemplo, um validador, um formatador, uma regra de desconto).

Exemplo: regra de desconto chamável

class DescontoPercentual:    def __init__(self, percentual: float):        self.percentual = float(percentual)    def __call__(self, preco: float) -> float:        return float(preco) * (1 - self.percentual / 100.0)    def __repr__(self):        return f"DescontoPercentual(percentual={self.percentual!r})"
desconto10 = DescontoPercentual(10)print(desconto10(200))  # 180.0

Vantagem: você pode passar o objeto para APIs que esperam uma função (callbacks), mantendo estado/configuração dentro da instância.

Contexto: __enter__ e __exit__ (integração com with)

O protocolo de contexto permite usar with para garantir aquisição/liberação de recursos, mesmo se ocorrer erro. Você já usa isso com arquivos: with open(...) as f:. Ao implementar __enter__ e __exit__, sua classe pode oferecer a mesma segurança.

Exemplo: temporizador simples para logs

import timeclass Timer:    def __init__(self, label: str = "bloco"):        self.label = label        self._inicio = None    def __enter__(self):        self._inicio = time.perf_counter()        return self    def __exit__(self, exc_type, exc, tb):        fim = time.perf_counter()        duracao = fim - self._inicio        print(f"{self.label}: {duracao:.6f}s")        # Retornar False propaga exceções (comportamento padrão)        return False
with Timer("processamento"):    total = sum(i * i for i in range(100000))

Como isso ajuda: você padroniza logs e medições sem espalhar start/end pelo código.

Desafio guiado: implementar uma coleção com impressão amigável, comparações e iteração

Objetivo: criar uma classe Biblioteca que se comporte como uma coleção de livros, integrando-se com print, len, for, in e sorted.

Requisitos

  • Armazenar livros internamente (por exemplo, em uma lista).
  • __repr__ deve ajudar na depuração (mostrar quantidade e talvez uma prévia).
  • __str__ deve ser amigável (ex.: “Biblioteca com N livros”).
  • __len__ deve retornar a quantidade de livros.
  • __iter__ deve permitir iterar sobre os livros.
  • __eq__ deve comparar duas bibliotecas (por exemplo, pelo conjunto/ordem de ISBNs).
  • __lt__ deve permitir ordenar bibliotecas (por exemplo, por quantidade de livros e depois por nome).
  • Extra: implementar __call__ para buscar livros por termo.

Passo a passo sugerido (com esqueleto)

1) Comece com uma classe simples Livro com boa representação e igualdade por ISBN.

class Livro:    def __init__(self, isbn: str, titulo: str, autor: str):        self.isbn = isbn        self.titulo = titulo        self.autor = autor    def __repr__(self):        return f"Livro(isbn={self.isbn!r}, titulo={self.titulo!r}, autor={self.autor!r})"    def __str__(self):        return f"{self.titulo} — {self.autor} (ISBN {self.isbn})"    def __eq__(self, other):        if not isinstance(other, Livro):            return NotImplemented        return self.isbn == other.isbn

2) Crie a classe Biblioteca com lista interna e implemente __len__ e __iter__ delegando para a lista.

class Biblioteca:    def __init__(self, nome: str):        self.nome = nome        self._livros = []    def adicionar(self, livro: Livro):        self._livros.append(livro)    def __len__(self):        return len(self._livros)    def __iter__(self):        return iter(self._livros)

3) Adicione __str__ e __repr__ com foco em usabilidade.

class Biblioteca:    def __init__(self, nome: str):        self.nome = nome        self._livros = []    def adicionar(self, livro: Livro):        self._livros.append(livro)    def __len__(self):        return len(self._livros)    def __iter__(self):        return iter(self._livros)    def __str__(self):        return f"Biblioteca '{self.nome}' com {len(self)} livros"    def __repr__(self):        preview = self._livros[:2]        sufixo = "..." if len(self._livros) > 2 else ""        return f"Biblioteca(nome={self.nome!r}, livros={preview!r}{sufixo})"

4) Implemente comparações: __eq__ e __lt__.

Uma definição possível:

  • Duas bibliotecas são iguais se tiverem o mesmo nome e a mesma sequência de ISBNs.
  • Uma biblioteca é “menor” que outra se tiver menos livros; em empate, ordena por nome.
class Biblioteca:    def __init__(self, nome: str):        self.nome = nome        self._livros = []    def adicionar(self, livro: Livro):        self._livros.append(livro)    def __len__(self):        return len(self._livros)    def __iter__(self):        return iter(self._livros)    def __str__(self):        return f"Biblioteca '{self.nome}' com {len(self)} livros"    def __repr__(self):        preview = self._livros[:2]        sufixo = "..." if len(self._livros) > 2 else ""        return f"Biblioteca(nome={self.nome!r}, livros={preview!r}{sufixo})"    def __eq__(self, other):        if not isinstance(other, Biblioteca):            return NotImplemented        isbns_self = [l.isbn for l in self._livros]        isbns_other = [l.isbn for l in other._livros]        return (self.nome, isbns_self) == (other.nome, isbns_other)    def __lt__(self, other):        if not isinstance(other, Biblioteca):            return NotImplemented        return (len(self), self.nome) < (len(other), other.nome)

5) Extra: implemente __call__ para busca simples por termo (título ou autor). Assim, você pode fazer biblioteca("machado").

class Biblioteca:    def __init__(self, nome: str):        self.nome = nome        self._livros = []    def adicionar(self, livro: Livro):        self._livros.append(livro)    def __len__(self):        return len(self._livros)    def __iter__(self):        return iter(self._livros)    def __str__(self):        return f"Biblioteca '{self.nome}' com {len(self)} livros"    def __repr__(self):        preview = self._livros[:2]        sufixo = "..." if len(self._livros) > 2 else ""        return f"Biblioteca(nome={self.nome!r}, livros={preview!r}{sufixo})"    def __eq__(self, other):        if not isinstance(other, Biblioteca):            return NotImplemented        isbns_self = [l.isbn for l in self._livros]        isbns_other = [l.isbn for l in other._livros]        return (self.nome, isbns_self) == (other.nome, isbns_other)    def __lt__(self, other):        if not isinstance(other, Biblioteca):            return NotImplemented        return (len(self), self.nome) < (len(other), other.nome)    def __call__(self, termo: str):        termo = termo.lower().strip()        return [            livro            for livro in self._livros            if termo in livro.titulo.lower() or termo in livro.autor.lower()        ]

Roteiro de testes (o aluno deve executar e conferir o comportamento)

b1 = Biblioteca("Central")b1.adicionar(Livro("1", "Dom Casmurro", "Machado de Assis"))b1.adicionar(Livro("2", "Memórias Póstumas", "Machado de Assis"))b2 = Biblioteca("Bairro")b2.adicionar(Livro("3", "O Alienista", "Machado de Assis"))print(str(b1))          # amigávelprint(repr(b1))         # depuraçãoprint(len(b1))          # tamanhofor livro in b1:        # iteração    print(livro)print(b1("machado"))    # chamada como função (busca)print(sorted([b1, b2])) # ordenação via __lt__

Desafios (para praticar)

Desafio 1: coleção com filtro iterável

Crie uma classe Playlist que armazena músicas e:

  • Implementa __len__ e __iter__.
  • Implementa __str__ (ex.: “Playlist X — N faixas”).
  • Implementa __call__(artista) para retornar um iterável (ou lista) apenas com músicas daquele artista.

Desafio 2: comparação consistente

Crie uma classe Ponto2D com:

  • __repr__ mostrando coordenadas.
  • __eq__ comparando pontos por (x, y).
  • __lt__ ordenando por distância à origem (em empate, por x e depois y).

Desafio 3: context manager para recurso simulado

Crie uma classe ArquivoSimulado que:

  • No __enter__, marca como “aberto” e retorna a si mesma.
  • No __exit__, marca como “fechado”.
  • Possui um método escrever(texto) que só funciona se estiver aberto (caso contrário, levanta erro).
  • Implemente __repr__ para facilitar logs do estado (aberto/fechado).

Agora responda o exercício sobre o conteúdo:

Ao imprimir uma lista de objetos de uma classe, qual prática torna a inspeção no console e em logs mais útil e por quê?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Ao imprimir uma lista, o Python exibe cada elemento usando repr(). Por isso, um __repr__ claro e não ambíguo melhora muito a depuração e a leitura de logs.

Próximo capitúlo

Classes abstratas e contratos em POO Python: abc, métodos abstratos e design orientado a interface

Arrow Right Icon
Capa do Ebook gratuito Python Orientado a Objetos para Iniciantes: Classes, Herança e Boas Práticas
64%

Python Orientado a Objetos para Iniciantes: Classes, Herança e Boas Práticas

Novo curso

14 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.