Estou tentando entender o que são os descritores do Python e para que eles podem ser úteis.
Descritores são atributos de classe (como propriedades ou métodos) com qualquer um dos seguintes métodos especiais:
__get__
(método não descritor de dados, por exemplo, em um método / função)
__set__
(método do descritor de dados, por exemplo, em uma instância de propriedade)
__delete__
(método do descritor de dados)
Esses objetos descritores podem ser usados como atributos em outras definições de classe de objeto. (Ou seja, eles vivem no __dict__
objeto de classe.)
Os objetos descritores podem ser usados para gerenciar programaticamente os resultados de uma pesquisa pontilhada (por exemplo foo.descriptor
) em uma expressão normal, uma atribuição e até mesmo uma exclusão.
Funções / métodos, métodos vinculados, property
, classmethod
, e staticmethod
todo o uso destes métodos especiais para controlar como eles são acessados via a pesquisa pontilhada.
Um descritor de dados , como property
, pode permitir uma avaliação lenta de atributos com base em um estado mais simples do objeto, permitindo que as instâncias usem menos memória do que se você precomputasse cada atributo possível.
Outro descritor de dados, a member_descriptor
, criado por __slots__
, permite economia de memória, permitindo que a classe armazene dados em uma estrutura de dados mutável, do tipo tupla, em vez da mais flexível, mas que consome espaço __dict__
.
Descritores que não são de dados, geralmente instância, classe e métodos estáticos, obtêm seus primeiros argumentos implícitos (geralmente nomeados cls
e self
, respectivamente) a partir do método que não é o descritor de dados __get__
,.
A maioria dos usuários do Python precisa aprender apenas o uso simples e não precisa aprender ou entender mais a implementação dos descritores.
Em profundidade: o que são descritores?
Um descritor é um objecto com qualquer um dos seguintes métodos ( __get__
, __set__
, ou __delete__
), destina-se a ser utilizada por meio de tracejado-pesquisa como se fosse um atributo típico de uma instância. Para um objeto-proprietário,, obj_instance
com um descriptor
objeto:
obj_instance.descriptor
chama
descriptor.__get__(self, obj_instance, owner_class)
retornando a value
É assim que todos os métodos e a get
propriedade on funcionam.
obj_instance.descriptor = value
chama
descriptor.__set__(self, obj_instance, value)
retornando None
É assim que a setter
propriedade on funciona.
del obj_instance.descriptor
chama
descriptor.__delete__(self, obj_instance)
retornando None
É assim que a deleter
propriedade on funciona.
obj_instance
é a instância cuja classe contém a instância do objeto descritor. self
é a instância do descritor (provavelmente apenas uma para a classe do obj_instance
)
Para definir isso com código, um objeto é um descritor se o conjunto de seus atributos cruzar com qualquer um dos atributos necessários:
def has_descriptor_attrs(obj):
return set(['__get__', '__set__', '__delete__']).intersection(dir(obj))
def is_descriptor(obj):
"""obj can be instance of descriptor or the descriptor class"""
return bool(has_descriptor_attrs(obj))
Um Descritor de Dados possui um __set__
e / ou __delete__
.
Um não descritor de dados não tem nem __set__
nem __delete__
.
def has_data_descriptor_attrs(obj):
return set(['__set__', '__delete__']) & set(dir(obj))
def is_data_descriptor(obj):
return bool(has_data_descriptor_attrs(obj))
Exemplos de objetos do descritor interno:
classmethod
staticmethod
property
- funções em geral
Descritores que não são de dados
Podemos ver isso classmethod
e staticmethod
somos não descritores de dados:
>>> is_descriptor(classmethod), is_data_descriptor(classmethod)
(True, False)
>>> is_descriptor(staticmethod), is_data_descriptor(staticmethod)
(True, False)
Ambos têm apenas o __get__
método:
>>> has_descriptor_attrs(classmethod), has_descriptor_attrs(staticmethod)
(set(['__get__']), set(['__get__']))
Observe que todas as funções também são descritores que não são de dados:
>>> def foo(): pass
...
>>> is_descriptor(foo), is_data_descriptor(foo)
(True, False)
Descritor de Dados, property
No entanto, property
é um descritor de dados:
>>> is_data_descriptor(property)
True
>>> has_descriptor_attrs(property)
set(['__set__', '__get__', '__delete__'])
Ordem de pesquisa pontilhada
Essas são distinções importantes , pois afetam a ordem de pesquisa de uma pesquisa pontilhada.
obj_instance.attribute
- Primeiro, o texto acima verifica se o atributo é um descritor de dados na classe da instância,
- Caso contrário, parece ver se o atributo está no
obj_instance
's __dict__
, então
- finalmente retorna a um descritor que não é de dados.
A conseqüência dessa ordem de pesquisa é que descritores que não são de dados, como funções / métodos, podem ser substituídos por instâncias .
Recapitulação e próximas etapas
Nós aprendemos que os descritores são objetos com qualquer um __get__
, __set__
ou __delete__
. Esses objetos descritores podem ser usados como atributos em outras definições de classe de objeto. Agora veremos como eles são usados, usando seu código como exemplo.
Análise do código da pergunta
Aqui está o seu código, seguido pelas suas perguntas e respostas para cada um:
class Celsius(object):
def __init__(self, value=0.0):
self.value = float(value)
def __get__(self, instance, owner):
return self.value
def __set__(self, instance, value):
self.value = float(value)
class Temperature(object):
celsius = Celsius()
- Por que preciso da classe descritor?
Seu descritor garante que você sempre tenha um float para este atributo de classe Temperature
e que não possa usá-lo del
para excluir o atributo:
>>> t1 = Temperature()
>>> del t1.celsius
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: __delete__
Caso contrário, seus descritores ignoram a classe de proprietário e as instâncias do proprietário, em vez disso, armazenam o estado no descritor. Você poderia compartilhar facilmente o estado em todas as instâncias com um atributo de classe simples (desde que você sempre o defina como flutuador da classe e nunca exclua-o ou se sinta à vontade com os usuários do seu código):
class Temperature(object):
celsius = 0.0
Isso dá a você exatamente o mesmo comportamento do seu exemplo (consulte a resposta à pergunta 3 abaixo), mas usa o Pythons builtin ( property
), e seria considerado mais idiomático:
class Temperature(object):
_celsius = 0.0
@property
def celsius(self):
return type(self)._celsius
@celsius.setter
def celsius(self, value):
type(self)._celsius = float(value)
- O que é instância e proprietário aqui? (em get ). Qual é o objetivo desses parâmetros?
instance
é a instância do proprietário que está chamando o descritor. O proprietário é a classe na qual o objeto descritor é usado para gerenciar o acesso ao ponto de dados. Veja as descrições dos métodos especiais que definem os descritores ao lado do primeiro parágrafo desta resposta para obter nomes de variáveis mais descritivos.
- Como eu chamaria / usaria este exemplo?
Aqui está uma demonstração:
>>> t1 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1
>>>
>>> t1.celsius
1.0
>>> t2 = Temperature()
>>> t2.celsius
1.0
Você não pode excluir o atributo:
>>> del t2.celsius
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: __delete__
E você não pode atribuir uma variável que não pode ser convertida em um float:
>>> t1.celsius = '0x02'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in __set__
ValueError: invalid literal for float(): 0x02
Caso contrário, o que você tem aqui é um estado global para todas as instâncias, gerenciado pela atribuição a qualquer instância.
A maneira esperada pela qual os programadores Python mais experientes conseguiriam esse resultado seria usar o property
decorador, que utiliza os mesmos descritores sob o capô, mas traz o comportamento para a implementação da classe owner (novamente, conforme definido acima):
class Temperature(object):
_celsius = 0.0
@property
def celsius(self):
return type(self)._celsius
@celsius.setter
def celsius(self, value):
type(self)._celsius = float(value)
Que tem exatamente o mesmo comportamento esperado do trecho de código original:
>>> t1 = Temperature()
>>> t2 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1.0
>>> t2.celsius
1.0
>>> del t1.celsius
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't delete attribute
>>> t1.celsius = '0x02'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 8, in celsius
ValueError: invalid literal for float(): 0x02
Conclusão
Abordamos os atributos que definem os descritores, a diferença entre descritores de dados e não descritores de dados, objetos internos que os utilizam e perguntas específicas sobre o uso.
Então, novamente, como você usaria o exemplo da pergunta? Espero que não. Espero que você comece com a minha primeira sugestão (um atributo de classe simples) e passe para a segunda sugestão (o decorador de propriedades), se achar necessário.
self
einstance
?