2

I would like to represent floats in a JSON file in scientific notation using Python 3.6+. None of

import json

a = 0.001234567

print(json.dumps(a))

json.encoder.FLOAT_REPR = lambda x: "{:e}".format(x)
print(json.dumps(a))

json.encoder.c_make_encoder = None
json.encoder.FLOAT_REPR = lambda x: "{:e}".format(x)
print(json.dumps(a))

work: All three prints give

0.001234567

instead of the desired

1.234567e-03

(Note that the last version works at least in in Python 2.7.15rc1.)

The answer should work with lists of floats as well.

Any hints?

Jean-François Fabre
  • 137,073
  • 23
  • 153
  • 219
Nico Schlömer
  • 53,797
  • 27
  • 201
  • 249

2 Answers2

3

You have to add some special casing for dicts, lists, sets, etc., but by referencing abstract base classes from collections.abc, you avoid explicitly testing for specific types.

Note that the test for Sequence has to avoid matching on str types, since iterating over a str gives a bunch of 1-character strs, which are also iterable Sequences, and so on until you reach the recursion limit. I could not find an ABC that represents "a sequence container, but not a str".

(I also have to echo Alex Martelli's criticism from a related post, that having to do this much work just to format a particular type speaks to issues in the design of the classes in this module.)

import json
from collections.abc import Mapping, Sequence

a = 0.001234567

class ScientificNotationEncoder(json.JSONEncoder):
    def iterencode(self, o, _one_shot=False):
        if isinstance(o, float):
            return "{:e}".format(o)
        elif isinstance(o, Mapping):
            return "{{{}}}".format(', '.join('"{}" : {}'.format(str(ok), self.iterencode(ov))
                                             for ok, ov in o.items()))
        elif isinstance(o, Sequence) and not isinstance(o, str):
            return "[{}]".format(', '.join(map(self.iterencode, o)))
        return ', '.join(super().iterencode(o, _one_shot))

aout = json.dumps([a, a, "xyzzy", 42, {'z': a}, (a, a, a),],
                  cls=ScientificNotationEncoder)
print(aout)

# loading back in seems to still work okay!
print(json.loads(aout))

Prints:

[1.234567e-03, 1.234567e-03, "xyzzy", 42, {"z" : 1.234567e-03}, [1.234567e-03, 1.234567e-03, 1.234567e-03]]
[0.001234567, 0.001234567, 'xyzzy', 42, {'z': 0.001234567}, [0.001234567, 0.001234567, 0.001234567]]
mkrieger1
  • 19,194
  • 5
  • 54
  • 65
PaulMcG
  • 62,419
  • 16
  • 94
  • 130
  • Thanks for the answer. This only works when converting a _single_ float. Put in `[a, a]` and it won't work. – Nico Schlömer Jun 05 '18 at 13:20
  • Not working for dicts à la `{"key": 0.00123}` now. This will probably be an uphill battle if all cases have to be listed separately. – Nico Schlömer Jun 05 '18 at 13:43
  • Maybe not so terrible if you can use abstract types from `collections.abc` – PaulMcG Jun 05 '18 at 14:53
  • Discussion on meta about your reopening: https://meta.stackoverflow.com/q/369085/4909087 – cs95 Jun 06 '18 at 06:09
  • 1
    this code of this answer looks slightly superior to https://stackoverflow.com/a/1733105/6451573 which should be the original question for this question. I think it would make better sense if it was posted there, and with a little more explanation. – Jean-François Fabre Jun 06 '18 at 07:22
0

Eventually I created my own json library that can format floats, fjson. Install with

pip install fjson

and use as

import math
import fjson


data = {"a": 1, "b": math.pi}
string = fjson.dumps(data, float_format=".6e", indent=2, separators=(", ", ": "))
print(string)
{
  "a": 1,
  "b": 3.141593e+00
}
Nico Schlömer
  • 53,797
  • 27
  • 201
  • 249