Python JSON сериализует десятичный объект

242

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

Есть ли хороший способ сериализовать это? JSONDecoder не принимает десятичные объекты, и предварительное преобразование в число с плавающей запятой дает {'x': 3.8999999999999999}неправильный результат, и это приведет к большой трате пропускной способности.

Knio
источник
3.8999999999999999 не более неправильный, чем 3.4. 0.2 не имеет точного представления с плавающей точкой.
Jasen
@Jasen 3.89999999999 примерно на 12,8% больше, чем 3,4. Стандарт JSON касается только сериализации и записи, а не реализации. Использование IEEE754 не является частью необработанной спецификации JSON, это всего лишь наиболее распространенный способ его реализации. Реализация, которая использует только точную десятичную арифметику, полностью (фактически, даже более строго) соответствует.
Грабан
😂 менее неправильно. иронично.
Храбан

Ответы:

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)
Михал Марчик
источник
Я только что заметил, что на самом деле это не сработает. Буду редактировать соответственно. (Однако идея остается прежней.)
Михал Марчик,
Проблема заключалась в том, что DecimalEncoder()._iterencode(decimal.Decimal('3.9')).next()возвращался правильный '3.9', но DecimalEncoder()._iterencode(3.9).next()возвращался объект генератора, который возвращался только '3.899...'тогда, когда вы наваливали другой .next(). Генератор забавного бизнеса. Ну хорошо ... Должно работать сейчас.
Михал Марчик
8
Вы не можете просто return (str(o),)вместо этого? [o]список только с одним элементом, зачем его зацикливать?
mpen
2
@Mark: return (str(o),)вернет кортеж длины 1, а код в ответе вернет генератор длины 1. См. Документы iterencode ()
Abgan
30
Эта реализация больше не работает. Элиас Замария - тот, кто работает в том же стиле.
Пиро
224

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, и он перестал работать. Я попытался придумать какой-нибудь способ кодирования десятичных объектов, и вот что я придумал:

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чтобы обеспечить точность.
Сеппо Эрвиаля
2
Проблема с 54.3999 ... была важна в Python 2.6.x и старше, где преобразование с плавающей точкой в ​​строку не работало регулярно, но преобразование Decimal в str гораздо более некорректно, поскольку оно было бы сериализовано как строка с двойными кавычками "54.4", а не число.
hynekcer
1
Работает в python3
SeanFromIT
43

В моем приложении Flask, которое использует python 2.7.11, алхимия фляг (с типами 'db.decimal) и Flask Marshmallow (для сериализатора и десериализатора' instant '), у меня была эта ошибка, каждый раз, когда я делал GET или POST , Сериализатору и десериализатору не удалось преобразовать десятичные типы в любой идентифицируемый формат JSON.

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

import simplejson as json

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

ISONecroMAn
источник
1
самое простое исправление
SMDC
1
Как ни странно, вам даже не нужно импортировать simplejson- просто установка делает свое дело. Первоначально упоминается этот ответ .
bsplosion
Это не работает на меня, и все же получил это Decimal('0.00') is not JSON serializable после установки его через pip. Это ситуация, когда вы используете и зефир, и графен. Когда запрос вызывается для API остальных, зефир работает ожидаемо для десятичных полей. Однако, когда он вызывается с graphql, возникает is not JSON serializableошибка.
Roel
Фантастика, Превосходно,
Человек-паук
Отлично! Это работает в ситуациях, когда вы используете модуль, написанный кем-то другим, который вы не можете легко изменить (в моем случае это распространено за использование Google Sheets)
happyskeptic
32

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

Если default возвращает объект класса, который наследуется от 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]'
tesdal
источник
Ницца! Это гарантирует, что десятичное значение попадает в JSON как плавание Javascript, без необходимости Python сначала округлять его до ближайшего значения с плавающей точкой.
Конрад
3
К сожалению, это не работает в последних версиях Python 3. Теперь есть некоторый код быстрого пути, который рассматривает все подклассы с плавающей точкой как плавающие и не вызывает repr для них вообще.
Антти Хаапала
@AnttiHaapala, пример отлично работает на Python 3.6.
Кристиан Чиупиту
@CristianCiupitu, действительно, сейчас я не могу воспроизвести плохое поведение
Антти Хаапала
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)

Presto!

Хавьер Буззи
источник
Хотя это приятно знать, ОП не спрашивал о Джанго?
std''OrgnlDave
4
@ std''OrgnlDave вы на 100% правы. Я забыл, как я попал сюда, но я погуглил этот вопрос с «django», прикрепленным к поисковому запросу, и он возник, после немного большего количества поисков, я нашел ответ и добавил его здесь для следующего человека, как я, который наткнулся это
Хавьер Баззи
6
ты спас мой день
гаожидф
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

Делает мою жизнь намного проще ...

std''OrgnlDave
источник
3
Это неверно: он будет воспроизводить 3.9 как "thing": "3.9".
Символ
лучшие решения из всех, очень простые, спасибо, что вы сохранили мой день, для меня достаточно, чтобы сохранить число, в строке для десятичного числа в порядке
stackdave
@ Глиф по стандартам JSON (которых несколько ...), число без кавычек - это число с плавающей запятой двойной точности, а не десятичное число. Цитирование это единственный способ гарантировать совместимость.
std''OrgnlDave
2
у вас есть цитата для этого? Каждая спецификация, которую я прочитал, подразумевает, что она зависит от реализации.
Glyph
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")будет работать, и я хотел бы, чтобы это было
Книо
@Anurag Вы имели в виду repr (obj) вместо repr (o) в вашем примере.
orokusaki
Разве это не умрет, если вы попытаетесь закодировать что-то не десятичное?
mikemaccana
1
@nailer, нет, не будет, вы можете попробовать это, потому что исключение повышения по умолчанию
указывает на
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 могут испортить представление при конвертации в float, но это уже не так.

Чтобы вернуть десятичное число в 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Þó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)
Может Kavaklıoğlu
Для object_hook требуется возвращаемое значение по умолчанию, если obj не является десятичным.
Может Kavaklıoğlu
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-совместимый тип (например, float), который затем кодируется обычным способом. Проблема в 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}'
ECP
источник
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, но я надеялся на лучшее решение

Knio
источник
5
Вы можете создать подкласс JSONEncoder, как описано выше, редактирование установленных файлов Python установленной библиотеки или самого интерпретатора должно быть последним средством.
Justanr