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