格式浮动与标准的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]]'