Comprendre les descripteurs __get__ et __set__ et Python

310

J'essaie de comprendre ce que les descripteurs de Python sont et ce qu'ils sont utiles pour. Je comprends comment ils fonctionnent, mais voici mes doutes. Considérez 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()
  1. Pourquoi ai-je besoin de la classe de descripteurs?

  2. Qu'est-ce que c'est instanceet ownerici? (en __get__). Quel est le but de ces paramètres?

  3. Comment puis-je appeler / utiliser cet exemple?

Matt Bronson
la source

Réponses:

147

Le descripteur est la façon dont le propertytype de Python est implémenté. Un descripteur simplement des outils __get__, __set__etc. et est ensuite ajouté à une autre classe dans sa définition (comme vous l'avez fait ci - dessus avec la classe de température). Par exemple:

temp=Temperature()
temp.celsius #calls celsius.__get__

L'accès à la propriété à laquelle vous avez affecté le descripteur ( celsiusdans l'exemple ci-dessus) appelle la méthode de descripteur appropriée.

instancein __get__est l'instance de la classe (donc ci-dessus, __get__recevrait temp, tandis que ownerla classe avec le descripteur (ce serait le cas Temperature).

Vous devez utiliser une classe de descripteurs pour encapsuler la logique qui l'alimente. De cette façon, si le descripteur est utilisé pour mettre en cache une opération coûteuse (par exemple), il pourrait 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, ce instancesera le cas None.

li.davidm
la source
6
Quelle est la différence entre selfet instance?
Lemma Prism
2
'instance' peut être une instance de n'importe quelle classe, self sera une instance de la même classe.
TheBeginner
3
@LemmaPrism selfest l'instance de descripteur, instanceest l'instance de la classe (si instanciée) le descripteur est entre ( instance.__class__ is owner).
Tcll
Temperature.celsiusdonne la valeur 0.0selon le code celsius = Celsius(). Le descripteur Celsius est appelé, donc son instance a la valeur init 0.0attribuée à l'attribut de classe de température, celsius.
Angel Salazar
109

Pourquoi ai-je besoin de la classe de descripteurs?

Il 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 avantage est qu'il ressemble aux utilisateurs comme un attribut (il n'y a pas de changement de syntaxe). Vous pouvez donc commencer avec un attribut ordinaire, puis, lorsque vous avez besoin de faire quelque chose de fantaisiste, passez à un descripteur.

Un attribut n'est qu'une valeur modifiable. 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 à 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 __set__, rendant ainsi "l'attribut" en lecture seule.

Qu'est-ce que c'est instanceet ownerici? (en __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 je n'ai pas trouvé la réponse existante si bien).

Un descripteur est défini sur une classe, mais est généralement appelé à partir d'une instance. Quand il est appelé à partir d'une instance à la fois instanceet ownerest défini (et vous pouvez travailler à ownerpartir de instancesorte qu'il semble un peu inutile). Mais lorsqu'il est appelé à partir d'une classe, seul ownerest défini - c'est pourquoi il est là.

Ceci n'est nécessaire que __get__parce que c'est le seul qui peut être appelé sur une classe. Si vous définissez la valeur de classe, vous définissez le descripteur lui-même. De même pour la suppression. C'est pourquoi le ownern'est pas nécessaire là-bas.

Comment puis-je appeler / utiliser cet exemple?

Eh bien, voici une astuce intéressante 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.0et / 9.0). Ça donne:

100.0
32.0

Il existe maintenant d'autres façons, sans doute meilleures, d'obtenir le même effet en python (par exemple, si celsius était une propriété, qui est le même mécanisme de base mais place toute la source à l'intérieur de la classe de température), mais cela montre ce qui peut être fait ...

Andrew Cooke
la source
2
Les conversions sont fausses: elles doivent être C = 5 (F − 32) / 9, F = 32 + 9C / 5.
musiphil
1
Assurez-vous que vous avez un objet de température. Faire ce qui suit gâche tout. t1 = Température (190) imprime t1.celsius t1.celsius = 100 imprime t1.fahrenheit Maintenant, lorsque vous cochez t.celcius et t.fahrenheit, ils sont également modifiés. t.celcius est 115 et t.fahrenheit est 32. ce qui est clairement faux. @Eric
Ishan Bhatt
1
@IshanBhatt: Je pense que c'est à cause de l'erreur signalée par musiphil ci-dessus. De plus, ce n'est pas ma réponse
Eric
69

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, classmethodet staticmethodtoute 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 clset 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 descriptorobjet:

  • obj_instance.descriptorinvoque le
    descriptor.__get__(self, obj_instance, owner_class)retour d'un value
    C'est ainsi que fonctionnent toutes les méthodes et getsur une propriété.

  • obj_instance.descriptor = valueinvoque le
    descriptor.__set__(self, obj_instance, value)retour None
    Voici comment fonctionne le settersur une propriété.

  • del obj_instance.descriptorinvoque le
    descriptor.__delete__(self, obj_instance)retour None
    Voici comment fonctionne le deletersur une propriété.

obj_instanceest l'instance dont la classe contient l'instance de l'objet descripteur. selfest 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 classmethodet staticmethodsommes 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, propertyest 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
  1. Tout d'abord, ce qui précède cherche à voir si l'attribut est un Data-Descriptor sur la classe de l'instance,
  2. Sinon, il cherche à voir si l'attribut est dans le obj_instance's __dict__, alors
  3. 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()
  1. Pourquoi ai-je besoin de la classe de descripteurs?

Votre descripteur garantit que vous disposez toujours d'un flottant pour cet attribut de classe Temperatureet que vous ne pouvez pas utiliser delpour 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)
  1. Quelle est l'instance et le propriétaire ici? (en get ). Quel est le but de ces paramètres?

instanceest 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.

  1. 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 propertydé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.

Aaron Hall
la source
1
Bien, j'ai appris le plus de cette réponse (certainement aussi des autres). Une question à propos de cette déclaration "La manière attendue que les programmeurs Python les plus expérimentés parviennent à ce résultat ...". La classe de température que vous définissez avant et après l'instruction est identique. Ai-je raté ce que vous voulez dire ici?
Yolo Voe
1
@YoloVoe non, c'est vrai, j'ai ajouté un verbiage entre parenthèses pour souligner qu'il s'agit d'une répétition de ce qui précède.
Aaron Hall
1
Ceci est une réponse incroyable. Je devrai le relire quelques fois de plus, mais j'ai l'impression que ma compréhension de Python vient de grimper de quelques crans
Lucas Young
20

Avant d'entrer dans les détails des descripteurs, il peut être important de savoir comment fonctionne la recherche d'attributs en 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ées pour "personnaliser" le comportement).

La meilleure illustration de la recherche d'attributs (dans Python 3.x ou pour les classes de nouveau style dans Python 2.x) dans ce cas est de comprendre les métaclasses Python (le codeelog d'ionel) . L'image utilise :comme substitut à la "recherche d'attribut non personnalisable".

Cela représente la recherche d'un attribut foobarsur l'un instancedes éléments suivants Class:

entrez la description de l'image ici

Deux conditions sont ici importantes:

  • Si la classe de instancepossède une entrée pour le nom d'attribut et qu'elle contient __get__et __set__.
  • Si le instancen'a pas d' entrée pour le nom d'attribut mais que la classe en a un et il en a un __get__.

C'est là que les descripteurs entrent en jeu:

  • Descripteurs de données qui ont à la fois __get__et __set__.
  • Descripteurs non-données qui ont seulement __get__.

Dans les deux cas, la valeur renvoyée est __get__appelée avec l'instance comme premier argument et la classe comme deuxième 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 descripteurs?

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 régulier. Par exemple des fonctions. Les fonctions sont des descripteurs, c'est ainsi que les fonctions peuvent être utilisées comme des méthodes avec selfimplicitement passé comme premier argument.

def test_function(self):
    return self

class TestClass(object):
    def test_method(self):
        ...

Si vous recherchez test_methodune instance, vous obtiendrez 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 __get__méthode manuellement (pas vraiment recommandé, juste à 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 des descripteurs non-données !

Certains exemples intégrés d'un descripteur de données le seraient property. Négliger getter, setteret deleterle propertydescripteur est (à partir du guide descriptif HowTo "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 il est un descripteur de données , il est invoqué chaque fois que vous regardez le « nom » du propertyet il délègue simplement aux fonctions décorées avec @property, @name.setteret @name.deleter( le cas échéant).

Il y a plusieurs autres descripteurs dans la bibliothèque standard, par exemple staticmethod, classmethod.

Le point des descripteurs est facile (bien que vous en ayez rarement besoin): Code commun abstrait pour l'accès aux attributs. propertyest une abstraction pour l'accès aux variables d'instance, functionfournit une abstraction pour les méthodes, staticmethodfournit une abstraction pour les méthodes qui n'ont pas besoin d'accès aux instances et classmethodfournit une abstraction pour les méthodes qui nécessitent un accès aux classes plutôt qu'un accès aux instances (c'est un peu simplifié).

Un autre exemple serait une propriété de classe .

Un exemple amusant (à l'aide __set_name__de Python 3.6) pourrait également être une propriété qui n'autorise qu'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 jouer 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ù le déplacement de la logique dans un descripteur commun pourrait avoir un sens, mais on pourrait également les résoudre (mais peut-être en répétant du code) avec d'autres moyens.

Qu'est-ce que c'est instanceet ownerici? (en __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:

  • le deuxième argument est l'instance sur laquelle vous recherchez l'attribut
  • le troisième argument est la classe de l'instance

Dans le cas où vous recherchez l'attribut sur la classe (en supposant que le descripteur est défini sur la classe):

  • le deuxième argument est None
  • le troisième argument est la classe où vous recherchez l'attribut

Donc, fondamentalement, le troisième argument est nécessaire si vous souhaitez personnaliser le comportement lorsque vous effectuez une recherche au niveau de la classe (car instancec'est le cas None).

Comment puis-je appeler / utiliser cet exemple?

Votre exemple est fondamentalement une propriété qui n'autorise que les valeurs qui peuvent être converties floatet qui sont partagées entre toutes les instances de la classe (et sur la classe - bien que l'on ne puisse utiliser que l'accès en lecture sur la classe, sinon vous remplaceriez 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 et éviter de la partager. Cependant, dans certains cas, le partage d'une valeur entre les instances peut être souhaité (bien que je ne puisse pas penser à un scénario pour le moment). Cependant, cela n'a pratiquement aucun sens pour une propriété Celsius sur une classe de température ... sauf peut-être comme un exercice purement académique.

MSeifert
la source
Je ne sais pas si l'arrière-plan transparent du graphique qui souffre vraiment en mode sombre doit être signalé comme un bug dans stackoverflow.
Tshirtman
@Tshirtman Je pense que c'est un problème avec l'image elle-même. Ce n'est pas complètement transparent ... Je l'ai pris du billet de blog et je ne sais pas comment le recréer avec un fond transparent approprié. C'est dommage qu'il soit si bizarre avec le fond sombre :(
MSeifert
9

Pourquoi ai-je besoin de la classe de descripteurs?

Inspiré par Fluent Python par Buciano Ramalho

Imaginez que vous avez une classe comme celle-ci

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 devons valider le poids et le prix pour éviter de leur attribuer un nombre négatif, nous pouvons écrire moins de code si nous utilisons le descripteur comme proxy car cela

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)

définissez ensuite 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 Quantité pour faire une validation plus courante

wllbll
la source
1
Cas d'utilisation intéressant, car il montre comment utiliser le descripteur pour interagir avec plusieurs instances d'utilisateurs. Au départ, je ne comprenais pas le point important: un attribut avec un descripteur doit être créé dans l'espace de noms de classe (par exemple weight = Quantity(), mais les valeurs doivent être définies dans l'espace de noms d'instance uniquement en utilisant self(par exemple self.weight = 4), sinon l'attribut serait renvoyé à la nouvelle valeur et le descripteur serait jeté. Bien!
min
Je ne peux pas comprendre une chose. Vous définissez en weight = Quantity()tant que variable de classe et son __get__et __set__travaillez sur la variable d'instance. Comment?
Technocrate
0

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 __name__ == "__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 que vous sous-classe de l'objet qui fera fonctionner le descripteur correctement car la magie get ne fonctionne pas pour les anciennes classes de style.

Gregory Kuhn
la source
1
Les descripteurs ne fonctionnent qu'avec de nouvelles classes de style. Pour python 2.x, cela signifie dériver votre classe de "objet", qui est par défaut dans Python 3.
Ivo van der Wijk
0

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__)
Yonks Somarl
la source
1
Cela ne répond pas à la question ou ne fournit aucune information utile.
Sebastian Nielsen