Pular para o conteúdo principal

Tutorial de descritores em Python

Entenda o que são descritores em Python, quando usar e por que usar.
Atualizado 17 de set. de 2026  · 13 min lido

Explorar com IA

ChatGPTClaudePerplexity

Descritores em Python — ou, de forma mais geral, descritores — oferecem uma técnica poderosa para escrever código reutilizável que pode ser compartilhado entre classes. Eles podem parecer semelhantes a herança, mas tecnicamente não são. São uma forma genérica de interceptar o acesso a atributos. Descritores são o mecanismo por trás de propriedades, métodos estáticos, métodos de classe, super, etc.

Os descritores foram adicionados ao Python na versão 2.2 e, desde então, são vistos como uma peça “mágica” que trouxe um novo estilo às classes tradicionais. São classes que permitem gerenciar propriedades em outra classe. Especificamente, implementam a interface dos métodos __get__(), __set__() e __delete__(), o que os torna interessantes por vários motivos. Por exemplo, decoradores de classe e de propriedade que você já deve ter visto em Python.

Em poucas palavras, uma classe que implementa os métodos __get__(), __set()__ ou __delete()__ para um objeto é chamada de "Descriptor". Citando diretamente a documentação oficial do Python, um descritor é um atributo de objeto com binding behavior, cujo acesso foi sobrescrito por métodos do protocolo de descritor. Esses métodos são __get__(), __set__() e __delete__() (Fonte).

Binding behavior, no contexto de descritores, significa vincular a forma como um valor pode ser definido, consultado (get) ou excluído para uma variável, objeto ou conjunto de dados. Toda essa interação fica vinculada àquele pedaço de dado, pois só se aplica ao dado no qual você a configurou.

Os descritores podem ser categorizados em descritores de dados e não de dados. Se o descritor que você escrever tiver apenas o método __get__(), ele é um descritor não de dados; por outro lado, implementações que envolvem os métodos __set__() e __delete__() são chamadas de descritores de dados. Descritores não de dados são apenas legíveis, enquanto os de dados são legíveis e graváveis.

É importante notar que descritores são atribuídos a uma classe, não a uma instância. Modificar a classe sobrescreve ou remove o próprio descritor, em vez de disparar o código dele (IBM Developer).

Por fim, a classe do descritor não se limita a esses três métodos; ela pode conter quaisquer outros atributos além de get, set e delete.

Vamos entender os métodos get, set e delete em mais detalhes, com base nesta página da IBM Developer:

  • self é a instância do descritor que você cria (Real Python).
  • object é a instância do objeto ao qual seu descritor está anexado (Real Python).
  • type é o tipo do objeto ao qual o descritor está anexado (Real Python).
  • value é o valor atribuído ao atributo do descritor. get(self, object, type) set(self, object, value) delete(self, object)
  • __get__() acessa o atributo, ou seja, quando você quer extrair alguma informação. Ele retorna o valor do atributo ou lança a exceção AttributeError se o atributo solicitado não existir.

  • __set__() é chamado em uma atribuição de atributo que define o valor de um atributo. Não retorna nada, mas pode lançar a exceção AttributeError.

  • __delete__() controla a operação de exclusão, isto é, quando você quer remover o atributo de um objeto. Não retorna nada.

Agora vamos entender o propósito dos descritores com alguns exemplos!

Propósito dos descritores

Vamos definir uma classe car com três atributos: make, model e fuel_cap. Você usará o método __init__() para inicializar os atributos da classe. Depois, usará a função mágica __str__(), que simplesmente retorna uma string com os três atributos passados à classe ao criar o objeto.

Observe que o método __str__() retorna a representação em string do objeto. Ele é chamado quando a função print() ou str() é invocada em um objeto da classe.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car1 = Car("BMW","X7",40)
print(car1)
BMW model X7 with a fuel capacity of 40 ltr.

Como você viu acima, tudo certo até aqui!

Agora vamos mudar a capacidade de combustível do carro para menos 40.

car2 = Car("BMW","X7",-40)
print(car2)
BMW model X7 with a fuel capacity of -40 ltr.

Opa, tem algo errado, não é? A capacidade do tanque nunca pode ser negativa. Mesmo assim, o Python aceita a entrada sem erro. Isso acontece porque Python é uma linguagem dinâmica e não faz checagem de tipos explicitamente.

Para evitar isso, vamos adicionar uma condição if no método __init__() para verificar se a capacidade informada é válida. Se não for, lançamos um ValueError.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car1 = Car("BMW","X7",40)
print(car1)
BMW model X7 with a fuel capacity of 40 ltr.
car2 = Car("BMW","X7",-40)
print(car2)
----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-22-1c3d23be72f7> in <module>
----> 1 car2 = Car("BMW","X7",-40)
      2 print(car2)


<ipython-input-20-1e154783588d> in __init__(self, make, model, fuel_cap)
      5         self.fuel_cap = fuel_cap
      6         if self.fuel_cap < 0:
----> 7             raise ValueError("Fuel Capacity can never be less than zero")
      8
      9     def __str__(self):


ValueError: Fuel Capacity can never be less than zero

Pelo resultado acima, tudo funciona, já que o programa lança um ValueError se a capacidade for abaixo de zero.

Mas há outro problema: e se a capacidade for um float ou uma string? E não só a capacidade — o make e o model também podem ser inteiros. Em todos esses casos, o programa não levantará exceção.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car(-40,"X7",40)
print(car2)
-40 model X7 with a fuel capacity of 40 ltr.

Para tratar esse caso, você pode pensar em adicionar outro if ou usar isinstance para checagem de tipo.

Desta vez, vamos usar o método embutido isinstance para tratar o erro.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if isinstance(self.make, str):
            print(self.make)
        else:
            raise ValueError("Make of the car can never be an integer")

        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
BMW
BMW model X7 with a fuel capacity of 40 ltr.
car2 = Car(-40,"X7",40)
print(car2)
----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-34-75b08cba454f> in <module>
----> 1 car2 = Car(-40,"X7",40)
      2 print(car2)


<ipython-input-31-175690bf3b98> in __init__(self, make, model, fuel_cap)
      7             print(self.make)
      8         else:
----> 9             raise ValueError("Make of the car can never be an integer")
     10
     11         if self.fuel_cap < 0:


ValueError: Make of the car can never be an integer

Ótimo! Assim você tratou esse erro também.

Porém, e se você decidir alterar a capacidade do tanque para -40 mais tarde? Não vai funcionar, pois a checagem de tipo ocorre só uma vez, dentro de __init__(). Como você sabe, __init__() é um construtor chamado apenas na criação do objeto. Depois disso, sua verificação personalizada não será aplicada.

Veja o exemplo:

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if isinstance(self.make, str):
            print(self.make)
        else:
            raise ValueError("Make of the car can never be an integer")

        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
BMW
BMW model X7 with a fuel capacity of 40 ltr.
car2.make = -40
print(car2)
-40 model X7 with a fuel capacity of 40 ltr.

Pronto: você conseguiu burlar a checagem de tipo.

Agora pense que você tem vários outros atributos — consumo, preço, acessórios etc. — que também precisam de validação, e alguns deles deveriam ter apenas acesso de leitura. Não seria bem chato manter tudo isso?

Para resolver esses problemas, entram em cena os descritores do Python!

Como vimos, qualquer classe que implemente os métodos mágicos __get__(), __set()__ ou __delete()__ do protocolo de descritor é um descritor. Eles também dão controle extra sobre como um atributo deve funcionar, como restringir leitura e escrita.

Agora vamos estender o exemplo acima adicionando os métodos de descritor do Python.

class Descriptor:
    def __init__(self):
        self.__fuel_cap = 0
    def __get__(self, instance, owner):    
        return self.__fuel_cap
    def __set__(self, instance, value):
        if isinstance(value, int):
            print(value)
        else:
            raise TypeError("Fuel Capacity can only be an integer")

        if value < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

        self.__fuel_cap = value

    def __delete__(self, instance):
        del self.__fuel_cap
class Car:
    fuel_cap = Descriptor()
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
40
BMW model X7 with a fuel capacity of 40 ltr.

Se a classe Descriptor parece obscura, calma. Vamos destrinchar cada método.

  • O método __init__() da classe Descriptor define a variável local __fuel_cap como zero. O dunder (duplo sublinhado) no início indica que a variável é privada. Aqui, ele também ajuda a distinguir o atributo de capacidade da classe Descriptor do da classe Car.
  • Como você já sabe, o método __get__() é usado para recuperar atributos e retorna a capacidade de combustível. Ele recebe três argumentos: o próprio objeto descritor; a instância da classe que contém o descritor (por exemplo, car2); e o owner, que é a classe à qual a instância pertence (a classe Car). Nesse método, você simplesmente retorna o atributo cujo valor é definido em __set__().
  • O método __set__() é invocado quando um valor é atribuído ao atributo e, ao contrário de __get__(), não retorna nada. Além do descritor, ele recebe a instância (como em __get__()) e o valor que você quer atribuir. Aqui, verificamos se o valor para fuel_cap é um inteiro; se não for, levantamos TypeError. Depois verificamos se é menor que zero; se for, levantamos ValueError. Por fim, atualizamos fuel_cap com o valor válido.
  • Por último, __delete__() é chamado quando o atributo é removido do objeto e, assim como __set__(), não retorna nada.

A classe Car permanece praticamente igual, com a diferença de que adicionamos a instância fuel_cap da classe Descriptor(). Repare: a instância do descritor deve ser adicionada como atributo de classe, não como atributo de instância.

Assim que você define fuel_cap no __init__(), o método __set__() do descritor é invocado.

Agora vamos alterar a capacidade para um valor negativo e ver se o programa levanta um ValueError.

car2 = Car("BMW","X7",-40)
print(car2)
-40



----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-115-1c3d23be72f7> in <module>
----> 1 car2 = Car("BMW","X7",-40)
      2 print(car2)


<ipython-input-107-3e1f3e97d3c7> in __init__(self, make, model, fuel_cap)
      4         self.make = make
      5         self.model = model
----> 6         self.fuel_cap = fuel_cap
      7
      8     def __str__(self):


<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
     11
     12         if value < 0:
---> 13             raise ValueError("Fuel Capacity can never be less than zero")
     14
     15         self.__fuel_cap = value


ValueError: Fuel Capacity can never be less than zero

Se você lembrar deste ponto, a checagem falhava quando o atributo era alterado depois, já que a validação estava apenas em __init__(). Vamos agora definir fuel_cap como string e ver se dá erro.

car2.fuel_cap = -1
-1



----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-120-dea9dbe96ebe> in <module>
----> 1 car2.fuel_cap = -1


<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
     11
     12         if value < 0:
---> 13             raise ValueError("Fuel Capacity can never be less than zero")
     14
     15         self.__fuel_cap = value


ValueError: Fuel Capacity can never be less than zero
car2.fuel_cap = "BMW"
----------------------------------------

TypeErrorTraceback (most recent call last)

<ipython-input-121-0b316a9872c6> in <module>
----> 1 car2.fuel_cap = "BMW"


<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
      8             print(value)
      9         else:
---> 10             raise TypeError("Fuel Capacity can only be an integer")
     11
     12         if value < 0:


TypeError: Fuel Capacity can only be an integer

Perfeito! Como você viu, funciona mesmo ao atualizar o atributo depois.

Existe, porém, um pequeno problema com descritores: quando você cria uma nova instância da classe, o valor anterior pode ser sobrescrito. O motivo é que descritores estão ligados à classe, não à instância.

Veja:

car3 = Car("BMW","X7",48) # criou uma nova instância 'car3' com valores diferentes
48

Ao imprimir a instância car2, você notará que os valores foram sobrescritos por car3.

print(car2)
BMW model X7 with a fuel capacity of 48 ltr.

Conclusão

Parabéns por concluir o tutorial.

Este conteúdo é voltado para quem já tem familiaridade com Python e quer dominar recursos avançados. Como exercício, vale investigar como resolver o problema de sobrescrita entre instâncias que discutimos hoje.

Se tiver qualquer dúvida sobre este tutorial, fique à vontade para perguntar nos comentários abaixo.

Para aprender mais sobre Python, faça a trilha de habilidades completa Python Programming da DataCamp.

Confira também nosso tutorial de funções em Python.

Tópicos
Python

Cursos de Python

Curso

Introdução ao Python

4 h
7M
Domine os fundamentos da análise de dados com Python em quatro horas e explore pacotes populares.
Ver detalhesRight Arrow
Iniciar Curso
Ver maisRight Arrow
Relacionado

Tutorial

Tutorial de iteradores e generators em Python

Entenda a diferença entre iteradores e generators em Python e descubra quais usar em diferentes situações.
Kurtis Pykes 's photo

Kurtis Pykes

10 min

Tutorial

Tutorial de docstrings em Python

Saiba mais sobre os Docstrings do Python. Encontre diferentes exemplos e tipos de formatos de docstrings para Sphinx, Numpy e Pydoc.
Aditya Sharma's photo

Aditya Sharma

15 min

Tutorial

Tutorial de conjuntos e teoria de conjuntos em Python

Aprenda sobre os conjuntos do Python: o que são, como criá-los, quando usá-los, funções incorporadas e sua relação com as operações da teoria dos conjuntos.
DataCamp Team's photo

DataCamp Team

13 min

Tutorial

Tutorial de Python

Em Python, tudo é objeto. Números, cadeias de caracteres (strings), DataFrames, e até mesmo funções são objetos. Especificamente, qualquer coisa que você usa no Python tem uma classe, um modelo associado por trás.
DataCamp Team's photo

DataCamp Team

3 min

Tutorial

Funções em Python: como chamar e escrever funções

Descubra como escrever funções em Python reutilizáveis e eficientes. Domine parâmetros, instruções de retorno e temas avançados como funções lambda. Organize melhor seu código com main() e outras boas práticas.
Karlijn Willems's photo

Karlijn Willems

14 min

Tutorial

Programação orientada a objetos em Python (OOP): Tutorial

Aborde os fundamentos da programação orientada a objetos (OOP) em Python: explore classes, objetos, métodos de instância, atributos e muito mais!
Théo Vanderheyden's photo

Théo Vanderheyden

12 min

Ver MaisVer Mais