Comment sérialiser des ensembles JSON?

149

J'ai un Python setqui contient des objets avec __hash__et des __eq__méthodes afin de m'assurer qu'aucun doublon n'est inclus dans la collection.

J'ai besoin de json encoder ce résultat set, mais passer même un vide setà la json.dumpsméthode 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 pour la json.JSONEncoderclasse qui a une defaultméthode personnalisée , mais je ne sais même pas par où commencer la conversion via le set. Dois-je créer un dictionnaire à partir des setvaleurs de la méthode par défaut, puis renvoyer l'encodage à ce sujet? Idéalement, j'aimerais que la méthode par défaut soit capable de gérer tous les types de données sur lesquels l'encodeur d'origine s'étouffe (j'utilise Mongo comme source de données, donc les dates semblent également soulever cette erreur)

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

ÉDITER:

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 limites de la settraduction, mais il y a aussi des clés internes qui posent un problème.

Les objets dans le setsont des objets complexes qui se traduisent en __dict__, mais ils peuvent également contenir des valeurs pour leurs propriétés qui pourraient ne pas être éligibles pour les types de base dans l'encodeur json.

Il y a beaucoup de types différents qui entrent en jeu set, et le hachage calcule essentiellement un identifiant unique pour l'entité, mais dans le véritable esprit de NoSQL, il est impossible de dire 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 qui n'inclut aucune clé contenant des objets "non primitifs".

C'est pourquoi la seule solution à laquelle je pouvais penser était d'étendre le JSONEncoderpour remplacer la defaultméthode pour activer différents cas - mais je ne sais pas comment s'y prendre et la documentation est ambiguë. Dans les objets imbriqués, la valeur renvoyée par defaultpasse-t-elle par clé, ou s'agit-il simplement d'une inclusion / suppression générique qui regarde l'objet entier? Comment cette méthode prend-elle en charge 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 au cas (ce qui semble malheureusement être ce que je vais devoir faire ici).

DeaconDesperado
la source
3
pourquoi dicts? Je pense que vous voulez juste faire un listhors de l'ensemble et ensuite le passer à l'encodeur ... par exemple:encode(list(myset))
Constantinius
2
Au lieu d'utiliser JSON, vous pouvez utiliser YAML (JSON est essentiellement un sous-ensemble de YAML).
Paolo Moretti
@PaoloMoretti: Cela apporte-t-il un avantage? Je ne pense pas que les ensembles font partie des types de données universellement pris en charge de YAML, et ils sont moins largement pris en charge, en particulier en ce qui concerne les API.
@PaoloMoretti Merci pour votre contribution, mais l'interface de l'application nécessite JSON comme type de retour et cette exigence est à toutes fins fixe.
DeaconDesperado
2
@delnan Je suggérais YAML car il a un support natif pour les ensembles et les dates .
Paolo Moretti

Réponses:

117

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

Comme indiqué dans la documentation du module json , cette conversion peut être effectuée automatiquement par un JSONEncoder et JSONDecoder , mais vous abandonneriez alors 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; si vous convertissez des ensembles en dictionnaire en utilisant, dict.fromkeys(s)vous perdez la possibilité de récupérer des dictionnaires)

Une solution plus sophistiquée consiste à créer un type personnalisé qui peut coexister avec d'autres types JSON natifs. Cela vous permet de stocker des structures imbriquées qui incluent des listes, des ensembles, des dictées, 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 dictionnaires 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 le module pickle de Python . Celles-ci prennent chacune en charge une gamme beaucoup plus étendue de types de données.

Raymond Hettinger
la source
11
C'est le premier que j'ai entendu dire que YAML est plus polyvalent que JSON ... o_O
Karl Knechtel
13
@KarlKnechtel YAML est un sur-ensemble de JSON (presque). Il ajoute également des balises pour les données binaires, les ensembles, les cartes ordonnées et les horodatages. Prendre en charge plus de types de données est ce que j'entendais par «usage plus général». Vous semblez utiliser l'expression «usage général» dans un sens différent.
Raymond Hettinger
4
N'oubliez pas également jsonpickle , qui est destiné à être une bibliothèque généralisée pour le pickling d'objets Python en JSON, comme le suggère cette réponse.
Jason R. Coombs
4
Depuis la version 1.2, YAML est un sur-ensemble strict de JSON. Tout JSON légal est désormais YAML légal. yaml.org/spec/1.2/spec.html
steveha
2
cet exemple de code importe JSONDecodermais ne l'utilise pas
watsonic
115

Vous pouvez créer un encodeur personnalisé qui renvoie a listlorsqu'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 également détecter d'autres types de cette façon. Si vous devez conserver que la liste était en fait un ensemble, vous pouvez utiliser un encodage personnalisé. Quelque chose comme ça 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 listrésultat retourné et appellera récursivement le sérialiseur sur ses enfants. Pour ajouter un sérialiseur personnalisé pour plusieurs types, vous pouvez le faire:

>>> 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"]'
jterrace
la source
Merci, j'ai édité la question pour mieux préciser que c'était le type de chose dont j'avais besoin. Ce que je n'arrive pas à comprendre, c'est comment cette méthode gérera les objets imbriqués. Dans votre exemple, la valeur de retour est list for set, mais que se passe-t-il si l'objet passé était un ensemble avec des dates (un autre mauvais type de données) à l'intérieur? Dois-je explorer les clés dans la méthode par défaut elle-même? Merci beaucoup!
DeaconDesperado
1
Je pense que le module JSON gère les objets imbriqués pour vous. Une fois qu'il a récupéré la liste, il parcourra les éléments de la liste en essayant de coder chacun d'eux. Si l'un d'entre eux est une date, la defaultfonction sera appelée à nouveau, cette fois objétant un objet date, il vous suffit donc de la tester et de renvoyer une représentation de date.
jterrace
Ainsi, la méthode par défaut pourrait éventuellement s'exécuter plusieurs fois pour tout objet qui lui est passé, puisqu'elle examinera également les clés individuelles une fois qu'elle sera "listée"?
DeaconDesperado
En quelque sorte, il ne sera pas appelé plusieurs fois pour le même objet, mais il peut se répéter chez les enfants. Voir la réponse mise à jour.
jterrace
A travaillé exactement comme vous l'avez décrit. Je dois encore comprendre certains des défauts, mais la plupart sont probablement des choses qui peuvent être remodelées. Merci beaucoup pour vos conseils!
DeaconDesperado
7

J'ai adapté la solution de Raymond Hettinger à python 3.

Voici ce qui a changé:

  • unicode disparu
  • a mis à jour l'appel aux parents defaultavecsuper()
  • en utilisant base64pour sérialiser le bytestype en str(car il semble qu'en bytespython 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')]
simlmx
la source
4
Le code affiché à la fin de cette réponse à une question connexe accomplit la même chose en décodant et en encodant [seulement] les json.dumps()retours d' objets octets vers / depuis 'latin1', en sautant les base64choses qui ne sont pas nécessaires.
martineau
6

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

Joseph Le Brech
la source
5
"Type d'objet primitif" n'a aucun sens quand on parle de Python. "Objet intégré" a plus de sens, mais est ici trop large (pour commencer: il comprend des dictionnaires, des listes et aussi des ensembles). (La terminologie JSON peut être différente cependant.)
string number object array true false null
Joseph Le Brech
6

Vous n'avez pas besoin de créer une classe d'encodeur personnalisée pour fournir la defaultméthode - elle peut être transmise en tant qu'argument de mot-clé:

import json

def serialize_sets(obj):
    if isinstance(obj, set):
        return list(obj)

    return obj

json_str = json.dumps(set([1,2,3]), default=serialize_sets)
print(json_str)

aboutit à [1, 2, 3]toutes les versions de Python prises en charge.

Antti Haapala
la source
4

Si vous avez seulement besoin d'encoder des ensembles, pas des objets Python généraux, et que vous voulez 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
NeilenMarais
la source
1

Si vous avez besoin d'un simple vidage rapide et que vous ne souhaitez pas implémenter un encodeur personnalisé. Vous pouvez utiliser les éléments suivants:

json_string = json.dumps(data, iterable_as_array=True)

Cela convertira tous les ensembles (et autres itérables) en tableaux. Méfiez-vous simplement que ces champs resteront des tableaux lorsque vous analyserez le json. Si vous souhaitez conserver les types, vous devez écrire un encodeur personnalisé.

David Novák
la source
7
Quand j'essaye ceci, j'obtiens: TypeError: __init __ () a obtenu un argument de mot-clé inattendu 'iterable_as_array'
atm
Vous devez installer simplejson
JerryBringer
import simplejson en tant que json, puis json_string = json.dumps (data, iterable_as_array = True) fonctionne bien en Python 3.6
fraverta
1

Un inconvénient de la solution acceptée est que sa sortie est très spécifique à Python. C'est-à-dire que sa sortie json brute ne peut pas être observée par un humain ou chargée par un autre langage (par exemple, javascript). exemple:

db = {
        "a": [ 44, set((4,5,6)) ],
        "b": [ 55, set((4,3,2)) ]
        }

j = dumps(db, cls=PythonObjectEncoder)
print(j)

Vous obtiendrez:

{"a": [44, {"_python_object": "gANjYnVpbHRpbnMKc2V0CnEAXXEBKEsESwVLBmWFcQJScQMu"}], "b": [55, {"_python_object": "gANjYnVpbHRpbnMKc2V0CnEAXXEBKEsCSwNLBGWFcQJScQMu"}]}

Je peux proposer une solution qui rétrograde l'ensemble en un dict contenant une liste à la sortie, et le retourne à un ensemble lorsqu'il est chargé en python en utilisant le même encodeur, préservant ainsi l'observabilité et l'agnosticisme du langage:

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)
        elif isinstance(obj, set):
            return {"__set__": list(obj)}
        return {'_python_object': b64encode(pickle.dumps(obj)).decode('utf-8')}

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

db = {
        "a": [ 44, set((4,5,6)) ],
        "b": [ 55, set((4,3,2)) ]
        }

j = dumps(db, cls=PythonObjectEncoder)
print(j)
ob = loads(j)
print(ob["a"])

Ce qui vous permet:

{"a": [44, {"__set__": [4, 5, 6]}], "b": [55, {"__set__": [2, 3, 4]}]}
[44, {'__set__': [4, 5, 6]}]

Notez que la sérialisation d'un dictionnaire qui a un élément avec une clé "__set__"cassera ce mécanisme. Ainsi __set__est maintenant devenu une dictclé réservée . Évidemment, n'hésitez pas à utiliser une autre clé plus obscurcie.

sagisme
la source