accès aux attributs - second notebook
pour résumer les chapitres précédents, on a vu jusqu’ici:
la mécanique “usuelle”
les properties
l’attrape-tout avec
__getattr__
à présent on va voir encore un autre mécanisme, qui s’appelle les descriptors
en fait, il s’agit d’un mécanisme de très bas niveau, et il se trouve que c’est grâce à ce mécanisme que l’on peut proposer les properties
pourquoi c’est intéressant ?¶
la recherche des attributs est totalement centrale dans le langage
on va le voir, lorsqu’on écrit un code aussi banal que person = Person("jean"), il y a déjà plusieurs recherches d’attributs qui entrent en jeu !
de plus pour que le modèle fonctionne, on a dû implémenter deux mécanismes séparés pour les instances et les classes, qui sont voisines mais subtilement différentes; si on veut maitriser à fond le langage, on doit en passer par l’étude de ces mécanismes
mais bon à nouveau, si on s’en tient à une utilisation usuelle du langage, tout ceci est complètement optionnel !
descripteurs¶
un descripteur est une classe qui détermine le comportement lors de l’accès, l’affectation et l’effacement d’un attribut
une classe avec au moins une des méthodes suivantes est un descripteur
__get__()__set__()__delete__()
caractéristique troublante¶
la caractéristique assez troublante des descripteurs est la suivante:
si pendant la recherche “habituelle”
on trouve un attribut
et que celui-ci est une instance de descripteur
alors on l’appelle pour obtenir la valeur finale de l’attribut ou pour écrire/détruire selon le contexte
ça nous rappelle un peu les propriétés (et de fait les propriétés sont implémentées à base de descripteurs...)
exemple: un attribut d’instance¶
# a helper tool to turn verbosity on or off
VERBOSE = True
def verbose(*args, **kwds):
if VERBOSE:
print(*args, **kwds)v0: un peu poussif¶
# ici on implémente un attribut usuel (d'instance)
# il faut être attentif à bien ranger la donnée dans l'instance
# et pas dans le descripteur !!!
# une classe de descripteur pour implémenter l'attribut 'age'
class AgeDescriptor:
def __init__(self):
# ici self est l'instance du descripteur
# cet espace va être partagé par toutes les instances de la classe
# ce n'est **pas une bonne idée** d'y ranger
# le nom des personnes
pass
def __get__(self, instance, owner):
# self: l'instance du descripteur (de type Descriptor donc)
# instance: l'instance de type Person
# owner: ici ça va être None (pourrait recevoir
# la classe Person dans d'autres use cases)
# du coup bien faire attention à ranger dans `instance` et pas dans `self` !
current_age = instance._age
verbose(f"getter returning {current_age=}\n -- {self=} {instance=}")
return current_age
def __set__(self, instance, new_age):
# attention à ne pas utiliser instance.age
# car on cacherait le descripteur !
verbose(f"setting: age={new_age}\n -- with {self=} {instance=}")
instance._age = new_age
def __delete__(self, instance):
verbose(f"deleting: {instance._age}\n -- from {instance=}")
del instance._age
class Person:
# on range une instance de Descriptor dans Person.__dict__['name']
age = AgeDescriptor()
# fait une lecture et une écriture
def birthday(self):
self.age += 1# avec ce code, c'est comme si on avait ajouté
# un attribut 'name' à toutes les instances de Person
p1, p2 = Person(), Person()
p1.age = 12
p2.age = 20setting: age=12
-- with self=<__main__.AgeDescriptor object at 0x7fc31720fb90> instance=<__main__.Person object at 0x7fc30ccc55e0>
setting: age=20
-- with self=<__main__.AgeDescriptor object at 0x7fc31720fb90> instance=<__main__.Person object at 0x7fc30cce1670>
# sont bien différents
p1.age, p2.agegetter returning current_age=12
-- self=<__main__.AgeDescriptor object at 0x7fc31720fb90> instance=<__main__.Person object at 0x7fc30ccc55e0>
getter returning current_age=20
-- self=<__main__.AgeDescriptor object at 0x7fc31720fb90> instance=<__main__.Person object at 0x7fc30cce1670>
(12, 20)p1.birthday()getter returning current_age=12
-- self=<__main__.AgeDescriptor object at 0x7fc31720fb90> instance=<__main__.Person object at 0x7fc30ccc55e0>
setting: age=13
-- with self=<__main__.AgeDescriptor object at 0x7fc31720fb90> instance=<__main__.Person object at 0x7fc30ccc55e0>
v1: mieux avec __set_name__¶
notre descripteur est spécialisé pour l’attribut name, c’est franchement sous-optimal, on ne va pas récrire le même code à chaque fois
voici comment améliorer, en tirant profit de la dunder __set_name__ pour capturer le nom de l’attribut
# de nouveau un ici on implémente un attribut usuel (d'instance)
# mais qui peut marcher pour n'importe quel nom
class InstanceAttributeDescriptor:
def __set_name__(self, owner, name):
# par exemple 'age'
self.public_name = name
self.private_name = '_' + name
def __get__(self, instance, owner):
current_value = getattr(instance, self.private_name)
verbose(f"getter: {self.public_name} -> {current_value}")
return current_value
def __set__(self, instance, new_value):
verbose(f"setter: {self.public_name} -> {new_value}")
setattr(instance, self.private_name, new_value)
class Person:
age = InstanceAttributeDescriptor()
name = InstanceAttributeDescriptor()
def __init__(self, name, age):
self.name = name
self.age = age
def birthday(self):
self.age += 1# avec ce code, c'est comme si on avait ajouté
# un attribut 'name' à toutes les instances de Person
p1, p2 = Person("john", 12), Person("bill", 20)setter: name -> john
setter: age -> 12
setter: name -> bill
setter: age -> 20
# sont bien différents
p1.age, p2.agegetter: age -> 12
getter: age -> 20
(12, 20)p1.birthday()getter: age -> 12
setter: age -> 13
VERBOSETrueun peu d’introspection¶
# l'instance de descripteur est ici
vars(Person)['age']<__main__.InstanceAttributeDescriptor at 0x7fc30ccc5d00># et ses deux attributs sont
vars(vars(Person)['age']){'public_name': 'age', 'private_name': '_age'}# et dans un objet Person on trouve ceci
vars(p1){'_name': 'john', '_age': 13}stockage des attributs¶
le protocole nous expose à la fois les deux instances:
le descripteur
l’instance sujet de la recherche d’attribut
et comme il y a une instance de descripteur par attribut de la classe, on peut choisir de ranger les données
au niveau de l’instance - comme on vient de le faire;
où au niveau de la classe si on écrit dans le descripteur
voyons cette deuxième alternative, pour implémenter un attribut de classe
exemple: un attribut de classe¶
et ceci avec un tout petit changement:
# ici on implémente un attribut de classe
# pour ça on va ranger la valeur .. directement dans le (l'instance du) descripteur
# c-a-d dans self plutot que dans instance
class ClassAttributeDescriptor:
def __set_name__(self, owner, name):
self.public_name = name
# on pourrait aussi ranger un _private_name
# mais ce n'est pas nécessaire, car ici on veut un attribut
# de classe, donc on peut ranger la donnée dans le descripteur
# et du coup le nom n'a aucun importance, on va mettre en dur '_value'
def __get__(self, instance, owner):
# pas besoin de s'embeter à prendre un nom compliqué
# il n'y a pas de risque de conflit de nom cette fois
# le _ c'est juste par sécurité mais ça n'a pas d'importance ici
return self._value
def __set__(self, instance, newvalue):
self._value = newvalue
class Person:
# un attribut 'normal'
name = InstanceAttributeDescriptor()
# un attribut de classe
# ici du coup toutes les instances partagent l'attribut
shared = ClassAttributeDescriptor()VERBOSE = False
p1, p2 = Person(), Person()
# chacun son nom
p1.name, p2.name = "john", "doe"
# la première affectation est écrasée par la seconde
p1.shared, p2.shared = "useless", "because overwritten"
# la preuve
print("========== name:", p1.name, "<->", p2.name)
print("========== shared:", p1.shared, "<->", p2.shared)========== name: john <-> doe
========== shared: because overwritten <-> because overwritten
attribut en lecture seule¶
attention aux noms¶
comme avec les propriétés, il faut soigneusement éviter d’utiliser le même nom pour le descripteur et pour ranger la donnée dans l’instance (dans le descripteur on s’en moque)
la tradition est d’utiliser
namepour le descripteur_namepour la donnée
exemples pratiques¶
voyez quelques exemples utiles de validateurs ici:
https://
data descriptors¶
maintenant, il se trouve que la logique complète de recherche des attributs, qui est implémentée dans __getattribute__ et qu’on verra pour conclure dans le 3ème et dernier notebook de cette série, va avoir besoin de traiter un peu différemment les attributs de donnée et les attributs de fonction
pour cette raison, on a choisi à ce stade la définition suivante:
un data descriptor est prioritaire sur un attribut présent dans __dict__
ce qui n’est pas le cas pour les fonctions
class DataDescriptor:
def __get__(self, instance, owner):
value = instance._name
print(f"getting: {value}")
return value
# en fournissant __set__
# on devient un *data* descriptor
def __set__(self, instance, name):
value = name.title()
print(f"setting: {value}")
instance._name = value
class PersonData:
def __init__(self):
self._name = None
name = DataDescriptor()pd = PersonData()
pd.namegetting: None
pd.name = 'john'
pd.namesetting: John
getting: John
'John'class DescriptorNonData:
# sans __set__ on parle
# de *non-data* descriptor
def __get__(self, instance, owner):
value = instance._name
print("Getting: {}".format(value))
return value
class PersonNonData:
def __init__(self):
self._name = 'John Doe'
name = DescriptorNonData()pnd = PersonNonData()
pnd.name = 'bill'
pnd.name'bill'del pnd.name
pnd.nameGetting: John Doe
'John Doe'pour en savoir plus¶
la doc sur les descriptors excellente intro de Raymond Hettinger, qui parle aussi des properties