W3docs

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 PythonDunder chamado
str(obj)obj.__str__()
len(obj)obj.__len__()
a + ba.__add__(b)
a == ba.__eq__(b)
item in objobj.__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étodoChamado porFinalidade
__repr__repr(), shell interativoRepresentação inequívoca, voltada para desenvolvedores
__str__str(), print(), f-stringsExibiçã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.

OperadorMétodoMé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)  # True

Atalho: 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.celsius

Operadores Aritméticos

Os dunders aritméticos permitem que seus objetos funcionem com +, -, *, /, //, % e **.

ExpressãoMétodoNotas
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étodoChamado porO que habilita
__len__len(obj)Comprimento do contêiner
__getitem__obj[index]Acesso por índice e fatia
__setitem__obj[index] = valAtribuição por índice
__delitem__del obj[index]Exclusão por índice
__contains__item in objTeste 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
# cherry

Protocolo 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 por iter(obj) e no início de um loop for; deve retornar o objeto iterador (geralmente self).
  • __next__ — chamado repetidamente para produzir o próximo valor; deve lançar StopIteration quando 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
# 0

Para 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 items

Isso 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))  # True

double 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 bloco with começa; seu valor de retorno é vinculado à variável as.
  • __exit__(self, exc_type, exc_val, exc_tb) — executado quando o bloco termina, normalmente ou via exceção. Retorne True para suprimir a exceção; retorne False (ou None) 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)  # True

Se 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

CategoriaMétodoAcionado 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 classe User seria confuso.

Para padrões OOP mais avançados, veja Python Abstract Classes, Python Encapsulation e Python Polymorphism.

Prática

Prática
Which dunder method does Python call when you use an object in a boolean context such as 'if obj:'?
Which dunder method does Python call when you use an object in a boolean context such as 'if obj:'?
Was this page helpful?