Métodos Mágicos (Dunder) do Python
Aprenda os métodos mágicos do Python (__init__, __str__, __repr__, sobrecarga de operadores, protocolos de contêiner e gerenciador de contexto) com exemplos práticos.
Métodos mágicos — também chamados de métodos dunder (abreviação de double-underscore, duplo sublinhado) — são métodos especiais cujos nomes começam e terminam com dois sublinhados, como __init__ ou __len__. Eles são o sistema de hooks do Python: ao defini-los em suas próprias classes, você diz ao Python como um objeto deve se comportar com operadores e funções integrados como +, len(), print(), in e with.
Você nunca chama métodos dunder diretamente. Em vez disso, o Python os chama automaticamente nos bastidores:
| Expressão Python | Dunder chamado |
|---|---|
str(obj) | obj.__str__() |
len(obj) | obj.__len__() |
a + b | a.__add__(b) |
a == b | a.__eq__(b) |
item in obj | obj.__contains__(item) |
with obj as x: | obj.__enter__() / obj.__exit__(...) |
Este capítulo abrange:
- Representação em string —
__repr__e__str__ - Operadores de comparação —
__eq__,__lt__e similares - Operadores aritméticos —
__add__,__mul__,__rmul__e mais - Protocolo de contêiner —
__len__,__getitem__,__contains__ - Protocolo de iterador —
__iter__e__next__ - Veracidade —
__bool__ - Objetos chamáveis —
__call__ - Protocolo de gerenciador de contexto —
__enter__e__exit__ __hash__— tornando objetos utilizáveis como chaves de dicionário
Antes de continuar, certifique-se de estar confortável com classes e objetos Python e herança Python. Para atributos computados, veja @property.
Representação em String: __repr__ e __str__
Esses dois métodos controlam como um objeto é convertido em string.
| Método | Chamado por | Finalidade |
|---|---|---|
__repr__ | repr(), shell interativo | Representação inequívoca, voltada para desenvolvedores |
__str__ | str(), print(), f-strings | Exibição legível por humanos |
Se __str__ não for definido, o Python recorre a __repr__. Portanto, a melhor prática é sempre definir __repr__ e definir __str__ somente quando quiser um formato legível diferente.
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def __repr__(self):
return f"Book(title={self.title!r}, author={self.author!r}, pages={self.pages})"
def __str__(self):
return f'"{self.title}" by {self.author} ({self.pages} pages)'
b = Book("Clean Code", "Robert C. Martin", 431)
print(repr(b)) # Book(title='Clean Code', author='Robert C. Martin', pages=431)
print(str(b)) # "Clean Code" by Robert C. Martin (431 pages)
print(b) # "Clean Code" by Robert C. Martin (431 pages)O flag de conversão !r dentro de uma f-string chama repr() naquele valor, o que envolve strings entre aspas. Isso torna a saída de __repr__ um código Python que pode ser copiado e colado.
Dica: um bom __repr__ permite reconstruir o objeto a partir de sua saída. Pense nele como eval(repr(obj)) == obj como modelo mental (mesmo quando não for literalmente verdade).
Operadores de Comparação
Todos os operadores de comparação do Python são mapeados para métodos dunder. Defina-os quando quiser que ==, <, >, <= ou >= comparem seus objetos de forma significativa.
| Operador | Método | Método refletido |
|---|---|---|
== | __eq__ | __eq__ |
!= | __ne__ | __ne__ |
< | __lt__ | __gt__ |
<= | __le__ | __ge__ |
> | __gt__ | __lt__ |
>= | __ge__ | __le__ |
Refletido significa que o Python tenta o método do operando direito quando o operando esquerdo retorna NotImplemented. Por exemplo, se a < b chama a.__lt__(b) e isso retorna NotImplemented, o Python então tenta o método refletido: b.__gt__(a).
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __eq__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius == other.celsius
def __lt__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius < other.celsius
def __le__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius <= other.celsius
def __repr__(self):
return f"Temperature({self.celsius}°C)"
t1 = Temperature(20)
t2 = Temperature(30)
t3 = Temperature(20)
print(t1 == t3) # True
print(t1 < t2) # True
print(t2 > t1) # True — Python derives __gt__ from __lt__ via reflection
print(t1 <= t3) # TrueAtalho: se você apenas quiser que os objetos sejam ordenáveis sem se preocupar com os seis operadores individualmente, use o decorator @functools.total_ordering. Defina __eq__ e um dos __lt__, __le__, __gt__ ou __ge__, e total_ordering preenche o restante automaticamente.
from functools import total_ordering
@total_ordering
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __eq__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius == other.celsius
def __lt__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius < other.celsiusOperadores Aritméticos
Os dunders aritméticos permitem que seus objetos funcionem com +, -, *, /, //, % e **.
| Expressão | Método | Notas |
|---|---|---|
a + b | __add__ | |
a - b | __sub__ | |
a * b | __mul__ | |
b * a | __rmul__ | versão do lado direito; chamado quando b.__mul__(a) retorna NotImplemented |
-a | __neg__ | negação unária |
abs(a) | __abs__ | |
a += b | __iadd__ | in-place; recorre a __add__ se não definido |
Um caso de uso clássico é uma classe de vetor 2D:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar): # supports: 3 * v
return self.__mul__(scalar)
def __neg__(self):
return Vector(-self.x, -self.y)
def __abs__(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v2 - v1) # Vector(2, 2)
print(v1 * 3) # Vector(3, 6)
print(3 * v1) # Vector(3, 6) — uses __rmul__
print(-v1) # Vector(-1, -2)
print(abs(v2)) # 5.0__rmul__ é o que permite que 3 * v1 funcione. Quando o Python avalia 3 * v1, ele primeiro chama int.__mul__(3, v1). A classe inteira integrada não sabe como multiplicar um inteiro por um Vector, então retorna NotImplemented. O Python então tenta o método refletido: v1.__rmul__(3), que tem sucesso.
Protocolo de Contêiner
Implemente esses métodos para fazer sua classe se comportar como uma sequência ou coleção.
| Método | Chamado por | O que habilita |
|---|---|---|
__len__ | len(obj) | Comprimento do contêiner |
__getitem__ | obj[index] | Acesso por índice e fatia |
__setitem__ | obj[index] = val | Atribuição por índice |
__delitem__ | del obj[index] | Exclusão por índice |
__contains__ | item in obj | Teste de pertencimento |
Definir __len__ junto com __getitem__ é suficiente para tornar sua classe automaticamente iterável — o loop for do Python chamará __getitem__ com índices sucessivos a partir de 0 até obter um IndexError.
class WordBag:
def __init__(self, *words):
self._words = list(words)
def __len__(self):
return len(self._words)
def __contains__(self, item):
return item in self._words
def __getitem__(self, index):
return self._words[index]
def __repr__(self):
return f"WordBag({self._words!r})"
bag = WordBag("apple", "banana", "cherry")
print(len(bag)) # 3
print("banana" in bag) # True
print("grape" in bag) # False
print(bag[0]) # apple
print(bag[-1]) # cherry
# __len__ + __getitem__ makes the object iterable automatically
for word in bag:
print(word)
# apple
# banana
# cherryProtocolo de Iterador
Se você quiser o comportamento completo de iterador (funcionando com iter() e next() diretamente, ou sendo utilizável em lugares que exigem um iterador em vez de apenas um iterável), defina tanto __iter__ quanto __next__:
__iter__— chamado poriter(obj)e no início de um loopfor; deve retornar o objeto iterador (geralmenteself).__next__— chamado repetidamente para produzir o próximo valor; deve lançarStopIterationquando esgotado.
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
self.current = self.start
return self
def __next__(self):
if self.current < 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for n in Countdown(3):
print(n)
# 3
# 2
# 1
# 0Para padrões de iteração mais poderosos — especialmente sequências lazy que produzem valores sob demanda — veja Python Generators e Python Iterators.
Veracidade: __bool__
O Python chama __bool__ quando um objeto é usado em um contexto boolean (uma instrução if, loop while, not, and, or). Se __bool__ não for definido mas __len__ for, o Python usa len(obj) != 0 como valor de verdade. Se nenhum dos dois for definido, o objeto é sempre verdadeiro.
class Stack:
def __init__(self):
self._data = []
def push(self, item):
self._data.append(item)
def pop(self):
return self._data.pop()
def __len__(self):
return len(self._data)
def __bool__(self):
return len(self._data) > 0
def __repr__(self):
return f"Stack({self._data!r})"
s = Stack()
print(bool(s)) # False — empty stack is falsy
s.push(1)
print(bool(s)) # True
print(len(s)) # 1
if s:
print("stack has items") # stack has itemsIsso espelha o comportamento das coleções integradas: uma lista, dict ou set vazio é falso; um não vazio é verdadeiro.
Objetos Chamáveis: __call__
Definir __call__ permite usar uma instância como se fosse uma função. Isso é útil para objetos que mantêm estado entre chamadas — algo que uma função comum não pode fazer sem um closure ou variável global.
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
double = Multiplier(2)
triple = Multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
print(callable(double)) # Truedouble e triple são objetos comuns, mas você os chama com () como funções. A função integrada callable() retorna True para qualquer objeto que tenha __call__.
Esse padrão é comum em frameworks de machine learning (camadas, funções de perda) e em fábricas de decorators. Veja Python Decorators para um caso de uso intimamente relacionado.
Protocolo de Gerenciador de Contexto: __enter__ e __exit__
A instrução with é a forma do Python de configurar e desmontar um recurso de forma confiável — mesmo que ocorra uma exceção. Qualquer objeto que defina __enter__ e __exit__ pode ser usado como gerenciador de contexto.
__enter__(self)— executado quando o blocowithcomeça; seu valor de retorno é vinculado à variávelas.__exit__(self, exc_type, exc_val, exc_tb)— executado quando o bloco termina, normalmente ou via exceção. RetorneTruepara suprimir a exceção; retorneFalse(ouNone) para deixá-la se propagar.
class ManagedFile:
def __init__(self, path, mode="r"):
self.path = path
self.mode = mode
self._file = None
def __enter__(self):
self._file = open(self.path, self.mode)
return self._file # the value bound to the "as" variable
def __exit__(self, exc_type, exc_val, exc_tb):
if self._file:
self._file.close()
return False # do not suppress exceptions
with ManagedFile("/etc/hostname") as f:
content = f.read()
# The file is guaranteed to be closed here, even if an exception occurred inside the block.O decorator contextlib.contextmanager da biblioteca padrão permite escrever a mesma lógica como uma função geradora — uma alternativa mais leve para casos simples. Veja Python with Statement para cobertura completa.
Hashing: __hash__
O Python usa __hash__ para inserir objetos em sets e dicionários. O __hash__ padrão é baseado no endereço de memória do objeto (identidade). Quando você substitui __eq__, o Python automaticamente define __hash__ como None, tornando seus objetos não hasheáveis — você deve definir __hash__ explicitamente se ainda quiser que funcionem em sets ou como chaves de dict.
A regra: objetos que se comparam como iguais devem ter o mesmo hash.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y)) # hash of an immutable tuple
def __repr__(self):
return f"Point({self.x}, {self.y})"
p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)
print(p1 == p2) # True
print(p1 is p2) # False — different objects in memory
print(hash(p1) == hash(p2)) # True
seen = {p1, p2, p3}
print(len(seen)) # 2 — p1 and p2 are equal, so only one copy kept
print(p1 in seen) # TrueSe sua classe é mutável (seus campos podem mudar após a criação), não defina __hash__. Objetos mutáveis não devem ser hasheáveis porque alterar seus campos alteraria seu hash, corrompendo qualquer set ou dict que já os contenha.
Tabela Resumo
| Categoria | Método | Acionado por |
|---|---|---|
| Representação | __repr__ | repr(obj), shell interativo |
| Representação | __str__ | str(obj), print(obj), f-strings |
| Comparação | __eq__, __ne__ | ==, != |
| Comparação | __lt__, __le__, __gt__, __ge__ | <, <=, >, >= |
| Aritmética | __add__, __sub__, __mul__ | +, -, * |
| Aritmética | __rmul__, __radd__, … | formas refletidas do lado direito |
| Aritmética | __neg__, __abs__ | - unário, abs() |
| Contêiner | __len__ | len(obj) |
| Contêiner | __getitem__, __setitem__, __delitem__ | obj[i], obj[i] = v, del obj[i] |
| Contêiner | __contains__ | item in obj |
| Iterador | __iter__ | iter(obj), loop for |
| Iterador | __next__ | next(obj) |
| Veracidade | __bool__ | bool(obj), if obj: |
| Chamável | __call__ | obj(args) |
| Gerenciador de contexto | __enter__, __exit__ | with obj as x: |
| Hashing | __hash__ | hash(obj), chaves de dict, sets |
Quando Usar Métodos Mágicos
- Use-os quando sua classe representar um tipo de valor (um ponto, um vetor, um valor monetário, um intervalo de datas) — sobrecarregar operadores e comparações torna a classe mais natural.
- Use-os quando sua classe encapsular um recurso (um arquivo, uma conexão de banco de dados, um socket de rede) —
__enter__/__exit__garante que o recurso seja sempre liberado. - Use-os quando sua classe for uma coleção personalizada — os protocolos de contêiner e iterador permitem que ela funcione com
for,in,len()e list comprehensions. - Evite-os para classes de aplicação comuns que não são tipos de valor ou contêineres. Sobrecarregar
+em uma classeUserseria confuso.
Para padrões OOP mais avançados, veja Python Abstract Classes, Python Encapsulation e Python Polymorphism.