Partage
  • Partager sur Facebook
  • Partager sur Twitter

[JSON] La sérialisation et l'héritage

ValueError: Circular reference detected

Sujet résolu
Anonyme
    14 novembre 2011 à 0:32:38

    Bonsoir,

    Dans le cadre de la rédaction d'un tutoriel sur le module json de Python, je m'attaque à la sérialisation avec héritage.
    Comme vous pouvez le voir, mes essais ne sont pas fructueux :

    import json
    
    class Playlist:
        def __init__(self, nom):
            self.nom = nom
            self.musiques = []
            
    class ImagedPlaylist(Playlist):
        def __init__(self, nom):
            Playlist.__init__(self, nom)
            self.image = ""
    
    def serialiseur_perso(obj):
        if isinstance(obj, ImagedPlaylist):
            return {"__class__": "ImagedPlaylist",
                    "__parent__": Playlist(obj)}
        
        if isinstance(obj, Playlist):
            return {"__class__": "Playlist",
                    "nom": obj.nom,
                    "musiques": obj.musiques}
        
        raise TypeError(repr(pobjet) + " n'est pas sérialisable !")
    
    if __name__ == "__main__":
        print(json.dumps(ImagedPlaylist("MeshowRandom"), indent=4, default=serialiseur_perso))
    


    Traceback (most recent call last):
      File "C:\Users\Romain\Documents\Site du Zéro\Tutoriels\Sérialisez vos objets avec JSON !\Héritage.py", line 26, in <module>
        print(json.dumps(ImagedPlaylist("MeshowRandom"), indent=4, default=serialiseur_perso))
      File "C:\Python32\lib\json\__init__.py", line 231, in dumps
        **kw).encode(obj)
      File "C:\Python32\lib\json\encoder.py", line 190, in encode
        chunks = list(chunks)
      File "C:\Python32\lib\json\encoder.py", line 420, in _iterencode
        for chunk in _iterencode(o, _current_indent_level):
      File "C:\Python32\lib\json\encoder.py", line 411, in _iterencode
        for chunk in _iterencode_dict(o, _current_indent_level):
      File "C:\Python32\lib\json\encoder.py", line 385, in _iterencode_dict
        for chunk in chunks:
      File "C:\Python32\lib\json\encoder.py", line 420, in _iterencode
        for chunk in _iterencode(o, _current_indent_level):
      File "C:\Python32\lib\json\encoder.py", line 411, in _iterencode
        for chunk in _iterencode_dict(o, _current_indent_level):
      File "C:\Python32\lib\json\encoder.py", line 385, in _iterencode_dict
        for chunk in chunks:
      File "C:\Python32\lib\json\encoder.py", line 417, in _iterencode
        raise ValueError("Circular reference detected")
    ValueError: Circular reference detected
    


    J'essaye ici de transformer l'objet ImagedPlaylist en Playlist pour que la fonction resérialise l'objet en tant que tel et l'ajoute dans l'entrée "__parent__" mais cela ne fonctionne pas.

    Avez-vous déjà été confrontés à ce problème ?
    Auriez-vous des idées ?

    Merci d'avance,
    MicroJoe. :)
    • Partager sur Facebook
    • Partager sur Twitter
      14 novembre 2011 à 3:30:50

      Bonsoir,

      Citation :

      J'essaye ici de transformer l'objet ImagedPlaylist en Playlist



      Je pense que Playlist(obj) ne fait pas ce que tu crois (un downcasting ?)

      Ce qui se passe avec Playlist(obj) c'est que tu créés un objet Playlist don l'attribut nom contiendra une référence vers obj.

      ipl = ImagedPlaylist('souchon')
      pl = Playlist(ipl)
      print(pl.nom)
      

      <__main__.ImagedPlaylist object at 0x8adde4c>

      Ce n'est donc pas un moyen de transformer ImagedPlaylist en Playlist.


      disclaimer: je n'ai jamais utilisé json avant ce qui suis peux donc fortement ressembler à de la bidouille. Attendre l'avis d'un bon :D

      Pour changer la classe d'un objet (aux yeux de isinstance), tu peux modifier son attribut __class__
      >>> ipl = ImagedPlaylist('plop')
      >>> isinstance(ipl, ImagedPlaylist)
      True
      >>> isinstance(ipl, Playlist)
      True
      >>> ipl.__class__ = Playlist
      >>> isinstance(ipl, ImagedPlaylist)
      False
      >>> isinstance(ipl, Playlist)
      True
      >>>
      


      Ensuite, en regardant dans ce fameux lib\json\encoder.py, on voit qu'une référence circulaire est détectée à partir du moment ou l'id d'un objet à déjà été rencontré :
      markerid = id(o)
      if markerid in markers:
          raise ValueError("Circular reference detected")
      


      Pour contourner ça on peut copier l'objet. En le copiant avant de modifier le __class__ ça permet de plus de ne pas le "corrompre" pour l'utilisateur de l'encodeur.

      Au final ça donnerait ça :

      import json
      
      class Playlist:
          def __init__(self, nom):
              self.nom = nom
              self.musiques = []
              
      class ImagedPlaylist(Playlist):
          def __init__(self, nom):
              Playlist.__init__(self, nom)
              self.image = ""
      
      import copy
      def serialiseur_perso(obj):
          if isinstance(obj, ImagedPlaylist):
              obj_cpy = copy.copy(obj)
              obj_cpy.__class__ = Playlist
              return {"__class__": "ImagedPlaylist",
                      'image' : obj.image,
                     "__parent__": obj_cpy}
          
          if isinstance(obj, Playlist):
              return {"__class__": "Playlist",
                      "nom": obj.nom,
                      "musiques": obj.musiques}
          
          raise TypeError(repr(pobjet) + " n'est pas sérialisable !")
      
      if __name__ == "__main__":
          print(json.dumps(ImagedPlaylist("MeshowRandom"), indent=4, default=serialiseur_perso))
      


      Et ça a l'air de donner ce qu'on attend :
      {
          "image": "", 
          "__class__": "ImagedPlaylist", 
          "__parent__": {
              "musiques": [], 
              "nom": "MeshowRandom", 
              "__class__": "Playlist"
          }
      }


      Il y a au moins un cas limite : l'héritage multiple.
      • Partager sur Facebook
      • Partager sur Twitter
      Anonyme
        4 décembre 2011 à 12:31:46

        Merci beaucoup, cela marche à merveille et je suis en train de le rajouter dans l'annexe de mon tuto. :)
        • Partager sur Facebook
        • Partager sur Twitter
          4 décembre 2011 à 14:53:25

          Citation : MicroJoe

          J'essaye ici de transformer l'objet ImagedPlaylist en Playlist pour que la fonction resérialise l'objet en tant que tel et l'ajoute dans l'entrée "__parent__" mais cela ne fonctionne pas.


          Tu es sûr que c'est nécessaire de garder ainsi la structure de l'objet ?

          Je ne vois pas dans la solution ci-dessus que tu garde une trace de la classe parente, si ce n'est que tu dissocie les attributs de la classe parente de ceux de la classe fille. Pourquoi alors ne pas faire simplement comme ça (attention, je ne m'y connais pas) :
          def serialiseur_perso(obj):
              if isinstance(obj, ImagedPlaylist):
                  return {"__class__": "ImagedPlaylist",
                         "__dict__": obj.__dict__}    
              elif isinstance(obj, Playlist):
                  return {"__class__": "Playlist",
                         "__dict__": obj.__dict__}  
              raise TypeError(repr(obj) + " n'est pas sérialisable !")
          


          {
              "__dict__": {
                  "musiques": [], 
                  "nom": "MeshowRandom", 
                  "image": ""
              }, 
              "__class__": "ImagedPlaylist"
          }
          
          • Partager sur Facebook
          • Partager sur Twitter
          Anonyme
            4 décembre 2011 à 15:09:08

            Mouais, je préfère distinguer la classe fille et la classe parent lors de l'enregistrement, après c'est vrai que ça peut dépendre des préférences.
            Je pense que les deux moyens se valent.
            • Partager sur Facebook
            • Partager sur Twitter

            [JSON] La sérialisation et l'héritage

            × 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