not - serialize json python
Python JSON serializa un objeto Decimal (12)
Tengo un Decimal(''3.9'')
como parte de un objeto, y deseo codificarlo en una cadena JSON que debería verse como {''x'': 3.9}
. No me importa la precisión del lado del cliente, por lo que un flotador está bien.
¿Hay una buena manera de serializar esto? JSONDecoder no acepta objetos decimales, y la conversión a un flotante de antemano produce {''x'': 3.8999999999999999}
que es incorrecto, y será un gran desperdicio de ancho de banda.
¡Mi $ .02!
Extiendo un montón del codificador JSON ya que estoy serializando toneladas de datos para mi servidor web. Aquí hay un buen código. Tenga en cuenta que es fácilmente extensible a prácticamente cualquier formato de datos que desee y reproducirá 3.9 como "thing": 3.9
JSONEncoder_olddefault = json.JSONEncoder.default
def JSONEncoder_newdefault(self, o):
if isinstance(o, UUID): return str(o)
if isinstance(o, datetime): return str(o)
if isinstance(o, time.struct_time): return datetime.fromtimestamp(time.mktime(o))
if isinstance(o, decimal.Decimal): return str(o)
return JSONEncoder_olddefault(self, o)
json.JSONEncoder.default = JSONEncoder_newdefault
Hace mi vida mucho más fácil ...
¿Qué tal subclasificar json.JSONEncoder
?
class DecimalEncoder(json.JSONEncoder):
def _iterencode(self, o, markers=None):
if isinstance(o, decimal.Decimal):
# wanted a simple yield str(o) in the next line,
# but that would mean a yield on the line with super(...),
# which wouldn''t work (see my comment below), so...
return (str(o) for o in [o])
return super(DecimalEncoder, self)._iterencode(o, markers)
Entonces úsalo así:
json.dumps({''x'': decimal.Decimal(''5.5'')}, cls=DecimalEncoder)
Basado en la respuesta de , he definido este contenedor para que pueda llamarse con tipos opcionales, por lo que el codificador solo funcionará para ciertos tipos dentro de sus proyectos. Creo que el trabajo debe hacerse dentro de su código y no utilizar este codificador "predeterminado" ya que "es mejor explícito que implícito", pero entiendo que usarlo ahorrará algo de tiempo. :-)
import time
import json
import decimal
from uuid import UUID
from datetime import datetime
def JSONEncoder_newdefault(kind=[''uuid'', ''datetime'', ''time'', ''decimal'']):
''''''
JSON Encoder newdfeault is a wrapper capable of encoding several kinds
Use it anywhere on your code to make the full system to work with this defaults:
JSONEncoder_newdefault() # for everything
JSONEncoder_newdefault([''decimal'']) # only for Decimal
''''''
JSONEncoder_olddefault = json.JSONEncoder.default
def JSONEncoder_wrapped(self, o):
''''''
json.JSONEncoder.default = JSONEncoder_newdefault
''''''
if (''uuid'' in kind) and isinstance(o, uuid.UUID):
return str(o)
if (''datetime'' in kind) and isinstance(o, datetime):
return str(o)
if (''time'' in kind) and isinstance(o, time.struct_time):
return datetime.fromtimestamp(time.mktime(o))
if (''decimal'' in kind) and isinstance(o, decimal.Decimal):
return str(o)
return JSONEncoder_olddefault(self, o)
json.JSONEncoder.default = JSONEncoder_wrapped
# Example
if __name__ == ''__main__'':
JSONEncoder_newdefault()
Del documento estándar JSON , como se vincula en json.org :
JSON es agnóstico sobre la semántica de los números. En cualquier lenguaje de programación, puede haber una variedad de tipos de números de varias capacidades y complementos, fijos o flotantes, binarios o decimales. Eso puede dificultar el intercambio entre diferentes lenguajes de programación. En cambio, JSON solo ofrece la representación de los números que los humanos usan: una secuencia de dígitos. Todos los lenguajes de programación saben cómo dar sentido a las secuencias de dígitos, incluso si no están de acuerdo con las representaciones internas. Eso es suficiente para permitir el intercambio.
Por lo tanto, es realmente correcto representar los decimales como números (en lugar de cadenas) en JSON. Bellow es una posible solución al problema.
Definir un codificador JSON personalizado:
import json
class CustomJsonEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Decimal):
return float(obj)
return super(CustomJsonEncoder, self).default(obj)
Luego úsala al serializar tus datos:
json.dumps(data, cls=CustomJsonEncoder)
Como se observa en los comentarios sobre las otras respuestas, las versiones anteriores de python podrían arruinar la representación al convertir a flotación, pero ese ya no es el caso.
Para recuperar ese decimal exacto en Python:
Decimal(str(value))
Esta solución se insinúa en la documentación de Python 3.0 sobre decimales :
Para crear un decimal de un flotante, primero conviértalo en una cadena.
En mi aplicación Flask, que usa Python 2.7.11, alquimia de matraz (con tipos ''db.decimal'') y Flask Marshmallow (para serializador y deserializador ''instantáneo''), tuve este error, cada vez que hacía un GET o POST . El serializador y el deserializador no pudieron convertir los tipos de Decimal en ningún formato identificable de JSON.
Hice un "pip install simplejson", luego simplemente agregando
import simplejson as json
el serializador y el deserializador comienzan a ronronear nuevamente. No hice nada más ... Los deciamls se muestran en formato flotante ''234.00''.
Esto es lo que tengo, extraído de nuestra clase
class CommonJSONEncoder(json.JSONEncoder):
"""
Common JSON Encoder
json.dumps(myString, cls=CommonJSONEncoder)
"""
def default(self, obj):
if isinstance(obj, decimal.Decimal):
return {''type{decimal}'': str(obj)}
class CommonJSONDecoder(json.JSONDecoder):
"""
Common JSON Encoder
json.loads(myString, cls=CommonJSONEncoder)
"""
@classmethod
def object_hook(cls, obj):
for key in obj:
if isinstance(key, six.string_types):
if ''type{decimal}'' == key:
try:
return decimal.Decimal(obj[key])
except:
pass
def __init__(self, **kwargs):
kwargs[''object_hook''] = self.object_hook
super(CommonJSONDecoder, self).__init__(**kwargs)
Que pasa la prueba de unidad:
def test_encode_and_decode_decimal(self):
obj = Decimal(''1.11'')
result = json.dumps(obj, cls=CommonJSONEncoder)
self.assertTrue(''type{decimal}'' in result)
new_obj = json.loads(result, cls=CommonJSONDecoder)
self.assertEqual(new_obj, obj)
obj = {''test'': Decimal(''1.11'')}
result = json.dumps(obj, cls=CommonJSONEncoder)
self.assertTrue(''type{decimal}'' in result)
new_obj = json.loads(result, cls=CommonJSONDecoder)
self.assertEqual(new_obj, obj)
obj = {''test'': {''abc'': Decimal(''1.11'')}}
result = json.dumps(obj, cls=CommonJSONEncoder)
self.assertTrue(''type{decimal}'' in result)
new_obj = json.loads(result, cls=CommonJSONDecoder)
self.assertEqual(new_obj, obj)
Intenté cambiar de simplejson a builtin json para GAE 2.7, y tuve problemas con el decimal. Si el valor predeterminado devuelto str (o) había comillas (porque _iterencode llama a _iterencode en los resultados de la configuración predeterminada), y float (o) eliminaría el 0 final.
Si el valor predeterminado devuelve un objeto de una clase que hereda de float (o cualquier cosa que llame a repr sin formato adicional) y tiene un método personalizado __repr__, parece funcionar como yo quiero.
import json
from decimal import Decimal
class fakefloat(float):
def __init__(self, value):
self._value = value
def __repr__(self):
return str(self._value)
def defaultencode(o):
if isinstance(o, Decimal):
# Subclass float with custom repr?
return fakefloat(o)
raise TypeError(repr(o) + " is not JSON serializable")
json.dumps([10.20, "10.20", Decimal(''10.20'')], default=defaultencode)
''[10.2, "10.20", 10.20]''
Me gustaría que todos sepan que probé la respuesta de MichaĆ Marczyk en mi servidor web que ejecutaba Python 2.6.5 y funcionó bien. Sin embargo, actualicé a Python 2.7 y dejó de funcionar. Traté de pensar en algún tipo de forma de codificar objetos Decimal y esto es lo que se me ocurrió:
class DecimalEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, decimal.Decimal):
return float(o)
return super(DecimalEncoder, self).default(o)
Esto debería ayudar a cualquiera que tenga problemas con Python 2.7. Lo probé y parece funcionar bien. Si alguien nota algún error en mi solución o se le ocurre una forma mejor, por favor avíseme.
Si desea pasar un diccionario que contiene decimales a la biblioteca de requests
(utilizando el argumento de la palabra clave json
), simplemente necesita instalar simplejson
:
$ pip3 install simplejson
$ python3
>>> import requests
>>> from decimal import Decimal
>>> # This won''t error out:
>>> requests.post(''https://www.google.com'', json={''foo'': Decimal(''1.23'')})
La razón del problema es que las requests
usan simplejson
solo si está presente, y vuelve al json
incorporado si no está instalado.
esto se puede hacer agregando
elif isinstance(o, decimal.Decimal):
yield str(o)
en /Lib/json/encoder.py:JSONEncoder._iterencode
, pero esperaba una solución mejor
Simplejson 2.1 y superior tiene soporte nativo para el tipo Decimal:
>>> json.dumps(Decimal(''3.9''), use_decimal=True)
''3.9''
Tenga en cuenta que use_decimal
es True
por defecto:
def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True,
allow_nan=True, cls=None, indent=None, separators=None,
encoding=''utf-8'', default=None, use_decimal=True,
namedtuple_as_object=True, tuple_as_array=True,
bigint_as_string=False, sort_keys=False, item_sort_key=None,
for_json=False, ignore_nan=False, **kw):
Asi que:
>>> json.dumps(Decimal(''3.9''))
''3.9''
Con suerte, esta característica se incluirá en la biblioteca estándar.
3.9
no se puede representar exactamente en flotantes IEEE, siempre vendrá como 3.8999999999999999
, por ejemplo, try print repr(3.9)
, puede leer más sobre esto aquí:
http://en.wikipedia.org/wiki/Floating_point
http://docs.sun.com/source/806-3568/ncg_goldberg.html
Entonces, si no quiere flotar, solo tiene que enviar la opción como cadena, y para permitir la conversión automática de objetos decimales a JSON, haga algo como esto:
import decimal
from django.utils import simplejson
def json_encode_decimal(obj):
if isinstance(obj, decimal.Decimal):
return str(obj)
raise TypeError(repr(obj) + " is not JSON serializable")
d = decimal.Decimal(''3.5'')
print simplejson.dumps([d], default=json_encode_decimal)