Capítulo 45, Avançado
Descritores, __slots__ e metaclasses
O mecanismo que faz `property`, métodos e `dataclass` funcionarem é público, e você pode usá-lo. Mas o mais importante deste capítulo é saber quando **não** usar.
Código deste capítulo: avancado/cap45_descritores_slots.py
O protocolo do descritor
Um descritor é um objeto, guardado em uma classe, que define __get__, __set__ ou __delete__. Quando você lê ou grava o atributo, o Python delega a esse objeto. O __set_name__ avisa ao descritor o nome com que ele foi atribuído:
class Positivo:
def __set_name__(self, dono, nome):
self.nome = "_" + nome
def __get__(self, instancia, dono=None):
if instancia is None:
return self
return getattr(instancia, self.nome)
def __set__(self, instancia, valor):
if valor <= 0:
raise ValueError(f"{self.nome[1:]} deve ser positivo")
setattr(instancia, self.nome, valor)
class Item:
preco = Positivo()
quantidade = Positivo()
def __init__(self, preco, quantidade):
self.preco = preco
self.quantidade = quantidade
item = Item(10, 2)
print(item.preco * item.quantidade)
try:
item.quantidade = 0
except ValueError as erro:
print(erro)
print(type(Item.preco).__name__)
20
quantidade deve ser positivo
Positivo
A vantagem sobre @property é o reuso: a regra "positivo" foi escrita uma vez e aplicada a dois atributos. Com property, seriam dois pares de getter e setter.
Descritores estão por toda parte. A própria property é um. E as funções também: é o __get__ da função que a transforma em método ligado ao objeto:
class A:
def metodo(self):
pass
bruto = A.__dict__["metodo"]
print(type(bruto).__name__, hasattr(bruto, "__get__"))
function True
__slots__
Por padrão, cada instância guarda os atributos em um dicionário (__dict__). Declarar __slots__ troca esse dicionário por um espaço fixo, o que economiza memória quando há muitas instâncias, e impede a criação de atributos novos:
import tracemalloc
class Comum:
def __init__(self, x, y):
self.x, self.y = x, y
class Enxuta:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x, self.y = x, y
def medir(classe):
tracemalloc.start()
objetos = [classe(i, i) for i in range(50_000)]
atual, _ = tracemalloc.get_traced_memory()
tracemalloc.stop()
return atual
print(medir(Enxuta) < medir(Comum))
e = Enxuta(1, 2)
print(hasattr(e, "__dict__"))
try:
e.z = 3
except AttributeError as erro:
print(erro)
True
False
'Enxuta' object has no attribute 'z'
Eu só uso __slots__ quando medi um problema de memória com milhões de objetos. Para o resto, a economia não compensa a rigidez. A forma mais simples de obter o benefício é @dataclass(slots=True).
Metaclasses
Em Python, uma classe também é um objeto, e quem a cria é a metaclasse, por padrão type. Dá para criar classes dinamicamente e interceptar a criação de instâncias:
Ponto = type("Ponto", (), {"x": 0, "ola": lambda self: "oi"})
print(Ponto().ola(), type(Ponto).__name__)
class Singleton(type):
_instancias = {}
def __call__(cls, *args, **kwargs):
if cls not in Singleton._instancias:
Singleton._instancias[cls] = super().__call__(*args, **kwargs)
return Singleton._instancias[cls]
class Configuracao(metaclass=Singleton):
def __init__(self):
self.valores = {}
print(Configuracao() is Configuracao())
oi type
True
Evite metaclasses
Eu quase nunca escrevo uma. Para registrar subclasses, use
__init_subclass__. Para validar atributos, use descritores oudataclass. Para um objeto único, use uma instância no nível do módulo (módulos já são carregados uma vez só). A metaclasse é o recurso mais difícil de depurar e de combinar com outras, e raramente é a ferramenta certa.
Exercício 1
Um descritor de texto curto
Escreva o descritor TextoCurto(limite) que levante ValueError se o texto passar do limite, e use-o em Perfil.bio com limite 5.