web-dev-qa-db-ja.com

Python JSONはDecimalオブジェクトをシリアル化します

オブジェクトの一部としてDecimal('3.9')があり、これを{'x': 3.9}のように見えるJSON文字列にエンコードしたいです。クライアント側の精度は気にしないので、floatは問題ありません。

これをシリアル化する良い方法はありますか? JSONDecoderはDecimalオブジェクトを受け入れず、事前にfloatに変換すると{'x': 3.8999999999999999}が生成されますが、これは間違っており、帯域幅の大きな浪費になります。

189
Knio

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)
125
Michał Marczyk

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'

この機能が標準ライブラリに含まれることを願っています。

196
Lukas Cenovsky

Python 2.6.5を実行しているWebサーバーでMichałMarczykの回答を試してみて、うまくいったことをみんなに知らせたいです。ただし、Python 2.7にアップグレードしたため、動作しなくなりました。私は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で問題を抱えている人を助けることを願っています。私はそれをテストしましたが、うまくいくようです。誰かが私のソリューションのバグに気づいたり、より良い方法を思いついたら、私に知らせてください。

152
Elias Zamaria

私のFlaskアプリでは、python 2.7.11、flask alchemy(「db.decimal」タイプ)、およびFlaskを使用しますマシュマロ(「インスタント」シリアライザーおよびデシリアライザー用)、GETまたはPOSTを行うたびにこのエラーが発生しました。シリアライザーとデシリアライザーは、Decimal型をJSONで識別可能な形式に変換できませんでした。

「pip install simplejson」を実行した後、追加するだけで

import simplejson as json

シリアライザーとデシリアライザーが再びゴロゴロ鳴り始めます。私は他に何もしませんでした... DEciamlsは '234.00'フロート形式で表示されます。

28
ISONecroMAn

GAE 2.7でsimplejsonから組み込みjsonに切り替えてみましたが、小数に問題がありました。デフォルトが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]'
26
tesdal

ネイティブオプションが欠落しているため、それを探す次の男/ゴールのために追加します。

Django 1.7.x以降では、Django.core.serializers.jsonから取得できる組み込みのDjangoJSONEncoderがあります。

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)

プレスト!

14
Javier Buzzi

3.9はIEEE floatで正確に表すことができません。常に3.8999999999999999のようになります。 print repr(3.9)を試してください。詳細についてはこちらをご覧ください。

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

したがって、floatが必要ない場合は、オプションとして文字列として送信する必要があり、decimalオブジェクトから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)
11
Anurag Uniyal

私の$ .02!

Webサーバーの大量のデータをシリアル化するため、JSONエンコーダーを拡張します。これがいいコードです。それはあなたが好きなほぼすべてのデータフォーマットに簡単に拡張可能であり、"thing": 3.9として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

人生がずっと楽になります...

10
std''OrgnlDave

これは私がクラスから抽出したものです

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)
6
James Lin

json.org にリンクされている JSON Standard Document から:

JSONは、数値のセマンティクスにとらわれません。どのプログラミング言語でも、固定または浮動、2進数または10進数のさまざまな容量と補数のさまざまな数値タイプがあります。そのため、異なるプログラミング言語間の交換が困難になる場合があります。 JSONは代わりに、人間が使用する数字の表現、つまり数字のシーケンスのみを提供します。すべてのプログラミング言語は、内部表現に同意しない場合でも、数字列の意味を理解する方法を知っています。交換を許可するにはこれで十分です。

そのため、実際にはJSONで(文字列ではなく)数値としてDecimalを表すことは正確です。ベローは問題の可能な解決策です。

カスタム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))

この解決策は 10進数のPython 3.0ドキュメント で示唆されています:

FloatからDecimalを作成するには、まずそれを文字列に変換します。

5
Hugo Mota

stdOrgnlDave answerに基づいて、オプションの種類で呼び出すことができるようにこのラッパーを定義したため、エンコーダーはプロジェクト内の特定の種類でのみ動作します。 「暗黙的よりも明示的である」ため、この「デフォルト」エンコーダーを使用せずにコード内で作業を行う必要があると思いますが、これを使用すると時間を節約できると理解しています。 :-)

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 __== '__main__':
    JSONEncoder_newdefault()
2
Juanmi Taboada

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)

ただし、Decimal値は文字列になり、parse_floatjson.loadsオプションを使用して、データをデコードするときにdecimal/float値パーサーを明示的に設定できるようになりました。

import decimal 

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

要件に応じてカスタム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}'
1
sparrow

10進数を含む辞書を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にフォールバックするためです。

0
Max Malysh