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

    A vibrant collection of blue sewing threads arranged with hands on a white background.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    queue no Python: coordene threads

    Aprenda queue no Python para coordenar threads com FIFO, prioridade, backpressure, task_done, join, retries e shutdown seguro.

    Ler mais

    Tempo de leitura: 5 minutos
    17/08/2026
    A male software engineer working on code in a modern office setting.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    struct no Python: trabalhe com binário

    Aprenda struct no Python para empacotar dados binários, controlar endianness, usar buffers e validar protocolos e arquivos externos.

    Ler mais

    Tempo de leitura: 5 minutos
    17/08/2026
    Close-up of a laptop screen with code and a coffee mug, perfect for tech abstract themes.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    tarfile no Python: crie TAR seguro

    Aprenda tarfile no Python para criar TAR comprimido, inspecionar membros e extrair com filtros, limites e proteção contra path traversal.

    Ler mais

    Tempo de leitura: 5 minutos
    17/08/2026
    Row of colorful office binders neatly arranged on a shelf, ideal for organization concepts.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    gzip no Python: comprima arquivos .gz

    Aprenda gzip no Python para ler e gravar .gz, criar saídas reproduzíveis, trabalhar com streams e limitar a expansão de

    Ler mais

    Tempo de leitura: 4 minutos
    17/08/2026
    Color-coded office binders organized neatly in a storage shelf, featuring labels and a striking red binder.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    lzma no Python: comprima arquivos XZ

    Aprenda lzma no Python para criar arquivos XZ, usar streams, checks, filtros e limites de memória ao descompactar dados externos.

    Ler mais

    Tempo de leitura: 5 minutos
    17/08/2026
    Exquisite python skin handbag with intricate snake emblem and elegant design.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    bz2 no Python: comprima com bzip2

    Aprenda bz2 no Python para comprimir arquivos e bytes, processar fluxos em blocos e limitar a expansão de dados externos.

    Ler mais

    Tempo de leitura: 5 minutos
    16/08/2026