Partage
  • Partager sur Facebook
  • Partager sur Twitter

[TP][Intermédiaire - Avancé] Un mécanisme de "properties"

… en pur Python

    22 décembre 2011 à 22:30:08

    Salut,

    Cela fait un moment que l'on n'a pas fait de TP sur ce forum ! Pour rattraper le temps perdu, je vous propose aujourd'hui une activité de niveau avancé qui va vous faire aborder plusieurs notions de notre langage favori sur lesquelles on travaille peu d'ordinaire, et vous faire regarder Python non pas en tant que simple utilisateur, mais plutôt, pour une fois, de l'intérieur.

    Avant de commencer, voici quelques notions sur lesquelles nous allons nous pencher dans ce TP :
    • Les properties, évidemment, puisque ce TP tourne autour de leur mécanisme,
    • Les descripteurs, qui sont une notion d'assez bas niveau (et, comme vous allez le voir, puissante en Python) que vous ne connaissez peut-être pas,
    • Les décorateurs dont nous allons voir une utilisation astucieuse.


    Introduction



    Rappels sur les properties



    Il n'est pas rare les développeurs POO de tout poil habitués à C++ ou Java reprochent à Python son aspect tout-est-public, arguant qu'à cause de cela, on ne peut exercer aucun contrôle sur les attributs de nos classes, parce que n'importe qui peut modifier n'importe quel attribut de n'importe quelle classe, « à moins de faire des trucs un peu dégueu à bas niveau ».

    En fait, Python dispose d'un mécanisme qui permet, entre autres, de contrôler l'accès en lecture et en écriture aux attributs d'une classe donnée, et ce de façon totalement transparente : les properties. Je vous ai mis la version longue du « pourquoi du comment » dans la balise secret suivante. Si vous connaissez déjà ce mécanisme vous pouvez passer à la suite.


    Prenons une classe toute bête :

    class Rectangle:
        def __init__(self, width, height):
            self.width = width
            self.height = height
    

    Comme vous le savez déjà, si j'instancie cette classe, j'obtiendrai un objet avec les attributs duquel je pourrai jouer comme bon me semble. En fait, rien ne m'empêcherait de faire des choses qui n'ont aucun sens, comme assigner une chaîne de caractères à rect.width et un nombre flottant strictement négatif à rect.height. Python ne bronchera pas, sauf qu'il déclenchera une erreur bizarre lorsque je voudrai calculer l'aire de mon rectangle, par exemple.

    Un premier moyen de garder un peu de contrôle sur mes attributs serait de faire quelque-chose comme ceci :

    class Rectangle:
        def __init__(self, width, height):
            if not isinstance(width, int) or not isinstance(height, int):
                raise TypeError ("width et height doivent être entiers")
            if width < 0 or height < 0:
                raise ValueError("width et height doivent être positifs")
            
            self.__width = width
            self.__height = height
    
        def get_width(self):
            return self.__width
        
        def get_height(self):
            return self.__height
    
        def set_width(self, val):
            if not isinstance(val, int) or val < 0:
                raise TypeError("width doit être un entier positif")
            self.__width = val
            
        def set_height(self, val):
            if not isinstance(val, int) or val < 0:
                raise TypeError("height doit être un entier positif")
            self.__height = val
    

    L'interface de cette classe est beaucoup plus lourde à utiliser que la précédente (4 méthodes à retenir pour l'utilisateur...), mais au moins, elle a le mérite de ne pas nous obliger à faire une confiance aveugle à l'utilisateur : s'il fait n'importe quoi, son programme plantera.


    Les properties sont un moyen de simplifier l'interface de votre classe tout en gardant le contrôle sur vos attributs :

    class Rectangle:
        def __init__(self, width, height):
            if not isinstance(width, int) or not isinstance(height, int):
                raise TypeError ("width et height doivent être entiers")
            if width < 0 or height < 0:
                raise ValueError("width et height doivent être positifs")
            
            self.__width = width
            self.__height = height
    
        @property
        def width(self):
            return self.__width
        
        @property
        def height(self):
            return self.__height
    
        @width.setter
        def width(self, val):
            if not isinstance(val, int) or val < 0:
                raise TypeError("width doit être un entier positif")
            self.__width = val
            
        @height.setter
        def height(self, val):
            if not isinstance(val, int) or val < 0:
                raise TypeError("height doit être un entier positif")
            self.__height = val
    


    Testons cette classe :

    Python 3.2.2 (default, Nov 21 2011, 16:51:01) 
    [GCC 4.6.2] on linux2
    Type "help", "copyright", "credits" or "license" for more information.
    >>> import rect
    >>> r = rect.Rectangle(13, 37)
    >>> r.width
    13
    >>> r.height
    37
    >>> r.width = 3
    >>> r.height = -14
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "rect.py", line 29, in height
        raise TypeError("height doit être un entier positif")
    TypeError: height doit être un entier positif
    >>> r.height = 14
    >>> print(r.width, ".", r.height)
    3 . 14
    


    Et mieux encore. Imaginons maintenant que je veuille donner à mon rectangle un attribut pour représenter son aire. Il est inconcevable de laisser à l'utilisateur la possibilité d'écrire dans cet attribut, étant donné que dans ce cas, on ne pourrait plus garantir que l'aire du rectangle est égale au produit de sa hauteur et de sa largeur.

    Les properties nous fournissent une solution élégante à ce problème :

    class Rectangle:
        # ...
        @property
        def area(self):
            return self.__width * self.__height
    


    >>> r = Rectangle(13, 37)
    >>> r.area
    481
    >>> r.area = 28
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: can't set attribute
    


    Les properties font partie des built-ins de Python. C'est-à-dire qu'elles ont été implémentées en C et qu'elles sont accessibles sans importer le moindre module. Elles sont donc une feature de Python d'assez bas niveau. Ce TP va consister à réimplémenter de A à Z ce mécanisme en pur Python, mais rassurez-vous, nous allons faire tout cela pas à pas, et on en profitera pour apprendre deux-trois nouveaux trucs au passage. ;)

    L'interface de base



    Dans cette partie, je vais essayer de vous aider à démarrer. Un autre moyen que les décorateurs pour créer une property est d'invoquer directement un constructeur comme ceci :

    class Rectangle:
        def __init__(self, width, height):
            if not isinstance(width, int) or not isinstance(height, int):
                raise TypeError ("width et height doivent être entiers")
            if width < 0 or height < 0:
                raise ValueError("width et height doivent être positifs")
            
            self.__width = width
            self.__height = height
    
        def get_width(self):
            return self.__width
        
        def set_width(self, val):
            if not isinstance(val, int) or val < 0:
                raise TypeError("width doit être un entier positif")
            self.__width = val
    
        width = property(fget=get_width, fset=set_width)
    


    Ce code nous dévoile clairement la nature des properties. Il s'agit d'un objet, qui encapsule ici deux fonctions fset et fget, et qui est contenu dans un attribut statique de la classe Rectangle, c'est-à-dire un attribut qui est partagé par toutes ses instances.

    Cela signifie que notre code commencera par ceci :

    class prop():
        """ Classe décrivant une property. """
        def __init__(self, fget=None, fset=None):
            self.__fget = fget
            self.__fset = fset
    


    Les descripteurs



    Un descripteur est un objet capable de contrôler ce qui se passe lorsque l'on accède à sa référence en lecture ou en écriture. Par exemple, si l'objet obj a le descripteur desc dans ses attributs statiques, alors desc est capable entre autres de contrôler ce qui se passe lorsque l'on l'assigne à une variable, (var = obj.desc), ou lorsque l'on veut remplacer sa référence par une autre (obj.desc = var).

    Concrêtement, un descripteur n'est rien d'autre qu'un objet qui surcharge ses méthodes spéciales __get__ ou __set__ et __delete__.

    Je vous montre comment cela se passe pour l'accès en lecture, la documentation devrait suffire pour le reste :

    >>> class Foo:
    ...     def __get__(self, instance, owner):
    ...             print("Accès à l'attribut Foo de l'instance", instance, "de la classe", owner.__name__)
    ...             return self
    ... 
    >>> class MaClasse:
    ...     foo = Foo()
    ... 
    >>> obj = MaClasse()
    >>> obj.foo
    Accès à l'attribut Foo de l'instance <__main__.MaClasse object at 0xb70b11cc> de la classe MaClasse
    <__main__.Foo object at 0xb70b12cc>
    


    Sympathique, non ? :)

    Allez, au travail !



    1. Surchargez la méthode __get__ de la classe prop, de façon qu'elle appelle l'accesseur qu'elle encapsule chaque fois que l'on accède à elle en lecture. Si une propriété n'encapsule pas de méthode fget, alors elle lève une exception de type AttributeError indiquant que l'attribut n'est pas accessible en lecture.


    2. Surchargez la méthode __set__ de la classe prop, de façon qu'elle appelle le mutateur qu'elle encapsule chaque fois que l'on accède à elle en écriture. Si une propriéte n'encapsule pas de méthode fset, alors elle lève une exception de type AttributeError indiquant que l'attribut n'est pas accessible en écriture.

    Bravo ! Vous venez de coder les fameux « 80% de fonctionnalités qui demandent 20% du code ». En particulier, cela signifie que vos properties faites maison sont déjà parfaitement utilisables sur n'importe quelle classe, pourvu que vous la créiez avec la syntaxe width = property(get_width, set_width).

    Il ne nous reste plus qu'à fignoler notre classe pour qu'elle se comporte exacement comme la classe property.

    Les autres attributs des propriétés



    3. Donnez à la classe prop le contrôle de ce qui se produit lorsque l'on supprime une propriété au moyen de la syntaxe del foo.bar_prop. Notez que si la propriété n'encapsule pas de fonction fdel, aucune exception n'est levée.

    4. Lorsque l'on appelle la fonction help() sur une property, celle-ci affiche la documentation de la fonction fget(), ou bien l'attribut __doc__ de la propriété, si celui-ci a été passé au constructeur de la classe property. Implémentez ce comportement.

    L'interface avancée : les décorateurs



    5. Les propriétés disposent de deux méthodes setter() et deletter() qui s'utilisent comme des décorateurs (voir plus haut). Implémentez ces méthodes.

    6. Question-piège : implémentez la création d'une propriété au moyen du décorateur @prop, comme dans l'exemple du début. Attention, c'est assez compliqué…

    Et voilà ! Vous venez de réimplémenter les properties de Python dans leur totalité.

    Joyeux Noël ! :magicien:
    • Partager sur Facebook
    • Partager sur Twitter
    Zeste de Savoir, le site qui en a dans le citron !
    Anonyme
      23 décembre 2011 à 19:00:50

      Tu me donnes mal le crâne juste avant noël :)

      me suis viandé, je repasse bientôt

      Pour la question 1 et 2

      Edit : ça y est j'ai trouvé :)

      class prop(object):
          """ Classe décrivant une property. """
          def __init__(self, fget=None, fset=None):
              self.__fget = fget
              self.__fset = fset
          def __get__(self, instance, classe):
              if self.__fget is None:
                  raise AttributeError('Proteger en lecture')
              return getattr(instance, self.__fget.__name__)()
          def __set__(self, instance, valeur):
              if self.__fset is None:
                  raise AttributeError('Proteger en ecriture')
              getattr(instance, self.__fset.__name__)(valeur)
      
      class Rectangle(object):
          def __init__(self, width, height):
              
              if not isinstance(width, int) or not isinstance(height, int):
                  raise TypeError ("width et height doivent être entiers")
              if width < 0 or height < 0:
                  raise ValueError("width et height doivent être positifs")
              
              self.__width = width
              self.__height = height
              
          def get_width(self):
              return self.__width
      
          def set_width(self, val):
              if not isinstance(val, int) or val < 0:
                  raise TypeError("width doit être un entier positif")
              self.__width = val
      
          width = prop(fget=get_width, fset=set_width)
      
      r = Rectangle(15, 5)
      print r.width
      r.width = 20
      print r.width
      
      • Partager sur Facebook
      • Partager sur Twitter
        23 décembre 2011 à 22:41:41

        @fred1599: voilà, c'est ça pour les deux premières questions.

        Si on veut chipoter, on peut aussi faire en sorte d'obtenir le comportement suivant

        J'ai rien dit.
        • Partager sur Facebook
        • Partager sur Twitter
        Zeste de Savoir, le site qui en a dans le citron !
        Anonyme
          23 décembre 2011 à 22:53:30

          La 3

          # -*- coding:utf8 -*-
          
          class prop(object):
              """ Classe décrivant une property. """
              def __init__(self, fget=None, fset=None, fdel=None):
                  self.__fget = fget
                  self.__fset = fset
                  self.__fdel = fdel
              def __get__(self, instance, classe):
                  if self.__fget is None:
                      raise AttributeError('Proteger en lecture')
                  return getattr(instance, self.__fget.__name__)()
              def __set__(self, instance, valeur):
                  if self.__fset is None:
                      raise AttributeError('Proteger en ecriture')
                  getattr(instance, self.__fset.__name__)(valeur)
              def __delete__(self, instance):
                  if self.__fdel is None:
                      raise AttributeError('suppression impossible')
                  getattr(instance, self.__fdel.__name__)()
          
          class Rectangle(object):
              def __init__(self, width, height):
                  
                  if not isinstance(width, int) or not isinstance(height, int):
                      raise TypeError ("width et height doivent être entiers")
                  if width < 0 or height < 0:
                      raise ValueError("width et height doivent être positifs")
                  
                  self.__width = width
                  self.__height = height
                    
              def get_width(self):
                  return self.__width
              
              def set_width(self, val):
                  if not isinstance(val, int) or val < 0:
                      raise TypeError("width doit être un entier positif")
                  self.__width = val
          
              def del_width(self):
                  print "suppression de l'attribut __width"
                  del self.__width
          
              width = prop(fget=get_width, fset=set_width, fdel=del_width)
          
          r = Rectangle(15, 5)
          print r.width
          r.width = 20
          print r.width
          del r.width
          print r.width
          
          • Partager sur Facebook
          • Partager sur Twitter
            25 décembre 2011 à 20:10:28

            Question 1, 2 et 3 :

            class prop:
                def __init__(self, fget=None, fset=None, fdel=None):
                    self.__fget = fget
                    self.__fset = fset
                    self.__fdel = fdel
                def __get__(self, instance, owner):
                    if self.__fget == None:
                        raise AttributeError
                    return self.__fget(instance)
                def __set__(self, instance, value):
                    if self.__fset == None:
                        raise AttributeError
                    return self.__fset(instance, value)
                def __delete__(self, instance):
                    if self.__fdel == None:
                        raise AttributeError
                    self.__fdel(instance)
            


            Je crois que ça marche, mais j'ai encore du mal avec le principe des descripteurs, donc je ne saurais pas bien expliquer comment ça marche... :-°

            Du coup, j'ai pas du tout compris pourquoi fred1599 s'est cassé la tête à ce point. o_O
            • Partager sur Facebook
            • Partager sur Twitter
            Anonyme
              25 décembre 2011 à 20:31:26

              Citation

              Du coup, j'ai pas du tout compris pourquoi fred1599 s'est cassé la tête à ce point.



              Parce-que getattr demande une chaîne de caractères.

              En effet ce que tu as fais doit fonctionner.

              par contre je préfère is None à == None

              Edit :Quoique après réflexion, je ne comprend pas pourquoi tes fonction __fget, etc... prennent une instance comme argument, sachant que l'instance est l'objet créé grâce à la classe Rectangle.

              La notation serait instance.methode ou instance.attribut

              • Partager sur Facebook
              • Partager sur Twitter
                25 décembre 2011 à 22:28:10

                Citation : fred1599

                par contre je préfère is None à == None


                Tu as raison. Un réflexe C/C++, sans doute...

                Sinon, voici mon code pour les questions 5 et 6 :
                class prop:
                    def __init__(self, fget=None, fset=None, fdel=None):
                        self.__fget = fget
                        self.__fset = fset
                        self.__fdel = fdel
                    def __get__(self, instance, owner):
                        if self.__fget is None:
                            raise AttributeError
                        return self.__fget(instance)
                    def __set__(self, instance, value):
                        if self.__fset is None:
                            raise AttributeError
                        return self.__fset(instance, value)
                    def __delete__(self, instance):
                        if self.__fdel is None:
                            raise AttributeError
                        self.__fdel(instance)
                    def setter(self, fn):
                        self.__fset = fn
                        return self
                    def deletter(self, fdel):
                        self.__fdel = fdel
                        return self
                


                Un grand merci pour cet exo, nohar ! :)
                • Partager sur Facebook
                • Partager sur Twitter
                  25 décembre 2011 à 22:38:28

                  Citation : fred1599


                  Parce-que getattr demande une chaîne de caractères.



                  Pourquoi as-tu besoin de getattr ?

                  Citation : fred1599


                  Edit :Quoique après réflexion, je ne comprend pas pourquoi tes fonction __fget, etc... prennent une instance comme argument, sachant que l'instance est l'objet créé grâce à la classe Rectangle.

                  La notation serait instance.methode ou instance.attribut



                  >>> class Foo:
                  ...     def __init__(self, x):
                  ...             self.x = x
                  ...     def get(self):
                  ...             return self.x
                  ... 
                  >>> foo = Foo(42)
                  >>> foo.get()
                  42
                  >>> bar = Foo.get
                  >>> bar()
                  Traceback (most recent call last):
                    File "<stdin>", line 1, in <module>
                  TypeError: get() takes exactly 1 argument (0 given)
                  >>> bar(foo)
                  42
                  


                  Citation : yoch


                  Un grand merci pour cet exo, nohar !



                  De rien. :)
                  Si la formule "recoder une feature de Python pour jouer avec des notions avancées" vous plaît, j'ai déjà une ou deux autres idées de TP (beaucoup plus longs, par contre) pour la prochaine fois.
                  • Partager sur Facebook
                  • Partager sur Twitter
                  Zeste de Savoir, le site qui en a dans le citron !
                  Anonyme
                    25 décembre 2011 à 22:48:50

                    Citation

                    Pourquoi as-tu besoin de getattr ?



                    Je sais pas, bref bizarrement c'est la première qui me soit venu en lisant les docs.

                    >>> bar(foo)
                    42
                    


                    Je comprend pas le mécanisme, faut m'expliquer là! Parce qu'on pourrait très bien écrire cela


                    Enfin bref je suis sûr de ne pas avoir compris quelquechose, mais là je suis largué, je veux bien qu'on m'explique.

                    C'est bon j'ai pigé
                    • Partager sur Facebook
                    • Partager sur Twitter
                      26 décembre 2011 à 9:20:45

                      Citation : nohar

                      Si la formule "recoder une feature de Python pour jouer avec des notions avancées" vous plaît, j'ai déjà une ou deux autres idées de TP (beaucoup plus longs, par contre) pour la prochaine fois.


                      Perso, je trouve ça assez sympa, ça nous permet de découvrir certains rouages méconnus du langage (surtout pour ceux qui pratiquent peu comme moi). En revanche, si c'est vraiment long, je ne sais pas... :-°
                      La seule chose que je me suis amusé à ré-implémenter, c'est les namedtuple, c'était d'ailleurs assez intéressant. :)
                      • Partager sur Facebook
                      • Partager sur Twitter
                        26 décembre 2011 à 11:32:58

                        Citation : yoch

                        En revanche, si c'est vraiment long, je ne sais pas... :-°



                        En fait, l'idée que j'ai en tête serait un TP où l'on jouerait avec une méta-classe pour implémenter des classes abstraites (un peu comme le module abc de la bibliothèque standard). Ce qui le rendrait long, ce serait surtout les explications (pour ne perdre personne en route et bien comprendre le principe des méta-classes, et l'intérêt des ABC), plutôt que la quantité de code à produire. On le voit bien dans ce TP : il suffit d'une trentaine de lignes bien tassées pour reproduire les properties, mais le plus dur reste de comprendre le mécanisme qui entre en action derrière. ;)
                        • Partager sur Facebook
                        • Partager sur Twitter
                        Zeste de Savoir, le site qui en a dans le citron !
                          26 décembre 2011 à 11:39:01

                          Très intéressant. Je suis partant. ;)
                          • Partager sur Facebook
                          • Partager sur Twitter
                          Anonyme
                            26 décembre 2011 à 11:59:50

                            Tu peux utiliser dans ces explications pour t'aider à créer ton TP.

                            En ce qui me concerne je n'ai jamais eu besoin des métaclasses, et je ne suis pas sûr d'en avoir besoin un jour.

                            • Partager sur Facebook
                            • Partager sur Twitter
                              26 décembre 2011 à 12:04:24

                              Citation : fred1599

                              Tu peux utiliser dans ces explications pour t'aider à créer ton TP.



                              Ou bien la doc + la PEP-3119.

                              Le principe des méta-classes, en lui-même, n'est pas extrêmement compliqué (c'est ni plus ni moins qu'une classe qui surcharge sa méthode spéciale __new__), c'est surtout leur portée et la catégorie de problèmes qu'elles permettent de résoudre qui est assez difficile à cerner. ;)
                              • Partager sur Facebook
                              • Partager sur Twitter
                              Zeste de Savoir, le site qui en a dans le citron !
                              Anonyme
                                26 décembre 2011 à 12:09:18

                                Citation

                                c'est surtout leur portée et la catégorie de problèmes qu'elles permettent de résoudre qui est assez difficile à cerner.



                                Je le conçois aisément, et trouver un exemple concret où cette utilisation te permet de démontrer son utilité va être difficile.
                                • Partager sur Facebook
                                • Partager sur Twitter
                                  26 décembre 2011 à 12:16:51

                                  Citation : fred1599

                                  Citation

                                  c'est surtout leur portée et la catégorie de problèmes qu'elles permettent de résoudre qui est assez difficile à cerner.



                                  Je le conçois aisément, et trouver un exemple concret où cette utilisation te permet de démontrer son utilité va être difficile.



                                  Comme je l'ai dit : les Abstract Base Classes, ou bien les interfaces, ou bien tout autre genre de classe que l'on peut trouver dans des langages comme Java ou C++ et qui n'existent pas par défaut en Python, sont de bons exemples d'application des métaclasses.
                                  Mais de préférence les Abstract Base Classes puisqu'il s'agit d'un module de la bibliothèque standard (et que c'est aussi l'occasion d'expliquer/de découvrir comment ça fonctionne), et que dans la PEP-3119 , Guido semble montrer qu'il s'agit d'un mécanisme un plus puissant et plus commode à utiliser en Python que les interfaces…
                                  À la réflexion, du point de vue pédagogique les interfaces pourraient même être une étape vers les ABC. :)

                                  Mais on commence à dériver sérieusement du sujet, là.
                                  • Partager sur Facebook
                                  • Partager sur Twitter
                                  Zeste de Savoir, le site qui en a dans le citron !

                                  [TP][Intermédiaire - Avancé] Un mécanisme de "properties"

                                  × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                                  × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
                                  • Editeur
                                  • Markdown