Формат плавает со стандартным модулем json
Я использую стандартный json module в python 2.6 для сериализации списка поплавков. Тем не менее, я получаю такие результаты:
>>> 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.
Как я могу это достичь?
Ответы
Ответ 1
К сожалению, я считаю, что вы должны сделать это путем обезглавливания (что, на мой взгляд, указывает на дефект дизайна в стандартном пакете библиотеки 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])
испускает:
23.67
[23.67, 23.97, 23.87]
как вы пожелаете. Очевидно, что должен быть защищенный способ переопределить FLOAT_REPR
, чтобы каждое представление float находилось под вашим контролем, если вы этого хотите; но, к сожалению, не так, как был разработан пакет json
: - (.
Ответ 2
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) # in Python3 do: list(map(pretty_floats, obj))
return obj
print simplejson.dumps(pretty_floats([23.67, 23.97, 23.87]))
излучает
[23.67, 23.97, 23.87]
Нет необходимости в обезьяне.
Ответ 3
Если вы используете 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'
Это работает, потому что 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'
Решения, упомянутые выше, являются обходными решениями для 2.6, но ни один из них не является полностью адекватным. Исправление Monkey json.encoder.FLOAT_REPR не работает, если ваша среда исполнения Python использует версию C модуля JSON. Класс PrettyFloat в Tom Wuttke отвечает, но только в том случае, если кодирование% g работает в глобальном масштабе для вашего приложения. %.15g немного мал, он работает, потому что точность float составляет 17 значащих цифр, а% g не печатает конечные нули.
Я потратил некоторое время, пытаясь сделать PrettyFloat, который позволил настроить точность для каждого числа. То есть, синтаксис типа
>>> json.dumps(PrettyFloat(1.0 / 3.0, 4))
'0.3333'
Это не так просто. Унаследовать от float неловко. Наследование объекта и использование подкласса JSONEncoder с его собственным методом default() должны работать, за исключением того, что модуль json, по-видимому, предполагает, что все пользовательские типы должны быть сериализованы как строки. Т.е.: вы получаете строку Javascript "0.33" на выходе, а не цифру 0.33. Возможно, еще есть способ сделать эту работу, но это сложнее, чем кажется.
Ответ 4
Действительно неудачно, что dumps
не позволяет вам ничего делать, чтобы плавать. Однако loads
. Поэтому, если вы не против дополнительной загрузки процессора, вы можете пропустить его через кодировщик/декодер/кодировщик и получить правильный результат:
>>> json.dumps(json.loads(json.dumps([.333333333333, .432432]), parse_float=lambda x: round(float(x), 3)))
'[0.333, 0.432]'
Ответ 5
Если вы застряли с Python 2.5 или более ранними версиями: трюк обезьяны-патча, похоже, не работает с исходным модулем simplejson, если установлены ускорения C:
$ 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]'
>>>
Ответ 6
Вы можете делать то, что вам нужно, но это не документировано:
>>> import json
>>> json.encoder.FLOAT_REPR = lambda f: ("%.2f" % f)
>>> json.dumps([23.67, 23.97, 23.87])
'[23.67, 23.97, 23.87]'
Ответ 7
Вот решение, которое работало для меня в 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]
Копирует данные, но с закругленными числами.
Ответ 8
Если вам нужно сделать это в 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 это не совсем работает, как указывает Матвей Шинкель:
>>> import MyEncoder
>>> enc = MyEncoder()
>>> enc.encode([23.67, 23.97, 23.87])
'["23.67", "23.97", "23.87"]'
Ответ 9
Решение 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.decimal_places количество нужных вам десятичных знаков, а следующий вызов json.dumps() в этом потоке будет использовать это число десятичных знаков
Ответ 10
Плюсы:
- Работает с любым JSON-кодировщиком или даже с python.
- Short (ish), похоже, работает.
Минусы:
- Ugly regexp hack, едва протестированный.
-
Квадратичная сложность.
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
Ответ 11
При импорте стандартного 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]'
Ответ 12
Я согласен с @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
Пример использования:
>>> 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]]'