web-dev-qa-db-fra.com

Format flottants avec module json standard

J'utilise le standard module json in python 2.6 pour sérialiser une liste de flottants. 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 objet flottant unique:

>>> 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 avoir de dépendances externes, donc je préfère m'en tenir au module json standard.

Comment puis-je atteindre cet objectif?

83
Manuel Ceron

Malheureusement, je crois que vous devez le faire par patch de singe (ce qui, à mon avis, indique un défaut de conception dans le package de la bibliothèque standard json). 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 un moyen architecturé de remplacer FLOAT_REPR pour 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 package json a été conçu :-(.

72
Alex Martelli
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 map(pretty_floats, obj)  # in Python3 do: 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.

56
Tom Wuttke

Si vous utilisez Python 2.7, une solution simple consiste à simplement arrondir explicitement vos flotteurs à 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 fait 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 runtime 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, il fonctionne car la précision du flottant est de 17 chiffres significatifs et% g n'imprime pas les zéros de fin.

J'ai passé un peu de temps à essayer de créer un PrettyFloat qui permettait une personnalisation de la précision pour chaque numéro. C'est à dire, 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 maladroit. L'héritage d'Object et l'utilisation d'une sous-classe JSONEncoder avec sa propre méthode default () devraient fonctionner, sauf que le module json semble supposer que tous les types personnalisés doivent être sérialisés en chaînes. C'est à dire: vous vous retrouvez avec la chaîne Javascript "0,33" dans la sortie, pas le nombre 0,33. Il y a peut-être encore un moyen de faire fonctionner cela, mais c'est plus difficile qu'il n'y paraît.

26
Nelson

Vraiment malheureux que dumps ne vous permette de rien faire pour flotter. Cependant, loads le fait. Donc, si cela ne vous dérange pas la charge CPU supplémentaire, vous pouvez le jeter à 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]'
13
Claude

Si vous êtes coincé avec Python 2.5 ou 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]'
>>> 
9
Carlos Valiente

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]'
7
Ned Batchelder

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

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

4
jcoffland

La solution d'Alex Martelli fonctionnera pour les applications à un seul thread, mais peut ne pas fonctionner pour les applications à plusieurs threads qui doivent contrôler le nombre de décimales par thread. Voici une solution qui devrait fonctionner dans les applications multithread:

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

2
Anton I. Sipos

Si vous devez le faire dans python 2.7 sans redéfinir le json.encoder.FLOAT_REPR global, voici une façon.

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, dans python 2.7:

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

Dans 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"]'
2
Mike Fogel

Lors de l'importation du module json standard, il suffit de changer l'encodeur 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 produire en json la meilleure représentation 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]'
1
F Pereira

Je suis d'accord avec @Nelson que l'héritage de float est maladroit, mais peut-être qu'une solution qui ne touche que la fonction __repr__ Pourrait être pardonnable. J'ai fini par utiliser le package decimal pour cela afin de reformater les flottants en cas de besoin. L'avantage est que cela fonctionne dans tous les contextes où repr() est appelé, donc aussi lors de l'impression de listes sur stdout par exemple. De plus, la précision est configurable lors de 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 utiliser le patch singe float.__repr__). Pour cela, je fournis 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]]'
1
user1556435

Avantages:

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

Les inconvénients:

  • Hack 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
    
1
Sam Watkins