Descritores: o protocolo por trás de @property

1. O que são Descritores?

Descritores em Python são objetos que implementam um ou mais métodos do protocolo de descritores: __get__, __set__ e __delete__. Eles permitem controlar como atributos são acessados, modificados ou deletados em uma classe.

Existem dois tipos de descritores:
- Descritores de dado: implementam __set__ ou __delete__
- Descritores não-dado: implementam apenas __get__

class LogDescriptor:
    """Descritor simples que loga acessos a atributos"""
    def __get__(self, obj, objtype=None):
        print(f"Acessando atributo via {self.__class__.__name__}")
        return 42

    def __set__(self, obj, value):
        print(f"Atribuindo valor {value} ao atributo")
        obj.__dict__[self._name] = value

class MinhaClasse:
    attr = LogDescriptor()

obj = MinhaClasse()
print(obj.attr)  # Loga o acesso e retorna 42

2. O Protocolo de Descritores em Detalhe

O protocolo consiste em três métodos:

class DescritorCompleto:
    def __get__(self, obj, objtype=None):
        """Chamado quando o atributo é acessado"""
        if obj is None:
            return self
        return obj.__dict__.get(self._name, "valor padrão")

    def __set__(self, obj, value):
        """Chamado quando o atributo é modificado"""
        obj.__dict__[self._name] = value

    def __delete__(self, obj):
        """Chamado quando o atributo é deletado"""
        del obj.__dict__[self._name]

A ordem de resolução de atributos é:
1. Descritores de dado da classe
2. Atributos de instância (__dict__)
3. Descritores não-dado da classe

3. O Decorador @property como Caso de Uso Clássico

O @property é um descritor embutido que transforma métodos em atributos gerenciados:

class Temperatura:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, valor):
        if valor < -273.15:
            raise ValueError("Temperatura abaixo do zero absoluto")
        self._celsius = valor

    @celsius.deleter
    def celsius(self):
        print("Deletando temperatura...")
        del self._celsius

# Implementação simplificada de property como descritor
class MinhaProperty:
    def __init__(self, fget=None, fset=None, fdel=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("atributo não legível")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("atributo não modificável")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("atributo não deletável")
        self.fdel(obj)

4. Descritores na Prática: Validadores e Transformadores

class IntegerField:
    def __set_name__(self, owner, name):
        self._name = f"_{name}"

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self._name, 0)

    def __set__(self, obj, value):
        if not isinstance(value, int):
            raise TypeError(f"Esperado int, recebido {type(value).__name__}")
        if value < 0:
            raise ValueError("Valor não pode ser negativo")
        setattr(obj, self._name, value)

class StringField:
    def __set_name__(self, owner, name):
        self._name = f"_{name}"

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self._name, "")

    def __set__(self, obj, value):
        if not isinstance(value, str):
            raise TypeError(f"Esperado string, recebido {type(value).__name__}")
        setattr(obj, self._name, value.strip().lower())

class LazyProperty:
    def __init__(self, func):
        self.func = func
        self.name = func.__name__

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        valor = self.func(obj)
        setattr(obj, self.name, valor)
        return valor

class Produto:
    nome = StringField()
    quantidade = IntegerField()

    def __init__(self, nome, quantidade):
        self.nome = nome
        self.quantidade = quantidade

    @LazyProperty
    def descricao_completa(self):
        print("Calculando descrição...")
        return f"{self.nome} (estoque: {self.quantidade})"

p = Produto("  CAMISETA  ", 10)
print(p.nome)  # "camiseta" (normalizado)
print(p.descricao_completa)  # Calcula e cacheia
print(p.descricao_completa)  # Retorna do cache

5. Descritores e Herança: Cuidados e Padrões

class ValidatedField:
    def __set_name__(self, owner, name):
        self._name = f"_{name}_{id(self)}"  # Evita colisão entre classes

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj.__dict__.get(self._name)

    def __set__(self, obj, value):
        self.validate(value)
        obj.__dict__[self._name] = value

    def validate(self, value):
        raise NotImplementedError

class PositiveInteger(ValidatedField):
    def validate(self, value):
        if not isinstance(value, int):
            raise TypeError("Esperado inteiro")
        if value < 0:
            raise ValueError("Valor deve ser positivo")

class Pessoa:
    idade = PositiveInteger()

class Funcionario(Pessoa):
    salario = PositiveInteger()

    def __init__(self, idade, salario):
        self.idade = idade
        self.salario = salario

f = Funcionario(30, 5000)
print(f.idade, f.salario)  # 30 5000

6. Descritores Não-Dado: Métodos e Funções

Funções em Python são descritores não-dado que implementam o binding automático:

class MeuClassMethod:
    def __init__(self, func):
        self.func = func

    def __get__(self, obj, objtype=None):
        if objtype is None:
            objtype = type(obj)
        return lambda *args, **kwargs: self.func(objtype, *args, **kwargs)

class MeuStaticMethod:
    def __init__(self, func):
        self.func = func

    def __get__(self, obj, objtype=None):
        return self.func

class Exemplo:
    @MeuClassMethod
    def metodo_classe(cls):
        return f"Método de classe: {cls.__name__}"

    @MeuStaticMethod
    def metodo_estatico():
        return "Método estático"

print(Exemplo.metodo_classe())  # Método de classe: Exemplo
print(Exemplo.metodo_estatico())  # Método estático

7. Performance e Boas Práticas com Descritores

Descritores têm custo de chamada maior que acesso direto a atributos. Use-os quando precisar de lógica de validação ou transformação.

from timeit import timeit

class DescritorRapido:
    __slots__ = ('_name',)

    def __set_name__(self, owner, name):
        self._name = f"_{name}"

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj.__dict__.get(self._name)

    def __set__(self, obj, value):
        obj.__dict__[self._name] = value

class MinhaClasse:
    __slots__ = ('_attr',)  # Economia de memória
    attr = DescritorRapido()

    def __init__(self, valor):
        self.attr = valor

# Quando usar descritores vs @property:
# - Descritores: lógica reutilizável entre múltiplas classes
# - @property: lógica específica para um atributo único
# - __getattr__: fallback para atributos inexistentes

8. Projeto Final: Framework de Validação de Dados com Descritores

class Field:
    def __set_name__(self, owner, name):
        self.name = name
        self._storage_name = f"_{name}_{id(self)}"

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj.__dict__.get(self._storage_name)

    def __set__(self, obj, value):
        self.validate(value)
        obj.__dict__[self._storage_name] = value

    def validate(self, value):
        raise NotImplementedError

class CharField(Field):
    def __init__(self, min_length=0, max_length=None):
        self.min_length = min_length
        self.max_length = max_length

    def validate(self, value):
        if not isinstance(value, str):
            raise TypeError(f"{self.name} deve ser string")
        if len(value) < self.min_length:
            raise ValueError(f"{self.name} deve ter no mínimo {self.min_length} caracteres")
        if self.max_length and len(value) > self.max_length:
            raise ValueError(f"{self.name} deve ter no máximo {self.max_length} caracteres")

class IntegerField(Field):
    def __init__(self, min_value=None, max_value=None):
        self.min_value = min_value
        self.max_value = max_value

    def validate(self, value):
        if not isinstance(value, int):
            raise TypeError(f"{self.name} deve ser inteiro")
        if self.min_value is not None and value < self.min_value:
            raise ValueError(f"{self.name} deve ser >= {self.min_value}")
        if self.max_value is not None and value > self.max_value:
            raise ValueError(f"{self.name} deve ser <= {self.max_value}")

class EmailField(CharField):
    def validate(self, value):
        super().validate(value)
        if "@" not in value:
            raise ValueError(f"{self.name} deve ser um email válido")

class ModelMeta(type):
    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        cls._fields = {}
        for key, value in namespace.items():
            if isinstance(value, Field):
                cls._fields[key] = value
        return cls

class Model(metaclass=ModelMeta):
    def validate_all(self):
        errors = {}
        for name, field in self._fields.items():
            try:
                value = getattr(self, name)
                field.validate(value)
            except (TypeError, ValueError) as e:
                errors[name] = str(e)
        return errors

class CadastroUsuario(Model):
    nome = CharField(min_length=2, max_length=100)
    idade = IntegerField(min_value=0, max_value=150)
    email = EmailField(min_length=5, max_length=200)

    def __init__(self, nome, idade, email):
        self.nome = nome
        self.idade = idade
        self.email = email

# Exemplo de uso
try:
    usuario = CadastroUsuario("João", 25, "joao@email.com")
    erros = usuario.validate_all()
    if erros:
        print(f"Erros de validação: {erros}")
    else:
        print(f"Usuário válido: {usuario.nome}, {usuario.idade} anos")
except (TypeError, ValueError) as e:
    print(f"Erro: {e}")

Referências