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:
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)
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:
from collections.abc import Container, Iterable, Sequence, Sized
print(isinstance(baralho, Sized), isinstance(baralho, Iterable), isinstance(baralho, Container))
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:
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")))
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:
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)
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:
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}")
8080 padrão True
10.50 10.5
Cuidado com __getattr__ e atributos internos
No
__getattr__acima, se_dadosainda não existir (durantecopyoupickle, por exemplo), a leitura deself._dadoschama__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__.