Python JSON 序列化一个 Decimal 对象

Posted

技术标签:

【中文标题】Python JSON 序列化一个 Decimal 对象【英文标题】:Python JSON serialize a Decimal object 【发布时间】:2010-12-29 22:50:39 【问题描述】:

我有一个Decimal('3.9') 作为对象的一部分,并希望将其编码为一个类似于'x': 3.9 的JSON 字符串。我不关心客户端的精度,所以浮点数就可以了。

有没有好办法序列化这个? JSONDecoder 不接受 Decimal 对象,预先转换为浮点数会产生'x': 3.8999999999999999 这是错误的,会浪费很大的带宽。

【问题讨论】:

相关Python bug:json encoder unable to handle decimal 3.8999999999999999 并不比 3.4 更错误。 0.2 没有精确的浮点表示。 @Jasen 3.89999999999 比 3.4 多出 12.8% 的错误。 JSON 标准只是关于序列化和符号,而不是实现。使用 IEEE754 不是原始 JSON 规范的一部分,它只是实现它的最常见方式。仅使用精确十进制算术的实现完全(实际上,甚至更严格)符合。 ???? 错了。讽刺。 【参考方案1】:

3.9 不能在 IEEE 浮点数中精确表示,它总是以 3.8999999999999999 表示,例如试试print repr(3.9),你可以在这里阅读更多信息:

http://en.wikipedia.org/wiki/Floating_pointhttp://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") 会起作用,我希望它是这样的 @Anurag 你的意思是 repr(obj) 而不是 repr(o) 在你的例子中。 如果你尝试编码一些不是十进制的东西,这不会死吗? @nailer,不,不会的,你可以试试,因为默认引发异常以表示应该使用下一个处理程序 查看 mikez302 的答案 - 在 Python 2.7 或更高版本中,这不再适用。【参考方案2】:

子类化json.JSONEncoder怎么样?

class DecimalEncoder(json.JSONEncoder):
    def default(self, o):
        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).default(o)

然后像这样使用它:

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

【讨论】:

哎呀,我只是注意到它实际上不会像这样工作。将相应地进行编辑。 (不过,这个想法保持不变。) 你不能只用return (str(o),) 代替吗? [o] 是一个只有 1 个元素的列表,为什么还要循环遍历它? @Mark: return (str(o),) 将返回长度为 1 的元组,而答案中的代码返回长度为 1 的生成器。请参阅 iterencode() docs 这个实现不再起作用了。 Elias Zamaria 的作品风格相同。 它给我这个错误:TypeError: at 0x7fd42908da20> is not JSON serializable【参考方案3】:

Simplejson 2.1 及更高版本原生支持 Decimal 类型:

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

注意use_decimal默认是True

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'

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

【讨论】:

嗯,对我来说,这会将 Decimal 对象转换为浮点数,这是不可接受的。例如,在使用货币时会丢失精度。 @MatthewSchinckel 我认为不是。它实际上用它制作了一个字符串。如果您将结果字符串反馈给json.loads(s, use_decimal=True),它会返回小数点。整个过程没有浮动。编辑上面的答案。希望原始海报能很好。 啊哈,我想我也没有在负载上使用use_decimal=True 对我来说json.dumps('a' : Decimal('3.9'), use_decimal=True) 给了'"a": 3.9'。目标不是'"a": "3.9"' 吗? simplejson.dumps(decimal.Decimal('2.2')) 也有效:没有明确的 use_decimal(在 simplejson/3.6.0 上测试)。加载它的另一种方法是:json.loads(s, parse_float=Decimal) 即,您可以使用 stdlib json 读取它(也支持旧的simplejson 版本)。【参考方案4】:

我想让每个人都知道,我在运行 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 str(o)
        return super(DecimalEncoder, self).default(o)

请注意,这会将十进制转换为其字符串表示形式(例如,"1.2300")为 a。不丢失有效数字和 b。防止舍入错误。

这有望帮助任何遇到 Python 2.7 问题的人。我测试了它,它似乎工作正常。如果有人注意到我的解决方案中的任何错误或提出更好的方法,请告诉我。

使用示例:

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

【讨论】:

Python 2.7 更改了舍入浮点数的规则,因此可以正常工作。请参阅***.com/questions/1447287/… 中的讨论 对于我们这些不能使用 simplejson(即在 Google App Engine 上)的人来说,这个答案是天赐之物。 使用unicodestr 代替float 以确保精度。 54.3999... 的问题在 Python 2.6.x 及更早版本中很重要,因为浮点到字符串的转换不能正常工作,但 Decimal 到 str 的转换更不正确,因为它会被序列化作为带有双引号 "54.4" 的字符串,而不是数字。 在 python3 中工作【参考方案5】:

我尝试从 simplejson 切换到 GAE 2.7 的内置 json,但小数点有问题。如果默认返回 str(o) 则有引号(因为 _iterencode 在默认结果上调用 _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]'

【讨论】:

不错!这可以确保十进制值在 JSON 中作为 javascript 浮点数结束,而无需让 Python 先将其四舍五入到最接近的浮点值。 不幸的是,这在最近的 Python 3 中不起作用。现在有一些快速路径代码将所有浮点子类都视为浮点数,并且不会对它们完全调用 repr。 @AnttiHaapala,该示例在 Python 3.6 上运行良好。 @CristianCiupitu 确实,我现在似乎无法重现不良行为 该解决方案自 v3.5.2rc1 起停止工作,请参阅 github.com/python/cpython/commit/…。有float.__repr__ 硬编码(失去精度),并且根本不调用fakefloat.__repr__。如果 fakefloat 有额外的方法def __float__(self): return self,上述解决方案确实适用于 python3 到 3.5.1。【参考方案6】:

这是我所拥有的,从我们的班级中提取的

class CommonJSONEncoder(json.JSONEncoder):

    """
    Common JSON Encoder
    json.dumps(myString, cls=CommonJSONEncoder)
    """

    def default(self, obj):

        if isinstance(obj, decimal.Decimal):
            return 'typedecimal': 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 'typedecimal' == 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('typedecimal' 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('typedecimal' 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('typedecimal' in result)
    new_obj = json.loads(result, cls=CommonJSONDecoder)
    self.assertEqual(new_obj, obj)

【讨论】:

json.loads(myString, cls=CommonJSONEncoder) 评论应该是json.loads(myString, cls=CommonJSONDecoder) object_hook 如果 obj 不是十进制,则需要一个默认返回值。【参考方案7】:

在我的 Flask 应用程序中,它使用 python 2.7.11、flask alchemy(带有“db.decimal”类型)和 Flask Marshmallow(用于“即时”序列化器和反序列化器),每次我做一个获取或发布。序列化器和反序列化器未能将 Decimal 类型转换为任何 JSON 可识别格式。

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

import simplejson as json

序列化器和反序列化器再次开始发出咕噜声。我什么也没做... DEciamls 显示为“234.00”浮点格式。

【讨论】:

奇怪的是,您甚至不必导入simplejson - 只需安装它就可以了。最初由this answer 提及。 这对我不起作用,通过 pip 安装后仍然得到Decimal('0.00') is not JSON serializable。这种情况是当您同时使用棉花糖和石墨烯时。当在 rest api 上调用查询时,marshmallow 预期适用于十进制字段。但是,当使用 graphql 调用它时,会引发 is not JSON serializable 错误。 完美!这适用于您使用由其他人编写的模块而您无法轻松修改的情况(在我的情况下,gspread 用于使用 Google 表格)【参考方案8】:

我的 $.02!

我扩展了一堆 JSON 编码器,因为我正在为我的 Web 服务器序列化大量数据。这是一些不错的代码。请注意,它可以轻松扩展到几乎任何您喜欢的数据格式,并且可以将 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.9 复制为 "thing": "3.9" @Glyph 通过 JSON 标准(其中有一些......),未加引号的数字是双精度浮点数,而不是十进制数。引用它是保证兼容性的唯一方法。 您对此有引用吗?我读过的每个规范都暗示它是依赖于实现的。【参考方案9】:

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

【讨论】:

【参考方案10】:

来自JSON Standard Document,链接在json.org:

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

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

定义自定义 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)

正如 cmets 在其他答案中所指出的,旧版本的 python 在转换为浮点数时可能会弄乱表示,但现在情况不再如此。

在 Python 中取回小数点:

Decimal(str(value))

Python 3.0 documentation on decimals 中暗示了此解决方案:

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

【讨论】:

这在 Python 3 中不是“固定的”。转换为 float 必然会使您失去十进制表示,并且 领先到差异。如果Decimal很重要,我觉得还是用字符串比较好。 我相信从 python 3.1 开始这样做是安全的。精度损失可能对算术运算有害,但在 JSON 编码的情况下,您只是生成值的字符串显示,因此精度对于大多数用例来说已经绰绰有余。 JSON 中的所有内容都已经是一个字符串,因此在值周围加上引号就违反了 JSON 规范。 话虽如此,我理解转换为浮动的担忧。编码器可能使用不同的策略来生成所需的显示字符串。不过,我认为不值得产生引用值。 @HugoMota " JSON 中的所有内容都已经是一个字符串,因此在值周围加上引号就违反了 JSON 规范。"否:rfc-editor.org/rfc/rfc8259.txt -- JSON 是一种基于文本的编码格式,但这并不意味着其中的所有内容都将被解释为字符串。该规范定义了如何对数字进行编码,与字符串分开。 @GunnarÞórMagnússon “JSON 是一种基于文本的编码格式”——这就是我所说的“一切都是字符串”的意思。预先将数字转换为字符串不会神奇地保持精度,因为当它变成 JSON 时它无论如何都会是字符串。根据规范,数字周围没有引号。 阅读时保持精确是读者的责任(不是引用,只是我的看法)。【参考方案11】:

如果您想将包含小数的字典传递给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

【讨论】:

【参考方案12】:

缺少原生 Django 选项,所以我会为下一个寻找它的人/胆小鬼添加它。

从 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 你是 100% 正确的。我忘记了我是怎么到这里的,但是我在搜索词上附加了“django”这个问题,然后这个问题出现了,经过更多的谷歌搜索,我找到了答案并将其添加到了像我这样的下一个人,偶然发现它【参考方案13】:

您可以根据需要创建自定义 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'

【讨论】:

【参考方案14】:

对于 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 值将是一个字符串,现在我们可以在解码数据时显式设置十进制/浮点值解析器,使用 json.loads 中的 parse_float 选项:

import decimal 

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

【讨论】:

【参考方案15】:

对于那些不想使用第三方库的人... Elias Zamaria 的答案的一个问题是它转换为浮点数,这可能会遇到问题。例如:

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

【讨论】:

谢谢。这正是我想要的。它以数字形式输出,但不会因浮点转换而损失精度。我遇到了一个小问题,因为我使用的是json.dump 而不是json.dumps,你必须覆盖iterencode 而不是encode,解释here。【参考方案16】:

对于任何想要快速解决方案的人来说,我是如何从 Django 中的查询中删除 Decimal 的

total_development_cost_var = process_assumption_objects.values('total_development_cost').aggregate(sum_dev = Sum('total_development_cost', output_field=FloatField()))
total_development_cost_var = list(total_development_cost_var.values())
第 1 步:在您的查询中使用 , output_field=FloatField() 第 2 步:使用列表,例如 list(total_development_cost_var.values())

希望对你有帮助

【讨论】:

【参考方案17】:

这个问题很老了,但对于大多数用例来说,Python3 中似乎有一个更好、更简单的解决方案:

number = Decimal(0.55)
converted_number = float(number) # Returns: 0.55 (as type float)

您只需将Decimal 转换为float

【讨论】:

问题已经描述了为什么不需要转换为浮点数【参考方案18】:

如果有人仍在寻找答案,很可能是您尝试编码的数据中有一个“NaN”。因为 NaN 被 Python 认为是浮点数。

【讨论】:

这并不能真正回答问题。如果您有其他问题,可以点击 进行提问。要在此问题有新答案时收到通知,您可以follow this question。一旦你有足够的reputation,你也可以add a bounty 来引起对这个问题的更多关注。 - From Review

以上是关于Python JSON 序列化一个 Decimal 对象的主要内容,如果未能解决你的问题,请参考以下文章

json decimal and datetime

使用 Json.NET 代理 int、float 和 decimal

Newtonsoft JSON- 与 DataSet 的转换导致 Decimal 变为 Double?

反序列化Newtonsoft.Json.JsonReaderException:“Could not convert string to decimal: . Path 'SETTLEAMT&

[oeasy]python0045_四种进制_binary_octal_decimal_hexadecimal

decimal模块