J'ai un Decimal('3.9')
faisant partie d'un objet et je souhaite l'encoder en une chaîne JSON qui devrait ressembler à {'x': 3.9}
. Je me fiche de la précision du côté client, donc un float convient.
Y at-il un bon moyen de sérialiser cela? JSONDecoder n'accepte pas les objets Decimal, et la conversion en float au préalable donne {'x': 3.8999999999999999}
qui est incorrect, ce qui représente un gros gaspillage de bande passante.
Qu'en est-il du sous-classement 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 suit:
json.dumps({'x': decimal.Decimal('5.5')}, cls=DecimalEncoder)
Simplejson 2.1 et supérieur possède un 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):
Alors:
>>> json.dumps(Decimal('3.9'))
'3.9'
Espérons que cette fonctionnalité sera incluse dans la bibliothèque standard.
Je voudrais que tout le monde sache que j'ai essayé la réponse de Michał Marczyk sur mon serveur Web qui exécutait Python 2.6.5 et tout fonctionnait bien. Cependant, j'ai mis à niveau vers Python 2.7 et cela a cessé de fonctionner. J'ai essayé de trouver un moyen d'encoder les objets décimaux et voici 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, espérons-le, aider toute personne ayant des problèmes avec Python 2.7. Je l'ai testé et il semble bien fonctionner. Si quelqu'un remarque des bugs dans ma solution ou propose une meilleure solution, merci de me le faire savoir.
Dans mon application Flask, qui utilise python 2.7.11, flask alchimie (avec les types 'db.decimal') et Flask Marshmallow (pour le sérialiseur et le désérialiseur 'instantané'), j'avais cette erreur, chaque fois que je faisais un GET ou un POST. Le sérialiseur et le désérialiseur n'ont pas réussi à convertir les types Decimal en un format identifiable par JSON.
J'ai fait un "pip install simplejson", puis juste en ajoutant
import simplejson as json
le sérialiseur et le désérialiseur recommencent à ronronner. Je n'ai rien fait d'autre ... Les diamants sont affichés au format float '234.00'.
J'ai essayé de passer de simplejson à json intégré pour GAE 2.7 et des problèmes de décimales. Si default renvoyait str (o), il y avait des guillemets (parce que _iterencode appelle _iterencode sur les résultats par défaut), et float (o) supprimerait le dernier 0.
Si default renvoie un objet d'une classe qui hérite de float (ou de tout ce qui appelle repr sans un formatage supplémentaire) et possède une méthode personnalisée __repr__, il semble fonctionner comme je le souhaite.
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]'
L'option native est manquante alors je l'ajouterai au prochain gars/gall qui le cherchera.
À partir de Django 1.7.x, il existe un DjangoJSONEncoder
intégré que vous pouvez obtenir à partir 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!
3.9
ne peut pas être représenté exactement dans les flotteurs IEEE, il viendra toujours sous la forme 3.8999999999999999
, par ex. essayez print repr(3.9)
, vous pouvez en lire plus ici:
http://en.wikipedia.org/wiki/Floating_point
http://docs.Sun.com/source/806-3568/ncg_goldberg.html
Donc, si vous ne voulez pas que float, seule option, vous devez l'envoyer sous forme de chaîne, et pour permettre la conversion automatique des objets décimaux en JSON, procédez comme suit:
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 0,02 $!
J'étends beaucoup de l'encodeur JSON puisque je sérialise des tonnes de données pour mon serveur Web. Voici du code de Nice. Notez qu'il est facilement extensible à n'importe quel format de données et reproduira la version 3.9 sous la forme "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
Me rend la vie tellement plus facile ...
Pour Django utilisateurs:
Est récemment tombé sur TypeError: Decimal('2337.00') is not JSON serializable
alors que l'encodage JSON, c'est-à-dire json.dumps(data)
Solution:
# converts Decimal, Datetime, UUIDs to str for Encoding
from Django.core.serializers.json import DjangoJSONEncoder
json.dumps(response.data, cls=DjangoJSONEncoder)
Mais, maintenant la valeur décimale sera une chaîne, nous pouvons maintenant définir explicitement l’analyseur de valeur décimale/float lors du décodage de données, en utilisant l’option parse_float
dans json.loads
:
import decimal
data = json.loads(data, parse_float=decimal.Decimal) # default is float(num_str)
À partir du Document standard JSON , lié à json.org :
JSON est agnostique à propos de la sémantique des nombres. Dans n'importe quel langage de programmation, il peut exister une variété de types de nombres de capacités et de compléments variés, fixes ou flottants, binaires ou décimaux. Cela peut rendre difficile les échanges entre différents langages de programmation. JSON n'offre que la représentation des nombres utilisés par les humains: une séquence de chiffres. Tous les langages de programmation savent donner un sens aux séquences de chiffres même s’ils ne sont pas d’accord sur les représentations internes. Cela suffit pour permettre l'échange.
Il est donc exact de représenter Decimals sous forme de nombres (plutôt que de chaînes) en JSON. Ci-dessous se trouve une solution possible au problème.
Définissez un encodeur JSON personnalisé:
import json
class CustomJsonEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Decimal):
return float(obj)
return super(CustomJsonEncoder, self).default(obj)
Ensuite, utilisez-le pour sérialiser vos données:
json.dumps(data, cls=CustomJsonEncoder)
Comme indiqué dans les commentaires sur les autres réponses, les anciennes versions de python pourraient gâcher la représentation lors de la conversion en float, mais ce n'est plus le cas.
Pour récupérer la décimale en Python:
Decimal(str(value))
Cette solution est suggérée dans documentation Python 3.0 sur les décimales :
Pour créer un nombre décimal à partir d'un flottant, convertissez-le d'abord en chaîne.
Voici 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)
Ce qui passe le moins du monde:
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)
Basé sur stdOrgnlDave answer J'ai défini ce wrapper comme quoi il peut être appelé avec des types facultatifs afin que l'encodeur ne fonctionne que pour certains types au sein de vos projets. Je pense que le travail doit être effectué dans votre code et ne pas utiliser cet encodeur "par défaut", car "il est plus explicite qu'implicite", mais je pense qu'en l'utilisant, vous gagnerez 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 __== '__main__':
JSONEncoder_newdefault()
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 ça,
import json
from decimal import Decimal
json.dumps({'x': Decimal('3.9')}, cls=CustomJSONEncoder)
et le résultat sera:
>>'{"x": 3.9}'
Si vous souhaitez transmettre un dictionnaire contenant des décimales à la bibliothèque requests
(à l'aide de l'argument de mot clé json
, vous devez simplement 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
uniquement si elle est présente et revient à la fonction intégrée json
si elle n'est pas installée.