【问题标题】:How to limit the number of float digits JSONEncoder produces?如何限制 JSONEncoder 产生的浮点数?
【发布时间】:2019-06-19 14:04:10
【问题描述】:

我正在尝试设置 python json 库,以便将具有其他字典元素的字典保存到文件中。浮点数很多,我想将位数限制为,例如7

根据 SO encoder.FLOAT_REPR 上的其他帖子,应使用。但是它不起作用。

例如下面的代码,在 Python3.7.1 中运行,打印所有数字:

import json
json.encoder.FLOAT_REPR = lambda o: format(o, '.7f' )
d = dict()
d['val'] = 5.78686876876089075543
d['name'] = 'kjbkjbkj'
f = open('test.json', 'w')
json.dump(d, f, indent=4)
f.close()

我该如何解决?

这可能无关紧要,但我在 macOS 上。

编辑

这个问题被标记为重复。但是在the accepted answer (and until now the only one) to the original post 中明确指出:

注意:此解决方案不适用于 python 3.6+

所以那个解决方案不是正确的。另外它正在使用库simplejson不是json

【问题讨论】:

  • @Tomas Farias 在您发布的问题的答案中明确说明:Note: This solution doesn't work on python 3.6+ 所以我认为它不是重复的,除非您当然确定它有效:如果是这样,请告诉我怎么做。
  • 我同意这不是另一个问题的重复。 FWIW,过去我花了很多时间研究用json.JSONEncoder 类做类似的事情,我从查看它的源代码得出的结论是,如果不改变数据,做这种事情是不可行的-structure 在传入之前。也就是说,由于源代码可用,因此您可以创建库的自定义版本。也不是说simplejson 与 Python 自己的 json 模块非常相似——以至于您几乎可以互换使用它们。
  • 你能看看我的解决方案吗?如果我正确理解了这个问题,我相信我发布的内容是最简单和最好的。
  • @SwimBikeRun 可能是最简单的,但你永远不应该说你的答案是最好的:这有点自大,这里有些人可能比你更有经验。
  • @FrancescoBoi 是的,我在这个上看起来像个傻瓜:D。我几乎可以肯定 Decode 拥有所有的花里胡哨的 Encode,因为我只是做了这件事,但方向相反。这就是我粘贴未经测试的代码所得到的。糟糕!

标签: python json python-3.x floating-point


【解决方案1】:

选项1:使用正则表达式匹配进行舍入。

您可以使用json.dumps 将对象转储为字符串,然后使用this post 中显示的技术来查找和舍入您的浮点数。

为了测试它,我在您提供的示例之上添加了一些更复杂的嵌套结构::

d = dict()
d['val'] = 5.78686876876089075543
d['name'] = 'kjbkjbkj'
d["mylist"] = [1.23456789, 12, 1.23, {"foo": "a", "bar": 9.87654321}]
d["mydict"] = {"bar": "b", "foo": 1.92837465}

# dump the object to a string
d_string = json.dumps(d, indent=4)

# find numbers with 8 or more digits after the decimal point
pat = re.compile(r"\d+\.\d{8,}")
def mround(match):
    return "{:.7f}".format(float(match.group()))

# write the modified string to a file
with open('test.json', 'w') as f:
    f.write(re.sub(pat, mround, d_string))

test.json 的输出看起来像:

{
    "val": 5.7868688,
    "name": "kjbkjbkj",
    "mylist": [
        1.2345679,
        12,
        1.23,
        {
            "foo": "a",
            "bar": 9.8765432
        }
    ],
    "mydict": {
        "bar": "b",
        "foo": 1.9283747
    }
}

此方法的一个限制是它还将匹配双引号内的数字(表示为字符串的浮点数)。根据您的需要,您可以想出一个更严格的正则表达式来处理这个问题。

选项2:子类json.JSONEncoder

以下内容适用于您的示例并处理您将遇到的大多数边缘情况:

import json

class MyCustomEncoder(json.JSONEncoder):
    def iterencode(self, obj):
        if isinstance(obj, float):
            yield format(obj, '.7f')
        elif isinstance(obj, dict):
            last_index = len(obj) - 1
            yield '{'
            i = 0
            for key, value in obj.items():
                yield '"' + key + '": '
                for chunk in MyCustomEncoder.iterencode(self, value):
                    yield chunk
                if i != last_index:
                    yield ", "
                i+=1
            yield '}'
        elif isinstance(obj, list):
            last_index = len(obj) - 1
            yield "["
            for i, o in enumerate(obj):
                for chunk in MyCustomEncoder.iterencode(self, o):
                    yield chunk
                if i != last_index: 
                    yield ", "
            yield "]"
        else:
            for chunk in json.JSONEncoder.iterencode(self, obj):
                yield chunk

现在使用自定义编码器写入文件。

with open('test.json', 'w') as f:
    json.dump(d, f, cls = MyCustomEncoder)

输出文件test.json:

{"val": 5.7868688, "name": "kjbkjbkj", "mylist": [1.2345679, 12, 1.2300000, {"foo": "a", "bar": 9.8765432}], "mydict": {"bar": "b", "foo": 1.9283747}}

为了让indent等其他关键字参数起作用,最简单的方法是读入刚刚写入的文件,然后使用默认编码器将其写回:

# write d using custom encoder
with open('test.json', 'w') as f:
    json.dump(d, f, cls = MyCustomEncoder)

# load output into new_d
with open('test.json', 'r') as f:
    new_d = json.load(f)

# write new_d out using default encoder
with open('test.json', 'w') as f:
    json.dump(new_d, f, indent=4)

现在输出文件与选项 1 中显示的相同。

【讨论】:

  • 首先感谢并抱歉回复晚了。看起来不错,但现在indent=4 没有效果。
  • @Francesco 不是最优雅的解决方案,但最简单的方法是读取您编写的文件并使用默认编码器将其写回。另一个(更复杂的)选项是更新自定义编码器以处理kwargs,如indent
  • 应该重载哪些方法来处理indent
  • @FrancescoBoi 您可以在iterencode 内部执行此操作,类似于在json.JSONEncoder 中执行此操作。然而,我只是有一个想法。你也可以将对象转储到一个字符串,然后use regex to round - 如果我能轻松地工作,我会发布更新。
  • @pault 这个复杂的答案和简单的class MyCustomEncoder(json.JSONEncoder): def iterencode(self, obj): if isinstance(obj, float): return format(obj, '.7f') 之间有什么区别吗?除了这个之外,您还期待什么样的输入?我在主要问题中没有看到这一点。
【解决方案2】:

在 Python 3 中仍然可以对 json 进行猴子补丁,但您需要修改 float 而不是 FLOAT_REPR。确保禁用 c_make_encoder,就像在 Python 2 中一样。

import json

class RoundingFloat(float):
    __repr__ = staticmethod(lambda x: format(x, '.2f'))

json.encoder.c_make_encoder = None
if hasattr(json.encoder, 'FLOAT_REPR'):
    # Python 2
    json.encoder.FLOAT_REPR = RoundingFloat.__repr__
else:
    # Python 3
    json.encoder.float = RoundingFloat

print(json.dumps({'number': 1.0 / 81}))

优点:简单,可以进行其他格式化(例如科学记数法、去除尾随零等)。缺点:看起来比实际更危险。

【讨论】:

    【解决方案3】:

    根据我对问题的回答,您可以使用以下内容:

        Write two-dimensional list to JSON file.

    我说可能,因为它需要在使用dump() 对其进行 JSON 编码之前“包装” Python 字典(或列表)中的所有浮点值。

    (使用 Python 3.7.2 测试。)

    from _ctypes import PyObj_FromPtr
    import json
    import re
    
    
    class FloatWrapper(object):
        """ Float value wrapper. """
        def __init__(self, value):
            self.value = value
    
    
    class MyEncoder(json.JSONEncoder):
        FORMAT_SPEC = '@@{}@@'
        regex = re.compile(FORMAT_SPEC.format(r'(\d+)'))  # regex: r'@@(\d+)@@'
    
        def default(self, obj):
            return (self.FORMAT_SPEC.format(id(obj)) if isinstance(obj, FloatWrapper)
                    else super(MyEncoder, self).default(obj))
    
        def iterencode(self, obj, **kwargs):
            for encoded in super(MyEncoder, self).iterencode(obj, **kwargs):
                # Check for marked-up float values (FloatWrapper instances).
                match = self.regex.search(encoded)
                if match:  # Get FloatWrapper instance.
                    id = int(match.group(1))
                    float_wrapper = PyObj_FromPtr(id)
                    json_obj_repr = '%.7f' % float_wrapper.value  # Create alt repr.
                    encoded = encoded.replace(
                                '"{}"'.format(self.FORMAT_SPEC.format(id)), json_obj_repr)
                yield encoded
    
    
    d = dict()
    d['val'] = FloatWrapper(5.78686876876089075543)  # Must wrap float values.
    d['name'] = 'kjbkjbkj'
    
    with open('float_test.json', 'w') as file:
        json.dump(d, file, cls=MyEncoder, indent=4)
    

    创建的文件内容:

    {
        "val": 5.7868688,
        "name": "kjbkjbkj"
    }
    

    更新:

    正如我所提到的,上述要求在调用json.dump() 之前包装所有float 值。幸运的是,可以通过添加和使用以下(经过最少测试的)实用程序来自动执行此操作:

    def wrap_type(obj, kind, wrapper):
        """ Recursively wrap instances of type kind in dictionary and list
            objects.
        """
        if isinstance(obj, dict):
            new_dict = {}
            for key, value in obj.items():
                if not isinstance(value, (dict, list)):
                    new_dict[key] = wrapper(value) if isinstance(value, kind) else value
                else:
                    new_dict[key] = wrap_type(value, kind, wrapper)
            return new_dict
    
        elif isinstance(obj, list):
            new_list = []
            for value in obj:
                if not isinstance(value, (dict, list)):
                    new_list.append(wrapper(value) if isinstance(value, kind) else value)
                else:
                    new_list.append(wrap_type(value, kind, wrapper))
            return new_list
    
        else:
            return obj
    
    
    d = dict()
    d['val'] = 5.78686876876089075543
    d['name'] = 'kjbkjbkj'
    
    with open('float_test.json', 'w') as file:
        json.dump(wrap_type(d, float, FloatWrapper), file, cls=MyEncoder, indent=4)
    

    【讨论】:

    • 为什么是FloatWrapper?我不需要将它应用于我的每个字典浮点值:是吗?
    • 我没有看到输出有任何变化
    • 是的,恐怕是这样,如果它不起作用,那么您一定是没有完成或错过了一个。查看更新。
    【解决方案4】:

    不回答这个问题,但是对于解码端,你可以做这样的事情,或者覆盖钩子方法。

    用这种方法解决这个问题虽然需要编码,解码,然后再次编码,这过于复杂,不再是最佳选择。我以为 Encode 拥有 Decode 的所有花里胡哨,我错了。

    # d = dict()
    class Round7FloatEncoder(json.JSONEncoder): 
        def iterencode(self, obj): 
            if isinstance(obj, float): 
                yield format(obj, '.7f')
    
    
    with open('test.json', 'w') as f:
        json.dump(d, f, cls=Round7FloatEncoder)
    

    【讨论】:

    • 你用的是什么版本的python?当我在 3.6.5 上尝试此代码时,它会抛出 TypeError: 'NoneType' object is not iterable编辑 2.7.14 出现同样的错误。
    • 感谢您的回答 thriathlon-athlete :) 目前我无法测试您的答案,但我会尽快测试。
    • 啊,我明白我的错误了。要使其正常工作,必须先编码,在解码时执行此方法,然后最后一次编码。这并不比上述解决方案更令人费解。我的立场是正确的
    【解决方案5】:

    这里是一个python代码sn-p,展示了如何将json输出量化到指定位数:

    #python example code, error handling not shown
    
    #open files
    fin  = open(input_file_name)
    fout = open(output_file_name, "w+")
    
    #read file input (note this could be done in one step but breaking it up allows more flexibilty )
    indata = fin.read()
    
    # example quantization function
    def quant(n):
        return round((float(n) * (10 ** args.prec))) / (
            10 ** args.prec
        )  # could use decimal.quantize
    
    # process the data streams by parsing and using call back to quantize each float as it parsed
    outdata = json.dumps(json.loads(indata, parse_float=quant), separators=(",", ":"))
    
    #write output
    fout.write(outdata)
    

    以上是 jsonvice 命令行工具用于将浮点 json 数字量化为所需的任何精度以节省空间的内容。

    https://pypi.org/project/jsonvice/

    这可以使用 pip 或 pipx 安装(请参阅文档)。

    pip3 install jsonvice
    

    免责声明:我在需要测试量化的机器学习模型权重时写了这个。

    【讨论】:

    • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接答案可能会失效。 - From Review
    • @zkoza 感谢反馈,包括源代码和其他材料
    猜你喜欢
    • 2011-11-04
    • 2021-04-23
    • 1970-01-01
    • 2016-04-18
    • 2021-11-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-02-21
    相关资源
    最近更新 更多