J'essaie de comprendre ce que sont les descripteurs de Python et à quoi ils peuvent être utiles.
Les descripteurs sont des attributs de classe (comme des propriétés ou des méthodes) avec l'une des méthodes spéciales suivantes:
__get__
(méthode non descripteur de données, par exemple sur une méthode / fonction)
__set__
(méthode de descripteur de données, par exemple sur une instance de propriété)
__delete__
(méthode du descripteur de données)
Ces objets descripteurs peuvent être utilisés comme attributs sur d'autres définitions de classe d'objets. (Autrement dit, ils vivent dans l' __dict__
objet de classe.)
Les objets descripteurs peuvent être utilisés pour gérer par programme les résultats d'une recherche en pointillés (par exemple foo.descriptor
) dans une expression normale, une affectation et même une suppression.
Fonctions / méthodes, méthodes liées, property
, classmethod
et staticmethod
toute utilisation de ces méthodes spéciales pour contrôler la façon dont ils sont accessibles via la recherche en pointillés.
Un descripteur de données , comme property
, peut permettre une évaluation paresseuse des attributs sur la base d'un état plus simple de l'objet, permettant aux instances d'utiliser moins de mémoire que si vous aviez précalculé chaque attribut possible.
Un autre descripteur de données, a member_descriptor
, créé par __slots__
, permet des économies de mémoire en permettant à la classe de stocker des données dans une structure de données de type tuple mutable au lieu de la plus flexible mais consommatrice d'espace __dict__
.
Descripteurs non données, généralement exemple, la classe et les méthodes statiques, obtenir leurs premiers arguments implicites (généralement nommés cls
et self
, respectivement) de leur méthode de descripteur non données, __get__
.
La plupart des utilisateurs de Python n'ont besoin d'apprendre que l'utilisation simple et n'ont pas besoin d'apprendre ou de comprendre davantage la mise en œuvre des descripteurs.
En détail: que sont les descripteurs?
Un descripteur est un objet avec l' une des méthodes suivantes ( __get__
, __set__
, ou __delete__
), destinée à être utilisée par-lookup pointillés comme si elle était une caractéristique typique d'une instance. Pour un objet propriétaire obj_instance
, avec un descriptor
objet:
obj_instance.descriptor
invoque le
descriptor.__get__(self, obj_instance, owner_class)
retour d'un value
C'est ainsi que fonctionnent toutes les méthodes et get
sur une propriété.
obj_instance.descriptor = value
invoque le
descriptor.__set__(self, obj_instance, value)
retour None
Voici comment fonctionne le setter
sur une propriété.
del obj_instance.descriptor
invoque le
descriptor.__delete__(self, obj_instance)
retour None
Voici comment fonctionne le deleter
sur une propriété.
obj_instance
est l'instance dont la classe contient l'instance de l'objet descripteur. self
est l'instance du descripteur (probablement un seul pour la classe du obj_instance
)
Pour définir cela avec du code, un objet est un descripteur si l'ensemble de ses attributs croise l'un des attributs requis:
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))
Un descripteur de données a un __set__
et / ou __delete__
.
Un non-descripteur de données n'a ni __set__
ni __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))
Exemples d'objets de descripteur intégré:
classmethod
staticmethod
property
- fonctions en général
Descripteurs non liés aux données
Nous pouvons voir cela classmethod
et staticmethod
sommes des non-descripteurs de données:
>>> is_descriptor(classmethod), is_data_descriptor(classmethod)
(True, False)
>>> is_descriptor(staticmethod), is_data_descriptor(staticmethod)
(True, False)
Les deux n'ont que la __get__
méthode:
>>> has_descriptor_attrs(classmethod), has_descriptor_attrs(staticmethod)
(set(['__get__']), set(['__get__']))
Notez que toutes les fonctions sont également des non-descripteurs de données:
>>> def foo(): pass
...
>>> is_descriptor(foo), is_data_descriptor(foo)
(True, False)
Descripteur de données, property
Cependant, property
est un Data-Descriptor:
>>> is_data_descriptor(property)
True
>>> has_descriptor_attrs(property)
set(['__set__', '__get__', '__delete__'])
Ordre de recherche en pointillé
Ce sont des distinctions importantes , car elles affectent l'ordre de recherche pour une recherche en pointillés.
obj_instance.attribute
- Tout d'abord, ce qui précède cherche à voir si l'attribut est un Data-Descriptor sur la classe de l'instance,
- Sinon, il cherche à voir si l'attribut est dans le
obj_instance
's __dict__
, alors
- il retombe finalement sur un non-descripteur de données.
La conséquence de cet ordre de recherche est que les non-descripteurs de données comme les fonctions / méthodes peuvent être remplacés par des instances .
Récapitulatif et prochaines étapes
Nous avons appris que les descripteurs sont des objets avec l' une des __get__
, __set__
ou __delete__
. Ces objets descripteurs peuvent être utilisés comme attributs sur d'autres définitions de classe d'objets. Nous allons maintenant voir comment ils sont utilisés, en utilisant votre code comme exemple.
Analyse du code de la question
Voici votre code, suivi de vos questions et réponses à chacun:
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()
- Pourquoi ai-je besoin de la classe de descripteurs?
Votre descripteur garantit que vous disposez toujours d'un flottant pour cet attribut de classe Temperature
et que vous ne pouvez pas utiliser del
pour supprimer l'attribut:
>>> t1 = Temperature()
>>> del t1.celsius
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: __delete__
Sinon, vos descripteurs ignorent la classe propriétaire et les instances du propriétaire, à la place, stockant l'état dans le descripteur. Vous pouvez tout aussi facilement partager l'état entre toutes les instances avec un simple attribut de classe (tant que vous le définissez toujours comme un flottant pour la classe et que vous ne le supprimez jamais, ou que vous êtes à l'aise avec les utilisateurs de votre code qui le font):
class Temperature(object):
celsius = 0.0
Cela vous donne exactement le même comportement que votre exemple (voir la réponse à la question 3 ci-dessous), mais utilise un Pythons builtin ( property
), et serait considéré comme plus idiomatique:
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)
- Quelle est l'instance et le propriétaire ici? (en get ). Quel est le but de ces paramètres?
instance
est l'instance du propriétaire qui appelle le descripteur. Le propriétaire est la classe dans laquelle l'objet descripteur est utilisé pour gérer l'accès au point de données. Voir les descriptions des méthodes spéciales qui définissent les descripteurs à côté du premier paragraphe de cette réponse pour des noms de variables plus descriptifs.
- Comment puis-je appeler / utiliser cet exemple?
Voici une démonstration:
>>> t1 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1
>>>
>>> t1.celsius
1.0
>>> t2 = Temperature()
>>> t2.celsius
1.0
Vous ne pouvez pas supprimer l'attribut:
>>> del t2.celsius
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: __delete__
Et vous ne pouvez pas affecter une variable qui ne peut pas être convertie en flottant:
>>> 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
Sinon, ce que vous avez ici est un état global pour toutes les instances, qui est géré en l'attribuant à n'importe quelle instance.
La manière attendue que les programmeurs Python les plus expérimentés atteignent ce résultat serait d'utiliser le property
décorateur, qui utilise les mêmes descripteurs sous le capot, mais apporte le comportement dans l'implémentation de la classe propriétaire (encore une fois, comme défini ci-dessus):
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)
Qui a exactement le même comportement attendu du morceau de code d'origine:
>>> 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
Conclusion
Nous avons couvert les attributs qui définissent les descripteurs, la différence entre les descripteurs de données et les non-descripteurs de données, les objets intégrés qui les utilisent et des questions spécifiques sur l'utilisation.
Encore une fois, comment utiliseriez-vous l'exemple de la question? J'espère que non. J'espère que vous commencerez par ma première suggestion (un attribut de classe simple) et passerez à la deuxième suggestion (le décorateur de propriété) si vous le jugez nécessaire.
self
etinstance
?