Formater les flottants avec le module json standard

100

J'utilise le module json standard en python 2.6 pour sérialiser une liste de flotteurs. Cependant, j'obtiens des résultats comme celui-ci:

>>> import json
>>> json.dumps([23.67, 23.97, 23.87])
'[23.670000000000002, 23.969999999999999, 23.870000000000001]'

Je veux que les flotteurs soient formatés avec seulement deux chiffres décimaux. La sortie devrait ressembler à ceci:

>>> json.dumps([23.67, 23.97, 23.87])
'[23.67, 23.97, 23.87]'

J'ai essayé de définir ma propre classe d'encodeur JSON:

class MyEncoder(json.JSONEncoder):
    def encode(self, obj):
        if isinstance(obj, float):
            return format(obj, '.2f')
        return json.JSONEncoder.encode(self, obj)

Cela fonctionne pour un seul objet flottant:

>>> json.dumps(23.67, cls=MyEncoder)
'23.67'

Mais échoue pour les objets imbriqués:

>>> json.dumps([23.67, 23.97, 23.87])
'[23.670000000000002, 23.969999999999999, 23.870000000000001]'

Je ne veux pas de dépendances externes, donc je préfère m'en tenir au module json standard.

Comment puis-je atteindre cet objectif?

Manuel Ceron
la source

Réponses:

80

Note: Cela ne pas travailler dans une version récente de Python.

Malheureusement, je pense que vous devez le faire par monkey-patching (ce qui, à mon avis, indique un défaut de conception dans le jsonpaquet standard de la bibliothèque ). Par exemple, ce code:

import json
from json import encoder
encoder.FLOAT_REPR = lambda o: format(o, '.2f')
    
print(json.dumps(23.67))
print(json.dumps([23.67, 23.97, 23.87]))

émet:

23.67
[23.67, 23.97, 23.87]

comme vous le désirez. De toute évidence, il devrait y avoir une manière architecturée de remplacer FLOAT_REPRafin que CHAQUE représentation d'un flotteur soit sous votre contrôle si vous le souhaitez; mais malheureusement ce n'est pas ainsi que le jsonpackage a été conçu :-(.

Alex Martelli
la source
10
Cette solution ne fonctionne pas dans Python 2.7 en utilisant la version C de Python de l'encodeur JSON.
Nelson
25
Cependant, vous faites cela, utilisez quelque chose comme% .15g ou% .12g au lieu de% .3f.
Guido van Rossum
23
J'ai trouvé cet extrait dans le code d'un programmeur junior. Cela aurait créé un bug très sérieux mais subtil s'il n'avait pas été attrapé. Pouvez-vous s'il vous plaît placer un avertissement sur ce code expliquant les implications globales de cette correction de singe.
Rory Hart
12
C'est une bonne hygiène de le remettre en place lorsque vous avez terminé: original_float_repr = encoder.FLOAT_REPR encoder.FLOAT_REPR = lambda o: format(o, '.2f') print json.dumps(1.0001) encoder.FLOAT_REPR = original_float_repr
Jeff Kaufman
6
Comme d'autres l'ont souligné, cela ne fonctionne plus dans au moins Python 3.6+. Ajoutez quelques chiffres pour 23.67voir en quoi .2fn'est pas respecté.
Nico Schlömer
57
import simplejson
    
class PrettyFloat(float):
    def __repr__(self):
        return '%.15g' % self
    
def pretty_floats(obj):
    if isinstance(obj, float):
        return PrettyFloat(obj)
    elif isinstance(obj, dict):
        return dict((k, pretty_floats(v)) for k, v in obj.items())
    elif isinstance(obj, (list, tuple)):
        return list(map(pretty_floats, obj))
    return obj
    
print(simplejson.dumps(pretty_floats([23.67, 23.97, 23.87])))

émet

[23.67, 23.97, 23.87]

Aucun monkeypatching nécessaire.

Tom Wuttke
la source
2
J'aime cette solution; meilleure intégration, et fonctionne avec 2.7. Parce que je construis moi-même les données de toute façon, j'ai éliminé la pretty_floatsfonction et l' ai simplement intégrée dans mon autre code.
mikepurvis
1
Dans Python3, cela donne l' erreur "L'objet de la carte n'est pas sérialisable JSON" , mais vous pouvez résoudre la conversion de la carte () en une liste aveclist( map(pretty_floats, obj) )
Guglie
1
@Guglie: c'est parce que dans Python 3 maprenvoie l'itérateur, pas unlist
Azat Ibrakov
4
Cela ne fonctionne pas pour moi (Python 3.5.2, simplejson 3.16.0). Essayé avec% .6g et [23.671234556, 23.971234556, 23.871234556], il imprime toujours le nombre entier.
szali
27

Si vous utilisez Python 2.7, une solution simple consiste simplement à arrondir explicitement vos flottants à la précision souhaitée.

>>> sys.version
'2.7.1 (r271:86832, Nov 27 2010, 18:30:46) [MSC v.1500 32 bit (Intel)]'
>>> json.dumps(1.0/3.0)
'0.3333333333333333'
>>> json.dumps(round(1.0/3.0, 2))
'0.33'

Cela fonctionne parce que Python 2.7 a rendu l' arrondi flottant plus cohérent . Malheureusement, cela ne fonctionne pas dans Python 2.6:

>>> sys.version
'2.6.6 (r266:84292, Dec 27 2010, 00:02:40) \n[GCC 4.4.5]'
>>> json.dumps(round(1.0/3.0, 2))
'0.33000000000000002'

Les solutions mentionnées ci-dessus sont des solutions de contournement pour 2.6, mais aucune n'est entièrement adéquate. Monkey patching json.encoder.FLOAT_REPR ne fonctionne pas si votre environnement d'exécution Python utilise une version C du module JSON. La classe PrettyFloat dans la réponse de Tom Wuttke fonctionne, mais seulement si l'encodage% g fonctionne globalement pour votre application. Le% .15g est un peu magique, cela fonctionne car la précision du flotteur est de 17 chiffres significatifs et% g n'imprime pas les zéros de fin.

J'ai passé du temps à essayer de faire un PrettyFloat qui permettait de personnaliser la précision de chaque nombre. Ie, une syntaxe comme

>>> json.dumps(PrettyFloat(1.0 / 3.0, 4))
'0.3333'

Ce n'est pas facile de bien faire les choses. Hériter de float est gênant. Hériter d'Object et utiliser une sous-classe JSONEncoder avec sa propre méthode default () devrait fonctionner, sauf que le module json semble supposer que tous les types personnalisés doivent être sérialisés sous forme de chaînes. Ie: vous vous retrouvez avec la chaîne Javascript "0.33" dans la sortie, pas le nombre 0.33. Il existe peut-être encore un moyen de faire en sorte que cela fonctionne, mais c'est plus difficile qu'il n'y paraît.

Nelson
la source
Une autre approche pour Python 2.6 utilisant JSONEncoder.iterencode et la correspondance de modèles peut être vue sur github.com/migurski/LilJSON/blob/master/liljson.py
Nelson
J'espère que cela rendra le passage de vos flotteurs plus léger - j'aime comment nous pouvons éviter de jouer avec les classes JSON qui peuvent être nulles.
Lincoln B
20

Vraiment dommage qui dumpsne vous permet pas de faire quoi que ce soit pour les flotteurs. Mais le loadsfait. Donc, si cela ne vous dérange pas la charge supplémentaire du processeur, vous pouvez le lancer à travers l'encodeur / décodeur / encodeur et obtenir le bon résultat:

>>> json.dumps(json.loads(json.dumps([.333333333333, .432432]), parse_float=lambda x: round(float(x), 3)))
'[0.333, 0.432]'
Claude
la source
Merci, c'est une suggestion vraiment utile. Je ne savais pas pour le parse_floatkwarg!
Anonyme
La suggestion la plus simple ici qui fonctionne également dans 3.6.
Brent Faust
Notez la phrase "ne vous souciez pas de la charge CPU supplémentaire". N'utilisez certainement pas cette solution si vous avez beaucoup de données à sérialiser. Pour moi, en ajoutant cela seul, un programme effectuant un calcul non trivial prend 3 fois plus de temps.
shaneb
11

Voici une solution qui a fonctionné pour moi dans Python 3 et qui ne nécessite pas de patch monkey:

import json

def round_floats(o):
    if isinstance(o, float): return round(o, 2)
    if isinstance(o, dict): return {k: round_floats(v) for k, v in o.items()}
    if isinstance(o, (list, tuple)): return [round_floats(x) for x in o]
    return o


json.dumps(round_floats([23.63437, 23.93437, 23.842347]))

La sortie est:

[23.63, 23.93, 23.84]

Il copie les données mais avec des flottants arrondis.

jcoffland
la source
9

Si vous êtes bloqué avec Python 2.5 ou des versions antérieures: L'astuce monkey-patch ne semble pas fonctionner avec le module simplejson d'origine si les accélérations C sont installées:

$ python
Python 2.5.4 (r254:67916, Jan 20 2009, 11:06:13) 
[GCC 4.2.1 (SUSE Linux)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import simplejson
>>> simplejson.__version__
'2.0.9'
>>> simplejson._speedups
<module 'simplejson._speedups' from '/home/carlos/.python-eggs/simplejson-2.0.9-py2.5-linux-i686.egg-tmp/simplejson/_speedups.so'>
>>> simplejson.encoder.FLOAT_REPR = lambda f: ("%.2f" % f)
>>> simplejson.dumps([23.67, 23.97, 23.87])
'[23.670000000000002, 23.969999999999999, 23.870000000000001]'
>>> simplejson.encoder.c_make_encoder = None
>>> simplejson.dumps([23.67, 23.97, 23.87])
'[23.67, 23.97, 23.87]'
>>> 
Carlos Valiente
la source
7

Vous pouvez faire ce que vous devez faire, mais ce n'est pas documenté:

>>> import json
>>> json.encoder.FLOAT_REPR = lambda f: ("%.2f" % f)
>>> json.dumps([23.67, 23.97, 23.87])
'[23.67, 23.97, 23.87]'
Ned Batchelder
la source
5
Cela a l'air soigné, mais ne semble pas fonctionner sur Python 3.6. En particulier, je n'ai pas vu de FLOAT_REPRconstante dans le json.encodermodule.
Tomasz Gandor
2

La solution d'Alex Martelli fonctionnera pour les applications à thread unique, mais peut ne pas fonctionner pour les applications multithreads qui doivent contrôler le nombre de décimales par thread. Voici une solution qui devrait fonctionner dans les applications multi-threadées:

import threading
from json import encoder

def FLOAT_REPR(f):
    """
    Serialize a float to a string, with a given number of digits
    """
    decimal_places = getattr(encoder.thread_local, 'decimal_places', 0)
    format_str = '%%.%df' % decimal_places
    return format_str % f

encoder.thread_local = threading.local()
encoder.FLOAT_REPR = FLOAT_REPR     

#As an example, call like this:
import json

encoder.thread_local.decimal_places = 1
json.dumps([1.56, 1.54]) #Should result in '[1.6, 1.5]'

Vous pouvez simplement définir encoder.thread_local.decimal_places sur le nombre de décimales souhaité, et le prochain appel à json.dumps () dans ce thread utilisera ce nombre de décimales

Anton I. Sipos
la source
2

Si vous avez besoin de le faire en python 2.7 sans remplacer le json.encoder.FLOAT_REPR global, voici une solution.

import json
import math

class MyEncoder(json.JSONEncoder):
    "JSON encoder that renders floats to two decimal places"

    FLOAT_FRMT = '{0:.2f}'

    def floatstr(self, obj):
        return self.FLOAT_FRMT.format(obj)

    def _iterencode(self, obj, markers=None):
        # stl JSON lame override #1
        new_obj = obj
        if isinstance(obj, float):
            if not math.isnan(obj) and not math.isinf(obj):
                new_obj = self.floatstr(obj)
        return super(MyEncoder, self)._iterencode(new_obj, markers=markers)

    def _iterencode_dict(self, dct, markers=None):
        # stl JSON lame override #2
        new_dct = {}
        for key, value in dct.iteritems():
            if isinstance(key, float):
                if not math.isnan(key) and not math.isinf(key):
                    key = self.floatstr(key)
            new_dct[key] = value
        return super(MyEncoder, self)._iterencode_dict(new_dct, markers=markers)

Ensuite, en python 2.7:

>>> from tmp import MyEncoder
>>> enc = MyEncoder()
>>> enc.encode([23.67, 23.98, 23.87])
'[23.67, 23.98, 23.87]'

En python 2.6, cela ne fonctionne pas tout à fait comme le souligne Matthew Schinckel ci-dessous:

>>> import MyEncoder
>>> enc = MyEncoder()  
>>> enc.encode([23.67, 23.97, 23.87])
'["23.67", "23.97", "23.87"]'
Mike Fogel
la source
4
Ceux-ci ressemblent à des chaînes, pas à des nombres.
Matthew Schinckel
1

Avantages:

  • Fonctionne avec n'importe quel encodeur JSON, ou même repr de python.
  • Court (ish), semble fonctionner.

Les inconvénients:

  • Hack de regexp laid, à peine testé.
  • Complexité quadratique.

    def fix_floats(json, decimals=2, quote='"'):
        pattern = r'^((?:(?:"(?:\\.|[^\\"])*?")|[^"])*?)(-?\d+\.\d{'+str(decimals)+'}\d+)'
        pattern = re.sub('"', quote, pattern) 
        fmt = "%%.%df" % decimals
        n = 1
        while n:
            json, n = re.subn(pattern, lambda m: m.group(1)+(fmt % float(m.group(2)).rstrip('0')), json)
        return json
Sam Watkins
la source
1

Lors de l'importation du module json standard, il suffit de changer le codeur par défaut FLOAT_REPR. Il n'est pas vraiment nécessaire d'importer ou de créer des instances Encoder.

import json
json.encoder.FLOAT_REPR = lambda o: format(o, '.2f')

json.dumps([23.67, 23.97, 23.87]) #returns  '[23.67, 23.97, 23.87]'

Parfois, il est également très utile de générer en json la meilleure représentation que python peut deviner avec str. Cela garantira que les chiffres significatifs ne sont pas ignorés.

import json
json.dumps([23.67, 23.9779, 23.87489])
# output is'[23.670000000000002, 23.977900000000002, 23.874890000000001]'

json.encoder.FLOAT_REPR = str
json.dumps([23.67, 23.9779, 23.87489])
# output is '[23.67, 23.9779, 23.87489]'
F Pereira
la source
1

Je suis d'accord avec @Nelson pour dire qu'hériter de float est gênant, mais peut-être qu'une solution qui ne touche que la __repr__fonction pourrait être pardonnable. J'ai fini par utiliser le decimalpackage pour reformater les flotteurs en cas de besoin. L'avantage est que cela fonctionne dans tous les contextes où il repr()est appelé, donc également lors de l'impression de listes sur stdout par exemple. En outre, la précision est configurable à l'exécution, une fois les données créées. L'inconvénient est bien sûr que vos données doivent être converties dans cette classe flottante spéciale (car malheureusement, vous ne pouvez pas sembler monkey patchfloat.__repr__ ). Pour cela, je propose une brève fonction de conversion.

Le code:

import decimal
C = decimal.getcontext()

class decimal_formatted_float(float):
   def __repr__(self):
       s = str(C.create_decimal_from_float(self))
       if '.' in s: s = s.rstrip('0')
       return s

def convert_to_dff(elem):
    try:
        return elem.__class__(map(convert_to_dff, elem))
    except:
        if isinstance(elem, float):
            return decimal_formatted_float(elem)
        else:
            return elem

Exemple d'utilisation:

>>> import json
>>> li = [(1.2345,),(7.890123,4.567,890,890.)]
>>>
>>> decimal.getcontext().prec = 15
>>> dff_li = convert_to_dff(li)
>>> dff_li
[(1.2345,), (7.890123, 4.567, 890, 890)]
>>> json.dumps(dff_li)
'[[1.2345], [7.890123, 4.567, 890, 890]]'
>>>
>>> decimal.getcontext().prec = 3
>>> dff_li = convert_to_dff(li)
>>> dff_li
[(1.23,), (7.89, 4.57, 890, 890)]
>>> json.dumps(dff_li)
'[[1.23], [7.89, 4.57, 890, 890]]'
user1556435
la source
Cela ne fonctionne pas avec le package json Python3 intégré, qui n'utilise pas __repr __ ().
Ian Goldpar
0

Utilisation de numpy

Si vous avez de très longs flotteurs, vous pouvez les arrondir correctement avec numpy:

import json 

import numpy as np

data = np.array([23.671234, 23.97432, 23.870123])

json.dumps(np.around(data, decimals=2).tolist())

'[23.67, 23.97, 23.87]'

Mikhail
la source
-1

Je viens de publier fjson , une petite bibliothèque Python pour résoudre ce problème. Installer avec

pip install fjson

et utilisez juste comme json, avec l'ajout du float_formatparamètre:

import math
import fjson


data = {"a": 1, "b": math.pi}
print(fjson.dumps(data, float_format=".6e", indent=2))
{
  "a": 1,
  "b": 3.141593e+00
}
Nico Schlömer
la source