web-dev-qa-db-fra.com

Comment sérialiser des ensembles JSON?

J'ai un Python set qui contient des objets avec les méthodes __hash__ et __eq__ afin de s'assurer qu'aucun doublon ne soit inclus dans la collection.

J'ai besoin de json pour encoder ce résultat set, mais le passage même d'un set vide à la méthode json.dumps lève un TypeError.

  File "/usr/lib/python2.7/json/encoder.py", line 201, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/lib/python2.7/json/encoder.py", line 264, in iterencode
    return _iterencode(o, 0)
  File "/usr/lib/python2.7/json/encoder.py", line 178, in default
    raise TypeError(repr(o) + " is not JSON serializable")
TypeError: set([]) is not JSON serializable

Je sais que je peux créer une extension de la classe json.JSONEncoder avec une méthode personnalisée default, mais je ne sais même pas par où commencer pour convertir set. Devrais-je créer un dictionnaire à partir des valeurs set dans la méthode par défaut, puis renvoyer le codage à ce sujet? Idéalement, j'aimerais que la méthode par défaut puisse gérer tous les types de données sur lesquels le codeur d'origine s'étouffe (j'utilise Mongo en tant que source de données, les dates semblent donc générer cette erreur également)

Tout indice dans la bonne direction serait apprécié.

EDIT:

Merci d'avoir répondu! J'aurais peut-être dû être plus précis.

J'ai utilisé (et voté) les réponses ici pour contourner les limitations du set en cours de traduction, mais il y a des clés internes qui posent également un problème.

Les objets dans set sont des objets complexes qui se traduisent par __dict__, mais ils peuvent également contenir des valeurs pour leurs propriétés qui pourraient ne pas être éligibles pour les types de base du codeur json.

Il y a beaucoup de types différents entrant dans cette set, et le hachage calcule en principe un identifiant unique pour l'entité, mais dans le véritable esprit de NoSQL, il est impossible de savoir exactement ce que contient l'objet enfant.

Un objet peut contenir une valeur de date pour starts, tandis qu'un autre peut avoir un autre schéma n'incluant aucune clé contenant des objets "non primitifs".

C’est pourquoi la seule solution à laquelle je pouvais penser était d’étendre la méthode JSONEncoder pour remplacer la méthode default afin d’activer différents cas - mais je ne suis pas sûr de la marche à suivre et la documentation est ambiguë. . Dans les objets imbriqués, la valeur renvoyée par default est-elle associée à une clé ou s'agit-il simplement d'un include/discard générique qui examine l'objet entier? Comment cette méthode gère-t-elle les valeurs imbriquées? J'ai parcouru les questions précédentes et je n'arrive pas à trouver la meilleure approche pour l'encodage spécifique à chaque cas (ce qui semble malheureusement être ce que je vais devoir faire ici).

131
DeaconDesperado

La notation JSON n'a qu'une poignée de types de données natifs (objets, tableaux, chaînes, nombres, booléens et null), de sorte que tout ce qui est sérialisé en JSON doit être exprimé comme l'un de ces types.

Comme indiqué dans le documentation du module json , cette conversion peut être effectuée automatiquement par un JSONEncoder et JSONDecoder , mais vous abandonneriez une autre structure dont vous pourriez avoir besoin (si vous convertissez des ensembles en liste, vous perdez la possibilité de récupérer des listes régulières; si vous convertissez des ensembles en un dictionnaire utilisant dict.fromkeys(s) alors vous perdez la possibilité de récupérer des dictionnaires).

Une solution plus sophistiquée consiste à créer un type personnalisé pouvant coexister avec d'autres types JSON natifs. Cela vous permet de stocker des structures imbriquées comprenant des listes, des ensembles, des dessins, des décimales, des objets datetime, etc.:

from json import dumps, loads, JSONEncoder, JSONDecoder
import pickle

class PythonObjectEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (list, dict, str, unicode, int, float, bool, type(None))):
            return JSONEncoder.default(self, obj)
        return {'_python_object': pickle.dumps(obj)}

def as_python_object(dct):
    if '_python_object' in dct:
        return pickle.loads(str(dct['_python_object']))
    return dct

Voici un exemple de session montrant qu'il peut gérer des listes, des dict et des ensembles:

>>> data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'}, Decimal('3.14')]

>>> j = dumps(data, cls=PythonObjectEncoder)

>>> loads(j, object_hook=as_python_object)
[1, 2, 3, set(['knights', 'say', 'who', 'ni']), {u'key': u'value'}, Decimal('3.14')]

Alternativement, il peut être utile d'utiliser une technique de sérialisation plus générale, telle que YAML , Twisted Jelly , ou celle de Python pickle module . Ceux-ci prennent en charge une gamme beaucoup plus étendue de types de données.

103
Raymond Hettinger

Vous pouvez créer un encodeur personnalisé qui retourne un list lorsqu'il rencontre un set. Voici un exemple:

>>> import json
>>> class SetEncoder(json.JSONEncoder):
...    def default(self, obj):
...       if isinstance(obj, set):
...          return list(obj)
...       return json.JSONEncoder.default(self, obj)
... 
>>> json.dumps(set([1,2,3,4,5]), cls=SetEncoder)
'[1, 2, 3, 4, 5]'

Vous pouvez aussi détecter d’autres types. Si vous devez conserver le fait que la liste est en réalité un ensemble, vous pouvez utiliser un codage personnalisé. Quelque chose comme return {'type':'set', 'list':list(obj)} pourrait fonctionner.

Pour illustrer les types imbriqués, envisagez de sérialiser ceci:

>>> class Something(object):
...    pass
>>> json.dumps(set([1,2,3,4,5,Something()]), cls=SetEncoder)

Cela soulève l'erreur suivante:

TypeError: <__main__.Something object at 0x1691c50> is not JSON serializable

Cela indique que l'encodeur prendra le résultat list renvoyé et appellera le sérialiseur de manière récursive sur ses enfants. Pour ajouter un sérialiseur personnalisé à plusieurs types, procédez comme suit:

>>> class SetEncoder(json.JSONEncoder):
...    def default(self, obj):
...       if isinstance(obj, set):
...          return list(obj)
...       if isinstance(obj, Something):
...          return 'CustomSomethingRepresentation'
...       return json.JSONEncoder.default(self, obj)
... 
>>> json.dumps(set([1,2,3,4,5,Something()]), cls=SetEncoder)
'[1, 2, 3, 4, 5, "CustomSomethingRepresentation"]'
94
jterrace

Je me suis adapté solution de Raymond Hettinger à python 3.

Voici ce qui a changé:

  • unicode disparu
  • mis à jour l'appel à default des parents avec super()
  • utiliser base64 pour sérialiser le type bytes en str (car il semble que bytes dans python 3 ne puisse pas être converti en JSON)
from decimal import Decimal
from base64 import b64encode, b64decode
from json import dumps, loads, JSONEncoder
import pickle

class PythonObjectEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (list, dict, str, int, float, bool, type(None))):
            return super().default(obj)
        return {'_python_object': b64encode(pickle.dumps(obj)).decode('utf-8')}

def as_python_object(dct):
    if '_python_object' in dct:
        return pickle.loads(b64decode(dct['_python_object'].encode('utf-8')))
    return dct

data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'}, Decimal('3.14')]
j = dumps(data, cls=PythonObjectEncoder)
print(loads(j, object_hook=as_python_object))
# prints: [1, 2, 3, {'knights', 'who', 'say', 'ni'}, {'key': 'value'}, Decimal('3.14')]
5
simlmx

Seuls les dictionnaires, les listes et les types d'objet primitif (int, string, bool) sont disponibles en JSON.

5
Joseph Le Brech

Si vous avez seulement besoin d'encoder des ensembles, et non de simples objets Python, et que vous souhaitez le garder facilement lisible par l'homme, une version simplifiée de la réponse de Raymond Hettinger peut être utilisée:

import json
import collections

class JSONSetEncoder(json.JSONEncoder):
    """Use with json.dumps to allow Python sets to be encoded to JSON

    Example
    -------

    import json

    data = dict(aset=set([1,2,3]))

    encoded = json.dumps(data, cls=JSONSetEncoder)
    decoded = json.loads(encoded, object_hook=json_as_python_set)
    assert data == decoded     # Should assert successfully

    Any object that is matched by isinstance(obj, collections.Set) will
    be encoded, but the decoded value will always be a normal Python set.

    """

    def default(self, obj):
        if isinstance(obj, collections.Set):
            return dict(_set_object=list(obj))
        else:
            return json.JSONEncoder.default(self, obj)

def json_as_python_set(dct):
    """Decode json {'_set_object': [1,2,3]} to set([1,2,3])

    Example
    -------
    decoded = json.loads(encoded, object_hook=json_as_python_set)

    Also see :class:`JSONSetEncoder`

    """
    if '_set_object' in dct:
        return set(dct['_set_object'])
    return dct
3
NeilenMarais