349 votes

Python JSON sérialise un objet Decimal

J'ai un Decimal('3.9') en tant que partie d'un objet, et je souhaite l'encoder en une chaîne JSON qui devrait ressembler à ceci {'x': 3.9} . Je ne me soucie pas de la précision du côté client, donc une valeur flottante convient parfaitement.

Existe-t-il un bon moyen de sérialiser ceci ? Le décodeur JSOND n'accepte pas les objets décimaux, et la conversion préalable en flottant donne les résultats suivants {'x': 3.8999999999999999} ce qui est faux, et sera un grand gaspillage de bande passante.

3 votes

0 votes

3.8999999999999999 n'est pas plus faux que 3.4. 0,2 n'a pas de représentation flottante exacte.

9 votes

@Jasen 3.8999999999999 est environ 12,8% plus faux que 3.4. La norme JSON ne concerne que la sérialisation et la notation, pas la mise en œuvre. L'utilisation de IEEE754 ne fait pas partie de la spécification JSON brute, c'est seulement la façon la plus courante de l'implémenter. Une mise en œuvre qui n'utilise que l'arithmétique décimale précise est tout à fait (en fait, même plus strictement) conforme.

275voto

Lukas Cenovsky Points 2425

Simplejson 2.1 et plus ont un support natif pour le type Decimal :

>>> json.dumps(Decimal('3.9'), use_decimal=True)
'3.9'

Notez que use_decimal es 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):

Donc :

>>> json.dumps(Decimal('3.9'))
'3.9'

Espérons que cette fonctionnalité sera incluse dans la bibliothèque standard.

11 votes

Hmm, pour moi, cela convertit les objets décimaux en flottants, ce qui n'est pas acceptable. Perte de précision lorsque l'on travaille avec des devises, par exemple.

17 votes

@MatthewSchinckel Je ne pense pas. Il en fait une chaîne de caractères. Et si vous renvoyez la chaîne résultante à json.loads(s, use_decimal=True) il vous rend la décimale. Aucun flottement dans tout le processus. J'ai modifié la réponse ci-dessus. J'espère que l'affiche originale est d'accord avec cela.

2 votes

Aha, je pense que je n'utilisais pas use_decimal=True sur les charges, aussi.

267voto

Elias Zamaria Points 13196

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 cela a bien fonctionné. Cependant, j'ai effectué une mise à niveau vers Python 2.7 et cela a cessé de fonctionner. J'ai essayé de penser à une sorte de moyen d'encoder les objets décimaux et voici ce que j'ai trouvé :

import decimal

class DecimalEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, decimal.Decimal):
            return str(o)
        return super(DecimalEncoder, self).default(o)

Notez que cela convertira la décimale en sa représentation sous forme de chaîne de caractères (par exemple ; "1.2300" ) pour a. ne pas perdre de chiffres significatifs et b. éviter les erreurs d'arrondi.

Ceci devrait aider tous ceux qui ont des problèmes avec Python 2.7. Je l'ai testé et il semble fonctionner correctement. Si quelqu'un remarque des bogues dans ma solution ou trouve une meilleure façon de procéder, veuillez me le faire savoir.

Exemple d'utilisation :

json.dumps({'x': decimal.Decimal('5.5')}, cls=DecimalEncoder)

4 votes

Python 2.7 a modifié les règles d'arrondi des nombres flottants, ce qui fait que cela fonctionne. Voir la discussion dans stackoverflow.com/questions/1447287/

3 votes

Pour ceux d'entre nous qui ne peuvent pas utiliser simplejson (c'est-à-dire sur Google App Engine), cette réponse est un cadeau du ciel.

0 votes

Je suppose que cela dépend de la plateforme. Je vois {"x": 54.399999999999999} . Oui, le résultat n'est pas une erreur, mais ce n'est pas ce qui était prévu.

183voto

Michał Marczyk Points 54179

Et si vous sous-classiez json.JSONEncoder ?

class DecimalEncoder(json.JSONEncoder):
    def default(self, o):
        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).default(o)

Ensuite, utilisez-le comme suit :

json.dumps({'x': decimal.Decimal('5.5')}, cls=DecimalEncoder)

0 votes

Aïe, je viens de remarquer que ça ne fonctionne pas vraiment comme ça. Je modifierai en conséquence. (L'idée reste la même, cependant).

0 votes

Le problème était que DecimalEncoder()._iterencode(decimal.Decimal('3.9')).next() a renvoyé le bon '3.9' mais DecimalEncoder()._iterencode(3.9).next() retourne un objet générateur qui ne renvoie que '3.899...' quand vous avez empilé un autre .next() . Le générateur est une drôle d'affaire. Oh bien... Devrait fonctionner maintenant.

0 votes

Wow, ça m'a valu un downvote sans aucun commentaire ? S'il y a quelque chose qui cloche avec ce code, j'aimerais bien le savoir, car je suis susceptible d'utiliser quelque chose de similaire - sans compter que le PO aimerait le savoir.

41voto

tesdal Points 1781

J'ai essayé de passer de simplejson à builtin json pour GAE 2.7, et j'ai eu des problèmes avec la décimale. Si default retournait str(o), il y avait des guillemets (parce que _iterencode appelle _iterencode sur les résultats de default), et float(o) enlevait le 0 de fin de ligne.

Si default renvoie un objet d'une classe qui hérite de float (ou de toute autre classe qui appelle repr sans formatage supplémentaire) et possède une méthode __repr__ personnalisée, cela 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]'

0 votes

Joli ! Cela permet de s'assurer que la valeur décimale se retrouve dans le JSON sous la forme d'un flottant Javascript, sans que Python ait à l'arrondir à la valeur flottante la plus proche.

3 votes

Malheureusement, cela ne fonctionne pas dans les versions récentes de Python 3. Il existe maintenant un code de chemin rapide qui considère toutes les sous-classes de flottants comme des flottants, et n'appelle pas repr sur eux.

0 votes

@AnttiHaapala, l'exemple fonctionne bien sur Python 3.6.

14voto

Anurag Uniyal Points 31931

3.9 ne peut pas être représentée exactement en flottants IEEE, elle sera toujours exprimée en tant que 3.8999999999999999 par exemple, essayez print repr(3.9) Vous pouvez en savoir 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 flottant, la seule option que vous avez est de l'envoyer en tant que chaîne, et pour permettre la conversion automatique des objets décimaux en JSON, faites 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)

0 votes

Je sais que ce ne sera pas 3.9 en interne une fois qu'il sera analysé par le client, mais 3.9 est un flotteur JSON valide, c'est-à-dire qu'il n'y a pas d'erreur, json.loads("3.9") fonctionnera, et je voudrais que ce soit ceci

0 votes

@Anurag Vous vouliez dire repr(obj) au lieu de repr(o) dans votre exemple.

0 votes

Cela ne va-t-il pas mourir si vous essayez de coder quelque chose qui n'est pas décimal ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X