Descritores O Que É - Professor Jeronimo: O que são os descritores?
Professor Jeronimo: O que são os descritores?

O que são descritores no Python e por que você vai se dar mal se ignorar esse conceito

Descritores são um padrão de protocolo em Python que permite controlar como atributos de uma classe são acessados, armazenados e deletados. Quando você vê descritores o que é sendo pesquisado, geralmente é porque alguém tentou criar uma classe e percebeu que o comportamento padrão do atributo não era suficiente. A solução não é bolar um mecanismo do zero, é entender que o Python já tem algo pronto para isso desde a versão 2.2.

A mecânica por trás de tudo

Um descritor é qualquer objeto que implemente __get__, __set__ ou __delete__. O Python chama esses métodos automaticamente quando você acessa um atributo via instância. A ordem de prioridade é simples mas traiçoeira: atributos de instância têm precedência sobre descritores não dados, e descritores de dados (com __set__) vencem até mesmo attributes de classe. Isso significa que se você definir um descritor chamado name na classe e depois fizer obj.name = "value" na instância, o que acontece depende exclusivamente de quais métodos o seu descritor implementa. No dia a dia, você já usa descritores sem perceber. Propriedades criadas com @property, métodos estáticos com @staticmethod, e métodos de classe com @classmethod são todos implementados via protocolo de descritor. O property é um descritor de dados porque implementa __set__. O staticmethod é um descritor não-dado porque só implementa __get__. Essa distinção define completamente o comportamento quando há conflitos entre definições de classe e atribuições de instância.

Um problema real que encontrei na prática

Há alguns anos precisei implementar um sistema de validação de campos em uma classe que representava dados financeiros. A exigência era simples: cada campo numérico deveria ser validado antes de ser atribuído, mas também precisava manter compatibilidade com código legado que acessava os atributos diretamente. A solução óbvia seria usar @property para cada campo, mas são mais de vinte campos no objeto e repetir o padrão getter/setter vinte vezes gerava uma classe ilegível. Criei um descritor genérico que aceitava funções de validação como parâmetro. O código ficou assim:

class ValidatedField:
    def __init__(self, validator=None):
        self.validator = validator
        
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)
        
    def __set__(self, obj, value):
        if self.validator and not self.validator(value):
            raise ValueError(f"Valor inválido: {value}")
        obj.__dict__[self.name] = value
        
    def __set_name__(self, owner, name):
        self.name = name

O detalhe importante é o __set_name__. Esse método foi adicionado no Python 3.6 especificamente para resolver o problema de descritores que precisam saber seu próprio nome dentro da classe. Antes disso, você tinha que fazer gambarras manuais ou passar o nome explicitamente na definição do campo. Sem o __set_name__, o descritor não consegue acessar o dicionário __dict__ da instância corretamente, o que quebra todo o mecanismo de armazenamento. Depois de implementar o descritor, a classe ficou muito mais limpa:

class Transacao:
    valor = ValidatedField(validator=lambda x: isinstance(x, (int, float)) and x > 0)
    taxa = ValidatedField(validator=lambda x: 0 <= x = 1)
    
    def __init__(self, valor, taxa, descricao):
        self.valor = valor
        self.taxa = taxa
        self.descricao = descricao

Note que descricao não é um campo validado, então ele se comporta normalmente. Essa heterogeneidade é comum em sistemas reais: nem tudo precisa de validação, e forçar tudo a passar pelo mesmo mecanismo é perda de tempo.

👉 Clique no botão abaixo para saber mais sobre o assunto!

Vantagens que nem sempre são óbvias

Um benefício prático dos descritores é a capacidade de criar campos calculados que não precisam ser armazenados. Em vez de calcular algo toda vez que o atributo é acessado e guardar o resultado em memória, você pode usar um descritor para cache lazy. Isso reduz a complexidade do código e evita problemas de consistência quando os dados subjacentes mudam. Também é possível usar descritores para criar campos que se comportam de forma diferente dependendo do contexto. Por exemplo, um descritor pode retornar uma cópia rasa do valor em modo de leitura e uma cópia profunda em modo de escrita, prevenindo efeitos colaterais indesejados quando objetos mutáveis são compartilhados entre várias partes do sistema. Essa técnica é especialmente útil em sistemas concorrentes onde o acesso simultâneo a objetos mutáveis causa race conditions silenciosas.

Limitações e armadilhas comuns

Descritores não são bala de prata. Eles introduzem uma camada de indireção que pode tornar o código mais difícil de debugar. Quando um atributo não se comporta como esperado, o rastreamento passa por __get__, __set__ e possíveis middlewares, o que aumenta o tempo de investigação em comparação com atributos normais. Se a classe já tem muitos descritores, o overhead de invocação dos métodos mágicos pode se tornar relevante em loops apertados. Outro problema é a confusão entre descritores de dados e não dados. Um descritor sem __set__ pode ser sobrescrito por uma atribuição de instância, o que quebra a lógica de validação se você não prestar atenção. Esse erro é particularmente traiçoeiro porque não gera exceção — o código simplesmente para de validar e continua executando com valores inválidos. A recomendação é sempre implementar __set__ em descritores que precisam manter integridade dos dados, mesmo que o método apenas delegue para o armazenamento padrão.

Existe também uma limitação técnica importante: descritores não funcionam corretamente com __slots__ a menos que você tenha muito cuidado com a implementação. O __slots__ economiza memória ao eliminar o __dict__ da instância, mas descritores dependem desse dicionário para armazenar valores. Se você precisa de ambos, terá que implementar um mecanismo de armazenamento alternativo dentro do próprio descritor, o que adiciona complexidade desnecessária na maioria dos casos.

Quando evitar descritores

Se você precisa apenas de validação simples, @property é mais legível e direto. Descritores ganham em escala quando o padrão se repete muitas vezes, mas para dois ou três campos, a verbosidade adicional não compensa. Também evite descritores se a classe for parte de uma API pública e você quiser manter a interface o mais simples possível. Outros desenvolvedores que forem usar sua biblioteca vão ter que aprender o funcionamento interno do descritor, o que aumenta a curva de aprendizado sem ganho proporcional. Para sistemas que exigem alta performance em acesso a atributos, considere usar estruturas de dados mais simples ou bibliotecas especializadas como pydantic. O pydantic implementa validação de campos de forma otimizada em C, com menos overhead que descritores puros em Python. Em benchmarks reais, a diferença pode ser de 10 a 30 por cento em velocidade de acesso, dependendo da complexidade da validação.

Alternativas modernas

O ecossistema Python evoluiu desde que descritores foram introduzidos. Bibliotecas como dataclasses (Python 3.7+) oferecem uma sintaxe mais concisa para objetos que precisam de campos com validação. O field() com validator embutido resolve muitos casos de uso sem exigir a criação de classes de descritor personalizadas. Para casos mais complexos, pydantic e attrs fornecem metaprogramação automatizada que gera o código de descritor para você, com performance próxima da escrita manual. Se o objetivo é apenas armazenar dados com comportamento controlado, talvez você nem precise de descritores. Um simples módulo com funções de validação externas e acesso via métodos da classe pode ser suficiente e muito mais fácil de manter. A tendência atual do ecossistema Python é favorecer abstrações de nível mais alto que escondem a complexidade dos descritores em vez de expô-la diretamente ao desenvolvedor.