Định dạng nổi với mô-đun json tiêu chuẩn


100

Tôi đang sử dụng mô-đun json tiêu chuẩn trong python 2.6 để tuần tự hóa danh sách các phao. Tuy nhiên, tôi nhận được kết quả như thế này:

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

Tôi muốn các phao được định dạng chỉ với hai chữ số thập phân. Đầu ra sẽ giống như sau:

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

Tôi đã thử xác định lớp Bộ mã hóa JSON của riêng mình:

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

Điều này hoạt động cho một đối tượng float duy nhất:

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

Nhưng không thành công đối với các đối tượng lồng nhau:

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

Tôi không muốn có các phụ thuộc bên ngoài, vì vậy tôi thích gắn bó với mô-đun json tiêu chuẩn.

Làm thế nào tôi có thể đạt được điều này?

Câu trả lời:


80

Lưu ý: Điều này không hoạt động trong bất kỳ phiên bản Python nào gần đây.

Thật không may, tôi tin rằng bạn phải làm điều này bằng cách vá lỗi (theo ý kiến ​​của tôi, chỉ ra một lỗi thiết kế trong jsongói thư viện tiêu chuẩn ). Ví dụ: mã này:

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

phát ra:

23.67
[23.67, 23.97, 23.87]

như bạn muốn. Rõ ràng, cần phải có một cách có cấu trúc để ghi đè FLOAT_REPRđể MỌI biểu diễn của float đều nằm trong tầm kiểm soát của bạn nếu bạn muốn; nhưng rất tiếc đó không phải là cách jsongói được thiết kế :-(.


10
Giải pháp này không hoạt động trong Python 2.7 sử dụng phiên bản C của bộ mã hóa JSON của Python.
Nelson

25
Tuy nhiên, bạn làm điều này, hãy sử dụng một cái gì đó như% .15g hoặc% .12g thay vì% .3f.
Guido van Rossum

23
Tôi tìm thấy đoạn mã này trong mã của một lập trình viên cơ sở. Điều này sẽ tạo ra một lỗi rất nghiêm trọng nhưng tinh vi nếu nó không bị bắt. Bạn có thể vui lòng đặt một cảnh báo trên mã này giải thích những tác động toàn cầu của việc vá lỗi khỉ này không.
Rory Hart

12
Đó là vệ sinh tốt để đặt nó trở lại khi bạn đã hoàn tất: 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
Như những người khác đã chỉ ra, điều này không còn hoạt động trong ít nhất là Python 3.6+. Thêm một vài chữ số 23.67để xem cách .2fkhông được tôn trọng.
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])))

phát ra

[23.67, 23.97, 23.87]

Không cần khớp khỉ.


2
Tôi thích giải pháp này; tích hợp tốt hơn và hoạt động với 2.7. Bởi vì dù sao thì tôi cũng đang tự xây dựng dữ liệu nên tôi đã loại bỏ pretty_floatschức năng này và chỉ đơn giản là tích hợp nó vào mã khác của mình.
mikepurvis

1
Trong Python3 nó mang lại cho "Bản đồ đối tượng không phải là JSON serializable" lỗi, nhưng bạn có thể giải quyết chuyển đổi bản đồ () vào một danh sách vớilist( map(pretty_floats, obj) )
Guglie

1
@Guglie: đó là bởi vì trong Python 3 maplợi nhuận iterator, không phải là mộtlist
Azat Ibrakov

4
Không hoạt động với tôi (Python 3.5.2, simplejson 3.16.0). Đã thử với% .6g và [23.671234556, 23.971234556, 23.871234556], nó vẫn in toàn bộ số.
szali

27

Nếu bạn đang sử dụng Python 2.7, một giải pháp đơn giản là chỉ cần làm tròn các phao của bạn một cách rõ ràng đến độ chính xác mong muốn.

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

Điều này hoạt động vì Python 2.7 đã làm cho việc làm tròn float nhất quán hơn . Thật không may, điều này không hoạt động trong 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'

Các giải pháp được đề cập ở trên là giải pháp thay thế cho 2.6, nhưng không có giải pháp nào là hoàn toàn phù hợp. Bản vá khỉ json.encoder.FLOAT_REPR không hoạt động nếu thời gian chạy Python của bạn sử dụng phiên bản C của mô-đun JSON. Lớp PrettyFloat trong câu trả lời của Tom Wuttke hoạt động, nhưng chỉ khi mã hóa% g hoạt động trên toàn cầu cho ứng dụng của bạn. % .15g hơi kỳ diệu, nó hoạt động vì độ chính xác float là 17 chữ số có nghĩa và% g không in các số 0 ở cuối.

Tôi đã dành một khoảng thời gian để cố gắng tạo ra một PrettyFloat cho phép tùy chỉnh độ chính xác cho từng số. Tức là, một cú pháp như

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

Thật không dễ dàng để làm được điều này đúng. Kế thừa từ float thật khó xử. Kế thừa từ Object và sử dụng lớp con JSONEncoder với phương thức default () của riêng nó sẽ hoạt động, ngoại trừ mô-đun json dường như giả định tất cả các kiểu tùy chỉnh phải được tuần tự hóa dưới dạng chuỗi. Tức là: bạn kết thúc bằng chuỗi Javascript "0,33" trong đầu ra, không phải số 0,33. Có thể vẫn còn một cách để thực hiện điều này, nhưng nó khó hơn vẻ ngoài của nó.


Bạn có thể xem một cách tiếp cận khác cho Python 2.6 bằng cách sử dụng JSONEncoder.iterencode và khớp mẫu tại github.com/migurski/LilJSON/blob/master/liljson.py
Nelson

Hy vọng rằng điều này làm cho việc truyền xung quanh phao của bạn nhẹ hơn - tôi thích cách chúng ta có thể tránh làm rối tung các lớp JSON có thể tệ hơn.
Lincoln B

19

Thật không may điều dumpsđó không cho phép bạn làm bất cứ điều gì để nổi. Tuy nhiên loadskhông. Vì vậy, nếu bạn không bận tâm đến việc tải thêm CPU, bạn có thể ném nó qua bộ mã hóa / bộ giải mã / bộ mã hóa và nhận được kết quả phù hợp:

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

Cảm ơn bạn, đây là gợi ý thực sự hữu ích. Tôi không biết về parse_floatkwarg!
Ẩn danh

Gợi ý đơn giản nhất ở đây cũng hoạt động trong 3.6.
Brent Faust

Lưu ý cụm từ "đừng bận tâm đến việc tải thêm CPU". Chắc chắn không sử dụng giải pháp này nếu bạn có nhiều dữ liệu để tuần tự hóa. Đối với tôi, chỉ thêm điều này đã khiến một chương trình thực hiện một phép tính không tầm thường mất thời gian lâu hơn 3 lần.
shaneb

10

Đây là một giải pháp phù hợp với tôi trong Python 3 và không yêu cầu vá lỗi khỉ:

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

Đầu ra là:

[23.63, 23.93, 23.84]

Nó sao chép dữ liệu nhưng với các phao tròn.


9

Nếu bạn bị mắc kẹt với Python 2.5 hoặc các phiên bản cũ hơn: Thủ thuật khỉ vá dường như không hoạt động với mô-đun simplejson ban đầu nếu cài đặt tăng tốc C:

$ 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

Bạn có thể làm những gì bạn cần làm, nhưng nó không được ghi lại:

>>> 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
Trông gọn gàng, nhưng dường như không hoạt động trên Python 3.6. Đặc biệt, tôi không thấy một FLOAT_REPRhằng số trong json.encodermô-đun.
Tomasz Gandor

2

Giải pháp của Alex Martelli sẽ hoạt động đối với các ứng dụng đơn luồng, nhưng có thể không hoạt động đối với các ứng dụng đa luồng cần kiểm soát số vị trí thập phân trên mỗi luồng. Đây là một giải pháp sẽ hoạt động trong các ứng dụng đa luồng:

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

Bạn chỉ có thể đặt encoder.thread_local.decimal_places thành số vị trí thập phân bạn muốn và lệnh gọi tiếp theo tới json.dumps () trong chuỗi đó sẽ sử dụng số vị trí thập phân đó


2

Nếu bạn cần thực hiện việc này trong python 2.7 mà không ghi đè json.encoder.FLOAT_REPR toàn cục, đây là một cách.

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)

Sau đó, trong python 2.7:

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

Trong python 2.6, nó không hoàn toàn hoạt động như Matthew Schinckel chỉ ra bên dưới:

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

4
Chúng trông giống như chuỗi, không phải số.
Matthew Schinckel

1

Ưu điểm:

  • Hoạt động với bất kỳ bộ mã hóa JSON nào hoặc thậm chí là repr của python.
  • Ngắn (ish), có vẻ hiệu quả.

Nhược điểm:

  • Hack regexp xấu xí, hầu như không được thử nghiệm.
  • Độ phức tạp bậc hai.

    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

Khi nhập mô-đun json tiêu chuẩn, chỉ cần thay đổi bộ mã hóa mặc định FLOAT_REPR là đủ. Không thực sự cần thiết phải nhập hoặc tạo các phiên bản Bộ mã hóa.

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

Đôi khi cũng rất hữu ích để xuất ra dưới dạng json mà python đại diện tốt nhất có thể đoán bằng str. Điều này sẽ đảm bảo các chữ số có nghĩa không bị bỏ qua.

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

Tôi đồng ý với @Nelson rằng kế thừa từ float là khó xử, nhưng có lẽ một giải pháp chỉ liên quan đến __repr__chức năng có thể được tha thứ. Tôi đã sử dụng decimalgói này để định dạng lại các phao khi cần thiết. Ưu điểm là điều này hoạt động trong tất cả các ngữ cảnh nơi repr()đang được gọi, vì vậy cũng có thể khi chỉ cần in danh sách ra stdout chẳng hạn. Ngoài ra, độ chính xác có thể định cấu hình thời gian chạy, sau khi dữ liệu đã được tạo. Tất nhiên, nhược điểm là dữ liệu của bạn cần được chuyển đổi sang lớp float đặc biệt này (rất tiếc là bạn dường như không thể vá lỗifloat.__repr__ ). Đối với điều đó, tôi cung cấp một chức năng chuyển đổi ngắn gọn.

Mật mã:

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

Ví dụ sử dụng:

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

Điều này không hoạt động với gói json Python3 được tích hợp sẵn, không sử dụng __repr __ ().
Ian Goldby

0

Sử dụng numpy

Nếu bạn thực sự có phao thực sự dài, bạn có thể làm tròn chúng lên / xuống một cách chính xác với 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

Tôi vừa phát hành fjson , một thư viện Python nhỏ để khắc phục sự cố này. Cài đặt với

pip install fjson

và sử dụng giống như json, với việc bổ sung float_formattham số:

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
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.