len(x) chama x.__len__(). a + b chama a.__add__(b). for i in x chama x.__iter__(). A sintaxe do Python é um conjunto de perguntas, e os métodos dunder são como o seu objeto responde.
Implemente os certos e sua classe deixa de parecer aparafusada por fora.
Um tipo de valor, feito direito
class Money:
def __init__(self, amount, currency='BRL'):
self.amount, self.currency = amount, currency
def __repr__(self):
return f"Money({self.amount!r}, {self.currency!r})"
def __eq__(self, other):
if not isinstance(other, Money): return NotImplemented
return (self.amount, self.currency) == (other.amount, other.currency)
def __hash__(self):
return hash((self.amount, self.currency))
def __add__(self, other):
if not isinstance(other, Money): return NotImplemented
if other.currency != self.currency:
raise ValueError(f"cannot add {other.currency} to {self.currency}")
return Money(self.amount + other.amount, self.currency)
def __lt__(self, other):
return self.amount < other.amount
a, b = Money(10), Money(10)
print(a == b, a is b)
print(a + Money(5))
print(sorted([Money(30), Money(10), Money(20)]))
print({Money(10), Money(10), Money(20)})
Ele imprime:
True False
Money(15, 'BRL')
[Money(10, 'BRL'), Money(20, 'BRL'), Money(30, 'BRL')]
{Money(10, 'BRL'), Money(20, 'BRL')}
Quatro ganhos distintos de quatro métodos. Igualdade por valor e não por identidade. Adição. Ordenação sem nenhuma função key, só a partir do __lt__. Remoção de duplicados num set, a partir do __hash__.
__eq__ sem __hash__ quebra o seu objeto
Defina __eq__ e o Python coloca __hash__ como None, porque dois objetos que comparam iguais precisam ter o mesmo hash e o Python não tem como adivinhar como:
class NoHash:
def __init__(self, v): self.v = v
def __eq__(self, other): return self.v == other.v
try:
{NoHash(1)}
except TypeError as err:
print(type(err).__name__ + ':', err)
Ele imprime:
TypeError: unhashable type: 'NoHash'
O objeto não pode mais entrar num set nem ser chave de dicionário. Se a sua classe é um tipo de valor, defina os dois, sobre os mesmos campos, como o Money faz. Se ela é genuinamente mutável, deixar sem hash é o resultado correto.
Devolva NotImplemented, não False
print(a == 'not money')
try:
a + 5
except TypeError as err:
print(type(err).__name__ + ':', err)
Ele imprime:
False
unsupported operand type(s) for +: 'Money' and 'int'
Devolver NotImplemented diz ao Python “não sei lidar com isso, tente o outro operando”. O Python então recorre ao plano B — comparação por identidade no caso de ==, e um TypeError claro no caso de +. Devolva False do __eq__ e você tira do outro tipo a chance de responder.
Repare que NotImplemented é um valor que você devolve. NotImplementedError é uma exceção que você levanta, para métodos abstratos. Confundir os dois é comum.
Contêineres ganham dois comportamentos de graça
class Deck:
def __init__(self, cards): self._cards = list(cards)
def __len__(self): return len(self._cards)
def __getitem__(self, i): return self._cards[i]
d = Deck(['A', 'K', 'Q', 'J'])
print(len(d), d[0], d[-1], d[1:3])
print([c for c in d]) # iteration for free from __getitem__
print('K' in d) # membership for free too
Ele imprime:
4 A J ['K', 'Q']
['A', 'K', 'Q', 'J']
True
Dois métodos compraram indexação, índice negativo, fatiamento, iteração e in. Iteração e pertencimento vêm de graça porque o Python cai para o __getitem__ quando __iter__ e __contains__ não existem. O fatiamento funciona porque o índice é repassado direto para a lista.
Valor verdade
class Basket:
def __init__(self, items): self.items = items
def __len__(self): return len(self.items)
print(bool(Basket([])), bool(Basket(['apple'])))
Ele imprime:
False True
if basket: agora significa “se a cesta tem alguma coisa dentro”. O Python pergunta ao __bool__, e cai para o __len__. Sem nenhum dos dois, todo objeto é verdadeiro — que é por que if meu_objeto: numa classe sem __len__ é sempre True e nunca a verificação que você queria.
Objetos chamáveis
class Multiplier:
def __init__(self, by): self.by = by
def __call__(self, x): return x * self.by
triple = Multiplier(3)
print(triple(5), list(map(triple, [1, 2, 3])))
Ele imprime:
15 [3, 6, 9]
Um objeto que se comporta como função mas carrega estado. É isso que faz decorators-como-classe e callbacks configuráveis funcionarem.
O que lembrar
-
__repr__sempre;__eq__e__hash__juntos para tipos de valor. -
Devolva
NotImplementeddos operadores que você não sabe tratar, nuncaFalse. -
__len__e__getitem__compram iteração, pertencimento e fatiamento. -
__bool__(ou__len__) para queif obj:signifique alguma coisa.
Não implemente dunders que você não vai usar. Cada um é uma promessa sobre como o seu objeto se comporta, e uma promessa que ninguém pediu é só mais uma para manter verdadeira.