Python JSON sérialize un objet décimal
j'ai un Decimal('3.9')
comme partie d'un objet, et je souhaite l'encoder à une chaîne JSON qui devrait ressembler à {'x': 3.9}
. Je me fiche de la précision côté client, donc un flotteur est parfait.
y a-t-il un bon moyen de sérialiser cela? JSONDecoder n'accepte pas les objets décimaux, et la conversion à un flotteur à l'avance donne {'x': 3.8999999999999999}
ce qui est faux, et sera un grand gaspillage de bande passante.
14 réponses
Que Diriez-vous de sous-classer 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)
alors utilisez - le comme ceci:
json.dumps({'x': decimal.Decimal('5.5')}, cls=DecimalEncoder)
Simplejson 2.1 et plus a le support natif pour le type décimal:
>>> json.dumps(Decimal('3.9'), use_decimal=True)
'3.9'
notez que use_decimal
est True
par défaut:
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):
:
>>> json.dumps(Decimal('3.9'))
'3.9'
avec un peu de chance, cette fonctionnalité sera incluse dans la bibliothèque standard.
je voudrais faire savoir à tout le monde que j'ai essayé la réponse de Michał Marczyk sur mon serveur web qui exécutait Python 2.6.5 et ça a bien fonctionné. Cependant, je suis passé à Python 2.7 et il a cessé de fonctionner. J'ai essayé de penser à une sorte de façon D'encoder des objets décimaux et c'est ce que j'ai trouvé:
class DecimalEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, decimal.Decimal):
return float(o)
return super(DecimalEncoder, self).default(o)
cela devrait aider quiconque a des problèmes avec Python 2.7. Je l'ai testé et il semble bien fonctionner. Si quelqu'un remarque des bugs dans mon solution ou est livré avec une meilleure manière, s'il vous plaît laissez-moi savoir.
j'ai essayé de passer de simplejson à builtin json pour GAE 2.7, et avait des problèmes avec la décimale. Si default renvoie str (o), il y a des guillemets(parce que _iterencode appelle _iterencode sur les résultats de default), et float (o) supprimera le tracing 0.
si par défaut renvoie un objet d'une classe qui hérite de float (ou quelque chose qui appelle repr sans formatage supplémentaire) et a une méthode __repr__ personnalisée, il semble fonctionner comme je le veux.
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]'
dans mon application Flask, qui utilise python 2.7.11, flask alchemy (avec 'db.décimal ' types), et Flask Marshmallow ( pour 'instant' serializer et deserializer), j'ai eu cette erreur, chaque fois que j'ai fait un GET ou POST. Le serializer et deserializer, n'a pas réussi à convertir les types décimaux dans n'importe quel format identifiable JSON.
j'ai fait un "pip install simplejson", puis Juste en ajoutant
import simplejson as json
le serializer et deserializer recommence à ronronner. Je n'a rien fait d'autre... Les DEciamls sont affichés en format float '234.00'.
3.9
ne peut pas être représenté exactement dans les flotteurs IEEE, il viendra toujours comme 3.8999999999999999
, par exemple essayer print repr(3.9)
, vous pouvez lire plus à ce sujet ici:
http://en.wikipedia.org/wiki/Floating_point
http://docs.sun.com/source/806-3568/ncg_goldberg.html
donc si vous ne voulez pas de float, seule option vous devez l'envoyer en string, et pour permettre conversion automatique d'objets décimaux en JSON, faire quelque chose comme ceci:
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)
mon $.02!
j'étends un tas de l'encodeur JSON puisque je sérialise des tonnes de données pour mon serveur web. Voici un peu de bon code. Notez qu'il est facilement extensible à peu près n'importe quel format de données que vous vous sentez et va reproduire 3.9 comme "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
rend ma vie tellement plus facile...
C'est ce que j'ai, extrait de notre classe
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)
qui passe unittest:
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)
l'option native est manquante donc je vais l'Ajouter pour le prochain gars/Galle qui le cherche.
à partir de Django 1.7.x il y a un DjangoJSONEncoder
intégré que vous pouvez obtenir de django.core.serializers.json
.
import json
from django.core.serializers.json import DjangoJSONEncoder
from django.forms.models import model_to_dict
model_instance = YourModel.object.first()
model_dict = model_to_dict(model_instance)
json.dumps(model_dict, cls=DjangoJSONEncoder)
Presto!
Basé sur stdOrgnlDave réponse j'ai défini ce wrapper qu'il peut être appelé avec l'option types de sorte que le codeur fonctionne uniquement pour certains types à l'intérieur de vos projets. Je pense que le travail devrait être fait à l'intérieur de votre code et de ne pas utiliser cet encodeur "par défaut" car "c'est mieux explicite qu'implicite", mais je comprends que l'utilisation de ce code vous fera gagner du temps. :- )
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()
De la JSON Document Standard , lié à json.org :
JSON est agnostique sur la sémantique des nombres. Dans tout langage de programmation, il peut être une variété de nombre types de différentes capacités et compléments, fixes ou flottants, binaires ou décimaux. Que peut faire échange entre différents langages de programmation difficile. JSON offre plutôt seulement la représentation de nombre que les humains utilisent: une séquence de chiffres. Tous les langages de programmation savent donner un sens à digit même s'ils ne sont pas d'accord sur les représentations internes. Qui est suffisant pour permettre l'échange.
donc il est en fait exact de représenter des décimales en tant que nombres (plutôt que des chaînes) dans JSON. Soufflet se trouve une solution possible au problème.
Définir un JSON personnalisé encodeur:
import json
class CustomJsonEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Decimal):
return float(obj)
return super(CustomJsonEncoder, self).default(obj)
alors utilisez-le quand sérialiser vos données:
json.dumps(data, cls=CustomJsonEncoder)
comme noté dans les commentaires sur les autres réponses, les versions plus anciennes de python pourraient gâcher la représentation lors de la conversion en float, mais ce n'est plus le cas.
pour obtenir ce retour décimal exact en Python:
Decimal(str(value))
cette solution est suggérée dans Python 3.0 documentation sur les décimales :
pour créer une décimale de un char, convertissez-le d'abord en chaîne.
si vous voulez passer un dictionnaire contenant des décimales à la bibliothèque requests
(en utilisant l'argument de mot-clé json
), vous avez simplement besoin d'installer 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 raison du problème est que requests
utilise simplejson
seulement si elle est présente, et retombe à la json
intégrée si elle n'est pas installée.
vous pouvez créer un encodeur JSON personnalisé selon vos besoins.
import json
from datetime import datetime, date
from time import time, struct_time, mktime
import decimal
class CustomJSONEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, datetime):
return str(o)
if isinstance(o, date):
return str(o)
if isinstance(o, decimal.Decimal):
return float(o)
if isinstance(o, struct_time):
return datetime.fromtimestamp(mktime(o))
# Any other serializer if needed
return super(CustomJSONEncoder, self).default(o)
le décodeur peut être appelé comme ceci,
import json
from decimal import Decimal
json.dumps({'x': Decimal('3.9')}, cls=CustomJSONEncoder)
et la sortie sera:
>>'{"x": 3.9}'
cela peut être fait en ajoutant
elif isinstance(o, decimal.Decimal):
yield str(o)
dans \Lib\json\encoder.py:JSONEncoder._iterencode
, mais j'espérais une meilleure solution