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



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



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

653   14  

14 ответов:

как насчет подклассы 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)

Simplejson 2.1 и выше, имеет встроенную поддержку для десятичного типа:

>>> 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'

Надеюсь, эта функция будет включена в стандартную библиотеку.

Я хотел бы, чтобы все знали, что я попробовал ответ Михаила Марчика на моем веб-сервере, на котором был запущен Python 2.6.5, и он работал нормально. Однако я обновился до Python 2.7, и он перестал работать. Я попытался придумать какой-то способ кодирования десятичных объектов, и вот что я придумал:

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. Я проверил его, и он, кажется, работает нормально. Если кто-нибудь заметит какие-либо ошибки в моем решении или придумает с лучшим способом, пожалуйста, дайте мне знать.

Я попытался переключиться с simplejson на встроенный json для GAE 2.7 и имел проблемы с десятичным числом. Если возвращается значение по умолчанию стр(о) там были кавычки (потому что _iterencode звонки _iterencode по результатам умолчанию), и плавать(О) бы удалить трейлинг-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]'

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

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

import simplejson as json

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

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

так что если вы не хотите float, только вариант вы должны отправить его в виде строки, и чтобы разрешить автоматическое преобразование десятичных объектов в 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)

мои $.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

делает мою жизнь намного легче...

это то, что у меня есть, извлеченные из нашего класса

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)

родной вариант отсутствует, поэтому я добавлю его для следующего парня/желчи, который его ищет.

начиная с 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)

вуаля!

на основе 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()

С стандартный документ 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 документация по десятичным дробям:

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

если вы хотите передать словарь, содержащий дроби к 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 если он не установлен.

вы можете создать пользовательский кодер 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}'

Это можно сделать, добавив

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

на \Lib\json\encoder.py:JSONEncoder._iterencode, но я надеялся на лучшее решение

Comments

    Ничего не найдено.