Les descripteurs Python et l’accès aux attributs

Les descripteurs en Python nous permettent de contrôler la manière dont les attributs des classes sont accessibles ou modifiés. Un modèle souvent rencontré consiste à définir des propriétés pour utiliser les méthodes setter et getter comme s’il s’agissait d’un attribut.

descripteur

Les descripteurs en Python, sont des classes qui implémentent des méthodes __get__(), __set__() ou __delete__(). En fait, ils sont souvent utilisés pour protéger les attributs de toutes modifications en créant des variables privées. En outre, les descripteurs peuvent augmenter la lisibilité d’un programme et la qualité du code.

Il est important de noter que les descripteurs sont affectés à une classe et non à une instance. Donc, la modification de la classe écrase ou supprime le descripteur lui-même.

Les descripteurs Python vous fournissent une technique puissante et avancée pour écrire du code réutilisable qui peut être partagé entre les classes.

En fait, le protocole de descripteur Python est le mécanisme qui alimente les propriétés, les méthodes, les méthodes statiques et les méthodes de classe.

Le fonctionnement des attributs en Python

En Python, un espace de noms est un mappage entre des objets et des noms. Pour faire simple, disons que c’est un dictionnaire Python qui a comme clé le nom de l’objet et sa valeur comme valeur.
Les classes et les instances possèdent des espaces de noms différents, par exemple, nous avons notre_classe.__dict__ comme espace de noms pour la classe notre_classe et notre_instance.__dict__ comme espace de noms pour l’instance notre_instance.

Donc, lorsque vous accédez à un attribut en tant que propriété d’un objet à l’aide de la convention de point, il recherche d’abord dans l’espace de nom de cette instance.
S’il est trouvé, il renvoie sa valeur, sinon, il recherche dans l’espace de noms de la classe. Si rien n’est trouvé, cela soulèvera une AttributeError.
Par conséquent, l’espace de noms de l’instance est recherché avant l’espace de noms de la classe.

Il existe trois façons pour accéder à un attribut. Disons que nous avons l’attribut nom et un objet obj :

Rechercher la valeur de l’attribut nom : une_variable = obj.nom.
Modifier sa valeur : obj.nom = nouvelle_valeur.
Pour le supprimer : del obj.nom.

Prenons l’extrait de code suivant :

class Foo:
    def __init__(self):
        self.bar = 'bonjour!'

foo = Foo()
print(foo.bar)
bonjour!

Utiliser le dictionnaire __dict__ pour accéder à un attribut

La plupart des classes Python possèdent un dictionnaire interne appelé __dict__ qui contient toutes les variables.

print(foo.__dict__)
{'bar': 'bonjour!'}

Comme nos attributs sont dans un dictionnaire, nous allons pouvoir utiliser les clés pour accéder aux valeurs.

foo.__dict__['bar']
bonjour!

Nous allons donc arriver à l’hypothèse suivante :

foo.bar

équivaut à :

foo.__dict__['bar']

Les méthodes spéciales __getattr__ et __getattribute__

Les appels d’accès aux attributs sont transformés en appels de méthodes spéciales, grâce à __getattribute__(), __setattr__() et __delattr__().

En fait, ces trois méthodes existent dans tous les objets normaux, hérités via la classe d’objet (et les classes l’héritent de la métaclasse type). Comme vous pouvez l’imaginer, ces méthodes sont appelées lorsqu’un attribut d’un objet est récupéré, défini ou supprimé, respectivement. Par conséquent, c’est ces méthodes qui décident quel descripteur utiliser et s’il faut retourner ou définir quelque chose dans la classe ou dans l’instance.

À présent, essayons d’accéder à nos attributs d’une manière dynamique ou disons Pythonique :

class Foo:
     def __init__(self):
        self.bar = 'bonjour!'

     def __getattr__(self, item):
         return 'bonsoir!'

foo = Foo()
print(foo.bar)
# baz est un attribut inexistant
print(foo.baz)
# le dictionnaire de l'instance
print(foo.__dict__)
bonjour!
bonsoir!
{'bar': 'bonjour!'}

Mais, d’ou vient la valeur de 'baz' ? Et puis, pourquoi nous n’avons pas un élément –
'baz' : 'bonsoir!' dans le dictionnaire.

En fait, ce mécanisme est très complexe, et il existe un fonctionnement spécifique pour chaque situation lors de l’accès aux attributs. Toutefois, il existe une méthode spéciale qui est appelée chaque fois que nous accédons aux attributs d’instance, mais ce n’est pas __getattr__, comme nous pouvons le voir dans l’exemple ci-dessus. En fait, il s’agit de la méthode __getattribute__.

Comment fonctionne la méthode __getattribute__() ?

Nous constatons qu’il existe réellement une méthode __getattr__(), mais elle n’est pas utilisée de la même manière que les autres. En réalité, __getattribute__() gère toute la logique de la recherche d’attribut normale tandis que __getattr__() est appelé par __getattribute__() en dernier lieu si rien n’est trouvé.

Comme dans l’exemple précédent, nous n’avons pas d’attribut 'baz', mais une valeur lui a été attribuée.
Il est recommandé par Python de ne pas apporter de modifications à __getattribute__() sauf dans des circonstances extrêmes, et uniquement si vous savez vraiment ce que vous faites.

Nous allons observer les différents comportements de la méthode __getattribute__().
modifions notre hypothèse précédente :

foo.bar

équivaut à appeler :

foo.__getattribute__('bar')

Et qui est à peu près :

def __getattribute__(self, item):
  if item in self.__dict__:
    return self.__dict__[item]
  return self.__getattr__(item)

Et si nous testions cela en implémentant réellement cette méthode (une méthode normale avec un nom différent) et en l’appelant directement :

class Foo:
     def __init__(self):
        self.bar = 'bonjour!'

     def __getattr__(self, item):
         return 'bonsoir!'

     def mon_getattribute(self, item):
         if item in self.__dict__:
             return self.__dict__[item]
         return self.__getattr__(item)


foo = Foo()
print(foo.bar)
print(foo.baz)
print("*******************")
print(foo.mon_getattribute('bar'))
print(foo.mon_getattribute('baz'))

L’exécution du code :

bonjour!
bonsoir!
*******************
bonjour!
bonsoir!

Donc, lorsque l’attribut baz n’a pas été trouvé, la méthode __getattr__() a été appelée. De plus le même résultat a été réalisé avec la méthode qu’on a créé mon_getattribute().

Comment modifier un attribut avec une méthode ?

Maintenant, essayons de modifier l’attribut baz.

foo.baz = 17
print(foo.baz)
17

Mais, est il possible de modifier l’attribut baz avec la méthode mon_getattribute().

foo.mon_getattribute('baz') = 'oops'
print(foo.mon_getattribute('baz'))
SyntaxError: cannot assign to function call

En réalité, mon_getattribute() renvoie quelque chose qui ressemble à une référence. Nous pouvons la modifier, mais nous ne pouvons pas réaffecter la valeur d’origine de la fonction à un nouvel objet. Alors comment pouvons-nous lui attribuer une nouvelle valeur ?

Dans une déclaration comme foo.bar = 1, il se passe quelque chose de plus. Et il semble que nous n’accédons tout simplement pas aux attributs de la même manière que lorsque nous les définissons.
Cependant, nous pouvons également surcharger la méthode __setattr__ :

class Foo:
    def __init__(self):
        self.__dict__['mon_dict'] = {}
        self.bar = 'bonjour!'

    def __setattr__(self, item, value):
        self.mon_dict[item] = value

    def __getattr__(self, item):
        return self.mon_dict[item]

Afficher l’attribut bar.

foo = Foo()
print(foo.bar)
bonjour!

Modifier l’attribut bar.

foo.bar = 'bonsoir!'
bonsoir!

Voyons avec l’attribut baz :

print(foo.baz)
KeyError: 'baz'

Parce que baz n’existe pas dans les espaces de noms de l’instance foo et de la classe Foo.

print("Dictionnaire de l‘instance : " , foo.__dict__)
print("Dictionnaire de la classe : ", Foo.__dict__)
Dictionnaire de l‘instance : {'bar': 'bonjour!'}

Dictionnaire de la classe : {'__module__': '__main__', '__init__': <function Foo.__init__ at 0x7f205ead4ee0>, '__getattr__': <function Foo.__getattr__ at 0x7f205ead4f70>, 'mon_getattribute': <function Foo.mon_getattribute at 0x7f205eae6040>, '__dict__': <attribute '__dict__' of 'Foo' objects>, '__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__doc__': None}

Modifions l’attribut baz à présent.

foo.baz = 30
print(foo.baz)
30

Vérifions à nouveau le dictionnaire.

print(foo.__dict__)
{'mon_dict': {'bar': 'bonsoir!', 'baz': 30}}

Donc, contrairement à la méthode __getattr_() qui vient avec la méthode __getattribute__(), la méthode __setattr__() ne possède pas de méthode __setattribute__().

Aussi, __setattr__() fonctionne avec __init__(), c’est pourquoi nous avons procédé à cette affectation étrange :
self.__dict__['mon_dict'] = {}. Sinon, nous aurons une récursivité infinie.

Utiliser le décorateur @property

Les propriétés sont des décorateurs qui permettent d’accéder aux méthodes en tant qu’attributs. Voici un tutoriel sur @property.

Essayons de comprendre comment cela se passe.

class Foo(object):
     def __getattribute__(self, item):
         print('__getattribute__ est appelée')
         return super().__getattribute__(item)

     def __getattr__(self, item):
         print('__getattr__ est appelée')
         return super().__getattr__(item)

     @property
     def bar(self):
          print('La propriété bar est appelée')
          return 100
f = Foo()
f.bar

La méthode bar() se comporte comme un attribut bar.

__getattribute__ est appelée
La propriété bar est appelée

Voyons qu’est ce qu’il y a dans f.__dict__ :

f.__dict__
__getattribute__ est appelée
{}

Donc, bar n’est pas dans __dict__ de l’instance, mais notez également que la méthode __getattr__ n’est pas appelée.

En fait, bar est une méthode et elle accepte l’instance de classe, mais c’est un membre de l’objet de classe, pas de l’instance. Vérifions :

Foo.__dict__
{'__module__': '__main__', '__getattribute__': <function Foo.__getattribute__ at 0x7f96418b5ee0>, '__getattr__': <function Foo.__getattr__ at 0x7f96418b5f70>, 'bar': <property object at 0x7f96418b64a0>, '__dict__': <attribute '__dict__' of 'Foo' objects>, '__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__doc__': None}

bar est un élément de ce dictionnaire. Afin de reconstruire, __getattribute__ nous devons répondre à une autre question ici – qui a la priorité, l’instance ou la classe ?

f.__dict__['bar'] = 'Est ce que ce méssage sera affiché ?'
__getattribute__ est appelée

Afficher l’attribut bar.

print(f.bar)
__getattribute__ est appelée
La propriété bar est appelée
100

Bien. On sait maintenant que __dict__ de la classe est également vérifié et qu’il a la priorité. Donc, le comportement par défaut de __getattribute__() est de rechercher d’abord l’attribut dans le dictionnaire de la classe.

Nous savons à présent que __getattribute__() gère toute la logique de recherche d’attribut, tandis que __getattr__() est appelée par __getattribute__() dans un dernier lieu, si tout le reste échoue.

Attendez, quand nous avons appelé la méthode bar, notre pseudo-code pour __getattribute__() n’a pas appelé l’objet, alors que s’est il passé ?

En réalité, lorsque vous avez un protocole de descripteur, on peut remplacer le comportement par défaut de la recherche d’un attribut. En fait, le décorateur @property est un descripteur.

Et la manière dont nous l’avons implémenté, définie un descripteur de données en lecture seule, qui est ensuite appelé dans __getattribute__().

Notre hypothèse du début devient :

foo.bar

En tant que getter, équivaut à appeler :

foo.__getattribute__('bar')

Qui est à peu près :

def __getattribute__(self, item):
  if item in self.__class__.__dict__:
    v = self.__class__.__dict__[item]
  elif item in self.__dict__:
    v = self.__dict__[item]
  else:
    v = self.__getattr__(item)
  if hasattr(v, '__get__'):
    v = v.__get__(self, type(self))
  return v

Essayons de récapituler tous les comportements que nous avons vu :

class Foo:
    class_attr = "Je suis un attribut de classe!"

    def __init__(self):
        self.dict_attr = "Je suis dans le dictionnaire!"

    @property
    def property_attr(self):
        return "Je suis une propriété en lecture seulement !"

    def __getattr__(self, item):
        return "Je suis renvoyé dynamiquement!"

    def my_getattribute(self, item):
        if item in self.__class__.__dict__:
            print('Renvoyé à partir de self.__class__.__dict__')
            v = self.__class__.__dict__[item]
        elif item in self.__dict__:
            print('Renvoyé à partir de self.__dict__')
            v = self.__dict__[item]
        else:
            print('Renvoyé à partir de self.__getattr__')
            v = self.__getattr__(item)
        if hasattr(v, '__get__'):
            print("Appeler le descripteur __get__")
            v = v.__get__(self, type(self))
        return v
foo = Foo()

print(foo.class_attr)
print(foo.dict_attr)
print(foo.property_attr)
print(foo.dynamic_attr)

print('=========================================\n')

print(foo.my_getattribute('class_attr'))
print()
print(foo.my_getattribute('dict_attr'))
print()
print(foo.my_getattribute('property_attr'))
print()
print(foo.my_getattribute('dynamic_attr'))

L’exécution du code :

Je suis un attribut de classe!
Je suis dans le dictionnaire!
Je suis une propriété en lecture seulement !
Je suis renvoyé dynamiquement!
=========================================

Renvoyé à partir de self.__class__.__dict__
Je suis un attribut de classe!

Renvoyé à partir de self.__dict__
Je suis dans le dictionnaire!

Renvoyé à partir de self.__class__.__dict__
Appeler le descripteur __get__
Je suis une propriété en lecture seulement !

Renvoyé à partir de self.__getattr__
Je suis renvoyé dynamiquement!

Un petit rappel sur les classes type et object

Avant de continuer, nous allons voir un autre concept.

x = "bonjour"
print(type(x))
print(type(type(x)))
<class 'str'>
<class 'type'>

Donc, une classe définie par l’utilisateur est une instance de la classe type. Ainsi, nous pouvons constater que les classes en Python sont créées à partir de la classe type. De plus, le type des classes intégrées que vous connaissez est type.
Même le type de la classe type est type.

Voyons un autre exemple :

class X:
    pass

print(X.__class__)
print(X.__class__.__base__)
<class 'type'>
<class 'object'>

Une classe est un objet de la classe nommée type et la classe de base de notre classe est la classe nommée object. En réalité, chaque définition de classe Python a une superclasse implicite : object.

Maintenant que vous avez compris le fonctionnement des attributs, passons à l’étape suivante.

Le protocole de descripteur en Python

Dans d’autres langages de programmation, les descripteurs sont appelés setter et getter, où les fonctions publiques sont utilisées pour obtenir et définir une variable privée. Cependant, Python n’a pas de concept de variables privées, et le protocole de descripteurs peut être considéré comme un moyen pythonique pour réaliser quelque chose de similaire.

Pour créer un descripteur, nous avons besoin d’un protocole de descripteur. En définissant des méthodes spéciales, nous pouvons gérer efficacement les attributs.

__get__(self, object, type=None) -> objet
__set__(self, object, value) -> None
__delete__(self, object) -> None
__set_name__(self, owner, name)

self – l’instance du descripteur lui même.
object – l’instance de l’objet auquel votre descripteur est lié.
type – le type de l’objet auquel le descripteur est lié.
value – la valeur affectée à l’attribut du descripteur.

__get__ : récupère la valeur d’un objet et la renvoie ou déclenche une exception AttributeError s’il n’est pas trouvé.
__set__ : définit une valeur pour l’objet et ne renvoie rien. Mais peut lever une exception AttributeError.
__delete__ : supprime la valeur de l’objet et ne renvoie rien.
__set_name__ : est appelée une fois lors de la création de l’objet de classe (Python 3.6+). Cela fournit des fonctionnalités qui n’étaient auparavant possibles que via des métaclasses.

Si le descripteur implémente uniquement __get__(), on dit qu’il s’agit d’un descripteur de non-données. Par contre, s’il implémente __set__() ou __delete__(), on aura un descripteur de données.

Priorité des descripteurs Python

Chaque classe possède ce qu’on appelle une method resolution order ou MRO, qui détermine l’ordre des classes à interroger.

La simple recherche d’attributs est la plus complexe des trois utilisations des attributs car il existe plusieurs endroits pour rechercher des attributs : dans l’instance et dans la classe.
De plus, s’il s’agit d’un descripteur de la classe, vous avez deux comportements différents pour les descripteurs de données et de non-données.
__getattribute__() passe par un ordre de priorité qui définit où rechercher les attributs et comment les gérer. Cette priorité est la principale différence entre les descripteurs de données et les descripteurs de non-données.
– Descripteurs de données
– Attributs d’instance
– Descripteurs de non-données et attributs de classe
– La méthode __getattr__() peut être appelée séparément à partir de __getattribute__().

Les descripteurs ne sont pas tous créés égaux. Ils suivent une priorité de descripteur. Ainsi, lorsque vous essayez d’accéder à un attribut en Python, il passe par un processus de recherche. Les descripteurs de données ont la priorité dans la chaîne de recherche python. Voici comment Python cherche un attribut :

  • Tout d’abord, vous obtiendrez le résultat renvoyé par la méthode __get__ du descripteur de données.
  • Si rien n’est trouvé, la recherche se fera dans __dict__ de votre objet.
  • Sinon, vous obtiendrez le résultat renvoyé par la méthode __get__ du descripteur de non-données.
  • Si rien n’est trouvé, vous obtiendrez la valeur correspondante dans __dict__ de votre type(objet).
  • Sinon, la recherche se fera dans __dict__ de votre type(objet) parent en cas d’héritage.
  • En cas d’échec, l’étape précédente est répétée pour tous les types du parent dans l’ordre de résolution des méthodes MRO de votre objet.
  • Si tout échoue, vous obtiendrez une exception AttributeError.

Donc, il est important de savoir si un descripteur est un descripteur de données ou un descripteur de non-données.

Dans la méthode __set__(), vous n’avez pas la variable type, car vous ne pouvez appeler __set__() que sur l’objet. En revanche, vous pouvez appeler __get()__ à la fois sur l’objet et sur la classe.

Comment créer des descripteurs en Python ?

Si vous implémentez les méthodes __get__(), __set__() ou __delete__() vous avez un protocole de descripteur. C’est exactement ce que fait le décorateur @property.

De plus, nous allons créer une autre classe contenant l’attribut qui sera une instance du descripteur.

Chaque accès, modification ou suppression d’un l’attribut d’une instance déclenchera un appel à la méthode appropriée du descripteur.

Une autre chose à noter, les descripteurs Python ne sont instanciés qu’une seule fois par classe. Cela signifie que chaque instance d’une classe contenant un descripteur partage cette instance de descripteur.
C’est quelque chose auquel vous ne vous attendez peut-être pas et qui peut conduire à un problème.

# créer le descripteur
class Portion():
    def __init__(self):
        self.value = "Pomme"
    def __get__(self, obj, type=None):
        return self.value
    def __set__(self, obj, value):
        self.value = value

class Fruit():
    # l'attribut fruit est une instance du descripteur
    fruit = Portion() 

fruit_1 = Fruit()
fruit_2 = Fruit()

print(fruit_1.fruit)
print(fruit_2.fruit)

print("<---assigner une nouvelle valeur 'Banane' à fruit_1.fruit----->")
fruit_1.fruit = 'Banane'

print(fruit_1.fruit)
print(fruit_2.fruit)

fruit_3 = Fruit()
print(fruit_3.fruit)
Pomme
Pomme
<---assigner une nouvelle valeur 'Banane' à fruit_1.fruit----->
Banane
Banane
Banane

Ici, vous avez une classe Fruit qui définit un attribut fruit, qui est un descripteur. Ce descripteur accepte une valeur chaîne de caractère « Pomme » et la stocke dans une propriété du descripteur lui-même. Cependant, cette approche ne fonctionnera pas, car chaque instance de la classe Fruit partage la même instance de descripteur. Ce que nous avons créé n’est qu’un nouvel attribut au niveau de la classe.

En exécutant le code vous allez vous retrouver avec des instances de la classe Fruit qui ont la même valeur pour l’attribut fruit. De plus, toute modification à partir de l’instance sera appliquée à chaque instance de la classe.

L’utilisation d’un dictionnaire pour le stockage

L’une des solutions est d’utiliser un dictionnaire qui va enregistrer toutes les valeurs du descripteur de tous les objets auxquels il est attaché.

Malheureusement, cette solution présente un gros inconvénient, voyons :

class Portion():
    def __init__(self):
        self.data = {}
    def __get__(self, obj, type=None):
        return self.data[obj]
    def __set__(self, obj, value):
        self.data[obj] = value

class Fruit:
    fruit = Portion()

fruit_1 = Fruit()
fruit_2 = Fruit()

fruit_1.fruit = 'Pomme'
print(fruit_1.fruit)

fruit_2.fruit = 'Fraise'
print(fruit_2.fruit)

print("<---assigner une nouvelle valeur 'Banane' à fruit_1.fruit----->")
fruit_1.fruit = 'Banane'
print(fruit_1.fruit)
Pomme
Fraise
<---assigner une nouvelle valeur 'Banane' à fruit_1.fruit----->
Banane

Le problème ici, est que le descripteur a une forte référence à l’objet propriétaire. Cela signifie que si vous détruisez l’objet, la mémoire ne sera pas libérée.

L’utilisation du module WeakKeyDictionary

Le module WeakKeyDictionary conserve une référence faible avec la clé et, par conséquent, une fois que l’instance utilisée comme clé n’est plus utilisée, le dictionnaire nettoie toute l’entrée.

from weakref import WeakKeyDictionary
class Portion():
    def __init__(self):
        self.data = WeakKeyDictionary()
    def __get__(self, obj, type=None):
        return self.data[obj]
    def __set__(self, obj, value):
        self.data[obj] = value

class Fruit:
    fruit = Portion()

fruit_1 = Fruit()
fruit_2 = Fruit()

fruit_1.fruit = 'Pomme'
print(fruit_1.fruit)

fruit_2.fruit = 'Fraise'
print(fruit_2.fruit)

print("<---assigner une nouvelle valeur 'Banane' à fruit_1.fruit----->")
fruit_1.fruit = 'Banane'
print(fruit_1.fruit)

Et vous aurez le même résultat, mais amélioré.
Bien que cela puisse résoudre le problème de la mémoire, vous devrez gérer le fait que tout ne peut pas être référencé comme faible et que, lorsque vos objets sont collectés, ils disparaissent de votre dictionnaire.

Cette solution fonctionne dans la plupart des situations, ce n’est donc pas une mauvaise idée.
Malheureusement, elle souffre toujours de l’autre inconvénient d’un dictionnaire standard : elle ne prend pas en charge les types non hashables, à savoir les collections mutables (list, set et dict), et peut-être quelques autres.

L’utilisation de l’objet attaché au descripteur pour le stockage

Une autre solution consiste à ne pas stocker les valeurs dans le descripteur lui-même, mais de les stocker dans l’objet auquel le descripteur est attaché.

class Portion():
    def __init__(self, name):
        self.name = name
    def __get__(self, obj, type=None):
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        obj.__dict__[self.name] = value


class Fruit:
    fruit = Portion('fruit')

fruit_1 = Fruit()
fruit_2 = Fruit()

fruit_1.fruit = 'Pomme'
print(fruit_1.fruit)

fruit_2.fruit = 'Fraise'
print(fruit_2.fruit)

print("<---assigner une nouvelle valeur 'Banane' à fruit_1.fruit----->")
fruit_1.fruit = 'Banane'
print(fruit_1.fruit)

En fait, lorsque vous définissez une valeur pour l’attribut fruit de votre objet, le descripteur la stocke dans le __dict__ de l’attribut de l’objet auquel il est attaché en utilisant le même nom que le descripteur lui-même.

Le seul problème ici est que lorsque vous instanciez le descripteur, vous devez spécifier le nom en tant que paramètre.

La méthode set_name() dans les descripteurs

Dans Python 3.6, quelque chose d’autre a été ajouté et qui rend la récupération du nom encore plus facile. Python a gagné une méthode optionnelle supplémentaire dans son protocole :
__set_name__(). Cette nouvelle méthode est appelée lors de la création d’une classe contenant un objet descripteur. Ses paramètres sont self, owner et name. De plus, le paramètre name est automatiquement défini.
Avec Python 3.6 et plus, le code précédent deviendra :

class Portion():
    def __set_name__(self, owner, name):
        self.name = name
    def __get__(self, obj, type=None):
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        obj.__dict__[self.name] = value


class Fruit:
    fruit = Portion()

fruit_1 = Fruit()
fruit_2 = Fruit()

fruit_1.fruit = 'Pomme'
print(fruit_1.fruit)

fruit_2.fruit = 'Fraise'
print(fruit_2.fruit)

print("<---assigner une nouvelle valeur 'Banane' à fruit_1.fruit----->")
fruit_1.fruit = 'Banane'
print(fruit_1.fruit)

La méthode __set_name__() a remplacé __init__() et le paramètre que nous avons ajouté au descripteur lors de l’instantiation de l’objet a disparu.

Les descripteurs Python et l’accès aux attributs

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.

Retour en haut