Das Format schwebt mit dem Standard-JSON-Modul

100

Ich verwende das Standard- JSON-Modul in Python 2.6, um eine Liste von Floats zu serialisieren. Ich erhalte jedoch folgende Ergebnisse:

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

Ich möchte, dass die Gleitkommazahlen nur aus zwei Dezimalstellen bestehen. Die Ausgabe sollte folgendermaßen aussehen:

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

Ich habe versucht, meine eigene JSON-Encoder-Klasse zu definieren:

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

Dies funktioniert für ein einziges Float-Objekt:

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

Aber schlägt für verschachtelte Objekte fehl:

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

Ich möchte keine externen Abhängigkeiten haben, deshalb bleibe ich lieber beim Standard-JSON-Modul.

Wie kann ich das erreichen?

Manuel Ceron
quelle

Antworten:

80

Hinweis: Dies gilt nicht funktioniert in jeder aktuellen Version von Python.

Leider glaube ich, dass Sie dies durch Affen-Patching tun müssen (was meiner Meinung nach auf einen Designfehler im Standard-Bibliothekspaket hinweist json). ZB dieser Code:

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

emittiert:

23.67
[23.67, 23.97, 23.87]

wie du willst. Natürlich sollte es eine architektonische Möglichkeit zum Überschreiben geben, FLOAT_REPRdamit JEDE Darstellung eines Floats unter Ihrer Kontrolle steht, wenn Sie dies wünschen. aber leider jsonwurde das paket nicht so entworfen :-(.

Alex Martelli
quelle
10
Diese Lösung funktioniert in Python 2.7 nicht mit Pythons C-Version des JSON-Encoders.
Nelson
25
Verwenden Sie jedoch% .15g oder% .12g anstelle von% .3f.
Guido van Rossum
23
Ich habe diesen Ausschnitt im Code eines Junior-Programmierers gefunden. Dies hätte einen sehr ernsten, aber subtilen Fehler verursacht, wenn er nicht abgefangen worden wäre. Können Sie bitte eine Warnung auf diesen Code setzen, in der die globalen Auswirkungen dieses Affen-Patches erläutert werden?
Rory Hart
12
Es ist eine gute Hygiene, es zurückzusetzen, wenn Sie fertig sind: 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
Wie andere bereits betont haben, funktioniert dies zumindest in Python 3.6+ nicht mehr. Fügen Sie ein paar Ziffern hinzu, um 23.67zu sehen, wie dies .2fnicht beachtet wird.
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])))

emittiert

[23.67, 23.97, 23.87]

Kein Monkeypatching notwendig.

Tom Wuttke
quelle
2
Ich mag diese Lösung; bessere Integration und funktioniert mit 2.7. Da ich die Daten sowieso selbst aufbaue, habe ich die pretty_floatsFunktion entfernt und sie einfach in meinen anderen Code integriert.
Mikepurvis
1
In Python3 gibt es den Fehler " Kartenobjekt ist nicht JSON-serialisierbar" , aber Sie können das Konvertieren der Karte () in eine Liste mitlist( map(pretty_floats, obj) )
Guglie
1
@ Guglie: Das liegt daran, dass in Python 3 mapder Iterator zurückgegeben wird, nicht einlist
Azat Ibrakov
4
Funktioniert bei mir nicht (Python 3.5.2, simplejson 3.16.0). Versucht es mit% .6g und [23.671234556, 23.971234556, 23.871234556], druckt es immer noch die ganze Zahl.
Szali
27

Wenn Sie Python 2.7 verwenden, besteht eine einfache Lösung darin, Ihre Floats einfach explizit auf die gewünschte Genauigkeit zu runden.

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

Dies funktioniert, weil Python 2.7 die Float-Rundung konsistenter gemacht hat . Leider funktioniert dies in Python 2.6 nicht:

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

Die oben genannten Lösungen sind Problemumgehungen für 2.6, aber keine ist völlig ausreichend. Das Affen-Patching json.encoder.FLOAT_REPR funktioniert nicht, wenn Ihre Python-Laufzeit eine C-Version des JSON-Moduls verwendet. Die PrettyFloat-Klasse in Tom Wuttkes Antwort funktioniert, aber nur, wenn die% g-Codierung für Ihre Anwendung global funktioniert. % .15g ist ein bisschen magisch, es funktioniert, weil die Float-Genauigkeit 17 signifikante Stellen beträgt und% g keine nachgestellten Nullen druckt.

Ich habe einige Zeit damit verbracht, ein PrettyFloat zu erstellen, mit dem die Genauigkeit für jede Zahl angepasst werden kann. Dh eine Syntax wie

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

Es ist nicht einfach, das richtig zu machen. Das Erben vom Schwimmer ist umständlich. Das Erben von Object und das Verwenden einer JSONEncoder-Unterklasse mit einer eigenen default () -Methode sollte funktionieren, außer das json-Modul scheint davon auszugehen, dass alle benutzerdefinierten Typen als Zeichenfolgen serialisiert werden sollten. Dh: Sie erhalten die Javascript-Zeichenfolge "0.33" in der Ausgabe, nicht die Zahl 0.33. Es mag noch einen Weg geben, diese Arbeit zu machen, aber es ist schwieriger als es aussieht.

Nelson
quelle
Ein anderer Ansatz für Python 2.6 unter Verwendung von JSONEncoder.iterencode und Mustervergleich ist unter github.com/migurski/LilJSON/blob/master/liljson.py
Nelson
Hoffentlich wird dadurch das Weitergeben Ihrer Floats leichter - ich mag es, wie wir vermeiden können, mit den JSON-Klassen herumzuspielen, die saugen können.
Lincoln B
19

Wirklich unglücklich, dumpsdass Sie nichts tun können, um zu schweben. Tut es loadsjedoch. Wenn Ihnen die zusätzliche CPU-Auslastung nichts ausmacht, können Sie sie durch den Encoder / Decoder / Encoder werfen und das richtige Ergebnis erzielen:

>>> json.dumps(json.loads(json.dumps([.333333333333, .432432]), parse_float=lambda x: round(float(x), 3)))
'[0.333, 0.432]'
Claude
quelle
Vielen Dank, das ist wirklich ein hilfreicher Vorschlag. Ich wusste nichts über den parse_floatKwarg!
Anonym
Der einfachste Vorschlag hier, der auch in 3.6 funktioniert.
Brent Faust
Beachten Sie den Satz "Stört die zusätzliche CPU-Auslastung nicht". Verwenden Sie diese Lösung auf keinen Fall, wenn Sie viele Daten serialisieren müssen. Wenn ich dies alleine hinzufüge, dauert ein Programm, das eine nicht triviale Berechnung durchführt, dreimal länger.
Shaneb
10

Hier ist eine Lösung, die in Python 3 für mich funktioniert hat und kein Affen-Patching erfordert:

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

Ausgabe ist:

[23.63, 23.93, 23.84]

Es kopiert die Daten, jedoch mit gerundeten Gleitkommazahlen.

jcoffland
quelle
9

Wenn Sie mit Python 2.5 oder früheren Versionen nicht weiterkommen: Der Monkey-Patch-Trick scheint mit dem ursprünglichen simplejson-Modul nicht zu funktionieren, wenn die C-Beschleunigungen installiert sind:

$ 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
quelle
7

Sie können tun, was Sie tun müssen, aber es ist nicht dokumentiert:

>>> 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
quelle
5
Sieht ordentlich aus, scheint aber unter Python 3.6 nicht zu funktionieren. Insbesondere habe ich FLOAT_REPRim json.encoderModul keine Konstante gesehen .
Tomasz Gandor
2

Die Lösung von Alex Martelli funktioniert für Apps mit einem Thread, möglicherweise jedoch nicht für Apps mit mehreren Threads, die die Anzahl der Dezimalstellen pro Thread steuern müssen. Hier ist eine Lösung, die in Multithread-Apps funktionieren sollte:

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

Sie können encoder.thread_local.decimal_places lediglich auf die gewünschte Anzahl von Dezimalstellen setzen, und der nächste Aufruf von json.dumps () in diesem Thread verwendet diese Anzahl von Dezimalstellen

Anton I. Sipos
quelle
2

Wenn Sie dies in Python 2.7 tun müssen, ohne den globalen json.encoder.FLOAT_REPR zu überschreiben, haben Sie folgende Möglichkeiten.

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)

Dann in Python 2.7:

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

In Python 2.6 funktioniert es nicht ganz, wie Matthew Schinckel unten ausführt:

>>> import MyEncoder
>>> enc = MyEncoder()  
>>> enc.encode([23.67, 23.97, 23.87])
'["23.67", "23.97", "23.87"]'
Mike Fogel
quelle
4
Diese sehen aus wie Zeichenfolgen, nicht wie Zahlen.
Matthew Schinckel
1

Vorteile:

  • Funktioniert mit jedem JSON-Encoder oder sogar mit Pythons Repr.
  • Kurz (ish), scheint zu funktionieren.

Nachteile:

  • Hässlicher Regexp-Hack, kaum getestet.
  • Quadratische Komplexität.

    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
quelle
1

Beim Importieren des Standard-JSON-Moduls reicht es aus, den Standard-Encoder FLOAT_REPR zu ändern. Encoder-Instanzen müssen nicht wirklich importiert oder erstellt werden.

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

Manchmal ist es auch sehr nützlich, als json die beste Darstellung auszugeben, die Python mit str erraten kann. Dadurch wird sichergestellt, dass wichtige Ziffern nicht ignoriert werden.

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
quelle
1

Ich stimme @Nelson zu, dass das Erben von float umständlich ist, aber vielleicht ist eine Lösung, die nur die __repr__Funktion berührt , verzeihbar. Am Ende habe ich das decimalPaket verwendet, um Floats bei Bedarf neu zu formatieren. Der Vorteil ist, dass dies in allen Kontexten funktioniert, in denen repr()aufgerufen wird, beispielsweise auch beim einfachen Drucken von Listen auf stdout. Außerdem kann die Genauigkeit zur Laufzeit konfiguriert werden, nachdem die Daten erstellt wurden. Nachteil ist natürlich, dass Ihre Daten in diese spezielle Float-Klasse konvertiert werden müssen (da Sie leider keinen Affen-Patch zu haben scheinen float.__repr__). Dafür stelle ich eine kurze Konvertierungsfunktion zur Verfügung.

Der Code:

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

Anwendungsbeispiel:

>>> 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
quelle
Dies funktioniert nicht mit dem integrierten Python3-json-Paket, das __repr __ () nicht verwendet.
Ian Goldby
0

Mit numpy

Wenn Sie tatsächlich sehr lange Schwimmer haben, können Sie diese mit numpy richtig auf- und abrunden:

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
quelle
-1

Ich habe gerade fjson veröffentlicht , eine kleine Python-Bibliothek, um dieses Problem zu beheben. Installieren mit

pip install fjson

und verwenden Sie genau wie jsonmit der Hinzufügung des float_formatParameters:

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
quelle