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.
classCar: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)Tecnologia
BMW model X7with a fuel capacity of-40 ltr.Tecnologia
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.
classCar: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)Tecnologia
----------------------------------------ValueErrorTraceback(most recent call last)<ipython-input-22-1c3d23be72f7>in<module>---->1 car2 =Car("BMW","X7",-40)2print(car2)<ipython-input-20-1e154783588d>in__init__(self, make, model, fuel_cap)5 self.fuel_cap = fuel_cap
6if self.fuel_cap <0:---->7 raise ValueError("Fuel Capacity can never be less than zero")89 def __str__(self):ValueError: Fuel Capacity can never be less than zero
Tecnologia
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.
classCar: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)Tecnologia
-40 model X7with a fuel capacity of40 ltr.Tecnologia
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.
classCar:def__init__(self,make,model,fuel_cap):
self.make = make
self.model = model
self.fuel_cap = fuel_cap
ifisinstance(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)Tecnologia
----------------------------------------ValueErrorTraceback(most recent call last)<ipython-input-34-75b08cba454f>in<module>---->1 car2 =Car(-40,"X7",40)2print(car2)<ipython-input-31-175690bf3b98>in__init__(self, make, model, fuel_cap)7print(self.make)8else:---->9 raise ValueError("Make of the car can never be an integer")1011if self.fuel_cap <0:ValueError: Make of the car can never be an integer
Tecnologia
Ó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:
classCar:def__init__(self,make,model,fuel_cap):
self.make = make
self.model = model
self.fuel_cap = fuel_cap
ifisinstance(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)Tecnologia
-40 model X7with a fuel capacity of40 ltr.Tecnologia
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.
classDescriptor:def__init__(self):
self.__fuel_cap =0def__get__(self, instance, owner):return self.__fuel_cap
def__set__(self, instance, value):ifisinstance(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
Tecnologia
classCar:
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)Tecnologia
40BMW model X7with a fuel capacity of40 ltr.Tecnologia
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.
-40----------------------------------------ValueErrorTraceback(most recent call last)<ipython-input-115-1c3d23be72f7>in<module>---->1 car2 =Car("BMW","X7",-40)2print(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
78 def __str__(self):<ipython-input-106-0b252695aeed>in__set__(self, instance, value)1112if value <0:--->13 raise ValueError("Fuel Capacity can never be less than zero")1415 self.__fuel_cap = value
ValueError: Fuel Capacity can never be less than zero
Tecnologia
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.
-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)1112if value <0:--->13 raise ValueError("Fuel Capacity can never be less than zero")1415 self.__fuel_cap = value
ValueError: Fuel Capacity can never be less than zero
Tecnologia
----------------------------------------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)8print(value)9else:--->10 raise TypeError("Fuel Capacity can only be an integer")1112if value <0:TypeError: Fuel Capacity can only be an integer
Tecnologia
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 diferentesTecnologia
BMW model X7with a fuel capacity of48 ltr.Tecnologia
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.
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.
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.
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.
Mergulhe na ciência de dados com Python para analisar e visualizar seus dados de forma eficaz. Não precisa ter experiência ou conhecimento em programação.
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 combinding 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 ficavinculadaà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,setedelete.Vamos entender os métodos
get,setedeleteem 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
carcom três atributos:make,modelefuel_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çãoprint()oustr()é invocada em um objeto da classe.Como você viu acima, tudo certo até aqui!
Agora vamos mudar a capacidade de combustível do carro para menos 40.
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
ifno método__init__()para verificar se a capacidade informada é válida. Se não for, lançamos um ValueError.Pelo resultado acima, tudo funciona, já que o programa lança um
ValueErrorse a capacidade for abaixo de zero.Mas há outro problema: e se a
capacidadefor um float ou uma string? E não só a capacidade — omakee omodeltambém podem ser inteiros. Em todos esses casos, o programa não levantará exceção.Para tratar esse caso, você pode pensar em adicionar outro
ifou usarisinstancepara checagem de tipo.Desta vez, vamos usar o método embutido
isinstancepara tratar o erro.Ó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:
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.
Se a classe Descriptor parece obscura, calma. Vamos destrinchar cada método.
__init__()da classe Descriptor define a variável local__fuel_capcomo zero. Odunder(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.__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 classeCar). Nesse método, você simplesmente retorna o atributo cujo valor é definido em__set__().__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 parafuel_capé um inteiro; se não for, levantamosTypeError. Depois verificamos se é menor que zero; se for, levantamosValueError. Por fim, atualizamosfuel_capcom o valor válido.__delete__()é chamado quando o atributo é removido do objeto e, assim como__set__(), não retorna nada.A classe
Carpermanece praticamente igual, com a diferença de que adicionamos a instânciafuel_capda classeDescriptor(). 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_capno__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.
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 definirfuel_capcomo string e ver se dá erro.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:
Ao imprimir a instância
car2, você notará que os valores foram sobrescritos porcar3.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.