¿Cómo establecer el número de dígitos flotantes que produce JSONEncoder?

Estoy intentando configurar la biblioteca python json para guardar y archivar un diccionario que contenga como elementos otros diccionarios. Hay muchos números flotantes y me gustaría limitar el número de dígitos a, por ejemplo, 7.

De acuerdo con otras publicaciones en el encoder.FLOAT_REPR SO, se utilizará encoder.FLOAT_REPR . Sin embargo no está funcionando.

Por ejemplo, el siguiente código, ejecutado en Python3.7.1, imprime todos los dígitos:

 import json json.encoder.FLOAT_REPR = lambda o: format(o, '.7f' ) d = dict() d['val'] = 5.78686876876089075543 d['name'] = 'kjbkjbkj' f = open('test.json', 'w') json.dump(d, f, indent=4) f.close() 

¿Cómo puedo resolver eso?

Puede ser irrelevante pero estoy en OSX.

EDITAR

Esta pregunta fue marcada como duplicada. Sin embargo, en la respuesta aceptada (y hasta ahora la única) a la publicación original se indica claramente:

Nota: esta solución no funciona en Python 3.6+

Entonces esa solución no es la correcta. Además, está usando la biblioteca simplejson no la biblioteca json .

Opción 1: usar la expresión regular que coincida para redondear

Puede volcar su objeto en una cadena usando json.dumps y luego usar la técnica que se muestra en esta publicación para encontrar y redondear sus números de punto flotante.

Para probarlo, agregué algunas estructuras anidadas más complicadas en la parte superior del ejemplo que proporcionaste:

 d = dict() d['val'] = 5.78686876876089075543 d['name'] = 'kjbkjbkj' d["mylist"] = [1.23456789, 12, 1.23, {"foo": "a", "bar": 9.87654321}] d["mydict"] = {"bar": "b", "foo": 1.92837465} # dump the object to a string d_string = json.dumps(d, indent=4) # find numbers with 8 or more digits after the decimal point pat = re.compile(r"\d+\.\d{8,}") def mround(match): return "{:.7f}".format(float(match.group())) # write the modified string to a file with open('test.json', 'w') as f: f.write(re.sub(pat, mround, d_string)) 

La salida test.json ve como:

 { "val": 5.7868688, "name": "kjbkjbkj", "mylist": [ 1.2345679, 12, 1.23, { "foo": "a", "bar": 9.8765432 } ], "mydict": { "bar": "b", "foo": 1.9283747 } } 

Una limitación de este método es que también coincidirá con los números que están entre comillas dobles (flotantes representados como cadenas). Podría llegar a una expresión regular más restrictiva para manejar esto, dependiendo de sus necesidades.

Opción 2: subclase json.JSONEncoder

Aquí hay algo que funcionará en su ejemplo y manejará la mayoría de los casos de borde que encontrará:

 import json class MyCustomEncoder(json.JSONEncoder): def iterencode(self, obj): if isinstance(obj, float): yield format(obj, '.7f') elif isinstance(obj, dict): last_index = len(obj) - 1 yield '{' i = 0 for key, value in obj.items(): yield '"' + key + '": ' for chunk in MyCustomEncoder.iterencode(self, value): yield chunk if i != last_index: yield ", " i+=1 yield '}' elif isinstance(obj, list): last_index = len(obj) - 1 yield "[" for i, o in enumerate(obj): for chunk in MyCustomEncoder.iterencode(self, o): yield chunk if i != last_index: yield ", " yield "]" else: for chunk in json.JSONEncoder.iterencode(self, obj): yield chunk 

Ahora escribe el archivo usando el codificador personalizado.

 with open('test.json', 'w') as f: json.dump(d, f, cls = MyCustomEncoder) 

El archivo de salida test.json :

 {"val": 5.7868688, "name": "kjbkjbkj", "mylist": [1.2345679, 12, 1.2300000, {"foo": "a", "bar": 9.8765432}], "mydict": {"bar": "b", "foo": 1.9283747}} 

Para que otros argumentos de palabras clave como la indent funcionen, la forma más sencilla sería leer el archivo que se acaba de escribir y volver a escribir con el codificador predeterminado:

 # write d using custom encoder with open('test.json', 'w') as f: json.dump(d, f, cls = MyCustomEncoder) # load output into new_d with open('test.json', 'r') as f: new_d = json.load(f) # write new_d out using default encoder with open('test.json', 'w') as f: json.dump(new_d, f, indent=4) 

Ahora el archivo de salida es el mismo que se muestra en la opción 1.

Aquí hay algo que puede usar que se basa en mi respuesta a la pregunta:

Escribir lista bidimensional en el archivo JSON .

Digo que puede porque requiere “envolver” todos los valores flotantes en el diccionario de Python (o en la lista) antes de que JSON lo codifique con dump() .

(Probado con Python 3.7.2.)

 from _ctypes import PyObj_FromPtr import json import re class FloatWrapper(object): """ Float value wrapper. """ def __init__(self, value): self.value = value class MyEncoder(json.JSONEncoder): FORMAT_SPEC = '@@{}@@' regex = re.compile(FORMAT_SPEC.format(r'(\d+)')) # regex: r'@@(\d+)@@' def default(self, obj): return (self.FORMAT_SPEC.format(id(obj)) if isinstance(obj, FloatWrapper) else super(MyEncoder, self).default(obj)) def iterencode(self, obj, **kwargs): for encoded in super(MyEncoder, self).iterencode(obj, **kwargs): # Check for marked-up float values (FloatWrapper instances). match = self.regex.search(encoded) if match: # Get FloatWrapper instance. id = int(match.group(1)) float_wrapper = PyObj_FromPtr(id) json_obj_repr = '%.7f' % float_wrapper.value # Create alt repr. encoded = encoded.replace( '"{}"'.format(self.FORMAT_SPEC.format(id)), json_obj_repr) yield encoded d = dict() d['val'] = FloatWrapper(5.78686876876089075543) # Must wrap float values. d['name'] = 'kjbkjbkj' with open('float_test.json', 'w') as file: json.dump(d, file, cls=MyEncoder, indent=4) 

Contenido del archivo creado:

 { "val": 5.7868688, "name": "kjbkjbkj" } 

Actualizar:

Como mencioné, lo anterior requiere que todos los valores float estén ajustados antes de llamar a json.dump() . Afortunadamente, esto se puede automatizar agregando y utilizando la siguiente utilidad (mínimamente probada):

 def wrap_type(obj, kind, wrapper): """ Recursively wrap instances of type kind in dictionary and list objects. """ if isinstance(obj, dict): new_dict = {} for key, value in obj.items(): if not isinstance(value, (dict, list)): new_dict[key] = wrapper(value) if isinstance(value, kind) else value else: new_dict[key] = wrap_type(value, kind, wrapper) return new_dict elif isinstance(obj, list): new_list = [] for value in obj: if not isinstance(value, (dict, list)): new_list.append(wrapper(value) if isinstance(value, kind) else value) else: new_list.append(wrap_type(value, kind, wrapper)) return new_list else: return obj d = dict() d['val'] = 5.78686876876089075543 d['name'] = 'kjbkjbkj' with open('float_test.json', 'w') as file: json.dump(wrap_type(d, float, FloatWrapper), file, cls=MyEncoder, indent=4) 

No responde a esta pregunta, pero por el lado de la deencoding, podría hacer algo como esto, o anular el método de enlace.

Sin embargo, para resolver este problema con este método se requeriría la encoding, la deencoding y luego la encoding nuevamente, lo cual es demasiado complicado y ya no es la mejor opción. Asumí que Encode tenía todas las campanas y silbidos que tenía Decode, error mío.

 # d = dict() class Round7FloatEncoder(json.JSONEncoder): def iterencode(self, obj): if isinstance(obj, float): yield format(obj, '.7f') with open('test.json', 'w') as f: json.dump(d, f, cls=Round7FloatEncoder) 

Podría usar la función de formato de cadena para convertir su número en una cadena con solo 7 puntos decimales. Luego conviértelo de nuevo a un flotador como este:

 float("{:.7f}".format(5.78686876876089075543)) 

Los corchetes en la cadena le dicen al formater que debe seguir las reglas que se encuentran dentro.

Los dos puntos comienzan la regla de formato.

el 7 le dice al formater cómo el hombre coloca después del decimal para irse.

y la f significa formatear un flotador.

luego pase su número a la función de formato que devuelve: '5.7868688' , luego puede volver a pasar a la función de flotador para salir a flote.

Busque aquí más información sobre la función de formato en python: https://pyformat.info/