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ção | Dunder chamado | Exemplo |
|---|---|---|
| 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:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
- Use
{valor!r}no__repr__para aplicarrepr(valor)e deixar strings com aspas, facilitando ver espaços, caracteres especiais etc. - Evite
printdentro 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 elementosQuando 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 listausa 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.skuPor 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 objlist(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.0Vantagem: 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 Falsewith 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.isbn2) 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, porxe depoisy).
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).