Pular para o conteúdo

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

    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)
    
    Saída
    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:

    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)))
    
    Saída
    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:

    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))
    
    Saída
    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__:

    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"))
    
    Saída
    [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".