Python JSON序列化Decimal对象


242

我有一个Decimal('3.9')对象的一部分,希望将其编码为JSON字符串,看起来像{'x': 3.9}。我不在乎客户端的精度,因此浮点数很好。

是否有序列化此序列的好方法?JSONDecoder不接受Decimal对象,并且事先转换为float会产生{'x': 3.8999999999999999}错误,这将浪费大量带宽。


2
相关的Python错误:json编码器无法处理小数
jfs 2014年

3.8999999999999999的错误不超过3.4。0.2没有精确的浮点表示形式。
2015年

@Jasen 3.89999999999的错误比3.4的错误多出12.8%。JSON标准仅与序列化和符号有关,与实现无关。使用IEEE754并不是原始JSON规范的一部分,它只是实现它的最常用方法。仅使用精确的十进制算术的实现是完全符合(实际上,甚至更为严格)的。
阿拉伯

😂 错。具有讽刺意味的。
阿拉伯

Answers:


147

子类化如何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)

然后像这样使用它:

json.dumps({'x': decimal.Decimal('5.5')}, cls=DecimalEncoder)

哎呀,我只是注意到它实际上不会像这样工作。将进行相应的编辑。(但是,这个想法保持不变。)
MichałMarczyk,2009年

问题是DecimalEncoder()._iterencode(decimal.Decimal('3.9')).next()返回了正确的对象'3.9',但是DecimalEncoder()._iterencode(3.9).next()返回了一个生成器对象,该对象仅'3.899...'在您堆放在另一个对象上时才会返回.next()。发电机有趣的业务。哦,好了,应该现在就工作。
米哈尔Marczyk

8
你不能return (str(o),)代替吗?[o]是只有1个元素的列表,为什么要遍历它呢?
mpen 2011年

2
@Mark:return (str(o),)将返回长度为1的元组,而答案中的代码将返回长度为1的生成器。请参阅iterencode()文档
Abgan 2013年

30
此实现不再起作用。Elias Zamaria的作品是采用相同风格的作品。
piro 2014年

223

Simplejson 2.1及更高版本具有对Decimal类型的本地支持:

>>> json.dumps(Decimal('3.9'), use_decimal=True)
'3.9'

请注意,use_decimalTrue在默认情况下:

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):

所以:

>>> json.dumps(Decimal('3.9'))
'3.9'

希望此功能将包含在标准库中。


7
嗯,对我来说,这会将Decimal对象转换为float,这是不可接受的。例如,使用货币时精度会下降。
马修·辛克尔

12
@MatthewSchinckel我认为不是。它实际上使它成为一个字符串。如果将结果字符串反馈给json.loads(s, use_decimal=True)它,则返回十进制数。在整个过程中没有漂浮。编辑以上答案。希望原始海报可以使用。
Shekhar

1
啊哈,我想我也没用过use_decimal=True负载。
马修·申克尔

1
对于我json.dumps({'a' : Decimal('3.9')}, use_decimal=True)'{"a": 3.9}'。目标不是'{"a": "3.9"}'吗?
MrJ

5
simplejson.dumps(decimal.Decimal('2.2'))也可以:无显式use_decimal(在simplejson / 3.6.0上测试)。加载回去的另一种方法是:json.loads(s, parse_float=Decimal)即,您可以使用stdlib读取它jsonsimplejson也支持旧版本)。
jfs 2014年

181

我想让大家知道,我在运行Python 2.6.5的Web服务器上尝试了MichałMarczyk的答案,并且运行良好。但是,我升级到了Python 2.7,它停止工作了。我试图想到某种编码Decimal对象的方法,这就是我想出的:

import decimal

class DecimalEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, decimal.Decimal):
            return float(o)
        return super(DecimalEncoder, self).default(o)

希望这应该对任何遇到Python 2.7问题的人有所帮助。我测试了它,看来效果很好。如果有人注意到我的解决方案中的任何错误或提出了更好的方法,请告诉我。


4
Python 2.7更改了浮点取整规则,因此可以正常工作。参见stackoverflow.com/questions/1447287/…中的
Nelson

2
对于不能使用simplejson(例如,在Google App Engine上)的我们来说,这是天赐之物。
乔尔·克罗斯

17
使用unicodestr代替float以确保精度。
SeppoErviälä16年

2
54.3999 ...的问题在Python 2.6.x和更早版本中很重要,在该版本中,将float转换为string不能正常工作,但将Decimal转换为str则更不正确,因为它将序列化为带双引号的字符串"54.4",而不是一个号码。
hynekcer

1
在python3中工作
SeanFromIT '18

43

在我的Flask应用程序中,它使用python 2.7.11,flask alchemy(具有“ db.decimal”类型)和Flask Marshmallow(用于“即时”序列化器和反序列化器),每次执行GET或POST时,我都会遇到此错误。序列化器和反序列化器无法将Decimal类型转换为任何JSON可识别格式。

我做了一个“ pip install simplejson”,然后只需添加

import simplejson as json

序列化器和解串器再次开始发出嗡嗡声。我什么也没做... DEciamls显示为'234.00'浮点格式。


1
最简单的解决方法
-SMDC

1
奇怪的是,您甚至不必导入simplejson-只需安装就可以了。最初由这个答案提到。
bsplosion

这对我不起作用,Decimal('0.00') is not JSON serializable 通过pip安装它后仍然可以解决。当您同时使用棉花糖和石墨烯时,就是这种情况。当在rest api上调用查询时,棉花糖可用于十进制字段。但是,当用graphql调用它时,会引发is not JSON serializable错误。
Roel

太棒了,太棒了
蜘蛛侠

完善!如果您使用的是别人编写的模块,而您却无法轻易修改该模块(在我的情况下是使用Google表格的原因)
那么此

32

我尝试从GAE 2.7的simplejson切换到内置的json,但小数位数有问题。如果default返回的str(o)有引号(因为_iterencode在default的结果上调用_iterencode),并且float(o)会删除结尾的0。

如果default返回一个从float继承的类的对象(或任何不带其他格式调用repr的类)并具有自定义__repr__方法,则它看起来像我希望的那样工作。

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

真好!这样可以确保十进制值以Javascript浮点数形式出现在JSON中,而无需让Python首先将其四舍五入为最接近的浮点值。
konrad 2013年

3
不幸的是,这在最近的Python 3中不起作用。现在有一些快速路径代码将所有float子类都视为float,并且完全不对其调用repr。
Antti Haapala '01

@AnttiHaapala,该示例在Python 3.6上运行良好。
Cristian Ciupitu

@CristianCiupitu实际上,我现在似乎无法重现不良行为
Antti Haapala

2
自v3.5.2rc1起,该解决方案停止工作,请参阅github.com/python/cpython/commit/…。有float.__repr__硬编码(会降低精度),fakefloat.__repr__根本不会被调用。如果fakefloat具有其他方法,则上述解决方案对于python3到3.5.1均适用def __float__(self): return self
myroslav

30

缺少本机选项,因此我将其添加到寻找它的下一个家伙/画廊。

从Django 1.7.x开始,有一个内置组件DjangoJSONEncoder,您可以从中获取它django.core.serializers.json

import json
from django.core.serializers.json import DjangoJSONEncoder
from django.forms.models import model_to_dict

model_instance = YourModel.object.first()
model_dict = model_to_dict(model_instance)

json.dumps(model_dict, cls=DjangoJSONEncoder)

快点!


尽管很高兴知道这一点,但是OP并没有询问Django?
std''OrgnlDave

4
@ std''OrgnlDave您是100%正确的。我忘记了我是怎么到达这里的,但是我用搜索词附加了“ django”来搜索这个问题,经过更多的搜索之后,我找到了答案,并在这里添加给了像我这样的下一个人,结果绊倒了它
哈维尔·巴齐

6
你救了我的一天
gaozhidf

14

我的$ .02!

因为要为Web服务器序列化大量数据,所以我扩展了许多JSON编码器。这是一些不错的代码。请注意,它很容易扩展为几乎任何您想要的数据格式,并且可以将3.9复制为"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

让我的生活变得更加轻松...


3
这是错误的:它将3.9复制为"thing": "3.9"
雕文

最好的解决方案,非常简单,谢谢您节省了我的一天,对我而言,足以保存数字,用十进制字符串表示还可以
stackdave 18'Jan

@Glyph通过JSON标准(其中有一些...),未加引号的数字是双精度浮点数,而不是十进制数。引用它是保证兼容性的唯一方法。
std''OrgnlDave

2
你为此被引用吗?我读过的每个规范都暗示它与实现有关。
雕文'18

12

3.9不能在IEEE浮点数中精确表示,它总是以表示3.8999999999999999,例如try print repr(3.9),您可以在此处了解更多信息:

http://en.wikipedia.org/wiki/Floating_point
http://docs.sun.com/source/806-3568/ncg_goldberg.html

因此,如果您不希望浮动,则只能将其作为字符串发送,并且要允许将十进制对象自动转换为JSON,请执行以下操作:

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)

我知道一旦在客户端上对其进行解析,它内部将不会是3.9,但是3.9是有效的JSON浮点数。即,json.loads("3.9")将工作,我希望是这样
Knio

@Anurag在您的示例中,您的意思是repr(obj)而不是repr(o)。
orokusaki 2010年

如果您尝试对非十进制的内容进行编码,这会死吗?
mikemaccana 2011年

1
@nailer,不行,您可以尝试一下,原因是默认引发异常以指示应使用下一个处理程序
Anurag Uniyal 2011年

1
请参阅mikez302的答案-在Python 2.7或更高版本中,这不再适用。
乔尔·克罗斯

9

对于Django用户

最近遇到了TypeError: Decimal('2337.00') is not JSON serializable JSON编码,即json.dumps(data)

解决方案

# converts Decimal, Datetime, UUIDs to str for Encoding
from django.core.serializers.json import DjangoJSONEncoder  

json.dumps(response.data, cls=DjangoJSONEncoder)

但是,现在Decimal值将是一个字符串,现在我们可以在解码数据时使用parse_floatin中的option 显式设置十进制/浮点值解析器json.loads

import decimal 

data = json.loads(data, parse_float=decimal.Decimal) # default is float(num_str)

8

JSON标准文档(如json.org中链接):

JSON与数字的语义无关。在任何编程语言中,可以有各种容量和补码形式的数字类型,固定或浮动,二进制或十进制。这可能使不同编程语言之间的交换变得困难。JSON而是仅提供人类使用的数字表示形式:数字序列。所有编程语言都知道如何理解数字序列,即使它们在内部表示形式上存在分歧。这足以允许互换。

因此,在JSON中将小数表示为数字(而不是字符串)实际上是准确的。贝娄为该问题提供了可能的解决方案。

定义自定义JSON编码器:

import json


class CustomJsonEncoder(json.JSONEncoder):

    def default(self, obj):
        if isinstance(obj, Decimal):
            return float(obj)
        return super(CustomJsonEncoder, self).default(obj)

然后在序列化数据时使用它:

json.dumps(data, cls=CustomJsonEncoder)

从其他答案的注释中可以看出,较旧版本的python在转换为float时可能会弄乱表示形式,但现在不再如此。

要在Python中返回小数:

Decimal(str(value))

Python 3.0文档中的小数点提示了该解决方案:

要从浮点数创建小数,请首先将其转换为字符串。


2
这在Python 3中不是“固定的”。转换为float 必然会丢失十进制表示,并导致差异。如果Decimal很重要,我认为最好使用字符串。
juanpa.arrivillaga

我相信从python 3.1开始这样做是安全的。精度的损失在算术运算中可能是有害的,但是对于JSON编码,您只是生成该值的字符串显示,因此对于大多数用例而言,精度已绰绰有余。JSON中的所有内容都已经是一个字符串,因此将引号引起来只是违反了JSON规范。
雨果·莫塔

话虽如此,我理解转换为浮动的担忧。编码器可能使用不同的策略来生成所需的显示字符串。不过,我认为值得产生一个引用的值。
雨果·莫塔

“ @HugoMota” JSON中的所有内容都已经是一个字符串,因此将引号引起来只是违反了JSON规范。否:rfc-editor.org/rfc/rfc8259.txt-JSON是基于文本的编码格式,但这并不意味着其中的所有内容都应解释为字符串。规范定义了如何与字符串分开编码数字。
GunnarnarórMagnússon19年

@GunnarÞórMagnússon“ JSON是一种基于文本的编码格式”-这就是我所说的“一切都是字符串”。事先将数字转换为字符串不会神奇地保持精度,因为当它变成JSON时无论如何都会是字符串。而且根据规范,数字周围没有引号。读者有责任在阅读时保持准确性(而不是引用,只是我的看法)。
雨果·莫塔

6

这就是我从课堂上摘录的

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)

通过单元测试:

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)

json.loads(myString, cls=CommonJSONEncoder)评论应该是json.loads(myString, cls=CommonJSONDecoder)
CanKavaklıoğlu2015年

如果obj不是十进制,则object_hook需要默认返回值。
CanKavaklıoğlu2016年

3

您可以根据需要创建自定义JSON编码器。

import json
from datetime import datetime, date
from time import time, struct_time, mktime
import decimal

class CustomJSONEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, datetime):
            return str(o)
        if isinstance(o, date):
            return str(o)
        if isinstance(o, decimal.Decimal):
            return float(o)
        if isinstance(o, struct_time):
            return datetime.fromtimestamp(mktime(o))
        # Any other serializer if needed
        return super(CustomJSONEncoder, self).default(o)

解码器可以这样称呼,

import json
from decimal import Decimal
json.dumps({'x': Decimal('3.9')}, cls=CustomJSONEncoder)

输出将是:

>>'{"x": 3.9}'

太棒了...感谢您的一站式解决方案(y)
穆罕默德·罗勒

这确实有效!感谢您分享您的解决方案
tthreetorch

3

对于那些不想使用第三方库的人来说……Elias Zamaria的问题是它会转换为float,这会遇到问题。例如:

>>> json.dumps({'x': Decimal('0.0000001')}, cls=DecimalEncoder)
'{"x": 1e-07}'
>>> json.dumps({'x': Decimal('100000000000.01734')}, cls=DecimalEncoder)
'{"x": 100000000000.01733}'

JSONEncoder.encode()方法使您可以返回原义的json内容,与不同的是JSONEncoder.default(),该方法返回的是json兼容类型(例如float),然后以常规方式对其进行编码。问题encode()在于它(通常)仅在最高级别上起作用。但是它仍然可以使用,但需要做一些额外的工作(python 3.x):

import json
from collections.abc import Mapping, Iterable
from decimal import Decimal

class DecimalEncoder(json.JSONEncoder):
    def encode(self, obj):
        if isinstance(obj, Mapping):
            return '{' + ', '.join(f'{self.encode(k)}: {self.encode(v)}' for (k, v) in obj.items()) + '}'
        if isinstance(obj, Iterable) and (not isinstance(obj, str)):
            return '[' + ', '.join(map(self.encode, obj)) + ']'
        if isinstance(obj, Decimal):
            return f'{obj.normalize():f}'  # using normalize() gets rid of trailing 0s, using ':f' prevents scientific notation
        return super().encode(obj)

这给你:

>>> json.dumps({'x': Decimal('0.0000001')}, cls=DecimalEncoder)
'{"x": 0.0000001}'
>>> json.dumps({'x': Decimal('100000000000.01734')}, cls=DecimalEncoder)
'{"x": 100000000000.01734}'

2

根据stdOrgnlDave的答案,我定义了此包装器,可以使用可选的种类调用该包装器,因此编码器仅适用于您项目中的某些种类。我认为应该在代码内完成工作,而不要使用此“默认”编码器,因为“它比隐式更好”是显式的,但是我知道使用它可以节省您的时间。:-)

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()

0

如果要将包含小数的字典传递给requests库(使用json关键字参数),则只需安装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')})

问题的原因是仅在存在时才requests使用simplejson,而在json未安装时退回到内置。


-6

这可以通过添加

    elif isinstance(o, decimal.Decimal):
        yield str(o)

\Lib\json\encoder.py:JSONEncoder._iterencode,但我希望有一个更好的解决方案


5
您可以按照上面的示例子类化JSONEncoder,编辑已建立的库的已安装Python文件或解释器本身应该是最后的选择。
justanr 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.