Je essayant de comprendre ce que sont les descripteurs Python et à quoi ils peuvent être utiles. Cependant, j'y échoue. Je comprends comment ils fonctionnent, mais voici mes doutes. Considérons le code suivant:
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 descripteur?
Qu'est-ce que instance
et owner
ici? (dans __get__
). Quel est le but de ces paramètres?
Comment pourrais-je appeler/utiliser cet exemple?
Le descripteur décrit comment le type property
de Python est implémenté. Un descripteur implémente simplement __get__
, __set__
, etc., puis est ajouté à une autre classe dans sa définition (comme vous l'avez fait ci-dessus avec la classe Temperature). Par exemple:
temp=Temperature()
temp.celsius #calls celsius.__get__
L'accès à la propriété à laquelle vous avez affecté le descripteur (celsius
dans l'exemple ci-dessus) appelle la méthode de descripteur appropriée.
instance
dans __get__
est l'instance de la classe (donc ci-dessus, __get__
recevrait temp
, tandis que owner
serait la classe avec le descripteur (donc être Temperature
).
Vous devez utiliser une classe de descripteur pour encapsuler la logique qui l’alimente. Ainsi, si le descripteur est utilisé pour mettre en cache une opération coûteuse (par exemple), il peut stocker la valeur sur lui-même et non sur sa classe.
Un article sur les descripteurs peut être trouvé ici .
EDIT: Comme Jchl l’a souligné dans les commentaires, si vous essayez simplement Temperature.celsius
, instance
sera None
.
Pourquoi ai-je besoin de la classe de descripteur?
Cela vous donne un contrôle supplémentaire sur le fonctionnement des attributs. Si vous êtes habitué aux getters et setters en Java, par exemple, c'est la façon de faire de Python. Un des avantages est que les utilisateurs ont l’impression de ressembler à un attribut (la syntaxe ne change pas). Vous pouvez donc commencer avec un attribut ordinaire, puis basculer vers un descripteur lorsque vous devez faire quelque chose d'extraordinaire.
Un attribut est juste une valeur mutable. Un descripteur vous permet d'exécuter du code arbitraire lors de la lecture ou de la définition (ou de la suppression) d'une valeur. Vous pouvez donc imaginer l'utiliser pour mapper un attribut sur un champ dans une base de données, par exemple, une sorte d'ORM.
Une autre utilisation pourrait être de refuser d'accepter une nouvelle valeur en lançant une exception dans __set__
- rendant ainsi "l'attribut" en lecture seule.
Qu'est-ce que
instance
etowner
ici? (dans__get__
). Quel est le but de ces paramètres?
C'est assez subtil (et la raison pour laquelle j'écris une nouvelle réponse ici - j'ai trouvé cette question en me demandant la même chose et sans trouver la réponse existante aussi géniale).
Un descripteur est défini sur une classe, mais est généralement appelé à partir d'une instance. Lorsqu'il est appelé à partir d'une instance, instance
et owner
sont définis (et vous pouvez résoudre owner
à partir de instance
de sorte qu'il semble inutile). Mais lorsqu'il est appelé depuis une classe, seul owner
est défini - c'est pourquoi il est présent.
Ceci n'est nécessaire que pour __get__
car c'est le seul qui puisse être appelé sur une classe. Si vous définissez la valeur de la classe, vous définissez le descripteur lui-même. De même pour la suppression. C'est pourquoi la owner
n'est pas nécessaire ici.
Comment pourrais-je appeler/utiliser cet exemple?
Eh bien, voici un truc sympa utilisant des classes similaires:
class Celsius:
def __get__(self, instance, owner):
return 5 * (instance.Fahrenheit - 32) / 9
def __set__(self, instance, value):
instance.Fahrenheit = 32 + 9 * value / 5
class Temperature:
celsius = Celsius()
def __init__(self, initial_f):
self.Fahrenheit = initial_f
t = Temperature(212)
print(t.celsius)
t.celsius = 0
print(t.Fahrenheit)
(J'utilise Python 3; pour python 2, vous devez vous assurer que ces divisions sont / 5.0
et / 9.0
). Ça donne:
100.0
32.0
Il existe maintenant d’autres moyens, sans doute meilleurs, d’obtenir le même effet dans python (par exemple, si Celsius était une propriété, qui est le même mécanisme de base mais place toute la source dans la classe Temperature), mais cela montre ce que peut être fait...
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) comportant l'une des méthodes spéciales suivantes:
__get__
_ (méthode sans descripteur, par exemple pour une méthode/fonction)__set__
_ (méthode du 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 dans d'autres définitions de classe d'objets. (C’est-à-dire qu’ils vivent dans le ___dict__
_ de l’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.
Les fonctions/méthodes, les méthodes liées, property
, classmethod
et staticmethod
utilisent toutes ces méthodes spéciales pour contrôler l'accès à celles-ci via la recherche en pointillé.
Un descripteur de données , comme property
, peut permettre une évaluation paresseuse des attributs basée sur un état plus simple de l'objet, permettant ainsi aux instances d'utiliser moins mémoire que si vous avez précalculé chaque attribut possible.
Un autre descripteur de données, un _member_descriptor
_, créé par __slots__
, permet d’économiser de la mémoire en permettant à la classe de stocker des données dans une structure de données mutable de type Tuple au lieu de plus flexible mais espace. -consuming ___dict__
_.
Les descripteurs autres que des données, généralement les méthodes d'instance, de classe et statiques, obtiennent leurs premiers arguments implicites (généralement nommés cls
et self
, respectivement) de leur méthode de descripteur non de 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 ni de comprendre davantage la mise en oeuvre des descripteurs.
Un descripteur est un objet avec l'une des méthodes suivantes (___get__
_, ___set__
_ ou ___delete__
_), destiné à être utilisé via une recherche en pointillés comme s'il s'agissait d'un attribut typique d'une instance. . Pour un objet propriétaire, _obj_instance
_, avec un objet descriptor
:
_obj_instance.descriptor
_ invoquedescriptor.__get__(self, obj_instance, owner_class)
retourne un value
Voici comment fonctionnent toutes les méthodes et la get
d'une propriété.
_obj_instance.descriptor = value
_ invoquedescriptor.__set__(self, obj_instance, value)
retournant None
Voici comment fonctionne la setter
sur une propriété.
_del obj_instance.descriptor
_ invoquedescriptor.__delete__(self, obj_instance)
retournant None
Voici comment fonctionne la 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 une seule pour la classe de la _obj_instance
_)
Pour définir ceci 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__
_.
A Le descripteur non-data 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))
_
classmethod
staticmethod
property
Nous pouvons voir que classmethod
et staticmethod
sont des descripteurs non-data:
_>>> is_descriptor(classmethod), is_data_descriptor(classmethod)
(True, False)
>>> is_descriptor(staticmethod), is_data_descriptor(staticmethod)
(True, False)
_
Tous deux ont uniquement la méthode ___get__
_:
_>>> has_descriptor_attrs(classmethod), has_descriptor_attrs(staticmethod)
(set(['__get__']), set(['__get__']))
_
Notez que toutes les fonctions sont aussi des descripteurs non-données:
_>>> def foo(): pass
...
>>> is_descriptor(foo), is_data_descriptor(foo)
(True, False)
_
property
Cependant, property
est un descripteur de données:
_>>> is_data_descriptor(property)
True
>>> has_descriptor_attrs(property)
set(['__set__', '__get__', '__delete__'])
_
Celles-ci sont importantes distinctions , car elles affectent l'ordre de recherche pour une recherche en pointillé.
_obj_instance.attribute
_
obj_instance
_'s ___dict__
_, puisLa conséquence de cet ordre de recherche est que les non-descripteurs de données tels que les fonctions/méthodes peuvent être remplacés par des instances .
Nous avons appris que les descripteurs sont des objets avec l'un quelconque de ___get__
_, ___set__
_ ou ___delete__
_. Ces objets descripteurs peuvent être utilisés comme attributs dans d'autres définitions de classe d'objets. Nous allons maintenant voir comment ils sont utilisés, en utilisant votre code comme exemple.
Voici votre code, suivi de vos questions et réponses à chacune d’elles:
_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 descripteur?
Votre descripteur garantit que vous avez toujours un flottant pour cet attribut de classe de 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 de propriétaire et les instances de ce dernier, mais stockent l'état dans le descripteur. Vous pouvez tout aussi facilement partager l'état de toutes les instances avec un attribut de classe simple (tant que vous le définissez toujours comme un float de la classe et ne le supprimez jamais, ou que vous êtes à l'aise avec les utilisateurs de votre code):
_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 une fonction intégrée Pythons (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? (dans 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 plus de noms de variables descriptives.
- Comment pourrais-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 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
_
Sinon, vous avez ici un état global pour toutes les instances, géré en affectant une instance.
Le moyen attendu par les programmeurs Python les plus expérimentés d'atteindre ce résultat serait d'utiliser le décorateur property
, qui utilise les mêmes descripteurs sous le capot, mais introduit le comportement dans la mise en œuvre du classe de 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)
_
Ce qui a exactement le même comportement attendu du morceau de code 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
_
Nous avons abordé 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 relatives à leur utilisation.
Encore une fois, comment utiliseriez-vous l'exemple de la question? J'espère que tu ne le ferais pas. 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 la propriété) si vous le jugez nécessaire.
Avant d'entrer dans les détails des descripteurs, il peut être important de savoir comment fonctionne la recherche d'attribut dans Python. Cela suppose que la classe n'a pas de métaclasse et qu'elle utilise l'implémentation par défaut de __getattribute__
(les deux peuvent être utilisés pour "personnaliser" le comportement).
La meilleure illustration de la recherche d'attribut (dans Python 3.x ou dans le cas de classes de style nouveau dans Python 2.x) provient de Comprendre Python métaclasses (codelog d'ionel) . L'image utilise :
comme substitut de "recherche d'attribut non personnalisable".
Ceci représente la recherche d'un attribut foobar
sur un instance
de Class
:
Deux conditions sont importantes ici:
instance
a une entrée pour le nom de l'attribut et qu'elle a __get__
et __set__
.instance
a aucune entrée pour le nom de l'attribut, mais la classe en a une et elle a __get__
.C'est là que les descripteurs entrent en jeu:
__get__
et __set__
.__get__
.Dans les deux cas, la valeur renvoyée passe par __get__
appelé avec l'instance comme premier argument et la classe comme second argument.
La recherche est encore plus compliquée pour la recherche d'attribut de classe (voir par exemple Recherche d'attribut de classe (dans le blog mentionné ci-dessus) ).
Passons à vos questions spécifiques:
Pourquoi ai-je besoin de la classe de descripteur?
Dans la plupart des cas, vous n'avez pas besoin d'écrire des classes de descripteurs! Cependant, vous êtes probablement un utilisateur final très habituel. Par exemple des fonctions. Les fonctions sont des descripteurs, c'est ainsi que les fonctions peuvent être utilisées comme méthodes avec self
implicitement passé en tant que premier argument.
def test_function(self):
return self
class TestClass(object):
def test_method(self):
...
Si vous recherchez test_method
sur une instance, vous obtenez une "méthode liée":
>>> instance = TestClass()
>>> instance.test_method
<bound method TestClass.test_method of <__main__.TestClass object at ...>>
De même, vous pouvez également lier une fonction en appelant sa méthode __get__
manuellement (ce qui n'est pas vraiment recommandé, à des fins d'illustration):
>>> test_function.__get__(instance, TestClass)
<bound method test_function of <__main__.TestClass object at ...>>
Vous pouvez même appeler cette "méthode auto-liée":
>>> test_function.__get__(instance, TestClass)()
<__main__.TestClass at ...>
Notez que je n'ai fourni aucun argument et que la fonction a renvoyé l'instance que j'avais liée!
Les fonctions sont Descripteurs non-données !
Quelques exemples intégrés d'un descripteur de données seraient property
. Négliger getter
, setter
et deleter
le descripteur property
est (from Guide de description "Propriétés" ):
class Property(object):
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)
Comme c'est un descripteur de données, il est appelé à chaque fois que vous recherchez le "nom" du property
et qu'il délègue simplement les fonctions décorées avec @property
, @name.setter
et @name.deleter
( si présent).
Il existe plusieurs autres descripteurs dans la bibliothèque standard, par exemple staticmethod
, classmethod
.
Le point des descripteurs est simple (bien que vous en ayez rarement besoin): Code commun abstrait pour l’accès aux attributs. property
est une abstraction pour l'accès à une variable d'instance, function
fournit une abstraction pour les méthodes, staticmethod
fournit une abstraction pour les méthodes ne nécessitant pas d'accès à l'instance et classmethod
fournit une abstraction pour les méthodes nécessitant un accès de classe plutôt qu'un accès d'instance (ceci est un peu simplifié).
Un autre exemple serait un propriété de classe .
Un exemple amusant (utilisant __set_name__
de Python 3.6) pourrait également être une propriété qui autorise uniquement un type spécifique:
class TypedProperty(object):
__slots__ = ('_name', '_type')
def __init__(self, typ):
self._type = typ
def __get__(self, instance, klass=None):
if instance is None:
return self
return instance.__dict__[self._name]
def __set__(self, instance, value):
if not isinstance(value, self._type):
raise TypeError(f"Expected class {self._type}, got {type(value)}")
instance.__dict__[self._name] = value
def __delete__(self, instance):
del instance.__dict__[self._name]
def __set_name__(self, klass, name):
self._name = name
Ensuite, vous pouvez utiliser le descripteur dans une classe:
class Test(object):
int_prop = TypedProperty(int)
Et en jouant un peu avec:
>>> t = Test()
>>> t.int_prop = 10
>>> t.int_prop
10
>>> t.int_prop = 20.0
TypeError: Expected class <class 'int'>, got <class 'float'>
Ou une "propriété paresseuse":
class LazyProperty(object):
__slots__ = ('_fget', '_name')
def __init__(self, fget):
self._fget = fget
def __get__(self, instance, klass=None):
if instance is None:
return self
try:
return instance.__dict__[self._name]
except KeyError:
value = self._fget(instance)
instance.__dict__[self._name] = value
return value
def __set_name__(self, klass, name):
self._name = name
class Test(object):
@LazyProperty
def lazy(self):
print('calculating')
return 10
>>> t = Test()
>>> t.lazy
calculating
10
>>> t.lazy
10
Ce sont des cas où il serait logique de déplacer la logique dans un descripteur commun, mais on pourrait aussi les résoudre (mais peut-être en répétant du code) avec d'autres moyens.
Qu'est-ce que
instance
etowner
ici? (dans__get__
). Quel est le but de ces paramètres?
Cela dépend de la façon dont vous recherchez l'attribut. Si vous recherchez l'attribut sur une instance, alors:
Si vous recherchez l'attribut sur la classe (en supposant que le descripteur est défini sur la classe):
None
Donc, en gros, le troisième argument est nécessaire si vous souhaitez personnaliser le comportement lorsque vous effectuez une recherche au niveau de la classe (car la variable instance
est None
).
Comment pourrais-je appeler/utiliser cet exemple?
Votre exemple est essentiellement une propriété qui n'autorise que les valeurs pouvant être converties en float
et qui est partagée entre toutes les instances de la classe (et sur la classe - bien que vous ne puissiez utiliser qu'un accès "en lecture" sur la classe, sinon remplacerait l'instance de descripteur):
>>> t1 = Temperature()
>>> t2 = Temperature()
>>> t1.celsius = 20 # setting it on one instance
>>> t2.celsius # looking it up on another instance
20.0
>>> Temperature.celsius # looking it up on the class
20.0
C'est pourquoi les descripteurs utilisent généralement le deuxième argument (instance
) pour stocker la valeur afin d'éviter de la partager. Cependant, dans certains cas, le partage d'une valeur entre instances peut être souhaité (bien que je ne puisse penser à un scénario pour le moment). Cependant, cela n’a pratiquement aucun sens pour une propriété Celsius dans une classe de température ... sauf peut-être comme un exercice purement académique.
Pourquoi ai-je besoin de la classe de descripteur?
Inspiré par Fluent Python par Buciano Ramalho
Imaging vous avez une classe comme ça
class LineItem:
price = 10.9
weight = 2.1
def __init__(self, name, price, weight):
self.name = name
self.price = price
self.weight = weight
item = LineItem("Apple", 2.9, 2.1)
item.price = -0.9 # it's price is negative, you need to refund to your customer even you delivered the Apple :(
item.weight = -0.8 # negative weight, it doesn't make sense
Nous devrions valider le poids et le prix en évitant de leur attribuer un nombre négatif, nous pouvons écrire moins de code si nous utilisons un descripteur comme proxy
class Quantity(object):
__index = 0
def __init__(self):
self.__index = self.__class__.__index
self._storage_name = "quantity#{}".format(self.__index)
self.__class__.__index += 1
def __set__(self, instance, value):
if value > 0:
setattr(instance, self._storage_name, value)
else:
raise ValueError('value should >0')
def __get__(self, instance, owner):
return getattr(instance, self._storage_name)
puis définissez la classe LineItem comme ceci:
class LineItem(object):
weight = Quantity()
price = Quantity()
def __init__(self, name, weight, price):
self.name = name
self.weight = weight
self.price = price
et nous pouvons étendre la classe de quantité à faire la validation plus commune
Vous verriez https://docs.python.org/3/howto/descriptor.html#properties
class Property(object):
"Emulate PyProperty_Type() in Objects/descrobject.c"
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)
def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)
J'ai essayé (avec des modifications mineures comme suggéré) le code de la réponse d'Andrew Cooke. (J'utilise python 2.7).
Le code:
#!/usr/bin/env python
class Celsius:
def __get__(self, instance, owner): return 9 * (instance.Fahrenheit + 32) / 5.0
def __set__(self, instance, value): instance.Fahrenheit = 32 + 5 * value / 9.0
class Temperature:
def __init__(self, initial_f): self.Fahrenheit = initial_f
celsius = Celsius()
if __== "__main__":
t = Temperature(212)
print(t.celsius)
t.celsius = 0
print(t.Fahrenheit)
Le résultat:
C:\Users\gkuhn\Desktop>python test2.py
<__main__.Celsius instance at 0x02E95A80>
212
Avec Python avant 3, assurez-vous de sous-classe à partir de l'objet qui fera en sorte que le descripteur fonctionne correctement, car la magie get ne fonctionne pas pour les anciennes classes de style.