2009-09-19 14 views
65

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

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

Quiero que los flotadores a ser formateados con sólo dos dígitos decimales. La salida debe tener este aspecto:

>>> 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 objeto flotante única:

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

Pero falla por anidada objetos:

>>> 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 lograr esto?

Respuesta

62

Por desgracia, creo que usted tiene que hacer esto mediante la aplicación de parches mono-(que, a mi opinión, indica un diseño defecto en el paquete de biblioteca estándar json). 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 se desee. Obviamente, debe haber una forma arquitectónica para anular FLOAT_REPR de modo que CADA representación de un flotador esté bajo su control si lo desea; pero lamentablemente no es así como se diseñó el paquete json :-(.

+8

Esta solución no funciona en Python 2.7 utilizando la versión de Python C del codificador JSON. – Nelson

+1

Funciona para mí en Python 2.7.3. – MiniQuark

+15

Sin embargo, haga esto, use algo como% .15g o% .12g en vez de% .3f. –

8

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

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 son instalado:.

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

Sin monkeypatching necesaria

+2

Me gusta esta solución; mejor integración, y funciona con 2.7. Como de todos modos estoy construyendo los datos, eliminé la función 'pretty_floats' y simplemente la integé en mi otro código. – mikepurvis

22

Si está utilizando Python 2.7, una solución simple es simplemente redondear sus flotadores de manera 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 float rounding more consistent. 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 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é algún tiempo intentando 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 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.

+0

Otro enfoque para Python 2.6 usando JSONEncoder.iterencode y la coincidencia de patrones se puede ver en https://github.com/migurski/LilJSON/blob/master/liljson.py – Nelson

+0

Espero que esto haga que el pasar alrededor de tus carrozas sea más liviano. Me gusta cómo podemos evitar jugar con las clases JSON que pueden apestar. –

1

Si necesita hacer esto en python 2.7 sin sobreescribir el json.encoder global.FLOAT_REPR, aquí hay una manera.

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 tan Mateo Schinckel señala a continuación:

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

Esos parecen cuerdas, no números. –

+0

¿Funciona en cualquier nivel de anidación? – Mark

3

solución de Alex Martelli va a trabajar para aplicaciones de subproceso único, pero es posible que no funcionen para aplicaciones de subprocesos múltiples que necesitan controlar el número de lugares decimales por subproceso. Aquí es una solución que debería funcionar en múltiples hilos:

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

Usted sólo puede establecer encoder.thread_local.decimal_places al número de cifras decimales que desea, y la siguiente llamada a json.dumps() en ese hilo utilizará ese número de decimales

1

Pros:

  • funciona con cualquier codificador JSON, o incluso en representación del pitón.
  • Corto (ish), parece que funciona.

Contras:

  • de hackers regexp 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 
    
2

Al importar el módulo json estándar, es suficiente para cambiar la FLOAT_REPR codificador predeterminado. Realmente no existe la necesidad de 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 para mostrar como json la mejor representación que python puede adivinar con str. Esto asegurará que los dígitos significativos no sean ignorados.

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

realmente lamentable que dumps no permite que hagas nada a los flotadores. Sin embargo, loads sí.Así que si no te importa la carga de CPU adicional, que podría lanzar a través del 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]' 
+0

Gracias, esta es una sugerencia realmente útil. ¡No sabía sobre el 'parse_float' kwarg! – Anonymous

1

Estoy de acuerdo con @Nelson que la herencia de flotación es torpe, pero quizás una solución que solo toca la función __repr__ puede ser perdonable. Terminé usando el paquete decimal para reformatear flotadores cuando sea necesario. Lo bueno es que esto funciona en todos los contextos en los que se llama al repr(), por lo que también cuando se imprimen listas a stdout, por ejemplo. Además, la precisión es configurable en tiempo de ejecución, una vez que se han creado los datos. Lo malo es, por supuesto, que sus datos deben convertirse a esta clase de flotación especial (como desafortunadamente no se puede ver el parche de mono float.__repr__). Para eso proporciono una breve función de conversión.

el código: ejemplo

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 

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