Pular para o conteúdo

    Capítulo 42, Avançado

    O modelo de dados e os protocolos

    Quase tudo que parece mágica em Python é o interpretador chamando métodos especiais nos seus objetos. Quem domina esses protocolos escreve classes que se encaixam na linguagem.

    Código deste capítulo: avancado/cap42_modelo_dados.py

    Uma sequência com dois métodos

    Se um objeto tem __len__ e __getitem__, ele já se comporta como uma sequência: aceita índice, fatia, in, iteração, reversed e random.choice. Nenhuma herança é necessária. É o duck typing levado a sério:

    avancado/cap42_modelo_dados.pylinhas 10 a 34
    import random
    from collections import namedtuple
    
    Carta = namedtuple("Carta", ["valor", "naipe"])
    
    
    class Baralho:
        valores = [str(n) for n in range(2, 11)] + list("JQKA")
        naipes = ["paus", "ouros", "copas", "espadas"]
    
        def __init__(self):
            self._cartas = [Carta(v, n) for n in self.naipes for v in self.valores]
    
        def __len__(self):
            return len(self._cartas)
    
        def __getitem__(self, posicao):
            return self._cartas[posicao]
    
    
    baralho = Baralho()
    print(len(baralho), baralho[0], baralho[-1])
    print(baralho[:3])
    print(Carta("Q", "copas") in baralho)
    print(random.choice(baralho) in baralho)
    
    Saída
    52 Carta(valor='2', naipe='paus') Carta(valor='A', naipe='espadas')
    [Carta(valor='2', naipe='paus'), Carta(valor='3', naipe='paus'), Carta(valor='4', naipe='paus')]
    True
    True
    

    O in funciona porque, sem __contains__ nem __iter__, o Python cai no protocolo antigo de iteração: chama __getitem__ com 0, 1, 2... até receber IndexError.

    Ser, de fato, uma Sequence

    O fato de o objeto se comportar como sequência não o torna uma Sequence para quem verifica o tipo. As classes abstratas de collections.abc testam a presença de métodos específicos, e essa verificação é mais exigente do que a iteração real:

    avancado/cap42_modelo_dados.pylinhas 39 a 41
    from collections.abc import Container, Iterable, Sequence, Sized
    
    print(isinstance(baralho, Sized), isinstance(baralho, Iterable), isinstance(baralho, Container))
    
    Saída
    True False False
    

    Ele é Sized (tem __len__), mas não é Iterable (falta __iter__), embora o for funcione. A lição prática: para saber se algo é iterável, tente iter(obj). E, se você quer o contrato completo, herde de Sequence: você implementa só __len__ e __getitem__, e ganha de graça __contains__, __iter__, __reversed__, index e count:

    avancado/cap42_modelo_dados.pylinhas 43 a 56
    class BaralhoSeq(Sequence):
        def __init__(self):
            self._cartas = Baralho()._cartas
    
        def __len__(self):
            return len(self._cartas)
    
        def __getitem__(self, posicao):
            return self._cartas[posicao]
    
    
    b = BaralhoSeq()
    print(isinstance(b, Sequence), Carta("A", "copas") in b)
    print(b.index(Carta("3", "paus")), b.count(Carta("2", "paus")))
    
    Saída
    True True
    1 1
    

    O contrato entre __eq__ e __hash__

    Dois objetos iguais precisam ter o mesmo hash, e o hash de um objeto não pode mudar enquanto ele estiver em um conjunto ou for chave de dicionário. Quebrar a segunda regra produz um dos bugs mais confusos da linguagem: o objeto está no conjunto, mas a busca não o encontra:

    avancado/cap42_modelo_dados.pylinhas 61 a 89
    class Chave:
        def __init__(self, valor):
            self.valor = valor
    
        def __eq__(self, outro):
            return self.valor == outro.valor
    
    
    try:
        {Chave(1)}
    except TypeError as erro:
        print(erro)
    
    
    class Mutavel:
        def __init__(self, v):
            self.v = v
    
        def __eq__(self, outro):
            return self.v == outro.v
    
        def __hash__(self):
            return hash(self.v)
    
    
    m = Mutavel(1)
    conjunto = {m}
    m.v = 2
    print(m in conjunto)
    
    Saída
    unhashable type: 'Chave'
    False
    

    A primeira classe define __eq__ e, por isso, o Python remove o __hash__ dela. A segunda define o hash a partir de um campo mutável: depois de m.v = 2, o objeto continua no conjunto, guardado no "balde" do hash antigo, e a consulta procura no balde novo. A regra que eu sigo: só defina __hash__ para objetos imutáveis, e calcule o hash apenas de campos imutáveis.

    Acesso dinâmico a atributos

    O método __getattr__ só é chamado quando a busca normal falha. É a base de proxies, objetos de configuração e clientes de API dinâmicos. O __format__ controla o que vem depois dos dois pontos em uma f-string:

    avancado/cap42_modelo_dados.pylinhas 94 a 117
    class Config:
        def __init__(self, **dados):
            self._dados = dados
    
        def __getattr__(self, nome):
            try:
                return self._dados[nome]
            except KeyError:
                raise AttributeError(nome) from None
    
    
    c = Config(porta=8080)
    print(c.porta, getattr(c, "host", "padrão"), hasattr(c, "porta"))
    
    
    class Dinheiro:
        def __init__(self, centavos):
            self.centavos = centavos
    
        def __format__(self, formato):
            return format(self.centavos / 100, formato or ".2f")
    
    
    print(f"{Dinheiro(1050)}", f"{Dinheiro(1050):.1f}")
    
    Saída
    8080 padrão True
    10.50 10.5
    

    Cuidado com __getattr__ e atributos internos

    No __getattr__ acima, se _dados ainda não existir (durante copy ou pickle, por exemplo), a leitura de self._dados chama __getattr__ de novo, e a recursão não tem fim. Em código de produção, trate os nomes que começam com sublinhado de forma explícita.

    Exercício 1

    Um intervalo que se comporta como range

    Escreva a classe Intervalo(inicio, fim) com __len__, __getitem__ (aceitando índices negativos e levantando IndexError fora do limite) e __contains__.