格式浮动与标准的JSON模块

我在Python 2.6中使用标准的json模块来序列化浮动列表。 不过,我得到这样的结果:

>>> 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编码器类:

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

这适用于唯一的浮动对象:

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

但嵌套对象失败:

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

我不想有外部的依赖关系,所以我宁愿坚持使用标准的json模块。

我怎样才能做到这一点?

不幸的是,我相信你必须通过猴子修补(这在我看来,标志着json包标准库中的devise缺陷)。 例如,这个代码:

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

发出:

 23.67 [23.67, 23.97, 23.87] 

如你所愿。 显然,应该有一个架构的方法来覆盖FLOAT_REPR以便每个浮点的表示都在你的控制之下,如果你愿意的话; 但不幸的是,这不是如何devisejson包:-(。

 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 map(pretty_floats, obj) return obj print simplejson.dumps(pretty_floats([23.67, 23.97, 23.87])) 

发射

 [23.67, 23.97, 23.87] 

没有monkeypatching必要的。

如果你使用的是Python 2.7,一个简单的解决scheme就是将你的浮点数显式地舍入到所需的精度。

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

这是有效的,因为Python 2.7使浮动四舍五入更加一致 。 不幸的是,这在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' 

上面提到的解决scheme是2.6的解决方法,但没有一个是完全适合的。 如果您的Python运行时使用JSON模块的C版本,则Monkey修补json.encoder.FLOAT_REPR不起作用。 Tom Wuttke的答案中的PrettyFloat类是有效的,但前提是%g编码在全球范围内适用于您的应用程序。 %.15g有点不可思议,因为float精度是17位有效数字,%g不打印结尾零。

我花了一些时间试图做一个PrettyFloat,允许定制每个数字的精度。 即,像一个语法

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

要做到这一点并不容易。 从浮动inheritance是尴尬的。 从Objectinheritance,并使用JSONEncoder子类自己的默认()方法应该工作,除了json模块似乎假定所有的自定义types应串行化为string。 即:你输出的JavaScriptstring“0.33”,而不是数字0.33。 可能还有一种方法可以使这项工作,但它比看起来更难。

你可以做你需要做的,但没有logging:

 >>> 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 2.5或更早的版本中:如果安装了C加速,猴子补丁技巧似乎不能与原始的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]' >>> 

真的不幸的是, dumps不允许你做任何事情漂浮。 然而, loads 。 所以,如果你不介意额外的CPU负载,你可以通过编码器/解码器/编码器,并得到正确的结果:

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

Alex Martelli的解决scheme将适用于单线程应用程序,但可能不适用于需要控制每个线程的小数位数的multithreading应用程序。 这是一个可以在multithreading应用程序中工作的解决scheme:

 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.decimal_places设置为您想要的小数位数,然后在该线程中对json.dumps()的下一次调用将使用该小数位数

导入标准json模块时,更改默认编码器FLOAT_REPR就足够了。 实际上并不需要导入或创build编码器实例。

 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来猜测。 这将确保signifficant数字不会被忽略。

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

优点:

  • 与任何JSON编码器,甚至python的repr。
  • 短(ish),似乎工作。

缺点:

  • 丑陋的正则expression式黑客攻击,勉强testing。
  • 二次复杂性。

     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 

如果您需要在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"]' 

我同意@Nelson从floatinheritance是尴尬的,但也许只有触及__repr__函数的解决scheme可能是可以原谅的。 我最终使用这个decimal包来重新格式化漂浮在需要的时候。 好处在于,在调用repr()所有上下文中都可以工作,所以在简单地将列表打印到stdout时也是如此。 另外,数据创build后,精度是运行时可configuration的。 当然,下面的问题是你的数据需要被转换成这个特殊的float类(不幸的是你似乎无法修补float.__repr__ )。 为此我提供了一个简短的转换function。

代码:

 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 

用法示例:

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