Sam & Max » descriptors http://sametmax.com Du code, du cul Sat, 07 Nov 2015 10:56:13 +0000 en-US hourly 1 http://wordpress.org/?v=4.1 Les descripteurs en Python 17 http://sametmax.com/les-descripteurs-en-python/ http://sametmax.com/les-descripteurs-en-python/#comments Sat, 24 Nov 2012 13:11:56 +0000 http://sametmax.com/?p=3276 __get__ et __set__ qui seront exécutées quand on essayer d'assigner ou lire l'attribut. ]]> 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…).

]]>
http://sametmax.com/les-descripteurs-en-python/feed/ 17