Blog

Métodos dunder em Python

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 NotImplemented dos operadores que você não sabe tratar, nunca False.

  • __len__ e __getitem__ compram iteração, pertencimento e fatiamento.

  • __bool__ (ou __len__) para que if 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.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.