Format pływa ze standardowym modułem json

100

Używam standardowego modułu json w Pythonie 2.6 do serializacji listy pływaków. Jednak otrzymuję takie wyniki:

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

Chcę, aby liczby zmiennoprzecinkowe były sformatowane za pomocą tylko dwóch cyfr dziesiętnych. Wynik powinien wyglądać następująco:

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

Próbowałem zdefiniować własną klasę JSON Encoder:

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

Działa to dla jedynego obiektu pływającego:

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

Ale niepowodzenie w przypadku obiektów zagnieżdżonych:

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

Nie chcę mieć zewnętrznych zależności, więc wolę trzymać się standardowego modułu json.

Jak mogę to osiągnąć?

Manuel Ceron
źródło

Odpowiedzi:

80

Uwaga: to nie działa w żadnej najnowszej wersji Pythona.

Niestety, uważam, że trzeba to zrobić przez małpowanie (co, moim zdaniem, wskazuje na błąd projektowy w standardowym jsonpakiecie biblioteki ). Np. Ten 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]))

emituje:

23.67
[23.67, 23.97, 23.87]

jak chcesz. Oczywiście powinien istnieć FLOAT_REPRzaplanowany sposób nadpisywania, tak aby KAŻDA reprezentacja zmiennoprzecinkowa była pod twoją kontrolą, jeśli chcesz; ale niestety nie tak jsonzostał zaprojektowany pakiet :-(.

Alex Martelli
źródło
10
To rozwiązanie nie działa w Pythonie 2.7 przy użyciu kodera JSON w wersji C dla języka Python.
Nelson,
25
Jednak robisz to, użyj czegoś takiego jak% .15g lub% .12g zamiast% .3f.
Guido van Rossum
23
Znalazłem ten fragment w kodzie młodszego programisty. Stworzyłoby to bardzo poważny, ale subtelny błąd, gdyby nie został złapany. Czy możesz umieścić ostrzeżenie na tym kodzie wyjaśniające globalne konsekwencje tego małpiego łatania.
Rory Hart
12
To dobra higiena, aby odłożyć to, kiedy skończysz: 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
Jak zauważyli inni, nie działa to już co najmniej w Pythonie 3.6+. Dodaj kilka cyfr, 23.67aby zobaczyć, jak .2fnie jest przestrzegany.
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])))

emituje

[23.67, 23.97, 23.87]

Nie jest konieczne śledzenie małp.

Tom Wuttke
źródło
2
Podoba mi się to rozwiązanie; lepsza integracja i działa z wersją 2.7. Ponieważ i tak sam tworzę dane, wyeliminowałem tę pretty_floatsfunkcję i po prostu zintegrowałem ją z innym kodem.
mikepurvis
1
W Pythonie3 powoduje to błąd „Obiekt mapy nie jest serializowany w formacie JSON” , ale można rozwiązać konwersję mapy () na listę za pomocąlist( map(pretty_floats, obj) )
Guglie
1
@Guglie: to dlatego, że w Pythonie 3 mapzwraca iterator, a nielist
Azat Ibrakov
4
Nie działa dla mnie (Python 3.5.2, simplejson 3.16.0). Wypróbowano z% .6g i [23.671234556, 23.971234556, 23.871234556], nadal wypisuje całą liczbę.
szali
27

Jeśli używasz Pythona 2.7, prostym rozwiązaniem jest po prostu jawne zaokrąglenie pływaków do żądanej precyzji.

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

Działa to, ponieważ w Pythonie 2.7 zaokrąglanie zmiennoprzecinkowe jest bardziej spójne . Niestety to nie działa w Pythonie 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'

Wymienione powyżej rozwiązania są obejściami dla wersji 2.6, ale żadne nie są w pełni wystarczające. Łata małpy json.encoder.FLOAT_REPR nie działa, jeśli środowisko wykonawcze Pythona używa wersji C modułu JSON. Klasa PrettyFloat w odpowiedzi Toma Wuttke działa, ale tylko wtedy, gdy kodowanie% g działa globalnie dla twojej aplikacji. % .15g jest trochę magiczne, działa, ponieważ precyzja float to 17 cyfr znaczących, a% g nie wypisuje zer końcowych.

Spędziłem trochę czasu, próbując stworzyć PrettyFloat, który pozwoliłby dostosować precyzję dla każdej liczby. To znaczy składnia taka jak

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

Nie jest łatwo zrobić to dobrze. Dziedziczenie po float jest niezręczne. Dziedziczenie z Object i używanie podklasy JSONEncoder z własną metodą default () powinno działać, z wyjątkiem tego, że moduł json wydaje się zakładać, że wszystkie niestandardowe typy powinny być serializowane jako ciągi. To znaczy: w wyniku otrzymujesz ciąg Javascript „0,33”, a nie liczbę 0,33. Być może jest jeszcze sposób, aby to zadziałało, ale jest trudniejsze, niż się wydaje.

Nelson
źródło
Inne podejście do Pythona 2.6 wykorzystujące JSONEncoder.iterencode i dopasowywanie wzorców można zobaczyć na github.com/migurski/LilJSON/blob/master/liljson.py
Nelson
Miejmy nadzieję, że dzięki temu przekazywanie wokół twoich pływaków będzie lżejsze - podoba mi się, jak możemy uniknąć mieszania się z klasami JSON, które mogą być do niczego.
Lincoln B,
20

Naprawdę niefortunne, że dumpsnie pozwala ci nic zrobić, aby pływać. Jednak loadstak. Więc jeśli nie masz nic przeciwko dodatkowemu obciążeniu procesora, możesz rzucić go przez koder / dekoder / koder i uzyskać właściwy wynik:

>>> json.dumps(json.loads(json.dumps([.333333333333, .432432]), parse_float=lambda x: round(float(x), 3)))
'[0.333, 0.432]'
Claude
źródło
Dziękuję, to bardzo pomocna sugestia. Nie wiedziałem o parse_floatkwargu!
Anonimowy
Najprostsza sugestia, która działa również w 3.6.
Brent Faust
Zwróć uwagę na wyrażenie „nie przejmuj się dodatkowym obciążeniem procesora”. Zdecydowanie nie używaj tego rozwiązania, jeśli masz dużo danych do serializacji. Dla mnie dodanie tego samego sprawiło, że program wykonujący nietrywialne obliczenia trwało 3 razy dłużej.
shaneb
11

Oto rozwiązanie, które działało dla mnie w Pythonie 3 i nie wymaga małpiego łatania:

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

Wynik to:

[23.63, 23.93, 23.84]

Kopiuje dane, ale z zaokrąglonymi zmiennymi.

jcoffland
źródło
9

Jeśli utkniesz w Pythonie 2.5 lub wcześniejszych wersjach: Trik z małpą poprawką nie wydaje się działać z oryginalnym modułem simplejson, jeśli przyspieszenia C są zainstalowane:

$ 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
źródło
7

Możesz zrobić to, co musisz, ale nie jest to udokumentowane:

>>> 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
źródło
5
Wygląda schludnie, ale wydaje się nie działać w Pythonie 3.6. W szczególności nie widziałem FLOAT_REPRstałej w json.encodermodule.
Tomasz Gandor
2

Rozwiązanie Alexa Martelliego będzie działać w przypadku aplikacji jednowątkowych, ale może nie działać w przypadku aplikacji wielowątkowych, które muszą kontrolować liczbę miejsc dziesiętnych w wątku. Oto rozwiązanie, które powinno działać w aplikacjach wielowątkowych:

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

Możesz po prostu ustawić encoder.thread_local.decimal_places na żądaną liczbę miejsc dziesiętnych, a następne wywołanie json.dumps () w tym wątku użyje tej liczby miejsc dziesiętnych

Anton I. Sipos
źródło
2

Jeśli chcesz to zrobić w Pythonie 2.7 bez nadpisywania globalnego json.encoder.FLOAT_REPR, oto jeden sposób.

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)

Następnie w Pythonie 2.7:

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

W Pythonie 2.6 nie działa, jak zauważa Matthew Schinckel poniżej:

>>> import MyEncoder
>>> enc = MyEncoder()  
>>> enc.encode([23.67, 23.97, 23.87])
'["23.67", "23.97", "23.87"]'
Mike Fogel
źródło
4
Wyglądają jak łańcuchy, a nie liczby.
Matthew Schinckel
1

Plusy:

  • Działa z dowolnym koderem JSON, a nawet repr.
  • Krótki (ish), wydaje się działać.

Cons:

  • Ugly regexp hack, ledwo przetestowane.
  • Kwadratowa złożoność.

    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
źródło
1

Podczas importu standardowego modułu json wystarczy zmienić domyślny koder FLOAT_REPR. Naprawdę nie ma potrzeby importowania ani tworzenia instancji programu 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]'

Czasami jest również bardzo przydatne do wypisania jako json najlepszej reprezentacji, jaką Python może odgadnąć za pomocą str. Dzięki temu istotne cyfry nie zostaną zignorowane.

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
źródło
1

Zgadzam się z @Nelsonem, że dziedziczenie po float jest niewygodne, ale być może rozwiązanie, które dotyczy tylko __repr__funkcji, można wybaczyć. Skończyło się decimalna tym, że w tym celu użyłem pakietu do ponownego sformatowania pływaków, gdy było to konieczne. Zaletą jest to, że działa to we wszystkich kontekstach, w których repr()jest wywoływane, więc także podczas zwykłego drukowania list na przykład na standardowe wyjście. Ponadto precyzja jest konfigurowalna w czasie wykonywania, po utworzeniu danych. Wadą jest oczywiście to, że twoje dane muszą zostać przekonwertowane do tej specjalnej klasy float (ponieważ niestety nie możesz wydawać się małpą łatką float.__repr__). W tym celu podaję krótką funkcję konwersji.

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

Przykład użycia:

>>> 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]]'
user1556435
źródło
To nie działa z wbudowanym pakietem json Python3, który nie używa __repr __ ().
Ian Goldby
0

Używanie numpy

Jeśli faktycznie masz naprawdę długie elementy zmiennoprzecinkowe, możesz je poprawnie zaokrąglić w górę / w dół za pomocą 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]'

Michaił
źródło
-1

Właśnie wydałem fjson , małą bibliotekę Pythona, aby rozwiązać ten problem. Zainstaluj za pomocą

pip install fjson

i używaj tak jak json, z dodatkiem float_formatparametru:

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
źródło