Formatta i float con il modulo json standard


100

Sto usando il modulo json standard in python 2.6 per serializzare un elenco di float. Tuttavia, ottengo risultati come questo:

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

Voglio che i float siano formattati con solo due cifre decimali. L'output dovrebbe essere simile a questo:

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

Ho provato a definire la mia classe JSON Encoder:

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

Funziona per un unico oggetto float:

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

Ma fallisce per gli oggetti annidati:

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

Non voglio avere dipendenze esterne, quindi preferisco restare con il modulo json standard.

Come posso raggiungere questo obiettivo?

Risposte:


80

Nota: questo non funziona in nessuna versione recente di Python.

Sfortunatamente, credo che tu debba farlo mediante patch di scimmia (che, a mio parere, indica un difetto di progettazione nel jsonpacchetto della libreria standard ). Ad esempio, questo codice:

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]))

emette:

23.67
[23.67, 23.97, 23.87]

come desideri. Ovviamente, dovrebbe esserci un modo progettato per eseguire l'override in FLOAT_REPRmodo che OGNI rappresentazione di un galleggiante sia sotto il tuo controllo, se lo desideri; ma sfortunatamente non è così jsonche è stato progettato il pacchetto :-(.


10
Questa soluzione non funziona in Python 2.7 utilizzando la versione C di Python del codificatore JSON.
Nelson

25
Comunque tu faccia questo, usa qualcosa come% .15g o% .12g invece di% .3f.
Guido van Rossum

23
Ho trovato questo frammento nel codice di un programmatore junior. Questo avrebbe creato un bug molto grave ma sottile se non fosse stato catturato. Puoi inserire un avviso su questo codice che spiega le implicazioni globali di questa patch scimmia.
Rory Hart

12
È buona igiene rimetterlo a posto quando hai finito: 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
Come altri hanno sottolineato, questo non funziona più almeno in Python 3.6+. Aggiungi alcune cifre per 23.67vedere come .2fnon viene rispettato.
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])))

emette

[23.67, 23.97, 23.87]

Nessun Monkeypatching necessario.


2
Mi piace questa soluzione; migliore integrazione e funziona con 2.7. Poiché comunque sto costruendo i dati da solo, ho eliminato la pretty_floatsfunzione e l'ho semplicemente integrata nel mio altro codice.
mikepurvis

1
In Python3 restituisce l'errore "Map object is not JSON serializable" , ma puoi risolvere la conversione di map () in un elenco conlist( map(pretty_floats, obj) )
Guglie

1
@Guglie: questo perché in Python 3 maprestituisce l'iteratore, non unlist
Azat Ibrakov

4
Non funziona per me (Python 3.5.2, simplejson 3.16.0). Ho provato con% .6g e [23.671234556, 23.971234556, 23.871234556], stampa ancora il numero intero.
szali

27

Se stai usando Python 2.7, una soluzione semplice è semplicemente arrotondare i tuoi float esplicitamente alla precisione desiderata.

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

Questo funziona perché Python 2.7 ha reso l' arrotondamento a virgola mobile più coerente . Sfortunatamente questo non funziona in 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'

Le soluzioni sopra menzionate sono soluzioni alternative per 2.6, ma nessuna è del tutto adeguata. Monkey patching json.encoder.FLOAT_REPR non funziona se il tuo runtime Python utilizza una versione C del modulo JSON. La classe PrettyFloat nella risposta di Tom Wuttke funziona, ma solo se la codifica% g funziona globalmente per la tua applicazione. % .15g è un po 'magico, funziona perché la precisione del float è di 17 cifre significative e% g non stampa gli zeri finali.

Ho passato un po 'di tempo a provare a creare un PrettyFloat che consentisse la personalizzazione della precisione per ogni numero. Cioè, una sintassi come

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

Non è facile farlo bene. Ereditare da float è scomodo. Ereditare da Object e utilizzare una sottoclasse JSONEncoder con il proprio metodo default () dovrebbe funzionare, tranne per il fatto che il modulo json sembra presumere che tutti i tipi personalizzati debbano essere serializzati come stringhe. Vale a dire: si finisce con la stringa Javascript "0.33" nell'output, non il numero 0.33. Potrebbe esserci ancora un modo per farlo funzionare, ma è più difficile di quanto sembri.


Un altro approccio per Python 2.6 usando JSONEncoder.iterencode e il pattern matching può essere visto su github.com/migurski/LilJSON/blob/master/liljson.py
Nelson

Si spera che questo renda il passaggio dei galleggianti più leggero - Mi piace come possiamo evitare di fare confusione con le classi JSON che possono fare schifo.
Lincoln B

20

Davvero un peccato che dumpsnon ti permetta di fare nulla per galleggiare. Tuttavia loadsfa. Quindi, se non ti dispiace il carico aggiuntivo della CPU, puoi lanciarlo attraverso l'encoder / decoder / encoder e ottenere il risultato giusto:

>>> json.dumps(json.loads(json.dumps([.333333333333, .432432]), parse_float=lambda x: round(float(x), 3)))
'[0.333, 0.432]'

Grazie, questo è un suggerimento davvero utile. Non sapevo del parse_floatkwarg!
Anonimo

Il suggerimento più semplice qui che funziona anche in 3.6.
Brent Faust

Nota la frase "non preoccuparti del carico aggiuntivo della CPU". Sicuramente non utilizzare questa soluzione se hai molti dati da serializzare. Per me, aggiungendo questo da solo, un programma che esegue un calcolo non banale richiede 3 volte più tempo.
Shaneb

11

Ecco una soluzione che ha funzionato per me in Python 3 e non richiede patch di scimmia:

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]))

L'output è:

[23.63, 23.93, 23.84]

Copia i dati ma con float arrotondati.


9

Se sei bloccato con Python 2.5 o versioni precedenti: il trucco della patch di scimmia non sembra funzionare con il modulo simplejson originale se sono installati gli acceleratori C:

$ 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]'
>>> 

7

Puoi fare quello che devi fare, ma non è documentato:

>>> import json
>>> json.encoder.FLOAT_REPR = lambda f: ("%.2f" % f)
>>> json.dumps([23.67, 23.97, 23.87])
'[23.67, 23.97, 23.87]'

5
Sembra pulito, ma sembra non funzionare su Python 3.6. In particolare, non ho visto una FLOAT_REPRcostante nel json.encodermodulo.
Tomasz Gandor

2

La soluzione di Alex Martelli funzionerà per le app a thread singolo, ma potrebbe non funzionare per le app multi-thread che devono controllare il numero di posizioni decimali per thread. Ecco una soluzione che dovrebbe funzionare nelle app multi thread:

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

Puoi semplicemente impostare encoder.thread_local.decimal_places sul numero di posizioni decimali che desideri, e la prossima chiamata a json.dumps () in quel thread utilizzerà quel numero di posizioni decimali


2

Se hai bisogno di farlo in python 2.7 senza sovrascrivere il json.encoder.FLOAT_REPR globale, ecco un modo.

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)

Quindi, in Python 2.7:

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

In python 2.6, non funziona come sottolinea Matthew Schinckel di seguito:

>>> import MyEncoder
>>> enc = MyEncoder()  
>>> enc.encode([23.67, 23.97, 23.87])
'["23.67", "23.97", "23.87"]'

4
Quelle sembrano stringhe, non numeri.
Matthew Schinckel

1

Professionisti:

  • Funziona con qualsiasi codificatore JSON o anche con la riproduzione di Python.
  • Breve (ish), sembra funzionare.

Contro:

  • Brutto hack regexp, appena testato.
  • Complessità quadratica.

    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

Quando si importa il modulo json standard, è sufficiente modificare l'encoder predefinito FLOAT_REPR. Non c'è davvero la necessità di importare o creare istanze di 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]'

A volte è anche molto utile produrre come json la migliore rappresentazione che Python può indovinare con str. Ciò assicurerà che le cifre significative non vengano ignorate.

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

Sono d'accordo con @Nelson sul fatto che ereditare da float è scomodo, ma forse una soluzione che tocca solo la __repr__funzione potrebbe essere perdonabile. Ho finito per usare il decimalpacchetto per questo per riformattare i float quando necessario. L'aspetto positivo è che funziona in tutti i contesti in cui repr()viene chiamato, quindi anche quando si stampano semplicemente elenchi su stdout, ad esempio. Inoltre, la precisione è configurabile in fase di esecuzione, dopo che i dati sono stati creati. Il rovescio della medaglia è ovviamente che i tuoi dati devono essere convertiti in questa speciale classe float (dato che sfortunatamente non puoi sembrare una patch di scimmia float.__repr__). Per questo fornisco una breve funzione di conversione.

Il codice:

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

Esempio di utilizzo:

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

Questo non funziona con il pacchetto json Python3 integrato, che non usa __repr __ ().
Ian Goldby

0

Utilizzando numpy

Se in realtà hai float molto lunghi puoi arrotondarli correttamente per eccesso / per difetto con 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]'


-1

Ho appena rilasciato fjson , una piccola libreria Python per risolvere questo problema. Installa con

pip install fjson

e usa esattamente come json, con l'aggiunta del float_formatparametro:

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
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.