Les descripteurs en Python 17


Un descripteur est une classe qu’on instancie comme attribut d’une autre classe pour faire office de setter et de getter sur cet attribut. Le descripteur doit implémenter les méthodes __get__ et __set__ qui seront exécutées quand on essaye d’assigner ou lire l’attribut. Respecter cette signature, c’est adopter ce qu’on appelle pompeusement le “descriptor protocol”.

Si le principe vous rappelle les propriétés, c’est normal, les propriétés sont implémentées en utilisant des descripteurs.

Exemple balot et complètement arbitraire :

class JeSuisUnDescripteurEtJeVousEmmerde(object):
 
    # les noms des attributs sont des conventions
    def __get__(self, obj, objtype):
        return obj, objtype
 
    def __set__(self, obj, value):
        print obj, value
 
 
class JeSuisUneClasseNormaleEtJeVousAime(object):
 
    ze_attribute = JeSuisUnDescripteurEtJeVousEmmerde()
 
 
>>> objet_affecteux = JeSuisUneClasseNormaleEtJeVousAime()
>>> print objet_affecteux.ze_attribute
<__main__.JeSuisUneClasseNormaleEtJeVousAime object at 0x1cb20d0> <class '__main__.JeSuisUneClasseNormaleEtJeVousAime'>
>>> objet_affecteux.ze_attribute = 'dtc '
<__main__.JeSuisUneClasseNormaleEtJeVousAime object at 0x1cedf10> dtc

Vous noterez que obj est donc toujours l’instance de l’objet qui possède l’attribut sur lequel on agit. Dans __get__, objtype est la classe de cet objet. Dans __set__, value est la nouvelle valeur qu’on assigne à l’objet.

Vous allez me dire: pourquoi utiliser les descriptors plutôt que les properties ?

D’abord, les descripteurs sont des unités de code indépendantes. Vous pouvez faire un module avec vos descripteurs, et les distribuer en tant que lib. Donc c’est réutilisable. Ensuite, vous n’êtes pas limités à votre méthode en cours, vous avez accès à tout l’arsenal de la programmation OO.

Par exemple, si vous pouvez faire un descriptor d’alerte, qui envoie un signal à tous les abonnés pour cette valeur:

class SignalDescriptor(object):
 
    abonnements = {}
 
    @classmethod
    def previens_moi(cls, obj, attr, callback):
        cls.abonnements.setdefault(obj, {}).setdefault(attr, set()).add(callback)
 
    def __init__(self, nom, valeur_initiale=None):
        self.nom = nom
        self.valeur = valeur_initiale
 
    def __get__(self, obj, objtype):
        for callback in self.abonnements.get(obj, {}).get(self.nom, ()):
            callback('get', obj, self.nom, self.valeur)
        return self.valeur
 
    def __set__(self, obj, valeur):
        for callback in self.abonnements.get(obj, {}).get(self.nom, ()):
            callback('set', obj, self.nom, self.valeur, valeur)
        self.valeur = valeur

Et voilà, vous pouvez distribuer ça sur Github, c’est plug and play.

Par exemple, pour créer un objet Joueur sur lequel on veut monitorer le nombre de crédits :

class Joueur(object):
 
    credits = SignalDescriptor("credits", 0)

On l’utilise normalement:

>>> j = Joueur()
>>> j.credits
0
>>> j.credits = 15
>>> j.credits
15
>>> j.credits += 5
>>> j.credits
20

Mais si on rajoute un abonné :

 
def monitorer_credits(action, obj, attribut, valeur_actuelle, nouvelle_valeur=None):
 
   if action == 'set':
       print "Les crédits ont changé:"
   else:
       print "Les crédits ont été consultés:"
   print action, obj, attribut, valeur_actuelle, nouvelle_valeur
 
>>> SignalDescriptor.previens_moi(j, 'credits', monitorer_credits)

Alors à chaque action sur les crédits, tous les abonnés sont appelés :

>>> j.credits
Les crédits ont été consultés:
get <__main__.Joueur object at 0x1f6b190> credits 20 None
20
>>> j.credits = -20
Les crédits ont changé:
set <__main__.Joueur object at 0x1f6b190> credits 20 -20
>>> j.credits -= 10 # get ET set
Les crédits ont été consultés:
get <__main__.Joueur object at 0x1f6b190> credits -20 None
Les crédits ont changé:
set <__main__.Joueur object at 0x1f6b190> credits -20 -30

On vient d’implémenter une version encapsulée du pattern observer dédié à un attribut. On peut faire de nombreuses choses avec les descripteurs: grouper des attributs, les transformer à la volée, les sauvegarder ailleurs (imaginez un objet de config qui sauvegarde automatiquement chaque modification de ses attributs dans un fichier…).

17 thoughts on “Les descripteurs en Python

  • JeromeJ

    “On vient d’implémenter une version encapsulée du pattern observer […]” I knew it!

    Merci pour l’idée d’implémentation :o Utile !

  • alexandre

    article intéressant, cela permet d’avoir un aperçu de certaine fonction avancé. Par contre la photo d’en-tête n’a pas forcément sa place sur un blog qui n’est pas interdit au moins de 18ans. Il serait bon de rester dans l’impertinence légale.

  • Réchèr

    C’est sympatoche.

    Juste une petite remarque, concernant l’exemple :
    Dans la fonction monitorer_credits, au lieu du print indiquant que “les crédits ont changé”, j’aurais distingué les deux cas.

    if action == 'get:
        print "Les crédits ont été consultés:"
    else:
        print "Les crédits ont changés:"

    Et ça clarifie les choses dans le code d’exemple qui se trouve juste après,

    Certes, les codes d’exemple doivent rester le plus simple possible. Mais c’est également important qu’ils soient explicites. Dire qu’une valeur a changé alors qu’elle n’a pas forcément changé, ce n’est pas très cavalier.

  • JM

    Grammar nazisme :
    > pouvez distribue[r]

    Typography nazisme :
    >>> re.compile(‘([^ ]):’).sub(r’\1 :’, …)

  • Recher

    Ah oui, “Bien vu“, c’est ce que m’avais dit Gilbert Montagné la semaine dernière.

    Cependant, il reste une dernière petite correction de code à faire.
    Dans le dernier bloc de code, il faut remplacer l’avant dernier “Les crédits ont changé:” par “Les crédits ont été consultés:”

    Quand on fait l’opération ” -= “, il y a d’abord une consultation, puis un changement.

    Et tant qu’à faire, il faudrait corriger le fameux S de “changés” dans les autres endroits de ce dernier bloc de code.

    “Sous le soleil des tropiiiiiques” (etc.)

  • leplatrem

    Un bon cas d’utilisation : les classproperty

    class classproperty(object):
        def __init__(self, getter):
            self.getter = getter
     
        def __get__(self, instance, owner):
            return self.getter(owner)

    Ensuite, par exemple :

    class MyClass(object):
        @classproperty
        def tagname(cls):
            return "data-%s" % cls.__name__
  • Etienne

    @Sam
    Ça fait quand même plusieurs heures que je m’escrime là-dessus et que je n’arrive pas à y voir tout à fait clair. A mes yeux myopes de néophyte frileux mais fringant, tout ça paraît fort emberlificoté. Faudrait une visite guidée je trouve… (je dis ça parce que je sais que ça t’intéresse, mais je demandes rien, hein)

  • Désanuseur

    Moi j’aime bien vos articles, mais je ne sais jamais au final à quoi cela sert, du coup je fonctionne sans ;)

  • Sam Post author

    Ah, je hais quand j’échoue à faire comprendre quelque chose. On va corriger ça !

    Dites moi tout. Quel point vous parait le plus flou ? Quelles questions vous vient à l’esprit. Quelle ligne exactement votre compilateur mental détecte une “ComphrensionError: no such concept my namespace” ?

  • enigma

    Je fait apparaître le sujet encore :)
    Et je reprends le premier exemple:

    dans la methode __get__:
    def __get__(self, obj, objtype):
    return obj, objtype

    il est clair que obj est l’instance de l’objet qui possède l’attribut sur lequel on agit, objtype c’est la classe de cet objet et suivant la doc officielle obj c’est instance et objtype c’est owner.

    ma question c’est self reprèsente quoi ?

  • Sam Post author

    Self représente l’instance de l’objet descripteur. Self est toujours l’objet en cours, du point de vue de la classe dans laquelle on est.

Leave a comment

Your email address will not be published. Required fields are marked *

Utilisez <pre lang='python'>VOTRE CODE</pre> pour insérer un block de code coloré

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.