Capítulo 31, Intermediário
Métodos especiais
Os métodos com sublinhado duplo (os *dunders*) são o gancho entre os seus objetos e a sintaxe da linguagem: `+`, `len()`, `in`, `==`, `print()`.
Código deste capítulo: intermediario/cap31_dunders.py
Operadores e comparação
Quando você escreve a + b, o Python chama a.__add__(b). Implementar os métodos especiais faz o seu objeto se comportar como um tipo nativo. Duas regras que eu sigo: devolver NotImplemented (e não levantar erro) quando o outro tipo não é suportado, e definir __hash__ sempre que definir __eq__:
class Vetor:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f"Vetor({self.x}, {self.y})"
def __add__(self, outro):
if not isinstance(outro, Vetor):
return NotImplemented
return Vetor(self.x + outro.x, self.y + outro.y)
def __mul__(self, escalar):
if not isinstance(escalar, (int, float)):
return NotImplemented
return Vetor(self.x * escalar, self.y * escalar)
__rmul__ = __mul__
def __eq__(self, outro):
if not isinstance(outro, Vetor):
return NotImplemented
return (self.x, self.y) == (outro.x, outro.y)
def __hash__(self):
return hash((self.x, self.y))
def __bool__(self):
return bool(self.x or self.y)
def __abs__(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
v1, v2 = Vetor(1, 2), Vetor(3, 4)
print(v1 + v2, v1 * 3, 3 * v1)
print(v1 == Vetor(1, 2), bool(Vetor(0, 0)), abs(v2))
print(len({v1, Vetor(1, 2)}))
try:
v1 + 5
except TypeError as erro:
print(erro)
Vetor(4, 6) Vetor(3, 6) Vetor(3, 6)
True False 5.0
1
unsupported operand type(s) for +: 'Vetor' and 'int'
O __rmul__ faz 3 * v1 funcionar: quando int.__mul__ não sabe o que fazer com um Vetor, o Python tenta o método "refletido" do operando da direita. Sem __hash__, um objeto que define __eq__ deixa de poder entrar em conjuntos e servir de chave de dicionário.
Fazer o objeto virar um contêiner
Com __len__, __getitem__ e __contains__, o seu objeto se comporta como uma sequência. E, se existe __getitem__ com índices a partir de zero, a iteração, reversed() e o in funcionam sozinhos:
class Playlist:
def __init__(self, *musicas):
self._musicas = list(musicas)
def __len__(self):
return len(self._musicas)
def __getitem__(self, indice):
return self._musicas[indice]
def __contains__(self, musica):
return musica in self._musicas
p = Playlist("a", "b", "c")
print(len(p), p[0], p[-1], "b" in p)
print(list(p), list(reversed(p)))
3 a c True
['a', 'b', 'c'] ['c', 'b', 'a']
Objetos chamáveis
Com __call__, a instância pode ser chamada como uma função. Isso é útil quando a "função" precisa carregar estado:
class Multiplicador:
def __init__(self, fator):
self.fator = fator
def __call__(self, valor):
return valor * self.fator
dobro = Multiplicador(2)
print(dobro(21), callable(dobro))
42 True
Ordenação sem escrever seis métodos
Para tornar objetos ordenáveis, o decorador total_ordering completa os operadores de comparação a partir de __eq__ e __lt__:
from functools import total_ordering
@total_ordering
class Versao:
def __init__(self, texto):
self.partes = tuple(int(p) for p in texto.split("."))
def __eq__(self, outro):
return self.partes == outro.partes
def __lt__(self, outro):
return self.partes < outro.partes
def __repr__(self):
return "Versao(" + ".".join(map(str, self.partes)) + ")"
print(sorted([Versao("1.10.0"), Versao("1.2.0"), Versao("1.9.5")]))
print(Versao("2.0") > Versao("1.9"))
[Versao(1.2.0), Versao(1.9.5), Versao(1.10.0)]
True
Repare por que comparar versões como texto dá errado: "1.10" vem antes de "1.9" na ordem alfabética. Comparar tuplas de inteiros resolve.
Exercício 1
Uma classe Dinheiro
Crie Dinheiro(centavos) que suporte +, igualdade, ordenação e str() no formato "R$ 10,50".