Operadores sobrescritos com métodos dunder
1. Introdução aos Métodos Dunder para Operadores
Métodos dunder (double underscore) são métodos especiais em Python identificados por nomes que começam e terminam com dois underscores, como __init__, __str__ e __add__. Eles formam a base do modelo de dados da linguagem, permitindo que objetos definam seu comportamento em relação a operadores nativos.
Quando você escreve a + b, Python internamente chama a.__add__(b). Sobrescrever esses métodos permite que suas classes personalizadas respondam a operadores de forma intuitiva. Isso traz três benefícios principais:
- Legibilidade: código como
salario + bonusé mais claro quesalario.calcular_com_bonus(bonus) - Expressividade: objetos se comportam como tipos nativos, reduzindo barreiras conceituais
- Consistência: a mesma sintaxe funciona para tipos built-in e classes customizadas
Python suporta sobrescrita para operadores aritméticos (+, -, *, /), comparação (==, <, >), lógicos (and, or via __bool__), conversão (int(), float()), acesso ([], in), entre outros.
2. Sobrescrita de Operadores Aritméticos
Vamos criar uma classe Dinheiro que representa valores monetários com moeda:
class Dinheiro:
def __init__(self, valor: float, moeda: str = "BRL"):
self.valor = round(valor, 2)
self.moeda = moeda
def __repr__(self):
return f"Dinheiro({self.valor}, '{self.moeda}')"
# Operadores aritméticos básicos
def __add__(self, outro):
if isinstance(outro, Dinheiro):
if self.moeda != outro.moeda:
raise ValueError("Moedas diferentes não podem ser somadas")
return Dinheiro(self.valor + outro.valor, self.moeda)
elif isinstance(outro, (int, float)):
return Dinheiro(self.valor + outro, self.moeda)
return NotImplemented
def __sub__(self, outro):
if isinstance(outro, Dinheiro):
if self.moeda != outro.moeda:
raise ValueError("Moedas diferentes não podem ser subtraídas")
return Dinheiro(self.valor - outro.valor, self.moeda)
elif isinstance(outro, (int, float)):
return Dinheiro(self.valor - outro, self.moeda)
return NotImplemented
def __mul__(self, vezes):
if isinstance(vezes, (int, float)):
return Dinheiro(self.valor * vezes, self.moeda)
return NotImplemented
def __truediv__(self, divisor):
if isinstance(divisor, (int, float)):
return Dinheiro(self.valor / divisor, self.moeda)
return NotImplemented
# Operadores reversos (quando o objeto está à direita do operador)
def __radd__(self, outro):
return self.__add__(outro)
def __rsub__(self, outro):
return Dinheiro(outro - self.valor, self.moeda)
# Operadores in-place para += e *=
def __iadd__(self, outro):
if isinstance(outro, Dinheiro):
if self.moeda != outro.moeda:
raise ValueError("Moedas diferentes")
self.valor += outro.valor
elif isinstance(outro, (int, float)):
self.valor += outro
self.valor = round(self.valor, 2)
return self
def __imul__(self, vezes):
if isinstance(vezes, (int, float)):
self.valor *= vezes
self.valor = round(self.valor, 2)
return self
return NotImplemented
# Exemplo de uso
salario = Dinheiro(5000.00)
bonus = Dinheiro(800.00)
total = salario + bonus
print(total) # Dinheiro(5800.0, 'BRL')
# Operador in-place
salario += Dinheiro(200.00)
print(salario) # Dinheiro(5200.0, 'BRL')
# Multiplicação
dobro = salario * 2
print(dobro) # Dinheiro(10400.0, 'BRL')
3. Operadores de Comparação e Ordenação
Implementar comparações permite usar sorted(), max(), min() e operadores como ==, <:
class Pessoa:
def __init__(self, nome: str, idade: int):
self.nome = nome
self.idade = idade
def __repr__(self):
return f"Pessoa('{self.nome}', {self.idade})"
# Igualdade e diferença
def __eq__(self, outro):
if not isinstance(outro, Pessoa):
return NotImplemented
return self.nome == outro.nome and self.idade == outro.idade
def __ne__(self, outro):
return not self.__eq__(outro)
# Ordenação por idade
def __lt__(self, outro):
if not isinstance(outro, Pessoa):
return NotImplemented
return self.idade < outro.idade
def __le__(self, outro):
if not isinstance(outro, Pessoa):
return NotImplemented
return self.idade <= outro.idade
def __gt__(self, outro):
if not isinstance(outro, Pessoa):
return NotImplemented
return self.idade > outro.idade
def __ge__(self, outro):
if not isinstance(outro, Pessoa):
return NotImplemented
return self.idade >= outro.idade
# Testando
p1 = Pessoa("Ana", 30)
p2 = Pessoa("Carlos", 25)
p3 = Pessoa("Ana", 30)
print(p1 == p3) # True
print(p1 > p2) # True
pessoas = [p1, p2, Pessoa("Beatriz", 28)]
for p in sorted(pessoas):
print(p) # Ordenado por idade
Decorador @total_ordering
Para reduzir código repetitivo, o módulo functools oferece @total_ordering:
from functools import total_ordering
@total_ordering
class Produto:
def __init__(self, nome: str, preco: float):
self.nome = nome
self.preco = preco
def __eq__(self, outro):
if not isinstance(outro, Produto):
return NotImplemented
return self.preco == outro.preco
def __lt__(self, outro):
if not isinstance(outro, Produto):
return NotImplemented
return self.preco < outro.preco
def __repr__(self):
return f"Produto('{self.nome}', R${self.preco:.2f})"
# Agora todos os operadores de comparação funcionam
p1 = Produto("Notebook", 3500.00)
p2 = Produto("Mouse", 150.00)
print(p1 >= p2) # True (gerado automaticamente)
print(p1 > p2) # True
4. Operadores Unários e de Conversão
Operadores unários atuam em um único objeto. Conversões permitem que seu objeto seja usado com int(), float(), bool():
class Temperatura:
def __init__(self, celsius: float):
self.celsius = celsius
def __repr__(self):
return f"{self.celsius:.1f}°C"
# Operadores unários
def __neg__(self):
return Temperatura(-self.celsius)
def __pos__(self):
return Temperatura(abs(self.celsius))
def __abs__(self):
return Temperatura(abs(self.celsius))
def __invert__(self):
# Inverte sinal como curiosidade
return Temperatura(-self.celsius)
# Conversão para tipos nativos
def __int__(self):
return int(self.celsius)
def __float__(self):
return float(self.celsius)
def __bool__(self):
# Zero graus Celsius é considerado False
return self.celsius != 0.0
temp = Temperatura(-5.0)
print(-temp) # 5.0°C
print(abs(temp)) # 5.0°C
print(int(temp)) # -5
print(bool(temp)) # True (diferente de zero)
print(bool(Temperatura(0))) # False
5. Operadores de Acesso e Indexação
Implementar __getitem__, __setitem__, __contains__ e __iter__ torna seus objetos tão flexíveis quanto listas e dicionários:
class ListaMultidimensional:
def __init__(self, *dimensoes):
self.dimensoes = dimensoes
self._dados = [0] * self._calcular_tamanho()
def _calcular_tamanho(self):
total = 1
for d in self.dimensoes:
total *= d
return total
def _indice_para_posicao(self, indices):
if len(indices) != len(self.dimensoes):
raise IndexError("Número de índices incompatível")
pos = 0
for i, idx in enumerate(indices):
if idx < 0 or idx >= self.dimensoes[i]:
raise IndexError("Índice fora dos limites")
pos = pos * self.dimensoes[i] + idx
return pos
def __getitem__(self, indices):
if not isinstance(indices, tuple):
indices = (indices,)
return self._dados[self._indice_para_posicao(indices)]
def __setitem__(self, indices, valor):
if not isinstance(indices, tuple):
indices = (indices,)
self._dados[self._indice_para_posicao(indices)] = valor
def __delitem__(self, indices):
if not isinstance(indices, tuple):
indices = (indices,)
self._dados[self._indice_para_posicao(indices)] = 0
def __contains__(self, item):
return item in self._dados
def __iter__(self):
return iter(self._dados)
def __next__(self):
return next(iter(self._dados))
# Exemplo: matriz 3x3
matriz = ListaMultidimensional(3, 3)
matriz[0, 0] = 10
matriz[1, 2] = 20
print(matriz[0, 0]) # 10
print(20 in matriz) # True
for valor in matriz:
print(valor, end=" ") # 10 0 0 0 0 0 0 0 20
6. Operadores Bit a Bit e de Deslocamento
Operadores bitwise (&, |, ^, <<, >>) podem ser reaproveitados para conceitos não-bitwise, como permissões:
class Permissoes:
LEITURA = 0b001
ESCRITA = 0b010
EXECUCAO = 0b100
def __init__(self, permissoes: int = 0):
self._permissoes = permissoes
def __and__(self, outro):
if isinstance(outro, Permissoes):
return Permissoes(self._permissoes & outro._permissoes)
elif isinstance(outro, int):
return Permissoes(self._permissoes & outro)
return NotImplemented
def __or__(self, outro):
if isinstance(outro, Permissoes):
return Permissoes(self._permissoes | outro._permissoes)
elif isinstance(outro, int):
return Permissoes(self._permissoes | outro)
return NotImplemented
def __xor__(self, outro):
if isinstance(outro, Permissoes):
return Permissoes(self._permissoes ^ outro._permissoes)
elif isinstance(outro, int):
return Permissoes(self._permissoes ^ outro)
return NotImplemented
def __lshift__(self, bits):
return Permissoes(self._permissoes << bits)
def __rshift__(self, bits):
return Permissoes(self._permissoes >> bits)
def __invert__(self):
return Permissoes(~self._permissoes & 0b111)
def __repr__(self):
permissoes_str = []
if self._permissoes & self.LEITURA:
permissoes_str.append("leitura")
if self._permissoes & self.ESCRITA:
permissoes_str.append("escrita")
if self._permissoes & self.EXECUCAO:
permissoes_str.append("execução")
return f"Permissoes({', '.join(permissoes_str)})" if permissoes_str else "Permissoes(nenhuma)"
# Uso prático
user = Permissoes(Permissoes.LEITURA | Permissoes.ESCRITA)
print(user) # Permissoes(leitura, escrita)
# Verificar permissão
if user & Permissoes.LEITURA:
print("Pode ler")
# Adicionar permissão
user |= Permissoes.EXECUCAO
print(user) # Permissoes(leitura, escrita, execução)
# Remover permissão
user ^= Permissoes.ESCRITA
print(user) # Permissoes(leitura, execução)
7. Boas Práticas e Cuidados ao Sobrescrever Operadores
Princípio da Menor Surpresa
Sobrescreva operadores apenas quando o comportamento for natural e esperado. Um operador + em uma classe Cachorro que retorna um filhote é intuitivo; o mesmo operador retornando uma string JSON seria surpreendente.
Evitar Efeitos Colaterais
Operadores como __add__ normalmente não alteram o objeto original. Retorne uma nova instância:
# Correto
def __add__(self, outro):
return Dinheiro(self.valor + outro.valor, self.moeda)
# Incorreto (efeito colateral)
def __add__(self, outro):
self.valor += outro.valor # Modifica self!
return self
Tratar Tipos Incompatíveis
Sempre verifique tipos e retorne NotImplemented para tipos não suportados. Isso permite que Python tente o operador reverso do outro objeto:
def __add__(self, outro):
if isinstance(outro, MinhaClasse):
# processar
pass
return NotImplemented # Deixa Python tentar __radd__ do outro
Documentar Comportamento Customizado
Documente claramente como seus operadores funcionam, especialmente se o comportamento desvia do esperado:
class Matriz:
"""Multiplicação de matrizes usa @ (__matmul__), não *."""
def __mul__(self, escalar):
"""Multiplicação escalar, não multiplicação de matrizes."""
Seguindo essas práticas, seus objetos se integrarão naturalmente ao ecossistema Python, proporcionando código mais limpo e expressivo.
Referências
- Python Data Model - Official Documentation — Documentação oficial sobre métodos especiais e sobrescrita de operadores em Python.
- Python's Magic Methods: A Guide — Tutorial completo da Real Python sobre métodos dunder com exemplos práticos.
- functools.total_ordering - Python Docs — Documentação oficial do decorador que gera automaticamente métodos de comparação.
- Operator Overloading in Python — Artigo técnico do GeeksforGeeks com exemplos de sobrescrita de operadores aritméticos e de comparação.
- Python Dunder Methods Cheat Sheet — Referência rápida com lista completa de métodos dunder e seus propósitos.
- Emulating Numeric Types - Python Docs — Seção específica da documentação oficial sobre emulação de tipos numéricos com operadores.