web-dev-qa-db-fra.com

Python JSON sérialise un objet Decimal

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.

199
Knio

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)
131
Michał Marczyk

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.

199
Lukas Cenovsky

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.

156
Elias Zamaria

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

30
ISONecroMAn

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]'
27
tesdal

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!

16
Javier Buzzi

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)
11
Anurag Uniyal

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

11
std''OrgnlDave

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)
6
Nabeel Ahmed

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

6
Hugo Mota

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)
6
James Lin

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()
2
Juanmi Taboada

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}'
1
sparrow

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.

0
Max Malysh