Форматирование плавающих объектов с помощью стандартного модуля json

100

Я использую стандартный модуль json в 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)

Это работает для единственного плавающего объекта:

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

Но не работает для вложенных объектов:

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

Я не хочу иметь внешние зависимости, поэтому предпочитаю использовать стандартный модуль json.

Как я могу этого добиться?

Мануэль Серон
источник

Ответы:

80

Примечание. Это не работает ни в одной из последних версий 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]))

испускает:

23.67
[23.67, 23.97, 23.87]

как пожелаете. Очевидно, должен быть продуманный способ переопределения, FLOAT_REPRчтобы КАЖДОЕ представление поплавка находилось под вашим контролем, если вы этого хотите; но, к сожалению, jsonпакет был разработан не так :-(.

Алекс Мартелли
источник
10
Это решение не работает в Python 2.7 с использованием версии C кодировщика JSON для Python.
Нельсон
25
Как бы то ни было, используйте что-то вроде% .15g или% .12g вместо% .3f.
Гвидо ван Россум,
23
Я нашел этот фрагмент в коде младшего программиста. Если бы она не была обнаружена, это привело бы к очень серьезной, но незаметной ошибке. Не могли бы вы поместить предупреждение в этот код, объясняя глобальные последствия этого исправления обезьяны.
Рори Харт
12
Когда вы закончите, это хорошая гигиена: original_float_repr = encoder.FLOAT_REPR encoder.FLOAT_REPR = lambda o: format(o, '.2f') print json.dumps(1.0001) encoder.FLOAT_REPR = original_float_repr
Джефф Кауфман,
6
Как отмечали другие, это больше не работает по крайней мере в Python 3.6+. Добавьте несколько цифр, чтобы 23.67увидеть, как .2fэто не соблюдается.
Нико Шлёмер 05
58
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]

Никакой обезьяны не требуется.

Том Вуттке
источник
2
Мне нравится это решение; лучшая интеграция, работает с 2.7. Поскольку я все равно сам собираю данные, я исключил эту pretty_floatsфункцию и просто интегрировал ее в свой другой код.
mikepurvis
1
В Python3 выдается ошибка «Объект карты не сериализуем в JSON» , но вы можете разрешить преобразование карты () в список с помощьюlist( map(pretty_floats, obj) )
Guglie
1
@Guglie: это потому, что в Python 3 mapвозвращает итератор, а не alist
Азат Ибраков
4
У меня не работает (Python 3.5.2, simplejson 3.16.0). Пробовал с% .6g и [23.671234556, 23.971234556, 23.871234556], он все равно печатает целое число.
szali
27

Если вы используете 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, но ни одно из них не является полностью адекватным. Исправление обезьяны json.encoder.FLOAT_REPR не работает, если ваша среда выполнения Python использует версию C модуля JSON. Класс PrettyFloat в ответе Тома Вутке работает, но только если кодировка% g работает глобально для вашего приложения. % .15g - это немного волшебство, оно работает, потому что точность с плавающей запятой составляет 17 значащих цифр, а% g не выводит конечные нули.

Я потратил некоторое время, пытаясь создать PrettyFloat, который позволял бы настраивать точность для каждого числа. Т.е. синтаксис вроде

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

Это непросто сделать правильно. Наследование от float неудобно. Наследование от Object и использование подкласса JSONEncoder с его собственным методом default () должно работать, за исключением того, что модуль json, похоже, предполагает, что все настраиваемые типы должны быть сериализованы как строки. То есть: вы получите строку Javascript «0,33» на выходе, а не число 0,33. Возможно, еще есть способ заставить эту работу работать, но это сложнее, чем кажется.

Нельсон
источник
Другой подход для Python 2.6 с использованием JSONEncoder.iterencode и сопоставления с образцом можно увидеть на github.com/migurski/LilJSON/blob/master/liljson.py
Нельсон,
Надеюсь, это облегчит передачу ваших поплавков - мне нравится, как мы можем избежать путаницы с классами JSON, которые могут быть отстойными.
Lincoln B
20

Очень жаль, что dumpsвы не можете ничего делать с плавающими. Однако loadsделает. Так что, если вы не возражаете против дополнительной загрузки процессора, вы можете пропустить его через кодировщик / декодер / кодировщик и получить правильный результат:

>>> json.dumps(json.loads(json.dumps([.333333333333, .432432]), parse_float=lambda x: round(float(x), 3)))
'[0.333, 0.432]'
Клод
источник
Спасибо, это действительно полезное предложение. Я не знал про parse_floatкварг!
Anonymous
Самое простое предложение, которое также работает в версии 3.6.
Brent Faust
Обратите внимание на фразу «не обращайте внимания на дополнительную нагрузку на процессор». Определенно не используйте это решение, если у вас есть много данных для сериализации. По мне, добавление одного только этого заставило программу, выполняющую нетривиальные вычисления, занять в 3 раза больше времени.
shaneb
11

Вот решение, которое сработало для меня в 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]

Он копирует данные, но с округленными числами.

Jcoffland
источник
9

Если вы застряли с 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]'
>>> 
Карлос Валиенте
источник
7

Вы можете делать то, что вам нужно, но это не задокументировано:

>>> import json
>>> json.encoder.FLOAT_REPR = lambda f: ("%.2f" % f)
>>> json.dumps([23.67, 23.97, 23.87])
'[23.67, 23.97, 23.87]'
Нед Батчелдер
источник
5
Выглядит аккуратно, но, похоже, не работает на Python 3.6. В частности, я не увидел FLOAT_REPRв json.encoderмодуле константу .
Tomasz
2

Решение 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 () в этом потоке будет использовать это количество десятичных знаков.

Антон И. Сипос
источник
2

Если вам нужно сделать это в 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"]'
Майк Фогель
источник
4
Они выглядят как строки, а не числа.
Мэтью Шинкель
1

Плюсы:

  • Работает с любым кодировщиком JSON или даже с python repr.
  • Коротко (иш), похоже, работает.

Минусы:

  • Уродливый хак с регулярным выражением, почти не проверенный.
  • Квадратичная сложность.

    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
Сэм Уоткинс
источник
1

При импорте стандартного модуля 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]'
Ф. Перейра
источник
1

Я согласен с @Nelson в том, что наследование от float неудобно, но, возможно, решение, касающееся только __repr__функции, может быть простительным. В итоге я использовал decimalпакет для этого, чтобы при необходимости переформатировать поплавки. Положительным моментом является то, что это работает во всех контекстах, в которых repr()вызывается, а также при простой печати списков, например, в стандартный вывод. Кроме того, точность настраивается во время выполнения после создания данных. Обратной стороной является, конечно, то, что ваши данные должны быть преобразованы в этот специальный класс с плавающей запятой (поскольку, к сожалению, вы не можете использовать патч обезьяны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]]'
user1556435
источник
Это не работает со встроенным json-пакетом Python3, который не использует __repr __ ().
Ian Goldby
0

Использование numpy

Если у вас действительно длинные числа с плавающей запятой, вы можете правильно округлить их вверх / вниз с помощью 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]'

Михаил
источник
-1

Я только что выпустил 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
}
Нико Шлёмер
источник