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 que salario.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