Descriptors em Python são um dos recursos mais importantes por trás de propriedades, métodos, validações e muitos comportamentos automáticos da linguagem. Embora pareçam avançados, eles seguem uma ideia simples: um objeto pode controlar o que acontece quando um atributo é lido, alterado ou removido. Neste guia, você vai entender o protocolo de descriptors, criar exemplos com __get__, __set__ e __delete__, evitar erros comuns e descobrir quando esse recurso realmente melhora um projeto.
Antes de avançar, vale revisar programação orientada a objetos em Python, porque descriptors trabalham diretamente com classes, instâncias e atributos. Também é útil conhecer decoradores em Python, já que @property é uma forma prática de aplicar um descriptor pronto.
O que são descriptors em Python?
Um descriptor é qualquer objeto de classe que implemente pelo menos um dos métodos especiais __get__, __set__ ou __delete__. Quando esse objeto é colocado como atributo de outra classe, o Python chama esses métodos automaticamente durante o acesso ao atributo. Isso permite centralizar validação, conversão, registro de eventos, carregamento preguiçoso e outras regras.
O comportamento faz parte do modelo de dados oficial do Python. A documentação do modelo de dados explica quando cada método é chamado e como descriptors interagem com instâncias e classes. O Descriptor Guide oficial apresenta exemplos detalhados e mostra como vários recursos internos da linguagem são construídos sobre esse protocolo.
Primeiro exemplo com __get__
Comece com um descriptor somente de leitura. Ele retorna um texto diferente dependendo de ser acessado pela classe ou por uma instância.
class Saudacao:
def __get__(self, instance, owner):
if instance is None:
return self
return f"Olá, {instance.nome}!"
class Usuario:
mensagem = Saudacao()
def __init__(self, nome):
self.nome = nome
usuario = Usuario("Ana")
print(usuario.mensagem)
print(Usuario.mensagem)O parâmetro instance representa o objeto que realizou o acesso. Quando o atributo é lido diretamente pela classe, ele recebe None. O parâmetro owner contém a classe proprietária. Retornar o próprio descriptor no acesso pela classe é uma convenção útil para inspeção e configuração.
Validando atributos com __set__
Um dos usos mais comuns é validar valores antes de armazená-los. O descriptor abaixo garante que uma idade seja inteira e não negativa.
class InteiroPositivo:
def __set_name__(self, owner, name):
self.public_name = name
self.private_name = f"_{name}"
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.private_name)
def __set__(self, instance, value):
if not isinstance(value, int):
raise TypeError(f"{self.public_name} deve ser inteiro")
if value < 0:
raise ValueError(f"{self.public_name} não pode ser negativo")
setattr(instance, self.private_name, value)
class Pessoa:
idade = InteiroPositivo()
def __init__(self, idade):
self.idade = idadeO método __set_name__ é executado quando a classe é criada. Ele informa ao descriptor o nome do atributo ao qual foi associado. Assim, a mesma classe descriptor pode ser reutilizada em campos diferentes sem receber o nome manualmente.
Data descriptor e non-data descriptor
Descriptors são divididos em duas categorias. Um data descriptor implementa __set__ ou __delete__, além de poder implementar __get__. Um non-data descriptor implementa apenas __get__. Essa diferença altera a prioridade de busca de atributos.
- Data descriptors têm prioridade sobre atributos armazenados no dicionário da instância.
- Non-data descriptors podem ser substituídos por um atributo de mesmo nome na instância.
- Métodos normais são exemplos de non-data descriptors.
propertyé um exemplo de data descriptor.
Entender essa prioridade ajuda a explicar por que alguns atributos parecem ignorar valores gravados diretamente em __dict__. Para compreender melhor a resolução de atributos em hierarquias, consulte também o guia sobre herança múltipla em Python.
Como property usa descriptors
Quando você cria uma propriedade com @property, o Python coloca um objeto descriptor na classe. Esse objeto chama as funções definidas para leitura, escrita e remoção. Em muitos projetos, property é mais simples do que criar um descriptor próprio.
class Produto:
def __init__(self, preco):
self.preco = preco
@property
def preco(self):
return self._preco
@preco.setter
def preco(self, value):
if value < 0:
raise ValueError("Preço inválido")
self._preco = float(value)Use property quando a lógica pertence a um único atributo de uma única classe. Prefira um descriptor personalizado quando a mesma regra será aplicada em várias classes ou campos.
Descriptor reutilizável para textos
O exemplo seguinte valida textos obrigatórios, remove espaços extras e permite definir um tamanho máximo.
class TextoObrigatorio:
def __init__(self, maximo=100):
self.maximo = maximo
def __set_name__(self, owner, name):
self.name = name
self.storage_name = f"_{name}"
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.storage_name)
def __set__(self, instance, value):
if not isinstance(value, str):
raise TypeError(f"{self.name} deve ser texto")
value = value.strip()
if not value:
raise ValueError(f"{self.name} é obrigatório")
if len(value) > self.maximo:
raise ValueError(f"{self.name} excede {self.maximo} caracteres")
setattr(instance, self.storage_name, value)
class Artigo:
titulo = TextoObrigatorio(80)
autor = TextoObrigatorio(50)
def __init__(self, titulo, autor):
self.titulo = titulo
self.autor = autorEsse padrão reduz repetição e mantém as classes de domínio mais limpas. Ainda assim, a validação deve produzir mensagens claras, porque erros genéricos tornam a manutenção mais difícil.
Armazenamento por instância
Um erro comum é guardar o valor diretamente no objeto descriptor, por exemplo, usando self.value. Como o descriptor pertence à classe e é compartilhado por todas as instâncias, isso faz diferentes objetos dividirem o mesmo valor. O armazenamento deve ocorrer na instância, normalmente com setattr em um nome privado.
Outra alternativa é usar weakref.WeakKeyDictionary, que associa valores às instâncias sem impedir sua coleta de memória. Essa abordagem é útil quando não é possível modificar o __dict__ do objeto, mas adiciona complexidade e deve ser usada com critério.
Descriptors com dataclasses
Descriptors podem funcionar com dataclasses, mas é importante testar a ordem de inicialização e os valores padrão. Em modelos simples, recursos como field e __post_init__ podem ser suficientes. Para conhecer essa alternativa, leia dataclasses em Python.
Quando a validação precisa ser reutilizada por dezenas de campos, o descriptor pode ser uma boa escolha. Quando a regra depende de vários atributos ao mesmo tempo, normalmente é melhor validar no construtor, em um método de domínio ou em __post_init__.
Testando descriptors
Teste tanto os caminhos válidos quanto os inválidos. Confirme que duas instâncias não compartilham valores, que mensagens de erro são úteis e que o acesso pela classe não falha.
import pytest
def test_idades_independentes():
ana = Pessoa(20)
joao = Pessoa(35)
assert ana.idade == 20
assert joao.idade == 35
def test_idade_negativa():
with pytest.raises(ValueError):
Pessoa(-1)Para estruturar uma suíte completa, veja o tutorial de Pytest em Python. Testes são especialmente importantes em descriptors porque eles alteram um comportamento básico da linguagem: o acesso a atributos.
Quando usar descriptors
- Validação reutilizável em várias classes.
- Conversão automática de valores.
- Campos calculados ou carregados sob demanda.
- Mapeamento entre objetos e bancos de dados.
- Registro de leituras e alterações para auditoria.
- Construção de frameworks e bibliotecas.
Evite descriptors quando uma função, uma propriedade ou uma validação simples no construtor resolver o problema. Eles são poderosos, mas escondem chamadas automáticas e podem dificultar a leitura para pessoas que não conhecem o protocolo.
Erros comuns
- Guardar valores no próprio descriptor e compartilhá-los entre instâncias.
- Usar o mesmo nome público dentro de
__set__e causar recursão infinita. - Esquecer de tratar o acesso pela classe quando
instanceéNone. - Criar abstrações complexas para regras usadas apenas uma vez.
- Não testar herança, cópia, serialização e ferramentas de inspeção.
Conclusão
Descriptors em Python permitem controlar atributos de maneira reutilizável e ajudam a explicar recursos como métodos, propriedades e vários frameworks. O protocolo é baseado em poucos métodos especiais, mas exige atenção ao armazenamento por instância, à prioridade de busca e ao acesso pela classe.
Comece com um exemplo pequeno, escreva testes e compare a solução com alternativas mais simples. Quando existe repetição real de validação ou comportamento entre classes, um descriptor bem projetado pode reduzir código duplicado e tornar as regras mais consistentes.





