Se você já usou @property em Python e se perguntou como ele funciona magicamente, a resposta está nos descritores (descriptors). Descritores são a mecanismo subjacente que torna possível property, staticmethod, classmethod e até mesmo a ORM do Django. Entender esse conceito avançado vai elevar sua compreensão do Python a um novo patamar.
Neste guia completo, você vai aprender o que são descritores, como implementá-los, as diferenças entre data e non-data descriptors, casos de uso práticos e como eles se aplicam em frameworks reais. Vamos mergulhar fundo no protocolo descriptor e entender como o Python gerencia atributos por baixo dos panos.
O que são Descritores em Python?
Um descritor é qualquer objeto Python que implemente pelo menos um dos métodos do protocolo descriptor: __get__, __set__ ou __delete__. Quando um atributo de uma classe é um descritor, o Python desvia o comportamento padrão de acesso, atribuição e deleção de atributos para esses métodos especiais.
Na prática, descritores permitem que você crie atributos com comportamento personalizado. Em vez de apenas armazenar e recuperar um valor, você pode executar lógica de validação, transformação, cache ou qualquer outra operação sempre que o atributo for acessado ou modificado.
Segundo a documentação oficial sobre descritores, esse mecanismo é um dos pilares do modelo de objetos do Python e controla como atributos são resolvidos em praticamente todas as classes que você escreve.
Para entender descritores, primeiro precisamos revisar como o Python resolve atributos. O método __getattribute__ é chamado para todo acesso a atributo, e é nele que o Python verifica se o atributo é um descritor. Consulte a documentação de __getattribute__ para uma visão detalhada desse processo.
O Protocolo Descriptor
O protocolo descriptor consiste em três métodos mágicos:
__get__(self, obj, objtype=None)— chamado quando o atributo é acessado__set__(self, obj, value)— chamado quando o atributo é modificado__delete__(self, obj)— chamado quando o atributo é deletado comdel
Um objeto que implementa __set__ ou __delete__ é chamado de data descriptor. Um objeto que implementa apenas __get__ é chamado de non-data descriptor. Essa distinção é crucial para entender a precedência de resolução de atributos em Python.
Veja a implementação mais simples possível de um descritor:
class MeuDescritor:
def __get__(self, obj, objtype=None):
return 42
class MinhaClasse:
atributo = MeuDescritor()
obj = MinhaClasse()
print(obj.atributo) # 42
Quando acessamos obj.atributo, o Python chama MeuDescritor.__get__, que retorna 42. Esse é o princípio básico. A seção de descritores do data model esclarece todos os detalhes do protocolo.
Data Descriptors vs Non-Data Descriptors
A diferença entre data e non-data descriptors determina o que tem precedência na busca de atributos. Quando você acessa obj.atributo, o Python segue esta ordem:
- Data descriptors definidos na classe do objeto
- Atributos da instância (o
__dict__do objeto) - Non-data descriptors e outros atributos da classe
Isso significa que um data descriptor sempre tem prioridade sobre o dicionário da instância. Já um non-data descriptor pode ser "sobrescrito" por um atributo de instância com o mesmo nome.
class DataDescriptor:
def __get__(self, obj, objtype=None):
return "DATA DESCRIPTOR"
def __set__(self, obj, value):
print(f"Setando {value}")
class NonDataDescriptor:
def get(self, obj, objtype=None):
return "NON-DATA DESCRIPTOR"
class Teste:
data = DataDescriptor()
non_data = NonDataDescriptor()
def __init__(self):
self.data = "instância" # Ignorado! Data descriptor vence
self.non_data = "instância" # Funciona! Sobrescreve non-data
t = Teste()
print(t.data) # "DATA DESCRIPTOR"
print(t.non_data) # "instância"
O guia de descritores do Real Python explica essa hierarquia com exemplos didáticos que ajudam a fixar o conceito.
Como a Property Funciona por Baixo dos Panos
A função embutida property é o exemplo mais conhecido de data descriptor. Ela implementa __get__, __set__ e __delete__ para conectar getters, setters e deleters definidos pelo usuário.
Você pode recriar uma versão simplificada da property para entender o mecanismo:
class Property:
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)
class Pessoa:
def init(self, nome):
self._nome = nome
@Property
def nome(self):
return self._nome
@nome.setter
def nome(self, valor):
self._nome = valor.upper()
A implementação oficial da property na biblioteca padrão é mais robusta, mas o princípio é exatamente este: um data descriptor que delega chamadas para funções personalizadas.
Casos de Uso Práticos de Descritores
Descritores resolvem problemas reais de forma elegante. Vamos explorar aplicações práticas que você pode usar hoje.
1. Validação de Atributos
Validar dados na atribuição é uma das aplicações mais comuns de descritores:
class Validado:
def __init__(self, tipo, minimo=None, maximo=None):
self.tipo = tipo
self.minimo = minimo
self.maximo = maximo
def __set_name__(self, owner, name):
self.nome = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(self.nome)
def __set__(self, obj, value):
if not isinstance(value, self.tipo):
raise TypeError(f"{self.nome} deve ser {self.tipo.__name__}")
if self.minimo is not None and value < self.minimo:
raise ValueError(f"{self.nome} não pode ser menor que {self.minimo}")
if self.maximo is not None and value > self.maximo:
raise ValueError(f"{self.nome} não pode ser maior que {self.maximo}")
obj.__dict__[self.nome] = value
class Produto:
nome = Validado(str, maximo=100)
preco = Validado(float, minimo=0)
def __init__(self, nome, preco):
self.nome = nome
self.preco = preco
p = Produto("Notebook", 3500.0)
p.preco = -10 # Levanta ValueError
p.nome = 123 # Levanta TypeError
Note o uso de __set_name__, um método opcional do protocolo descriptor introduzido no Python 3.6. Ele é chamado quando a classe é criada e informa ao descritor o nome do atributo ao qual foi associado. A PEP 487 documenta essa funcionalidade em detalhes.
2. Lazy Loading e Propriedades com Cache
Descritores podem atrasar o cálculo de um atributo até que ele seja realmente necessário, armazenando o resultado em cache:
class LazyProperty:
def __init__(self, func):
self.func = func
self.nome = func.__name__
def __get__(self, obj, objtype=None):
if obj is None:
return self
valor = self.func(obj)
obj.__dict__[self.nome] = valor # Armazena no __dict__ da instância
return valor
class AnaliseDados:
def init(self, dados):
self.dados = dados
@LazyProperty
def media(self):
print("Calculando média...")
return sum(self.dados) / len(self.dados)
@LazyProperty
def mediana(self):
print("Calculando mediana...")
ordenados = sorted(self.dados)
n = len(ordenados)
meio = n // 2
return ordenados[meio] if n % 2 else (ordenados[meio-1] + ordenados[meio]) / 2
a = AnaliseDados([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
print(a.media) # Calculando média... 5.5
print(a.media) # 5.5 (sem recalcular!)
print(a.mediana) # Calculando mediana... 5.5
Esse padrão é amplamente usado em ORMs como SQLAlchemy e Django, onde consultas ao banco são adiadas até o acesso ao atributo. Complemente seus estudos com nosso guia sobre Programação Orientada a Objetos em Python para entender como descritores se encaixam no ecossistema OOP do Python.
3. Descritores de Unidade (Medidas)
Descritores permitem criar atributos com unidades de medida inteligentes:
class Medida:
def __init__(self, unidade):
self.unidade = unidade
def __set_name__(self, owner, name):
self.nome = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(self.nome)
def __set__(self, obj, value):
if value < 0:
raise ValueError(f"{self.nome} não pode ser negativo")
obj.__dict__[self.nome] = f"{value} {self.unidade}"
class Pedido:
peso = Medida("kg")
altura = Medida("cm")
pedido = Pedido()
pedido.peso = 5.5
pedido.altura = 30
print(pedido.peso) # 5.5 kg
print(pedido.altura) # 30 cm
Descritores em Frameworks Populares
Frameworks Python fazem uso intenso de descritores. O Django ORM, por exemplo, usa descritores para implementar seus campos de modelo. Quando você define nome = models.CharField(max_length=100), o CharField é um descritor que gerencia a leitura e escrita do atributo, incluindo validação e conversão para SQL.
O sistema de modelos do Django é um dos exemplos mais didáticos de descritores em produção. Cada tipo de campo (CharField, IntegerField, ForeignKey) implementa o protocolo descriptor para traduzir atributos Python em colunas de banco de dados.
SQLAlchemy também usa descritores extensivamente. As colunas mapeadas em uma classe declarativa são descritores que interceptam o acesso para carregar dados do banco sob demanda (lazy loading) e rastrear alterações. O tutorial ORM do SQLAlchemy mostra como esses descritores funcionam na prática.
O próprio Python padrão usa descritores em vários lugares. O decorador @staticmethod e @classmethod são descritores que modificam como os métodos são chamados. A documentação oficial tem uma seção dedicada a métodos estáticos e de classe como descritores.
Para se aprofundar em como decorators se relacionam com descritores, confira nosso tutorial sobre Decorators em Python — ambos os conceitos andam juntos no design de APIs elegantes.
Descritores vs Property vs __getattr__
Python oferece múltiplas maneiras de personalizar acesso a atributos. Veja quando usar cada uma:
| Mecanismo | Escopo | Uso recomendado |
|---|---|---|
| Descritor | Atributo específico | Reutilização entre classes, validação complexa |
| Property | Atributo específico | Getter/setter simples, sem reutilização |
__getattr__ |
Todos os atributos | Fallback para atributos inexistentes |
__getattribute__ |
Todos os atributos | Controle total (use com cautela) |
Descritores são a escolha ideal quando você precisa do mesmo comportamento validado em múltiplas classes ou múltiplos atributos. Se a lógica é específica de um único atributo, @property é mais simples. O artigo Descriptor in Python do GeeksforGeeks compara essas abordagens com exemplos práticos.
Armadilhas Comuns e Boas Práticas
Descritores são poderosos, mas exigem cuidado. Aqui estão os erros mais frequentes:
1. Esquecer de Armazenar no __dict__ da Instância
Um erro clássico é armazenar valores no próprio descritor (que é um atributo de classe) em vez de no dicionário da instância:
class DescritorErrado:
def __init__(self):
self.valor = None # Erro: isso é compartilhado entre instâncias!
def __get__(self, obj, objtype=None):
return self.valor
def __set__(self, obj, value):
self.valor = value # Todas as instâncias compartilham o mesmo valor
Sempre armazene dados da instância em obj.__dict__, não em self. O descritor em si é um atributo de classe e é compartilhado por todas as instâncias.
2. Confundir Data com Non-Data Descriptors
Lembre-se: se você implementa __set__ ou __delete__, seu descritor vira um data descriptor e sempre terá precedência sobre atributos de instância. Isso pode ser surpreendente se você só queria um __get__ personalizado.
3. Performance
Cada acesso a atributo mediado por descritor tem um custo adicional de chamada de função. Para atributos acessados milhões de vezes em loops internos, considere armazenar em cache (como no exemplo de LazyProperty) ou usar atributos simples.
A documentação de __setattr__ esclarece como a atribuição de atributos interage com descritores e o ciclo de vida dos objetos.
Conclusão
Descritores são um dos conceitos mais elegantes e subestimados do Python. Eles formam a base de property, staticmethod, classmethod, ORMs, sistemas de validação e muito mais. Dominar descritores significa entender verdadeiramente como o modelo de objetos do Python funciona.
Agora você sabe implementar seus próprios descritores para validação, lazy loading, cache e atributos com unidades. Mais importante, você entende o protocolo descriptor e como ele se encaixa na hierarquia de resolução de atributos do Python.
Pratique criando descritores reais: comece com um validador de tipos, evolua para lazy properties e depois explore padrões mais avançados. Quanto mais você usar descritores, mais natural será identificar oportunidades onde eles simplificam seu código.
Referências para continuar seus estudos:
- Python Descriptor HowTo (documentação oficial)
- Data Model: Descriptors (documentação oficial)
- Real Python: Python Descriptors Tutorial
- PEP 487 — Simpler customization of class creation
- Documentação oficial: property()
- GeeksforGeeks: Descriptor in Python
- Django ORM: Modelos e campos (uso de descritores)
- SQLAlchemy ORM Tutorial (lazy loading com descritores)