Autour du codeDevelopper toujours mieux
Posté le

Les descripteurs

Un descripteur permet de définir des comportements lorsque l'on souhaite accèder à un attribut d'une classe.

Un descripteur peut définir trois méthodes:

  • __get__ qui sera appelé lorsque l'on accède à l'attribut d'un objet (obj.attr)
  • __set__ qui sera appelé lorsque l'on modifie l'attribut (obj.attr = truc) et
  • __delete__ qui sera appelé lorsque l'on veux supprimer un attribut via del

Voici un exemple de descripteur qui ne fait rien d'autre que d'afficher l'appel à ses méthodes __get__, __set__ et __delete__.

class MyDescriptor:

    def __get__(self, obj, cls=None):
        print("__get__", obj, type)

    def __set__(self, obj, value):
        print("__set__", obj, value)

    def __delete__(self, obj):
        print("__del__", obj)

Je crée ensuite une classe A dans laquelle je définis un attribut qui contiendra une instance de mon descripteur.

class A:
    my_attr = MyDescriptor()

Maintenant, tout accès, modification ou suppression de l'attribut my_attr d'une instance de A déclenchera un appel à la méthode appropriée du descripteur.

a = A()
a.my_attr # __get__ <__main__.a object="" at="" 0x7f912c0e9f50=""> <class __main__="" a="">
a.my_attr = 33 # __set__ <__main__.a object="" at="" 0x7f912c0e9f50=""> 33
del a.my_attr # __del__ <__main__.a object="" at="" 0x7f912c0e9f50=""><!--__main__.a--><!--__main__.a--></class><!--__main__.a-->

Un descripteur bien connu est property. Il vous demande quelle méthode vous souhaitez utiliser pour accéder, modifier ou supprimer un attribut. Si vous ne spécifier pas de methode pour une action, cette action ne sera pas autorisé.Voici l'exemple d'une classe où x n'est accessible qu'en lecture.

class ClassX:
    def __init__(self):
        self._x = 42 # Je crée un attribut privé.

    def get_x(self): # Une méthode pour accéder à mon attribut.
        return self._x

    x = property(get_x) # J'indique que get_x est la méthode
                        # pour accéder à x.

obj_x = ClassX()
print(obj_x.x) # 42
# obj_x.x = 33 # AttributeError: can't set attribute

Cela peut aussi s'utiliser en décorant la méthode.

class ClassX:
    def __init__(self):
        self._x = 42

    @property
    def x(self):
        return self._x

Passon aux choses sérieuses, on va écrire une classe Personne qui instanciera un objet à partir d'une ligne lu dans un fichier tabulé. Chaque ligne du fichier tabulé ce présente comme ceci:

prenom nom age

Chaque champ du fichier deviendra un attribut et on utilisera des property pour vérifier est transtyper les attributs.

class Personne:
    def __init__(self, line):
        self._fields = line.split('\t')

    @property
    def prenom(self):
        return self._fields[0]

    @prenom.setter
    def prenom(self, value):
        self._fields[0] = value

    @property
    def nom(self):
        return self._fields[1]

    @nom.setter
    def nom(self, value):
        self._fields[1] = value

    @property
    def age(self):
        return int(self._fields[2])

    @age.setter
    def age(self, value):
        self._fields[2] = str(value)

    def __str__(self):
        return "\t".join(self._fields)

Comme on peut le voir ça se fait, mais c'est barbant, rasoir, suant, assommant, soulant, et tout ce que vous voulez d'autre et il y avait seulement 3 champs ! On va donc utiliser notre propre descripteur.

class GetField:

    def __init__(self, num, type=str):
        """
        num - numero de champs a extraire.
        type - type du champs.
        """
        self._num = num
        self._type = type

    def __get__(self, obj, cls=None):
        return self._type(obj._fields[self._num])

    def __set__(self, obj, value):
        if isinstance(value, self._type):
            obj._fields[self._num] = str(value)
        else:
            raise TypeError("%s expected" % self._type)

Ça s'utilise comme ça. smile

class Personne:

    prenom = GetField(0)
    nom = GetField(1)
    age = GetField(2, int)

    def __init__(self, line):
        self._fields = line.split('\t')

    def __str__(self):
        return "\t".join(self._fields)

C'est quand même plus sympa, et GetField est réutilisable. Que demander de plus ? Un exemple d'utilisation de la classe Personne peut être ? wink

p = Personne("Gaston\tLagaffe\t18")
print(p.prenom) # Gaston
print(p.nom)    # Lagaffe
print(p.age)    # 18

p.prenom = "Pépé"
p.nom = "La Jactance"
p.age = 80
print(p) # "Pépé\tLa Jactance\t80"

Voila j'espère que ça vous a plus alors à vos IDE et bon code ! smile