可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I have a Decimal('3.9')
as part of an object, and wish to encode this to a JSON string which should look like {'x': 3.9}
. I don't care about precision on the client side, so a float is fine.
Is there a good way to serialize this? JSONDecoder doesn't accept Decimal objects, and converting to a float beforehand yields {'x': 3.8999999999999999}
which is wrong, and will be a big waste of bandwidth.
回答1:
How about subclassing json.JSONEncoder
?
class DecimalEncoder(json.JSONEncoder): def _iterencode(self, o, markers=None): if isinstance(o, decimal.Decimal): # wanted a simple yield str(o) in the next line, # but that would mean a yield on the line with super(...), # which wouldn't work (see my comment below), so... return (str(o) for o in [o]) return super(DecimalEncoder, self)._iterencode(o, markers)
Then use it like so:
json.dumps({'x': decimal.Decimal('5.5')}, cls=DecimalEncoder)
回答2:
Simplejson 2.1 and higher has native support for Decimal type:
>>> json.dumps(Decimal('3.9'), use_decimal=True) '3.9'
Note that use_decimal
is True
by default:
def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, encoding='utf-8', default=None, use_decimal=True, namedtuple_as_object=True, tuple_as_array=True, bigint_as_string=False, sort_keys=False, item_sort_key=None, for_json=False, ignore_nan=False, **kw):
So:
>>> json.dumps(Decimal('3.9')) '3.9'
Hopefully, this feature will be included in standard library.
回答3:
class DecimalEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, decimal.Decimal): return float(o) return super(DecimalEncoder, self).default(o)
This should hopefully help anyone who is having problems with Python 2.7. I tested it and it seems to work fine. If anyone notices any bugs in my solution or comes up with a better way, please let me know.
回答4:
I tried switching from simplejson to builtin json for GAE 2.7, and had issues with the decimal. If default returned str(o) there were quotes (because _iterencode calls _iterencode on the results of default), and float(o) would remove trailing 0.
If default returns an object of a class that inherits from float (or anything that calls repr without additional formatting) and has a custom __repr__ method, it seems to work like I want it to.
import json from decimal import Decimal class fakefloat(float): def __init__(self, value): self._value = value def __repr__(self): return str(self._value) def defaultencode(o): if isinstance(o, Decimal): # Subclass float with custom repr? return fakefloat(o) raise TypeError(repr(o) + " is not JSON serializable") json.dumps([10.20, "10.20", Decimal('10.20')], default=defaultencode) '[10.2, "10.20", 10.20]'
回答5:
In my Flask app, Which uses python 2.7.11, flask alchemy(with 'db.decimal' types), and Flask Marshmallow ( for 'instant' serializer and deserializer), i had this error, every time i did a GET or POST. The serializer and deserializer, failed to convert Decimal types into any JSON identifiable format.
I did a "pip install simplejson", then Just by adding
import simplejson as json
the serializer and deserializer starts to purr again. I did nothing else... DEciamls are displayed as '234.00' float format.
回答6:
3.9
can not be exactly represented in IEEE floats, it will always come as 3.8999999999999999
, e.g. try print repr(3.9)
, you can read more about it here:
http://en.wikipedia.org/wiki/Floating_point
http://docs.sun.com/source/806-3568/ncg_goldberg.html
So if you don't want float, only option you have to send it as string, and to allow automatic conversion of decimal objects to JSON, do something like this:
import decimal from django.utils import simplejson def json_encode_decimal(obj): if isinstance(obj, decimal.Decimal): return str(obj) raise TypeError(repr(obj) + " is not JSON serializable") d = decimal.Decimal('3.5') print simplejson.dumps([d], default=json_encode_decimal)
回答7:
My $.02!
I extend a bunch of the JSON encoder since I am serializing tons of data for my web server. Here's some nice code. Note that it's easily extendable to pretty much any data format you feel like and will reproduce 3.9 as "thing": 3.9
JSONEncoder_olddefault = json.JSONEncoder.default def JSONEncoder_newdefault(self, o): if isinstance(o, UUID): return str(o) if isinstance(o, datetime): return str(o) if isinstance(o, time.struct_time): return datetime.fromtimestamp(time.mktime(o)) if isinstance(o, decimal.Decimal): return str(o) return JSONEncoder_olddefault(self, o) json.JSONEncoder.default = JSONEncoder_newdefault
Makes my life so much easier...
回答8:
This is what I have, extracted from our class
class CommonJSONEncoder(json.JSONEncoder): """ Common JSON Encoder json.dumps(myString, cls=CommonJSONEncoder) """ def default(self, obj): if isinstance(obj, decimal.Decimal): return {'type{decimal}': str(obj)} class CommonJSONDecoder(json.JSONDecoder): """ Common JSON Encoder json.loads(myString, cls=CommonJSONEncoder) """ @classmethod def object_hook(cls, obj): for key in obj: if isinstance(key, six.string_types): if 'type{decimal}' == key: try: return decimal.Decimal(obj[key]) except: pass def __init__(self, **kwargs): kwargs['object_hook'] = self.object_hook super(CommonJSONDecoder, self).__init__(**kwargs)
Which passes unittest:
def test_encode_and_decode_decimal(self): obj = Decimal('1.11') result = json.dumps(obj, cls=CommonJSONEncoder) self.assertTrue('type{decimal}' in result) new_obj = json.loads(result, cls=CommonJSONDecoder) self.assertEqual(new_obj, obj) obj = {'test': Decimal('1.11')} result = json.dumps(obj, cls=CommonJSONEncoder) self.assertTrue('type{decimal}' in result) new_obj = json.loads(result, cls=CommonJSONDecoder) self.assertEqual(new_obj, obj) obj = {'test': {'abc': Decimal('1.11')}} result = json.dumps(obj, cls=CommonJSONEncoder) self.assertTrue('type{decimal}' in result) new_obj = json.loads(result, cls=CommonJSONDecoder) self.assertEqual(new_obj, obj)
回答9:
Based on stdOrgnlDave answer I have defined this wrapper that it can be called with optional kinds so the encoder will work only for certain kinds inside your projects. I believe the work should be done inside your code and not to use this "default" encoder since "it is better explicit than implicit", but I understand using this will save some of your time. :-)
import time import json import decimal from uuid import UUID from datetime import datetime def JSONEncoder_newdefault(kind=['uuid', 'datetime', 'time', 'decimal']): ''' JSON Encoder newdfeault is a wrapper capable of encoding several kinds Use it anywhere on your code to make the full system to work with this defaults: JSONEncoder_newdefault() # for everything JSONEncoder_newdefault(['decimal']) # only for Decimal ''' JSONEncoder_olddefault = json.JSONEncoder.default def JSONEncoder_wrapped(self, o): ''' json.JSONEncoder.default = JSONEncoder_newdefault ''' if ('uuid' in kind) and isinstance(o, uuid.UUID): return str(o) if ('datetime' in kind) and isinstance(o, datetime): return str(o) if ('time' in kind) and isinstance(o, time.struct_time): return datetime.fromtimestamp(time.mktime(o)) if ('decimal' in kind) and isinstance(o, decimal.Decimal): return str(o) return JSONEncoder_olddefault(self, o) json.JSONEncoder.default = JSONEncoder_wrapped # Example if __name__ == '__main__': JSONEncoder_newdefault()
回答10:
From the JSON Standard Document, as linked in json.org:
JSON is agnostic about the semantics of numbers. In any programming language, there can be a variety of number types of various capacities and complements, fixed or floating, binary or decimal. That can make interchange between different programming languages difficult. JSON instead offers only the representation of numbers that humans use: a sequence of digits. All programming languages know how to make sense of digit sequences even if they disagree on internal representations. That is enough to allow interchange.
So it's actually accurate to represent Decimals as numbers (rather than strings) in JSON. Bellow lies a possible solution to the problem.
Define a custom JSON encoder:
import json class CustomJsonEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, Decimal): return float(obj) return super(CustomJsonEncoder, self).default(obj)
Then use it when serializing your data:
json.dumps(data, cls=CustomJsonEncoder)
As noted from comments on the other answers, older versions of python might mess up the representation when converting to float, but that's not the case anymore.
To get that exact decimal back in Python:
Decimal(str(value))
This solution is hinted in Python 3.0 documentation on decimals:
To create a Decimal from a float, first convert it to a string.
回答11:
If you want to pass a dictionary containing decimals to the requests
library (using the json
keyword argument), you simply need to install simplejson
:
$ pip3 install simplejson $ python3 >>> import requests >>> from decimal import Decimal >>> # This won't error out: >>> requests.post('https://www.google.com', json={'foo': Decimal('1.23')})
The reason of the problem is that requests
uses simplejson
only if it is present, and falls back to the built-in json
if it is not installed.
回答12:
this can be done by adding
elif isinstance(o, decimal.Decimal): yield str(o)
in \Lib\json\encoder.py:JSONEncoder._iterencode
, but I was hoping for a better solution