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


242

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

これをシリアル化する良い方法はありますか?JSONDecoderはDecimalオブジェクトを受け入れず、事前に浮動小数点数に変換するとエラーが{'x': 3.8999999999999999}発生し、帯域幅の大きな浪費になります。



3.8999999999999999は3.4と同じです。0.2は正確な浮動小数点表現を持ちません。
Jasen 2015年

@Jasen 3.89999999999は3.4よりも12.8%間違っています。JSON標準は、シリアル化と表記のみに関するものであり、実装に関するものではありません。IEEE754の使用は未加工のJSON仕様の一部ではなく、実装する最も一般的な方法にすぎません。正確な10進算術のみを使用する実装は、完全に(実際には、さらに厳密に)準拠しています。
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)

痛い、私はそれが実際にはこのように機能しないことに気づきました。それに応じて編集します。(ただし、考え方は
変わり

問題は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つしかないリストですが、なぜループするのですか?
mpen 2011

2
@Mark:return (str(o),)長さ1のタプルを返しますが、回答のコードは長さ1のジェネレータを返します。iterencode()のドキュメントを
Abgan

30
この実装は機能しなくなりました。エリアス・ザマリアのものは同じスタイルに取り組んでいるものです。
ピロ2014年

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
うーん、私にとってこれはDecimalオブジェクトを浮動小数点数に変換しますが、これは受け入れられません。たとえば、通貨で作業するときに精度が失われます。
Matthew Schinckel

12
@MatthewSchinckelそうではないと思います。それは実際にそれから文字列を作ります。そして、結果の文字列をフィードバックするjson.loads(s, use_decimal=True)と、小数点が返されます。プロセス全体でフロートはありません。上記の回答を編集しました。オリジナルのポスターで問題ないことを願っています。
Shekhar

1
ああ、私use_decimal=Trueも荷物を使っていなかったと思います。
Matthew Schinckel

1
私のためにjson.dumps({'a' : Decimal('3.9')}, use_decimal=True)与え'{"a": 3.9}'ます。目標はありませんでした'{"a": "3.9"}'か?
MrJ、2014年

5
simplejson.dumps(decimal.Decimal('2.2'))また、動作します:明示的ではありませんuse_decimal(simplejson / 3.6.0でテスト済み)。それを再度ロードする別の方法はjson.loads(s, parse_float=Decimal)、次のとおりです。つまり、stdlibを使用して読み取ることができますjson(古いsimplejsonバージョンもサポートされています)。
jfs 2014

181

Python 2.6.5を実行しているWebサーバーでMichałMarczykの回答を試したところ、問題なく動作したことを皆さんに知らせたいと思います。しかし、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で)私たちの場合、この答えは天の恵みです。
Joel Cross

17
unicodeまたはのstr代わりにfloatを使用して、精度を確保します。
SeppoErviälä16年

2
54.3999の問題は、フロートからストリングへの変換が定期的に機能しないPython 2.6.x以前では重要でしたが、Decimalからstrへの変換は"54.4"、二重引用符で囲まれたストリングとしてシリアル化されるため、はるかに不正確です。数。
hynekcer 2017年

1
python3で動作
SeanFromIT

43

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

「pip install simplejson」を実行しました。

import simplejson as json

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


1
最も簡単な修正
SMDC 2018年

1
奇妙なことに、インポートする必要すらありませんsimplejson。インストールするだけで十分です。この回答で最初に言及されました
bsplosion

これは私にはうまくDecimal('0.00') is not JSON serializable いきません、そしてそれをpip経由でインストールした後もそれを得ました。この状況は、マシュマロとグラフェンの両方を使用している場合です。クエリがREST APIで呼び出されると、マシュマロは10進数フィールドで正常に機能します。ただし、graphqlで呼び出されると、is not JSON serializableエラーが発生しました。
ロエル

素晴らしい、素晴らしい
スパイダーマン

パーフェクト!これは、他の人が簡単に変更できないモジュールを使用している状況で機能します(私の場合は、Googleスプレッドシートを使用するために
gspread

32

GAE 2.7でsimplejsonから組み込みjsonに切り替えてみましたが、小数点に問題がありました。defaultがstr(o)を返した場合、引用符があり(_iterencodeがdefaultの結果に対して_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]'

いいね!これにより、Pythonが最初に最も近い浮動小数点値に丸めることなく、10進数値がJSONでJavascript浮動小数点として確実に終了します。
konrad 2013

3
残念ながら、これは最近のPython 3では機能しません。現在、すべてのfloatサブクラスをfloatと見なし、それらに対してreprをまったく呼び出さないいくつかの高速パスコードがあります。
Antti Haapala 2017年

@AnttiHaapala、この例はPython 3.6で正常に動作します。
クリスチャンCiupitu 2017

@CristianCiupitu確かに、私は今、悪い振る舞いを再現することができないようです
Antti Haapala

2
解決策はv3.5.2rc1以来、参照動作を停止github.com/python/cpython/commit/...を。ありfloat.__repr__(つまり、精度を失っ)ハードコード、およびfakefloat.__repr__すべてで呼び出されません。上記のソリューションは、fakefloatに追加のメソッドがある場合、3.5.1までのpython3で正しく機能しますdef __float__(self): return self
myroslav 2017年

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)

プレスト!


これは素晴らしいことですが、OPはDjangoについて質問しませんでしたか?
std''OrgnlDave 2018

4
@ std''100%正解です、OrgnlDave。私がここにたどり着く方法を忘れてしまったが、検索用語に「django」を付けてこの質問をググってみたところ、これが思いついた。もう少しググった後で、答えを見つけて、私のような次の人のためにつまずいた。 it
Javier Buzzi

6
あなたは私の日を救う
gaozhidf

14

私の$ .02!

Webサーバーの大量のデータをシリアル化しているので、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"ます。
グリフ2017

すべての最善の解決策、非常にシンプル、おかげで私の日を節約できました。数字を保存するのに十分です
。10

JSON規格による@Glyph(そのうちのいくつかは...)、引用符で囲まれていない数値は、10進数ではなく倍精度浮動小数点です。引用することは互換性を保証する唯一の方法です。
std''OrgnlDave

2
これについての引用はありますか?私が読んだすべての仕様は、実装に依存していることを意味しています。
グリフ

12

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

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

したがって、フロートが必要ない場合は、オプションを文字列として送信する必要があり、10進数オブジェクトを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(o)ではなくrepr(obj)を意味していました。
orokusaki

10進数でないものをエンコードしようとすると、これはただ死ぬのではないですか?
mikemaccana

1
@nailer、そうではありません、あなたはそれを試すことができます、理由はデフォルトで例外を発生させ、次のハンドラーを使用する必要があることを
知らせる

1
mikez302の回答を参照してください-Python 2.7以降では、これは適用されなくなりました。
Joel Cross

9

Djangoユーザーの場合

最近TypeError: Decimal('2337.00') is not JSON serializable 、JSONエンコーディングIE中に遭遇しました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)

8

json.orgでリンクされているJSON標準ドキュメントから:

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

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

カスタム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のドキュメントに示唆されています

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


2
これは、への変換はPython 3で「固定」されていないfloat 必ずしもあなたが小数表現を失う、となります矛盾につながります。Decimal使用することが重要な場合は、文字列を使用する方が良いと思います。
juanpa.arrivillaga

私はこれをpython 3.1以降で安全だと信じています。精度の低下は算術演算で害を及ぼす可能性がありますが、JSONエンコードの場合、値の文字列表示を生成するだけなので、精度はほとんどのユースケースで十分です。JSONのすべてはすでに文字列であるため、値を引用符で囲むと、JSON仕様に反するだけです。
ヒューゴモタ

そうは言っても、floatへの変換に関する懸念を理解しています。必要な表示文字列を生成するために、エンコーダーと一緒に使用する別の戦略がおそらくあります。それでも、引用値を作成する価値はないと思います。
ヒューゴモタ

@HugoMota「JSONのすべてはすでに文字列であるため、値を引用符で囲むと、JSON仕様に反するだけです。」いいえ:rfc-editor.org/rfc/rfc8259.txt-JSONはテキストベースのエンコード形式ですが、そのすべてが文字列として解釈されるわけではありません。仕様では、文字列とは別に、数値をエンコードする方法を定義しています。
GunnarÞórMagnússon19年

@GunnarÞórMagnússon "JSONはテキストベースのエンコード形式です"-それは私が "すべてが文字列である"と意味したものです。数値を事前に文字列に変換しても、それがJSONになると文字列になるため、魔法のように精度が維持されません。そして、仕様によると、数値には引用符がありません。読みながら正確さを保つのは読者の責任です(引用ではなく、私の考えです)。
Hugo Mota

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)
CanKavaklıoğlu

objが10進数でない場合、object_hookにはデフォルトの戻り値が必要です。
CanKavaklıoğlu、2016

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

すばらしい...ワンストップソリューションをありがとう(y)
ムハンマドバジル

これは本当にうまくいきます!ソリューションを共有していただきありがとうございます
3

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()メソッドJSONEncoder.default()では、json互換の型(floatなど)を返して通常の方法でエンコードするのとは異なり、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

requestsjsonキーワード引数を使用して)小数を含むディクショナリをライブラリに渡したい場合は、単にインストールする必要があります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 2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.