Formatee los flotadores con el módulo json estándar

100

Estoy usando el módulo json estándar en python 2.6 para serializar una lista de flotantes. Sin embargo, obtengo resultados como este:

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

Quiero que los flotantes estén formateados con solo dos dígitos decimales. La salida debería verse así:

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

He intentado definir mi propia clase de codificador JSON:

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

Esto funciona para un único objeto flotante:

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

Pero falla para los objetos anidados:

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

No quiero tener dependencias externas, por lo que prefiero seguir con el módulo json estándar.

¿Cómo puedo conseguir esto?

Manuel Ceron
fuente

Respuestas:

80

Nota: esto no funciona en ninguna versión reciente de Python.

Desafortunadamente, creo que debe hacer esto mediante un parche de mono (que, en mi opinión, indica un defecto de diseño en el jsonpaquete de biblioteca estándar ). Por ejemplo, este código:

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

emite:

23.67
[23.67, 23.97, 23.87]

Como desees. Obviamente, debería haber una manera de anular la arquitectura FLOAT_REPRpara que CADA representación de un flotador esté bajo su control si así lo desea; pero desafortunadamente no es así como jsonse diseñó el paquete :-(.

Alex Martelli
fuente
10
Esta solución no funciona en Python 2.7 con la versión C de Python del codificador JSON.
Nelson
25
Independientemente de cómo lo haga, utilice algo como% .15g o% .12g en lugar de% .3f.
Guido van Rossum
23
Encontré este fragmento en el código de un programador junior. Esto habría creado un error muy grave pero sutil si no se hubiera detectado. ¿Puede colocar una advertencia en este código que explique las implicaciones globales de este parche de mono?
Rory Hart
12
Es una buena higiene retrasarlo cuando haya terminado: 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
Como han señalado otros, esto ya no funciona en al menos Python 3.6+. Agregue algunos dígitos para 23.67ver cómo .2fno se respeta.
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])))

emite

[23.67, 23.97, 23.87]

No es necesario parchear los monos.

Tom Wuttke
fuente
2
Me gusta esta solución; mejor integración y funciona con 2.7. Debido a que de todos modos estoy construyendo los datos yo mismo, eliminé la pretty_floatsfunción y simplemente la integré en mi otro código.
mikepurvis
1
En Python3 da el error "El objeto de mapa no es serializable JSON" , pero puede resolver la conversión del mapa () a una lista conlist( map(pretty_floats, obj) )
Guglie
1
@Guglie: eso es porque en Python 3 mapdevuelve un iterador, no unlist
Azat Ibrakov
4
No me funciona (Python 3.5.2, simplejson 3.16.0). Probé con% .6g y [23.671234556, 23.971234556, 23.871234556], todavía imprime el número entero.
szali
27

Si está utilizando Python 2.7, una solución simple es simplemente redondear sus flotadores explícitamente a la precisión deseada.

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

Esto funciona porque Python 2.7 hizo que el redondeo flotante fuera más consistente . Desafortunadamente, esto no funciona en 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'

Las soluciones mencionadas anteriormente son alternativas para 2.6, pero ninguna es del todo adecuada. El parche de mono json.encoder.FLOAT_REPR no funciona si el tiempo de ejecución de Python usa una versión C del módulo JSON. La clase PrettyFloat en la respuesta de Tom Wuttke funciona, pero solo si la codificación% g funciona globalmente para su aplicación. El% .15g es un poco mágico, funciona porque la precisión flotante es de 17 dígitos significativos y% g no imprime ceros finales.

Pasé algún tiempo tratando de hacer un PrettyFloat que permitiera personalizar la precisión de cada número. Es decir, una sintaxis como

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

No es fácil hacerlo bien. Heredar de flotar es incómodo. Heredar de Object y usar una subclase JSONEncoder con su propio método default () debería funcionar, excepto que el módulo json parece asumir que todos los tipos personalizados deberían serializarse como cadenas. Es decir: terminas con la cadena Javascript "0.33" en la salida, no el número 0.33. Puede que todavía haya una forma de hacer que esto funcione, pero es más difícil de lo que parece.

Nelson
fuente
Otro enfoque para Python 2.6 usando JSONEncoder.iterencode y la coincidencia de patrones se puede ver en github.com/migurski/LilJSON/blob/master/liljson.py
Nelson
Con suerte, esto hace que pasar tus flotadores sea más liviano; me gusta cómo podemos evitar jugar con las clases JSON que pueden apestar.
Lincoln B
20

Realmente lamentable que dumpsno te permita hacer nada con los flotadores. Sin embargo lo loadshace. Entonces, si no le importa la carga adicional de la CPU, puede pasarla por el codificador / decodificador / codificador y obtener el resultado correcto:

>>> json.dumps(json.loads(json.dumps([.333333333333, .432432]), parse_float=lambda x: round(float(x), 3)))
'[0.333, 0.432]'
Claude
fuente
Gracias, esta es una sugerencia realmente útil. ¡No sabía nada del parse_floatkwarg!
Anónimo
La sugerencia más simple aquí que también funciona en 3.6.
Brent Faust
Tenga en cuenta la frase "no importa la carga extra de CPU". Definitivamente no use esta solución si tiene muchos datos para serializar. Para mí, agregar esto solo hizo que un programa que realiza un cálculo no trivial tomara 3 veces más tiempo.
shaneb
10

Aquí hay una solución que funcionó para mí en Python 3 y no requiere parches de mono:

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 salida es:

[23.63, 23.93, 23.84]

Copia los datos pero con flotadores redondeados.

jcoffland
fuente
9

Si está atascado con Python 2.5 o versiones anteriores: el truco del parche de mono no parece funcionar con el módulo simplejson original si las aceleraciones de C están instaladas:

$ 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
fuente
7

Puede hacer lo que necesite hacer, pero no está documentado:

>>> 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
fuente
5
Se ve bien, pero parece no funcionar en Python 3.6. En particular, no vi una FLOAT_REPRconstante en el json.encodermódulo.
Tomasz Gandor
2

La solución de Alex Martelli funcionará para aplicaciones de un solo subproceso, pero es posible que no funcione para aplicaciones de subprocesos múltiples que necesitan controlar la cantidad de decimales por subproceso. Aquí hay una solución que debería funcionar en aplicaciones de subprocesos múltiples:

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

Simplemente puede establecer encoder.thread_local.decimal_places en el número de lugares decimales que desee, y la próxima llamada a json.dumps () en ese hilo usará ese número de lugares decimales

Anton I. Sipos
fuente
2

Si necesita hacer esto en Python 2.7 sin anular el json.encoder.FLOAT_REPR global, aquí hay una forma.

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)

Luego, 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, no funciona del todo como Matthew Schinckel señala a continuación:

>>> import MyEncoder
>>> enc = MyEncoder()  
>>> enc.encode([23.67, 23.97, 23.87])
'["23.67", "23.97", "23.87"]'
Mike Fogel
fuente
4
Parecen cadenas, no números.
Matthew Schinckel
1

Pros:

  • Funciona con cualquier codificador JSON, o incluso con la repr.
  • Corto (más o menos), parece funcionar.

Contras:

  • Hack de expresiones regulares feo, apenas probado.
  • Complejidad cuadrática.

    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
fuente
1

Al importar el módulo json estándar, basta con cambiar el codificador predeterminado FLOAT_REPR. Realmente no es necesario importar o crear instancias de 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 veces también es muy útil generar como json la mejor representación que Python puede adivinar con str. Esto asegurará que no se ignoren los dígitos significativos.

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
fuente
1

Estoy de acuerdo con @Nelson en que heredar de float es incómodo, pero quizás una solución que solo toque la __repr__función podría ser perdonable. Terminé usando el decimalpaquete para reformatear los flotadores cuando fuera necesario. La ventaja es que esto funciona en todos los contextos donde repr()se está llamando, por lo que también cuando simplemente se imprimen listas en stdout, por ejemplo. Además, la precisión se puede configurar en tiempo de ejecución, después de que se hayan creado los datos. La desventaja es, por supuesto, que sus datos deben convertirse a esta clase flotante especial (ya que, desafortunadamente, parece que no puede parchear float.__repr__). Para eso proporciono una breve función de conversión.

El código:

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

Ejemplo de uso:

>>> 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]]'
usuario1556435
fuente
Esto no funciona con el paquete integrado Python3 json, que no usa __repr __ ().
Ian Goldby
0

Usando numpy

Si realmente tiene flotadores realmente largos, puede redondearlos hacia arriba / abajo correctamente 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]'

Mikhail
fuente
-1

Acabo de lanzar fjson , una pequeña biblioteca de Python para solucionar este problema. Instalar con

pip install fjson

y use como json, con la adición del float_formatparámetro:

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
fuente