使用标准的 json 模块设置浮点格式

我正在使用 python 2.6中的标准 json module序列化浮点数列表。然而,我得到的结果是这样的:

>>> import json
>>> json.dumps([23.67, 23.97, 23.87])
'[23.670000000000002, 23.969999999999999, 23.870000000000001]'

我希望浮点数的格式只有两个小数位。输出应该是这样的:

>>> json.dumps([23.67, 23.97, 23.87])
'[23.67, 23.97, 23.87]'

我尝试定义自己的 JSON Encoder 类:

class MyEncoder(json.JSONEncoder):
def encode(self, obj):
if isinstance(obj, float):
return format(obj, '.2f')
return json.JSONEncoder.encode(self, obj)

这适用于惟一的 float 对象:

>>> json.dumps(23.67, cls=MyEncoder)
'23.67'

但对于嵌套对象,这种方法失败了:

>>> json.dumps([23.67, 23.97, 23.87])
'[23.670000000000002, 23.969999999999999, 23.870000000000001]'

我不想有外部依赖项,所以我更喜欢使用标准的 json 模块。

我怎么才能做到呢?

98939 次浏览

你可以做你需要做的事情,但是没有记录在案:

>>> import json
>>> json.encoder.FLOAT_REPR = lambda f: ("%.2f" % f)
>>> json.dumps([23.67, 23.97, 23.87])
'[23.67, 23.97, 23.87]'

注意: 这在任何最新版本的 Python 中都可以实现 没有

不幸的是,我相信您必须通过修补程序来实现这一点(在我看来,这意味着标准库 json包中存在设计缺陷)。例如,这个代码:

import json
from json import encoder
encoder.FLOAT_REPR = lambda o: format(o, '.2f')
    

print(json.dumps(23.67))
print(json.dumps([23.67, 23.97, 23.87]))

emits:

23.67
[23.67, 23.97, 23.87]

如你所愿。显然,应该有一种体系结构化的方法来覆盖 FLOAT_REPR,以便如果您希望的话,每个浮点的表示都在您的控制之下; 但不幸的是,这不是 json包的设计方式。

如果你只能使用 Python 2.5或更早的版本: 如果安装了 C 加速程序,Monkey-patch 技巧似乎不适用于原来的 simplejson 模块:

$ python
Python 2.5.4 (r254:67916, Jan 20 2009, 11:06:13)
[GCC 4.2.1 (SUSE Linux)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import simplejson
>>> simplejson.__version__
'2.0.9'
>>> simplejson._speedups
<module 'simplejson._speedups' from '/home/carlos/.python-eggs/simplejson-2.0.9-py2.5-linux-i686.egg-tmp/simplejson/_speedups.so'>
>>> simplejson.encoder.FLOAT_REPR = lambda f: ("%.2f" % f)
>>> simplejson.dumps([23.67, 23.97, 23.87])
'[23.670000000000002, 23.969999999999999, 23.870000000000001]'
>>> simplejson.encoder.c_make_encoder = None
>>> simplejson.dumps([23.67, 23.97, 23.87])
'[23.67, 23.97, 23.87]'
>>>
import simplejson
    

class PrettyFloat(float):
def __repr__(self):
return '%.15g' % self
    

def pretty_floats(obj):
if isinstance(obj, float):
return PrettyFloat(obj)
elif isinstance(obj, dict):
return dict((k, pretty_floats(v)) for k, v in obj.items())
elif isinstance(obj, (list, tuple)):
return list(map(pretty_floats, obj))
return obj
    

print(simplejson.dumps(pretty_floats([23.67, 23.97, 23.87])))

发射

[23.67, 23.97, 23.87]

不需要给猴子贴补丁。

如果您正在使用 Python 2.7,一个简单的解决方案是显式地将浮点数四舍五入到所需的精度。

>>> sys.version
'2.7.1 (r271:86832, Nov 27 2010, 18:30:46) [MSC v.1500 32 bit (Intel)]'
>>> json.dumps(1.0/3.0)
'0.3333333333333333'
>>> json.dumps(round(1.0/3.0, 2))
'0.33'

This works because Python 2.7 made 浮动周期更加一致. Unfortunately this does not work in Python 2.6:

>>> sys.version
'2.6.6 (r266:84292, Dec 27 2010, 00:02:40) \n[GCC 4.4.5]'
>>> json.dumps(round(1.0/3.0, 2))
'0.33000000000000002'

上面提到的解决方案是针对2.6的变通方案,但没有一个是完全足够的。如果 Python 运行时使用 JSON 模块的 C 版本,那么猴子修补 JSON.encoder.FLOAT _ REPR 将无法工作。Tom Wuttke 的答案中的 PrettyFloat 类可以工作,但只有当% g 编码在全局范围内适用于您的应用程序时才能工作。15g 有点神奇,因为浮点精度是17有效数字,而且% g 不打印后面的零。

I spent some time trying to make a PrettyFloat that allowed customization of precision for each number. Ie, a syntax like

>>> json.dumps(PrettyFloat(1.0 / 3.0, 4))
'0.3333'

要做好这件事并不容易。从 float 继承是很尴尬的。继承 Object 并使用带有自己的 default ()方法的 JSONEncoder 子类应该可以工作,除了 json 模块似乎假定所有自定义类型都应该序列化为字符串。您的输出结果是 Javascript 字符串“0.33”,而不是数字0.33。也许还有一个办法可以解决这个问题,但是比看起来要难得多。

如果您需要在 python 2.7中在不覆盖全局 json.encoder.FLOAT _ REPR 的情况下执行此操作,这里有一种方法。

import json
import math


class MyEncoder(json.JSONEncoder):
"JSON encoder that renders floats to two decimal places"


FLOAT_FRMT = '{0:.2f}'


def floatstr(self, obj):
return self.FLOAT_FRMT.format(obj)


def _iterencode(self, obj, markers=None):
# stl JSON lame override #1
new_obj = obj
if isinstance(obj, float):
if not math.isnan(obj) and not math.isinf(obj):
new_obj = self.floatstr(obj)
return super(MyEncoder, self)._iterencode(new_obj, markers=markers)


def _iterencode_dict(self, dct, markers=None):
# stl JSON lame override #2
new_dct = {}
for key, value in dct.iteritems():
if isinstance(key, float):
if not math.isnan(key) and not math.isinf(key):
key = self.floatstr(key)
new_dct[key] = value
return super(MyEncoder, self)._iterencode_dict(new_dct, markers=markers)

然后,在 python 2.7中:

>>> from tmp import MyEncoder
>>> enc = MyEncoder()
>>> enc.encode([23.67, 23.98, 23.87])
'[23.67, 23.98, 23.87]'

在 python 2.6中,它并不像 Matthew Schinckel 指出的那样工作:

>>> import MyEncoder
>>> enc = MyEncoder()
>>> enc.encode([23.67, 23.97, 23.87])
'["23.67", "23.97", "23.87"]'

Alex Martelli 的解决方案将适用于单线程应用程序,但可能不适用于需要控制每个线程的小数位数的多线程应用程序。下面是一个可以在多线程应用程序中使用的解决方案:

import threading
from json import encoder


def FLOAT_REPR(f):
"""
Serialize a float to a string, with a given number of digits
"""
decimal_places = getattr(encoder.thread_local, 'decimal_places', 0)
format_str = '%%.%df' % decimal_places
return format_str % f


encoder.thread_local = threading.local()
encoder.FLOAT_REPR = FLOAT_REPR


#As an example, call like this:
import json


encoder.thread_local.decimal_places = 1
json.dumps([1.56, 1.54]) #Should result in '[1.6, 1.5]'

只需设置 encoder.thread _ local 即可。您需要的十进制位数,并且该线程中对 json.dump ()的下一个调用将使用该十进制位数

优点:

  • 可以使用任何 JSON 编码器,甚至是 python 的 repr。
  • 短(差不多) ,似乎有用。

缺点:

  • 难看的 regexp 入侵,几乎没有经过测试。
  • 二次复杂度。

    def fix_floats(json, decimals=2, quote='"'):
    pattern = r'^((?:(?:"(?:\\.|[^\\"])*?")|[^"])*?)(-?\d+\.\d{'+str(decimals)+'}\d+)'
    pattern = re.sub('"', quote, pattern)
    fmt = "%%.%df" % decimals
    n = 1
    while n:
    json, n = re.subn(pattern, lambda m: m.group(1)+(fmt % float(m.group(2)).rstrip('0')), json)
    return json
    

在导入标准 json 模块时,只需更改默认编码器 FLOAT _ REPR 即可。实际上并不需要导入或创建 Encoder 实例。

import json
json.encoder.FLOAT_REPR = lambda o: format(o, '.2f')


json.dumps([23.67, 23.97, 23.87]) #returns  '[23.67, 23.97, 23.87]'

有时候输出 json 也非常有用,因为 python 可以用 str 猜测最佳表示。这将确保有效数字不被忽略。

import json
json.dumps([23.67, 23.9779, 23.87489])
# output is'[23.670000000000002, 23.977900000000002, 23.874890000000001]'


json.encoder.FLOAT_REPR = str
json.dumps([23.67, 23.9779, 23.87489])
# output is '[23.67, 23.9779, 23.87489]'

非常不幸的是,dumps不允许您对 float 进行任何操作。不管 loads怎么做。因此,如果你不介意额外的 CPU 负载,你可以把它通过编码器/解码器/编码器,并得到正确的结果:

>>> json.dumps(json.loads(json.dumps([.333333333333, .432432]), parse_float=lambda x: round(float(x), 3)))
'[0.333, 0.432]'

我同意@Nelson 的观点,从 float 继承是很尴尬的,但是也许只涉及 __repr__函数的解决方案是可以原谅的。最后,我使用 decimal包在需要时重新格式化浮点数。这样做的好处是,它可以在调用 repr()的所有上下文中工作,例如,在简单地将列表打印到 stdout 时也是如此。此外,在创建数据之后,精度在运行时是可配置的。缺点当然是您的数据需要转换为这个特殊的 float 类(不幸的是,您似乎不能修补 float.__repr__)。为此,我提供了一个简短的转换函数。

密码:

import decimal
C = decimal.getcontext()


class decimal_formatted_float(float):
def __repr__(self):
s = str(C.create_decimal_from_float(self))
if '.' in s: s = s.rstrip('0')
return s


def convert_to_dff(elem):
try:
return elem.__class__(map(convert_to_dff, elem))
except:
if isinstance(elem, float):
return decimal_formatted_float(elem)
else:
return elem

Usage example:

>>> import json
>>> li = [(1.2345,),(7.890123,4.567,890,890.)]
>>>
>>> decimal.getcontext().prec = 15
>>> dff_li = convert_to_dff(li)
>>> dff_li
[(1.2345,), (7.890123, 4.567, 890, 890)]
>>> json.dumps(dff_li)
'[[1.2345], [7.890123, 4.567, 890, 890]]'
>>>
>>> decimal.getcontext().prec = 3
>>> dff_li = convert_to_dff(li)
>>> dff_li
[(1.23,), (7.89, 4.57, 890, 890)]
>>> json.dumps(dff_li)
'[[1.23], [7.89, 4.57, 890, 890]]'

下面是我在 Python 3中使用的一个解决方案,它不需要猴子修补程序:

import json


def round_floats(o):
if isinstance(o, float): return round(o, 2)
if isinstance(o, dict): return {k: round_floats(v) for k, v in o.items()}
if isinstance(o, (list, tuple)): return [round_floats(x) for x in o]
return o




json.dumps(round_floats([23.63437, 23.93437, 23.842347]))

产出为:

[23.63, 23.93, 23.84]

它复制数据,但使用圆形浮点数。

我刚刚发布了 Fjson,一个小的 Python 库来解决这个问题

pip install fjson

并使用类似于 json的方法,添加 float_format参数:

import math
import fjson




data = {"a": 1, "b": math.pi}
print(fjson.dumps(data, float_format=".6e", indent=2))
{
"a": 1,
"b": 3.141593e+00
}

用麻木

如果你实际上有很长的浮点数,你可以用 numpy 正确地四舍五入:

import json


import numpy as np


data = np.array([23.671234, 23.97432, 23.870123])


json.dumps(np.around(data, decimals=2).tolist())

'[23.67, 23.97, 23.87]'

我这样做了:)注意,在我的代码中,逗号后面总是有两个数字

>>> json_dumps_with_two_digit_float({'a': 1.0})
'{"a": 1.00}'

My custom function:

from unittest.mock import patch
import json
# We need to ensure that c encoder will not be launched
@patch('json.encoder.c_make_encoder', None)
def json_dumps_with_two_digit_float(some_object):
# saving original method
of = json.encoder._make_iterencode
def inner(*args, **kwargs):
args = list(args)
# fifth argument is float formater which will we replace
args[4] = lambda o: '{:.2f}'.format(o)
return of(*args, **kwargs)
    

with patch('json.encoder._make_iterencode', wraps=inner):
return json.dumps(some_object)

不要忘记在您的项目中创建一些测试,因为我的 func 与 python json 模块实现密切相关,将来可以对其进行更改。

新答案:

灵感来自 这个答案,它看起来很吓人,但实际上工作得很完美

import json


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


json.encoder.c_make_encoder = None


json.encoder.float = RoundingFloat


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

旧答案如下:

令我惊讶/困惑的是,这不是一个特性,幸运的是,TensorFlow 的作者通过使用 regex 获得了 已经解决了这个问题:

import json
import re


def FormatFloat(json_str, float_digits):
pattern = re.compile(r'\d+\.\d+')
float_repr = '{:.' + '{}'.format(float_digits) + 'f}'


def MRound(match):
return float_repr.format(float(match.group()))


return re.sub(pattern, MRound, json_str)


def Dumps(obj, float_digits=-1, **params):
"""Wrapper of json.dumps that allows specifying the float precision used.


Args:
obj: The object to dump.
float_digits: The number of digits of precision when writing floats out.
**params: Additional parameters to pass to json.dumps.


Returns:
output: JSON string representation of obj.
"""
json_str = json.dumps(obj, **params)


if float_digits > -1:
json_str = FormatFloat(json_str, float_digits)


return json_str

这只需从标准包中包装 json.dumps,然后对结果运行正则表达式即可。

中实现固定精度的浮点输出。Json 文件中,一种方法是修改 python _ dir lib json 模块中的 encoder.py 模块。

I first created a class:

        class FloatRepr(reprlib.Repr):
def repr_float(self,value,level):
return format(value,'.2f')

然后,将 floatstr 函数修改为:

        def floatstr(o, allow_nan=self.allow_nan, _repr=float.__repr__, _inf=INFINITY,_neginf=-INFINITY):


if o != o:
text = 'NaN'
elif o == _inf:
text = 'Infinity'
elif o == _neginf:
text = '-Infinity'
else:
# return _repr(o) # commented out
return FloatRepr().repr(o) # changes made


if not allow_nan:
raise ValueError(
"Out of range float values are not JSON compliant: " +
repr(o))


return text

通过这样做,. json 浮点值将不是字符串。