¿Cómo cambiar el comportamiento de encoding json para un objeto de Python serializable?

Es fácil cambiar el formato de un objeto que no se puede serializar mediante JSON, por ejemplo, datetime.datetime.

Mi requisito, para propósitos de depuración, es alterar la forma en que algunos objetos personalizados extendidos desde los objetos base como dict y list , se serializan en formato json. Código:

 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() 

DEMO: http://ideone.com/hQJnLy

Salida:

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

Salida deseada:

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

¿El controlador default no funciona para los objetos serializables? Si no, ¿cómo puedo anular esto, sin agregar métodos de JSON a las clases extendidas?

Además, existe esta versión del codificador JSON que no funciona:

 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 hay un hack con pickle,__getstate__,__setstate__, y luego usar json.dumps sobre el objeto pickle.loads, estoy abierto a eso también, lo intenté, pero no funcionó.

Parece que para lograr el comportamiento que desea, con las restricciones dadas, tendrá que ahondar un poco en la clase JSONEncoder . A continuación, he escrito un JSONEncoder personalizado que invalida el método iterencode para pasar un método personalizado de isinstance a _make_iterencode . No es lo más limpio del mundo, pero parece ser lo mejor dadas las opciones y mantiene la personalización al mínimo.

 # 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) 

Ahora puede subclasificar el CustomObjectEncoder para que CustomObjectEncoder correctamente sus objetos personalizados. El CustomObjectEncoder también puede hacer cosas geniales como manejar objetos nesteds.

 # 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() 

La respuesta de FastTurtle podría ser una solución mucho más limpia.

Aquí hay algo parecido a lo que desea según la técnica que se explica en mi pregunta / respuesta: Anulación de la encoding JSON anidada de los objetos heredados predeterminados admitidos como dict, list

 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() 

Esto resulta en:

 {"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 esta manera, puedes codificarlo y decodificarlo de nuevo al objeto python del que proviene.

EDITAR:

Aquí hay una versión que realmente lo codifica a la salida que desea y también puede decodificarla. Siempre que un diccionario contenga ‘orig’ y ‘attr’ verificará si ‘orig’ contiene un diccionario o una lista, si es así convertirá el objeto de nuevo a mDict o 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() 

Aquí hay más información sobre la salida:

 # 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}} 

Lo siento por cualquier mala convención de nombres, es una configuración rápida. 😉

Nota: el datetime no se decodifica de nuevo a la representación de python. La implementación podría hacerse revisando cualquier clave de dictado que se llame “fecha” y contenga una representación de cadena válida de una fecha y hora.

Como otros ya han señalado, el controlador predeterminado solo recibe llamadas para valores que no son uno de los tipos reconocidos. Mi solución sugerida para este problema es preprocesar el objeto que desea serializar, recurriendo sobre listas, tuplas y diccionarios, pero envolviendo todos los demás valores en una clase personalizada.

Algo como esto:

 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) 

Deberías llamar a esta función, antes de pasar tu objeto a json.dumps , como esto:

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

Tenga en cuenta que este código está buscando objetos cuya clase coincide exactamente con una lista, tupla o diccionario, por lo que cualquier objeto personalizado que se extienda desde esos tipos se ajustará en lugar de analizarlo. Como resultado, las listas regulares, las tuplas y los diccionarios se serializarán como de costumbre, pero todos los demás valores se pasarán al controlador predeterminado.

El resultado final de todo esto es que se garantiza que todo valor que llegue al controlador predeterminado se incluirá en una de estas clases de depuración. Entonces, lo primero que querrás hacer es extraer el objeto original, de esta manera:

 obj = obj.originalObject 

A continuación, puede verificar el tipo de objeto original y manejar los tipos que necesiten un procesamiento especial. Para todo lo demás, solo debe devolver el objeto original (por lo tanto, la última devolución del controlador debe ser return obj no 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 

Tenga en cuenta que este código no busca valores que no sean serializables. Estos caerán en el return obj final, luego serán rechazados por el serializador y se pasarán nuevamente al controlador predeterminado, solo que esta vez sin el contenedor Debug.

Si necesita lidiar con ese escenario, puede agregar un cheque en la parte superior del controlador como este:

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

Demo de Ideone: http://ideone.com/tOloNq

La función predeterminada solo se llama cuando el nodo que se está volcando no se puede serializar de forma nativa, y sus clases mDict se serializan como están. Aquí hay una pequeña demostración que muestra cuándo se llama al valor predeterminado y cuándo no:

 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) 

Y la salida:

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

Tenga en cuenta que los conjuntos no son serializables de forma nativa, pero los dictados sí lo son.

Dado que sus clases m___ son serializables, nunca se llama a su controlador.

Actualización # 1 —–

Podría cambiar el código del codificador JSON. Los detalles de cómo hacer esto dependen de la implementación JSON que esté utilizando. Por ejemplo, en simplejson, el código relevante es este, en 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 otras palabras, hay un comportamiento fijo que llama a los valores predeterminados solo cuando el nodo que se está codificando no es uno de los tipos base reconocidos. Podría anular esto de una de las siguientes maneras:

1: subclase JSONEncoder como lo ha hecho anteriormente, pero agregue un parámetro a su inicializador que especifique la función que se usará en lugar del _make_iterencode estándar, en el que agrega una prueba que llamaría por defecto para las clases que cumplan con sus criterios. Este es un enfoque limpio ya que no está cambiando el módulo JSON, pero estaría reiterando una gran cantidad de código del _make_iterencode original. (Otras variaciones de este enfoque incluyen monkeypatching _make_iterencode o su subfunción _iterencode_dict).

2: modifique el origen del módulo JSON y use la constante __debug__ para cambiar el comportamiento:

 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: ... 

Idealmente, la clase JSONEncoder proporcionaría un parámetro para especificar “uso predeterminado para todos los tipos”, pero no lo hace. Lo anterior es un simple cambio de una sola vez que hace lo que está buscando.

Intente lo siguiente. Produce la salida que desea y parece relativamente simple. La única diferencia real de su clase de codificador es que deberíamos anular tanto los métodos de deencoding como los de encoding (ya que a este último todavía se le llama para los tipos que el codificador sabe cómo manejar).

 import json import datetime class JSONDebugEncoder(json.JSONEncoder): # transform objects known to JSONEncoder here def encode(self, o, *args, **kw): for_json = o if isinstance(o, mDict): for_json = { 'orig' : o, 'attrs' : vars(o) } elif isinstance(o, mList): for_json = { 'orig' : o, 'attrs' : vars(o) } return super(JSONDebugEncoder, self).encode(for_json, *args, **kw) # handle objects not known to JSONEncoder here def default(self, o, *args, **kw): if isinstance(o, datetime.datetime): return o.isoformat() else: return super(JSONDebugEncoder, self).default(o, *args, **kw) 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,cls=JSONDebugEncoder)) if __name__ == '__main__': test_debug_json() 

¿Por qué no puedes simplemente crear un nuevo tipo de objeto para pasar al codificador? Tratar:

 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 

Puede agregar validación en mDict y mList si lo desea.

Si define estos para anular __instancecheck__ :

 def strict_check(builtin): '''creates a new class from the builtin whose instance check method can be overridden to renounce particular types''' class BuiltIn(type): def __instancecheck__(self, other): print 'instance', self, type(other), other if type(other) in strict_check.blacklist: return False return builtin.__instancecheck__(other) # construct a class, whose instance check method is known. return BuiltIn('strict_%s' % builtin.__name__, (builtin,), dict()) # for safety, define it here. strict_check.blacklist = () 

luego parche json.encoder como este para anular _make_iterencode.func_defaults :

 # modify json encoder to use some new list/dict attr. import json.encoder # save old stuff, never know when you need it. old_defaults = json.encoder._make_iterencode.func_defaults old_encoder = json.encoder.c_make_encoder encoder_defaults = list(json.encoder._make_iterencode.func_defaults) for index, default in enumerate(encoder_defaults): if default in (list, dict): encoder_defaults[index] = strict_check(default) # change the defaults for _make_iterencode. json.encoder._make_iterencode.func_defaults = tuple(encoder_defaults) # disable C extension. json.encoder.c_make_encoder = None 

… tu ejemplo casi funcionaría literalmente:

 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): # degrade obj to more primitive dict() # to avoid cycles in the encoding. return {'orig': dict(obj) , 'attrs': vars(obj)} elif isinstance(obj,mList): # degrade obj to more primitive list() # to avoid cycles in the encoding. return {'orig': list(obj), 'attrs': vars(obj)} else: return None class mDict(dict): pass class mList(list): pass # set the stuff we want to process differently. strict_check.blacklist = (mDict, mList) def test_debug_json(): global test_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() 

Las cosas que necesitaba cambiar eran para asegurarme de que no hubiera ciclos:

  elif isinstance(obj,mDict): # degrade obj to more primitive dict() # to avoid cycles in the encoding. return {'orig': dict(obj) , 'attrs': vars(obj)} elif isinstance(obj,mList): # degrade obj to more primitive list() # to avoid cycles in the encoding. return {'orig': list(obj), 'attrs': vars(obj)} 

y agrega esto en algún lugar antes de test_debug_json :

 # set the stuff we want to process differently. strict_check.blacklist = (mDict, mList) 

Aquí está mi salida de consola:

 >>> test_debug_json() instance   {'date': datetime.datetime(2013, 7, 17, 12, 4, 40, 950637), 'games': ['mario', 'contra', 'tetris'], 'scores': {'pk': 45, 'dp': 10}} instance   {'date': datetime.datetime(2013, 7, 17, 12, 4, 40, 950637), 'games': ['mario', 'contra', 'tetris'], 'scores': {'pk': 45, 'dp': 10}} instance   2013-07-17 12:04:40.950637 instance   2013-07-17 12:04:40.950637 instance   2013-07-17 12:04:40.950637 instance   2013-07-17 12:04:40.950637 object received:  instance   ['mario', 'contra', 'tetris'] instance   ['mario', 'contra', 'tetris'] instance   ['mario', 'contra', 'tetris'] instance   ['mario', 'contra', 'tetris'] object received:  instance   {'attrs': {'src': 'console'}, 'orig': ['mario', 'contra', 'tetris']} instance   {'attrs': {'src': 'console'}, 'orig': ['mario', 'contra', 'tetris']} instance   {'src': 'console'} instance   {'src': 'console'} instance   ['mario', 'contra', 'tetris'] instance   {'pk': 45, 'dp': 10} instance   {'pk': 45, 'dp': 10} instance   {'pk': 45, 'dp': 10} instance   {'pk': 45, 'dp': 10} object received:  instance   {'attrs': {'processed': 'unprocessed'}, 'orig': {'pk': 45, 'dp': 10}} instance   {'attrs': {'processed': 'unprocessed'}, 'orig': {'pk': 45, 'dp': 10}} instance   {'processed': 'unprocessed'} instance   {'processed': 'unprocessed'} instance   {'pk': 45, 'dp': 10} instance   {'pk': 45, 'dp': 10} {"date": "2013-07-17T12:04:40.950637", "games": {"attrs": {"src": "console"}, "orig": ["mario", "contra", "tetris"]}, "scores": {"attrs": {"processed": "unprocessed"}, "orig": {"pk": 45, "dp": 10}}} 

Si eres capaz de cambiar la forma en que se llama json.dumps . Puede hacer todo el procesamiento requerido antes de que el codificador JSON le ponga las manos encima. Esta versión no utiliza ningún tipo de copia y editará las estructuras en el lugar. Puede agregar copy() si es necesario.

 import datetime import json import collections def json_debug_handler(obj): print("object received:") print type(obj) print("\n\n") if isinstance(obj, collections.Mapping): for key, value in obj.iteritems(): if isinstance(value, (collections.Mapping, collections.MutableSequence)): value = json_debug_handler(value) obj[key] = convert(value) elif isinstance(obj, collections.MutableSequence): for index, value in enumerate(obj): if isinstance(value, (collections.Mapping, collections.MutableSequence)): value = json_debug_handler(value) obj[index] = convert(value) return obj def convert(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 obj 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 = "qunprocessed" test_json = { 'games' : games , 'scores' : scores , 'date': datetime.datetime.now() } print(json.dumps(json_debug_handler(test_json))) if __name__ == '__main__': test_debug_json() 

Llame a json_debug_handler en el objeto que está serializando antes de pasarlo a json.dumps . Con este patrón también puede revertir fácilmente los cambios y / o agregar reglas de conversión adicionales.

editar:

Si no puede cambiar la forma en que se llama a json.dumps , siempre puede hacer que lo haga para que haga lo que quiera. Como hacer esto:

 json.dumps = lambda obj, *args, **kwargs: json.dumps(json_debug_handler(obj), *args, **kwargs) 

¿Podemos simplemente preprocesar el test_json , para que sea adecuado para su requerimiento? Es más fácil manipular un dictado de Python que escribir una encoding inútil.

 import datetime import json class mDict(dict): pass class mList(list): pass def prepare(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 obj def preprocessor(toJson): ret ={} for key, value in toJson.items(): ret[key] = prepare(value) return ret if __name__ == '__main__': 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(preprocessor(test_json))) test_debug_json() 

Debería poder anular JSONEncoder.encode () :

 class MyEncoder(JSONEncoder): def encode(self, o): if isinstance(o, dict): # directly call JSONEncoder rather than infinite-looping through self.encode() return JSONEncoder.encode(self, {'orig': o, 'attrs': vars(o)}) elif isinstance(o, list): return JSONEncoder.encode(self, {'orig': o, 'attrs': vars(o)}) else: return JSONEncoder.encode(self, o) 

y luego, si desea parchearlo en json.dumps se ve en http://docs.buildbot.net/latest/reference/json-pysrc.html como si json._default_encoder reemplazar json._default_encoder con una instancia de MyEncoder .

Si solo está buscando la serialización y no la deserialización, puede procesar el objeto antes de enviarlo a json.dumps . Vea el siguiente ejemplo

 import datetime import json def is_inherited_from(obj, objtype): return isinstance(obj, objtype) and not type(obj).__mro__[0] == objtype def process_object(data): if isinstance(data, list): if is_inherited_from(data, list): return process_object({"orig": list(data), "attrs": vars(data)}) new_data = [] for d in data: new_data.append(process_object(d)) elif isinstance(data, tuple): if is_inherited_from(data, tuple): return process_object({"orig": tuple(data), "attrs": vars(data)}) new_data = [] for d in data: new_data.append(process_object(d)) return tuple(new_data) elif isinstance(data, dict): if is_inherited_from(data, dict): return process_object({"orig": list(data), "attrs": vars(data)}) new_data = {} for k, v in data.items(): new_data[k] = process_object(v) else: return data return new_data def json_debug_handler(obj): print("object received:") print("\n\n") if isinstance(obj, datetime.datetime): return obj.isoformat() 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()} new_object = process_object(test_json) print(json.dumps(new_object, default=json_debug_handler)) if __name__ == '__main__': test_debug_json() 

La salida de la misma es.

{“juegos”: {“orig”: [“mario”, “contra”, “tetris”], “attrs”: {“src”: “console”}}, “scores”: {“orig”: [” dp “,” pk “],” attrs “: {” procesado “:” sin procesar “}},” fecha “:” 2018-01-24T12: 59: 36.581689 “}

También es posible anular el JSONEncoder, pero dado que utiliza métodos nesteds, sería complejo y requeriría las técnicas que se analizan a continuación.

¿Puede parchear * solo * una función anidada con cierre, o debe repetirse toda la función externa?

Ya que quieres mantener las cosas simples, no sugeriría ir por ese camino.

Siguiendo la línea de la sugerencia de FastTurtle, pero que requiere un poco menos de código y una simulación mucho más profunda, puede anular esta isinstance en todo el mundo. Probablemente no sea una buena idea, y puede que rompa algo. Pero funciona, ya que produce la salida requerida y es bastante simple.

En primer lugar, antes de importar json en cualquier lugar , aplique el modulo integrado en el módulo para reemplazar la isinstance con una que se encuentra, solo un poco, y solo en un contexto específico:

 _original_isinstance = isinstance def _isinstance(obj, class_or_tuple): if '_make_iterencode' in globals(): if not _original_isinstance(class_or_tuple, tuple): class_or_tuple = (class_or_tuple,) for custom in mList, mDict: if _original_isinstance(obj, custom): return custom in class_or_tuple return _original_isinstance(obj, class_or_tuple) try: import builtins # Python 3 except ImportError: import __builtin__ as builtins # Python 2 builtins.isinstance = _isinstance 

Luego, cree su codificador personalizado, implementando su serialización personalizada y forzando el uso de _make_iterencode (ya que la versión c no se verá afectada por el Monkeypatching):

 class CustomEncoder(json.JSONEncoder): def iterencode(self, o, _one_shot = False): return super(CustomEncoder, self).iterencode(o, _one_shot=False) def default(self, obj): 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 

Y eso es realmente todo lo que hay que hacer! Salida de Python 3 y Python 2 a continuación.

 Python 3.6.3 (default, Oct 10 2017, 21:06:48) ... >>> from test import test_debug_json >>> test_debug_json() {"games": {"orig": ["mario", "contra", "tetris"], "attrs": {"src": "console"}}, "scores": {"orig": {"dp": 10, "pk": 45}, "attrs": {"processed": "unprocessed"}}, "date": "2018-01-27T13:56:15.666655"} Python 2.7.13 (default, May 9 2017, 12:06:13) ... >>> from test import test_debug_json >>> test_debug_json() {"date": "2018-01-27T13:57:04.681664", "games": {"attrs": {"src": "console"}, "orig": ["mario", "contra", "tetris"]}, "scores": {"attrs": {"processed": "unprocessed"}, "orig": {"pk": 45, "dp": 10}}} 

I try to change the default resolver priority and change the default iterator outputs to achieve your purposes.

  1. change the default resolver priority, executed precede all standard type verifying:

    Inherits the json.JSONEncoder and overrides the iterencode() method.

    All values should be wrapped by ValueWrapper type, avoid the values are resolved by default standard resolvers.

  2. change the default iterator output;

    Implement three custom wrapper classes ValueWrapper , ListWrapper , and DictWrapper . The ListWrapper implement __iter__() and the DictWrapper implement __iter__() , items() and iteritems() .

 import datetime import json class DebugJsonEncoder(json.JSONEncoder): def iterencode(self, o, _one_shot=False): default_resolver = self.default # Rewrites the default resolve, self.default(), with the custom resolver. # It will process the Wrapper classes def _resolve(o): if isinstance(o, ValueWrapper): # Calls custom resolver precede others. Due to the _make_iterencode() # call the custom resolver following by all standard type verifying # failed. But we want custom resolver can be executed by all standard # verifying. # see https://github.com/python/cpython/blob/2.7/Lib/json/encoder.py#L442 result = default_resolver(o.data) if (o.data is not None) and (result is not None): return result elif isinstance(o.data, (list, tuple)): return ListWrapper(o.data) elif isinstance(o.data, dict): return DictWrapper(o.data) else: return o.data else: return default_resolver(o) # re-assign the default resolver self.default with custom resolver. # see https://github.com/python/cpython/blob/2.7/Lib/json/encoder.py#L161 self.default = _resolve # The input value must be wrapped by ValueWrapper, avoid the values are # resolved by the standard resolvers. # The last one arguemnt _one_shot must be False, we want to encode with # _make_iterencode(). # see https://github.com/python/cpython/blob/2.7/Lib/json/encoder.py#L259 return json.JSONEncoder.iterencode(self, _resolve(ValueWrapper(o)), False) class ValueWrapper(): """ a wrapper wrapped the given object """ def __init__(self, o): self.data = o class ListWrapper(ValueWrapper, list): """ a wrapper wrapped the given list """ def __init__(self, o): ValueWrapper.__init__(self, o) # see https://github.com/python/cpython/blob/2.7/Lib/json/encoder.py#L307 def __iter__(self): for chunk in self.data: yield ValueWrapper(chunk) class DictWrapper(ValueWrapper, dict): """ a wrapper wrapped the given dict """ def __init__(self, d): dict.__init__(self, d) def __iter__(self): for key, value in dict.items(self): yield key, ValueWrapper(value) # see https://github.com/python/cpython/blob/2.7/Lib/json/encoder.py#L361 def items(self): for key, value in dict.items(self): yield key, ValueWrapper(value) # see https://github.com/python/cpython/blob/2.7/Lib/json/encoder.py#L363 def iteritems(self): for key, value in dict.iteritems(self): yield key, ValueWrapper(value) 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(), 'default': None} print(json.dumps(test_json,cls=DebugJsonEncoder,default=json_debug_handler)) if __name__ == '__main__': test_debug_json()