11 min de leitura
#16 - Métodos Mágicos (Dunder Methods)
Como personalizar o comportamento de print(), comparações, operadores, len() e chamadas de objetos em Python usando métodos mágicos como __str__, __repr__, __eq__, __add__, __len__, __lt__, __contains__ e __call__.
Nos posts sobre orientação a objetos, você criou suas primeiras classes e já deu uma espiada rápida no __str__, aquele método que troca <__main__.Pessoa object at 0x...> por um texto legível quando você dá print() num objeto. Só que __str__ é só a ponta do iceberg: Python tem uma família inteira de métodos especiais — os métodos mágicos, ou dunder methods (de “double underscore”) — que o interpretador chama sozinho em situações específicas, sem você precisar pedir.
Neste post você vai aprofundar __str__ e conhecer seu parceiro __repr__, além de ver como fazer ==, +, len(), <, in e até a chamada objeto() funcionarem em cima das suas próprias classes.
O que são métodos mágicos
Métodos mágicos são métodos com nomes que começam e terminam com dois underscores, como __init__() ou __str__() — daí o apelido dunder. Você não chama esses métodos diretamente; o Python chama por conta própria, em resposta a alguma ação: criar um objeto, imprimir, comparar dois objetos, somar, medir o tamanho…
Você já usou um deles sem perceber: __init__(), visto nos posts sobre orientação a objetos, roda automaticamente toda vez que você cria um objeto. Os métodos deste post seguem a mesma lógica, só que cada um reage a uma situação diferente:
| Método | É chamado quando… |
|---|---|
__str__ |
print(obj), str(obj) |
__repr__ |
repr(obj), ou ao imprimir sem __str__ definido |
__eq__ |
obj1 == obj2 |
__add__ |
obj1 + obj2 |
__len__ |
len(obj) |
__lt__ |
obj1 < obj2, sorted(lista) |
__contains__ |
valor in obj |
__call__ |
obj() |
str e repr: a versão legível e a versão técnica
Sem nenhum dos dois definidos, imprimir um objeto mostra o endereço dele na memória:
class Pessoa:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
p1 = Pessoa("Ana", 28)
print(p1)
A saída é algo como:
<__main__.Pessoa object at 0x7f2e3c1a4d90>
O __repr__ já resolve isso, retornando uma representação pensada pra quem programa — geralmente parecida com o código necessário pra recriar o objeto:
class Pessoa:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
def __repr__(self):
return f"Pessoa(nome='{self.nome}', idade={self.idade})"
p1 = Pessoa("Ana", 28)
print(p1)
A saída é:
Pessoa(nome='Ana', idade=28)
Repare que print() usou o __repr__ mesmo sem você pedir — é o comportamento padrão quando não existe __str__. Se os dois estiverem definidos, cada um assume seu papel: __str__ é a versão legível pro usuário final, __repr__ é a versão técnica pro desenvolvedor (usada por repr(), e também no console interativo).
class Pessoa:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
def __str__(self):
return f"{self.nome} ({self.idade} anos)"
def __repr__(self):
return f"Pessoa(nome='{self.nome}', idade={self.idade})"
p1 = Pessoa("Ana", 28)
print(p1)
print(repr(p1))
A saída é:
Ana (28 anos)
Pessoa(nome='Ana', idade=28)
Nota:
__str__()precisa retornar uma string — se você retornar outra coisa (um número, por exemplo), o Python levanta umTypeError.
eq: comparando objetos pelo conteúdo
Ao contrário de strings, números e listas, dois objetos de uma classe sua não são considerados iguais só por terem os mesmos dados — por padrão, == compara a identidade (se é o mesmo objeto na memória), não o conteúdo:
class Pessoa:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
p1 = Pessoa("Ana", 28)
p2 = Pessoa("Ana", 28)
print(p1 == p2)
A saída é:
False
Definindo __eq__, você decide o que conta como “igual” pra sua classe. O parâmetro other representa o objeto do lado direito da comparação:
class Pessoa:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
def __eq__(self, other):
return self.nome == other.nome and self.idade == other.idade
p1 = Pessoa("Ana", 28)
p2 = Pessoa("Ana", 28)
print(p1 == p2)
A saída é:
True
add: sobrecarga do operador +
Sem __add__, somar dois objetos da sua classe com + gera um TypeError — o Python não tem como adivinhar o que “somar duas pessoas” deveria significar. Definindo __add__, você decide: essa técnica se chama sobrecarga de operadores (operator overloading), e o mesmo padrão vale pra outros operadores (__sub__ pra -, __mul__ pra *, e assim por diante).
class Pessoa:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
def __add__(self, other):
return self.idade + other.idade
p1 = Pessoa("Ana", 28)
p2 = Pessoa("Bruno", 34)
print(p1 + p2)
A saída é:
62
len: personalizando o retorno de len()
class Empresa:
def __init__(self, nome, funcionarios):
self.nome = nome
self.funcionarios = funcionarios
def __len__(self):
return len(self.funcionarios)
empresa1 = Empresa("Acme", ["Ana", "Bruno", "Carla"])
print(len(empresa1))
A saída é:
3
Sem __len__, chamar len() num objeto Empresa levantaria TypeError — a função embutida len() não sabe, por conta própria, o que deveria contar num objeto seu.
lt: comparando com < e ordenando com sorted()
class Pessoa:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
def __lt__(self, other):
return self.idade < other.idade
p1 = Pessoa("Ana", 28)
p2 = Pessoa("Bruno", 34)
print(p1 < p2)
A saída é:
True
A vantagem de definir __lt__ vai além do < isolado: é ele que sorted() usa por baixo dos panos pra decidir a ordem de uma lista de objetos.
pessoas = [Pessoa("Carla", 41), Pessoa("Ana", 28), Pessoa("Bruno", 34)]
pessoas_ordenadas = sorted(pessoas)
for pessoa in pessoas_ordenadas:
print(pessoa.nome, pessoa.idade)
A saída é:
Ana 28
Bruno 34
Carla 41
Sem __lt__, chamar sorted() numa lista de objetos Pessoa também levantaria TypeError — sorted() precisa de algum jeito de comparar dois itens entre si.
contains: personalizando o operador in
class Empresa:
def __init__(self, nome, funcionarios):
self.nome = nome
self.funcionarios = funcionarios
def __contains__(self, nome_funcionario):
return nome_funcionario in self.funcionarios
empresa1 = Empresa("Acme", ["Ana", "Bruno", "Carla"])
print("Ana" in empresa1)
print("Daniel" in empresa1)
A saída é:
True
False
call: tornando um objeto chamável como função
Por fim, __call__ permite que um objeto seja “chamado” com parênteses, objeto(), igual você faria com uma função:
class ContadorDeCliques:
def __init__(self):
self.cliques = 0
def __call__(self):
self.cliques += 1
return self.cliques
contador = ContadorDeCliques()
print(contador())
print(contador())
print(contador())
A saída é:
1
2
3
contador() parece uma chamada de função comum, mas é o __call__ do objeto rodando por trás. A diferença pra uma função de verdade é que o objeto guarda seu próprio estado entre uma chamada e outra — aqui, o valor de self.cliques — algo que uma função sozinha não tem como fazer.
Nota: você não precisa implementar todos esses métodos em toda classe que criar. Use-os quando fizerem sentido pro comportamento do objeto: um
Ponto(x, y)se beneficia bastante de__eq__e__repr__, por exemplo, mas__call__só costuma fazer sentido pra objetos que representam “algo que pode ser executado”, como o contador acima.
Com __str__, __repr__, __eq__, __add__, __len__, __lt__, __contains__ e __call__, suas classes passam a se comportar como qualquer tipo nativo do Python — imprimem de forma legível, comparam, somam, têm tamanho e participam de sorted(), in e chamadas, exatamente como str, list e dict já fazem por baixo dos panos. No próximo post, você vai conhecer um jeito de ganhar boa parte desses métodos de graça: as dataclasses.
Fonte adaptada: Python Magic Methods, Python str(), Python repr(), Python eq(), Python add(), Python len(), Python lt(), Python contains(), Python call()
Comentários