Формат плаває за допомогою стандартного модуля 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:

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 Python в кодері JSON.
Нельсон

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його не дотримуються.
Ніко Шльомер

57
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 це дає помилку "Об'єкт Map is не JSON serializable" , але ви можете вирішити перетворення карти () у список за допомогоюlist( map(pretty_floats, obj) )
Guglie

1
@Guglie: це тому, що в Python 3 mapповертається ітератор, а неlist
Азат Ібраков

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'

Виправити це правильно непросто. Успадкування від поплавця незручно. Наслідування від Object та використання підкласу JSONEncoder з власним методом default () має працювати, за винятком того, що модуль json передбачає, що всі власні типи повинні бути серіалізовані у вигляді рядків. Тобто, ви отримуєте рядок Javascript "0,33" на виході, а не число 0,33. Можливо, ще є спосіб зробити цю роботу, але це складніше, ніж це виглядає.


Інший підхід для Python 2.6, використовуючи JSONEncoder.iterencode та узор відповідності, можна побачити на сайті github.com/migurski/LilJSON/blob/master/liljson.py
Нельсон

Сподіваємось, це робить пересування ваших поплавців більш легким - мені подобається, як ми можемо уникати возитися з класами JSON, які можуть смоктати.
Лінкольн Б

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кваргу!
Анонім

Тут найпростіша пропозиція, яка також працює в 3.6.
Брент Фауст

Зверніть увагу на фразу "не заперечуйте зайве завантаження процесора". Однозначно не використовуйте це рішення, якщо у вас є багато даних для серіалізації. Для мене, додавши лише це, програма, яка робить нетривіальний обчислення, займає 3 рази більше.
shaneb

10

Ось рішення, яке працювало для мене в 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]

Він копіює дані, але із закругленими плавцями.


9

Якщо ви застрягли в Python 2.5 або більш ранніх версіях: фокус monkey-patch, здається, не працює з оригінальним модулем 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модулі.
Томаш

2

Рішення Алекса Мартеллі буде працювати для однопотокових програм, але може не працювати для багатопотокових програм, яким потрібно контролювати кількість десяткових знаків у потоці. Ось рішення, яке повинно працювати в багатопотокових додатках:

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 або навіть repr пітона.
  • Короткий (ish), здається, працює.

Мінуси:

  • Некрасивий регекс-хак, ледь перевірений.
  • Квадратична складність.

    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 найкращий пітон представлення може здогадатися зі 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()викликається, а також при простому друку списків для stdout, наприклад. Крім того, точність може бути налаштована після створення даних. Зворотний бік - це, звичайно, що ваші дані потрібно перетворити в цей спеціальний клас поплавця (як, на жаль, ви не можете здатись, що це патч мавпи 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]]'

Це не працює зі вбудованим пакетом json Python3, який не використовує __repr __ ().
Ян Голдбі

0

Використання нуме

Якщо у вас дійсно довгі поплавці, ви можете правильно їх округляти вгору / вниз:

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
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.