Partage
  • Partager sur Facebook
  • Partager sur Twitter

Lancer un signal à chaque modification d'un objet

    2 septembre 2020 à 22:46:52

    Bonjour,
    J'ai un objet que l'utilisateur pourra sauvegarder

    J'aimerais pouvoir détecter systématiquement quand certaines modifications sont effectuées sur l'objet, par exemple pour que le titre de la fenêtre principale ajoute un petit "*" à la fin tant que l'objet n'as pas été sauvegardé, comme dans Notepadd++

    j'ai d'abort pensé à utiliser __setattr__ ou __setattribut__. Je suis tombé sur la page de sam & max sur les descripteurs et les pattern observers qui semblent être exactement le genre de truc donc j'avais besoin (Avertissement au cas où: leur blog est très pédagogique, mais leurs humour n'est pas forcément tout public.)

    Dans les deux cas, le seul problème c'est que les attributs que contient mon objet, sont… une liste et un dictionnaire.

    Et là, même le code de S&M ne fonctionne pas:

    >>> j.credits
    Les crédits ont été consultés:
    get <__main__.Joueur object at 0x7fdc351512e0> credits 0 None
    0
    >>> j.credits = 5
    Les crédits ont changé:
    set <__main__.Joueur object at 0x7fdc351512e0> credits 0 5
    >>> j.credits = [1]
    Les crédits ont changé:
    set <__main__.Joueur object at 0x7fdc351512e0> credits 5 [1]
    >>> Les crédits ont été consultés:
    get <__main__.Joueur object at 0x7fdc351512e0> credits [1] None
    j.credits.append("Hop, j'appelle… __get__ !") # Là, ça ça marche pas.
    Les crédits ont été consultés:                # Non, gros, ils n'ont pas été seulement consultés, ils ont été modifiés !
    get <__main__.Joueur object at 0x7fdc351512e0> credits [1] None
    >>> 


    En effet, lorsque je fait j.credits[index] = [mettez ce que vous voulez ici] ou j.credits.append(de même ici). Ce n'est pas la méthodes __set__ de l'attribut "crédit" qui le modifie. C'est la méthode __get__ qui retourne l'attribut qui, en tant qu'objet se modifie lui même à l'aide d'une de ses propres méthodes…

    Du coup, quelqu'un connait-il une solution pour déclencher une fonction a chaque fois que l'attribut est modifié, même lorsque cet attribut est une liste ou un dictionnaire ?

    J'ai pensé hériter les classes de mes attributs en rajoutant par exemple un décorateur à toute les méthodes qui sont utilisées pour la modification de l'attribut (__set__, __setitem__, __delitem__, __add__, yen a toute une tripotée) ou même les overrider directement dans le constructeur de l'objet (self.attribut.__set__ = mondécorateur(self.attribut.__set__) . Mais ça ne me plaît pas. c'est verbeux. Il y a un risque que j'oublie d'overrider une méthode alors que le but de la manœuvre est justement d'éliminer le risque d'oubli lors d'un appel manuel. Pas ouf. J'ai pas d'idée qui me paraisse vraiment élégante.

    -
    Edité par Megalo 2 septembre 2020 à 22:50:00

    • Partager sur Facebook
    • Partager sur Twitter
      2 septembre 2020 à 23:21:09

      __setattr__ ?
      • Partager sur Facebook
      • Partager sur Twitter

      Python c'est bon, mangez-en. 

        3 septembre 2020 à 10:09:55

        1110012

        -
        Edité par __p_b2cs 4 décembre 2020 à 10:55:30

        • Partager sur Facebook
        • Partager sur Twitter
          3 septembre 2020 à 11:50:15

          C'est quoi qui va pas avec object.__setattr__ ? Ou bien je n'ai pas compris la question ?

          edit:

          je vois, à cause des itérables ...

          class Foo():
              
              def __init__(self):
                  self.list = []
                  self.dict = {}
              
              def __getattribute__(self,attr):
                  print(f"l'attribut <{attr}> a été accédé")
                  if attr in ('list','dict'):
                      print("peut-être faudra-t-il sauvegarder")
                  return object.__getattribute__(self,attr)
                  
              
              def __setattr__(self,attr,newValue):
                  if attr in object.__getattribute__(self,'__dict__').keys():
                      lastValue = object.__getattribute__(self,attr)
                      if lastValue != newValue:
                          print(f"l'attribut <{attr}> a été modifié")
                  else:
                      print(f"l'attribut <{attr}> a été créé")
                  object.__setattr__(self,attr,newValue)
          
          
          a = Foo()
          a.bar = 1
          a.bar = 2
          a.list.append(5)
          

          effectivement c'est une autre paires de manches.

          -
          Edité par josmiley 3 septembre 2020 à 12:38:33

          • Partager sur Facebook
          • Partager sur Twitter

          Python c'est bon, mangez-en. 

            3 septembre 2020 à 12:28:51

            0000001

            -
            Edité par __p_b2cs 4 décembre 2020 à 10:55:56

            • Partager sur Facebook
            • Partager sur Twitter
              3 septembre 2020 à 14:38:31

              autre piste, créer ses propres lists et dicts

              class Bar():
                  
                  def __getattribute__(self,attr):
                      if callable(object.__getattribute__(self,attr)):
                          print("méthode susceptible d'être appliquée:",attr)
                          print('sauvegarde effectuée\n')
                      return object.__getattribute__(self,attr)
                  
                  def __setitem__(self,key,value):
                      print(f"item susceptible d'être modifié: [{key}] = {value}")
                      print('sauvegarde effectuée\n')
                      super(Bar,self).__setitem__(key,value)
              
              
              class MyList(Bar,list):
                  
                  def __init__(self,*liste):
                      list.extend(self,liste)
              
              class MyDict(Bar,dict):
                  
                  def __init__(self,*items):
                      dict.update(self,items)
                  
                      
              class Foo():    
                  
                  def __init__(self):
                      self.list = MyList(1,2,3,4,5)
                      self.dict = MyDict((1,2),(3,4),(5,6))
              
              a = Foo()
              a.list.append(5)
              a.list[0] = 2
              print(a.list)
              
              a.dict[0] = 100
              a.dict.pop(5)
              print(a.dict)
              

              -
              Edité par josmiley 3 septembre 2020 à 15:57:49

              • Partager sur Facebook
              • Partager sur Twitter

              Python c'est bon, mangez-en. 

                3 septembre 2020 à 18:50:03

                josmiley a écrit:

                __setattr__ ?


                Non, __setattr__ n'est pas appelé quand l'attribut est modifié par une de ses propres méthodes.

                sedawk a écrit:

                À ta place je me contenterai de bannir tous les types mutables parce que vouloir gérer les modification des types mutables me parait très difficile (je ne vois même pas comment le faire bien en fait).

                En container immutable et indexable, il reste le tuple si je ne me trompe pas. Et du coup, faudrait que je rajoute des méthodes pour le modifier dans ma classe. Faut voir si se priver de la souplesse des listes est suffisamment confortable dans mon cas ou si ça implique trop de galères. Je vais tester ça.

                josmiley a écrit:

                autre piste, créer ses propres lists et dicts

                Ça implique de devoir réécrire toute les méthodes dont j'aurai besoin. Quitte à devoir faire un truc sur toutes les méthodes de la classe list que j'utilise, je trouve plus simple de toutes les décorer.

                class une_classe:
                    def __init__(self):
                        self.mon_attribut = list()
                
                        self.mon_attribut.__set__ = self.detect_call(self.mon_attribut.__set__)
                        self.mon_attribut.__setitem__ = self.detect_call(self.mon_attribut.__setitem__)
                        self.mon_attribut.__add__ = self.detect_call(self.mon_attribut.__add__)
                        #… etc. Déjà suffisamment lourd, alors s'il faut carrément recréer la classe entière…
                
                    def detect_call(self, fonction):
                        def send_signal(*args, **kwargs):
                            […] # du code
                            print("signal envoyé")
                            return fonction(*args, **kwargs)
                        return send_signal
                


                Le seul risque étant que le signal peut être lancer deux fois (par exemple si __add__ fait lui-même appel à la fonction __setitem__)

                Bon merci. En tout cas j'ai l'impression qu'il n'y a pas de réponse simple.

                • Partager sur Facebook
                • Partager sur Twitter
                  3 septembre 2020 à 20:00:26

                  Megalo a écrit:

                  josmiley a écrit:

                  autre piste, créer ses propres lists et dicts

                  Ça implique de devoir réécrire toute les méthodes dont j'aurai besoin.

                  meuh non, tu remplaces juste tes list par des MyList et tes dict par des MyDict; héritant de list et dict, aucune méthode n'est à réécrire. Tu as essayé le code que j'ai posté ?

                  -
                  Edité par josmiley 3 septembre 2020 à 20:00:56

                  • Partager sur Facebook
                  • Partager sur Twitter

                  Python c'est bon, mangez-en. 

                    3 septembre 2020 à 20:21:50

                    Il y aurait pas deux signaux ? un pour la liste et l'autre pour le dictionnaire ?

                    Sur quelle méthodes appelées de l'un et de l'autre objets pour que cette détection soit prise en compte ?

                    Il n'y a rien d'interne à ces objets pour détecter cela. Peut-être voir du côté d'un dictionnaire modifiants ces deux objets, le hash change...

                    d = {
                        "mylist": [],
                        "mydict": {},
                    }
                    
                    
                    my_hash = lambda obj: hash(frozenset(obj))
                    
                    hl = my_hash(d["mylist"])
                    print(hl)  # 133146708735736
                    d["mylist"].append(5)
                    hl = my_hash(d["mylist"])
                    print(hl)  # -5039393493211280168
                    
                    hl = my_hash(d["mydict"])
                    print(hl)  # 133146708735736
                    d["mydict"]["a"] = 5
                    hl = my_hash(d["mydict"])
                    print(hl)  # 4174052957970514183
                    



                    • Partager sur Facebook
                    • Partager sur Twitter

                    Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
                    La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)

                      3 septembre 2020 à 22:05:03

                      fred1599 a écrit:

                      Il y aurait pas deux signaux ? un pour la liste et l'autre pour le dictionnaire ?

                      Dans l'absolu, si. Mais si je trouve une méthode pour l'un, à priori elle marchera sur l'autre, non ?

                      fred1599 a écrit:

                      Sur quelle méthodes appelées de l'un et de l'autre objets pour que cette détection soit prise en compte ?

                       C'est toute la question. Je veux détecter les modifications. Donc, soit il faut modifier toutes les méthodes s'occupant de modifier c'est objets. Soit il faut detecter automatiquement si une méthode modifie l'objet ou pas. Enfin, pour le moment je ne vois que ça.

                      fred1599 a écrit:

                      Peut-être voir du côté d'un dictionnaire modifiants ces deux objets, le hash change...

                       J'avais déjà pensais a une comparaison avec une sorte de cache, enfin un truc style ma_liste == old_liste. Le hash c'est la version ++.

                      Mais il faut quand même un truc qui déclenche la comparaison. Peut-être en comparant à chaque __getattr__ ? Oui ça doit marcher ça… Je teste et te redis.

                      josmiley a écrit:

                      meuh non, tu remplaces juste tes list par des MyList et tes dict par des MyDict; héritant de list et dict, aucune méthode n'est à réécrire. Tu as essayé le code que j'ai posté ?

                       Pardon, je suis effectivement passé un peu vite sur ton code. Du coup, ça marche presque:

                      >>> f = Foo()
                      >>> f.list
                      [1, 2, 3, 4, 5]
                      >>> méthode susceptible d'être appliquée: append
                      sauvegarde effectuée
                      
                      méthode susceptible d'être appliquée: __class__
                      sauvegarde effectuée
                      
                      f.list.append(7) # ça, ça marche. Même un peu trop.
                      méthode susceptible d'être appliquée: append
                      sauvegarde effectuée
                      
                      >>> del f.list[2] # Ça, ça marche pas.
                      >>> f.list[2] = 6 # Ça, ça marche.
                      item susceptible d'être modifié: [2] = 6
                      sauvegarde effectuée
                      
                      >>> for el in f.list:
                      	print(el)
                      
                      	
                      1
                      2
                      6
                      5
                      7
                      >>> for el in f.list: # Et là, ça marche pas non plus.
                      	el = 5


                      Donc pas aussi simple.







                      • Partager sur Facebook
                      • Partager sur Twitter
                        3 septembre 2020 à 22:08:18

                        for el in f.list ne modifie pas f.list mais el.

                        Et effectivement, del passe au travers ... rajout de __delitem___ dans Bar.

                        class Bar():
                                
                            def __getattribute__(self,attr):
                                if callable(object.__getattribute__(self,attr)):
                                    print("méthode susceptible d'être appliquée:",attr)
                                    print('sauvegarde effectuée\n')
                                return object.__getattribute__(self,attr)
                            
                            def __setitem__(self,key,value):
                                print(f"item susceptible d'être modifié: [{key}] = {value}")
                                print('sauvegarde effectuée\n')
                                super(Bar,self).__setitem__(key,value)
                            
                            def __delitem__(self,key):
                                print(f"la clé <{key}> va être supprimée")
                                super(Bar,self).__delitem__(key)



                        -
                        Edité par josmiley 3 septembre 2020 à 22:32:45

                        • Partager sur Facebook
                        • Partager sur Twitter

                        Python c'est bon, mangez-en. 

                          3 septembre 2020 à 22:55:31

                          1010113

                          -
                          Edité par __p_b2cs 4 décembre 2020 à 10:56:28

                          • Partager sur Facebook
                          • Partager sur Twitter
                            3 septembre 2020 à 23:07:17

                            josmiley a écrit:

                            for el in f.list ne modifie pas f.list mais el.

                            Et effectivement, del passe au travers ... rajout de __delitem___ dans Bar.

                            class Bar():
                                    
                                def __getattribute__(self,attr):
                                    if callable(object.__getattribute__(self,attr)):
                                        print("méthode susceptible d'être appliquée:",attr)
                                        print('sauvegarde effectuée\n')
                                    return object.__getattribute__(self,attr)
                                
                                def __setitem__(self,key,value):
                                    print(f"item susceptible d'être modifié: [{key}] = {value}")
                                    print('sauvegarde effectuée\n')
                                    super(Bar,self).__setitem__(key,value)
                                
                                def __delitem__(self,key):
                                    print(f"la clé <{key}> va être supprimée")
                                    super(Bar,self).__delitem__(key)



                            -
                            Edité par josmiley il y a 24 minutes


                            Effectivement, autant pour moi. Du coup ta solution a l'air de marcher. J'essaierai de mieux la comprendre à une heure raisonnable demain ou après-demain.

                            sedawk a écrit:

                            Et que feras-tu si le dictionnaire ou la liste contient d'autres mutables ? Le problème est bien plus complexe qu'il n'y parait.

                             Mmm, j'y avais pas pensé. En l’occurrence, dans mon cas il s'agira d'une liste d'objet customs. Du coups, même la solution de remplacer la liste par un tuple ne suffira pas.



                            • Partager sur Facebook
                            • Partager sur Twitter
                              4 septembre 2020 à 5:24:10

                              sedawk a écrit:

                              Et que feras-tu si le dictionnaire ou la liste contient d'autres mutables ? Le problème est bien plus complexe qu'il n'y parait.


                              ha ben si en plus c'est récursif ^^

                              dans ce cas, autant partir du principe que quelque chose va être modifié et faire une sauvegarde d'emblée.

                              • Partager sur Facebook
                              • Partager sur Twitter

                              Python c'est bon, mangez-en. 

                                4 septembre 2020 à 8:58:43

                                Megalo a écrit:

                                fred1599 a écrit:

                                Il y aurait pas deux signaux ? un pour la liste et l'autre pour le dictionnaire ?

                                Dans l'absolu, si. Mais si je trouve une méthode pour l'un, à priori elle marchera sur l'autre, non ?

                                Non c'est deux objets différents... En plus de cela, dans un dictionnaire on peut modifier/ajouter/supprimer clés et valeurs

                                Megalo a écrit:

                                fred1599 a écrit:

                                Sur quelle méthodes appelées de l'un et de l'autre objets pour que cette détection soit prise en compte ?

                                 C'est toute la question. Je veux détecter les modifications. Donc, soit il faut modifier toutes les méthodes s'occupant de modifier c'est objets. Soit il faut detecter automatiquement si une méthode modifie l'objet ou pas. Enfin, pour le moment je ne vois que ça.

                                Ça me semble plutôt inquiétant, à mon sens c'est à toi d'y répondre en sachant déjà à l'avance si tu utiliseras la méthode append des listes, ou la méthode update des dictionnaires par exemple. On pourrait croire que tu as en tête une conception qui te permettrait de répondre à ces questions.

                                Megalo a écrit:

                                fred1599 a écrit:

                                Peut-être voir du côté d'un dictionnaire modifiants ces deux objets, le hash change...

                                 J'avais déjà pensais a une comparaison avec une sorte de cache, enfin un truc style ma_liste == old_liste. Le hash c'est la version ++.

                                Mais il faut quand même un truc qui déclenche la comparaison. Peut-être en comparant à chaque __getattr__ ? Oui ça doit marcher ça… Je teste et te redis.

                                J'ai pas tout compris, mais effectivement, la création d'un objet avec son hash renseigné paraît évident, pour comparer avec le hash d'un objet modifié.

                                • Partager sur Facebook
                                • Partager sur Twitter

                                Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
                                La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)

                                  6 septembre 2020 à 12:31:02

                                  fred1599 a écrit:
                                   
                                      Ça me semble plutôt inquiétant, à mon sens c'est à toi d'y répondre en sachant déjà à l'avance si tu utiliseras la méthode append des listes, ou la méthode update des dictionnaires par exemple. On pourrait croire que tu as en tête une conception qui te permettrait de répondre à ces questions.
                                   
                                  Ben justement, l'idée c'était de détecter les modifications, indépendamment du fait que ce soit la méthode toto ou tintin qui ait modifié l'objet en question. Le principe de l'encapsulation quoi. On crée une classe, et une fois celle-ci crée, l'utilisateur peut manipuler les instances comme bon lui semble sans avoir à se préoccuper d'utiliser telle ou telle méthode.
                                   
                                  En C++, je pense que le problème aurait été réglé en quelques lignes en surchargeant le getter (si la version const est appelé, ya rien à faire, si c'est la non-const, alors il y a eu modification de l'attribut). En Python la notion de méthode const n'existe pas. Le prix de la simplicité probablement.

                                  fred1599 a écrit:

                                  J'ai pas tout compris, mais effectivement, la création d'un objet avec son hash renseigné paraît évident, pour comparer avec le hash d'un objet modifié.

                                  Ben, c'est simple. Comparer les hash permet de détecter si l'objet a changé au moment où tu fais la comparaison. Mais le test ne se fait pas automatiquement à chaque fois que tu fais quelques chose susceptible de modifier l'objet. Donc tu reportes le problème "détecter une modification" à "détecter une possible modification."

                                  Le mieux que j'ai réussi à faire jusqu'à présent:

                                  class MaClasse:
                                  
                                      def __init__(self):
                                          self._maliste = list()
                                          self._mondic = dict()
                                          self._hash = hash(self)
                                  
                                      @property
                                      def maliste(self):
                                          if self._hash != hash(self):
                                              print("maliste modified")
                                              self._hash = hash(self)
                                          return self._maliste
                                  
                                      @maliste.setter
                                      def maliste(self, liste):
                                          print("maliste modified")
                                          self._maliste = list(maliste)
                                          self._hash = hash(self)
                                  
                                      @property
                                      def mondic(self):
                                          if self._hash != hash(self):
                                              print("mondic modified")
                                              self._hash = hash(self)
                                          return self._mondic
                                  
                                      @mondic.setter
                                      def mondic(self, dic):
                                          print("mondic_modified")
                                          self._mondic = dict(dic)
                                          self._hash = hash(self)
                                  
                                      def __hash__(self):
                                          h1 = (r for r in self._maliste)
                                          h2 = (i for i in self._mondic.items())
                                          return hash(tuple(itertools.chain(h1, h2)))

                                  Le problème est que le

                                  if self._hash != hash(self):

                                   dans le getter, est appelé avant le return. Hors, les méthodes de maliste ne modifient l'objet qu'au moment du return du getter. Dis autrement, la détection ne se fait qu'à l'appel suivant si appel il y a.

                                  Il faudrait donc pour voir lancer un équivalent de

                                      @property
                                      def maliste(self):
                                         return self._maliste
                                         if self._hash != hash(self):
                                              print("maliste modified")
                                              self._hash = hash(self)        
                                  Mais je ne connais pas de moyen de faire ça.

                                  -
                                  Edité par Megalo 6 septembre 2020 à 12:35:02

                                  • Partager sur Facebook
                                  • Partager sur Twitter
                                    6 septembre 2020 à 16:51:15

                                    Ce que tu veux ressemble au pattern state où je verrai tes objets soit dans l'état change ou no change

                                    Un exemple ICI... qui peut te donner une idée peut-être.

                                    • Partager sur Facebook
                                    • Partager sur Twitter

                                    Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
                                    La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)

                                      13 septembre 2020 à 14:54:17

                                      Merci, mais sauf erreur de ma part, ton exemple ne fait que reporter le problème: qu'est-ce qui va appeler la fonction switch() ou change() à chaque changement d'état de l'objet ?

                                      Non, pour le moment je pense que je vais partir sur autre chose. Je ne vois pas de solution qui soit plus simple que le problème qu'elle résout. Donc je pense que je vais rajouter une ligne de code à chaque fois que je modifie l'objet manuellement. Merci pour vois contributions.

                                      • Partager sur Facebook
                                      • Partager sur Twitter

                                      Lancer un signal à chaque modification d'un objet

                                      × 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