Chapter 31, Intermediate
Special methods
Methods with double underscores (the *dunders*) are the hook between your objects and the language syntax: `+`, `len()`, `in`, `==`, `print()`.
Code for this chapter: intermediario/cap31_dunders.py
Operators and comparison
When you write a + b, Python calls a.__add__(b). Implementing special methods makes your object behave like a native type. Two rules I follow: return NotImplemented (and do not raise an error) when the other type is not supported, and define __hash__ whenever defining __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'
The __rmul__ makes 3 * v1 work: when int.__mul__ does not know what to do with a Vetor, Python tries the "reflected" method of the right operand. Without __hash__, an object that defines __eq__ can no longer enter sets and serve as a dictionary key.
Making the object a container
With __len__, __getitem__ and __contains__, your object behaves like a sequence. And, if there is __getitem__ with zero-based indices, iteration, reversed() and the in work on their own:
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']
Callable objects
With __call__, the instance can be called like a function. This is useful when the "function" needs to carry state:
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
Sorting without writing six methods
To make objects sortable, the decorator total_ordering completes the comparison operators from __eq__ and __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
Notice why comparing versions as text goes wrong: "1.10" comes before "1.9" in alphabetical order. Comparing tuples of integers solves it.
Exercise 1
A Money class
Create Dinheiro(centavos) that supports +, equality, sorting and str() in the format "R$ 10,50".