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

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

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

【问题讨论】:

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

标签: python json floating-point decimal


【解决方案1】:

3.9 不能在 IEEE 浮点数中精确表示,它总是以 3.8999999999999999 表示,例如试试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") 会起作用,我希望它是这样的
  • @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 更改了舍入浮点数的规则,因此可以正常工作。请参阅stackoverflow.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 {'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)
  • 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
            猜你喜欢
            • 2018-11-15
            • 2010-11-30
            • 2017-09-26
            • 1970-01-01
            • 1970-01-01
            • 2017-09-03
            相关资源
            最近更新 更多