Pregunta Flotadores de formato con el módulo json estándar


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

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

Quiero que los flotadores se formateen 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 JSON Encoder:

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, así que prefiero seguir con el módulo json estándar.

¿Cómo puedo conseguir esto?


73
2017-09-19 00:08


origen


Respuestas:


Desafortunadamente, creo que tienes que hacer esto mediante el parche de mono (que, en mi opinión, indica un defecto de diseño en la biblioteca estándar) json paquete). 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, debe haber una forma de arquitectura para anular FLOAT_REPR de modo que CADA representación de un flotador está bajo su control si así lo desea; pero lamentablemente no es así como json el paquete fue diseñado :-(.


67
2017-09-19 02:48



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)             
    return obj

print simplejson.dumps(pretty_floats([23.67, 23.97, 23.87]))

Emite

[23.67, 23.97, 23.87]

No es necesario monopatching.


51
2017-11-14 03:16



Si está utilizando Python 2.7, una solución simple es simplemente redondear sus flotadores de forma explícita 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 flotador redondeando más consistente. Lamentablemente, 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 soluciones para 2.6, pero ninguna es totalmente 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 del flotador es de 17 dígitos significativos y% g no imprime ceros finales.

Pasé un tiempo tratando de crear 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 hacer esto bien. Heredar de la carroza es incómodo. Heredar de Object y usar una subclase JSONEncoder con su propio método predeterminado () debería funcionar, excepto que el módulo json parece suponer que todos los tipos personalizados se deben serializar como cadenas. Es decir: termina con la cadena de Javascript "0.33" en la salida, no en el número 0.33. Puede haber una forma de hacerlo funcionar, pero es más difícil de lo que parece.


23
2018-04-06 23:29



Puede hacer lo que necesita 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]'

8
2017-09-19 02:40



Si está atrapado con Python 2.5 o versiones anteriores: El truco de mono-parche no parece funcionar con el módulo simplejson original si las aceleraciones 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]'
>>> 

7
2017-10-29 19:15



Realmente desafortunado dumps no te permite hacer nada para flotar. sin embargo loads hace. Entonces, si no te importa la carga adicional de la CPU, puedes 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]'

5
2018-03-15 21:30



La solución de Alex Martelli funcionará para aplicaciones de subproceso único, pero puede no funcionar 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 multiproceso:

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 codder.thread_local.decimal_places en el número de decimales que desea, y la siguiente llamada a json.dumps () en ese hilo usará ese número de decimales


3
2017-07-20 23:35



Pros:

  • Funciona con cualquier codificador JSON, o incluso con répres de python.
  • Corto (ish), parece funcionar.

Contras:

  • Uffly regexp hack, 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
    

1
2018-03-19 20:34