標準のjsonモジュールで浮動小数点をフォーマットする


100

フロートのリストをシリアル化するために、Python 2.6の標準のjsonモジュールを使用しています。しかし、私はこのような結果を得ています:

>>> import json
>>> json.dumps([23.67, 23.97, 23.87])
'[23.670000000000002, 23.969999999999999, 23.870000000000001]'

浮動小数点数を2桁の10進数のみでフォーマットする必要があります。出力は次のようになります。

>>> json.dumps([23.67, 23.97, 23.87])
'[23.67, 23.97, 23.87]'

私は自分のJSON Encoderクラスを定義しようとしました:

class MyEncoder(json.JSONEncoder):
    def encode(self, obj):
        if isinstance(obj, float):
            return format(obj, '.2f')
        return json.JSONEncoder.encode(self, obj)

これは、単一のフロートオブジェクトに対して機能します。

>>> json.dumps(23.67, cls=MyEncoder)
'23.67'

しかし、ネストされたオブジェクトでは失敗します:

>>> json.dumps([23.67, 23.97, 23.87])
'[23.670000000000002, 23.969999999999999, 23.870000000000001]'

私は外部依存関係を持ちたくないので、標準のjsonモジュールを使い続けることを好みます。

どうすればこれを達成できますか?

回答:


80

注:これは、最新バージョンのPython では機能しませ

残念ながら、私はあなたがサルパッチングによってこれを行う必要があると信じています(私の意見では、これは標準ライブラリjsonパッケージの設計上の欠陥を示しています)。たとえば、このコード:

import json
from json import encoder
encoder.FLOAT_REPR = lambda o: format(o, '.2f')
    
print(json.dumps(23.67))
print(json.dumps([23.67, 23.97, 23.87]))

放出する:

23.67
[23.67, 23.97, 23.87]

あなたが望むように。明らかに、FLOAT_REPRフロートのすべての表現が必要な場合に制御できるように、オーバーライドする設計された方法があるはずです。しかし残念ながら、それはjsonパッケージの設計方法ではありません:-(。


10
このソリューションは、PythonのCバージョンのJSONエンコーダーを使用するPython 2.7では機能しません。
ネルソン

25
ただし、これを行う場合は、%.3fの代わりに%.15gまたは%.12gなどを使用してください。
Guido van Rossum

23
私は、このスニペットをジュニアプログラマのコードで見つけました。これがキャッチされなかった場合、これは非常に深刻ですが微妙なバグを作成します。このサルのパッチの全体的な影響を説明する警告をこのコードに配置してください。
Rory Hart

12
それはあなたが完了したら、戻ってそれを設定するために、良好な衛生う: original_float_repr = encoder.FLOAT_REPR encoder.FLOAT_REPR = lambda o: format(o, '.2f') print json.dumps(1.0001) encoder.FLOAT_REPR = original_float_repr
ジェフカウフマン

6
他の人が指摘したように、これは少なくともPython 3.6以降では機能しません。に数桁を追加して、が尊重されていない23.67方法を確認します.2f
NicoSchlömer2018年

57
import simplejson
    
class PrettyFloat(float):
    def __repr__(self):
        return '%.15g' % self
    
def pretty_floats(obj):
    if isinstance(obj, float):
        return PrettyFloat(obj)
    elif isinstance(obj, dict):
        return dict((k, pretty_floats(v)) for k, v in obj.items())
    elif isinstance(obj, (list, tuple)):
        return list(map(pretty_floats, obj))
    return obj
    
print(simplejson.dumps(pretty_floats([23.67, 23.97, 23.87])))

発する

[23.67, 23.97, 23.87]

モンキーパッチは必要ありません。


2
私はこの解決策が好きです。より良い統合、および2.7で動作します。とにかく自分でデータを作成しているので、pretty_floats関数を削除し、他のコードに統合しました。
mikepurvis

1
Python3では、「MapオブジェクトはJSONシリアル化可能ではありません」エラーが発生しますが、map()をリストに変換することで解決できますlist( map(pretty_floats, obj) )
Guglie

1
@Guglie:それはPython 3ではmapイテレータではなくaを返すからですlist
Azat Ibrakov '30年

4
私には機能しません(Python 3.5.2、simplejson 3.16.0)。%.6gと[23.671234556、23.971234556、23.871234556]で試してみましたが、それでも整数が出力されます。
szali

27

Python 2.7を使用している場合、単純な解決策は、浮動小数点数を明示的に希望の精度に丸めることです。

>>> sys.version
'2.7.1 (r271:86832, Nov 27 2010, 18:30:46) [MSC v.1500 32 bit (Intel)]'
>>> json.dumps(1.0/3.0)
'0.3333333333333333'
>>> json.dumps(round(1.0/3.0, 2))
'0.33'

これは、Python 2.7が浮動小数点の丸めをより一貫したものにするために機能します。残念ながら、これはPython 2.6では機能しません。

>>> sys.version
'2.6.6 (r266:84292, Dec 27 2010, 00:02:40) \n[GCC 4.4.5]'
>>> json.dumps(round(1.0/3.0, 2))
'0.33000000000000002'

上記の解決策は2.6の回避策ですが、完全に適切なものはありません。PythonランタイムがCバージョンのJSONモジュールを使用している場合、サルのパッチjson.encoder.FLOAT_REPRは機能しません。Tom Wuttkeの回答のPrettyFloatクラスは機能しますが、%gエンコーディングがアプリケーションでグローバルに機能する場合のみです。%.15gは少し不思議です。浮動小数点の精度が17桁の有効数字であり、%gが末尾のゼロを出力しないため、これは機能します。

私は、各数値の精度をカスタマイズできるPrettyFloatを作成することに少し時間を費やしました。つまり、次のような構文

>>> json.dumps(PrettyFloat(1.0 / 3.0, 4))
'0.3333'

これを正しく行うのは簡単ではありません。floatから継承するのは厄介です。Objectから継承し、独自のdefault()メソッドでJSONEncoderサブクラスを使用することは、jsonモジュールがすべてのカスタムタイプを文字列としてシリアル化する必要があると想定していることを除いて、機能するはずです。つまり、0.33ではなく、JavaScript文字列「0.33」が出力に含まれます。これを機能させる方法はまだあるかもしれませんが、見た目より難しいです。


JSONEncoder.iterencodeとパターンマッチングを使用したPython 2.6の別のアプローチは、github.com / migurski / LilJSON / blob / master / liljson.py
Nelson

うまくいけば、これによりフロートの周りのパスがより軽量になります。JSONクラスをいじることを回避できる方法が気に入っています。
リンカーンB

19

本当に残念なことに、dumpsフロートに対して何かをすることはできません。しかしloadsそうです。したがって、追加のCPU負荷を気にしない場合は、エンコーダー/デコーダー/エンコーダーを介してそれをスローし、正しい結果を得ることができます。

>>> json.dumps(json.loads(json.dumps([.333333333333, .432432]), parse_float=lambda x: round(float(x), 3)))
'[0.333, 0.432]'

ありがとう、これは本当に役立つ提案です。parse_floatクワーグについて知らなかった!
2016

3.6でも機能する、ここでの最も単純な提案。
ブレントファウスト

「余分なCPU負荷を気にしないでください」というフレーズに注意してください。シリアル化するデータが多い場合は、このソリューションを使用しないでください。私にとっては、これを追加するだけで、自明ではない計算を行うプログラムの実行時間が3倍長くなりました。
shaneb

10

Python 3で私のために機能し、サルのパッチを必要としないソリューションは次のとおりです。

import json

def round_floats(o):
    if isinstance(o, float): return round(o, 2)
    if isinstance(o, dict): return {k: round_floats(v) for k, v in o.items()}
    if isinstance(o, (list, tuple)): return [round_floats(x) for x in o]
    return o


json.dumps(round_floats([23.63437, 23.93437, 23.842347]))

出力は次のとおりです。

[23.63, 23.93, 23.84]

データをコピーしますが、丸めの浮動小数点数を使用します。


9

Python 2.5またはそれ以前のバージョンで立ち往生している場合:Cスピードアップがインストールされている場合、monkey-patchトリックは元のsimplejsonモジュールでは動作しないようです。

$ python
Python 2.5.4 (r254:67916, Jan 20 2009, 11:06:13) 
[GCC 4.2.1 (SUSE Linux)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import simplejson
>>> simplejson.__version__
'2.0.9'
>>> simplejson._speedups
<module 'simplejson._speedups' from '/home/carlos/.python-eggs/simplejson-2.0.9-py2.5-linux-i686.egg-tmp/simplejson/_speedups.so'>
>>> simplejson.encoder.FLOAT_REPR = lambda f: ("%.2f" % f)
>>> simplejson.dumps([23.67, 23.97, 23.87])
'[23.670000000000002, 23.969999999999999, 23.870000000000001]'
>>> simplejson.encoder.c_make_encoder = None
>>> simplejson.dumps([23.67, 23.97, 23.87])
'[23.67, 23.97, 23.87]'
>>> 

7

必要なことはできますが、文書化されていません。

>>> import json
>>> json.encoder.FLOAT_REPR = lambda f: ("%.2f" % f)
>>> json.dumps([23.67, 23.97, 23.87])
'[23.67, 23.97, 23.87]'

5
きれいに見えますが、Python 3.6では動作しないようです。特に、モジュールにFLOAT_REPR定数がありませんでしたjson.encoder
Tomasz Gandor

2

Alex Martelliのソリューションはシングルスレッドアプリでは機能しますが、スレッドごとの小数点以下の桁数を制御する必要があるマルチスレッドアプリでは機能しない場合があります。マルチスレッドアプリで機能するソリューションは次のとおりです。

import threading
from json import encoder

def FLOAT_REPR(f):
    """
    Serialize a float to a string, with a given number of digits
    """
    decimal_places = getattr(encoder.thread_local, 'decimal_places', 0)
    format_str = '%%.%df' % decimal_places
    return format_str % f

encoder.thread_local = threading.local()
encoder.FLOAT_REPR = FLOAT_REPR     

#As an example, call like this:
import json

encoder.thread_local.decimal_places = 1
json.dumps([1.56, 1.54]) #Should result in '[1.6, 1.5]'

必要なのは、encoder.thread_local.decimal_placesを小数点以下の桁数に設定するだけで、次のスレッドでjson.dumps()を呼び出すと、小数点以下の桁数が使用されます。


2

グローバルjson.encoder.FLOAT_REPRをオーバーライドせずにPython 2.7でこれを行う必要がある場合は、1つの方法があります。

import json
import math

class MyEncoder(json.JSONEncoder):
    "JSON encoder that renders floats to two decimal places"

    FLOAT_FRMT = '{0:.2f}'

    def floatstr(self, obj):
        return self.FLOAT_FRMT.format(obj)

    def _iterencode(self, obj, markers=None):
        # stl JSON lame override #1
        new_obj = obj
        if isinstance(obj, float):
            if not math.isnan(obj) and not math.isinf(obj):
                new_obj = self.floatstr(obj)
        return super(MyEncoder, self)._iterencode(new_obj, markers=markers)

    def _iterencode_dict(self, dct, markers=None):
        # stl JSON lame override #2
        new_dct = {}
        for key, value in dct.iteritems():
            if isinstance(key, float):
                if not math.isnan(key) and not math.isinf(key):
                    key = self.floatstr(key)
            new_dct[key] = value
        return super(MyEncoder, self)._iterencode_dict(new_dct, markers=markers)

次に、Python 2.7:

>>> from tmp import MyEncoder
>>> enc = MyEncoder()
>>> enc.encode([23.67, 23.98, 23.87])
'[23.67, 23.98, 23.87]'

Python 2.6では、Matthew Schinckelが以下のように指摘しているため、うまく機能しません。

>>> import MyEncoder
>>> enc = MyEncoder()  
>>> enc.encode([23.67, 23.97, 23.87])
'["23.67", "23.97", "23.87"]'

4
それらは数字ではなく文字列のように見えます。
マシュー・シンケル

1

長所:

  • 任意のJSONエンコーダー、またはpythonのreprでも動作します。
  • Short(ish)、動作するようです。

短所:

  • 醜い正規表現ハック、ほとんどテストされていません。
  • 二次の複雑さ。

    def fix_floats(json, decimals=2, quote='"'):
        pattern = r'^((?:(?:"(?:\\.|[^\\"])*?")|[^"])*?)(-?\d+\.\d{'+str(decimals)+'}\d+)'
        pattern = re.sub('"', quote, pattern) 
        fmt = "%%.%df" % decimals
        n = 1
        while n:
            json, n = re.subn(pattern, lambda m: m.group(1)+(fmt % float(m.group(2)).rstrip('0')), json)
        return json

1

標準のjsonモジュールをインポートする場合、デフォルトのエンコーダーFLOAT_REPRを変更するだけで十分です。Encoderインスタンスをインポートまたは作成する必要は実際にはありません。

import json
json.encoder.FLOAT_REPR = lambda o: format(o, '.2f')

json.dumps([23.67, 23.97, 23.87]) #returns  '[23.67, 23.97, 23.87]'

pythonがstrで推測できる最良の表現をjsonとして出力することも非常に便利です。これにより、有効数字が無視されなくなります。

import json
json.dumps([23.67, 23.9779, 23.87489])
# output is'[23.670000000000002, 23.977900000000002, 23.874890000000001]'

json.encoder.FLOAT_REPR = str
json.dumps([23.67, 23.9779, 23.87489])
# output is '[23.67, 23.9779, 23.87489]'

1

私はfloatからの継承は扱いにくいという@Nelsonに同意しますが、おそらく__repr__関数にのみ触れるソリューションは許されるかもしれません。結局decimal、このパッケージを使用して、必要に応じて浮動小数点数を再フォーマットしました。利点は、これrepr()がが呼び出されるすべてのコンテキストで機能することです。たとえば、単にリストをstdoutに出力する場合にも同様です。また、データが作成された後、精度は実行時に構成可能です。欠点は、もちろん、データをこの特別なfloatクラスに変換する必要があることです(残念ながら、モンキーパッチには思えないため)float.__repr__)。そのために、簡単な変換機能を提供します。

コード:

import decimal
C = decimal.getcontext()

class decimal_formatted_float(float):
   def __repr__(self):
       s = str(C.create_decimal_from_float(self))
       if '.' in s: s = s.rstrip('0')
       return s

def convert_to_dff(elem):
    try:
        return elem.__class__(map(convert_to_dff, elem))
    except:
        if isinstance(elem, float):
            return decimal_formatted_float(elem)
        else:
            return elem

使用例:

>>> import json
>>> li = [(1.2345,),(7.890123,4.567,890,890.)]
>>>
>>> decimal.getcontext().prec = 15
>>> dff_li = convert_to_dff(li)
>>> dff_li
[(1.2345,), (7.890123, 4.567, 890, 890)]
>>> json.dumps(dff_li)
'[[1.2345], [7.890123, 4.567, 890, 890]]'
>>>
>>> decimal.getcontext().prec = 3
>>> dff_li = convert_to_dff(li)
>>> dff_li
[(1.23,), (7.89, 4.57, 890, 890)]
>>> json.dumps(dff_li)
'[[1.23], [7.89, 4.57, 890, 890]]'

これは、__ repr __()を使用しない組み込みのPython3 jsonパッケージでは機能しません。
Ian Goldby

0

numpyの使用

あなたが実際に本当に長いフロートを持っているなら、あなたはそれらをnumpyで正しく丸める/丸めることができます:

import json 

import numpy as np

data = np.array([23.671234, 23.97432, 23.870123])

json.dumps(np.around(data, decimals=2).tolist())

'[23.67, 23.97, 23.87]'


-1

この問題を修正するための小さなPythonライブラリであるfjsonをリリースしました。でインストール

pip install fjson

パラメータをjson追加して、のように使用しfloat_formatます。

import math
import fjson


data = {"a": 1, "b": math.pi}
print(fjson.dumps(data, float_format=".6e", indent=2))
{
  "a": 1,
  "b": 3.141593e+00
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.