Descriptors em Python: guia prático

Atualizado em: 22/07/2026
Tempo de leitura: 6 minutos
Código Python com descriptors e atributos

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 = idade

O 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 = autor

Esse 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.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Conteúdo do artigo

    Artigos relacionados

    Criando instalador EXE com ícone personalizado em Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Como criar um instalador .exe com ícone personalizado no Python

    Se você já desenvolveu algum script útil, provavelmente já se perguntou como criar um instalador .exe com ícone personalizado no

    Ler mais

    Tempo de leitura: 11 minutos
    25/04/2026
    Herança múltipla em Python sem causar problemas no código
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Como usar herança múltipla no Python sem bugar seu código

    Entender como usar herança múltipla no Python sem bugar seu código é um dos grandes marcos na jornada de qualquer

    Ler mais

    Tempo de leitura: 9 minutos
    21/04/2026
    Uso do super em Python para resolver problemas de herança
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Como usar super() no Python e resolver erros de herança

    Entender como usar super() no Python é um divisor de águas para qualquer desenvolvedor que deseja dominar a Programação Orientada

    Ler mais

    Tempo de leitura: 9 minutos
    11/04/2026
    Leitura de arquivos grandes em Python sem travar o sistema
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Como ler arquivos gigantes sem travar o Python

    Lidar com grandes volumes de dados é um desafio comum na rotina de quem trabalha com programação e ciência de

    Ler mais

    Tempo de leitura: 12 minutos
    16/03/2026
    Uso de multiprocessing em Python para acelerar scripts
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Como usar multiprocessing em Python e acelerar seu script

    Você já sentiu que seu computador tem muito mais poder do que o seu código está realmente utilizando? Se você

    Ler mais

    Tempo de leitura: 11 minutos
    15/03/2026
    Criação de geradores eficientes usando yield em Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Descubra como criar geradores eficientes com yield no Python

    Você já sentiu que seu computador travou ao tentar processar uma lista gigantesca de dados? Isso acontece porque, por padrão,

    Ler mais

    Tempo de leitura: 9 minutos
    25/02/2026