Pular para o conteúdo

    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:

    avancado/cap45_descritores_slots.pylinhas 10 a 40
    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__)
    
    Saída
    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:

    avancado/cap45_descritores_slots.pylinhas 42 a 48
    class A:
        def metodo(self):
            pass
    
    
    bruto = A.__dict__["metodo"]
    print(type(bruto).__name__, hasattr(bruto, "__get__"))
    
    Saída
    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:

    avancado/cap45_descritores_slots.pylinhas 53 a 82
    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)
    
    Saída
    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:

    avancado/cap45_descritores_slots.pylinhas 87 a 105
    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())
    
    Saída
    oi type
    True
    

    Evite metaclasses

    Eu quase nunca escrevo uma. Para registrar subclasses, use __init_subclass__. Para validar atributos, use descritores ou dataclass. 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.