Format float dengan modul json standar

100

Saya menggunakan modul json standar di python 2.6 untuk membuat daftar pelampung. Namun, saya mendapatkan hasil seperti ini:

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

Saya ingin float diformat dengan hanya dua digit desimal. Outputnya akan terlihat seperti ini:

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

Saya telah mencoba menentukan kelas JSON Encoder saya sendiri:

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

Ini berfungsi untuk satu objek float:

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

Tetapi gagal untuk objek bersarang:

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

Saya tidak ingin memiliki dependensi eksternal, jadi saya lebih suka menggunakan modul json standar.

Bagaimana saya bisa mencapai ini?

Manuel Ceron
sumber

Jawaban:

80

Catatan: Ini tidak berfungsi di versi Python terbaru.

Sayangnya, saya yakin Anda harus melakukan ini dengan menambal monyet (yang, menurut saya, menunjukkan cacat desain pada jsonpaket pustaka standar ). Misal, kode ini:

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]))

memancarkan:

23.67
[23.67, 23.97, 23.87]

seperti yang Anda inginkan. Jelas, harus ada cara yang dirancang untuk menimpa FLOAT_REPRsehingga SETIAP representasi float berada di bawah kendali Anda jika Anda menginginkannya; tapi sayangnya bukan itu cara jsonpaket itu dirancang :-(.

Alex Martelli
sumber
10
Solusi ini tidak berfungsi pada Python 2.7 yang menggunakan encoder JSON versi C Python.
Nelson
25
Bagaimanapun Anda melakukan ini, gunakan sesuatu seperti% .15g atau% .12g sebagai ganti% .3f.
Guido van Rossum
23
Saya menemukan potongan ini dalam kode programmer junior. Ini akan menciptakan bug yang sangat serius tetapi tidak kentara jika tidak tertangkap. Bisakah Anda memberi peringatan pada kode ini yang menjelaskan implikasi global dari penambalan monyet ini.
Rory Hart
12
Kebersihan yang baik untuk mengaturnya kembali setelah Anda selesai: 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
Jeff Kaufman
6
Seperti yang ditunjukkan orang lain, ini tidak lagi berfungsi setidaknya pada Python 3.6+. Tambahkan beberapa digit 23.67untuk melihat bagaimana .2ftidak dihormati.
Nico Schlömer
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])))

memancarkan

[23.67, 23.97, 23.87]

Tidak perlu monkeypatching.

Tom Wuttke
sumber
2
Saya suka solusi ini; integrasi yang lebih baik, dan bekerja dengan 2.7. Karena saya membangun datanya sendiri, saya menghilangkan pretty_floatsfungsinya dan hanya mengintegrasikannya ke kode saya yang lain.
mikepurvis
1
Di Python3, ini memberikan kesalahan "Objek peta bukan serializable JSON" , tetapi Anda dapat menyelesaikan pengubahan peta () ke daftar denganlist( map(pretty_floats, obj) )
Guglie
1
@Guglie: itu karena dalam Python 3 mapmengembalikan iterator, bukanlist
Azat Ibrakov
4
Tidak berfungsi untuk saya (Python 3.5.2, simplejson 3.16.0). Mencobanya dengan% .6g dan [23.671234556, 23.971234556, 23.871234556], masih mencetak bilangan bulat.
szali
27

Jika Anda menggunakan Python 2.7, solusi sederhana adalah dengan membulatkan float Anda secara eksplisit ke presisi yang diinginkan.

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

Ini berfungsi karena Python 2.7 membuat pembulatan float lebih konsisten . Sayangnya ini tidak berfungsi di 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'

Solusi yang disebutkan di atas adalah solusi untuk 2.6, tetapi tidak ada yang sepenuhnya memadai. Penambalan monyet json.encoder.FLOAT_REPR tidak berfungsi jika waktu proses Python Anda menggunakan versi C dari modul JSON. Kelas PrettyFloat dalam jawaban Tom Wuttke berfungsi, tetapi hanya jika pengkodean% g berfungsi secara global untuk aplikasi Anda. % .15g agak ajaib, ini berfungsi karena presisi float adalah 17 digit signifikan dan% g tidak mencetak nol di belakangnya.

Saya menghabiskan beberapa waktu mencoba membuat PrettyFloat yang memungkinkan penyesuaian presisi untuk setiap angka. Yaitu, sintaks seperti

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

Tidak mudah untuk melakukannya dengan benar. Mewarisi dari float itu canggung. Mewarisi dari Object dan menggunakan subclass JSONEncoder dengan metode default () -nya sendiri seharusnya berfungsi, kecuali modul json tampaknya menganggap semua jenis kustom harus diserialkan sebagai string. Yaitu: Anda berakhir dengan string Javascript "0,33" pada output, bukan angka 0,33. Mungkin masih ada cara untuk membuat ini berhasil, tetapi ini lebih sulit daripada kelihatannya.

Nelson
sumber
Pendekatan lain untuk Python 2.6 menggunakan JSONEncoder.iterencode dan pencocokan pola dapat dilihat di github.com/migurski/LilJSON/blob/master/liljson.py
Nelson
Mudah-mudahan ini membuat pengoperan float Anda lebih ringan - saya suka bagaimana kita dapat menghindari mengotak-atik kelas JSON yang bisa menyebalkan.
Lincoln B
20

Sungguh disayangkan bahwa dumpstidak memungkinkan Anda melakukan apa pun untuk mengapung. Namun loadsdemikian. Jadi, jika Anda tidak keberatan dengan beban CPU tambahan, Anda dapat membuangnya melalui encoder / decoder / encoder dan mendapatkan hasil yang benar:

>>> json.dumps(json.loads(json.dumps([.333333333333, .432432]), parse_float=lambda x: round(float(x), 3)))
'[0.333, 0.432]'
Claude
sumber
Terima kasih, saran ini sangat membantu. Saya tidak tahu tentang parse_floatkwarg!
Anonim
Saran paling sederhana di sini yang juga berfungsi di 3.6.
Brent Faust
Perhatikan frasa "tidak keberatan dengan beban CPU ekstra". Pasti tidak menggunakan solusi ini jika Anda memiliki banyak data untuk diserialkan. Bagi saya, menambahkan ini saja membuat program yang melakukan kalkulasi non-sepele membutuhkan waktu 3X lebih lama.
shaneb
11

Berikut adalah solusi yang berhasil untuk saya dengan Python 3 dan tidak memerlukan tambalan monyet:

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]))

Outputnya adalah:

[23.63, 23.93, 23.84]

Ini menyalin data tetapi dengan float bulat.

jcoffland.dll
sumber
9

Jika Anda terjebak dengan Python 2.5 atau versi sebelumnya: Trik monkey-patch tidak bekerja dengan modul simplejson asli jika speedup C diinstal:

$ 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]'
>>> 
Carlos Valiente
sumber
7

Anda dapat melakukan apa yang perlu Anda lakukan, tetapi itu tidak didokumentasikan:

>>> import json
>>> json.encoder.FLOAT_REPR = lambda f: ("%.2f" % f)
>>> json.dumps([23.67, 23.97, 23.87])
'[23.67, 23.97, 23.87]'
Ned Batchelder
sumber
5
Terlihat rapi, tetapi sepertinya tidak berfungsi pada Python 3.6. Secara khusus, saya tidak melihat FLOAT_REPRkonstanta dalam json.encodermodul.
Tomasz Gandor
2

Solusi Alex Martelli akan berfungsi untuk aplikasi utas tunggal, tetapi mungkin tidak berfungsi untuk aplikasi multi utas yang perlu mengontrol jumlah tempat desimal per utas. Berikut adalah solusi yang seharusnya berfungsi di aplikasi multi-utas:

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

Anda cukup menyetel encoder.thread_local.decimal_places ke jumlah tempat desimal yang Anda inginkan, dan panggilan berikutnya ke json.dumps () di utas itu akan menggunakan jumlah tempat desimal itu

Anton I. Sipos
sumber
2

Jika Anda perlu melakukan ini di python 2.7 tanpa menimpa json.encoder.FLOAT_REPR global, inilah salah satu caranya.

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)

Kemudian, di python 2.7:

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

Di python 2.6, itu tidak berfungsi seperti yang ditunjukkan oleh Matthew Schinckel di bawah ini:

>>> import MyEncoder
>>> enc = MyEncoder()  
>>> enc.encode([23.67, 23.97, 23.87])
'["23.67", "23.97", "23.87"]'
Mike Fogel
sumber
4
Itu terlihat seperti string, bukan angka.
Matthew Schinckel
1

Kelebihan:

  • Bekerja dengan encoder JSON, atau bahkan repr python.
  • Singkat (ish), sepertinya berhasil.

Kekurangan:

  • Peretasan regexp yang jelek, hampir tidak diuji.
  • Kompleksitas kuadrat.

    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
Sam Watkins
sumber
1

Saat mengimpor modul json standar, cukup mengubah encoder default FLOAT_REPR. Sebenarnya tidak perlu mengimpor atau membuat instance 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]'

Kadang-kadang juga sangat berguna untuk menampilkan json representasi terbaik yang dapat ditebak python dengan str. Ini akan memastikan angka-angka penting tidak diabaikan.

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]'
F Pereira
sumber
1

Saya setuju dengan @Nelson bahwa mewarisi dari float itu canggung, tetapi mungkin solusi yang hanya menyentuh __repr__fungsi mungkin bisa dimaafkan. Saya akhirnya menggunakan decimalpaket untuk ini untuk memformat ulang float saat diperlukan. Keuntungannya adalah bahwa ini berfungsi dalam semua konteks di mana repr()dipanggil, begitu juga ketika hanya mencetak daftar ke stdout misalnya. Selain itu, ketepatan dapat dikonfigurasi waktu proses, setelah data dibuat. Kelemahannya tentu saja bahwa data Anda perlu diubah ke kelas float khusus ini (sayangnya Anda tidak dapat menggunakan patch monyetfloat.__repr__ ). Untuk itu saya berikan fungsi konversi singkat.

Kode:

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

Contoh penggunaan:

>>> 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]]'
pengguna1556435
sumber
Ini tidak berfungsi dengan paket Python3 json built-in, yang tidak menggunakan __repr __ ().
Ian Goldby
0

Menggunakan numpy

Jika Anda benar-benar memiliki pelampung yang sangat panjang, Anda dapat membulatkannya ke atas / bawah dengan benar dengan 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]'

Mikhail
sumber
-1

Saya baru saja merilis fjson , pustaka Python kecil untuk memperbaiki masalah ini. Pasang dengan

pip install fjson

dan gunakan seperti itu json, dengan penambahan float_formatparameter:

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
}
Nico Schlömer
sumber