Yüzenleri standart json modülü ile biçimlendirme


100

Bir kayan nokta listesini serileştirmek için python 2.6'daki standart json modülünü kullanıyorum . Ancak şöyle sonuçlar alıyorum:

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

Yüzen sayıların yalnızca iki ondalık basamakla biçimlendirilmesini istiyorum. Çıktı şöyle görünmelidir:

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

Kendi JSON Encoder sınıfımı tanımlamayı denedim:

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

Bu, tek bir kayan nesne için çalışır:

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

Ancak iç içe geçmiş nesnelerde başarısız olur:

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

Harici bağımlılık istemiyorum, bu yüzden standart json modülüne bağlı kalmayı tercih ediyorum.

Bunu nasıl başarabilirim?

Yanıtlar:


80

Not: Bu mu değil Python herhangi yeni sürümünde çalışmaz.

Ne yazık ki, bunu maymun yamalı yaparak yapmanız gerektiğine inanıyorum (ki bu benim düşünceme göre, standart kitaplık jsonpaketindeki bir tasarım hatasını gösterir ). Örneğin, bu kod:

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

yayar:

23.67
[23.67, 23.97, 23.87]

arzu ettiğiniz gibi. Açıktır ki, FLOAT_REPRbir şamandıranın HER gösteriminin kontrolünüz altında olmasını istiyorsanız geçersiz kılmanın mimari bir yolu olmalıdır; ama maalesef jsonpaket böyle tasarlanmadı :-(.


10
Bu çözüm, JSON kodlayıcının Python'un C sürümünü kullanan Python 2.7'de çalışmaz.
Nelson

25
Ancak bunu yaparsanız,% .3f yerine% .15g veya% .12g gibi bir şey kullanın.
Guido van Rossum

23
Bu parçacığı genç bir programcının kodunda buldum. Bu, yakalanmasaydı çok ciddi ama ince bir hata yaratırdı. Lütfen bu koda, bu maymun yamasının küresel sonuçlarını açıklayan bir uyarı koyabilir misiniz?
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
İşiniz

6
Diğerlerinin de belirttiği gibi, bu artık en azından Python 3.6+ sürümünde çalışmıyor. Buna 23.67nasıl .2fsaygı gösterilmediğini görmek için birkaç rakam ekleyin .
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])))

yayar

[23.67, 23.97, 23.87]

Maymun taraması gerekmez.


2
Bu çözümü beğendim; daha iyi entegrasyon ve 2.7. Verileri yine de kendim oluşturduğum için, pretty_floatsişlevi ortadan kaldırdım ve basitçe diğer koduma entegre ettim.
mikepurvis

1
Python3'te "Harita nesnesi JSON serileştirilebilir değil" hatası veriyor, ancak haritayı () bir listeye dönüştürmeyilist( map(pretty_floats, obj) )
Guglie

1
@Guglie: Bunun nedeni Python 3'te mapbir değil, yineleyici dönüyorlist
Azat Ibrakov

4
Benim için çalışmıyor (Python 3.5.2, simplejson 3.16.0). % .6g ve [23.671234556, 23.971234556, 23.871234556] ile denedim, yine de tam sayıyı yazdırıyor.
szali

27

Python 2.7 kullanıyorsanız, basit bir çözüm, kayan sayılarınızı açıkça istenen hassasiyete yuvarlamaktır.

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

Bu işe yarar çünkü Python 2.7, kayan yuvarlamayı daha tutarlı hale getirir . Maalesef bu Python 2.6'da çalışmıyor:

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

Yukarıda bahsedilen çözümler 2.6 için geçici çözümlerdir, ancak hiçbiri tamamen yeterli değildir. Maymun yama json.encoder.FLOAT_REPR, Python çalışma zamanınız JSON modülünün bir C sürümünü kullanıyorsa çalışmaz. Tom Wuttke'nin cevabındaki PrettyFloat sınıfı işe yarıyor, ancak yalnızca% g kodlaması uygulamanız için global olarak çalışıyorsa. % .15g biraz sihirdir, çünkü float duyarlılığı 17 anlamlı basamaktır ve% g sondaki sıfırları basmaz.

Her numara için hassasiyetin özelleştirilmesine izin veren bir PrettyFloat yapmaya çalışmak için biraz zaman harcadım. Yani, bir sözdizimi

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

Bunu doğru yapmak kolay değil. Float'tan miras almak garip. Nesneden devralma ve kendi default () yöntemiyle bir JSONEncoder alt sınıfını kullanma, json modülünün tüm özel türlerin dizge olarak serileştirilmesi gerektiğini varsayması dışında çalışmalıdır. Yani: çıktıda 0.33 değil Javascript dizesi "0.33" ile sonuçlanırsınız. Bunu çalıştırmanın bir yolu olabilir, ama göründüğünden daha zor.


JSONEncoder.iterencode ve desen eşleştirmeyi kullanan Python 2.6 için başka bir yaklaşım github.com/migurski/LilJSON/blob/master/liljson.py adresinde görülebilir
Nelson

Umarım bu, şamandıralarınızın etrafından dolaşmayı daha hafif hale getirir - emebilecek JSON sınıflarıyla uğraşmaktan nasıl kaçınabileceğimizi seviyorum.
Lincoln B

20

Yüzmek dumpsiçin hiçbir şey yapmanıza izin vermeyen gerçekten talihsiz bir durum . Ancak loadsöyle. Dolayısıyla, fazladan CPU yüküne aldırış etmezseniz, bunu kodlayıcı / kod çözücü / kodlayıcı aracılığıyla atabilir ve doğru sonucu alabilirsiniz:

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

Teşekkürler, bu gerçekten yararlı bir öneri. parse_floatKwarg'ı bilmiyordum !
Anonim

3.6'da da çalışan buradaki en basit öneri.
Brent Faust

"Ekstra CPU yüküne aldırış etmeyin" ifadesine dikkat edin. Serileştirecek çok fazla veriniz varsa kesinlikle bu çözümü kullanmayın. Benim için tek başına bunu eklemek, önemsiz olmayan bir hesaplama yapan bir programın 3 kat daha uzun sürmesini sağladı.
shaneb

11

İşte Python 3'te benim için çalışan ve maymun yaması gerektirmeyen bir çözüm:

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

Çıktı:

[23.63, 23.93, 23.84]

Verileri, ancak yuvarlatılmış kayan sayılarla kopyalar.


9

Python 2.5 veya önceki sürümlerle takıldıysanız: Monkey-patch hilesi, C speedup'ları kuruluysa orijinal simplejson modülüyle çalışmıyor gibi görünüyor:

$ 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

Yapmanız gereken şeyi yapabilirsiniz, ancak bu belgelenmemiştir:

>>> 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
Güzel görünüyor, ancak Python 3.6'da çalışmıyor gibi görünüyor. Özellikle modülde bir FLOAT_REPRsabit görmedim json.encoder.
Tomasz Gandor

2

Alex Martelli'nin çözümü tek iş parçacıklı uygulamalar için çalışacak, ancak iş parçacığı başına ondalık basamak sayısını kontrol etmesi gereken çok iş parçacıklı uygulamalar için çalışmayabilir. İşte çok iş parçacıklı uygulamalarda çalışması gereken bir çözüm:

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

Enkoder.thread_local.decimal_places yalnızca istediğiniz ondalık basamak sayısına ayarlayabilirsiniz ve bu iş parçacığındaki json.dumps () 'a yapılan sonraki çağrı bu ondalık basamak sayısını kullanacaktır.


2

Bunu python 2.7'de global json.encoder.FLOAT_REPR'yi geçersiz kılmadan yapmanız gerekiyorsa, işte bir yol.

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)

Ardından, python 2.7'de:

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

Python 2.6'da, Matthew Schinckel'in aşağıda işaret ettiği gibi tam olarak çalışmıyor:

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

4
Bunlar sayılara değil dizelere benziyor.
Matthew Schinckel

1

Artıları:

  • Herhangi bir JSON kodlayıcıyla veya hatta python'un repr'iyle çalışır.
  • Kısa (ish), işe yarıyor gibi görünüyor.

Eksileri:

  • Çirkin regexp hack, zar zor test edildi.
  • İkinci dereceden karmaşıklık.

    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

Standart json modülünü içe aktarırken, varsayılan FLOAT_REPR kodlayıcıyı değiştirmek yeterlidir. Kodlayıcı örneklerini içe aktarmaya veya oluşturmaya gerçekten gerek yoktur.

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

Bazen python'un str ile tahmin edebileceği en iyi temsili json olarak çıktı almak da çok kullanışlıdır. Bu, önemli rakamların göz ardı edilmemesini sağlayacaktır.

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'a, float'tan miras almanın garip olduğu konusunda hemfikirim, ancak belki de yalnızca __repr__işleve dokunan bir çözüm affedilebilir olabilir. decimalGerektiğinde yüzenleri yeniden biçimlendirmek için bunun için paketi kullanmaya son verdim . Bunun tersi, bunun repr()çağrıldığı tüm bağlamlarda çalışmasıdır , yani örneğin listeleri stdout'a yazdırırken de. Ayrıca, veriler oluşturulduktan sonra hassasiyet çalışma zamanında yapılandırılabilir. Elbette dezavantajı, verilerinizin bu özel float sınıfına dönüştürülmesi gerektiğidir (maalesef yamayı taklit edemezsiniz float.__repr__). Bunun için kısa bir dönüştürme işlevi sağlıyorum.

Kod:

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

Kullanım örneği:

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

Bu, __repr __ () kullanmayan yerleşik Python3 json paketi ile çalışmaz.
Ian Goldby

0

Numpy kullanma

Eğer gerçekten uzun kaymalarınız varsa, bunları numpy ile doğru şekilde yukarı / aşağı yuvarlayabilirsiniz:

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

Bu sorunu gidermek için küçük bir Python kitaplığı olan fjson'u piyasaya sürdüm . İle yükle

pip install fjson

ve parametrenin jsoneklenmesi gibi kullanın 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
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.