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
- PEP 252 – Making Types Look More Like Classes — Documento oficial que introduziu o protocolo de descritores em Python
- Python Documentation: Descriptor Protocol — Guia oficial detalhado sobre o protocolo de descritores
- Real Python: Python Descriptors — Tutorial completo com exemplos práticos de implementação
- IBM Developer: Understanding Python Descriptors — Artigo técnico aprofundado sobre o funcionamento interno dos descritores
- Python Documentation: property() function — Documentação oficial da função
propertye sua implementação como descritor