Skip to content

    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__:

    intermediario/cap31_dunders.pylinhas 10 a 51
    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)
    
    Output
    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:

    intermediario/cap31_dunders.pylinhas 56 a 72
    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)))
    
    Output
    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:

    intermediario/cap31_dunders.pylinhas 77 a 86
    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))
    
    Output
    42 True
    

    Sorting without writing six methods

    To make objects sortable, the decorator total_ordering completes the comparison operators from __eq__ and __lt__:

    intermediario/cap31_dunders.pylinhas 91 a 110
    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"))
    
    Output
    [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".