wake-up-neo.com

Formatiert Floats mit dem Standard-JSON-Modul

Ich verwende das Standard json-Modul in python 2.6, um eine Liste von Floats zu serialisieren. Allerdings erhalte ich folgende Ergebnisse:

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

Ich möchte, dass die Floats nur mit zwei Dezimalstellen formatiert werden. 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 einzelnes Float-Objekt:

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

Scheitert aber bei verschachtelten Objekten:

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

Ich möchte keine externen Abhängigkeiten haben, daher halte ich mich lieber an das Standard-JSON-Modul.

Wie kann ich das erreichen?

83
Manuel Ceron

Leider glaube ich, dass Sie dies durch Affen-Patching tun müssen (was meiner Meinung nach auf einen Konstruktionsfehler in der Standardbibliothek json hindeutet). Zum Beispiel 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])

gibt aus:

23.67
[23.67, 23.97, 23.87]

wie Sie es wünschen. Natürlich sollte es eine Möglichkeit geben, FLOAT_REPR Zu überschreiben, damit JEDE Darstellung eines Floats unter Ihrer Kontrolle steht, wenn Sie dies wünschen. aber leider wurde das json -Paket nicht so entworfen :-(.

72
Alex Martelli
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 map(pretty_floats, obj)  # in Python3 do: 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.

56
Tom Wuttke

Wenn Sie Python 2.7 verwenden, besteht eine einfache Lösung darin, Ihre Floats 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 gemacht Float-Rundung konsistenter . Leider funktioniert dies nicht in 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'

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

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 von float ist umständlich. Die Vererbung von Object und die Verwendung einer JSONEncoder-Unterklasse mit einer eigenen default () -Methode sollte funktionieren, mit der Ausnahme, dass das json-Modul davon auszugehen scheint, dass alle benutzerdefinierten Typen als Zeichenfolgen serialisiert werden sollten. Dh, Sie erhalten die Javascript-Zeichenfolge "0.33" in der Ausgabe, nicht die Nummer 0.33. Möglicherweise gibt es noch einen Weg, um diese Arbeit zu machen, aber es ist schwieriger, als es aussieht.

26
Nelson

Wirklich bedauerlich, dass dumps es Ihnen nicht erlaubt, irgendetwas zu tun, um zu schweben. Jedoch tut loads. Wenn Ihnen die zusätzliche CPU-Last 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]'
13
Claude

Wenn Sie mit Python 2.5 oder früheren Versionen nicht weiterkommen: Der Affen-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]'
>>> 
9
Carlos Valiente

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

Hier ist eine Lösung, die für mich in Python 3 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 abgerundeten Floats.

4
jcoffland

Die Lösung von Alex Martelli funktioniert für Singlethread-Apps, möglicherweise jedoch nicht für Multithread-Apps, bei denen die Anzahl der Dezimalstellen pro Thread gesteuert werden muss. 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 nur auf die Anzahl der gewünschten Dezimalstellen festlegen, und der nächste Aufruf von json.dumps () in diesem Thread verwendet diese Anzahl von Dezimalstellen

2
Anton I. Sipos

Wenn Sie dies in python 2.7 tun müssen, ohne den globalen json.encoder.FLOAT_REPR zu überschreiben, gibt es eine Möglichkeit.

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"]'
2
Mike Fogel

Wenn Sie das Standard-JSON-Modul importieren, reicht es aus, den Standard-Encoder FLOAT_REPR zu ändern. Es ist nicht wirklich notwendig, Encoder-Instanzen zu importieren oder zu erstellen.

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, die beste Darstellung als json auszugeben python kann mit str erraten. 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]'
1
F Pereira

Ich stimme @ Nelson zu, dass das Erben von float umständlich ist, aber möglicherweise ist eine Lösung, die nur die Funktion __repr__ Berührt, verzeihbar. Ich habe das Paket decimal verwendet, um Floats bei Bedarf neu zu formatieren. Der Vorteil ist, dass dies in allen Kontexten funktioniert, in denen repr() aufgerufen wird, also auch, wenn zum Beispiel Listen einfach auf stdout gedruckt werden. Die Genauigkeit kann auch zur Laufzeit konfiguriert werden, nachdem die Daten erstellt wurden. Der Nachteil ist natürlich, dass Ihre Daten in diese spezielle Float-Klasse konvertiert werden müssen (da Sie anscheinend leider keinen Affen-Patch float.__repr__ Ausführen können). Dafür stelle ich eine kurze Umrechnungsfunktion 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]]'
1
user1556435

Vorteile:

  • Funktioniert mit jedem JSON-Encoder oder sogar 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
    
1
Sam Watkins