69 votes

Comment changer le comportement de codage JSON pour un objet python sérialisable?

Il est facile de changer le format d'un objet qui n'est pas la sérialisation JSON par exemple de type datetime.datetime.

Mon exigence, à des fins de débogage, est de modifier la façon dont certains objets personnalisés étendu à partir de la base, comme l' dict et list , être sérialisé dans un format json . Code :

import datetime
import json

def json_debug_handler(obj):
    print("object received:")
    print type(obj)
    print("\n\n")
    if  isinstance(obj, datetime.datetime):
        return obj.isoformat()
    elif isinstance(obj,mDict):
        return {'orig':obj , 'attrs': vars(obj)}
    elif isinstance(obj,mList):
        return {'orig':obj, 'attrs': vars(obj)}
    else:
        return None


class mDict(dict):
    pass


class mList(list):
    pass


def test_debug_json():
    games = mList(['mario','contra','tetris'])
    games.src = 'console'
    scores = mDict({'dp':10,'pk':45})
    scores.processed = "unprocessed"
    test_json = { 'games' : games , 'scores' : scores , 'date': datetime.datetime.now() }
    print(json.dumps(test_json,default=json_debug_handler))

if __name__ == '__main__':
    test_debug_json()

DÉMO : http://ideone.com/hQJnLy

Sortie:

{"date": "2013-05-07T01:03:13.098727", "games": ["mario", "contra", "tetris"], "scores": {"pk": 45, "dp": 10}}

Sortie désirée:

{"date": "2013-05-07T01:03:13.098727", "games": { "orig": ["mario", "contra", "tetris"] ,"attrs" : { "src":"console"}} , "scores": { "orig": {"pk": 45, "dp": 10},"attrs": "processed":"unprocessed }}

L' default gestionnaire fonctionne pas pour les objets sérialisables ? Si non, comment puis-je remplacer ce, sans l'ajout de la méthode toJSON méthodes de l'étendue des classes ?

Il y a aussi cette version de JSON codeur qui ne fonctionne pas :

class JsonDebugEncoder(json.JSONEncoder):
    def default(self,obj):
        if  isinstance(obj, datetime.datetime):
            return obj.isoformat()
        elif isinstance(obj,mDict):
            return {'orig':obj , 'attrs': vars(obj)}
        elif isinstance(obj,mList):
            return {'orig':obj, 'attrs': vars(obj)}
        else:
            return json.JSONEncoder.default(self, obj)

Si il y a un hack avec pickle,__getstate__,__setstate__,, puis à l'aide de json.les vidages de plus de cornichon.les charges de l'objet , je suis ouvert à ça aussi, j'ai essayé , mais cela ne fonctionne pas.

27voto

FastTurtle Points 1345

Il semble que pour obtenir le comportement que vous souhaitez, avec les restrictions données, vous aurez à plonger dans l' JSONEncoder classe un peu. Ci-dessous, j'ai écrit une coutume JSONEncoder qui remplace l' iterencode méthode pour passer une coutume isinstance méthode de _make_iterencode. Ce n'est pas le plus propre du monde, mais qui semble être la meilleure étant donné les options et il ne cesse de personnalisation à un minimum.

# customencoder.py
from json.encoder import (_make_iterencode, JSONEncoder,
                          encode_basestring_ascii, FLOAT_REPR, INFINITY,
                          c_make_encoder, encode_basestring)


class CustomObjectEncoder(JSONEncoder):

    def iterencode(self, o, _one_shot=False):
        """
        Most of the original method has been left untouched.

        _one_shot is forced to False to prevent c_make_encoder from
        being used. c_make_encoder is a funcion defined in C, so it's easier
        to avoid using it than overriding/redefining it.

        The keyword argument isinstance for _make_iterencode has been set
        to self.isinstance. This allows for a custom isinstance function
        to be defined, which can be used to defer the serialization of custom
        objects to the default method.
        """
        # Force the use of _make_iterencode instead of c_make_encoder
        _one_shot = False

        if self.check_circular:
            markers = {}
        else:
            markers = None
        if self.ensure_ascii:
            _encoder = encode_basestring_ascii
        else:
            _encoder = encode_basestring
        if self.encoding != 'utf-8':
            def _encoder(o, _orig_encoder=_encoder, _encoding=self.encoding):
                if isinstance(o, str):
                    o = o.decode(_encoding)
                return _orig_encoder(o)

        def floatstr(o, allow_nan=self.allow_nan,
                     _repr=FLOAT_REPR, _inf=INFINITY, _neginf=-INFINITY):
            if o != o:
                text = 'NaN'
            elif o == _inf:
                text = 'Infinity'
            elif o == _neginf:
                text = '-Infinity'
            else:
                return _repr(o)

            if not allow_nan:
                raise ValueError(
                    "Out of range float values are not JSON compliant: " +
                    repr(o))

            return text

        # Instead of forcing _one_shot to False, you can also just
        # remove the first part of this conditional statement and only
        # call _make_iterencode
        if (_one_shot and c_make_encoder is not None
                and self.indent is None and not self.sort_keys):
            _iterencode = c_make_encoder(
                markers, self.default, _encoder, self.indent,
                self.key_separator, self.item_separator, self.sort_keys,
                self.skipkeys, self.allow_nan)
        else:
            _iterencode = _make_iterencode(
                markers, self.default, _encoder, self.indent, floatstr,
                self.key_separator, self.item_separator, self.sort_keys,
                self.skipkeys, _one_shot, isinstance=self.isinstance)
        return _iterencode(o, 0)

Vous pouvez maintenant créer des sous-classes CustomObjectEncoder sorte correctement sérialise vos objets personnalisés. L' CustomObjectEncoder peut aussi faire des trucs cool de traiter des objets imbriqués.

# test.py
import json
import datetime
from customencoder import CustomObjectEncoder


class MyEncoder(CustomObjectEncoder):

    def isinstance(self, obj, cls):
        if isinstance(obj, (mList, mDict)):
            return False
        return isinstance(obj, cls)

    def default(self, obj):
        """
        Defines custom serialization.

        To avoid circular references, any object that will always fail
        self.isinstance must be converted to something that is
        deserializable here.
        """
        if isinstance(obj, datetime.datetime):
            return obj.isoformat()
        elif isinstance(obj, mDict):
            return {"orig": dict(obj), "attrs": vars(obj)}
        elif isinstance(obj, mList):
            return {"orig": list(obj), "attrs": vars(obj)}
        else:
            return None


class mList(list):
    pass


class mDict(dict):
    pass


def main():
    zelda = mList(['zelda'])
    zelda.src = "oldschool"
    games = mList(['mario', 'contra', 'tetris', zelda])
    games.src = 'console'
    scores = mDict({'dp': 10, 'pk': 45})
    scores.processed = "unprocessed"
    test_json = {'games': games, 'scores': scores,
                 'date': datetime.datetime.now()}
    print(json.dumps(test_json, cls=MyEncoder))

if __name__ == '__main__':
    main()

11voto

Roy Nieterau Points 346

La réponse par FastTurtle pourrait être une bien meilleure solution.

Voici quelque chose de proche de ce que vous voulez basé sur la technique, comme expliqué dans ma question/réponse: Primordial imbriquée encodage JSON de héritées par défaut pris en charge les objets comme dict, liste

import json
import datetime


class mDict(dict):
    pass


class mList(list):
    pass


class JsonDebugEncoder(json.JSONEncoder):
    def _iterencode(self, o, markers=None):
        if isinstance(o, mDict):
            yield '{"__mDict__": '
            # Encode dictionary
            yield '{"orig": '
            for chunk in super(JsonDebugEncoder, self)._iterencode(o, markers):
                yield chunk
            yield ', '
            # / End of Encode dictionary
            # Encode attributes
            yield '"attr": '
            for key, value in o.__dict__.iteritems():
                yield '{"' + key + '": '
                for chunk in super(JsonDebugEncoder, self)._iterencode(value, markers):
                    yield chunk
                yield '}'
            yield '}'
            # / End of Encode attributes
            yield '}'
        elif isinstance(o, mList):
            yield '{"__mList__": '
            # Encode list
            yield '{"orig": '
            for chunk in super(JsonDebugEncoder, self)._iterencode(o, markers):
                yield chunk
            yield ', '
            # / End of Encode list
            # Encode attributes
            yield '"attr": '
            for key, value in o.__dict__.iteritems():
                yield '{"' + key + '": '
                for chunk in super(JsonDebugEncoder, self)._iterencode(value, markers):
                    yield chunk
                yield '}'
            yield '}'
            # / End of Encode attributes
            yield '}'
        else:
            for chunk in super(JsonDebugEncoder, self)._iterencode(o, markers=markers):
                yield chunk

    def default(self, obj):
        if isinstance(obj, datetime.datetime):
            return obj.isoformat()


class JsonDebugDecoder(json.JSONDecoder):
    def decode(self, s):
        obj = super(JsonDebugDecoder, self).decode(s)
        obj = self.recursiveObjectDecode(obj)
        return obj

    def recursiveObjectDecode(self, obj):
        if isinstance(obj, dict):
            decoders = [("__mList__", self.mListDecode),
                        ("__mDict__", self.mDictDecode)]
            for placeholder, decoder in decoders:
                if placeholder in obj:                  # We assume it's supposed to be converted
                    return decoder(obj[placeholder])
                else:
                    for k in obj:
                        obj[k] = self.recursiveObjectDecode(obj[k])
        elif isinstance(obj, list):
            for x in range(len(obj)):
                obj[x] = self.recursiveObjectDecode(obj[x])
        return obj

    def mDictDecode(self, o):
        res = mDict()
        for key, value in o['orig'].iteritems():
            res[key] = self.recursiveObjectDecode(value)
        for key, value in o['attr'].iteritems():
            res.__dict__[key] = self.recursiveObjectDecode(value)
        return res

    def mListDecode(self, o):
        res = mList()
        for value in o['orig']:
            res.append(self.recursiveObjectDecode(value))
        for key, value in o['attr'].iteritems():
            res.__dict__[key] = self.recursiveObjectDecode(value)
        return res


def test_debug_json():
    games = mList(['mario','contra','tetris'])
    games.src = 'console'
    scores = mDict({'dp':10,'pk':45})
    scores.processed = "unprocessed"
    test_json = { 'games' : games, 'scores' : scores ,'date': datetime.datetime.now() }
    jsonDump = json.dumps(test_json, cls=JsonDebugEncoder)
    print jsonDump
    test_pyObject = json.loads(jsonDump, cls=JsonDebugDecoder)
    print test_pyObject

if __name__ == '__main__':
    test_debug_json()

Il en résulte:

{"date": "2013-05-06T22:28:08.967000", "games": {"__mList__": {"orig": ["mario", "contra", "tetris"], "attr": {"src": "console"}}}, "scores": {"__mDict__": {"orig": {"pk": 45, "dp": 10}, "attr": {"processed": "unprocessed"}}}}

De cette façon, vous pouvez l'encoder et de décoder de retour à l'objet python elle est venue.

EDIT:

Voici une version qui en fait l'encode à la sortie que vous voulais et peut le décoder aussi bien. Chaque fois qu'un dictionnaire contenant 'orig' et 'attr' il va vérifier si "orig" contient un dictionnaire ou une liste, si oui il sera respectivement de convertir le retour à la mDict ou mList.

import json
import datetime


class mDict(dict):
    pass


class mList(list):
    pass


class JsonDebugEncoder(json.JSONEncoder):
    def _iterencode(self, o, markers=None):
        if isinstance(o, mDict):    # Encode mDict
            yield '{"orig": '
            for chunk in super(JsonDebugEncoder, self)._iterencode(o, markers):
                yield chunk
            yield ', '
            yield '"attr": '
            for key, value in o.__dict__.iteritems():
                yield '{"' + key + '": '
                for chunk in super(JsonDebugEncoder, self)._iterencode(value, markers):
                    yield chunk
                yield '}'
            yield '}'
            # / End of Encode attributes
        elif isinstance(o, mList):    # Encode mList
            yield '{"orig": '
            for chunk in super(JsonDebugEncoder, self)._iterencode(o, markers):
                yield chunk
            yield ', '
            yield '"attr": '
            for key, value in o.__dict__.iteritems():
                yield '{"' + key + '": '
                for chunk in super(JsonDebugEncoder, self)._iterencode(value, markers):
                    yield chunk
                yield '}'
            yield '}'
        else:
            for chunk in super(JsonDebugEncoder, self)._iterencode(o, markers=markers):
                yield chunk

    def default(self, obj):
        if isinstance(obj, datetime.datetime):    # Encode datetime
            return obj.isoformat()


class JsonDebugDecoder(json.JSONDecoder):
    def decode(self, s):
        obj = super(JsonDebugDecoder, self).decode(s)
        obj = self.recursiveObjectDecode(obj)
        return obj

    def recursiveObjectDecode(self, obj):
        if isinstance(obj, dict):
            if "orig" in obj and "attr" in obj and isinstance(obj["orig"], list):
                return self.mListDecode(obj)
            elif "orig" in obj and "attr" in obj and isinstance(obj['orig'], dict):
                return self.mDictDecode(obj)
            else:
                for k in obj:
                    obj[k] = self.recursiveObjectDecode(obj[k])
        elif isinstance(obj, list):
            for x in range(len(obj)):
                obj[x] = self.recursiveObjectDecode(obj[x])
        return obj

    def mDictDecode(self, o):
        res = mDict()
        for key, value in o['orig'].iteritems():
            res[key] = self.recursiveObjectDecode(value)
        for key, value in o['attr'].iteritems():
            res.__dict__[key] = self.recursiveObjectDecode(value)
        return res

    def mListDecode(self, o):
        res = mList()
        for value in o['orig']:
            res.append(self.recursiveObjectDecode(value))
        for key, value in o['attr'].iteritems():
            res.__dict__[key] = self.recursiveObjectDecode(value)
        return res


def test_debug_json():
    games = mList(['mario','contra','tetris'])
    games.src = 'console'
    scores = mDict({'dp':10,'pk':45})
    scores.processed = "unprocessed"
    test_json = { 'games' : games, 'scores' : scores ,'date': datetime.datetime.now() }
    jsonDump = json.dumps(test_json, cls=JsonDebugEncoder)
    print jsonDump
    test_pyObject = json.loads(jsonDump, cls=JsonDebugDecoder)
    print test_pyObject
    print test_pyObject['games'].src

if __name__ == '__main__':
    test_debug_json()

Voici quelques infos sur la sortie:

# Encoded
{"date": "2013-05-06T22:41:35.498000", "games": {"orig": ["mario", "contra", "tetris"], "attr": {"src": "console"}}, "scores": {"orig": {"pk": 45, "dp": 10}, "attr": {"processed": "unprocessed"}}}

# Decoded ('games' contains the mList with the src attribute and 'scores' contains the mDict processed attribute)
# Note that printing the python objects doesn't directly show the processed and src attributes, as seen below.
{u'date': u'2013-05-06T22:41:35.498000', u'games': [u'mario', u'contra', u'tetris'], u'scores': {u'pk': 45, u'dp': 10}}

Désolé pour les mauvaises conventions de nommage, c'est une installation rapide. ;)

Remarque: La date à ne pas avoir décodé retour à l'python représentation. La mise en œuvre qui pourrait être fait en vérifiant pour tout dict clé qui est appelée "date" et contient une chaîne valide représentation d'un datetime.

7voto

James Holderness Points 15849

Comme d'autres l'ont souligné déjà, le gestionnaire par défaut ne reçoit appelé pour des valeurs qui ne sont pas l'un des types reconnus. Ma proposition de solution à ce problème est de préparer l'objet à sérialiser, recursing sur les listes, les tuples et les dictionnaires, mais d'emballage tout autre valeur dans une classe personnalisée.

Quelque chose comme ceci:

def debug(obj):
    class Debug:
        def __init__(self,obj):
            self.originalObject = obj
    if obj.__class__ == list:
        return [debug(item) for item in obj]
    elif obj.__class__ == tuple:
        return (debug(item) for item in obj)
    elif obj.__class__ == dict:
        return dict((key,debug(obj[key])) for key in obj)
    else:
        return Debug(obj)

Vous appelez cette fonction, avant de passer votre objet json.décharges, comme ceci:

test_json = debug(test_json)
print(json.dumps(test_json,default=json_debug_handler))

Notez que ce code est de vérifier pour les objets dont la classe correspond exactement à une liste, tuple ou de dictionnaire, de sorte que tous les objets qui sont prolongés à partir de ces types seront enveloppés plutôt que de les analyser. En conséquence, les listes, les tuples et les dictionnaires sera sérialisé comme d'habitude, mais toutes les autres valeurs seront transmises au gestionnaire par défaut.

Le résultat final de tout cela, c'est que chaque valeur qui atteint le gestionnaire par défaut est garanti d'être enveloppé dans une de ces classes Debug. Donc, la première chose que vous allez vouloir faire est d'extraire l'objet d'origine, comme ceci:

obj = obj.originalObject

Vous pouvez ensuite vérifier l'origine de l'objet type de poignée et quel que soit le type de besoin de traitement spécial. Pour tout le reste, il suffit de retourner l'objet d'origine (donc le dernier retour de la fonction doit être return obj pas return None).

def json_debug_handler(obj):
    obj = obj.originalObject      # Add this line
    print("object received:")
    print type(obj)
    print("\n\n")
    if  isinstance(obj, datetime.datetime):
        return obj.isoformat()
    elif isinstance(obj,mDict):
        return {'orig':obj, 'attrs': vars(obj)}
    elif isinstance(obj,mList):
        return {'orig':obj, 'attrs': vars(obj)}
    else:
        return obj                # Change this line

Notez que ce code ne vérifie pas pour les valeurs qui ne sont pas sérialisables. Ces tombera final, return obj, puis sera rejetée par le sérialiseur et transmises au gestionnaire par défaut de nouveau - mais cette fois sans le Débogage de wrapper.

Si vous avez besoin pour faire face à ce scénario, vous pouvez ajouter une case en haut de la fonction comme ceci:

if not hasattr(obj, 'originalObject'):
    return None

Ideone démo: http://ideone.com/tOloNq

5voto

Chris Johnson Points 2887

La valeur par défaut de la fonction n'est appelée que lorsque le nœud de l'objet de dumping n'est pas nativement serializable, et votre mDict classes de sérialiser comme-est. Voici une petite démo qui montre que lorsque l'option par défaut est appelé et quand pas:

import json

def serializer(obj):
    print 'serializer called'
    return str(obj)

class mDict(dict):
    pass

class mSet(set):
    pass

d = mDict(dict(a=1))
print json.dumps(d, default=serializer)

s = mSet({1, 2, 3,})
print json.dumps(s, default=serializer)

Et la sortie:

{"a": 1}
serializer called
"mSet([1, 2, 3])"

Notez que les jeux ne sont pas nativement serializable, mais dicts sont.

Depuis votre m___ les classes sont sérialisables, votre gestionnaire n'est jamais appelée.

Mise à jour #1 -----

Vous pouvez changer JSON code de l'encodeur. Les détails de la procédure à suivre dépendent de l'implémentation JSON que vous utilisez. Par exemple, dans simplejson, le code correspondant est présent, dans encode.py:

def _iterencode(o, _current_indent_level):
    ...
        for_json = _for_json and getattr(o, 'for_json', None)
        if for_json and callable(for_json):
            ...
        elif isinstance(o, list):
            ...
        else:
            _asdict = _namedtuple_as_object and getattr(o, '_asdict', None)
            if _asdict and callable(_asdict):
                for chunk in _iterencode_dict(_asdict(),
                        _current_indent_level):
                    yield chunk
            elif (_tuple_as_array and isinstance(o, tuple)):
                ...
            elif isinstance(o, dict):
                ...
            elif _use_decimal and isinstance(o, Decimal):
                ...
            else:
                ...
                o = _default(o)
                for chunk in _iterencode(o, _current_indent_level):
                    yield chunk
                ...

En d'autres termes, il y a un câblé comportement qui appelle par défaut uniquement lorsque le nœud en cours de codage n'est pas l'un de l'reconnu les types de base. Vous pourriez remplacer ce de plusieurs manières:

1 -- sous-classe JSONEncoder comme vous l'avez fait ci-dessus, mais d'ajouter un paramètre à son initialiseur qui spécifie la fonction à être utilisé à la place de la norme _make_iterencode, dans laquelle vous pouvez ajouter un test qui permettrait d'appel par défaut pour les classes qui répondent à vos critères. C'est un propre approche, car vous n'êtes pas changer le module JSON, mais vous réitérant beaucoup de code à partir de l'original _make_iterencode. (D'autres variantes de cette approche comprennent monkeypatching _make_iterencode ou de ses sous-fonction _iterencode_dict).

2-modifier le JSON module de source, et l'utilisation de l' __debug__ constante pour un changement de comportement:

def _iterencode(o, _current_indent_level):
    ...
        for_json = _for_json and getattr(o, 'for_json', None)
        if for_json and callable(for_json):
            ...
        elif isinstance(o, list):
            ...
        ## added code below
        elif __debug__:
            o = _default(o)
            for chunk in _iterencode(o, _current_indent_level):
                yield chunk
        ## added code above
        else:
            ...

Idéalement, le JSONEncoder classe serait de fournir un paramètre pour spécifier "utiliser par défaut pour tous les types", mais il ne le fait pas. Le ci-dessus est un simple changement d'heure qui fait ce que vous cherchez.

2voto

marcoseu Points 2230

Pourquoi ne pouvez-vous pas simplement créer un nouveau type d'objet à transmettre au codeur? Essayer:

 class MStuff(object):
    def __init__(self, content):
        self.content = content

class mDict(MStuff):
    pass

class mList(MStuff):
    pass

def json_debug_handler(obj):
    print("object received:")
    print(type(obj))
    print("\n\n")
    if  isinstance(obj, datetime.datetime):
        return obj.isoformat()
    elif isinstance(obj,MStuff):
        attrs = {}
        for key in obj.__dict__:
            if not ( key.startswith("_") or key == "content"):
                attrs[key] = obj.__dict__[key]

        return {'orig':obj.content , 'attrs': attrs}
    else:
        return None
 

Vous pouvez ajouter une validation sur mDict et mList si vous le souhaitez.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X