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…).
“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 !
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.
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.
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.
Bien vu !
D’ailleurs, ils ont changé
s;-pGrammar nazisme :
> pouvez distribue[r]
Typography nazisme :
>>> re.compile(‘([^ ]):’).sub(r’\1 :’, …)
Corrigé ! J’ai même mis des espaces insécables rien que pour toi…
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.)
Bon, on pourra pas se plaindre qu’on a pas un lectorat attentif.
Un bon cas d’utilisation : les classproperty
Ensuite, par exemple :
Malin le coup de classproperty !
@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)
Moi j’aime bien vos articles, mais je ne sais jamais au final à quoi cela sert, du coup je fonctionne sans ;)
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” ?
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 ?
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.
Merci max
Donc self represente l’attribut lui même (ze_attribute)