Python JSON серіалізує об'єкт Decimal


242

У мене є Decimal('3.9')частина об'єкта, і я хочу його кодувати до рядка JSON, який повинен виглядати так {'x': 3.9}. Мене не хвилює точність на стороні клієнта, тому поплавок чудово.

Чи є хороший спосіб серіалізувати це? JSONDecoder не приймає об'єкти Decimal, і перетворення на поплавок заздалегідь дає вихід, {'x': 3.8999999999999999}що неправильно, і буде великою витратою пропускної здатності.



3.8999999999999999 - не більше, ніж 3,4 є. 0,2 не має точного представлення поплавця.
Ясен

@Jasen 3.89999999999 приблизно на 12,8% більше, ніж 3,4. Стандарт JSON стосується лише серіалізації та позначення, а не реалізації. Використання IEEE754 не є частиною необробленої специфікації JSON, це лише найпоширеніший спосіб її реалізації. Реалізація, яка використовує лише точну десяткову арифметику, повністю (насправді, навіть більш суворо) відповідає.
hraban

😂 менш неправильно. іронічно.
hraban

Відповіді:


147

Як щодо підкласифікації json.JSONEncoder?

class DecimalEncoder(json.JSONEncoder):
    def _iterencode(self, o, markers=None):
        if isinstance(o, decimal.Decimal):
            # wanted a simple yield str(o) in the next line,
            # but that would mean a yield on the line with super(...),
            # which wouldn't work (see my comment below), so...
            return (str(o) for o in [o])
        return super(DecimalEncoder, self)._iterencode(o, markers)

Потім використовуйте його так:

json.dumps({'x': decimal.Decimal('5.5')}, cls=DecimalEncoder)

Ну, я щойно помітив, що насправді це не вийде так. Відредагуємо відповідно. (Однак ідея залишається такою ж.)
Michał Marczyk

Проблема полягала в тому, що DecimalEncoder()._iterencode(decimal.Decimal('3.9')).next()повернув правильний '3.9', але DecimalEncoder()._iterencode(3.9).next()повернув об’єкт-генератор, який повертався б лише '3.899...'тоді, коли ви згрупували інший .next(). Генератор смішний бізнес. Ну добре ... Треба працювати зараз.
Michał Marczyk

8
Ви не можете просто return (str(o),)замість цього? [o]це список лише з 1 елементом, навіщо турбуватися циклічно над ним?
квітня 11

2
@Mark: return (str(o),)поверне кортеж довжиною 1, а код у відповіді повертає генератор довжини 1. Дивіться документи iterencode ()
Абган

30
Ця реалізація більше не працює. Еліас Замарія - той, хто працює над тим же стилем.
піро

223

Simplejson 2.1 і вище має вбудовану підтримку типу Decimal:

>>> json.dumps(Decimal('3.9'), use_decimal=True)
'3.9'

Зауважте, що use_decimalце Trueза замовчуванням:

def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True,
    allow_nan=True, cls=None, indent=None, separators=None,
    encoding='utf-8', default=None, use_decimal=True,
    namedtuple_as_object=True, tuple_as_array=True,
    bigint_as_string=False, sort_keys=False, item_sort_key=None,
    for_json=False, ignore_nan=False, **kw):

Так:

>>> json.dumps(Decimal('3.9'))
'3.9'

Сподіваємось, ця функція буде включена до стандартної бібліотеки.


7
Хм, для мене це перетворює десяткові об'єкти в плаваючі, що не прийнятно. Наприклад, втрата точності при роботі з валютою.
Меттью Шинкель

12
@MatthewSchinckel Я думаю, що це не так. Це фактично робить з нього рядок. І якщо ви повернете результуючий рядок назад до json.loads(s, use_decimal=True)нього, ви повернете десяткові. Немає плавця протягом усього процесу. Відповідь відредагована вище. Сподіваюся, оригінальний плакат добре з ним.
Шехар

1
Ага, я думаю, що я також не використовував use_decimal=Trueнавантаженнях.
Меттью Шинкель

1
Для мене json.dumps({'a' : Decimal('3.9')}, use_decimal=True)дає '{"a": 3.9}'. Ціль не була '{"a": "3.9"}'?
MrJ

5
simplejson.dumps(decimal.Decimal('2.2'))також працює: немає явного use_decimal(перевірено на simplejson / 3.6.0). Ще один спосіб завантажити його назад: json.loads(s, parse_float=Decimal)тобто, ви можете прочитати його за допомогою stdlib json(і старі simplejsonверсії також підтримуються).
jfs

181

Я хотів би повідомити всім про те, що я спробував відповідь Міхала Марчика на своєму веб-сервері, який працює під управлінням Python 2.6.5, і він працював чудово. Однак я перейшов на Python 2.7, і він перестав працювати. Я намагався придумати якийсь спосіб кодування об'єктів Decimal, і ось що я придумав:

import decimal

class DecimalEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, decimal.Decimal):
            return float(o)
        return super(DecimalEncoder, self).default(o)

Це, сподіваємось, допоможе всім, хто має проблеми з Python 2.7. Я перевірив це, і, здається, він працює добре. Якщо хтось помітить помилки в моєму рішенні або придумає кращий спосіб, будь ласка, дайте мені знати.


4
Python 2.7 змінив правила округлення плавців, щоб це працювало. Дивіться дискусію в stackoverflow.com/questions/1447287/…
Нельсон

2
Для тих із нас, хто не може використовувати simplejson (тобто в Google App Engine), ця відповідь є набогом даних.
Джоель Крос

17
Використовуйте unicodeабо strзамість цього, floatщоб забезпечити точність.
Seppo Erviälä

2
Проблема з 54.3999 ... була важливою в Python 2.6.x та старіших версіях, де плаваючий перехід у рядок не працював регулярно, але перетворення Десяткове число у str є набагато більш невірним, оскільки воно було б серіалізоване як рядок з подвійними лапками "54.4", а не як число.
hynekcer

1
Працює в python3
SeanFromIT

43

У моєму додатку Flask, який використовує python 2.7.11, алхіму колби (з типами "db.decimal") та Flask Marshmallow (для "миттєвого" серіалізатора та десеріалізатора), у мене була така помилка, кожного разу, коли я робив GET або POST . Не вдалося перетворити типи десятків у будь-який формат JSON.

Я зробив "pip install simplejson", потім просто додавши

import simplejson as json

серіалізатор і десеріалізатор знову починають муркотіти. Я більше нічого не робив ... DEciamls відображаються як плаваючий формат "234.00".


1
найпростіший виправлення
SMDC

1
Як не дивно, вам навіть не потрібно імпортувати simplejson- лише встановивши його, це зробити трюк. Спочатку згадується цією відповіддю .
bsplosion

Це не працює на мене, і все одно отримав це Decimal('0.00') is not JSON serializable після встановлення через pip. Така ситуація, коли ви використовуєте зефір і графен. Коли запит викликається на іншому api, зефір очікується для десяткових полів. Однак коли він викликається graphql, він викликав is not JSON serializableпомилку.
Roel

Фантастичний, Чудовий,
Людина-павук

Ідеально! Це працює в ситуаціях, коли ви використовуєте модуль, написаний кимось іншим, кого ви не можете легко змінити (у моєму випадку gspread для використання Google Sheets)
happyyskeptic

32

Я спробував перейти з simplejson на вбудований json для GAE 2.7, і виникли проблеми з десятковою. Якщо за замовчуванням повернуто str (o), там були лапки (тому що _iterencode викликає _iterencode за результатами за замовчуванням), а float (o) видалить 0.

Якщо за замовчуванням повертається об’єкт класу, який успадковується від float (або все, що викликає repr без додаткового форматування) і має користувацький метод __repr__, він, здається, працює так, як я цього хочу.

import json
from decimal import Decimal

class fakefloat(float):
    def __init__(self, value):
        self._value = value
    def __repr__(self):
        return str(self._value)

def defaultencode(o):
    if isinstance(o, Decimal):
        # Subclass float with custom repr?
        return fakefloat(o)
    raise TypeError(repr(o) + " is not JSON serializable")

json.dumps([10.20, "10.20", Decimal('10.20')], default=defaultencode)
'[10.2, "10.20", 10.20]'

Приємно! Це гарантує, що десяткове значення закінчується в JSON у вигляді поплавця Javascript, не маючи Python спочатку округляти його до найближчого значення поплавця.
konrad

3
На жаль, це не працює в останніх Python 3. Зараз існує деякий код швидкого шляху, який розглядає всі float-підкласи як плавні і не викликає repr на них взагалі.
Антті Хаапала

@AnttiHaapala, приклад прекрасно працює на Python 3.6.
Крістіан Цюпіту

@ CristianCiupitu справді, я, здається, не в змозі відтворити погану поведінку зараз
Antti Haapala

2
Рішення перестало працювати так v3.5.2rc1 см github.com/python/cpython/commit / ... . Є float.__repr__жорсткий код (який втрачає точність), і його fakefloat.__repr__взагалі не називають. Вищевказане рішення працює належним чином для python3 до 3.5.1, якщо fakefloat має додатковий метод def __float__(self): return self.
мирослав

30

Нативний варіант відсутній, тому я додам його для наступного хлопця / жовчного, який шукає його.

Починаючи з Django 1.7.x є вбудована версія, з DjangoJSONEncoderякої ви можете отримати її django.core.serializers.json.

import json
from django.core.serializers.json import DjangoJSONEncoder
from django.forms.models import model_to_dict

model_instance = YourModel.object.first()
model_dict = model_to_dict(model_instance)

json.dumps(model_dict, cls=DjangoJSONEncoder)

Престо!


Хоча це чудово знати, ОП не запитували про Джанго?
std''OrgnlDave

4
@ std''OrgnlDave ви на 100% правильні. Я забув, як я потрапив сюди, але я переглянув це запитання з "django", приєднаним до пошукового терміну, і це з'явилося, після трохи більше гуглінгу, я знайшов відповідь і додав його тут для наступної людини, як я, що натикається на це
Хав'єр Буцці

6
ти врятуєш мій день
gaozhidf

14

Мої $ 02!

Я розширюю купу кодера JSON, оскільки я серіалізую тони даних для свого веб-сервера. Ось приємний код. Зауважте, що це легко розширюється майже до будь-якого формату даних, який ви відчуваєте, і буде відтворювати 3,9 як"thing": 3.9

JSONEncoder_olddefault = json.JSONEncoder.default
def JSONEncoder_newdefault(self, o):
    if isinstance(o, UUID): return str(o)
    if isinstance(o, datetime): return str(o)
    if isinstance(o, time.struct_time): return datetime.fromtimestamp(time.mktime(o))
    if isinstance(o, decimal.Decimal): return str(o)
    return JSONEncoder_olddefault(self, o)
json.JSONEncoder.default = JSONEncoder_newdefault

Полегшує моє життя ...


3
Це неправильно: воно відтворює 3,9 як "thing": "3.9".
Гліф

найкращі рішення з усіх, дуже прості, дякую, що ти врятував мій день, для мене достатньо, щоб зберегти число, в рядку для
десятків

@Glyph за JSON стандартами (яких декілька ...), без котируваного числа є плаваюча крапка з подвійною точністю, а не десяткове число. Цитування це єдиний спосіб гарантувати сумісність.
std''OrgnlDave

2
у вас є цитування на це? Кожна специфікація, яку я прочитав, означає, що це залежить від реалізації.
Гліф

12

3.9не може бути точно представлено у плавках IEEE, це завжди буде як 3.8999999999999999, наприклад, спробуйте print repr(3.9), ви можете прочитати більше про це тут:

http://en.wikipedia.org/wiki/Floating_point
http://docs.sun.com/source/806-3568/ncg_goldberg.html

Отже, якщо ви не хочете плавати, єдиний варіант, вам потрібно надіслати його як рядок, а також дозволити автоматичне перетворення десяткових об'єктів у JSON, зробіть щось подібне:

import decimal
from django.utils import simplejson

def json_encode_decimal(obj):
    if isinstance(obj, decimal.Decimal):
        return str(obj)
    raise TypeError(repr(obj) + " is not JSON serializable")

d = decimal.Decimal('3.5')
print simplejson.dumps([d], default=json_encode_decimal)

Я знаю, що вона не буде 3,9 внутрішньо, коли вона буде розібрана на клієнті, але 3.9 є дійсним поплавком JSON. тобто json.loads("3.9")буде працювати, і я хотів би, щоб це було так
Knio

@Anurag У своєму прикладі ви мали на увазі repr (obj) замість repr (o).
orokusaki

Хіба це не просто помре, якщо спробувати закодувати щось, що не десяткове?
mikemaccana

1
@nailer, ні, не буде, ви можете спробувати це, оскільки причина за замовчуванням підвищити виняток для сигналу про те, що слід використовувати наступний обробник
Anurag Uniyal

1
Дивіться відповідь mikez302 - в Python 2.7 або вище це більше не стосується.
Джоель Крос

9

Для користувачів Django :

Нещодавно натрапили на TypeError: Decimal('2337.00') is not JSON serializable той час, як кодування JSON, тобтоjson.dumps(data)

Рішення :

# converts Decimal, Datetime, UUIDs to str for Encoding
from django.core.serializers.json import DjangoJSONEncoder  

json.dumps(response.data, cls=DjangoJSONEncoder)

Але тепер десяткове значення буде рядком, тепер ми можемо явно встановити аналізатор значення десяткового / плаваючого значення при декодуванні даних, використовуючи parse_floatопцію в json.loads:

import decimal 

data = json.loads(data, parse_float=decimal.Decimal) # default is float(num_str)

8

З стандартного документа JSON , як посилається на json.org :

JSON агностично стосується семантики чисел. У будь-якій мові програмування може бути безліч типів чисел різних ємностей та доповнень, фіксованих або плаваючих, двійкових чи десяткових. Це може ускладнити обмін між різними мовами програмування. Натомість JSON пропонує лише представлення чисел, якими користуються люди: послідовність цифр. Усі мови програмування знають, як осмислити послідовності цифр, навіть якщо вони не погоджуються щодо внутрішніх уявлень. Цього достатньо, щоб дозволити обмін.

Тож насправді точно представляти Десяткові числа як числа (а не рядки) в JSON. Внизу лежить можливий варіант вирішення проблеми.

Визначте спеціальний кодер JSON:

import json


class CustomJsonEncoder(json.JSONEncoder):

    def default(self, obj):
        if isinstance(obj, Decimal):
            return float(obj)
        return super(CustomJsonEncoder, self).default(obj)

Потім використовуйте це під час серіалізації даних:

json.dumps(data, cls=CustomJsonEncoder)

Як зазначається в коментарях до інших відповідей, старіші версії python можуть зіпсувати представлення при перетворенні в плавати, але це вже не так.

Щоб повернути десяткову кількість назад у Python:

Decimal(str(value))

Це рішення натякається в документації Python 3.0 щодо десяткових знаків :

Щоб створити десятковий знак з плавця, спочатку перетворіть його в рядок.


2
Це не «фіксованою» в Python 3. Перетворення до float обов'язково змушує вас втрачати десяткове подання, і буде призводити до невідповідностей. Якщо Decimalважливо використовувати, я вважаю, що краще використовувати рядки.
juanpa.arrivillaga

Я вважаю, що це безпечно зробити з python 3.1. Втрата точності може бути шкідливою при арифметичних операціях, але у випадку кодування JSON ви просто створюєте рядовий показник значення, тому точності більш ніж достатньо для більшості випадків використання. Все в JSON вже є рядком, тому розміщення лапок навколо значення просто не відповідає специфікації JSON.
Хуго Мота

З урахуванням сказаного, я розумію занепокоєння навколо перетворення в плавати. Можливо, існує інша стратегія, що використовується з кодером для створення потрібної рядка відображення. Проте я не думаю, що варто виробляти цінову вартість.
Хуго Мота

@HugoMota "Все в JSON вже є рядком, тому розміщення лапок навколо значення просто не відповідає специфікації JSON." Ні: rfc-editor.org/rfc/rfc8259.txt - JSON - це текстовий формат кодування, але це не означає, що все в ньому має бути інтерпретовано як рядок. Специфікація визначає, як кодувати числа, окремо від рядків.
Gunnar Þór

@ GunnarÞórMagnússon "JSON - це текстовий формат кодування" - ось що я мав на увазі під "все - це рядок". Заздалегідь перетворення чисел у рядки не дозволить магічно зберегти точність, оскільки це все одно буде рядком, коли він стане JSON. І згідно з специфікацією, цифри не мають лапок навколо неї. Це відповідальність читача за збереження точності під час читання (не цитата, я лише сприймаю це).
Хуго Мота

6

Це те, що я отримав з нашого класу

class CommonJSONEncoder(json.JSONEncoder):

    """
    Common JSON Encoder
    json.dumps(myString, cls=CommonJSONEncoder)
    """

    def default(self, obj):

        if isinstance(obj, decimal.Decimal):
            return {'type{decimal}': str(obj)}

class CommonJSONDecoder(json.JSONDecoder):

    """
    Common JSON Encoder
    json.loads(myString, cls=CommonJSONEncoder)
    """

    @classmethod
    def object_hook(cls, obj):
        for key in obj:
            if isinstance(key, six.string_types):
                if 'type{decimal}' == key:
                    try:
                        return decimal.Decimal(obj[key])
                    except:
                        pass

    def __init__(self, **kwargs):
        kwargs['object_hook'] = self.object_hook
        super(CommonJSONDecoder, self).__init__(**kwargs)

Який проходить unittest:

def test_encode_and_decode_decimal(self):
    obj = Decimal('1.11')
    result = json.dumps(obj, cls=CommonJSONEncoder)
    self.assertTrue('type{decimal}' in result)
    new_obj = json.loads(result, cls=CommonJSONDecoder)
    self.assertEqual(new_obj, obj)

    obj = {'test': Decimal('1.11')}
    result = json.dumps(obj, cls=CommonJSONEncoder)
    self.assertTrue('type{decimal}' in result)
    new_obj = json.loads(result, cls=CommonJSONDecoder)
    self.assertEqual(new_obj, obj)

    obj = {'test': {'abc': Decimal('1.11')}}
    result = json.dumps(obj, cls=CommonJSONEncoder)
    self.assertTrue('type{decimal}' in result)
    new_obj = json.loads(result, cls=CommonJSONDecoder)
    self.assertEqual(new_obj, obj)

json.loads(myString, cls=CommonJSONEncoder)коментар повинен бутиjson.loads(myString, cls=CommonJSONDecoder)
Can Kavaklıoğlu

object_hook потребує значення за замовчуванням, якщо obj не є десятковою.
Може Кавакльооглу

3

Ви можете створити спеціальний кодер JSON відповідно до ваших вимог.

import json
from datetime import datetime, date
from time import time, struct_time, mktime
import decimal

class CustomJSONEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, datetime):
            return str(o)
        if isinstance(o, date):
            return str(o)
        if isinstance(o, decimal.Decimal):
            return float(o)
        if isinstance(o, struct_time):
            return datetime.fromtimestamp(mktime(o))
        # Any other serializer if needed
        return super(CustomJSONEncoder, self).default(o)

Декодер можна назвати так,

import json
from decimal import Decimal
json.dumps({'x': Decimal('3.9')}, cls=CustomJSONEncoder)

і вихід буде:

>>'{"x": 3.9}'

приголомшливий ... Дякую за одне зупинкове рішення (у)
Мухаммед базилік

Це справді працює! Дякуємо, що поділилися своїм рішенням
tthreetorch

3

Для тих, хто не хоче використовувати сторонню бібліотеку ... Проблема з відповіддю Еліаса Замарії полягає в тому, що вона перетворюється на плаваючий характер, що може зіткнутися з проблемами. Наприклад:

>>> json.dumps({'x': Decimal('0.0000001')}, cls=DecimalEncoder)
'{"x": 1e-07}'
>>> json.dumps({'x': Decimal('100000000000.01734')}, cls=DecimalEncoder)
'{"x": 100000000000.01733}'

JSONEncoder.encode()Метод дозволяє повернути буквальне зміст JSon, в відміну JSONEncoder.default(), яке ви повертаєте сумісний тип JSon (як поплавок) , який потім буде закодований звичайним способом. Проблема в encode()тому, що вона (як правило) працює лише на найвищому рівні. Але це все-таки корисно, з невеликою кількістю додаткової роботи (python 3.x):

import json
from collections.abc import Mapping, Iterable
from decimal import Decimal

class DecimalEncoder(json.JSONEncoder):
    def encode(self, obj):
        if isinstance(obj, Mapping):
            return '{' + ', '.join(f'{self.encode(k)}: {self.encode(v)}' for (k, v) in obj.items()) + '}'
        if isinstance(obj, Iterable) and (not isinstance(obj, str)):
            return '[' + ', '.join(map(self.encode, obj)) + ']'
        if isinstance(obj, Decimal):
            return f'{obj.normalize():f}'  # using normalize() gets rid of trailing 0s, using ':f' prevents scientific notation
        return super().encode(obj)

Що дає вам:

>>> json.dumps({'x': Decimal('0.0000001')}, cls=DecimalEncoder)
'{"x": 0.0000001}'
>>> json.dumps({'x': Decimal('100000000000.01734')}, cls=DecimalEncoder)
'{"x": 100000000000.01734}'

2

На основі відповіді stdOrgnlDave я визначив цю обгортку, що її можна викликати за допомогою додаткових типів, так що кодер буде працювати лише для певних типів у ваших проектах. Я вважаю, що роботу слід виконувати всередині вашого коду, а не використовувати цей "кодер" за замовчуванням, оскільки "це краще явне, ніж неявне", але я розумію, що використання цього заощадить частину вашого часу. :-)

import time
import json
import decimal
from uuid import UUID
from datetime import datetime

def JSONEncoder_newdefault(kind=['uuid', 'datetime', 'time', 'decimal']):
    '''
    JSON Encoder newdfeault is a wrapper capable of encoding several kinds
    Use it anywhere on your code to make the full system to work with this defaults:
        JSONEncoder_newdefault()  # for everything
        JSONEncoder_newdefault(['decimal'])  # only for Decimal
    '''
    JSONEncoder_olddefault = json.JSONEncoder.default

    def JSONEncoder_wrapped(self, o):
        '''
        json.JSONEncoder.default = JSONEncoder_newdefault
        '''
        if ('uuid' in kind) and isinstance(o, uuid.UUID):
            return str(o)
        if ('datetime' in kind) and isinstance(o, datetime):
            return str(o)
        if ('time' in kind) and isinstance(o, time.struct_time):
            return datetime.fromtimestamp(time.mktime(o))
        if ('decimal' in kind) and isinstance(o, decimal.Decimal):
            return str(o)
        return JSONEncoder_olddefault(self, o)
    json.JSONEncoder.default = JSONEncoder_wrapped

# Example
if __name__ == '__main__':
    JSONEncoder_newdefault()

0

Якщо ви хочете передати до requestsбібліотеки словник, що містить десяткові знаки (використовуючи jsonаргумент ключового слова), вам просто потрібно встановити simplejson:

$ pip3 install simplejson    
$ python3
>>> import requests
>>> from decimal import Decimal
>>> # This won't error out:
>>> requests.post('https://www.google.com', json={'foo': Decimal('1.23')})

Причина проблеми полягає в тому, що він requestsвикористовується simplejsonлише за наявності, і повертається до вбудованого, jsonякщо він не встановлений.


-6

це можна зробити, додавши

    elif isinstance(o, decimal.Decimal):
        yield str(o)

в \Lib\json\encoder.py:JSONEncoder._iterencode, але я сподівався на краще рішення


5
Ви можете підкласифікувати JSONEncoder, як описано вище, редагування встановлених файлів Python встановленої бібліотеки або самого інтерпретатора має бути крайнім заходом.
justanr
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.