Accesseurs et descripteurs

Propriétés

Pré-requis : Décorateurs, Attributs, Descripteurs

Durant le chapitre sur les descripteurs, nous avons utilisé my_property, une copie minimaliste de la classe property. Je vous propose ici d’en terminer l’implémentation, afin de rendre ces deux classes similaires.

Pour rappel, property est une classe de descripteurs, utilisable en tant que décorateur sur les méthodes à transformer en propriétés. Les propriétés étant des descripteurs faisant appel à des fonctions particulières pour l’accès en lecture/écriture ou la suppression de l’attribut. En plus des méthodes __get__, __set__ et __delete__ des descripteurs, property possède aussi des méthodes/décorateurs getter, setter et deleter pour redéfinir ces fonctions.

Notre implémentation utilisée dans le chapitre sur les accesseurs était la suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class my_property:
    def __init__(self, fget, fset, fdel):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel

    def __get__(self, instance, owner):
        return self.fget(instance)

    def __set__(self, instance, value):
        return self.fset(instance, value)

    def __delete__(self, instance):
        return self.fdel(instance)

Nous implémentons bien les 3 méthodes propres aux descripteurs, mais il faut pour chaque propriété que ces 3 fonctions soient définies, et notre classe n’est pas utilisable comme décorateur. Il manque aussi la redéfinition des fonctions.

Un autre point qui n’a pas été abordé est celui de la documentation. En plus des fget, fset et fdel, la propriété peut-être initialisée avec un paramètre doc, qui sera la documentation de l’attribut. En l’absence de doc, l’attribut prendra pour documentation celle de la fonction fget, si existante.

Ces 4 paramètres sont tous facultatifs, nous les initialiserons alors à None. Nous pouvons alors écrire le nouvel initialisateur de notre classe my_property.

1
2
3
4
5
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        if doc is None and fget is not None:
            doc = getattr(fget, '__doc__', None)
        self.fget, self.fset, self.fdel = fget, fset, fdel
        self.__doc__ = doc

On récupère ainsi la documentation de fget si aucune documentation n’a été fournie à la construction (doc is None), et si fget est présente (fget is not None). En utilisant getattr, on permet à fget de ne pas avoir de documentation, auquel cas doc vaudra toujours None.

Les méthodes __get__, __set__ et __delete__ seront un peu plus complexes que précédemment. Il nous faudra maintenant tester la présence des fonctions cibles (fget, fset et fdel), et lever une exception AttributeError le cas échéant. Cette exception indiquera que l’attribut n’est pas lisible, redéfinissable, ou supprimable.

Nous ferons aussi en sorte que __get__ appelée sans instance (instance valant None) retourne la propriété elle-même.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    def __get__(self, instance, owner):
        'Return an attribute of instance, which is of type owner.'
        if instance is None:
            return self
        if self.fget is None:
            raise AttributeError('unreadable attribute')
        return self.fget(instance)

    def __set__(self, instance, value):
        'Set an attribute of instance to value.'
        if self.fset is None:
            raise AttributeError("can't set attribute")
        return self.fset(instance, value)

    def __delete__(self, instance):
        'Delete an attribute of instance.'
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        return self.fdel(instance)

Enfin, les méthodes getter, setter et deleter copieront celles de property. Plutôt que de modifier la proprité avec la fonction reçue en paramètre, elles retourneront une nouvelle propriété. Aucun changement ne sera effectué si le paramètre vaut None.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    def getter(self, fget):
        'Descriptor to change the getter on a property.'
        if fget is None:
            return self
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        'Descriptor to change the setter on a property.'
        if fset is None:
            return self
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        'Descriptor to change the deleter on a property.'
        if fdel is None:
            return self
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

Où nous utilisons type(self)(...) afin de faire appel au constructeur de notre propriété. Ce qui restera valable avec une nouvelle classe qui hériterait de my_property.

Une fois toutes ces méthodes réunies dans notre classe my_property, à laquelle on ajouterait encore une petite dose de documentation, on retrouve un équivalent complet de property.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class my_property:
    '''
    my_property(fget=None, fset=None, fdel=None, doc=None) -> property attribute

    fget is a function to be used for getting an attribute value, and likewise
    fset is a function for setting, and fdel a function for del'ing, an
    attribute.  Typical use is to define a managed attribute x:

    class C(object):
        def getx(self): return self._x
        def setx(self, value): self._x = value
        def delx(self): del self._x
        x = my_property(getx, setx, delx, "I'm the 'x' property.")

    Decorators make defining new properties or modifying existing ones easy:

    class C(object):
        @my_property
        def x(self):
            "I am the 'x' property."
            return self._x
        @x.setter
        def x(self, value):
            self._x = value
        @x.deleter
        def x(self):
            del self._x
    '''

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        if doc is None and fget is not None:
            doc = getattr(fget, '__doc__', None)
        self.fget, self.fset, self.fdel = fget, fset, fdel
        self.__doc__ = doc

    def __get__(self, instance, owner):
        'Return an attribute of instance, which is of type owner.'
        if instance is None:
            return self
        if self.fget is None:
            raise AttributeError('unreadable attribute')
        return self.fget(instance)

    def __set__(self, instance, value):
        'Set an attribute of instance to value.'
        if self.fset is None:
            raise AttributeError("can't set attribute")
        return self.fset(instance, value)

    def __delete__(self, instance):
        'Delete an attribute of instance.'
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        return self.fdel(instance)

    def getter(self, fget):
        'Descriptor to change the getter on a property.'
        if fget is None:
            return self
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        'Descriptor to change the setter on a property.'
        if fset is None:
            return self
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        'Descriptor to change the deleter on a property.'
        if fdel is None:
            return self
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

Que nous pouvons alors utiliser dans notre classe Temperature par exemple.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Temperature:
    def __init__(self):
        self.value = 0

    @my_property
    def celsius(self): # le nom de la méthode devient le nom de la propriété
        return self.value
    @celsius.setter
    def celsius(self, value): # le setter doit porter le même nom
        self.value = value

    @my_property
    def fahrenheit(self):
        return self.value * 1.8 + 32
    @fahrenheit.setter
    def fahrenheit(self, value):
        self.value = (value - 32) / 1.8
1
2
3
4
5
6
7
8
>>> t = Temperature()
>>> t.celsius
0
>>> t.fahrenheit
32.0
>>> t.celsius = 100
>>> t.fahrenheit
212.0