8

I'm running Python 2.7 and I'm trying to create a custom FloatEncoder subclass of JSONEncoder. I've followed many examples such as this but none seem to work. Here is my FloatEncoder class:

class FloatEncoder(JSONEncoder):
    def _iterencode(self, obj, markers=None):
         if isinstance(obj, float):
            return (str(obj) for obj in [obj])
        return super(FloatEncoder, self)._iterencode(obj, markers)

And here is where I call json.dumps:

with patch("utils.fileio.FloatEncoder") as float_patch:
        for val,res in ((.00123456,'0.0012'),(.00009,'0.0001'),(0.99999,'1.0000'),({'hello':1.00001,'world':[True,1.00009]},'{"world": [true, 1.0001], "hello": 1.0000}')): 
            untrusted = dumps(val, cls=FloatEncoder)
            self.assertTrue(float_patch._iterencode.called)
            self.assertEqual(untrusted, res)

The first assertion fails, meaning that _iterencode is not being executed. After reading the JSON documentation,I tried overriding the default() method but that also was not being called.

Community
  • 1
  • 1
Matt
  • 1,021
  • 4
  • 16
  • 25
  • 5
    FWIW, `default()` is not being called because if the input is one of the types the encoder supports by default, it will not even look at your custom method. Compare `lib/json/encoder.py`, in the definition of `_iterencode()`: The `_default()` is only called in the `else:` branch, after all known types have been covered. Therefore you cannot override handling for a known type. – Tomalak Aug 11 '12 at 09:07

2 Answers2

2

You seem to be trying to round float values down to 4 decimal points while generating JSON (based on test examples).

JSONEncoder shipping with Python 2.7 does not have have _iterencode method, so that's why it's not getting called. Also a quick glance at json/encoder.py suggests that this class is written in such a way that makes it difficult to change the float encoding behavior. Perhaps, it would be better to separate concerns, and round the floats before doing JSON serialization.

EDIT: Alex Martelli also supplies a monkey-patch solution in a related answer. The problem with that approach is that you're introducing a global modification to json library behavior that may unwittingly affect some other piece of code in your application that was written with assumption that floats were encoded without rounding.

Try this:

from collections import Mapping, Sequence
from unittest import TestCase, main
from json import dumps

def round_floats(o):
    if isinstance(o, float):
        return round(o, 4)
    elif isinstance(o, basestring):
        return o
    elif isinstance(o, Sequence):
        return [round_floats(item) for item in o]
    elif isinstance(o, Mapping):
        return dict((key, round_floats(value)) for key, value in o.iteritems())
    else:
        return o

class TestFoo(TestCase):
    def test_it(self):
        for val, res in ((.00123456, '0.0012'),
                         (.00009, '0.0001'),
                         (0.99999, '1.0'),
                         ({'hello': 1.00001, 'world': [True, 1.00009]},
                          '{"world": [true, 1.0001], "hello": 1.0}')):
            untrusted = dumps(round_floats(val))
            self.assertEqual(untrusted, res)

if __name__ == '__main__':
    main()
Community
  • 1
  • 1
Pavel Repin
  • 30,663
  • 1
  • 34
  • 41
0

Don't define _iterencode, define default, as shown in the third answer on that page.

Daniel Roseman
  • 588,541
  • 66
  • 880
  • 895
  • 1
    I did try that as well but it's the same problem: the default() method is not being called. – Matt Jun 20 '11 at 15:55