รูปแบบลอยด้วยโมดูล json มาตรฐาน


103

ฉันใช้โมดูล jsonมาตรฐานใน python 2.6 เพื่อจัดลำดับรายการลอย อย่างไรก็ตามฉันได้รับผลลัพธ์เช่นนี้:

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

ฉันต้องการให้โฟลถูกสร้างด้วยทศนิยมสองหลักเท่านั้น ผลลัพธ์ควรมีลักษณะดังนี้:

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

ฉันได้ลองกำหนดคลาสตัวเข้ารหัส JSON ของตัวเองแล้ว:

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 2.7 โดยใช้ตัวเข้ารหัส JSON เวอร์ชัน C ของ Python
Nelson

25
อย่างไรก็ตามคุณทำเช่นนี้ให้ใช้% .15g หรือ% .12g แทน% .3f
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
Jeff Kaufman

6
ตามที่คนอื่น ๆ ชี้ให้เห็นสิ่งนี้ใช้ไม่ได้อีกต่อไปใน Python 3.6+ เป็นอย่างน้อย เพิ่มตัวเลขสองสามหลัก23.67เพื่อดูว่า.2fไม่เคารพอย่างไร
Nico Schlömer

58
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 object is not JSON serializable"แต่คุณสามารถแก้ไขการแปลง map () เป็นรายการด้วยlist( map(pretty_floats, obj) )
Guglie

1
@Guglie: นั่นเป็นเพราะใน Python 3 mapส่งคืนตัวทำซ้ำไม่ใช่ alist
Azat Ibrakov

5
ไม่ได้ผลสำหรับฉัน (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'

งานนี้เพราะงูหลาม 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 แต่ไม่มีวิธีใดที่เพียงพอ Monkey patching json.encoder.FLOAT_REPR ไม่ทำงานหากรันไทม์ Python ของคุณใช้โมดูล JSON เวอร์ชัน C คลาส PrettyFloat ในคำตอบของ Tom Wuttke ใช้ได้ แต่ถ้าการเข้ารหัส% g ใช้ได้ทั่วโลกสำหรับแอปพลิเคชันของคุณ % .15g เป็นเวทย์มนตร์เล็กน้อยมันใช้งานได้เนื่องจากความแม่นยำของการลอยเป็นเลขนัยสำคัญ 17 หลักและ% g ไม่พิมพ์ศูนย์ต่อท้าย

ฉันใช้เวลาพยายามสร้าง PrettyFloat ที่อนุญาตให้ปรับแต่งความแม่นยำสำหรับแต่ละหมายเลขได้ กล่าวคือไวยากรณ์เช่น

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

ไม่ใช่เรื่องง่ายที่จะได้รับสิทธินี้ การสืบทอดจากการลอยเป็นเรื่องที่น่าอึดอัดใจ การรับค่าจาก Object และการใช้คลาสย่อย JSONEncoder ที่มีเมธอด default () ของตัวเองควรใช้งานได้ยกเว้นโมดูล json จะถือว่าประเภทที่กำหนดเองทั้งหมดควรต่อเนื่องเป็นสตริง เช่น: คุณลงท้ายด้วยสตริง Javascript "0.33" ในเอาต์พุตไม่ใช่หมายเลข 0.33 อาจยังมีวิธีที่จะทำให้งานนี้ได้ แต่มันยากกว่าที่คิด


อีกวิธีหนึ่งสำหรับ Python 2.6 โดยใช้ JSONEncoder.iterencode และการจับคู่รูปแบบสามารถดูได้ที่github.com/migurski/LilJSON/blob/master/liljson.py
Nelson

หวังว่าสิ่งนี้จะทำให้การลอยตัวของคุณมีน้ำหนักเบามากขึ้น - ฉันชอบวิธีที่เราสามารถหลีกเลี่ยงการยุ่งกับคลาส JSON ที่สามารถดูดได้
Lincoln B

22

โชคร้ายจริงๆที่dumpsไม่ยอมให้ทำอะไรลอยๆ อย่างไรก็ตามloadsไม่ ดังนั้นหากคุณไม่คำนึงถึงภาระ CPU เพิ่มเติมคุณสามารถส่งผ่านตัวเข้ารหัส / ตัวถอดรหัส / ตัวเข้ารหัสและรับผลลัพธ์ที่ถูกต้อง:

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

ขอบคุณนี่เป็นคำแนะนำที่เป็นประโยชน์จริงๆ ฉันไม่รู้เกี่ยวกับparse_floatkwarg!
ไม่ระบุชื่อ

คำแนะนำที่ง่ายที่สุดที่นี่ยังใช้ได้กับ 3.6
Brent Faust

สังเกตวลี "ไม่ต้องกังวลกับการโหลด CPU เพิ่มเติม" อย่าใช้วิธีนี้อย่างแน่นอนหากคุณมีข้อมูลจำนวนมากที่จะทำให้เป็นอนุกรม สำหรับฉันการเพิ่มสิ่งนี้เพียงอย่างเดียวทำให้โปรแกรมที่ทำการคำนวณแบบไม่สำคัญใช้เวลานานขึ้น 3 เท่า
shaneb

11

นี่เป็นวิธีแก้ปัญหาที่ใช้ได้ผลสำหรับฉันใน 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 หรือเวอร์ชันก่อนหน้า: เคล็ดลับ Monkey-patch ดูเหมือนจะไม่ทำงานกับโมดูล simplejson ดั้งเดิมหากติดตั้ง C speedups:

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

6
ดูเรียบร้อย แต่ดูเหมือนจะใช้ไม่ได้กับ 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]'

คุณสามารถตั้งค่าตัวเข้ารหัสได้เท่านั้น


2

หากคุณต้องการทำสิ่งนี้ใน python 2.7 โดยไม่แทนที่ global json.encoder.FLOAT_REPR นี่เป็นวิธีเดียว

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
สิ่งเหล่านี้ดูเหมือนสตริงไม่ใช่ตัวเลข
Matthew Schinckel

1

ข้อดี:

  • ทำงานร่วมกับตัวเข้ารหัส JSON หรือแม้แต่ repr ของ python
  • สั้น (ish) ดูเหมือนจะใช้งานได้

จุดด้อย:

  • แฮ็ค regexp ที่น่าเกลียดแทบไม่มีการทดสอบ
  • ความซับซ้อนกำลังสอง

    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 ไม่จำเป็นต้องนำเข้าหรือสร้างอินสแตนซ์ตัวเข้ารหัส

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

บางครั้งก็มีประโยชน์มากในการส่งออกเนื่องจาก json python ที่เป็นตัวแทนที่ดีที่สุดสามารถเดาได้ด้วย str สิ่งนี้จะช่วยให้แน่ใจว่าไม่มีการละเว้นตัวเลขที่มีนัยสำคัญ

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

ฉันเห็นด้วยกับ @Nelson ว่าการสืบทอดจากการลอยเป็นเรื่องที่น่าอึดอัดใจ แต่บางทีวิธีแก้ปัญหาที่สัมผัสเพียง__repr__ฟังก์ชั่นอาจจะให้อภัยได้ ฉันลงเอยด้วยการใช้decimalแพ็คเกจนี้เพื่อฟอร์แมตโฟลตใหม่เมื่อจำเป็น ข้อดีคือใช้งานได้ในทุกบริบทที่repr()มีการเรียกใช้ดังนั้นเมื่อพิมพ์รายการไปยัง stdout เช่น นอกจากนี้ความแม่นยำยังสามารถกำหนดค่ารันไทม์ได้หลังจากสร้างข้อมูลแล้ว ข้อเสียคือแน่นอนว่าข้อมูลของคุณจะต้องถูกแปลงเป็นคลาสโฟลตพิเศษนี้ (น่าเสียดายที่คุณไม่สามารถดูเหมือนลิงแพทช์ได้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]]'

สิ่งนี้ใช้ไม่ได้กับแพ็คเกจ Python3 json ในตัวซึ่งไม่ใช้ __repr __ ()
Ian Gold โดย

0

ฉันเพิ่งเปิดตัว fjsonซึ่งเป็นไลบรารี Python ขนาดเล็กเพื่อแก้ไขปัญหานี้ ติดตั้งด้วย

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
}

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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.